Skip to content

Commit

Permalink
Add PSBT v2
Browse files Browse the repository at this point in the history
Add an implementation of PSBT version 2 - BOOM!

Includes all test vectors from BIP-370. Also includes changes to `v0` to
explicitly exclude keys as required by PSBT v2 upgrade.
  • Loading branch information
tcharding committed Dec 14, 2023
1 parent 7967ef6 commit 7f1690f
Show file tree
Hide file tree
Showing 27 changed files with 6,016 additions and 91 deletions.
10 changes: 9 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ description = "Partially signed Bitcoin Transaction, v0 and v1"
categories = ["cryptography::cryptocurrencies"]
keywords = [ "crypto", "bitcoin" ]
readme = "../README.md"
edition = "2018"
edition = "2021"
rust-version = "1.56.1"
exclude = ["tests", "contrib"]

[package.metadata.docs.rs]
Expand Down Expand Up @@ -47,3 +48,10 @@ secp256k1 = { version = "0.28", features = ["rand-std", "global-context"] }
[[example]]
name = "v0"
required-features = ["std"]

[[example]]
name = "v2"
required-features = ["std"]

[[example]]
name = "v2-separate-creator-constructor"
4 changes: 4 additions & 0 deletions contrib/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ if [ "$DO_LINT" = true ]
then
cargo clippy --all-features --all-targets -- -D warnings
cargo clippy --example v0 -- -D warnings
cargo clippy --example v2 -- -D warnings
cargo clippy --example v2-separate-creator-constructor -- -D warnings
fi

# Test without any features other than std first (same as default)
Expand All @@ -38,6 +40,8 @@ do
done

cargo run --example v0
cargo run --example v2
cargo clippy --example v2-separate-creator-constructor

if [ "$DO_NO_STD" = true ]
then
Expand Down
65 changes: 65 additions & 0 deletions examples/v2-separate-creator-constructor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! PSBT v2 - Creator a PSBT and hand it around to various different entities to add inputs and outputs.

use psbt::bitcoin::hashes::Hash as _;
use psbt::bitcoin::{Amount, OutPoint, ScriptBuf, TxOut, Txid};
use psbt::v2::{
Constructor, Creator, InputBuilder, InputsOnlyModifiable, OutputBuilder, OutputsOnlyModifiable,
Psbt,
};

fn main() -> anyhow::Result<()> {
// Create the PSBT.
let created = Creator::new().inputs_modifiable().outputs_modifiable().psbt();

let ser = created.serialize();

// The first constructor entity receives the PSBT and adds an input.
let psbt = Psbt::deserialize(&ser)?;
let in_0 = dummy_out_point();
let ser = Constructor::<InputsOnlyModifiable>::new(psbt)?
.input(InputBuilder::new(in_0).build())
.psbt()
.expect("valid lock time combination")
.serialize();

// The second constructor entity receives the PSBT with one input and adds a second input.
let psbt = Psbt::deserialize(&ser)?;
let in_1 = dummy_out_point();
let ser = Constructor::<InputsOnlyModifiable>::new(psbt)?
.input(InputBuilder::new(in_1).build())
.no_more_inputs()
.psbt()
.expect("valid lock time combination")
.serialize();

// The third constructor entity receives the PSBT with inputs and adds an output.
let psbt = Psbt::deserialize(&ser)?;
let output = dummy_tx_out();
let ser = Constructor::<OutputsOnlyModifiable>::new(psbt)?
.output(OutputBuilder::new(output).build())
.no_more_outputs()
.psbt()
.expect("valid lock time combination")
.serialize();

// The PSBT is now ready for handling with the updater role.
let _updatable_psbt = Psbt::deserialize(&ser)?;

Ok(())
}

/// A dummy `OutPoint`, this would usually be the unspent transaction that we are spending.
fn dummy_out_point() -> OutPoint {
let txid = Txid::hash(b"some arbitrary bytes");
let vout = 0x15;
OutPoint { txid, vout }
}

