From 3a9c3ad9ca4ab9db3b4aad80dca7213d0c1a0b95 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Sun, 23 Jun 2024 19:00:09 -0500 Subject: [PATCH] refact(chain): Use hash of SPK at index 0 as Descriptor unique ID --- crates/chain/src/descriptor_ext.rs | 136 +++++++++++++++++++++++++++-- crates/wallet/tests/psbt.rs | 4 +- 2 files changed, 133 insertions(+), 7 deletions(-) diff --git a/crates/chain/src/descriptor_ext.rs b/crates/chain/src/descriptor_ext.rs index e42d291bb7..2fbf74640d 100644 --- a/crates/chain/src/descriptor_ext.rs +++ b/crates/chain/src/descriptor_ext.rs @@ -1,8 +1,11 @@ use crate::{ - alloc::{string::ToString, vec::Vec}, + alloc::vec::Vec, miniscript::{Descriptor, DescriptorPublicKey}, }; +use bitcoin::bip32; use bitcoin::hashes::{hash_newtype, sha256, Hash}; +use miniscript::descriptor::{ConversionError, DescriptorXKey, Wildcard}; +use miniscript::{translate_hash_clone, TranslatePk, Translator}; hash_newtype! { /// Represents the ID of a descriptor, defined as the sha256 hash of @@ -35,10 +38,133 @@ impl DescriptorExt for Descriptor { .to_sat() } + // fn descriptor_id(&self) -> DescriptorId { + // let desc = self.to_string(); + // let desc_without_checksum = desc.split('#').next().expect("Must be here"); + // let descriptor_bytes = >::from(desc_without_checksum.as_bytes()); + // DescriptorId(sha256::Hash::hash(&descriptor_bytes)) + // } + + /// Use the hash of the SPK at index 0 as the descriptor's unique ID. fn descriptor_id(&self) -> DescriptorId { - let desc = self.to_string(); - let desc_without_checksum = desc.split('#').next().expect("Must be here"); - let descriptor_bytes = >::from(desc_without_checksum.as_bytes()); - DescriptorId(sha256::Hash::hash(&descriptor_bytes)) + let desc = self.clone(); + struct Derivator {} + impl Translator for Derivator { + fn pk( + &mut self, + pk: &DescriptorPublicKey, + ) -> Result { + let pk = pk.clone(); + match pk { + DescriptorPublicKey::XPub(xkey) => { + if xkey.wildcard == Wildcard::None { + //dbg!(&xkey); + let new_path_len = &xkey.derivation_path.len().saturating_sub(1); + let new_path = xkey.derivation_path[..*new_path_len].to_vec(); + let new_path = bip32::DerivationPath::from(new_path); + let new_xkey = DescriptorXKey { + origin: xkey.origin, + xkey: xkey.xkey, + derivation_path: new_path, + wildcard: Wildcard::Unhardened, + }; + //dbg!(&new_xkey); + Ok(DescriptorPublicKey::XPub(new_xkey)) + } else { + Ok(DescriptorPublicKey::XPub(xkey)) + } + } + pk => Ok(pk), + } + } + + translate_hash_clone!(DescriptorPublicKey, DescriptorPublicKey, ConversionError); + } + let translated_desc = desc.translate_pk(&mut Derivator {}).unwrap(); + let spk = translated_desc + .at_derivation_index(0) + .unwrap() + .script_pubkey(); + let spk_bytes = >::from(spk.as_bytes()); + DescriptorId(Hash::hash(&spk_bytes)) + } +} + +#[cfg(test)] +mod test { + use crate::DescriptorExt; + use bitcoin::bip32::{DerivationPath, Xpriv, Xpub}; + use bitcoin::key::Secp256k1; + use core::str::FromStr; + use miniscript::{Descriptor, DescriptorPublicKey}; + + #[test] + fn test_descriptor_id_equivalents() { + let desc_a = test_desc("44'/0'/1'", "2/3", false); + let desc_b = test_desc("44'/0'/1'/2", "3", false); + assert!(equal_ids(desc_a, desc_b)); + + let desc_a = test_desc("44'/0'/1'", "2/3", true); + let desc_b = test_desc("44'/0'/1'/2", "3", true); + assert!(equal_ids(desc_a, desc_b)); + + let desc_a = test_desc("44'/0'/1'/2", "3/4", false); + let desc_b = test_desc("44'/0'/1'/2", "3", true); + assert!(equal_ids(desc_a, desc_b)); + + let desc_a = test_desc("44'/0'/1'/2", "3", false); + let desc_b = test_desc("44'/0'/1'/2", "0", false); + assert!(equal_ids(desc_a, desc_b)); + + // found/fixed in wallet psbt test_psbt_fee_rate_with_missing_txout() + let secp = Secp256k1::new(); + let (desc_a, _keymap) = Descriptor::::parse_descriptor(&secp, "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/0)").unwrap(); + let (desc_b, _keymap) = Descriptor::::parse_descriptor(&secp, "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)").unwrap(); + assert!(equal_ids(desc_a, desc_b)); + + // this test fails, no good solution allowing two non-overlapping fixed indexes + // let desc_a = test_desc("44'/0'/1'/2", "3/4", false); + // let desc_b = test_desc("44'/0'/1'/2", "3/5", false); + // assert!(!equal_ids(desc_a, desc_b)); + } + + fn test_desc( + parent_path: &str, + extended_path: &str, + add_wildcard: bool, + ) -> Descriptor { + let secp = Secp256k1::new(); + + // Create master extended private key + let master_xprv = Xpriv::from_str("xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2").unwrap(); + + let parent_path = DerivationPath::from_str(parent_path).unwrap(); + let extended_path = DerivationPath::from_str(extended_path).unwrap(); + + let derived_xprv = master_xprv + .derive_priv(&secp, &parent_path) + .expect("derived xprv"); + let derived_xpub = Xpub::from_priv(&secp, &derived_xprv); + let descriptor = if add_wildcard { + format!( + "wpkh([{}/{}]{}/{}/*)", + &derived_xpub.parent_fingerprint, &parent_path, &derived_xpub, &extended_path + ) + } else { + format!( + "wpkh([{}/{}]{}/{})", + &derived_xpub.parent_fingerprint, &parent_path, &derived_xpub, &extended_path + ) + }; + Descriptor::from_str(&descriptor).unwrap() + } + + fn equal_ids( + desc_a: Descriptor, + desc_b: Descriptor, + ) -> bool { + let desc_a_id = desc_a.descriptor_id(); + let desc_b_id = desc_b.descriptor_id(); + desc_a_id == desc_b_id } } diff --git a/crates/wallet/tests/psbt.rs b/crates/wallet/tests/psbt.rs index 155bb143a9..044e898414 100644 --- a/crates/wallet/tests/psbt.rs +++ b/crates/wallet/tests/psbt.rs @@ -142,8 +142,8 @@ fn test_psbt_fee_rate_with_missing_txout() { assert!(wpkh_psbt.fee_amount().is_none()); assert!(wpkh_psbt.fee_rate().is_none()); - let desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/0)"; - let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)"; + let desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/0/*)"; + let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1/*)"; let (mut pkh_wallet, _) = get_funded_wallet_with_change(desc, change_desc); let addr = pkh_wallet.peek_address(KeychainKind::External, 0); let mut builder = pkh_wallet.build_tx();