From 325bf6f6142620d63118135df2d5c5cec968e058 Mon Sep 17 00:00:00 2001 From: conduition Date: Thu, 5 Sep 2024 21:46:28 +0000 Subject: [PATCH] add tests demonstrating musig2 crate interop with bitcoin crate --- Cargo.toml | 1 + tests/bitcoin_tweak.rs | 58 +++++++++++++++++ tests/sign_btc_tx.rs | 140 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 tests/bitcoin_tweak.rs create mode 100644 tests/sign_btc_tx.rs diff --git a/Cargo.toml b/Cargo.toml index 2091c22..4d3fd3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ sha2 = { version = "0.10.8", default-features = false } subtle = { version = "2.5.0", default-features = false } [dev-dependencies] +bitcoin = "0.32" serde = { version = "1.0.188", features = ["serde_derive"] } serde_json = "1.0.107" csv = "1.3.0" diff --git a/tests/bitcoin_tweak.rs b/tests/bitcoin_tweak.rs new file mode 100644 index 0000000..7b434d9 --- /dev/null +++ b/tests/bitcoin_tweak.rs @@ -0,0 +1,58 @@ +use bitcoin::{ + key::{Secp256k1, TweakedPublicKey}, + ScriptBuf, +}; +use musig2::KeyAggContext; +use secp::{Point, Scalar, G}; + +use std::error::Error; + +// Demonstrates how our unspendable tweaking matches the `bitcoin` crate's tweaking. +#[test] +fn demo_unspendable_tweaks() -> Result<(), Box> { + let k1: Scalar = "d12bfbef9790c08b87ca6f8656e6a3660aad3db4698be7d4951d4a9e48c777a3" + .parse() + .unwrap(); + let k2: Scalar = "06a71b4ba66658e5c9ed311f4d90541bb95910b890b5f93f32b942e1c1e56c65" + .parse() + .unwrap(); + + let pubkeys = [k1 * G, k2 * G]; + + let key_agg_ctx = KeyAggContext::new(pubkeys)?.with_unspendable_taproot_tweak()?; + + // Untweaked (internal) key + let untweaked_pubkey_point: Point = key_agg_ctx.aggregated_pubkey_untweaked(); + let untweaked_pubkey_xonly = + bitcoin::XOnlyPublicKey::from_slice(&untweaked_pubkey_point.serialize_xonly()).unwrap(); + assert_eq!( + untweaked_pubkey_point.to_string(), + "03656d72bc43082f28d32360f2cf02d78574386d3bbeb8f944112f061edf0a8c47" + ); + assert_eq!( + untweaked_pubkey_xonly.to_string(), + "656d72bc43082f28d32360f2cf02d78574386d3bbeb8f944112f061edf0a8c47" + ); + + // Tweaked (output) key + let tweaked_pubkey_point: Point = key_agg_ctx.aggregated_pubkey(); + let tweaked_pubkey_xonly = + bitcoin::XOnlyPublicKey::from_slice(&tweaked_pubkey_point.serialize_xonly()).unwrap(); + assert_eq!( + tweaked_pubkey_point.to_string(), + "02a84db1877f2101c0ac472915c0bc63c4a1af8accbff2dd0b6944c70dbcf9f017" + ); + assert_eq!( + tweaked_pubkey_xonly.to_string(), + "a84db1877f2101c0ac472915c0bc63c4a1af8accbff2dd0b6944c70dbcf9f017" + ); + + // Our crate should ensure the tweaked KeyAggContext results in properly tweaked aggregated pubkey. + let tweaked_pubkey = TweakedPublicKey::dangerous_assume_tweaked(tweaked_pubkey_xonly); + let spk1 = ScriptBuf::new_p2tr_tweaked(tweaked_pubkey); + let spk2 = ScriptBuf::new_p2tr(&Secp256k1::new(), untweaked_pubkey_xonly, None); + + assert_eq!(spk1, spk2); + + Ok(()) +} diff --git a/tests/sign_btc_tx.rs b/tests/sign_btc_tx.rs new file mode 100644 index 0000000..a610bb0 --- /dev/null +++ b/tests/sign_btc_tx.rs @@ -0,0 +1,140 @@ +// use bitcoin::key::TweakedPublicKey; +use bitcoin::{ + absolute::LockTime, + sighash::{Prevouts, SighashCache, TapSighash, TapSighashType}, + transaction::Version, + Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, +}; +use musig2::{AggNonce, KeyAggContext, PartialSignature, SecNonce}; +use secp::{Point, Scalar, G}; + +use std::error::Error; + +const PREVOUT_TX: &str = "02000000000101f285fc5b46fbbcc4e8e25bd30500196384bacdcfadc67fba53a4f39e622f96be0100000000fdffffff0210270000000000002251204c2dfc6cfa8454e1b260a5d34083fc0e993a233a2fc794977d87302e8e401586b5ea010000000000160014c06885ae6fd959c8ad391a5215f479aea738825402473044022009e4d15a83b84f86eed8aa624b8ce15bef48c1c6595cd97c89772ac615fc08ed02206b96bda72c65c2d43b23e4f06335625545e4ef8d8c0eecfae74745ba1274e26f0121024833e9f45d6bdfec69743f890f033eb83b3cabda31dbb7d5fa72270feb6eed6e951f0d00"; + +const PREVOUT_VOUT: u32 = 0; +const PREVOUT_VALUE: Amount = Amount::from_sat(10_000); + +const EXPECTED_SPENDING_TX: &str = "020000000001016e6427e0edfdb793d5bc853597d20ce9229d128f39181dfb009ebf1fa91f42bb000000000000000000012224000000000000160014b1e6c11edebf01ec881bec426ecd8acc582d92f20140817c605659a6fbcc416224b456b96c6e79d3ad9e14d4c43c0b2a974d0040ae22b22d33ad68e9155b878afceda886b44c1e44f7e4147fbed417fe721dfc445f4d00000000"; + +#[test] +fn sign_btc_tx() -> Result<(), Box> { + let k1: Scalar = "d12bfbef9790c08b87ca6f8656e6a3660aad3db4698be7d4951d4a9e48c777a3" + .parse() + .unwrap(); + let k2: Scalar = "06a71b4ba66658e5c9ed311f4d90541bb95910b890b5f93f32b942e1c1e56c66" + .parse() + .unwrap(); + + let pubkeys = [k1 * G, k2 * G]; + + let key_agg_ctx = KeyAggContext::new(pubkeys)?.with_unspendable_taproot_tweak()?; + + let agg_pubkey: Point = key_agg_ctx.aggregated_pubkey(); + let agg_pubkey_xonly = + bitcoin::XOnlyPublicKey::from_slice(&agg_pubkey.serialize_xonly()).unwrap(); + let tweaked_pubkey = bitcoin::key::TweakedPublicKey::dangerous_assume_tweaked(agg_pubkey_xonly); + let prevout_spk = ScriptBuf::new_p2tr_tweaked(tweaked_pubkey); + + let prevout_tx_bytes: Vec = base16ct::mixed::decode_vec(PREVOUT_TX).unwrap(); + let prevout_tx: Transaction = bitcoin::consensus::encode::deserialize(&prevout_tx_bytes)?; + + assert_eq!( + prevout_tx.output[PREVOUT_VOUT as usize].script_pubkey, prevout_spk, + "prevout SPK must match" + ); + assert_eq!( + prevout_tx.output[PREVOUT_VOUT as usize].value, PREVOUT_VALUE, + "prevout value must match" + ); + + let dest_addr = "bc1qk8nvz8k7huq7ezqma3pxanv2e3vzmyhjgcztsz" + .parse::>()? + .assume_checked(); + + let mut spending_tx = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + sequence: Sequence::ZERO, + previous_output: OutPoint { + txid: prevout_tx.compute_txid(), + vout: PREVOUT_VOUT, + }, + script_sig: ScriptBuf::new(), + witness: Witness::new(), + }], + output: vec![TxOut { + value: PREVOUT_VALUE - Amount::from_sat(750), + script_pubkey: dest_addr.script_pubkey(), + }], + }; + + let sighash: TapSighash = SighashCache::new(&spending_tx).taproot_key_spend_signature_hash( + 0, // vin + &Prevouts::All(&[&prevout_tx.output[PREVOUT_VOUT as usize]]), + TapSighashType::Default, + )?; + + // Sign the sighash with musig + let signature = sign_musig(&key_agg_ctx, (k1, k2), sighash)?; + + musig2::verify_single(agg_pubkey, signature, sighash) + .expect("signature should be valid for aggregated key"); + + // Append the signature to the TxIn witness. + let serialized_sig: [u8; 64] = signature.serialize(); + spending_tx.input[0].witness.push(serialized_sig); + + assert_eq!( + bitcoin::consensus::encode::serialize_hex(&spending_tx), + EXPECTED_SPENDING_TX + ); + + // https://mempool.space/tx/d4601e5e4ed0ec9a2a012eea927d2954e499ce54e0ce5dd8ea59911cbeba4434 + assert_eq!( + spending_tx.compute_txid().to_string(), + "d4601e5e4ed0ec9a2a012eea927d2954e499ce54e0ce5dd8ea59911cbeba4434" + ); + + Ok(()) +} + +fn sign_musig( + key_agg_ctx: &KeyAggContext, + (k1, k2): (Scalar, Scalar), + message: impl AsRef<[u8]>, +) -> Result> { + let agg_pub: Point = key_agg_ctx.aggregated_pubkey(); + + let r1 = SecNonce::build([0x11; 32]) // insecure, use an actual secret nonce seed + .with_seckey(k1) + .with_aggregated_pubkey(agg_pub) + .with_message(&message) + .build(); + let r2 = SecNonce::build([0x22; 32]) // insecure, use an actual secret nonce seed + .with_seckey(k2) + .with_aggregated_pubkey(agg_pub) + .with_message(&message) + .build(); + + let pubnonce1 = r1.public_nonce(); + let pubnonce2 = r2.public_nonce(); + + let aggnonce = AggNonce::sum([pubnonce1, pubnonce2]); + + let partial_sig1: PartialSignature = + musig2::sign_partial(key_agg_ctx, k1, r1, &aggnonce, &message).unwrap(); + let partial_sig2: PartialSignature = + musig2::sign_partial(key_agg_ctx, k2, r2, &aggnonce, &message).unwrap(); + + let final_signature = musig2::aggregate_partial_signatures( + &key_agg_ctx, + &aggnonce, + [partial_sig1, partial_sig2], + message, + ) + .unwrap(); + + Ok(final_signature) +}