/// A dummy `TxOut`, this would usually be the output we are creating with this transaction.
fn dummy_tx_out() -> TxOut {
// Arbitrary script, may not even be a valid scriptPubkey.
let script = ScriptBuf::from_hex("76a914162c5ea71c0b23f5b9022ef047c4a86470a5b07088ac")
.expect("failed to parse script form hex");
let value = Amount::from_sat(123_456_789);
TxOut { value, script_pubkey: script }
}
288 changes: 288 additions & 0 deletions examples/v2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
//! PSBT v2 2 of 2 multisig example.
//!
//! An example of using PSBT v0 to create a 2 of 2 multisig by spending two native segwit v0 inputs
//! to a native segwit v0 output (the multisig output).
//!
//! We sign invalid inputs, this code is not run against Bitcoin Core so everything here should be
//! taken as NOT PROVEN CORRECT.
//!
//! This code is similar to `v0.rs` on purpose to show the differences between the APIs.

use std::str::FromStr;

use psbt::bitcoin::bip32::{DerivationPath, KeySource, Xpriv, Xpub};
use psbt::bitcoin::hashes::Hash as _;
use psbt::bitcoin::locktime::absolute;
use psbt::bitcoin::opcodes::all::OP_CHECKMULTISIG;
use psbt::bitcoin::secp256k1::{self, SECP256K1};
use psbt::bitcoin::{
script, Address, Amount, Network, OutPoint, PublicKey, ScriptBuf, Sequence, TxOut, Txid,
};
use psbt::v2::{
self, Constructor, Input, InputBuilder, Modifiable, Output, OutputBuilder, Psbt, Signer,
Updater,
};

pub const DUMMY_UTXO_AMOUNT: Amount = Amount::from_sat(20_000_000);
pub const SPEND_AMOUNT: Amount = Amount::from_sat(20_000_000);

const MAINNET: Network = Network::Bitcoin; // Bitcoin mainnet network.
const FEE: Amount = Amount::from_sat(1_000); // Usually this would be calculated.
const DUMMY_CHANGE_AMOUNT: Amount = Amount::from_sat(100_000);

fn main() -> anyhow::Result<()> {
// Mimic two people, Alice and Bob, who wish to create a 2-of-2 multisig output together.
let alice = Alice::new();
let bob = Bob::new();

// Each person provides the pubkey they want this multisig to be locked to.
let pk_a = alice.multisig_public_key()?;
let pk_b = bob.multisig_public_key()?;

// Use of a locktime is of course optional.
let min_required_height = absolute::Height::from_consensus(800_000).expect("valid height");

// Each party will be contributing 20,000,000 sats to the mulitsig output, as such each party
// provides an unspent input to create the multisig output (and any change details if needed).

// Alice has a UTXO that is too big, she needs change.
let (previous_output_a, change_address_a, change_value_a) = alice.contribute_to_multisig()?;

// Bob has a UTXO the right size so no change needed.
let previous_output_b = bob.contribute_to_multisig();

// In PSBT v1 the creator and constructor roles can be the same entity, for an example of having
// them separate see `./v2-separate-creator-constructor.rs`.

// The constructor role.

let constructor = Constructor::<Modifiable>::default();

let input_a = InputBuilder::new(previous_output_a)
.minimum_required_height_based_lock_time(min_required_height)
.build();

// If no lock time is required we can just create the `Input` directly.
let input_b = Input::new(previous_output_b);

// Build Alice's change output.
let change = TxOut { value: change_value_a, script_pubkey: change_address_a.script_pubkey() };

// Create the witness script, receive address, and the locking script.
let witness_script = multisig_witness_script(&pk_a, &pk_b);
let address = Address::p2wsh(&witness_script, MAINNET);
let value = SPEND_AMOUNT * 2 - FEE;
// The spend output is locked by the witness script.
let multi = TxOut { value, script_pubkey: address.script_pubkey() };

let psbt = constructor
.input(input_a)
.input(input_b)
.output(OutputBuilder::new(multi).build()) // Use of the `OutputBuilder` is identical
.output(Output::new(change)) // to just creating the `Output`.
.psbt()
.expect("valid lock time combination");

// The updater role.

// We can act as updater.
let psbt = Updater::new(psbt)?.set_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF, 1)?.psbt();

