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 0b98c65
Show file tree
Hide file tree
Showing 27 changed files with 5,983 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 }
}
239 changes: 239 additions & 0 deletions examples/v2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
//! 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::collections::BTreeMap;

use psbt::bitcoin::hashes::Hash as _;
use psbt::bitcoin::locktime::absolute;
use psbt::bitcoin::opcodes::all::OP_CHECKMULTISIG;
use psbt::bitcoin::secp256k1::{self, rand, 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 their pubkey.
let pk_a = alice.public_key();
let pk_b = bob.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.

let mut psbt = Updater::new(psbt)?.set_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF, 1)?.psbt();

// Update the PSBT with the inputs described by `previous_output_a` and `previous_output_b`
// above, here we get them from Alice and Bob, typically the update would have access to chain
// data and would get them from there.
psbt.inputs[0].witness_utxo = Some(alice.input_utxo());
psbt.inputs[1].witness_utxo = Some(bob.input_utxo());

// The signer role.

// Each party signs a copy of the PSBT.
let signed_by_a = alice.sign(psbt.clone());
let signed_by_b = bob.sign(psbt);

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 {
/// Creates a new actor with random keys.
pub fn new() -> Self { Self(Entity::new_random()) }

/// Returns the public key for this entity.
pub fn public_key(&self) -> bitcoin::PublicKey { self.0.public_key() }

/// 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) -> (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.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;

(out, address, amount)
}

/// Provides the actual UTXO that Alice is contributing, this would usually come from the chain.
pub fn input_utxo(&self) -> 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().wpubkey_hash().expect("uncompressed key"));
TxOut { value: DUMMY_UTXO_AMOUNT, script_pubkey }
}

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

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

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

impl Bob {
/// Creates a new actor with random keys.
pub fn new() -> Self { Self(Entity::new_random()) }

/// Returns the public key for this entity.
pub fn public_key(&self) -> bitcoin::PublicKey { self.0.public_key() }

/// 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 }
}

/// Provides the actual UTXO that Alice is contributing, this would usually come from the chain.
pub fn input_utxo(&self) -> TxOut {
// A dummy script_pubkey representing a UTXO that is locked to a pubkey that Bob controls.
let script_pubkey =
ScriptBuf::new_p2wpkh(&self.public_key().wpubkey_hash().expect("uncompressed key"));
TxOut { value: DUMMY_UTXO_AMOUNT, script_pubkey }
}

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

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

/// An entity that can take on one of the PSBT roles.
pub struct Entity {
sk: secp256k1::SecretKey,
pk: secp256k1::PublicKey,
}

impl Entity {
/// Creates a new entity with random keys.
pub fn new_random() -> Self {
let (sk, pk) = random_keys();
Entity { sk, pk }
}

/// Returns the private key for this entity.
fn private_key(&self) -> bitcoin::PrivateKey { bitcoin::PrivateKey::new(self.sk, MAINNET) }

/// Returns the public key for this entity.
///
/// All examples use segwit so this key is serialize in compressed form.
pub fn public_key(&self) -> bitcoin::PublicKey { bitcoin::PublicKey::new(self.pk) }

/// Signs any ECDSA inputs for which we have keys.
pub fn sign_ecdsa(&self, psbt: Psbt) -> Psbt {
let sk = self.private_key();
let pk = self.public_key();

let mut keys = BTreeMap::new();
keys.insert(pk, sk);

let signer = Signer::new(psbt).expect("determine lock time failed");
let (psbt, signing_keys) = signer.sign(&keys, &SECP256K1).expect("failed to sign psbt");
debug_assert!(signing_keys.len() == keys.len()); // Just a quick sanity check.
psbt
}
}

/// Creates a set of random secp256k1 keys.
///
/// In a real application these would come from actual secrets.
fn random_keys() -> (secp256k1::SecretKey, secp256k1::PublicKey) {
let sk = secp256k1::SecretKey::new(&mut rand::thread_rng());
let pk = sk.public_key(SECP256K1);
(sk, pk)
}
12 changes: 0 additions & 12 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,14 @@ pub(crate) const PSBT_GLOBAL_UNSIGNED_TX: u8 = 0x00;
/// Type: Extended Public Key PSBT_GLOBAL_XPUB = 0x01
pub(crate) const PSBT_GLOBAL_XPUB: u8 = 0x01;
/// Type: Transaction Version PSBT_GLOBAL_TX_VERSION = 0x02
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_GLOBAL_TX_VERSION: u8 = 0x02;
/// Type: Fallback Locktime PSBT_GLOBAL_FALLBACK_LOCKTIME = 0x03
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_GLOBAL_FALLBACK_LOCKTIME: u8 = 0x03;
/// Type: Input Count PSBT_GLOBAL_INPUT_COUNT = 0x04
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_GLOBAL_INPUT_COUNT: u8 = 0x04;
/// Type: Output Count PSBT_GLOBAL_OUTPUT_COUNT = 0x05
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_GLOBAL_OUTPUT_COUNT: u8 = 0x05;
/// Type: Transaction Modifiable Flags PSBT_GLOBAL_TX_MODIFIABLE = 0x06
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_GLOBAL_TX_MODIFIABLE: u8 = 0x06;
/// Type: Version Number PSBT_GLOBAL_VERSION = 0xFB
pub(crate) const PSBT_GLOBAL_VERSION: u8 = 0xFB;
Expand Down Expand Up @@ -59,19 +54,14 @@ pub(crate) const PSBT_IN_HASH160: u8 = 0x0c;
/// Type: HASH256 preimage PSBT_IN_HASH256 = 0x0d
pub(crate) const PSBT_IN_HASH256: u8 = 0x0d;
/// Type: Previous TXID PSBT_IN_PREVIOUS_TXID = 0x0e
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_IN_PREVIOUS_TXID: u8 = 0x0e;
/// Type: Spent Output Index PSBT_IN_OUTPUT_INDEX = 0x0f
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_IN_OUTPUT_INDEX: u8 = 0x0f;
/// Type: Sequence Number PSBT_IN_SEQUENCE = 0x10
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_IN_SEQUENCE: u8 = 0x10;
/// Type: Required Time-based Locktime PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_IN_REQUIRED_TIME_LOCKTIME: u8 = 0x11;
/// Type: Required Height-based Locktime PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_IN_REQUIRED_HEIGHT_LOCKTIME: u8 = 0x12;
/// Type: Taproot Signature in Key Spend PSBT_IN_TAP_KEY_SIG = 0x13
pub(crate) const PSBT_IN_TAP_KEY_SIG: u8 = 0x13;
Expand All @@ -95,10 +85,8 @@ pub(crate) const PSBT_OUT_WITNESS_SCRIPT: u8 = 0x01;
/// Type: BIP 32 Derivation Path PSBT_OUT_BIP32_DERIVATION = 0x02
pub(crate) const PSBT_OUT_BIP32_DERIVATION: u8 = 0x02;
/// Type: Output Amount PSBT_OUT_AMOUNT = 0x03
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_OUT_AMOUNT: u8 = 0x03;
/// Type: Output Script PSBT_OUT_SCRIPT = 0x04
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_OUT_SCRIPT: u8 = 0x04;
/// Type: Taproot Internal Key PSBT_OUT_TAP_INTERNAL_KEY = 0x05
pub(crate) const PSBT_OUT_TAP_INTERNAL_KEY: u8 = 0x05;
Expand Down
Loading

0 comments on commit 0b98c65

Please sign in to comment.