From bac64ea55b720958508356b6d9a3cce058026c86 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 21 Jul 2022 11:43:03 +1000 Subject: [PATCH] Use new PSBT signing API in example We have a PSBT example that includes a custom signing module, we can remove that now and use the new PSBT signing API. --- bitcoin/examples/ecdsa-psbt.rs | 353 ++------------------------------- 1 file changed, 19 insertions(+), 334 deletions(-) diff --git a/bitcoin/examples/ecdsa-psbt.rs b/bitcoin/examples/ecdsa-psbt.rs index d93ac6ed82..1effe0017c 100644 --- a/bitcoin/examples/ecdsa-psbt.rs +++ b/bitcoin/examples/ecdsa-psbt.rs @@ -28,27 +28,24 @@ //! `bt listunspent` //! +use std::boxed::Box; use std::collections::BTreeMap; use std::fmt; use std::str::FromStr; use bitcoin::consensus::encode; -use bitcoin::hashes::hex::{self, FromHex}; +use bitcoin::hashes::hex::FromHex; use bitcoin::locktime::absolute; use bitcoin::secp256k1::{Secp256k1, Signing, Verification}; -use bitcoin::util::amount::ParseAmountError; use bitcoin::util::bip32::{ - self, ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint, - IntoDerivationPath, + ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint, IntoDerivationPath, }; use bitcoin::util::psbt::{self, Input, Psbt, PsbtSighashType}; use bitcoin::{ - address, Address, Amount, Network, OutPoint, PrivateKey, PublicKey, Script, Sequence, - Transaction, TxIn, TxOut, Txid, Witness, + Address, Amount, Network, OutPoint, PublicKey, Script, Sequence, Transaction, TxIn, TxOut, + Txid, Witness, }; -use self::psbt_sign::*; - type Result = std::result::Result; // Get this from the output of `bt dumpwallet `. @@ -141,33 +138,14 @@ impl ColdStorage { /// Signs `psbt` with this signer. fn sign_psbt(&self, secp: &Secp256k1, mut psbt: Psbt) -> Result { - let sk = self.private_key_to_sign(secp, &psbt.inputs[0])?; - psbt_sign::sign(&mut psbt, &sk, 0, secp)?; - - Ok(psbt) - } - - /// Returns the private key required to sign `input` if we have it. - fn private_key_to_sign( - &self, - secp: &Secp256k1, - input: &Input, - ) -> Result { - match input.bip32_derivation.iter().next() { - Some((pk, (fingerprint, path))) => { - if *fingerprint != self.master_fingerprint() { - return Err(Error::WrongFingerprint); - } - - let sk = self.master_xpriv.derive_priv(secp, &path)?.to_priv(); - if *pk != sk.public_key(secp).inner { - return Err(Error::WrongPubkey); - } - - Ok(sk) + match psbt.sign(&self.master_xpriv, secp) { + Ok(keys) => assert_eq!(keys.len(), 1), + Err((_, e)) => { + let e = e.get(&0).expect("at least one error"); + return Err(e.clone().into()); } - None => Err(Error::MissingBip32Derivation), - } + }; + Ok(psbt) } } @@ -247,7 +225,7 @@ impl WatchOnly { map.insert(pk.inner, (fingerprint, path)); input.bip32_derivation = map; - let ty = PsbtSighashType::from_str("SIGHASH_ALL").map_err(|_| Error::SighashTypeParse)?; + let ty = PsbtSighashType::from_str("SIGHASH_ALL")?; input.sighash_type = Some(ty); psbt.inputs = vec![input]; @@ -260,11 +238,10 @@ impl WatchOnly { use bitcoin::util::psbt::serialize::Serialize; if psbt.inputs.is_empty() { - return Err(Error::InputsEmpty); + return Err(psbt::SignError::MissingInputUtxo.into()); } let sigs: Vec<_> = psbt.inputs[0].partial_sigs.values().collect(); - let mut script_witness: Witness = Witness::new(); script_witness.push(&sigs[0].serialize()); script_witness.push(self.input_xpub.to_pub().serialize()); @@ -313,304 +290,12 @@ fn previous_output() -> TxOut { TxOut { value: amount.to_sat(), script_pubkey } } -#[derive(Clone, Debug, PartialEq, Eq)] -enum Error { - /// Bip32 error. - Bip32(bip32::Error), - /// PSBT error. - Psbt(psbt::Error), - /// PSBT sighash error. - PsbtSighash(SighashError), - /// Bitcoin_hashes hex error. - Hex(hex::Error), - /// Address error. - Address(address::Error), - /// Parse amount error. - ParseAmount(ParseAmountError), - /// Parsing sighash type string failed. - SighashTypeParse, - /// PSBT inputs field is empty. - InputsEmpty, - /// BIP32 data missing. - MissingBip32Derivation, - /// Fingerprint does not match that in input. - WrongFingerprint, - /// Pubkey for derivation path does not match that in input. - WrongPubkey, -} +struct Error(Box); -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } +impl From for Error { + fn from(e: T) -> Self { Error(Box::new(e)) } } -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) } -} - -impl From for Error { - fn from(e: bip32::Error) -> Error { Error::Bip32(e) } -} - -impl From for Error { - fn from(e: psbt::Error) -> Error { Error::Psbt(e) } -} - -impl From for Error { - fn from(e: SighashError) -> Error { Error::PsbtSighash(e) } -} - -impl From for Error { - fn from(e: hex::Error) -> Error { Error::Hex(e) } -} - -impl From for Error { - fn from(e: address::Error) -> Error { Error::Address(e) } -} - -impl From for Error { - fn from(e: ParseAmountError) -> Error { Error::ParseAmount(e) } -} - -/// This module implements signing a PSBT. It is based on code in `rust-miniscript` with a bit of a -/// look at `bdk` as well. Since this example only uses ECDSA signatures the signing code is -/// sufficient however before we can merge this into the main `rust-bitcoin` crate we need to handle -/// taproot as well. See PR: https://github.com/rust-bitcoin/rust-bitcoin/pull/957 -/// -/// All functions that take a `psbt` argument should be implemented on `Psbt` and use `self` instead. -mod psbt_sign { - use std::fmt; - use std::ops::Deref; - - use bitcoin::psbt::{Input, Prevouts, Psbt, PsbtSighashType}; - use bitcoin::util::sighash::{self, SighashCache}; - use bitcoin::util::taproot::TapLeafHash; - use bitcoin::{ - EcdsaSig, EcdsaSigError, EcdsaSighashType, PrivateKey, SchnorrSighashType, Script, - Transaction, TxOut, - }; - use secp256k1::{Message, Secp256k1, Signing}; - - /// Signs the input at `input_index` with private key `sk`. - pub fn sign( - psbt: &mut Psbt, - sk: &PrivateKey, - input_index: usize, - secp: &Secp256k1, - ) -> Result<(), SighashError> { - check_index_is_within_bounds(psbt, input_index)?; - - let mut cache = SighashCache::new(&psbt.unsigned_tx); - let (msg, sighash_ty) = sighash(psbt, input_index, &mut cache, None)?; - - let sig = secp.sign_ecdsa(&msg, &sk.inner); - - let mut final_signature = Vec::with_capacity(75); - final_signature.extend_from_slice(&sig.serialize_der()); - final_signature.push(sighash_ty.to_u32() as u8); - - let pk = sk.public_key(secp); - psbt.inputs[input_index].partial_sigs.insert(pk, EcdsaSig::from_slice(&final_signature)?); - - Ok(()) - } - - /// Returns the sighash message to sign along with the sighash type. - fn sighash>( - psbt: &Psbt, - input_index: usize, - cache: &mut SighashCache, - tapleaf_hash: Option, - ) -> Result<(Message, PsbtSighashType), SighashError> { - check_index_is_within_bounds(psbt, input_index)?; - - let input = &psbt.inputs[input_index]; - let prevouts = prevouts(psbt)?; - - let utxo = spend_utxo(psbt, input_index)?; - let script = utxo.script_pubkey.clone(); // scriptPubkey for input spend utxo. - - if script.is_v1_p2tr() { - return taproot_sighash(input, prevouts, input_index, cache, tapleaf_hash); - } - - let hash_ty = input - .sighash_type - .map(|ty| ty.ecdsa_hash_ty()) - .unwrap_or(Ok(EcdsaSighashType::All)) - .map_err(|_| SighashError::InvalidSighashType)?; // Only support standard sighash types. - - let is_wpkh = script.is_v0_p2wpkh(); - let is_wsh = script.is_v0_p2wsh(); - - let is_nested_wpkh = script.is_p2sh() - && input.redeem_script.as_ref().map(|s| s.is_v0_p2wpkh()).unwrap_or(false); - - let is_nested_wsh = script.is_p2sh() - && input.redeem_script.as_ref().map(|x| x.is_v0_p2wsh()).unwrap_or(false); - - let is_segwit = is_wpkh || is_wsh || is_nested_wpkh || is_nested_wsh; - - let sighash = if is_segwit { - if is_wpkh || is_nested_wpkh { - let script_code = if is_wpkh { - Script::p2wpkh_script_code(&script).ok_or(SighashError::NotWpkh)? - } else { - Script::p2wpkh_script_code(input.redeem_script.as_ref().expect("checked above")) - .ok_or(SighashError::NotWpkh)? - }; - cache.segwit_signature_hash(input_index, &script_code, utxo.value, hash_ty)? - } else { - let script_code = - input.witness_script.as_ref().ok_or(SighashError::MissingWitnessScript)?; - cache.segwit_signature_hash(input_index, script_code, utxo.value, hash_ty)? - } - } else { - let script_code = if script.is_p2sh() { - input.redeem_script.as_ref().ok_or(SighashError::MissingRedeemScript)? - } else { - &script - }; - cache.legacy_signature_hash(input_index, script_code, hash_ty.to_u32())? - }; - - Ok((Message::from_slice(&sighash).expect("sighashes are 32 bytes"), hash_ty.into())) - } - - /// Returns the prevouts for this PSBT. - fn prevouts(psbt: &Psbt) -> Result, SighashError> { - let len = psbt.inputs.len(); - let mut utxos = Vec::with_capacity(len); - - for i in 0..len { - utxos.push(spend_utxo(psbt, i)?); - } - - Ok(utxos) - } - - /// Returns the spending utxo for this PSBT's input at `input_index`. - fn spend_utxo(psbt: &Psbt, input_index: usize) -> Result<&TxOut, SighashError> { - check_index_is_within_bounds(psbt, input_index)?; - - let input = &psbt.inputs[input_index]; - let utxo = if let Some(witness_utxo) = &input.witness_utxo { - witness_utxo - } else if let Some(non_witness_utxo) = &input.non_witness_utxo { - let vout = psbt.unsigned_tx.input[input_index].previous_output.vout; - &non_witness_utxo.output[vout as usize] - } else { - return Err(SighashError::MissingSpendUtxo); - }; - Ok(utxo) - } - - /// Checks `input_index` is within bounds for the PSBT `inputs` array and - /// for the PSBT `unsigned_tx` `input` array. - fn check_index_is_within_bounds(psbt: &Psbt, input_index: usize) -> Result<(), SighashError> { - if input_index >= psbt.inputs.len() { - return Err(SighashError::IndexOutOfBounds(input_index, psbt.inputs.len())); - } - - if input_index >= psbt.unsigned_tx.input.len() { - return Err(SighashError::IndexOutOfBounds(input_index, psbt.unsigned_tx.input.len())); - } - - Ok(()) - } - - /// Returns the sighash message and sighash type for this `input`. - fn taproot_sighash>( - input: &Input, - prevouts: Vec<&TxOut>, - input_index: usize, - cache: &mut SighashCache, - tapleaf_hash: Option, - ) -> Result<(Message, PsbtSighashType), SighashError> { - // Note that as per PSBT spec we should have access to spent utxos for the transaction. Even - // if the transaction does not require SIGHASH_ALL, we create `Prevouts::All` for simplicity. - let prevouts = Prevouts::All(&prevouts); - - let hash_ty = input - .sighash_type - .map(|ty| ty.schnorr_hash_ty()) - .unwrap_or(Ok(SchnorrSighashType::Default)) - .map_err(|_e| SighashError::InvalidSighashType)?; - - let sighash = match tapleaf_hash { - Some(leaf_hash) => cache.taproot_script_spend_signature_hash( - input_index, - &prevouts, - leaf_hash, - hash_ty, - )?, - None => cache.taproot_key_spend_signature_hash(input_index, &prevouts, hash_ty)?, - }; - let msg = Message::from_slice(&sighash).expect("sighashes are 32 bytes"); - Ok((msg, hash_ty.into())) - } - - /// Errors encountered while calculating the sighash message. - #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] - pub enum SighashError { - /// Input index out of bounds (actual index, maximum index allowed). - IndexOutOfBounds(usize, usize), - /// Missing spending utxo. - MissingSpendUtxo, - /// Missing witness script. - MissingWitnessScript, - /// Missing Redeem script. - MissingRedeemScript, - /// Invalid Sighash type. - InvalidSighashType, - /// The `scriptPubkey` is not a P2WPKH script. - NotWpkh, - /// Sighash computation error. - SighashComputation(sighash::Error), - /// An ECDSA key-related error occurred. - EcdsaSig(EcdsaSigError), - } - - impl fmt::Display for SighashError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - SighashError::IndexOutOfBounds(ind, len) => { - write!(f, "index {}, psbt input len: {}", ind, len) - } - SighashError::MissingSpendUtxo => write!(f, "missing spend utxon in PSBT"), - SighashError::MissingWitnessScript => write!(f, "missing witness script"), - SighashError::MissingRedeemScript => write!(f, "missing redeem script"), - SighashError::InvalidSighashType => write!(f, "invalid sighash type"), - SighashError::NotWpkh => write!(f, "the scriptPubkey is not a P2WPKH script"), - // If merged into rust-bitcoin these two should use `write_err!`. - SighashError::SighashComputation(e) => write!(f, "sighash: {}", e), - SighashError::EcdsaSig(e) => write!(f, "ecdsa: {}", e), - } - } - } - - impl From for SighashError { - fn from(e: sighash::Error) -> Self { SighashError::SighashComputation(e) } - } - - impl From for SighashError { - fn from(e: EcdsaSigError) -> Self { SighashError::EcdsaSig(e) } - } - - #[cfg(feature = "std")] - impl std::error::Error for SighashError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - use self::SighashError::*; - - match self { - IndexOutOfBounds(_, _) - | MissingSpendUtxo - | MissingWitnessScript - | MissingRedeemScript - | InvalidSighashType - | NotWpkh => None, - SighashComputation(e) => Some(e), - EcdsaSig(e) => Some(e), - } - } - } +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) } }