// Or we can get Alice and Bob to act as updaters.
let updated_by_a = alice.update(psbt.clone())?;
let updated_by_b = bob.update(psbt)?;

let updated = v2::combine(updated_by_a, updated_by_b)?;

// The signer role.

// Each party then acts in the signer role.
let signed_by_a = alice.sign(updated.clone())?;
let signed_by_b = bob.sign(updated)?;

let _signed = v2::combine(signed_by_a, signed_by_b);

// At this stage we would usually finalize with miniscript and extract the transaction.

Ok(())
}

/// Creates a 2-of-2 multisig script locking to a and b's keys.
fn multisig_witness_script(a: &PublicKey, b: &PublicKey) -> ScriptBuf {
script::Builder::new()
.push_int(2)
.push_key(a)
.push_key(b)
.push_int(2)
.push_opcode(OP_CHECKMULTISIG)
.into_script()
}

/// Party 1 in a 2-of-2 multisig.
pub struct Alice(Entity);

impl Alice {
/// The derivation path associated with the dummy utxo we are spending.
const PATH: &'static str = "m/84'/0'/0'/0/42";

/// Creates a new Alice.
pub fn new() -> Self {
let seed = [0x00; 32]; // Fake example with a fake seed :)
let xpriv = Xpriv::new_master(MAINNET, &seed).unwrap();

Self(Entity::new(xpriv))
}

/// Returns the public key for this entity.
pub fn multisig_public_key(&self) -> anyhow::Result<bitcoin::PublicKey> {
self.0.public_key("m/84'/0'/0'/123")
}

/// Alice provides an input to be used to create the multisig and the details required to get
/// some change back (change address and amount).
pub fn contribute_to_multisig(&self) -> anyhow::Result<(OutPoint, Address, Amount)> {
// An obviously invalid output, we just use all zeros then use the `vout` to differentiate
// Alice's output from Bob's.
let out = OutPoint { txid: Txid::all_zeros(), vout: 0 };

// The usual caveat about reusing addresses applies here, this is just an example.
let address = Address::p2wpkh(&self.multisig_public_key()?, Network::Bitcoin)
.expect("uncompressed key");

// This is a made up value, it is supposed to represent the outpoints value minus the value
// contributed to the multisig.
let amount = DUMMY_CHANGE_AMOUNT;

Ok((out, address, amount))
}

/// Signs `psbt`.
pub fn sign(&self, psbt: Psbt) -> anyhow::Result<Psbt> { self.0.sign_ecdsa(psbt, Self::PATH) }

/// Alice updates the PSBT, adding her utxo and key source.
pub fn update(&self, mut psbt: Psbt) -> anyhow::Result<Psbt> {
let input = &mut psbt.inputs[0];

// The dummy input utxo we are spending and the pubkey/keysource that will be used to sign it.
input.witness_utxo = Some(self.input_utxo()?);
let (pk, key_source) = self.bip32_derivation()?;
input.bip32_derivation.insert(pk, key_source);
Ok(psbt)
}

/// Provides the actual UTXO that Alice is contributing, this would usually come from the chain.
fn input_utxo(&self) -> anyhow::Result<TxOut> { self.0.input_utxo(Self::PATH) }

fn bip32_derivation(&self) -> anyhow::Result<(secp256k1::PublicKey, KeySource)> {
self.0.bip32_derivation(Self::PATH)
}
}

impl Default for Alice {
fn default() -> Self { Self::new() }
}

/// Party 2 in a 2-of-2 multisig.
pub struct Bob(Entity);

