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.
This commit is contained in:
Tobin C. Harding 2022-07-21 11:43:03 +10:00
parent d2367fb187
commit dd8730e14f
1 changed files with 20 additions and 332 deletions

View File

@ -28,27 +28,24 @@
//! `bt listunspent` //! `bt listunspent`
//! //!
use std::boxed::Box;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fmt; use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use bitcoin::consensus::encode; use bitcoin::consensus::encode;
use bitcoin::hashes::hex::{self, FromHex}; use bitcoin::hashes::hex::FromHex;
use bitcoin::locktime::absolute; use bitcoin::locktime::absolute;
use bitcoin::secp256k1::{Secp256k1, Signing, Verification}; use bitcoin::secp256k1::{Secp256k1, Signing, Verification};
use bitcoin::util::amount::ParseAmountError;
use bitcoin::util::bip32::{ use bitcoin::util::bip32::{
self, ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint, ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint, IntoDerivationPath,
IntoDerivationPath,
}; };
use bitcoin::util::psbt::{self, Input, Psbt, PsbtSighashType}; use bitcoin::util::psbt::{self, Input, Psbt, PsbtSighashType};
use bitcoin::{ use bitcoin::{
address, Address, Amount, Network, OutPoint, PrivateKey, PublicKey, Script, Sequence, Address, Amount, Network, OutPoint, PublicKey, Script, Sequence, Transaction, TxIn, TxOut,
Transaction, TxIn, TxOut, Txid, Witness, Txid, Witness,
}; };
use self::psbt_sign::*;
type Result<T> = std::result::Result<T, Error>; type Result<T> = std::result::Result<T, Error>;
// Get this from the output of `bt dumpwallet <file>`. // Get this from the output of `bt dumpwallet <file>`.
@ -141,34 +138,15 @@ impl ColdStorage {
/// Signs `psbt` with this signer. /// Signs `psbt` with this signer.
fn sign_psbt<C: Signing>(&self, secp: &Secp256k1<C>, mut psbt: Psbt) -> Result<Psbt> { fn sign_psbt<C: Signing>(&self, secp: &Secp256k1<C>, mut psbt: Psbt) -> Result<Psbt> {
let sk = self.private_key_to_sign(secp, &psbt.inputs[0])?; match psbt.sign(&self.master_xpriv, secp) {
psbt_sign::sign(&mut psbt, &sk, 0, 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());
}
};
Ok(psbt) Ok(psbt)
} }
/// Returns the private key required to sign `input` if we have it.
fn private_key_to_sign<C: Signing>(
&self,
secp: &Secp256k1<C>,
input: &Input,
) -> Result<PrivateKey> {
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)
}
None => Err(Error::MissingBip32Derivation),
}
}
} }
/// An example of an watch-only online wallet. /// An example of an watch-only online wallet.
@ -247,7 +225,7 @@ impl WatchOnly {
map.insert(pk.inner, (fingerprint, path)); map.insert(pk.inner, (fingerprint, path));
input.bip32_derivation = map; 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); input.sighash_type = Some(ty);
psbt.inputs = vec![input]; psbt.inputs = vec![input];
@ -260,11 +238,10 @@ impl WatchOnly {
use bitcoin::util::psbt::serialize::Serialize; use bitcoin::util::psbt::serialize::Serialize;
if psbt.inputs.is_empty() { 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 sigs: Vec<_> = psbt.inputs[0].partial_sigs.values().collect();
let mut script_witness: Witness = Witness::new(); let mut script_witness: Witness = Witness::new();
script_witness.push(&sigs[0].serialize()); script_witness.push(&sigs[0].serialize());
script_witness.push(self.input_xpub.to_pub().serialize()); script_witness.push(self.input_xpub.to_pub().serialize());
@ -313,301 +290,12 @@ fn previous_output() -> TxOut {
TxOut { value: amount.to_sat(), script_pubkey } TxOut { value: amount.to_sat(), script_pubkey }
} }
#[derive(Clone, Debug, PartialEq, Eq)] struct Error(Box<dyn std::error::Error>);
enum Error {
/// Bip32 error. impl<T: std::error::Error + 'static> From<T> for Error {
Bip32(bip32::Error), fn from(e: T) -> Self { Error(Box::new(e)) }
/// 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,
} }
impl std::error::Error for Error { impl fmt::Debug for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) }
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) }
}
impl From<bip32::Error> for Error {
fn from(e: bip32::Error) -> Error { Error::Bip32(e) }
}
impl From<psbt::Error> for Error {
fn from(e: psbt::Error) -> Error { Error::Psbt(e) }
}
impl From<SighashError> for Error {
fn from(e: SighashError) -> Error { Error::PsbtSighash(e) }
}
impl From<hex::Error> for Error {
fn from(e: hex::Error) -> Error { Error::Hex(e) }
}
impl From<address::Error> for Error {
fn from(e: address::Error) -> Error { Error::Address(e) }
}
impl From<ParseAmountError> 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::sighash::{self, EcdsaSighashType, SchnorrSighashType, SighashCache};
use bitcoin::util::taproot::TapLeafHash;
use bitcoin::{EcdsaSig, EcdsaSigError, PrivateKey, Script, Transaction, TxOut};
use secp256k1::{Message, Secp256k1, Signing};
/// Signs the input at `input_index` with private key `sk`.
pub fn sign<C: Signing>(
psbt: &mut Psbt,
sk: &PrivateKey,
input_index: usize,
secp: &Secp256k1<C>,
) -> 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<T: Deref<Target = Transaction>>(
psbt: &Psbt,
input_index: usize,
cache: &mut SighashCache<T>,
tapleaf_hash: Option<TapLeafHash>,
) -> 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<Vec<&TxOut>, 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<T: Deref<Target = Transaction>>(
input: &Input,
prevouts: Vec<&TxOut>,
input_index: usize,
cache: &mut SighashCache<T>,
tapleaf_hash: Option<TapLeafHash>,
) -> 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<sighash::Error> for SighashError {
fn from(e: sighash::Error) -> Self { SighashError::SighashComputation(e) }
}
impl From<EcdsaSigError> 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),
}
}
}
} }