use std::{ io::{self, Read}, collections::HashMap, }; use rand_core::RngCore; use transcript::{Transcript, RecommendedTranscript}; use k256::{elliptic_curve::sec1::ToEncodedPoint, Scalar}; use frost::{ curve::{Ciphersuite, Secp256k1}, ThresholdKeys, FrostError, algorithm::Schnorr, sign::*, }; use bitcoin::{ hashes::Hash, consensus::encode::{Encodable, Decodable, serialize}, util::sighash::{SchnorrSighashType, SighashCache, Prevouts}, OutPoint, Script, Sequence, Witness, TxIn, TxOut, PackedLockTime, Transaction, Address, }; use crate::crypto::{BitcoinHram, make_even}; #[derive(Clone, Debug)] pub struct SpendableOutput { pub offset: Scalar, pub output: TxOut, pub outpoint: OutPoint, } impl SpendableOutput { pub fn id(&self) -> [u8; 36] { serialize(&self.outpoint).try_into().unwrap() } pub fn read(r: &mut R) -> io::Result { 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"))?, }) } pub fn serialize(&self) -> Vec { let mut res = self.offset.to_bytes().to_vec(); self.output.consensus_encode(&mut res).unwrap(); self.outpoint.consensus_encode(&mut res).unwrap(); res } } #[derive(Clone, Debug)] pub struct SignableTransaction(Transaction, Vec, Vec); impl SignableTransaction { fn calculate_weight(inputs: usize, payments: &[(Address, u64)], change: Option<&Address>) -> u64 { let mut tx = Transaction { version: 2, lock_time: PackedLockTime::ZERO, input: vec![ TxIn { previous_output: OutPoint::default(), script_sig: Script::new(), sequence: Sequence::MAX, witness: Witness::from_vec(vec![vec![0; 64]]) }; inputs ], output: payments .iter() .map(|payment| TxOut { value: payment.1, script_pubkey: payment.0.script_pubkey() }) .collect(), }; if let Some(change) = change { tx.output.push(TxOut { value: 0, script_pubkey: change.script_pubkey() }); } u64::try_from(tx.weight()).unwrap() } pub fn new( mut inputs: Vec, payments: &[(Address, u64)], change: Option
, fee: u64, ) -> Option { let input_sat = inputs.iter().map(|input| input.output.value).sum::(); let offsets = inputs.iter().map(|input| input.offset).collect(); let tx_ins = inputs .iter() .map(|input| TxIn { previous_output: input.outpoint, script_sig: Script::new(), sequence: Sequence::MAX, witness: Witness::new(), }) .collect::>(); let payment_sat = payments.iter().map(|payment| payment.1).sum::(); let mut tx_outs = payments .iter() .map(|payment| TxOut { value: payment.1, script_pubkey: payment.0.script_pubkey() }) .collect::>(); let actual_fee = fee * Self::calculate_weight(tx_ins.len(), payments, None); if payment_sat > (input_sat - actual_fee) { return None; } // If there's a change address, check if there's a meaningful change if let Some(change) = change.as_ref() { let fee_with_change = fee * Self::calculate_weight(tx_ins.len(), payments, Some(change)); // If there's a non-zero change, add it if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) { tx_outs.push(TxOut { value, script_pubkey: change.script_pubkey() }); } } // TODO: Drop outputs which BTC will consider spam (outputs worth less than the cost to spend // them) Some(SignableTransaction( Transaction { version: 2, lock_time: PackedLockTime::ZERO, input: tx_ins, output: tx_outs }, offsets, inputs.drain(..).map(|input| input.output).collect(), )) } pub async fn multisig( self, keys: ThresholdKeys, mut transcript: RecommendedTranscript, ) -> Result { transcript.domain_separate(b"bitcoin_transaction"); transcript.append_message(b"root_key", keys.group_key().to_encoded_point(true).as_bytes()); // Transcript the inputs and outputs let tx = &self.0; for input in &tx.input { transcript.append_message(b"input_hash", input.previous_output.txid.as_hash().into_inner()); transcript.append_message(b"input_output_index", input.previous_output.vout.to_le_bytes()); } for payment in &tx.output { transcript.append_message(b"output_script", payment.script_pubkey.as_bytes()); transcript.append_message(b"output_amount", payment.value.to_le_bytes()); } let mut sigs = vec![]; for i in 0 .. tx.input.len() { // TODO: Use the above transcript here sigs.push( AlgorithmMachine::new( Schnorr::::new(), keys.clone().offset(self.1[i]), ) .unwrap(), ); } Ok(TransactionMachine { tx: self, transcript, sigs }) } } pub struct TransactionMachine { tx: SignableTransaction, transcript: RecommendedTranscript, sigs: Vec>>, } impl PreprocessMachine for TransactionMachine { type Preprocess = Vec>; type Signature = Transaction; type SignMachine = TransactionSignMachine; fn preprocess( mut self, rng: &mut R, ) -> (Self::SignMachine, Self::Preprocess) { let mut preprocesses = Vec::with_capacity(self.sigs.len()); let sigs = self .sigs .drain(..) .map(|sig| { let (sig, preprocess) = sig.preprocess(rng); preprocesses.push(preprocess); sig }) .collect(); (TransactionSignMachine { tx: self.tx, transcript: self.transcript, sigs }, preprocesses) } } pub struct TransactionSignMachine { tx: SignableTransaction, transcript: RecommendedTranscript, sigs: Vec>>, } impl SignMachine for TransactionSignMachine { type Params = (); type Keys = ThresholdKeys; type Preprocess = Vec>; type SignatureShare = Vec>; type SignatureMachine = TransactionSignatureMachine; fn cache(self) -> CachedPreprocess { unimplemented!( "Bitcoin transactions don't support caching their preprocesses due to {}", "being already bound to a specific transaction" ); } fn from_cache( _: (), _: ThresholdKeys, _: CachedPreprocess, ) -> Result { unimplemented!( "Bitcoin transactions don't support caching their preprocesses due to {}", "being already bound to a specific transaction" ); } fn read_preprocess(&self, reader: &mut R) -> io::Result { self.sigs.iter().map(|sig| sig.read_preprocess(reader)).collect() } fn sign( mut self, commitments: HashMap, msg: &[u8], ) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> { if !msg.is_empty() { Err(FrostError::InternalError( "message was passed to the TransactionMachine when it generates its own", ))?; } let commitments = (0 .. self.sigs.len()) .map(|c| { commitments .iter() .map(|(l, commitments)| (*l, commitments[c].clone())) .collect::>() }) .collect::>(); let mut cache = SighashCache::new(&self.tx.0); let prevouts = Prevouts::All(&self.tx.2); let mut shares = Vec::with_capacity(self.sigs.len()); let sigs = self .sigs .drain(..) .enumerate() .map(|(i, sig)| { let tx_sighash = cache .taproot_key_spend_signature_hash(i, &prevouts, SchnorrSighashType::Default) .unwrap(); let (sig, share) = sig.sign(commitments[i].clone(), &tx_sighash)?; shares.push(share); Ok(sig) }) .collect::>()?; Ok((TransactionSignatureMachine { tx: self.tx.0, sigs }, shares)) } } pub struct TransactionSignatureMachine { tx: Transaction, sigs: Vec>>, } impl SignatureMachine for TransactionSignatureMachine { type SignatureShare = Vec>; fn read_share(&self, reader: &mut R) -> io::Result { self.sigs.iter().map(|sig| sig.read_share(reader)).collect() } fn complete( mut self, mut shares: HashMap, ) -> Result { for (input, schnorr) in self.tx.input.iter_mut().zip(self.sigs.drain(..)) { let mut sig = schnorr.complete( shares.iter_mut().map(|(l, shares)| (*l, shares.remove(0))).collect::>(), )?; // TODO: Implement BitcoinSchnorr Algorithm to handle this let offset; (sig.R, offset) = make_even(sig.R); sig.s += Scalar::from(offset); let mut witness: Witness = Witness::new(); witness.push(&sig.serialize()[1 .. 65]); input.witness = witness; } Ok(self.tx) } }