use std::str::FromStr;
use psbt_v2::bitcoin::bip32::{DerivationPath, KeySource, Xpriv, Xpub};
use psbt_v2::bitcoin::hashes::Hash as _;
use psbt_v2::bitcoin::locktime::absolute;
use psbt_v2::bitcoin::opcodes::all::OP_CHECKMULTISIG;
use psbt_v2::bitcoin::secp256k1::{self, SECP256K1};
use psbt_v2::bitcoin::{
script, Address, Amount, CompressedPublicKey, Network, OutPoint, PublicKey, ScriptBuf,
Sequence, TxOut, Txid,
};
use psbt_v2::v2::{
self, Constructor, 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; const FEE: Amount = Amount::from_sat(1_000); const DUMMY_CHANGE_AMOUNT: Amount = Amount::from_sat(100_000);
fn main() -> anyhow::Result<()> {
let alice = Alice::new();
let bob = Bob::new();
let pk_a = alice.multisig_public_key()?;
let pk_b = bob.multisig_public_key()?;
let min_required_height = absolute::Height::from_consensus(800_000).expect("valid height");
let (previous_output_a, change_address_a, change_value_a) = alice.contribute_to_multisig()?;
let previous_output_b = bob.contribute_to_multisig();
let constructor = Constructor::<Modifiable>::default();
let input_a = InputBuilder::new(&previous_output_a)
.minimum_required_height_based_lock_time(min_required_height)
.build();
let input_b = InputBuilder::new(&previous_output_b)
.build();
let change = TxOut { value: change_value_a, script_pubkey: change_address_a.script_pubkey() };
let witness_script = multisig_witness_script(&pk_a, &pk_b);
let address = Address::p2wsh(&witness_script, MAINNET);
let value = SPEND_AMOUNT * 2 - FEE;
let multi = TxOut { value, script_pubkey: address.script_pubkey() };
let psbt = constructor
.input(input_a)
.input(input_b)
.output(OutputBuilder::new(multi).build()) .output(Output::new(change)) .psbt()
.expect("valid lock time combination");
let psbt = Updater::new(psbt)?.set_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF, 1)?.psbt();
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)?;
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);
Ok(())
}
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()
}
pub struct Alice(Entity);
impl Alice {
const PATH: &'static str = "m/84'/0'/0'/0/42";
pub fn new() -> Self {
let seed = [0x00; 32]; let xpriv = Xpriv::new_master(MAINNET, &seed).unwrap();
Self(Entity::new(xpriv))
}
pub fn multisig_public_key(&self) -> anyhow::Result<bitcoin::PublicKey> {
self.0.public_key("m/84'/0'/0'/123")
}
pub fn contribute_to_multisig(&self) -> anyhow::Result<(OutPoint, Address, Amount)> {
let out = OutPoint { txid: Txid::all_zeros(), vout: 0 };
let compressed =
CompressedPublicKey::try_from(self.multisig_public_key()?).expect("uncompressed key");
let address = Address::p2wpkh(&compressed, Network::Bitcoin);
let amount = DUMMY_CHANGE_AMOUNT;
Ok((out, address, amount))
}
pub fn sign(&self, psbt: Psbt) -> anyhow::Result<Psbt> { self.0.sign_ecdsa(psbt, Self::PATH) }
pub fn update(&self, mut psbt: Psbt) -> anyhow::Result<Psbt> {
let input = &mut psbt.inputs[0];
input.witness_utxo = Some(self.input_utxo()?);
let (pk, key_source) = self.bip32_derivation()?;
input.bip32_derivations.insert(pk, key_source);
Ok(psbt)
}
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() }
}
pub struct Bob(Entity);
impl Bob {
const PATH: &'static str = "m/84'/0'/0'/0/0";
pub fn new() -> Self {
let seed = [0x11; 32]; let xpriv = Xpriv::new_master(MAINNET, &seed).unwrap();
Self(Entity::new(xpriv))
}
pub fn multisig_public_key(&self) -> anyhow::Result<bitcoin::PublicKey> {
self.0.public_key("m/84'/0'/0'/20")
}
pub fn contribute_to_multisig(&self) -> OutPoint {
OutPoint { txid: Txid::all_zeros(), vout: 1 }
}
pub fn sign(&self, psbt: Psbt) -> anyhow::Result<Psbt> { self.0.sign_ecdsa(psbt, Self::PATH) }
pub fn update(&self, mut psbt: Psbt) -> anyhow::Result<Psbt> {
let input = &mut psbt.inputs[1];
input.witness_utxo = Some(self.input_utxo()?);
let (pk, key_source) = self.bip32_derivation()?;
input.bip32_derivations.insert(pk, key_source);
Ok(psbt)
}
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() }
}
pub struct Entity {
master: Xpriv,
}
impl Entity {
pub fn new(master: Xpriv) -> Self { Self { master } }
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().into())
}
fn input_utxo(&self, derivation_path: &str) -> anyhow::Result<TxOut> {
let script_pubkey = ScriptBuf::new_p2wpkh(
&self.public_key(derivation_path)?.wpubkey_hash().expect("uncompressed key"),
);
Ok(TxOut { value: DUMMY_UTXO_AMOUNT, script_pubkey })
}
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)))
}
pub fn sign_ecdsa(&self, psbt: Psbt, derivation_path: &str) -> anyhow::Result<Psbt> {
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),
}
}
}