-
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
5,850 additions
and
89 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,62 @@ | ||
//! 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() | ||
.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() | ||
.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() | ||
.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,231 @@ | ||
//! 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}; | ||
|
||
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(); | ||
|
||
// The updater role. | ||
|
||
let mut psbt = psbt.set_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF, 1)?; | ||
|
||
// 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()); | ||
|
||
// 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, mut psbt: Psbt) -> Psbt { | ||
let sk = self.private_key(); | ||
let pk = self.public_key(); | ||
|
||
let mut keys = BTreeMap::new(); | ||
keys.insert(pk, sk); | ||
psbt.sign(&keys, &SECP256K1).expect("failed to sign psbt"); | ||
|
||
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) | ||
} |
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
Oops, something went wrong.