-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
27 changed files
with
6,016 additions
and
91 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
} | ||
} | ||
} |
Oops, something went wrong.