diff --git a/examples/repl.rs b/examples/repl.rs index 5a78bed3..5eaf3f42 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -101,7 +101,12 @@ async fn main() { continue; } - cli::handle_matches(&Arc::clone(&wallet), matches.unwrap()).await; + if let Some(s) = cli::handle_matches(&Arc::clone(&wallet), matches.unwrap()) + .await + .unwrap() + { + println!("{}", s); + } } Err(ReadlineError::Interrupted) => continue, Err(ReadlineError::Eof) => break, @@ -114,6 +119,8 @@ async fn main() { // rl.save_history("history.txt").unwrap(); } else { - cli::handle_matches(&wallet, matches).await; + if let Some(s) = cli::handle_matches(&wallet, matches).await.unwrap() { + println!("{}", s); + } } } diff --git a/src/cli.rs b/src/cli.rs index 5b24544c..9dc37a32 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -119,6 +119,10 @@ pub fn make_cli_subcommands<'a, 'b>() -> App<'a, 'b> { SubCommand::with_name("policies") .about("Returns the available spending policies for the descriptor") ) + .subcommand( + SubCommand::with_name("public_descriptor") + .about("Returns the public version of the wallet's descriptor(s)") + ) .subcommand( SubCommand::with_name("sign") .about("Signs and tries to finalize a PSBT") @@ -271,6 +275,20 @@ where serde_json::to_string(&wallet.policies(ScriptType::External)?).unwrap(), serde_json::to_string(&wallet.policies(ScriptType::Internal)?).unwrap(), ))) + } else if let Some(_sub_matches) = matches.subcommand_matches("public_descriptor") { + let external = match wallet.public_descriptor(ScriptType::External)? { + Some(desc) => format!("{}", desc), + None => "".into(), + }; + let internal = match wallet.public_descriptor(ScriptType::Internal)? { + Some(desc) => format!("{}", desc), + None => "".into(), + }; + + Ok(Some(format!( + "External: {}\nInternal:{}", + external, internal + ))) } else if let Some(sub_matches) = matches.subcommand_matches("sign") { let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap(); let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); diff --git a/src/descriptor/extended_key.rs b/src/descriptor/extended_key.rs index 4645eea0..70cc208e 100644 --- a/src/descriptor/extended_key.rs +++ b/src/descriptor/extended_key.rs @@ -199,6 +199,15 @@ impl FromStr for DescriptorExtendedKey { } }; + if secret.is_none() + && path.into_iter().any(|child| match child { + ChildNumber::Hardened { .. } => true, + _ => false, + }) + { + return Err(super::Error::HardenedDerivationOnXpub); + } + Ok(DescriptorExtendedKey { master_fingerprint, master_derivation, diff --git a/src/descriptor/keys.rs b/src/descriptor/keys.rs new file mode 100644 index 00000000..7ff028d9 --- /dev/null +++ b/src/descriptor/keys.rs @@ -0,0 +1,181 @@ +use bitcoin::secp256k1::{All, Secp256k1}; +use bitcoin::{PrivateKey, PublicKey}; + +use bitcoin::util::bip32::{ + ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint, +}; + +use super::error::Error; +use super::extended_key::DerivationIndex; +use super::DescriptorExtendedKey; + +pub(super) trait Key: std::fmt::Debug + std::fmt::Display { + fn fingerprint(&self, secp: &Secp256k1) -> Option; + fn as_public_key(&self, secp: &Secp256k1, index: Option) -> Result; + fn as_secret_key(&self) -> Option; + fn xprv(&self) -> Option; + fn full_path(&self, index: u32) -> Option; + fn is_fixed(&self) -> bool; + + fn has_secret(&self) -> bool { + self.xprv().is_some() || self.as_secret_key().is_some() + } + + fn public(&self, secp: &Secp256k1) -> Result, Error> { + Ok(Box::new(self.as_public_key(secp, None)?)) + } +} + +impl Key for PublicKey { + fn fingerprint(&self, _secp: &Secp256k1) -> Option { + None + } + + fn as_public_key( + &self, + _secp: &Secp256k1, + _index: Option, + ) -> Result { + Ok(PublicKey::clone(self)) + } + + fn as_secret_key(&self) -> Option { + None + } + + fn xprv(&self) -> Option { + None + } + + fn full_path(&self, _index: u32) -> Option { + None + } + + fn is_fixed(&self) -> bool { + true + } +} + +impl Key for PrivateKey { + fn fingerprint(&self, _secp: &Secp256k1) -> Option { + None + } + + fn as_public_key( + &self, + secp: &Secp256k1, + _index: Option, + ) -> Result { + Ok(self.public_key(secp)) + } + + fn as_secret_key(&self) -> Option { + Some(PrivateKey::clone(self)) + } + + fn xprv(&self) -> Option { + None + } + + fn full_path(&self, _index: u32) -> Option { + None + } + + fn is_fixed(&self) -> bool { + true + } +} + +impl Key for DescriptorExtendedKey { + fn fingerprint(&self, secp: &Secp256k1) -> Option { + if let Some(fing) = self.master_fingerprint { + Some(fing.clone()) + } else { + Some(self.root_xpub(secp).fingerprint()) + } + } + + fn as_public_key(&self, secp: &Secp256k1, index: Option) -> Result { + Ok(self.derive_xpub(secp, index.unwrap_or(0))?.public_key) + } + + fn public(&self, secp: &Secp256k1) -> Result, Error> { + if self.final_index == DerivationIndex::Hardened { + return Err(Error::HardenedDerivationOnXpub); + } + + if self.xprv().is_none() { + return Ok(Box::new(self.clone())); + } + + // copy the part of the path that can be derived on the xpub + let path = self + .path + .into_iter() + .rev() + .take_while(|child| match child { + ChildNumber::Normal { .. } => true, + _ => false, + }) + .cloned() + .collect::>(); + // take the prefix that has to be derived on the xprv + let master_derivation_add = self + .path + .into_iter() + .take(self.path.as_ref().len() - path.len()) + .cloned() + .collect::>(); + let has_derived = !master_derivation_add.is_empty(); + + let derived_xprv = self + .secret + .as_ref() + .unwrap() + .derive_priv(secp, &master_derivation_add)?; + let pubkey = ExtendedPubKey::from_private(secp, &derived_xprv); + + let master_derivation = self + .master_derivation + .as_ref() + .map_or(vec![], |path| path.as_ref().to_vec()) + .into_iter() + .chain(master_derivation_add.into_iter()) + .collect::>(); + let master_derivation = match &master_derivation[..] { + &[] => None, + child_vec => Some(child_vec.into()), + }; + + let master_fingerprint = match self.master_fingerprint { + Some(desc) => Some(desc.clone()), + None if has_derived => Some(self.fingerprint(secp).unwrap()), + _ => None, + }; + + Ok(Box::new(DescriptorExtendedKey { + master_fingerprint, + master_derivation, + pubkey, + secret: None, + path: path.into(), + final_index: self.final_index, + })) + } + + fn as_secret_key(&self) -> Option { + None + } + + fn xprv(&self) -> Option { + self.secret + } + + fn full_path(&self, index: u32) -> Option { + Some(self.full_path(index)) + } + + fn is_fixed(&self) -> bool { + self.final_index == DerivationIndex::Fixed + } +} diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 71d9ae16..a2db112d 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -19,13 +19,16 @@ use crate::psbt::utils::PSBTUtils; pub mod checksum; pub mod error; pub mod extended_key; +mod keys; pub mod policy; pub use self::checksum::get_checksum; -pub use self::error::Error; +use self::error::Error; pub use self::extended_key::{DerivationIndex, DescriptorExtendedKey}; pub use self::policy::Policy; +use self::keys::Key; + trait MiniscriptExtractPolicy { fn extract_policy( &self, @@ -105,109 +108,6 @@ where } } -trait Key: std::fmt::Debug { - fn fingerprint(&self, secp: &Secp256k1) -> Option; - fn as_public_key(&self, secp: &Secp256k1, index: Option) -> Result; - fn as_secret_key(&self) -> Option; - fn xprv(&self) -> Option; - fn full_path(&self, index: u32) -> Option; - fn is_fixed(&self) -> bool; - - fn has_secret(&self) -> bool { - self.xprv().is_some() || self.as_secret_key().is_some() - } -} - -impl Key for PublicKey { - fn fingerprint(&self, _secp: &Secp256k1) -> Option { - None - } - - fn as_public_key( - &self, - _secp: &Secp256k1, - _index: Option, - ) -> Result { - Ok(PublicKey::clone(self)) - } - - fn as_secret_key(&self) -> Option { - None - } - - fn xprv(&self) -> Option { - None - } - - fn full_path(&self, _index: u32) -> Option { - None - } - - fn is_fixed(&self) -> bool { - true - } -} - -impl Key for PrivateKey { - fn fingerprint(&self, _secp: &Secp256k1) -> Option { - None - } - - fn as_public_key( - &self, - secp: &Secp256k1, - _index: Option, - ) -> Result { - Ok(self.public_key(secp)) - } - - fn as_secret_key(&self) -> Option { - Some(PrivateKey::clone(self)) - } - - fn xprv(&self) -> Option { - None - } - - fn full_path(&self, _index: u32) -> Option { - None - } - - fn is_fixed(&self) -> bool { - true - } -} - -impl Key for DescriptorExtendedKey { - fn fingerprint(&self, secp: &Secp256k1) -> Option { - if let Some(fing) = self.master_fingerprint { - Some(fing.clone()) - } else { - Some(self.root_xpub(secp).fingerprint()) - } - } - - fn as_public_key(&self, secp: &Secp256k1, index: Option) -> Result { - Ok(self.derive_xpub(secp, index.unwrap_or(0))?.public_key) - } - - fn as_secret_key(&self) -> Option { - None - } - - fn xprv(&self) -> Option { - self.secret - } - - fn full_path(&self, index: u32) -> Option { - Some(self.full_path(index)) - } - - fn is_fixed(&self) -> bool { - self.final_index == DerivationIndex::Fixed - } -} - #[serde(try_from = "&str", into = "String")] #[derive(Debug, Serialize, Deserialize)] pub struct ExtendedDescriptor { @@ -221,6 +121,12 @@ pub struct ExtendedDescriptor { ctx: Secp256k1, } +impl fmt::Display for ExtendedDescriptor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.internal) + } +} + impl std::clone::Clone for ExtendedDescriptor { fn clone(&self) -> Self { Self { @@ -421,6 +327,35 @@ impl ExtendedDescriptor { _ => false, } } + + pub fn as_public_version(&self) -> Result { + let keys: RefCell>> = RefCell::new(BTreeMap::new()); + + let translatefpk = |string: &String| -> Result<_, Error> { + let public = self.keys.get(string).unwrap().public(&self.ctx)?; + + let result = format!("{}", public); + keys.borrow_mut().insert(string.clone(), public); + + Ok(result) + }; + let translatefpkh = |string: &String| -> Result<_, Error> { + let public = self.keys.get(string).unwrap().public(&self.ctx)?; + + let result = format!("{}", public); + keys.borrow_mut().insert(string.clone(), public); + + Ok(result) + }; + + let internal = self.internal.translate_pk(translatefpk, translatefpkh)?; + + Ok(ExtendedDescriptor { + internal, + keys: keys.into_inner(), + ctx: self.ctx.clone(), + }) + } } impl ExtractPolicy for ExtendedDescriptor { diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index dac90aee..c0451230 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -496,6 +496,17 @@ where } } + pub fn public_descriptor( + &self, + script_type: ScriptType, + ) -> Result, Error> { + match (script_type, self.change_descriptor.as_ref()) { + (ScriptType::External, _) => Ok(Some(self.descriptor.as_public_version()?)), + (ScriptType::Internal, None) => Ok(None), + (ScriptType::Internal, Some(desc)) => Ok(Some(desc.as_public_version()?)), + } + } + // Internals #[cfg(not(target_arch = "wasm32"))]