From da8d16cc7740254ffb26e737903b21b97c61e46c Mon Sep 17 00:00:00 2001 From: Leonardo Comandini Date: Thu, 12 Oct 2023 16:50:31 +0200 Subject: [PATCH 1/4] CT descriptor: remove generic from blinding key The generic was only used by the `Bare` variant, which was inconsistent with its corresponding "secret" variant, `View`. The generic could be used to handle the conversion from `ConfidentialDescriptor` to `Descriptor`, specifically making sure that the descriptor blinding key does not have wildcards. For the `Bare` variant this could be done, but this cannot happen for the `View` variant, since we don't have the definite version of `DescriptorSecretKey`. --- src/confidential/mod.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 6f3e21d6..12d0221b 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -25,23 +25,23 @@ use std::fmt; use elements::secp256k1_zkp; use crate::descriptor::checksum::{desc_checksum, verify_checksum}; -use crate::descriptor::DescriptorSecretKey; +use crate::descriptor::{DescriptorSecretKey, DescriptorPublicKey}; use crate::expression::FromTree; use crate::extensions::{CovExtArgs, CovenantExt, Extension, ParseableExt}; use crate::{expression, Error, MiniscriptKey, ToPublicKey}; /// A description of a blinding key #[derive(Clone, PartialEq, Eq, Debug)] -pub enum Key { +pub enum Key { /// Blinding key is computed using SLIP77 with the given master key Slip77(slip77::MasterBlindingKey), /// Blinding key is given directly - Bare(Pk), + Bare(DescriptorPublicKey), /// Blinding key is given directly, as a secret key View(DescriptorSecretKey), } -impl fmt::Display for Key { +impl fmt::Display for Key { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Key::Slip77(data) => write!(f, "slip77({})", data), @@ -51,7 +51,7 @@ impl fmt::Display for Key { } } -impl Key { +impl Key { fn to_public_key( &self, secp: &secp256k1_zkp::Secp256k1, @@ -59,7 +59,7 @@ impl Key { ) -> secp256k1_zkp::PublicKey { match *self { Key::Slip77(ref mbk) => mbk.blinding_key(secp, spk), - Key::Bare(ref pk) => bare::tweak_key(secp, spk, pk), + Key::Bare(ref pk) => bare::tweak_key(secp, spk, &pk.clone().at_derivation_index(0).expect("FIXME deal with derivation paths properly")), Key::View(ref sk) => bare::tweak_key(secp, spk, &sk.to_public(secp).expect("view keys cannot be multipath keys").at_derivation_index(0).expect("FIXME deal with derivation paths properly")), } } @@ -69,7 +69,7 @@ impl Key { #[derive(Clone, PartialEq, Eq, Debug)] pub struct Descriptor> { /// The blinding key - pub key: Key, + pub key: Key, /// The script descriptor pub descriptor: crate::Descriptor, } @@ -137,7 +137,7 @@ impl_from_str!( ("slip77", _) => return Err(Error::BadDescriptor( "slip77() must have exactly one argument".to_owned() )), - _ => expression::terminal(keyexpr, Pk::from_str).map(Key::Bare) + _ => expression::terminal(keyexpr, DescriptorPublicKey::from_str).map(Key::Bare) .or_else(|_| expression::terminal(keyexpr, DescriptorSecretKey::from_str).map(Key::View))?, }, descriptor: crate::Descriptor::from_tree(&top.args[1])?, @@ -161,7 +161,7 @@ mod tests { // taken from libwally src/test/test_confidential_addr.py let mut addr = Address::from_str("Q7qcjTLsYGoMA7TjUp97R6E6AM5VKqBik6").unwrap(); let key = Key::Bare( - bitcoin::PublicKey::from_str( + DescriptorPublicKey::from_str( "02dce16018bbbb8e36de7b394df5b5166e9adb7498be7d881a85a09aeecf76b623", ) .unwrap(), @@ -174,7 +174,7 @@ mod tests { } struct ConfidentialTest { - key: Key, + key: Key, descriptor: crate::Descriptor, descriptor_str: String, conf_addr: &'static str, @@ -231,7 +231,7 @@ mod tests { let secp = secp256k1_zkp::Secp256k1::new(); // CT key used for bare keys - let ct_key = DefiniteDescriptorKey::from_str( + let ct_key = DescriptorPublicKey::from_str( "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL", ) .unwrap(); @@ -370,7 +370,7 @@ mod tests { let view_key = DescriptorSecretKey::from_str( "xprv9s21ZrQH143K28NgQ7bHCF61hy9VzwquBZvpzTwXLsbmQLRJ6iV9k2hUBRt5qzmBaSpeMj5LdcsHaXJvM7iFEivPryRcL8irN7Na9p65UUb", ).unwrap(); - let ct_key = view_key.to_public(&secp).unwrap().at_derivation_index(0).unwrap(); // FIXME figure out derivation + let ct_key = view_key.to_public(&secp).unwrap(); let spk_key = DefiniteDescriptorKey::from_str( "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", ) From 8ffda8c8821cf83086e93e6befdf6023bd209f59 Mon Sep 17 00:00:00 2001 From: Leonardo Comandini Date: Fri, 13 Oct 2023 10:25:02 +0200 Subject: [PATCH 2/4] CT descriptor: implement at_derivation_index --- src/confidential/mod.rs | 51 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 12d0221b..50b4f69f 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -22,10 +22,14 @@ pub mod slip77; use std::fmt; +use bitcoin::bip32; use elements::secp256k1_zkp; use crate::descriptor::checksum::{desc_checksum, verify_checksum}; -use crate::descriptor::{DescriptorSecretKey, DescriptorPublicKey}; +use crate::descriptor::{ + ConversionError, DefiniteDescriptorKey, DescriptorSecretKey, DescriptorPublicKey, + DescriptorXKey, Wildcard +}; use crate::expression::FromTree; use crate::extensions::{CovExtArgs, CovenantExt, Extension, ParseableExt}; use crate::{expression, Error, MiniscriptKey, ToPublicKey}; @@ -82,6 +86,51 @@ impl Descriptor { } } +impl Descriptor { + /// Replaces all wildcards (i.e. `/*`) in the descriptor and the descriptor blinding key + /// with a particular derivation index, turning it into a *definite* descriptor. + /// + /// # Errors + /// - If index ≥ 2^31 + pub fn at_derivation_index(&self, index: u32) -> Result, ConversionError> { + let definite_key = match self.key.clone() { + Key::Slip77(k) => Key::Slip77(k), + Key::Bare(k) => Key::Bare(k.at_derivation_index(index)?.into_descriptor_public_key()), + Key::View(k) => Key::View(match k { + // Consider implementing DescriptorSecretKey::at_derivation_index + DescriptorSecretKey::Single(_) => k, + DescriptorSecretKey::XPrv(xprv) => { + let derivation_path = match xprv.wildcard { + Wildcard::None => xprv.derivation_path, + Wildcard::Unhardened => xprv.derivation_path.into_child( + bip32::ChildNumber::from_normal_idx(index) + .ok() + .ok_or(ConversionError::HardenedChild)?, + ), + Wildcard::Hardened => xprv.derivation_path.into_child( + bip32::ChildNumber::from_hardened_idx(index) + .ok() + .ok_or(ConversionError::HardenedChild)?, + ), + }; + DescriptorSecretKey::XPrv(DescriptorXKey { + origin: xprv.origin, + xkey: xprv.xkey, + derivation_path, + wildcard: Wildcard::None, + }) + }, + DescriptorSecretKey::MultiXPrv(_) => return Err(ConversionError::MultiKey), + }), + }; + let definite_descriptor = self.descriptor.at_derivation_index(index)?; + Ok(Descriptor{ + key: definite_key, + descriptor: definite_descriptor, + }) + } +} + impl Descriptor { /// Obtains the unblinded address for this descriptor. pub fn unconfidential_address( From eafc28d2e8c2dfb9a5b5664ac36b28faec663a53 Mon Sep 17 00:00:00 2001 From: Leonardo Comandini Date: Fri, 13 Oct 2023 11:52:53 +0200 Subject: [PATCH 3/4] CT descriptor: handle bare/view keys with wildcards --- src/confidential/mod.rs | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 50b4f69f..2c0084be 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -60,11 +60,35 @@ impl Key { &self, secp: &secp256k1_zkp::Secp256k1, spk: &elements::Script, - ) -> secp256k1_zkp::PublicKey { + ) -> Result { match *self { - Key::Slip77(ref mbk) => mbk.blinding_key(secp, spk), - Key::Bare(ref pk) => bare::tweak_key(secp, spk, &pk.clone().at_derivation_index(0).expect("FIXME deal with derivation paths properly")), - Key::View(ref sk) => bare::tweak_key(secp, spk, &sk.to_public(secp).expect("view keys cannot be multipath keys").at_derivation_index(0).expect("FIXME deal with derivation paths properly")), + Key::Slip77(ref mbk) => Ok(mbk.blinding_key(secp, spk)), + Key::Bare(ref pk) => { + if pk.is_multipath() { + Err(Error::Unexpected("multipath blinding key".into())) + } else if pk.has_wildcard() { + Err(Error::Unexpected("wildcard blinding key".into())) + } else { + // Convert into a DefiniteDescriptorKey, note that we are deriving the xpub + // since there is not wildcard. + // Consider adding DescriptorPublicKey::to_definite_descriptor + let pk = pk.clone().at_derivation_index(0).expect("single or xpub without wildcards"); + Ok(bare::tweak_key(secp, spk, &pk)) + } + }, + Key::View(ref sk) => { + if sk.is_multipath() { + Err(Error::Unexpected("multipath blinding key".into())) + } else { + let pk = sk.to_public(secp).expect("single or xprv"); + if pk.has_wildcard() { + Err(Error::Unexpected("wildcard blinding key".into())) + } else { + let pk = pk.at_derivation_index(0).expect("single or xprv without wildcards"); + Ok(bare::tweak_key(secp, spk, &pk)) + } + } + }, } } } @@ -148,7 +172,7 @@ impl Descriptor Result { let spk = self.descriptor.script_pubkey(); self.descriptor - .blinded_address(self.key.to_public_key(secp, &spk), params) + .blinded_address(self.key.to_public_key(secp, &spk)?, params) } } @@ -215,7 +239,7 @@ mod tests { ) .unwrap(), ); - addr.blinding_pubkey = Some(key.to_public_key(&secp, &addr.script_pubkey())); + addr.blinding_pubkey = Some(key.to_public_key(&secp, &addr.script_pubkey()).unwrap()); assert_eq!( addr.to_string(), "VTpt7krqRQPJwqe3XQXPg2cVdEKYVFbuprTr7es7pNRMe8mndnq2iYWddxJWYowhLAwoDF8QrZ1v2EXv" From f4be6afefc7a46af96fda083b1be7bc3fb261498 Mon Sep 17 00:00:00 2001 From: Leonardo Comandini Date: Fri, 13 Oct 2023 12:39:09 +0200 Subject: [PATCH 4/4] CT descriptor: test cases with wildcards --- src/confidential/mod.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 2c0084be..d1f2265e 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -469,4 +469,38 @@ mod tests { }; test.check(&secp); } + + #[test] + fn descriptor_wildcard() { + let secp = secp256k1_zkp::Secp256k1::new(); + let params = &elements::AddressParams::ELEMENTS; + + let xprv = "xprv9s21ZrQH143K28NgQ7bHCF61hy9VzwquBZvpzTwXLsbmQLRJ6iV9k2hUBRt5qzmBaSpeMj5LdcsHaXJvM7iFEivPryRcL8irN7Na9p65UUb"; + let xpub = "xpub661MyMwAqRbcEcT9W98HZP2kFzyzQQZkYnrRnrM8uD8kH8kSeFoQHq1x2iihLgC6PXGy5LrjCL66uSNhJ8pwjfx2rMUTLWuRMns2EG9xnjs"; + let desc_view_str = format!("ct({}/*,elwpkh({}/*))#wk8ltq6h", xprv, xpub); + let desc_bare_str = format!("ct({}/*,elwpkh({}/*))#zzac2dpf", xpub, xpub); + let index = 1; + let conf_addr = "el1qqf6690fpw2y00hv5a84zsydjgztg2089d5xnll4k4cstzn63uvgudd907qpvlvvwd5ym9gx7j0v46elf23kfxunucm6ejjyk0"; + let unconf_addr = "ert1qkjhlqqk0kx8x6zdj5r0f8k2avl54gmyn7qjk2k"; + + let desc_view = Descriptor::::from_str(&desc_view_str).unwrap(); + let desc_bare = Descriptor::::from_str(&desc_bare_str).unwrap(); + let definite_desc_view = desc_view.at_derivation_index(index).unwrap(); + let definite_desc_bare = desc_bare.at_derivation_index(index).unwrap(); + assert_eq!(definite_desc_view.address(&secp, params).unwrap().to_string(), conf_addr.to_string()); + assert_eq!(definite_desc_bare.address(&secp, params).unwrap().to_string(), conf_addr.to_string()); + assert_eq!(definite_desc_view.unconfidential_address(params).unwrap().to_string(), unconf_addr.to_string()); + assert_eq!(definite_desc_bare.unconfidential_address(params).unwrap().to_string(), unconf_addr.to_string()); + + // It's not possible to get an address if the blinding key has a wildcard, + // because the descriptor blinding key is not *definite*, + // but we can't enforce this with the Descriptor generic. + let desc_view_str = format!("ct({}/*,elwpkh({}))#ls6mx2ac", xprv, xpub); + let desc_view = Descriptor::::from_str(&desc_view_str).unwrap(); + assert_eq!(desc_view.address(&secp, params).unwrap_err(), Error::Unexpected("wildcard blinding key".into())); + + let desc_bare_str = format!("ct({}/*,elwpkh({}))#czkz0hwn", xpub, xpub); + let desc_bare = Descriptor::::from_str(&desc_bare_str).unwrap(); + assert_eq!(desc_bare.address(&secp, params).unwrap_err(), Error::Unexpected("wildcard blinding key".into())); + } }