Skip to content

Commit

Permalink
WIP: Add improvements to psbt example
Browse files Browse the repository at this point in the history
This should be squashed with the previous commit if you are happy with
it Dan.
  • Loading branch information
tcharding committed Apr 4, 2022
1 parent 6d8a815 commit 22224c3
Showing 1 changed file with 31 additions and 67 deletions.
98 changes: 31 additions & 67 deletions examples/psbt.rs
Expand Up @@ -7,6 +7,23 @@
//! The private keys in the tests below are derived from the following master private key:
//!

// Attribution: `sign_psbt` logic is based on code in the `wallet` module of the `bdk` project. The
// code in the two functions `legacy_sighash()` and `segwit_v0_sighash` is copied directly from
// `bdk`, hence their licence applies to that code.
//
// ref: http://github.com/bitcoindevkit/bdk
//
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

extern crate bitcoin;

use std::fmt;
Expand All @@ -25,9 +42,6 @@ use bitcoin::util::bip32::{ExtendedPrivKey, ExtendedPubKey, Fingerprint, IntoDer
use bitcoin::util::psbt::{PartiallySignedTransaction, PsbtSigHashType};
use bitcoin::util::sighash::SigHashCache;

// Enable this to print debugging output.
const DEBUG: bool = false;

const NETWORK: Network = Network::Testnet;

type Psbt = PartiallySignedTransaction; // TODO: Consider adding this alias to `rust-bitcoin`.
Expand Down Expand Up @@ -133,6 +147,7 @@ fn build_extended_private_key() -> ExtendedPrivKey {

let ext_priv = ExtendedPrivKey::from_str(extended_private_key).unwrap();

// TODO: Fix this so parsing seed works.
let seed = base58::from(&seed).unwrap();
let seeded = ExtendedPrivKey::new_master(NETWORK, &seed).unwrap();
if seeded != ext_priv {
Expand Down Expand Up @@ -181,7 +196,7 @@ fn create_transaction() -> Transaction {
vout: input_0.index,
},
script_sig: Script::new(),
sequence: 0xFFFFFFFF, // TODO: Consider adding a const that documents why this is u32_max.
sequence: 0xFFFFFFFF, // Disable nSequence.
witness: Witness::default(),
},
TxIn {
Expand Down Expand Up @@ -243,9 +258,6 @@ fn update_psbt(mut psbt: Psbt, fingerprint: Fingerprint) -> Psbt {

let expected_psbt_hex = include_str!("data/update_1_psbt_hex");

print_transaction(previous_tx_0, "Previous transaction 0");
print_transaction(previous_tx_1, "Previous transaction 1");

let mut input_0 = psbt.inputs[0].clone();

let v = Vec::from_hex(&previous_tx_1).unwrap();
Expand Down Expand Up @@ -376,23 +388,11 @@ fn combine(mut this: Psbt, that: Psbt) -> Psbt {
fn finalize(psbt: Psbt) -> Psbt {
let expected_psbt_hex = include_str!("data/finalize_psbt_hex");

// let msg = "Pre-finalize PSBT";
// println!("\n\n{}\n", msg);
// println!("{:#?}", psbt);

let psbt = finalize_psbt(psbt);

let want = hex_psbt!(expected_psbt_hex).unwrap();
assert_eq!(psbt, want);

// let msg = "Expected finalized PSBT";
// println!("\n\n{}\n", msg);
// println!("{:#?}", want);

// let msg = "Actual finalized PSBT";
// println!("\n\n{}\n", msg);
// println!("{:#?}", psbt);

let got = serialize_hex(&psbt);
assert_eq!(got, expected_psbt_hex);

Expand Down Expand Up @@ -451,6 +451,7 @@ fn sign(mut psbt: Psbt, keys: &[PrivateKey]) -> Psbt {
}

/// Signs `psbt` input at `input_index` using `sk`.
// TODO: Consider adding this code to rust-bitcoin.
fn sign_psbt(
sk: &PrivateKey,
psbt: &mut Psbt,
Expand Down Expand Up @@ -516,17 +517,15 @@ fn sign_psbt(

let mut final_signature = Vec::with_capacity(75);
final_signature.extend_from_slice(&signature.serialize_der());
// Why only a single byte, this won't work for non-standard sighash types?
final_signature.push(sighash_type.to_u32() as u8);

// QUESTION: In bdk source code we insert into partial_sigs but according to
// the BIP test vector we only return the new signatures?
psbt.inputs[input_index]
.partial_sigs = BTreeMap::from([(pubkey, EcdsaSig::from_slice(&final_signature).map_err(|_| SignError::Ecdsa)?)]);

Ok(())
}

// Copied directly from `impl ComputeSighash for Legacy` in `bdk`.
fn legacy_sighash(psbt: &Psbt, input_index: usize) -> Result<(SigHash, EcdsaSigHashType), SignError> {
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
return Err(SignError::InputIndexOutOfRange);
Expand Down Expand Up @@ -559,6 +558,7 @@ fn legacy_sighash(psbt: &Psbt, input_index: usize) -> Result<(SigHash, EcdsaSigH
))
}

// Copied directly from `impl ComputeSighash for Segwitv0` in `bdk`.
fn segwit_v0_sighash(psbt: &Psbt, input_index: usize) -> Result<(SigHash, EcdsaSigHashType), SignError> {
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
return Err(SignError::InputIndexOutOfRange);
Expand Down Expand Up @@ -625,10 +625,11 @@ fn finalize_psbt(mut psbt: Psbt) -> Psbt {
use bitcoin::util::psbt::serialize::Serialize;

// Input 0: legacy UTXO

let sigs: Vec<_> = psbt.inputs[0].partial_sigs.values().collect();
let script_sig = script::Builder::new()
// OP_CHECKMULTISIG bug pops +1 value when evaluating. Need extra OP_0 byte in front
// Why is OP_0 called `OP_PUSHBYTES_0` in rust-bitcoin?
// OP_CHECKMULTISIG bug pops +1 value when evaluating so push OP_0.
// TODO: Consider adding OP_ZERO alias to rust-bitcoin (`pub static OP_ZERO: All = all::OP_PUSHBYTES_0;`).
.push_opcode(opcodes::all::OP_PUSHBYTES_0)
.push_slice(&sigs[0].serialize())
.push_slice(&sigs[1].serialize())
Expand All @@ -652,7 +653,7 @@ fn finalize_psbt(mut psbt: Psbt) -> Psbt {

let sigs: Vec<_> = psbt.inputs[1].partial_sigs.values().collect();
let mut script_witness: Witness = Witness::new();
script_witness.push([ ]); // What's the cleanest 0x00 push?
script_witness.push([]); // Pushes 0x00 to the stack.
script_witness.push(&sigs[1].serialize());
script_witness.push(&sigs[0].serialize());
script_witness.push(&psbt.inputs[1].witness_script.clone().unwrap().serialize());
Expand All @@ -678,63 +679,26 @@ fn p2wpkh_script_code(script: &Script) -> Script {
.into_script()
}

/// Deserializes `hex` string and pretty prints the PSBT.
fn pretty_print_psbt(hex: &str, msg: &str) {
if DEBUG {
let v = Vec::from_hex(&hex).unwrap();
let psbt: Psbt = deserialize(&v).unwrap();
println!("\n\n{}\n", msg);
println!("{:#?}", psbt);
}
}

/// Deserializes `hex` string and pretty prints the Transaction.
fn print_transaction(hex: &str, msg: &str) {
if DEBUG {
let v = Vec::from_hex(&hex).unwrap();
let tx: Transaction = deserialize(&v).unwrap();
println!("\n\n{}\n", msg);
println!("{:?}", tx);
}
}

// TODO: Clean up this error.
/// Signing error
/// Signing error.
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum SignError {
/// The private key is missing for the required public key
MissingKey,
/// The private key in use has the right fingerprint but derives differently than expected
InvalidKey,
/// The user canceled the operation
UserCanceled,
/// Input index is out of range
InputIndexOutOfRange,
/// The `non_witness_utxo` field of the transaction is required to sign this input
MissingNonWitnessUtxo,
/// The `non_witness_utxo` specified is invalid
InvalidNonWitnessUtxo,
/// The `witness_utxo` field of the transaction is required to sign this input
MissingWitnessUtxo,
/// The `witness_script` field of the transaction is required to sign this input
MissingWitnessScript,
/// The fingerprint and derivation path are missing from the psbt input
MissingHdKeypath,
/// The psbt contains a non-`SIGHASH_ALL` sighash in one of its input and the user hasn't
/// explicitly allowed them
///
/// To enable signing transactions with non-standard sighashes set
/// [`SignOptions::allow_all_sighashes`] to `true`.
NonStandardSighash,
/// bitcoin::ecdsa error.
Ecdsa,
///
/// Sighash encoding error.
SigHash,
///
/// BIP174: non-witness input txid must match txid of unsigned transaction.
InvalidTxid,
///
/// BIP174: witness script must match the hash in the redeem script.
WitnessRedeemMismatch,
///
/// BIP174: redeem script must match the hash in the UTXO.
ScritpPubkeyMismatch,
}

Expand Down

0 comments on commit 22224c3

Please sign in to comment.