diff --git a/Cargo.lock b/Cargo.lock index 84cffe3..2c40635 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,6 +175,7 @@ dependencies = [ "hex", "hidapi", "js-sys", + "miniscript", "noise-protocol", "noise-rust-crypto", "num-bigint", @@ -943,6 +944,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "miniscript" +version = "12.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b59c67956fd276ceec0cf194fbf80754ef4d88a496d5cf5e4fdf33561466183d" +dependencies = [ + "bech32", + "bitcoin", +] + [[package]] name = "miniz_oxide" version = "0.7.4" diff --git a/Cargo.toml b/Cargo.toml index c71bdfb..023ca8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,11 @@ wasm-bindgen-test = "0.3.42" tokio = { version = "1", features = ["time", "macros", "rt", "fs"] } reqwest = "0.12" url = "2.5" +# Enable this to be able to get coverage using `cargo tarpaulin --features=simulator,tokio --out=Html` without compilation error. +# See https://github.com/rust-bitcoin/rust-bitcoinconsensus/pull/94 +# bitcoinconsensus = { git = "https://github.com/benma/rust-bitcoinconsensus.git", rev = "d132eec12cf86038838c45185b083f834dfb7b26", default-features = false } bitcoinconsensus = { version = "0.106.0", default-features = false } +miniscript = "12.0.0" [build-dependencies] prost-build = { version = "0.11" } diff --git a/tests/subtests/test_btc_psbt.rs b/tests/subtests/test_btc_psbt.rs index 23afa57..21767df 100644 --- a/tests/subtests/test_btc_psbt.rs +++ b/tests/subtests/test_btc_psbt.rs @@ -1,12 +1,17 @@ use super::PairedBitBox; -use bitbox_api::pb; +use std::collections::HashMap; + +use bitbox_api::{btc::Xpub, pb, Keypath}; use bitcoin::bip32::DerivationPath; use bitcoin::psbt::Psbt; +use bitcoin::secp256k1; use bitcoin::{ transaction, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, }; +use miniscript::psbt::PsbtExt; +use miniscript::TranslatePk; // Checks that the psbt is fully signed and valid (all scripts execute correctly). fn verify_transaction(psbt: Psbt) { @@ -46,11 +51,12 @@ fn verify_transaction(psbt: Psbt) { pub async fn test(bitbox: &PairedBitBox) { test_taproot_key_spend(bitbox).await; test_mixed_spend(bitbox).await; + test_policy_wsh(bitbox).await; } // Test signing; all inputs are BIP86 Taproot keyspends. async fn test_taproot_key_spend(bitbox: &PairedBitBox) { - let secp = bitcoin::secp256k1::Secp256k1::new(); + let secp = secp256k1::Secp256k1::new(); let fingerprint = super::simulator_xprv().fingerprint(&secp); @@ -164,11 +170,7 @@ async fn test_taproot_key_spend(bitbox: &PairedBitBox) { .unwrap(); // Finalize, add witnesses. - psbt.inputs.iter_mut().for_each(|input| { - let mut script_witness = Witness::new(); - script_witness.push(input.tap_key_sig.unwrap().to_vec()); - input.final_script_witness = Some(script_witness); - }); + psbt.finalize_mut(&secp).unwrap(); // Verify the signed tx, including that all sigs/witnesses are correct. verify_transaction(psbt); @@ -176,7 +178,7 @@ async fn test_taproot_key_spend(bitbox: &PairedBitBox) { // Test signing; mixed input types (p2wpkh, p2wpkh-p2sh, p2tr) async fn test_mixed_spend(bitbox: &PairedBitBox) { - let secp = bitcoin::secp256k1::Secp256k1::new(); + let secp = secp256k1::Secp256k1::new(); let fingerprint = super::simulator_xprv().fingerprint(&secp); @@ -313,28 +315,239 @@ async fn test_mixed_spend(bitbox: &PairedBitBox) { .unwrap(); // Finalize, add witnesses. + psbt.finalize_mut(&secp).unwrap(); + + // // p2tr + // psbt.inputs[0].final_script_witness = Some(Witness::p2tr_key_spend( + // psbt.inputs[0].tap_key_sig.as_ref().unwrap(), + // )); + // // p2wpkh + // psbt.inputs[1].final_script_witness = Some({ + // let (pubkey, sig) = psbt.inputs[1].partial_sigs.first_key_value().unwrap(); + // Witness::p2wpkh(sig, &pubkey.inner) + // }); + // // p2wpkh-p2sh needs a witness (for the p2wpkh part) and a script_sig (for the p2sh part). + // psbt.inputs[2].final_script_sig = Some({ + // let redeemscript: &bitcoin::script::PushBytes = + // input2_redeemscript.as_bytes().try_into().unwrap(); + // let mut script = ScriptBuf::new(); + // script.push_slice(redeemscript); + // script + // }); + // psbt.inputs[2].final_script_witness = Some({ + // let (pubkey, sig) = psbt.inputs[2].partial_sigs.first_key_value().unwrap(); + // Witness::p2wpkh(sig, &pubkey.inner) + // }); + + // Verify the signed tx, including that all sigs/witnesses are correct. + verify_transaction(psbt); +} + +struct StrPkTranslator { + pk_map: HashMap<&'static str, bitcoin::PublicKey>, +} + +impl miniscript::Translator for StrPkTranslator { + fn pk(&mut self, pk: &String) -> Result { + self.pk_map.get(pk.as_str()).copied().ok_or(()) + } + + // We don't need to implement these methods as we are not using them in the policy. + // Fail if we encounter any hash fragments. See also translate_hash_clone! macro. + miniscript::translate_hash_fail!(String, bitcoin::PublicKey, ()); +} + +async fn test_policy_wsh(bitbox: &PairedBitBox) { + let secp = secp256k1::Secp256k1::new(); - // p2tr - psbt.inputs[0].final_script_witness = Some(Witness::p2tr_key_spend( - psbt.inputs[0].tap_key_sig.as_ref().unwrap(), - )); - // p2wpkh - psbt.inputs[1].final_script_witness = Some({ - let (pubkey, sig) = psbt.inputs[1].partial_sigs.first_key_value().unwrap(); - Witness::p2wpkh(sig, &pubkey.inner) - }); - // p2wpkh-p2sh needs a witness (for the p2wpkh part) and a script_sig (for the p2sh part). - psbt.inputs[2].final_script_sig = Some({ - let redeemscript: &bitcoin::script::PushBytes = - input2_redeemscript.as_bytes().try_into().unwrap(); - let mut script = ScriptBuf::new(); - script.push_slice(redeemscript); - script - }); - psbt.inputs[2].final_script_witness = Some({ - let (pubkey, sig) = psbt.inputs[2].partial_sigs.first_key_value().unwrap(); - Witness::p2wpkh(sig, &pubkey.inner) - }); + let coin = pb::BtcCoin::Tbtc; + let policy = "wsh(or_b(pk(@0/**),s:pk(@1/**)))"; + let our_root_fingerprint = super::simulator_xprv().fingerprint(&secp); + + let keypath_account: Keypath = "m/48'/1'/0'/3'".try_into().unwrap(); + + let our_xpub_str = bitbox + .btc_xpub( + coin, + &keypath_account, + pb::btc_pub_request::XPubType::Tpub, + false, + ) + .await + .unwrap(); + + let our_xpub: Xpub = our_xpub_str.parse().unwrap(); + let some_xpub: Xpub = "tpubDFgycCkexSxkdZfeyaasDHityE97kiYM1BeCNoivDHvydGugKtoNobt4vEX6YSHNPy2cqmWQHKjKxciJuocepsGPGxcDZVmiMBnxgA1JKQk".parse().unwrap(); + + let keys = &[ + // Our key: root fingerprint and keypath are required. + bitbox_api::btc::KeyOriginInfo { + root_fingerprint: Some(our_root_fingerprint), + keypath: Some(keypath_account.clone()), + xpub: our_xpub, + }, + // Foreign key: root fingerprint and keypath are optional. + bitbox_api::btc::KeyOriginInfo { + root_fingerprint: None, + keypath: None, + xpub: some_xpub, + }, + ]; + let policy_config = bitbox_api::btc::make_script_config_policy(policy, keys); + + // Register policy if not already registered. This must be done before any receive address is + // created or any transaction is signed. + let is_registered = bitbox + .btc_is_script_config_registered(coin, &policy_config, None) + .await + .unwrap(); + + if !is_registered { + bitbox + .btc_register_script_config( + coin, + &policy_config, + None, + pb::btc_register_script_config_request::XPubType::AutoXpubTpub, + Some("test wsh policy"), + ) + .await + .unwrap(); + } + + let descriptor: miniscript::Descriptor = policy.parse().unwrap(); + assert!(descriptor.sanity_check().is_ok()); + + let input_pubkey: bitcoin::PublicKey = our_xpub + .derive_pub(&secp, &[0.into(), 0.into()]) + .unwrap() + .to_pub() + .into(); + + let change_pubkey: bitcoin::PublicKey = some_xpub + .derive_pub(&secp, &[0.into(), 0.into()]) + .unwrap() + .to_pub() + .into(); + + let input_descriptor = descriptor + .translate_pk(&mut StrPkTranslator { + pk_map: { + let mut pk_map = HashMap::new(); + pk_map.insert("@0/**", input_pubkey.clone()); + pk_map.insert("@1/**", change_pubkey.clone()); + pk_map + }, + }) + .unwrap(); + + let change_descriptor = descriptor + .translate_pk(&mut StrPkTranslator { + pk_map: { + let mut pk_map = HashMap::new(); + pk_map.insert( + "@0/**", + our_xpub + .derive_pub(&secp, &[1.into(), 0.into()]) + .unwrap() + .to_pub() + .into(), + ); + pk_map.insert( + "@1/**", + some_xpub + .derive_pub(&secp, &[1.into(), 0.into()]) + .unwrap() + .to_pub() + .into(), + ); + pk_map + }, + }) + .unwrap(); + + // A previous tx which creates some UTXOs we can reference later. + let prev_tx = Transaction { + version: transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: "3131313131313131313131313131313131313131313131313131313131313131:0" + .parse() + .unwrap(), + script_sig: ScriptBuf::new(), + sequence: Sequence(0xFFFFFFFF), + witness: Witness::default(), + }], + output: vec![TxOut { + value: Amount::from_sat(100_000_000), + script_pubkey: input_descriptor.script_pubkey(), + }], + }; + + let tx = Transaction { + version: transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: prev_tx.compute_txid(), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence(0xFFFFFFFF), + witness: Witness::default(), + }], + output: vec![ + TxOut { + value: Amount::from_sat(70_000_000), + script_pubkey: change_descriptor.script_pubkey(), + }, + TxOut { + value: Amount::from_sat(20_000_000), + script_pubkey: ScriptBuf::new_p2tr( + &secp, + // random private key: + // 9dbb534622a6100a39b73dece43c6d4db14b9a612eb46a6c64c2bb849e283ce8 + "e4adbb12c3426ec71ebb10688d8ae69d531ca822a2b790acee216a7f1b95b576" + .parse() + .unwrap(), + None, + ), + }, + ], + }; + + let input_path: DerivationPath = "m/48'/1'/0'/3'/0/0".parse().unwrap(); + let change_path: DerivationPath = "m/48'/1'/0'/3'/1/0".parse().unwrap(); + + let mut psbt = Psbt::from_unsigned_tx(tx).unwrap(); + + // Add input and change infos. + psbt.inputs[0].non_witness_utxo = Some(prev_tx.clone()); + psbt.inputs[0] + .bip32_derivation + .insert(input_pubkey.inner, (our_root_fingerprint, input_path)); + psbt.inputs[0].witness_script = Some(input_descriptor.explicit_script().unwrap()); + + psbt.outputs[0] + .bip32_derivation + .insert(change_pubkey.inner, (our_root_fingerprint, change_path)); + + // Sign. + bitbox + .btc_sign_psbt( + pb::BtcCoin::Tbtc, + &mut psbt, + Some(pb::BtcScriptConfigWithKeypath { + script_config: Some(policy_config), + keypath: keypath_account.to_vec(), + }), + pb::btc_sign_init_request::FormatUnit::Default, + ) + .await + .unwrap(); + + // Finalize, add witnesses. + psbt.finalize_mut(&secp).unwrap(); // Verify the signed tx, including that all sigs/witnesses are correct. verify_transaction(psbt);