mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Add a Scanner to bitcoin-serai
Moves the processor to it. This ends up as a net-neutral LoC change to the processor, unfortunately, yet this makes bitcoin-serai safer/easier to use, and increases the processor's usage of bitcoin-serai. Also re-organizes bitcoin-serai a bit.
This commit is contained in:
@@ -1,131 +0,0 @@
|
|||||||
use core::fmt::Debug;
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use transcript::Transcript;
|
|
||||||
|
|
||||||
use secp256k1::schnorr::Signature;
|
|
||||||
use k256::{elliptic_curve::ops::Reduce, U256, Scalar, ProjectivePoint};
|
|
||||||
use frost::{
|
|
||||||
curve::{Ciphersuite, Secp256k1},
|
|
||||||
Participant, ThresholdKeys, ThresholdView, FrostError,
|
|
||||||
algorithm::{Hram as HramTrait, Algorithm, Schnorr as FrostSchnorr},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::crypto::{x, make_even};
|
|
||||||
|
|
||||||
/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm.
|
|
||||||
///
|
|
||||||
/// If passed an odd nonce, it will have the generator added until it is even.
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct Hram {}
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref TAG_HASH: [u8; 32] = Sha256::digest(b"BIP0340/challenge").into();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
impl HramTrait<Secp256k1> for Hram {
|
|
||||||
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
|
|
||||||
// Convert the nonce to be even
|
|
||||||
let (R, _) = make_even(*R);
|
|
||||||
|
|
||||||
let mut data = Sha256::new();
|
|
||||||
data.update(*TAG_HASH);
|
|
||||||
data.update(*TAG_HASH);
|
|
||||||
data.update(x(&R));
|
|
||||||
data.update(x(A));
|
|
||||||
data.update(m);
|
|
||||||
|
|
||||||
Scalar::from_uint_reduced(U256::from_be_slice(&data.finalize()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// BIP-340 Schnorr signature algorithm.
|
|
||||||
///
|
|
||||||
/// This must be used with a ThresholdKeys whose group key is even. If it is odd, this will panic.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Schnorr<T: Sync + Clone + Debug + Transcript>(FrostSchnorr<Secp256k1, T, Hram>);
|
|
||||||
impl<T: Sync + Clone + Debug + Transcript> Schnorr<T> {
|
|
||||||
/// Construct a Schnorr algorithm continuing the specified transcript.
|
|
||||||
pub fn new(transcript: T) -> Schnorr<T> {
|
|
||||||
Schnorr(FrostSchnorr::new(transcript))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Sync + Clone + Debug + Transcript> Algorithm<Secp256k1> for Schnorr<T> {
|
|
||||||
type Transcript = T;
|
|
||||||
type Addendum = ();
|
|
||||||
type Signature = Signature;
|
|
||||||
|
|
||||||
fn transcript(&mut self) -> &mut Self::Transcript {
|
|
||||||
self.0.transcript()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nonces(&self) -> Vec<Vec<ProjectivePoint>> {
|
|
||||||
self.0.nonces()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
|
||||||
&mut self,
|
|
||||||
rng: &mut R,
|
|
||||||
keys: &ThresholdKeys<Secp256k1>,
|
|
||||||
) {
|
|
||||||
self.0.preprocess_addendum(rng, keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_addendum<R: io::Read>(&self, reader: &mut R) -> io::Result<Self::Addendum> {
|
|
||||||
self.0.read_addendum(reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_addendum(
|
|
||||||
&mut self,
|
|
||||||
view: &ThresholdView<Secp256k1>,
|
|
||||||
i: Participant,
|
|
||||||
addendum: (),
|
|
||||||
) -> Result<(), FrostError> {
|
|
||||||
self.0.process_addendum(view, i, addendum)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign_share(
|
|
||||||
&mut self,
|
|
||||||
params: &ThresholdView<Secp256k1>,
|
|
||||||
nonce_sums: &[Vec<<Secp256k1 as Ciphersuite>::G>],
|
|
||||||
nonces: Vec<Zeroizing<<Secp256k1 as Ciphersuite>::F>>,
|
|
||||||
msg: &[u8],
|
|
||||||
) -> <Secp256k1 as Ciphersuite>::F {
|
|
||||||
self.0.sign_share(params, nonce_sums, nonces, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
fn verify(
|
|
||||||
&self,
|
|
||||||
group_key: ProjectivePoint,
|
|
||||||
nonces: &[Vec<ProjectivePoint>],
|
|
||||||
sum: Scalar,
|
|
||||||
) -> Option<Self::Signature> {
|
|
||||||
self.0.verify(group_key, nonces, sum).map(|mut sig| {
|
|
||||||
// Make the R of the final signature even
|
|
||||||
let offset;
|
|
||||||
(sig.R, offset) = make_even(sig.R);
|
|
||||||
// s = r + cx. Since we added to the r, add to s
|
|
||||||
sig.s += Scalar::from(offset);
|
|
||||||
// Convert to a secp256k1 signature
|
|
||||||
Signature::from_slice(&sig.serialize()[1 ..]).unwrap()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_share(
|
|
||||||
&self,
|
|
||||||
verification_share: ProjectivePoint,
|
|
||||||
nonces: &[Vec<ProjectivePoint>],
|
|
||||||
share: Scalar,
|
|
||||||
) -> Result<Vec<(Scalar, ProjectivePoint)>, ()> {
|
|
||||||
self.0.verify_share(verification_share, nonces, share)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,27 @@
|
|||||||
use k256::{
|
use core::fmt::Debug;
|
||||||
elliptic_curve::sec1::{Tag, ToEncodedPoint},
|
use std::io;
|
||||||
Scalar, ProjectivePoint,
|
|
||||||
};
|
|
||||||
|
|
||||||
use frost::{curve::Secp256k1, ThresholdKeys};
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use transcript::Transcript;
|
||||||
|
|
||||||
|
use secp256k1::schnorr::Signature;
|
||||||
|
use k256::{
|
||||||
|
elliptic_curve::{
|
||||||
|
ops::Reduce,
|
||||||
|
sec1::{Tag, ToEncodedPoint},
|
||||||
|
},
|
||||||
|
U256, Scalar, ProjectivePoint,
|
||||||
|
};
|
||||||
|
use frost::{
|
||||||
|
curve::{Ciphersuite, Secp256k1},
|
||||||
|
Participant, ThresholdKeys, ThresholdView, FrostError,
|
||||||
|
algorithm::{Hram as HramTrait, Algorithm, Schnorr as FrostSchnorr},
|
||||||
|
};
|
||||||
|
|
||||||
use bitcoin::XOnlyPublicKey;
|
use bitcoin::XOnlyPublicKey;
|
||||||
|
|
||||||
@@ -30,8 +48,113 @@ pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) {
|
|||||||
(key, c)
|
(key, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tweak keys to ensure they're usable with Bitcoin.
|
/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm.
|
||||||
pub fn tweak_keys(keys: &ThresholdKeys<Secp256k1>) -> ThresholdKeys<Secp256k1> {
|
///
|
||||||
let (_, offset) = make_even(keys.group_key());
|
/// If passed an odd nonce, it will have the generator added until it is even.
|
||||||
keys.offset(Scalar::from(offset))
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Hram {}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref TAG_HASH: [u8; 32] = Sha256::digest(b"BIP0340/challenge").into();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
impl HramTrait<Secp256k1> for Hram {
|
||||||
|
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
|
||||||
|
// Convert the nonce to be even
|
||||||
|
let (R, _) = make_even(*R);
|
||||||
|
|
||||||
|
let mut data = Sha256::new();
|
||||||
|
data.update(*TAG_HASH);
|
||||||
|
data.update(*TAG_HASH);
|
||||||
|
data.update(x(&R));
|
||||||
|
data.update(x(A));
|
||||||
|
data.update(m);
|
||||||
|
|
||||||
|
Scalar::from_uint_reduced(U256::from_be_slice(&data.finalize()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BIP-340 Schnorr signature algorithm.
|
||||||
|
///
|
||||||
|
/// This must be used with a ThresholdKeys whose group key is even. If it is odd, this will panic.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Schnorr<T: Sync + Clone + Debug + Transcript>(FrostSchnorr<Secp256k1, T, Hram>);
|
||||||
|
impl<T: Sync + Clone + Debug + Transcript> Schnorr<T> {
|
||||||
|
/// Construct a Schnorr algorithm continuing the specified transcript.
|
||||||
|
pub fn new(transcript: T) -> Schnorr<T> {
|
||||||
|
Schnorr(FrostSchnorr::new(transcript))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Sync + Clone + Debug + Transcript> Algorithm<Secp256k1> for Schnorr<T> {
|
||||||
|
type Transcript = T;
|
||||||
|
type Addendum = ();
|
||||||
|
type Signature = Signature;
|
||||||
|
|
||||||
|
fn transcript(&mut self) -> &mut Self::Transcript {
|
||||||
|
self.0.transcript()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nonces(&self) -> Vec<Vec<ProjectivePoint>> {
|
||||||
|
self.0.nonces()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
||||||
|
&mut self,
|
||||||
|
rng: &mut R,
|
||||||
|
keys: &ThresholdKeys<Secp256k1>,
|
||||||
|
) {
|
||||||
|
self.0.preprocess_addendum(rng, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_addendum<R: io::Read>(&self, reader: &mut R) -> io::Result<Self::Addendum> {
|
||||||
|
self.0.read_addendum(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_addendum(
|
||||||
|
&mut self,
|
||||||
|
view: &ThresholdView<Secp256k1>,
|
||||||
|
i: Participant,
|
||||||
|
addendum: (),
|
||||||
|
) -> Result<(), FrostError> {
|
||||||
|
self.0.process_addendum(view, i, addendum)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_share(
|
||||||
|
&mut self,
|
||||||
|
params: &ThresholdView<Secp256k1>,
|
||||||
|
nonce_sums: &[Vec<<Secp256k1 as Ciphersuite>::G>],
|
||||||
|
nonces: Vec<Zeroizing<<Secp256k1 as Ciphersuite>::F>>,
|
||||||
|
msg: &[u8],
|
||||||
|
) -> <Secp256k1 as Ciphersuite>::F {
|
||||||
|
self.0.sign_share(params, nonce_sums, nonces, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn verify(
|
||||||
|
&self,
|
||||||
|
group_key: ProjectivePoint,
|
||||||
|
nonces: &[Vec<ProjectivePoint>],
|
||||||
|
sum: Scalar,
|
||||||
|
) -> Option<Self::Signature> {
|
||||||
|
self.0.verify(group_key, nonces, sum).map(|mut sig| {
|
||||||
|
// Make the R of the final signature even
|
||||||
|
let offset;
|
||||||
|
(sig.R, offset) = make_even(sig.R);
|
||||||
|
// s = r + cx. Since we added to the r, add to s
|
||||||
|
sig.s += Scalar::from(offset);
|
||||||
|
// Convert to a secp256k1 signature
|
||||||
|
Signature::from_slice(&sig.serialize()[1 ..]).unwrap()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_share(
|
||||||
|
&self,
|
||||||
|
verification_share: ProjectivePoint,
|
||||||
|
nonces: &[Vec<ProjectivePoint>],
|
||||||
|
share: Scalar,
|
||||||
|
) -> Result<Vec<(Scalar, ProjectivePoint)>, ()> {
|
||||||
|
self.0.verify_share(verification_share, nonces, share)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ pub use bitcoin;
|
|||||||
|
|
||||||
/// Cryptographic helpers.
|
/// Cryptographic helpers.
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
/// BIP-340 Schnorr signature algorithm.
|
|
||||||
pub mod algorithm;
|
|
||||||
/// Wallet functionality to create transactions.
|
/// Wallet functionality to create transactions.
|
||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
/// A minimal asynchronous Bitcoin RPC client.
|
/// A minimal asynchronous Bitcoin RPC client.
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ use frost::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
crypto::{x_only, make_even},
|
crypto::{x_only, make_even, Schnorr},
|
||||||
algorithm::Schnorr,
|
|
||||||
rpc::Rpc,
|
rpc::Rpc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
158
coins/bitcoin/src/wallet/mod.rs
Normal file
158
coins/bitcoin/src/wallet/mod.rs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
use std::{
|
||||||
|
io::{self, Read, Write},
|
||||||
|
collections::HashMap,
|
||||||
|
};
|
||||||
|
|
||||||
|
use k256::{
|
||||||
|
elliptic_curve::sec1::{Tag, ToEncodedPoint},
|
||||||
|
Scalar, ProjectivePoint,
|
||||||
|
};
|
||||||
|
use frost::{
|
||||||
|
curve::{Ciphersuite, Secp256k1},
|
||||||
|
ThresholdKeys,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bitcoin::{
|
||||||
|
consensus::encode::{Decodable, serialize},
|
||||||
|
schnorr::TweakedPublicKey,
|
||||||
|
OutPoint, Script, TxOut, Transaction, Block, Network, Address,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::crypto::{x_only, make_even};
|
||||||
|
|
||||||
|
mod send;
|
||||||
|
pub use send::*;
|
||||||
|
|
||||||
|
/// Tweak keys to ensure they're usable with Bitcoin.
|
||||||
|
pub fn tweak_keys(keys: &ThresholdKeys<Secp256k1>) -> ThresholdKeys<Secp256k1> {
|
||||||
|
let (_, offset) = make_even(keys.group_key());
|
||||||
|
keys.offset(Scalar::from(offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the Taproot address for a public key.
|
||||||
|
pub fn address(network: Network, key: ProjectivePoint) -> Option<Address> {
|
||||||
|
if key.to_encoded_point(true).tag() != Tag::CompressedEvenY {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Address::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(x_only(&key)), network))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A spendable output.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct ReceivedOutput {
|
||||||
|
// The scalar offset to obtain the key usable to spend this output.
|
||||||
|
//
|
||||||
|
// This field exists in order to support HDKD schemes.
|
||||||
|
offset: Scalar,
|
||||||
|
// The output to spend.
|
||||||
|
output: TxOut,
|
||||||
|
// The TX ID and vout of the output to spend.
|
||||||
|
outpoint: OutPoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReceivedOutput {
|
||||||
|
/// The offset for this output.
|
||||||
|
pub fn offset(&self) -> Scalar {
|
||||||
|
self.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The outpoint for this output.
|
||||||
|
pub fn outpoint(&self) -> &OutPoint {
|
||||||
|
&self.outpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value of this output.
|
||||||
|
pub fn value(&self) -> u64 {
|
||||||
|
self.output.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a ReceivedOutput from a generic satisfying Read.
|
||||||
|
pub fn read<R: Read>(r: &mut R) -> io::Result<ReceivedOutput> {
|
||||||
|
Ok(ReceivedOutput {
|
||||||
|
offset: Secp256k1::read_F(r)?,
|
||||||
|
output: TxOut::consensus_decode(r)
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid TxOut"))?,
|
||||||
|
outpoint: OutPoint::consensus_decode(r)
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid OutPoint"))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a ReceivedOutput to a generic satisfying Write.
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
w.write_all(&self.offset.to_bytes())?;
|
||||||
|
w.write_all(&serialize(&self.output))?;
|
||||||
|
w.write_all(&serialize(&self.outpoint))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize a ReceivedOutput to a Vec<u8>.
|
||||||
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res = vec![];
|
||||||
|
self.write(&mut res).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A transaction scanner capable of being used with HDKD schemes.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Scanner {
|
||||||
|
key: ProjectivePoint,
|
||||||
|
scripts: HashMap<Script, Scalar>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scanner {
|
||||||
|
/// Construct a Scanner for a key.
|
||||||
|
///
|
||||||
|
/// Returns None if this key can't be scanned for.
|
||||||
|
pub fn new(key: ProjectivePoint) -> Option<Scanner> {
|
||||||
|
let mut scripts = HashMap::new();
|
||||||
|
// Uses Network::Bitcoin since network is irrelevant here
|
||||||
|
scripts.insert(address(Network::Bitcoin, key)?.script_pubkey(), Scalar::ZERO);
|
||||||
|
Some(Scanner { key, scripts })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register an offset to scan for.
|
||||||
|
///
|
||||||
|
/// Due to Bitcoin's requirement that points are even, not every offset may be used.
|
||||||
|
/// If an offset isn't usable, it will be incremented until it is. If this offset is already
|
||||||
|
/// present, None is returned. Else, Some(offset) will be, with the used offset.
|
||||||
|
pub fn register_offset(&mut self, mut offset: Scalar) -> Option<Scalar> {
|
||||||
|
loop {
|
||||||
|
match address(Network::Bitcoin, self.key + (ProjectivePoint::GENERATOR * offset)) {
|
||||||
|
Some(address) => {
|
||||||
|
let script = address.script_pubkey();
|
||||||
|
if self.scripts.contains_key(&script) {
|
||||||
|
None?;
|
||||||
|
}
|
||||||
|
self.scripts.insert(script, offset);
|
||||||
|
return Some(offset);
|
||||||
|
}
|
||||||
|
None => offset += Scalar::ONE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan a transaction.
|
||||||
|
pub fn scan_transaction(&self, tx: &Transaction) -> Vec<ReceivedOutput> {
|
||||||
|
let mut res = vec![];
|
||||||
|
for (vout, output) in tx.output.iter().enumerate() {
|
||||||
|
if let Some(offset) = self.scripts.get(&output.script_pubkey) {
|
||||||
|
res.push(ReceivedOutput {
|
||||||
|
offset: *offset,
|
||||||
|
output: output.clone(),
|
||||||
|
outpoint: OutPoint::new(tx.txid(), u32::try_from(vout).unwrap()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan a block.
|
||||||
|
pub fn scan_block(&self, block: &Block) -> Vec<ReceivedOutput> {
|
||||||
|
let mut res = vec![];
|
||||||
|
for tx in &block.txdata {
|
||||||
|
res.extend(self.scan_transaction(tx));
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
io::{self, Read, Write},
|
io::{self, Read},
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -9,22 +9,16 @@ use rand_core::{RngCore, CryptoRng};
|
|||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
|
|
||||||
use k256::{elliptic_curve::sec1::{Tag, ToEncodedPoint}, Scalar, ProjectivePoint};
|
use k256::{elliptic_curve::sec1::ToEncodedPoint, Scalar};
|
||||||
use frost::{
|
use frost::{curve::Secp256k1, Participant, ThresholdKeys, FrostError, sign::*};
|
||||||
curve::{Ciphersuite, Secp256k1},
|
|
||||||
Participant, ThresholdKeys, FrostError,
|
|
||||||
sign::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
hashes::Hash,
|
hashes::Hash,
|
||||||
consensus::encode::{Decodable, serialize},
|
|
||||||
schnorr::TweakedPublicKey,
|
|
||||||
util::sighash::{SchnorrSighashType, SighashCache, Prevouts},
|
util::sighash::{SchnorrSighashType, SighashCache, Prevouts},
|
||||||
OutPoint, Script, Sequence, Witness, TxIn, TxOut, PackedLockTime, Transaction, Network, Address,
|
OutPoint, Script, Sequence, Witness, TxIn, TxOut, PackedLockTime, Transaction, Address,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{crypto::x_only, algorithm::Schnorr};
|
use crate::{crypto::Schnorr, wallet::ReceivedOutput};
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/policy.h#L27
|
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/policy.h#L27
|
||||||
@@ -34,84 +28,6 @@ const MAX_STANDARD_TX_WEIGHT: u64 = 400_000;
|
|||||||
//https://github.com/bitcoin/bitcoin/blob/a245429d680eb95cf4c0c78e58e63e3f0f5d979a/src/test/transaction_tests.cpp#L815-L816
|
//https://github.com/bitcoin/bitcoin/blob/a245429d680eb95cf4c0c78e58e63e3f0f5d979a/src/test/transaction_tests.cpp#L815-L816
|
||||||
const DUST: u64 = 674;
|
const DUST: u64 = 674;
|
||||||
|
|
||||||
/// Return the Taproot address for a public key.
|
|
||||||
pub fn address(network: Network, key: ProjectivePoint) -> Option<Address> {
|
|
||||||
if key.to_encoded_point(true).tag() != Tag::CompressedEvenY {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Address::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(x_only(&key)), network))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A spendable output.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct SpendableOutput {
|
|
||||||
// The scalar offset to obtain the key usable to spend this output.
|
|
||||||
//
|
|
||||||
// This field exists in order to support HDKD schemes.
|
|
||||||
offset: Scalar,
|
|
||||||
// The output to spend.
|
|
||||||
output: TxOut,
|
|
||||||
// The TX ID and vout of the output to spend.
|
|
||||||
outpoint: OutPoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SpendableOutput {
|
|
||||||
/// Construct a SpendableOutput from an output.
|
|
||||||
pub fn new(key: ProjectivePoint, offset: Option<Scalar>, tx: &Transaction, o: usize) -> Option<SpendableOutput> {
|
|
||||||
let offset = offset.unwrap_or(Scalar::ZERO);
|
|
||||||
// Uses Network::Bitcoin since network is irrelevant here
|
|
||||||
let address = address(Network::Bitcoin, key + (ProjectivePoint::GENERATOR * offset))?;
|
|
||||||
|
|
||||||
let output = tx.output.get(o)?;
|
|
||||||
|
|
||||||
if output.script_pubkey == address.script_pubkey() {
|
|
||||||
return Some(SpendableOutput {
|
|
||||||
offset,
|
|
||||||
output: output.clone(),
|
|
||||||
outpoint: OutPoint { txid: tx.txid(), vout: u32::try_from(o).unwrap() },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The outpoint for this output.
|
|
||||||
pub fn outpoint(&self) -> &OutPoint {
|
|
||||||
&self.outpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The value of this output.
|
|
||||||
pub fn value(&self) -> u64 {
|
|
||||||
self.output.value
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a SpendableOutput from a generic satisfying Read.
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<SpendableOutput> {
|
|
||||||
Ok(SpendableOutput {
|
|
||||||
offset: Secp256k1::read_F(r)?,
|
|
||||||
output: TxOut::consensus_decode(r)
|
|
||||||
.map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid TxOut"))?,
|
|
||||||
outpoint: OutPoint::consensus_decode(r)
|
|
||||||
.map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid OutPoint"))?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a SpendableOutput to a generic satisfying Write.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
w.write_all(&self.offset.to_bytes())?;
|
|
||||||
w.write_all(&serialize(&self.output))?;
|
|
||||||
w.write_all(&serialize(&self.outpoint))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize a SpendableOutput to a Vec<u8>.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = vec![];
|
|
||||||
self.write(&mut res).unwrap();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
||||||
pub enum TransactionError {
|
pub enum TransactionError {
|
||||||
#[error("no inputs were specified")]
|
#[error("no inputs were specified")]
|
||||||
@@ -188,7 +104,7 @@ impl SignableTransaction {
|
|||||||
///
|
///
|
||||||
/// If data is specified, an OP_RETURN output will be added with it.
|
/// If data is specified, an OP_RETURN output will be added with it.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
mut inputs: Vec<SpendableOutput>,
|
mut inputs: Vec<ReceivedOutput>,
|
||||||
payments: &[(Address, u64)],
|
payments: &[(Address, u64)],
|
||||||
change: Option<Address>,
|
change: Option<Address>,
|
||||||
data: Option<Vec<u8>>,
|
data: Option<Vec<u8>>,
|
||||||
@@ -3,8 +3,12 @@ use std::{io, collections::HashMap};
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use transcript::RecommendedTranscript;
|
use transcript::RecommendedTranscript;
|
||||||
|
use group::ff::PrimeField;
|
||||||
use k256::{ProjectivePoint, Scalar};
|
use k256::{ProjectivePoint, Scalar};
|
||||||
use frost::{curve::Secp256k1, ThresholdKeys};
|
use frost::{
|
||||||
|
curve::{Curve, Secp256k1},
|
||||||
|
ThresholdKeys,
|
||||||
|
};
|
||||||
|
|
||||||
use bitcoin_serai::{
|
use bitcoin_serai::{
|
||||||
bitcoin::{
|
bitcoin::{
|
||||||
@@ -15,9 +19,9 @@ use bitcoin_serai::{
|
|||||||
blockdata::script::Instruction,
|
blockdata::script::Instruction,
|
||||||
Transaction, Block, Network,
|
Transaction, Block, Network,
|
||||||
},
|
},
|
||||||
crypto::{make_even, tweak_keys},
|
|
||||||
wallet::{
|
wallet::{
|
||||||
address, SpendableOutput, TransactionError, SignableTransaction as BSignableTransaction, TransactionMachine,
|
tweak_keys, address, ReceivedOutput, Scanner, TransactionError,
|
||||||
|
SignableTransaction as BSignableTransaction, TransactionMachine,
|
||||||
},
|
},
|
||||||
rpc::{RpcError, Rpc},
|
rpc::{RpcError, Rpc},
|
||||||
};
|
};
|
||||||
@@ -61,7 +65,7 @@ impl AsMut<[u8]> for OutputId {
|
|||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct Output {
|
pub struct Output {
|
||||||
kind: OutputType,
|
kind: OutputType,
|
||||||
output: SpendableOutput,
|
output: ReceivedOutput,
|
||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +100,7 @@ impl OutputTrait for Output {
|
|||||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||||
Ok(Output {
|
Ok(Output {
|
||||||
kind: OutputType::read(reader)?,
|
kind: OutputType::read(reader)?,
|
||||||
output: SpendableOutput::read(reader)?,
|
output: ReceivedOutput::read(reader)?,
|
||||||
data: {
|
data: {
|
||||||
let mut data_len = [0; 2];
|
let mut data_len = [0; 2];
|
||||||
reader.read_exact(&mut data_len)?;
|
reader.read_exact(&mut data_len)?;
|
||||||
@@ -179,25 +183,35 @@ impl BlockTrait<Bitcoin> for Block {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_key(mut key: ProjectivePoint, i: usize) -> (ProjectivePoint, Scalar) {
|
const KEY_DST: &[u8] = b"Bitcoin Key";
|
||||||
let mut offset = Scalar::ZERO;
|
lazy_static::lazy_static! {
|
||||||
for _ in 0 .. i {
|
static ref BRANCH_OFFSET: Scalar = Secp256k1::hash_to_F(KEY_DST, b"branch");
|
||||||
key += ProjectivePoint::GENERATOR;
|
static ref CHANGE_OFFSET: Scalar = Secp256k1::hash_to_F(KEY_DST, b"change");
|
||||||
offset += Scalar::ONE;
|
|
||||||
|
|
||||||
let even_offset;
|
|
||||||
(key, even_offset) = make_even(key);
|
|
||||||
offset += Scalar::from(even_offset);
|
|
||||||
}
|
|
||||||
(key, offset)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn branch(key: ProjectivePoint) -> (ProjectivePoint, Scalar) {
|
fn scanner(
|
||||||
next_key(key, 1)
|
key: ProjectivePoint,
|
||||||
}
|
) -> (Scanner, HashMap<OutputType, Scalar>, HashMap<Vec<u8>, OutputType>) {
|
||||||
|
let mut scanner = Scanner::new(key).unwrap();
|
||||||
|
let mut offsets = HashMap::from([(OutputType::External, Scalar::ZERO)]);
|
||||||
|
|
||||||
fn change(key: ProjectivePoint) -> (ProjectivePoint, Scalar) {
|
let zero = Scalar::ZERO.to_repr();
|
||||||
next_key(key, 2)
|
let zero_ref: &[u8] = zero.as_ref();
|
||||||
|
let mut kinds = HashMap::from([(zero_ref.to_vec(), OutputType::External)]);
|
||||||
|
|
||||||
|
let mut register = |kind, offset| {
|
||||||
|
let offset = scanner.register_offset(offset).expect("offset collision");
|
||||||
|
offsets.insert(kind, offset);
|
||||||
|
|
||||||
|
let offset = offset.to_repr();
|
||||||
|
let offset_ref: &[u8] = offset.as_ref();
|
||||||
|
kinds.insert(offset_ref.to_vec(), kind);
|
||||||
|
};
|
||||||
|
|
||||||
|
register(OutputType::Branch, *BRANCH_OFFSET);
|
||||||
|
register(OutputType::Change, *CHANGE_OFFSET);
|
||||||
|
|
||||||
|
(scanner, offsets, kinds)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -281,7 +295,8 @@ impl Coin for Bitcoin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn branch_address(key: ProjectivePoint) -> Self::Address {
|
fn branch_address(key: ProjectivePoint) -> Self::Address {
|
||||||
Self::address(branch(key).0)
|
let (_, offsets, _) = scanner(key);
|
||||||
|
Self::address(key + (ProjectivePoint::GENERATOR * offsets[&OutputType::Branch]))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_latest_block_number(&self) -> Result<usize, CoinError> {
|
async fn get_latest_block_number(&self) -> Result<usize, CoinError> {
|
||||||
@@ -299,39 +314,33 @@ impl Coin for Bitcoin {
|
|||||||
block: &Self::Block,
|
block: &Self::Block,
|
||||||
key: ProjectivePoint,
|
key: ProjectivePoint,
|
||||||
) -> Result<Vec<Self::Output>, CoinError> {
|
) -> Result<Vec<Self::Output>, CoinError> {
|
||||||
let external = (key, Scalar::ZERO);
|
let (scanner, _, kinds) = scanner(key);
|
||||||
let branch = branch(key);
|
|
||||||
let change = change(key);
|
|
||||||
|
|
||||||
let entry =
|
let mut outputs = vec![];
|
||||||
|pair: (_, _), kind| (Self::address(pair.0).0.script_pubkey().to_bytes(), (pair.1, kind));
|
|
||||||
let scripts = HashMap::from([
|
|
||||||
entry(external, OutputType::External),
|
|
||||||
entry(branch, OutputType::Branch),
|
|
||||||
entry(change, OutputType::Change),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let mut outputs = Vec::new();
|
|
||||||
// Skip the coinbase transaction which is burdened by maturity
|
// Skip the coinbase transaction which is burdened by maturity
|
||||||
for tx in &block.txdata[1 ..] {
|
for tx in &block.txdata[1 ..] {
|
||||||
for (vout, output) in tx.output.iter().enumerate() {
|
for output in scanner.scan_transaction(tx) {
|
||||||
if let Some(info) = scripts.get(&output.script_pubkey.to_bytes()) {
|
let offset_repr = output.offset().to_repr();
|
||||||
outputs.push(Output {
|
let offset_repr_ref: &[u8] = offset_repr.as_ref();
|
||||||
kind: info.1,
|
let kind = kinds[offset_repr_ref];
|
||||||
output: SpendableOutput::new(key, Some(info.0), tx, vout).unwrap(),
|
|
||||||
data: (|| {
|
let data = if kind == OutputType::External {
|
||||||
for output in &tx.output {
|
(|| {
|
||||||
if output.script_pubkey.is_op_return() {
|
for output in &tx.output {
|
||||||
match output.script_pubkey.instructions_minimal().last() {
|
if output.script_pubkey.is_op_return() {
|
||||||
Some(Ok(Instruction::PushBytes(data))) => return data.to_vec(),
|
match output.script_pubkey.instructions_minimal().last() {
|
||||||
_ => continue,
|
Some(Ok(Instruction::PushBytes(data))) => return data.to_vec(),
|
||||||
}
|
_ => continue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vec![]
|
}
|
||||||
})(),
|
vec![]
|
||||||
});
|
})()
|
||||||
}
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs.push(Output { kind, output, data })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +369,12 @@ impl Coin for Bitcoin {
|
|||||||
match BSignableTransaction::new(
|
match BSignableTransaction::new(
|
||||||
plan.inputs.iter().map(|input| input.output.clone()).collect(),
|
plan.inputs.iter().map(|input| input.output.clone()).collect(),
|
||||||
&payments,
|
&payments,
|
||||||
plan.change.map(|key| Self::address(change(key).0).0),
|
plan
|
||||||
|
.change
|
||||||
|
.map(|key| {
|
||||||
|
let (_, offsets, _) = scanner(key);
|
||||||
|
Self::address(key + (ProjectivePoint::GENERATOR * offsets[&OutputType::Change])).0
|
||||||
|
}),
|
||||||
None,
|
None,
|
||||||
fee.0,
|
fee.0,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub trait Id:
|
|||||||
}
|
}
|
||||||
impl<I: Send + Sync + Clone + Default + PartialEq + AsRef<[u8]> + AsMut<[u8]> + Debug> Id for I {}
|
impl<I: Send + Sync + Clone + Default + PartialEq + AsRef<[u8]> + AsMut<[u8]> + Debug> Id for I {}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
pub enum OutputType {
|
pub enum OutputType {
|
||||||
// Needs to be processed/sent up to Substrate
|
// Needs to be processed/sent up to Substrate
|
||||||
External,
|
External,
|
||||||
|
|||||||
Reference in New Issue
Block a user