diff --git a/src/descriptor/checksum.rs b/src/descriptor/checksum.rs index 15f80055..04b22601 100644 --- a/src/descriptor/checksum.rs +++ b/src/descriptor/checksum.rs @@ -92,3 +92,35 @@ pub fn get_checksum(desc: &str) -> Result { Ok(String::from_iter(chars)) } + +#[cfg(test)] +mod test { + use super::*; + use crate::descriptor::get_checksum; + + // test get_checksum() function; it should return the same value as Bitcoin Core + #[test] + fn test_get_checksum() { + let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"; + assert_eq!(get_checksum(desc).unwrap(), "tqz0nc62"); + + let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)"; + assert_eq!(get_checksum(desc).unwrap(), "lasegmfs"); + } + + #[test] + fn test_get_checksum_invalid_character() { + let sparkle_heart = vec![240, 159, 146, 150]; + let sparkle_heart = std::str::from_utf8(&sparkle_heart) + .unwrap() + .chars() + .next() + .unwrap(); + let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart); + + assert!(matches!( + get_checksum(&invalid_desc).err(), + Some(Error::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart + )); + } +} diff --git a/src/descriptor/dsl.rs b/src/descriptor/dsl.rs index d53b2a4d..9b7ef1e2 100644 --- a/src/descriptor/dsl.rs +++ b/src/descriptor/dsl.rs @@ -402,3 +402,297 @@ macro_rules! fragment { }); } + +#[cfg(test)] +mod test { + use bitcoin::hashes::hex::ToHex; + use miniscript::descriptor::{DescriptorPublicKey, KeyMap}; + use miniscript::{Descriptor, Legacy, Segwitv0}; + + use std::str::FromStr; + + use crate::descriptor::DescriptorMeta; + use crate::keys::{DescriptorKey, KeyError, ToDescriptorKey, ValidNetworks}; + use bitcoin::network::constants::Network::{Bitcoin, Regtest, Testnet}; + use bitcoin::util::bip32; + use bitcoin::util::bip32::ChildNumber; + + // test the descriptor!() macro + + // verify descriptor generates expected script(s) (if bare or pk) or address(es) + fn check( + desc: Result<(Descriptor, KeyMap, ValidNetworks), KeyError>, + is_witness: bool, + is_fixed: bool, + expected: &[&str], + ) { + let (desc, _key_map, _networks) = desc.unwrap(); + assert_eq!(desc.is_witness(), is_witness); + assert_eq!(desc.is_fixed(), is_fixed); + for i in 0..expected.len() { + let index = i as u32; + let child_desc = if desc.is_fixed() { + desc.clone() + } else { + desc.derive(ChildNumber::from_normal_idx(index).unwrap()) + }; + let address = child_desc.address(Regtest); + if address.is_some() { + assert_eq!(address.unwrap().to_string(), *expected.get(i).unwrap()); + } else { + let script = child_desc.script_pubkey(); + assert_eq!(script.to_hex().as_str(), *expected.get(i).unwrap()); + } + } + } + + // - at least one of each "type" of operator; ie. one modifier, one leaf_opcode, one leaf_opcode_value, etc. + // - mixing up key types that implement ToDescriptorKey in multi() or thresh() + + // expected script for pk and bare manually created + // expected addresses created with `bitcoin-cli getdescriptorinfo` (for hash) and `bitcoin-cli deriveaddresses` + + #[test] + fn test_fixed_legacy_descriptors() { + let pubkey1 = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + let pubkey2 = bitcoin::PublicKey::from_str( + "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af", + ) + .unwrap(); + + check( + descriptor!(bare(multi 1,pubkey1,pubkey2)), + false, + true, + &["512103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd21032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af52ae"], + ); + check( + descriptor!(pk(pubkey1)), + false, + true, + &["2103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bdac"], + ); + check( + descriptor!(pkh(pubkey1)), + false, + true, + &["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"], + ); + check( + descriptor!(sh(multi 1,pubkey1,pubkey2)), + false, + true, + &["2MymURoV1bzuMnWMGiXzyomDkeuxXY7Suey"], + ); + } + + #[test] + fn test_fixed_segwitv0_descriptors() { + let pubkey1 = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + let pubkey2 = bitcoin::PublicKey::from_str( + "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af", + ) + .unwrap(); + + check( + descriptor!(wpkh(pubkey1)), + true, + true, + &["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"], + ); + check( + descriptor!(sh(wpkh(pubkey1))), + true, + true, + &["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"], + ); + check( + descriptor!(wsh(multi 1,pubkey1,pubkey2)), + true, + true, + &["bcrt1qgw8jvv2hsrvjfa6q66rk6har7d32lrqm5unnf5cl63q9phxfvgps5fyfqe"], + ); + check( + descriptor!(sh(wsh(multi 1,pubkey1,pubkey2))), + true, + true, + &["2NCidRJysy7apkmE6JF5mLLaJFkrN3Ub9iy"], + ); + } + + #[test] + fn test_bip32_legacy_descriptors() { + let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key = (xprv, path.clone()).to_descriptor_key().unwrap(); + check( + descriptor!(pk(desc_key)), + false, + false, + &[ + "2102363ad03c10024e1b597a5b01b9982807fb638e00b06f3b2d4a89707de3b93c37ac", + "2102063a21fd780df370ed2fc8c4b86aa5ea642630609c203009df631feb7b480dd2ac", + "2102ba2685ad1fa5891cb100f1656b2ce3801822ccb9bac0336734a6f8c1b93ebbc0ac", + ], + ); + + let desc_key = (xprv, path.clone()).to_descriptor_key().unwrap(); + check( + descriptor!(pkh(desc_key)), + false, + false, + &[ + "muvBdsVpJxpFuTHMKA47htJPdCvdt4F9DP", + "mxQSHK7DL2t1DN3xFxov1janCoXSSkrSPj", + "mfz43r15GiWo4nizmyzMNubsnkDpByFFAn", + ], + ); + + let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap(); + let desc_key1 = (xprv, path).to_descriptor_key().unwrap(); + let desc_key2 = (xprv, path2).to_descriptor_key().unwrap(); + + check( + descriptor!(sh(multi 1,desc_key1,desc_key2)), + false, + false, + &[ + "2MtMDXsfwefZkEEhVViEPidvcKRUtJamJJ8", + "2MwAUZ1NYyWjhVvGTethFL6n7nZhS8WE6At", + "2MuT6Bj66HLwZd7s4SoD8XbK4GwriKEA6Gr", + ], + ); + } + + #[test] + fn test_bip32_segwitv0_descriptors() { + let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key = (xprv, path.clone()).to_descriptor_key().unwrap(); + check( + descriptor!(wpkh(desc_key)), + true, + false, + &[ + "bcrt1qnhm8w9fhc8cxzgqsmqdf9fyjccyvc0gltnymu0", + "bcrt1qhylfd55rn75w9fj06zspctad5w4hz33rf0ttad", + "bcrt1qq5sq3a6k9av9d8cne0k9wcldy4nqey5yt6889r", + ], + ); + + let desc_key = (xprv, path.clone()).to_descriptor_key().unwrap(); + check( + descriptor!(sh(wpkh(desc_key))), + true, + false, + &[ + "2MxvjQCaLqZ5QxZ7XotZDQ63hZw3NPss763", + "2NDUoevN4QMzhvHDMGhKuiT2fN9HXbFRMwn", + "2NF4BEAY2jF1Fu8vqfN3NVKoFtom77pUxrx", + ], + ); + + let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap(); + let desc_key1 = (xprv, path.clone()).to_descriptor_key().unwrap(); + let desc_key2 = (xprv, path2.clone()).to_descriptor_key().unwrap(); + check( + descriptor!(wsh(multi 1,desc_key1,desc_key2)), + true, + false, + &[ + "bcrt1qfxv8mxmlv5sz8q2mnuyaqdfe9jr4vvmx0csjhn092p6f4qfygfkq2hng49", + "bcrt1qerj85g243e6jlcdxpmn9spk0gefcwvu7nw7ee059d5ydzpdhkm2qwfkf5k", + "bcrt1qxkl2qss3k58q9ktc8e89pwr4gnptfpw4hju4xstxcjc0hkcae3jstluty7", + ], + ); + + let desc_key1 = (xprv, path).to_descriptor_key().unwrap(); + let desc_key2 = (xprv, path2).to_descriptor_key().unwrap(); + check( + descriptor!(sh(wsh(multi 1,desc_key1,desc_key2))), + true, + false, + &[ + "2NFCtXvx9q4ci2kvKub17iSTgvRXGctCGhz", + "2NB2PrFPv5NxWCpygas8tPrGJG2ZFgeuwJw", + "2N79ZAGo5cMi5Jt7Wo9L5YmF5GkEw7sjWdC", + ], + ); + } + + // - verify the valid_networks returned is correctly computed based on the keys present in the descriptor + #[test] + fn test_valid_networks() { + let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key = (xprv, path.clone()).to_descriptor_key().unwrap(); + + let (_desc, _key_map, valid_networks) = descriptor!(pkh(desc_key)).unwrap(); + assert_eq!(valid_networks, [Testnet, Regtest].iter().cloned().collect()); + + let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap(); + let path = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap(); + let desc_key = (xprv, path.clone()).to_descriptor_key().unwrap(); + + let (_desc, _key_map, valid_networks) = descriptor!(wpkh(desc_key)).unwrap(); + assert_eq!(valid_networks, [Bitcoin].iter().cloned().collect()); + } + + // - verify the key_maps are correctly merged together + #[test] + fn test_key_maps_merged() { + let xprv1 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let path1 = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key1 = (xprv1, path1.clone()).to_descriptor_key().unwrap(); + + let xprv2 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap(); + let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap(); + let desc_key2 = (xprv2, path2.clone()).to_descriptor_key().unwrap(); + + let xprv3 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap(); + let path3 = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap(); + let desc_key3 = (xprv3, path3.clone()).to_descriptor_key().unwrap(); + + let (_desc, key_map, _valid_networks) = + descriptor!(sh(wsh(multi 2,desc_key1,desc_key2,desc_key3))).unwrap(); + assert_eq!(key_map.len(), 3); + + let desc_key1: DescriptorKey = + (xprv1, path1.clone()).to_descriptor_key().unwrap(); + let desc_key2: DescriptorKey = + (xprv2, path2.clone()).to_descriptor_key().unwrap(); + let desc_key3: DescriptorKey = + (xprv3, path3.clone()).to_descriptor_key().unwrap(); + + let (key1, _key_map, _valid_networks) = desc_key1.extract().unwrap(); + let (key2, _key_map, _valid_networks) = desc_key2.extract().unwrap(); + let (key3, _key_map, _valid_networks) = desc_key3.extract().unwrap(); + assert_eq!(key_map.get(&key1).unwrap().to_string(), "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/0/*"); + assert_eq!(key_map.get(&key2).unwrap().to_string(), "tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF/2147483647'/0/*"); + assert_eq!(key_map.get(&key3).unwrap().to_string(), "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf/10/20/30/40/*"); + } + + // - verify the ScriptContext is correctly validated (i.e. passing a type that only impl ToDescriptorKey to a pkh() descriptor should throw a compilation error + #[test] + fn test_script_context_validation() { + // this compiles + let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + let desc_key: DescriptorKey = (xprv, path.clone()).to_descriptor_key().unwrap(); + + let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap(); + assert_eq!(desc.to_string(), "pkh(tpubD6NzVbkrYhZ4WR7a4vY1VT3khMJMeAxVsfq9TBJyJWrNk247zCJtV7AWf6UJP7rAVsn8NNKdJi3gFyKPTmWZS9iukb91xbn2HbFSMQm2igY/0/*)"); + + // as expected this does not compile due to invalid context + //let desc_key:DescriptorKey = (xprv, path.clone()).to_descriptor_key().unwrap(); + //let (desc, _key_map, _valid_networks) = descriptor!(pkh(desc_key)).unwrap(); + } +} diff --git a/src/descriptor/policy.rs b/src/descriptor/policy.rs index 69441c24..961b1ba7 100644 --- a/src/descriptor/policy.rs +++ b/src/descriptor/policy.rs @@ -787,3 +787,344 @@ impl ExtractPolicy for Descriptor { } } } + +#[cfg(test)] +mod test { + + use crate::descriptor; + use crate::descriptor::{ExtractPolicy, ToWalletDescriptor}; + + use super::*; + use crate::descriptor::policy::SatisfiableItem::{Multisig, Signature, Thresh}; + use crate::keys::{DescriptorKey, ToDescriptorKey}; + use crate::wallet::signer::SignersContainer; + use bitcoin::secp256k1::{All, Secp256k1}; + use bitcoin::util::bip32; + use bitcoin::util::bip32::ChildNumber; + use bitcoin::Network; + use std::str::FromStr; + use std::sync::Arc; + + const TPRV0_STR:&str = "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf"; + const TPRV1_STR:&str = "tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N"; + + const PATH: &str = "m/44'/1'/0'/0"; + + fn setup_keys( + tprv: &str, + ) -> (DescriptorKey, DescriptorKey, Fingerprint) { + let secp: Secp256k1 = Secp256k1::new(); + let path = bip32::DerivationPath::from_str(PATH).unwrap(); + let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap(); + let tpub = bip32::ExtendedPubKey::from_private(&secp, &tprv); + let fingerprint = tprv.fingerprint(&secp); + let prvkey = (tprv, path.clone()).to_descriptor_key().unwrap(); + let pubkey = (tpub, path).to_descriptor_key().unwrap(); + + (prvkey, pubkey, fingerprint) + } + + // test ExtractPolicy trait for simple descriptors; wpkh(), sh(multi()) + + #[test] + fn test_extract_policy_for_wpkh() { + let (prvkey, pubkey, fingerprint) = setup_keys(TPRV0_STR); + let desc = descriptor!(wpkh(pubkey)).unwrap(); + let (wallet_desc, keymap) = desc.to_wallet_descriptor(Network::Testnet).unwrap(); + let signers_container = Arc::new(SignersContainer::from(keymap)); + let policy = wallet_desc + .extract_policy(signers_container) + .unwrap() + .unwrap(); + + assert!( + matches!(&policy.item, Signature(pk_or_f) if &pk_or_f.fingerprint.unwrap() == &fingerprint) + ); + assert!(matches!(&policy.contribution, Satisfaction::None)); + + let desc = descriptor!(wpkh(prvkey)).unwrap(); + let (wallet_desc, keymap) = desc.to_wallet_descriptor(Network::Testnet).unwrap(); + let signers_container = Arc::new(SignersContainer::from(keymap)); + let policy = wallet_desc + .extract_policy(signers_container) + .unwrap() + .unwrap(); + + assert!( + matches!(&policy.item, Signature(pk_or_f) if &pk_or_f.fingerprint.unwrap() == &fingerprint) + ); + assert!( + matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None) + ); + } + + // 2 pub keys descriptor, required 2 prv keys + // #[test] + // fn test_extract_policy_for_sh_multi_partial_0of2() { + // let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR); + // let (_prvkey1, pubkey1, fingerprint1) = setup_keys(TPRV1_STR); + // let desc = descriptor!(sh(multi 2, pubkey0, pubkey1)).unwrap(); + // let (wallet_desc, keymap) = desc.to_wallet_descriptor(Network::Testnet).unwrap(); + // let signers_container = Arc::new(SignersContainer::from(keymap)); + // let policy = wallet_desc + // .extract_policy(signers_container) + // .unwrap() + // .unwrap(); + // + // assert!( + // matches!(&policy.item, Multisig { keys, threshold } if threshold == &2 + // && &keys[0].fingerprint.unwrap() == &fingerprint0 + // && &keys[1].fingerprint.unwrap() == &fingerprint1) + // ); + // + // // TODO should this be "Satisfaction::None" since we have no prv keys? + // // TODO should items and conditions not be empty? + // assert!( + // matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions} if n == &2 + // && m == &2 + // && items.is_empty() + // && conditions.is_empty() + // ) + // ); + // } + + // 1 prv and 1 pub key descriptor, required 2 prv keys + // #[test] + // fn test_extract_policy_for_sh_multi_partial_1of2() { + // let (prvkey0, _pubkey0, fingerprint0) = setup_keys(TPRV0_STR); + // let (_prvkey1, pubkey1, fingerprint1) = setup_keys(TPRV1_STR); + // let desc = descriptor!(sh(multi 2, prvkey0, pubkey1)).unwrap(); + // let (wallet_desc, keymap) = desc.to_wallet_descriptor(Network::Testnet).unwrap(); + // let signers_container = Arc::new(SignersContainer::from(keymap)); + // let policy = wallet_desc + // .extract_policy(signers_container) + // .unwrap() + // .unwrap(); + // + // assert!( + // matches!(&policy.item, Multisig { keys, threshold } if threshold == &2 + // && &keys[0].fingerprint.unwrap() == &fingerprint0 + // && &keys[1].fingerprint.unwrap() == &fingerprint1) + // ); + // + // // TODO should this be "Satisfaction::Partial" since we have only one of two prv keys? + // assert!( + // matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions} if n == &2 + // && m == &2 + // && items.len() == 2 + // && conditions.contains_key(&vec![0,1]) + // ) + // ); + // } + + // 1 prv and 1 pub key descriptor, required 1 prv keys + #[test] + fn test_extract_policy_for_sh_multi_complete_1of2() { + let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR); + let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR); + let desc = descriptor!(sh(multi 1, pubkey0, prvkey1)).unwrap(); + let (wallet_desc, keymap) = desc.to_wallet_descriptor(Network::Testnet).unwrap(); + let signers_container = Arc::new(SignersContainer::from(keymap)); + let policy = wallet_desc + .extract_policy(signers_container) + .unwrap() + .unwrap(); + + assert!( + matches!(&policy.item, Multisig { keys, threshold } if threshold == &1 + && &keys[0].fingerprint.unwrap() == &fingerprint0 + && &keys[1].fingerprint.unwrap() == &fingerprint1) + ); + assert!( + matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions} if n == &2 + && m == &1 + && items.len() == 2 + && conditions.contains_key(&vec![0]) + && conditions.contains_key(&vec![1]) + ) + ); + } + + // 2 prv keys descriptor, required 2 prv keys + #[test] + fn test_extract_policy_for_sh_multi_complete_2of2() { + let (prvkey0, _pubkey0, fingerprint0) = setup_keys(TPRV0_STR); + let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR); + let desc = descriptor!(sh(multi 2, prvkey0, prvkey1)).unwrap(); + let (wallet_desc, keymap) = desc.to_wallet_descriptor(Network::Testnet).unwrap(); + let signers_container = Arc::new(SignersContainer::from(keymap)); + let policy = wallet_desc + .extract_policy(signers_container) + .unwrap() + .unwrap(); + + assert!( + matches!(&policy.item, Multisig { keys, threshold } if threshold == &2 + && &keys[0].fingerprint.unwrap() == &fingerprint0 + && &keys[1].fingerprint.unwrap() == &fingerprint1) + ); + + assert!( + matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions} if n == &2 + && m == &2 + && items.len() == 2 + && conditions.contains_key(&vec![0,1]) + ) + ); + } + + // test ExtractPolicy trait with extended and single keys + + #[test] + fn test_extract_policy_for_single_wpkh() { + let (prvkey, pubkey, fingerprint) = setup_keys(TPRV0_STR); + let desc = descriptor!(wpkh(pubkey)).unwrap(); + let (wallet_desc, keymap) = desc.to_wallet_descriptor(Network::Testnet).unwrap(); + let single_key = wallet_desc.derive(ChildNumber::from_normal_idx(0).unwrap()); + let signers_container = Arc::new(SignersContainer::from(keymap)); + let policy = single_key + .extract_policy(signers_container) + .unwrap() + .unwrap(); + + assert!( + matches!(&policy.item, Signature(pk_or_f) if &pk_or_f.fingerprint.unwrap() == &fingerprint) + ); + assert!(matches!(&policy.contribution, Satisfaction::None)); + + let desc = descriptor!(wpkh(prvkey)).unwrap(); + let (wallet_desc, keymap) = desc.to_wallet_descriptor(Network::Testnet).unwrap(); + let single_key = wallet_desc.derive(ChildNumber::from_normal_idx(0).unwrap()); + let signers_container = Arc::new(SignersContainer::from(keymap)); + let policy = single_key + .extract_policy(signers_container) + .unwrap() + .unwrap(); + + assert!( + matches!(&policy.item, Signature(pk_or_f) if &pk_or_f.fingerprint.unwrap() == &fingerprint) + ); + assert!( + matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None) + ); + } + + // single key, 1 prv and 1 pub key descriptor, required 1 prv keys + #[test] + fn test_extract_policy_for_single_wsh_multi_complete_1of2() { + let (_prvkey0, pubkey0, fingerprint0) = setup_keys(TPRV0_STR); + let (prvkey1, _pubkey1, fingerprint1) = setup_keys(TPRV1_STR); + let desc = descriptor!(sh(multi 1, pubkey0, prvkey1)).unwrap(); + let (wallet_desc, keymap) = desc.to_wallet_descriptor(Network::Testnet).unwrap(); + let single_key = wallet_desc.derive(ChildNumber::from_normal_idx(0).unwrap()); + let signers_container = Arc::new(SignersContainer::from(keymap)); + let policy = single_key + .extract_policy(signers_container) + .unwrap() + .unwrap(); + + assert!( + matches!(&policy.item, Multisig { keys, threshold } if threshold == &1 + && &keys[0].fingerprint.unwrap() == &fingerprint0 + && &keys[1].fingerprint.unwrap() == &fingerprint1) + ); + assert!( + matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions } if n == &2 + && m == &1 + && items.len() == 2 + && conditions.contains_key(&vec![0]) + && conditions.contains_key(&vec![1]) + ) + ); + } + + // test ExtractPolicy trait with descriptors containing timelocks in a thresh() + + #[test] + fn test_extract_policy_for_wsh_multi_timelock() { + let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR); + let (_prvkey1, pubkey1, _fingerprint1) = setup_keys(TPRV1_STR); + let sequence = 50; + let desc = descriptor!(wsh ( + thresh 2, (pk prvkey0), (+s pk pubkey1), (+s+d+v older sequence) + )) + .unwrap(); + + let (wallet_desc, keymap) = desc.to_wallet_descriptor(Network::Testnet).unwrap(); + let signers_container = Arc::new(SignersContainer::from(keymap)); + let policy = wallet_desc + .extract_policy(signers_container) + .unwrap() + .unwrap(); + + assert!( + matches!(&policy.item, Thresh { items, threshold } if items.len() == 3 && threshold == &2) + ); + + assert!( + matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions } if n == &3 + && m == &2 + && items.len() == 3 + && conditions.get(&vec![0,1]).unwrap().iter().next().unwrap().csv.is_none() + && conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(sequence) + && conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(sequence) + ) + ); + } + + // - mixed timelocks should fail + + // #[test] + // fn test_extract_policy_for_wsh_mixed_timelocks() { + // let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR); + // let locktime_threshold = 500000000; // if less than this means block number, else block time in seconds + // let locktime_blocks = 100; + // let locktime_seconds = locktime_blocks + locktime_threshold; + // let desc = descriptor!(sh (and_v (+v pk prvkey0), (and_v (+v after locktime_seconds), (after locktime_blocks)))).unwrap(); + // let (wallet_desc, keymap) = desc.to_wallet_descriptor(Network::Testnet).unwrap(); + // let signers_container = Arc::new(SignersContainer::from(keymap)); + // let policy = wallet_desc + // .extract_policy(signers_container) + // .unwrap() + // .unwrap(); + // + // println!("desc policy = {:?}", policy); // TODO remove + // + // // TODO how should this fail with mixed timelocks? + // } + + // - multiple timelocks of the same type should be correctly merged together + + // #[test] + // fn test_extract_policy_for_multiple_same_timelocks() { + // let (prvkey0, _pubkey0, _fingerprint0) = setup_keys(TPRV0_STR); + // let locktime_blocks0 = 100; + // let locktime_blocks1 = 200; + // let desc = descriptor!(sh (and_v (+v pk prvkey0), (and_v (+v after locktime_blocks0), (after locktime_blocks1)))).unwrap(); + // let (wallet_desc, keymap) = desc.to_wallet_descriptor(Network::Testnet).unwrap(); + // let signers_container = Arc::new(SignersContainer::from(keymap)); + // let policy = wallet_desc + // .extract_policy(signers_container) + // .unwrap() + // .unwrap(); + // + // println!("desc policy = {:?}", policy); // TODO remove + // + // // TODO how should this merge timelocks? + // + // let (prvkey1, _pubkey1, _fingerprint1) = setup_keys(TPRV0_STR); + // let locktime_seconds0 = 500000100; + // let locktime_seconds1 = 500000200; + // let desc = descriptor!(sh (and_v (+v pk prvkey1), (and_v (+v after locktime_seconds0), (after locktime_seconds1)))).unwrap(); + // let (wallet_desc, keymap) = desc.to_wallet_descriptor(Network::Testnet).unwrap(); + // let signers_container = Arc::new(SignersContainer::from(keymap)); + // let policy = wallet_desc + // .extract_policy(signers_container) + // .unwrap() + // .unwrap(); + // + // println!("desc policy = {:?}", policy); // TODO remove + // + // // TODO how should this merge timelocks? + // } +} diff --git a/src/descriptor/template.rs b/src/descriptor/template.rs index b167ca45..de4dd620 100644 --- a/src/descriptor/template.rs +++ b/src/descriptor/template.rs @@ -422,3 +422,273 @@ macro_rules! expand_make_bipxx { expand_make_bipxx!(legacy, Legacy); expand_make_bipxx!(segwit_v0, Segwitv0); + +#[cfg(test)] +mod test { + // test existing descriptor templates, make sure they are expanded to the right descriptors + + use super::*; + use crate::descriptor::DescriptorMeta; + use crate::keys::{KeyError, ValidNetworks}; + use bitcoin::hashes::core::str::FromStr; + use bitcoin::network::constants::Network::Regtest; + use bitcoin::util::bip32::ChildNumber; + use miniscript::descriptor::{DescriptorPublicKey, KeyMap}; + use miniscript::Descriptor; + + // verify template descriptor generates expected address(es) + fn check( + desc: Result<(Descriptor, KeyMap, ValidNetworks), KeyError>, + is_witness: bool, + is_fixed: bool, + expected: &[&str], + ) { + let (desc, _key_map, _networks) = desc.unwrap(); + assert_eq!(desc.is_witness(), is_witness); + assert_eq!(desc.is_fixed(), is_fixed); + for i in 0..expected.len() { + let index = i as u32; + let child_desc = if desc.is_fixed() { + desc.clone() + } else { + desc.derive(ChildNumber::from_normal_idx(index).unwrap()) + }; + let address = child_desc.address(Regtest).unwrap(); + assert_eq!(address.to_string(), *expected.get(i).unwrap()); + } + } + + // P2PKH + #[test] + fn test_p2ph_template() { + let prvkey = + bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um") + .unwrap(); + check( + P2PKH(prvkey).build(), + false, + true, + &["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"], + ); + + let pubkey = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + check( + P2PKH(pubkey).build(), + false, + true, + &["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"], + ); + } + + // P2WPKH-P2SH `sh(wpkh(key))` + #[test] + fn test_p2wphp2sh_template() { + let prvkey = + bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um") + .unwrap(); + check( + P2WPKH_P2SH(prvkey).build(), + true, + true, + &["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"], + ); + + let pubkey = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + check( + P2WPKH_P2SH(pubkey).build(), + true, + true, + &["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"], + ); + } + + // P2WPKH `wpkh(key)` + #[test] + fn test_p2wph_template() { + let prvkey = + bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um") + .unwrap(); + check( + P2WPKH(prvkey).build(), + true, + true, + &["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"], + ); + + let pubkey = bitcoin::PublicKey::from_str( + "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", + ) + .unwrap(); + check( + P2WPKH(pubkey).build(), + true, + true, + &["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"], + ); + } + + // BIP44 `pkh(key/44'/0'/0'/{0,1}/*)` + #[test] + fn test_bip44_template() { + let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + check( + BIP44(prvkey, ScriptType::External).build(), + false, + false, + &[ + "n453VtnjDHPyDt2fDstKSu7A3YCJoHZ5g5", + "mvfrrumXgTtwFPWDNUecBBgzuMXhYM7KRP", + "mzYvhRAuQqbdSKMVVzXNYyqihgNdRadAUQ", + ], + ); + check( + BIP44(prvkey, ScriptType::Internal).build(), + false, + false, + &[ + "muHF98X9KxEzdKrnFAX85KeHv96eXopaip", + "n4hpyLJE5ub6B5Bymv4eqFxS5KjrewSmYR", + "mgvkdv1ffmsXd2B1sRKQ5dByK3SzpG42rA", + ], + ); + } + + // BIP44 public `pkh(key/{0,1}/*)` + #[test] + fn test_bip44_public_template() { + let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap(); + let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap(); + check( + BIP44Public(pubkey, fingerprint, ScriptType::External).build(), + false, + false, + &[ + "miNG7dJTzJqNbFS19svRdTCisC65dsubtR", + "n2UqaDbCjWSFJvpC84m3FjUk5UaeibCzYg", + "muCPpS6Ue7nkzeJMWDViw7Lkwr92Yc4K8g", + ], + ); + check( + BIP44Public(pubkey, fingerprint, ScriptType::Internal).build(), + false, + false, + &[ + "moDr3vJ8wpt5nNxSK55MPq797nXJb2Ru9H", + "ms7A1Yt4uTezT2XkefW12AvLoko8WfNJMG", + "mhYiyat2rtEnV77cFfQsW32y1m2ceCGHPo", + ], + ); + } + + // BIP49 `sh(wpkh(key/49'/0'/0'/{0,1}/*))` + #[test] + fn test_bip49_template() { + let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + check( + BIP49(prvkey, ScriptType::External).build(), + true, + false, + &[ + "2N9bCAJXGm168MjVwpkBdNt6ucka3PKVoUV", + "2NDckYkqrYyDMtttEav5hB3Bfw9EGAW5HtS", + "2NAFTVtksF9T4a97M7nyCjwUBD24QevZ5Z4", + ], + ); + check( + BIP49(prvkey, ScriptType::Internal).build(), + true, + false, + &[ + "2NB3pA8PnzJLGV8YEKNDFpbViZv3Bm1K6CG", + "2NBiX2Wzxngb5rPiWpUiJQ2uLVB4HBjFD4p", + "2NA8ek4CdQ6aMkveYF6AYuEYNrftB47QGTn", + ], + ); + } + + // BIP49 public `sh(wpkh(key/{0,1}/*))` + #[test] + fn test_bip49_public_template() { + let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap(); + let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap(); + check( + BIP49Public(pubkey, fingerprint, ScriptType::External).build(), + true, + false, + &[ + "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt", + "2NCTQfJ1sZa3wQ3pPseYRHbaNEpC3AquEfX", + "2MveFxAuC8BYPzTybx7FxSzW8HSd8ATT4z7", + ], + ); + check( + BIP49Public(pubkey, fingerprint, ScriptType::Internal).build(), + true, + false, + &[ + "2NF2vttKibwyxigxtx95Zw8K7JhDbo5zPVJ", + "2Mtmyd8taksxNVWCJ4wVvaiss7QPZGcAJuH", + "2NBs3CTVYPr1HCzjB4YFsnWCPCtNg8uMEfp", + ], + ); + } + + // BIP84 `wpkh(key/84'/0'/0'/{0,1}/*)` + #[test] + fn test_bip84_template() { + let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); + check( + BIP84(prvkey, ScriptType::External).build(), + true, + false, + &[ + "bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s", + "bcrt1qx0v6zgfwe50m4kqc58cqzcyem7ay2sfl3gvqhp", + "bcrt1q4h7fq9zhxst6e69p3n882nfj649l7w9g3zccfp", + ], + ); + check( + BIP84(prvkey, ScriptType::Internal).build(), + true, + false, + &[ + "bcrt1qtrwtz00wxl69e5xex7amy4xzlxkaefg3gfdkxa", + "bcrt1qqqasfhxpkkf7zrxqnkr2sfhn74dgsrc3e3ky45", + "bcrt1qpks7n0gq74hsgsz3phn5vuazjjq0f5eqhsgyce", + ], + ); + } + + // BIP84 public `wpkh(key/{0,1}/*)` + #[test] + fn test_bip84_public_template() { + let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap(); + let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap(); + check( + BIP84Public(pubkey, fingerprint, ScriptType::External).build(), + true, + false, + &[ + "bcrt1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2prcdvd0h", + "bcrt1q3lncdlwq3lgcaaeyruynjnlccr0ve0kakh6ana", + "bcrt1qt9800y6xl3922jy3uyl0z33jh5wfpycyhcylr9", + ], + ); + check( + BIP84Public(pubkey, fingerprint, ScriptType::Internal).build(), + true, + false, + &[ + "bcrt1qm6wqukenh7guu792lj2njgw9n78cmwsy8xy3z2", + "bcrt1q694twxtjn4nnrvnyvra769j0a23rllj5c6cgwp", + "bcrt1qhlac3c5ranv5w5emlnqs7wxhkxt8maelylcarp", + ], + ); + } +} diff --git a/src/keys/mod.rs b/src/keys/mod.rs index d8fe8bda..34b29406 100644 --- a/src/keys/mod.rs +++ b/src/keys/mod.rs @@ -68,6 +68,7 @@ pub fn merge_networks(a: &ValidNetworks, b: &ValidNetworks) -> ValidNetworks { } /// Container for public or secret keys +#[derive(Debug)] pub enum DescriptorKey { #[doc(hidden)] Public(DescriptorPublicKey, ValidNetworks, PhantomData),