From 0d44c0c9d8092239064ba8610d20376becdd8e66 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 19 Sep 2024 12:19:17 +1000 Subject: [PATCH] Add basic multisig example Add an example of creating a 2-of-2 multisig. Note this code is not tested against Core so is only best effort. --- Cargo-minimal.lock | 8 ++ Cargo-recent.lock | 8 ++ Cargo.toml | 6 ++ contrib/test_vars.sh | 2 +- examples/multisig.rs | 230 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 examples/multisig.rs diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index f40d547..e1e8cbc 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + [[package]] name = "arrayvec" version = "0.7.6" @@ -180,10 +186,12 @@ dependencies = [ name = "psbt-v0" version = "0.1.0" dependencies = [ + "anyhow", "base64", "bincode", "bitcoin", "bitcoin-internals", + "secp256k1", "serde", "serde_json", "serde_test", diff --git a/Cargo-recent.lock b/Cargo-recent.lock index f40d547..e1e8cbc 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + [[package]] name = "arrayvec" version = "0.7.6" @@ -180,10 +186,12 @@ dependencies = [ name = "psbt-v0" version = "0.1.0" dependencies = [ + "anyhow", "base64", "bincode", "bitcoin", "bitcoin-internals", + "secp256k1", "serde", "serde_json", "serde_test", diff --git a/Cargo.toml b/Cargo.toml index 54f4ebe..5cabb11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,12 @@ base64 = { version = "0.21.3", optional = true } actual-serde = { package = "serde", version = "1.0.103", default-features = false, features = [ "derive", "alloc" ], optional = true } [dev-dependencies] +anyhow = "1" bincode = "1.3.1" serde_json = "1.0.0" serde_test = "1.0.19" +secp256k1 = { version = "0.29", features = ["rand-std", "global-context"] } + +[[example]] +name = "multisig" +required-features = ["rand-std"] diff --git a/contrib/test_vars.sh b/contrib/test_vars.sh index 4d1a1e6..f642c1b 100644 --- a/contrib/test_vars.sh +++ b/contrib/test_vars.sh @@ -11,4 +11,4 @@ FEATURES_WITH_STD="rand-std serde base64" FEATURES_WITHOUT_STD="rand serde base64" # Run these examples. -EXAMPLES="" +EXAMPLES="multisig:rand-std" diff --git a/examples/multisig.rs b/examples/multisig.rs new file mode 100644 index 0000000..0040827 --- /dev/null +++ b/examples/multisig.rs @@ -0,0 +1,230 @@ +//! PSBT v0 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. + +use std::collections::BTreeMap; + +use psbt_v0::bitcoin::hashes::Hash as _; +use psbt_v0::bitcoin::locktime::absolute; +use psbt_v0::bitcoin::opcodes::all::OP_CHECKMULTISIG; +use psbt_v0::bitcoin::secp256k1::{self, rand, SECP256K1}; +use psbt_v0::bitcoin::{ + script, transaction, Address, Amount, CompressedPublicKey, Network, OutPoint, PublicKey, + ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, +}; +use psbt_v0::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(); + + // 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 v0 the creator is responsible for creating the transaction. + + // Build the inputs using information provide by each party. + let input_0 = TxIn { + previous_output: previous_output_a, + script_sig: ScriptBuf::default(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::default(), + }; + let input_1 = TxIn { + previous_output: previous_output_b, + script_sig: ScriptBuf::default(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::default(), + }; + + // 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() }; + + // And create the transaction. + let tx = Transaction { + version: transaction::Version::TWO, // Post BIP-68. + lock_time: absolute::LockTime::ZERO, // Ignore the locktime. + input: vec![input_0, input_1], + output: vec![multi, change], + }; + + // Now the creator can create the PSBT. + let mut psbt = Psbt::from_unsigned_tx(tx)?; + + // 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()); + + // Since we are spending 2 p2wpkh inputs there are no other updates needed. + + // Each party signs a copy of the PSBT. + let signed_by_a = alice.sign(psbt.clone())?; + let _ = bob.sign(signed_by_a)?; + + // 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 compressed = + CompressedPublicKey::try_from(self.public_key()).expect("uncompressed key"); + let address = Address::p2wpkh(&compressed, Network::Bitcoin); + + // 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) -> anyhow::Result { 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) -> anyhow::Result { 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) -> anyhow::Result { + 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"); + + Ok(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) +}