Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PSBT v2 #8

Merged
merged 4 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 - using BIP-32.
//!
//! 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