Skip to content

Commit

Permalink
Merge pull request #4 from tcharding/09-20-add-example
Browse files Browse the repository at this point in the history
Add basic multisig example
  • Loading branch information
tcharding authored Sep 20, 2024
2 parents 459ccbe + 0d44c0c commit 0c86360
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 1 deletion.
8 changes: 8 additions & 0 deletions Cargo-minimal.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions Cargo-recent.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
2 changes: 1 addition & 1 deletion contrib/test_vars.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
230 changes: 230 additions & 0 deletions examples/multisig.rs
Original file line number Diff line number Diff line change
@@ -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<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) -> anyhow::Result<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) -> anyhow::Result<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");

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

0 comments on commit 0c86360

Please sign in to comment.