impl Bob {
/// The derivation path associated with the dummy utxo we are spending.
const PATH: &'static str = "m/84'/0'/0'/0/0";

/// Creates a new Bob.
pub fn new() -> Self {
let seed = [0x11; 32]; // Fake example with a fake seed :)
let xpriv = Xpriv::new_master(MAINNET, &seed).unwrap();

Self(Entity::new(xpriv))
}

/// Returns the public key for this entity.
pub fn multisig_public_key(&self) -> anyhow::Result<bitcoin::PublicKey> {
self.0.public_key("m/84'/0'/0'/20")
}

/// Bob provides an input to be used to create the multisig, its the right size so no change.
pub fn contribute_to_multisig(&self) -> OutPoint {
// An obviously invalid output, we just use all zeros then use the `vout` to differentiate
// Alice's output from Bob's.
OutPoint { txid: Txid::all_zeros(), vout: 1 }
}

/// Signs `psbt`.
pub fn sign(&self, psbt: Psbt) -> anyhow::Result<Psbt> { self.0.sign_ecdsa(psbt, Self::PATH) }

/// Alice updates the PSBT, adding her utxo and key source.
pub fn update(&self, mut psbt: Psbt) -> anyhow::Result<Psbt> {
let input = &mut psbt.inputs[1];

// The dummy input utxo we are spending and the pubkey/keysource that will be used to sign it.
input.witness_utxo = Some(self.input_utxo()?);
let (pk, key_source) = self.bip32_derivation()?;
input.bip32_derivation.insert(pk, key_source);
Ok(psbt)
}

/// Provides the actual UTXO that Alice is contributing, this would usually come from the chain.
fn input_utxo(&self) -> anyhow::Result<TxOut> { self.0.input_utxo(Self::PATH) }

fn bip32_derivation(&self) -> anyhow::Result<(secp256k1::PublicKey, KeySource)> {
self.0.bip32_derivation(Self::PATH)
}
}

impl Default for Bob {
fn default() -> Self { Self::new() }
}

/// An entity that can take on one of the PSBT roles.
pub struct Entity {
master: Xpriv,
}

impl Entity {
/// Creates a new entity with random keys.
pub fn new(master: Xpriv) -> Self { Self { master } }

/// Returns the pubkey for this entity at `derivation_path`.
fn public_key(&self, derivation_path: &str) -> anyhow::Result<bitcoin::PublicKey> {
let path = DerivationPath::from_str(derivation_path)?;
let xpriv = self.master.derive_priv(SECP256K1, &path)?;
let pk = Xpub::from_priv(SECP256K1, &xpriv);
Ok(pk.to_pub())
}

/// Returns a dummy utxo that we can spend.
fn input_utxo(&self, derivation_path: &str) -> anyhow::Result<TxOut> {
// A dummy script_pubkey representing a UTXO that is locked to a pubkey that Alice controls.
let script_pubkey = ScriptBuf::new_p2wpkh(
&self.public_key(derivation_path)?.wpubkey_hash().expect("uncompressed key"),
);
Ok(TxOut { value: DUMMY_UTXO_AMOUNT, script_pubkey })
}

/// Returns the BOP-32 stuff needed to sign an ECDSA input using the [`v2::Psbt`] BIP-32 signing API.
fn bip32_derivation(
&self,
derivation_path: &str,
) -> anyhow::Result<(secp256k1::PublicKey, KeySource)> {
let path = DerivationPath::from_str(derivation_path)?;
let xpriv = self.master.derive_priv(SECP256K1, &path).expect("failed to derive xpriv");
let fingerprint = xpriv.fingerprint(SECP256K1);
let sk = xpriv.to_priv();
Ok((sk.public_key(SECP256K1).inner, (fingerprint, path)))
}

/// Signs any ECDSA inputs for which we have keys.
pub fn sign_ecdsa(&self, psbt: Psbt, derivation_path: &str) -> anyhow::Result<Psbt> {
// Usually we'd have to check this was our input and provide the correct key.
let path = DerivationPath::from_str(derivation_path)?;
let xpriv = self.master.derive_priv(SECP256K1, &path)?;

let signer = Signer::new(psbt)?;
match signer.sign(&xpriv, SECP256K1) {
Ok((psbt, _signing_keys)) => Ok(psbt),
Err(e) => panic!("signing failed: {:?}", e),
}
}
}
Loading

0 comments on commit 7f1690f

Please sign in to comment.