From: AmosOO7 Date: Wed, 30 Apr 2025 20:31:23 +0000 (+0100) Subject: feat: add descriptor generation X-Git-Url: http://internal-gitweb-vhost/?a=commitdiff_plain;h=6c9c3a1bb13eada4af00e41be1ccded2928a6ba2;p=bdk-cli feat: add descriptor generation - Created Subcommnds for the descriptor command; generate - Created function to get descriptors from mnemonics --- diff --git a/src/commands.rs b/src/commands.rs index 54cddf0..09cbc5d 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -13,12 +13,11 @@ //! All subcommands are defined in the below enums. #![allow(clippy::large_enum_variant)] - use bdk_wallet::bitcoin::{ Address, Network, OutPoint, ScriptBuf, bip32::{DerivationPath, Xpriv}, }; -use clap::{Args, Parser, Subcommand, ValueEnum, value_parser}; +use clap::{Args, Parser, Subcommand, ValueEnum, builder::TypedValueParser, value_parser}; #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] use crate::utils::parse_proxy_auth; @@ -107,8 +106,15 @@ pub enum CliSubCommand { #[command(flatten)] wallet_opts: WalletOpts, }, + /// Output Descriptors operations. + /// + /// Generate output descriptors from either extended key (Xprv/Xpub) or mnemonic phrase. + /// This feature is intended for development and testing purposes only. + Descriptor { + #[clap(subcommand)] + subcommand: DescriptorSubCommand, + }, } - /// Wallet operation subcommands. #[derive(Debug, Subcommand, Clone, PartialEq)] pub enum WalletSubCommand { @@ -473,3 +479,27 @@ pub enum ReplSubCommand { /// Exit REPL loop. Exit, } +/// Subcommands for Key operations. +#[derive(Debug, Subcommand, Clone, PartialEq, Eq)] +pub enum DescriptorSubCommand { + /// Generate a descriptor + Generate { + /// Descriptor type (script type). + #[arg( + long = "type", + short = 't', + value_parser = clap::builder::PossibleValuesParser::new(["44", "49", "84", "86"]) + .map(|s| s.parse::().unwrap()), + default_value = "84" + )] + r#type: u8, + /// Enable multipath descriptors + #[arg(long = "multipath", short = 'm', default_value_t = false)] + multipath: bool, + /// Optional key input + key: Option, + }, + + /// Show info about a given descriptor + Info { descriptor: String }, +} diff --git a/src/error.rs b/src/error.rs index 5f548d9..3916f26 100644 --- a/src/error.rs +++ b/src/error.rs @@ -103,6 +103,30 @@ pub enum BDKCliError { #[cfg(feature = "cbf")] #[error("BDK-Kyoto update error: {0}")] KyotoUpdateError(#[from] bdk_kyoto::UpdateError), + + #[error("Mnemonic generation failed: {0}")] + MnemonicGenerationError(String), + + #[error("Xpriv creation failed: {0}")] + XprivCreationError(String), + + #[error("Descriptor parsing failed: {0}")] + DescriptorParsingError(String), + + #[error("Invalid extended key (xpub): {0}")] + InvalidKey(String), + + #[error("Invalid derivation path: {0}")] + InvalidDerivationPath(String), + + #[error("Unsupported script type: {0}")] + UnsupportedScriptType(u8), + + #[error("Descriptor key conversion failed: {0}")] + DescriptorKeyError(String), + + #[error("Invalid arguments: {0}")] + InvalidArguments(String), } impl From for BDKCliError { diff --git a/src/handlers.rs b/src/handlers.rs index d9d2cbe..9264e47 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -35,6 +35,14 @@ use bdk_wallet::keys::{ bip39::WordCount, }; use bdk_wallet::miniscript::miniscript; +use bdk_wallet::bitcoin::bip32::{DerivationPath, KeySource}; +use bdk_wallet::bitcoin::consensus::encode::serialize_hex; +use bdk_wallet::bitcoin::script::PushBytesBuf; +use bdk_wallet::bitcoin::Network; +use bdk_wallet::bitcoin::{secp256k1::Secp256k1, Txid}; +use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, Sequence}; +use bdk_wallet::descriptor::{Descriptor, Segwitv0}; +use bdk_wallet::keys::bip39::WordCount; #[cfg(feature = "sqlite")] use bdk_wallet::rusqlite::Connection; use bdk_wallet::{KeychainKind, SignOptions, Wallet}; @@ -42,25 +50,40 @@ use bdk_wallet::{KeychainKind, SignOptions, Wallet}; use bdk_wallet::{ descriptor::{Descriptor, Legacy, Miniscript}, miniscript::{Tap, descriptor::TapTree, policy::Concrete}, + descriptor::{Legacy, Miniscript}, + miniscript::policy::Concrete, }; use cli_table::{Cell, CellStruct, Style, Table, format::Justify}; use serde_json::json; +use bdk_wallet::{KeychainKind, SignOptions, Wallet}; + +#[cfg(feature = "electrum")] +use crate::utils::BlockchainClient::Electrum; +#[cfg(feature = "cbf")] +use bdk_kyoto::LightClient; +#[cfg(feature = "compiler")] +use bdk_wallet::bitcoin::XOnlyPublicKey; +use bdk_wallet::bitcoin::base64::prelude::*; +use bdk_wallet::keys::DescriptorKey::Secret; +use bdk_wallet::keys::{ + DerivableKey, DescriptorKey, DescriptorKey::Secret, DescriptorPublicKey, ExtendedKey, + GeneratableKey, GeneratedKey, bip39::WordCount, +}; +use bdk_wallet::miniscript::miniscript; +use serde_json::{Value, json}; use std::collections::BTreeMap; #[cfg(any(feature = "electrum", feature = "esplora"))] use std::collections::HashSet; use std::convert::TryFrom; +use std::fmt; #[cfg(any(feature = "repl", feature = "electrum", feature = "esplora"))] use std::io::Write; use std::str::FromStr; -#[cfg(any(feature = "redb", feature = "compiler"))] -use std::sync::Arc; #[cfg(feature = "electrum")] use crate::utils::BlockchainClient::Electrum; #[cfg(feature = "cbf")] -use bdk_kyoto::LightClient; -#[cfg(feature = "compiler")] -use bdk_wallet::bitcoin::XOnlyPublicKey; +use bdk_kyoto::{Info, LightClient}; use bdk_wallet::bitcoin::base64::prelude::*; #[cfg(feature = "cbf")] use tokio::select; @@ -72,7 +95,7 @@ use tokio::select; ))] use { crate::commands::OnlineWalletSubCommand::*, - bdk_wallet::bitcoin::{Transaction, consensus::Decodable, hex::FromHex}, + bdk_wallet::bitcoin::{consensus::Decodable, hex::FromHex, Transaction}, }; #[cfg(feature = "esplora")] use {crate::utils::BlockchainClient::Esplora, bdk_esplora::EsploraAsyncExt}; @@ -1260,6 +1283,15 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { } Ok("".to_string()) } + CliSubCommand::Descriptor { + subcommand: descriptor_subcommand, + } => { + let network = cli_opts.network; + let descriptor = handle_descriptor_subcommand(network, descriptor_subcommand) + .map_err(|e| Error::Generic(e.to_string()))?; + let json = serde_json::to_string_pretty(&descriptor)?; + Ok(json) + } }; result } @@ -1333,6 +1365,103 @@ fn readline() -> Result { Ok(buffer) } +pub fn handle_descriptor_subcommand( + network: Network, + subcommand: DescriptorSubCommand, +) -> Result { + match subcommand { + DescriptorSubCommand::Generate { + r#type, + multipath, + key, + } => { + let (descriptor_type, derivation_path_str) = match r#type { + 44 => (DescriptorType::Bip44, "m/44h/1h/0h"), + 49 => (DescriptorType::Bip49, "m/49h/1h/0h"), + 84 => (DescriptorType::Bip84, "m/84h/1h/0h"), + 86 => (DescriptorType::Bip86, "m/86h/1h/0h"), + _ => return Err(Error::UnsupportedScriptType(r#type)), + }; + + match (multipath, key.as_ref()) { + (true, Some(k)) => generate_multipath_descriptor(&network, r#type, k), + (false, Some(k)) => { + if is_mnemonic(k) { + generate_descriptor_from_mnemonic_string( + k, + network, + derivation_path_str, + descriptor_type, + ) + } else { + generate_standard_descriptor(&network, r#type, k) + } + } + (false, None) => generate_new_descriptor_with_mnemonic(network, descriptor_type), + _ => Err(Error::InvalidArguments( + "Provide a key or weak string".to_string(), + )), + } + } + DescriptorSubCommand::Info { descriptor } => { + let parsed: Descriptor = descriptor + .parse() + .map_err(|e| Error::Generic(format!("Failed to parse descriptor: {}", e)))?; + + let checksum = parsed.to_string(); + let script_type = match parsed { + Descriptor::Wpkh(_) => "wpkh", + Descriptor::Pkh(_) => "pkh", + Descriptor::Sh(_) => "sh", + Descriptor::Tr(_) => "tr", + _ => "other", + }; + + let json = json!({ + "descriptor": checksum, + "type": script_type, + "is_multipath": descriptor.contains("/*"), + }); + + Ok(json) + } + } +} + +pub fn generate_standard_descriptor( + network: &Network, + script_type: u8, + key: &str, +) -> Result { + let descriptor_type = match script_type { + 44 => DescriptorType::Bip44, + 49 => DescriptorType::Bip49, + 84 => DescriptorType::Bip84, + 86 => DescriptorType::Bip86, + _ => return Err(Error::UnsupportedScriptType(script_type)), + }; + + generate_descriptor_from_key_by_type(network, key, descriptor_type) +} + +impl fmt::Display for DescriptorType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + DescriptorType::Bip44 => "bip44", + DescriptorType::Bip49 => "bip49", + DescriptorType::Bip84 => "bip84", + DescriptorType::Bip86 => "bip86", + }; + write!(f, "{}", s) + } +} + +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "cbf", + feature = "rpc" +))] #[cfg(test)] mod test { #[cfg(any( diff --git a/src/utils.rs b/src/utils.rs index cb81074..93392ba 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -10,7 +10,7 @@ //! //! This module includes all the utility tools used by the App. use crate::error::BDKCliError as Error; -use std::fmt::Display; +use std::{fmt::Display, path::{Path, PathBuf}, str::FromStr}; use std::str::FromStr; use std::path::{Path, PathBuf}; @@ -35,6 +35,26 @@ use bdk_wallet::Wallet; #[cfg(any(feature = "sqlite", feature = "redb"))] use bdk_wallet::{KeychainKind, PersistedWallet, WalletPersister}; +use bdk_wallet::bip39::{Language, Mnemonic}; +use bdk_wallet::bitcoin::bip32::ChildNumber; +use bdk_wallet::bitcoin::{ + bip32::{DerivationPath, Xpriv, Xpub}, + secp256k1::Secp256k1, +}; +use bdk_wallet::descriptor::{ + Segwitv0, {Descriptor, DescriptorPublicKey}, +}; +use bdk_wallet::keys::{ + DerivableKey, ExtendedKey, + bip39::WordCount, + {DescriptorSecretKey, GeneratableKey, GeneratedKey, IntoDescriptorKey}, +}; +use bdk_wallet::miniscript::{ + Tap, + descriptor::{DescriptorXKey, Wildcard}, +}; +use serde_json::{Value, json}; + /// Parse the recipient (Address,Amount) argument from cli input. pub(crate) fn parse_recipient(s: &str) -> Result<(ScriptBuf, u64), String> { let parts: Vec<_> = s.split(':').collect(); @@ -363,4 +383,354 @@ pub(crate) fn shorten(displayable: impl Display, start: u8, end: u8) -> String { let start_str: &str = &displayable[0..start as usize]; let end_str: &str = &displayable[displayable.len() - end as usize..]; format!("{start_str}...{end_str}") +pub fn generate_descriptor_from_key_by_type( + network: &Network, + key: &str, + descriptor_type: DescriptorType, +) -> Result { + let derivation_path = match descriptor_type { + DescriptorType::Bip44 => "m/44h/1h/0h", + DescriptorType::Bip49 => "m/49h/1h/0h", + DescriptorType::Bip84 => "m/84h/1h/0h", + DescriptorType::Bip86 => "m/86h/1h/0h", + }; + + generate_bip_descriptor_from_key(network, key, derivation_path, descriptor_type) +} + +pub fn generate_new_descriptor_with_mnemonic( + network: Network, + descriptor_type: DescriptorType, +) -> Result { + let secp = Secp256k1::new(); + + // Generate a new BIP39 mnemonic + let mnemonic: GeneratedKey = + Mnemonic::generate((WordCount::Words12, Language::English)).map_err(|e| { + Error::MnemonicGenerationError(format!("Mnemonic generation failed: {:?}", e)) + })?; + + let seed = mnemonic.to_seed(""); + let xprv = + Xpriv::new_master(network, &seed).map_err(|e| Error::XprivCreationError(e.to_string()))?; + + let origin = xprv.fingerprint(&secp); + + let (derivation_base, external_fmt, internal_fmt) = match descriptor_type { + DescriptorType::Bip44 => ("/44h/1h/0h", "pkh", "pkh"), + DescriptorType::Bip49 => ("/49h/1h/0h", "sh(wpkh", "sh(wpkh"), + DescriptorType::Bip84 => ("/84h/1h/0h", "wpkh", "wpkh"), + DescriptorType::Bip86 => ("/86h/1h/0h", "tr", "tr"), + }; + let path = DerivationPath::from_str(&format!("m{}", derivation_base)) + .map_err(|e| Error::Generic(e.to_string()))?; + + let derived_xprv = xprv + .derive_priv(&secp, &path) + .map_err(|e| Error::Generic(e.to_string()))?; + + let xprv_str = derived_xprv.to_string(); + + // Construct descriptors + let external_desc = match descriptor_type { + DescriptorType::Bip49 => format!( + "{}([{}{}]{}{}))", + external_fmt, origin, derivation_base, xprv_str, "/0" + ), + _ => format!( + "{}([{}{}]{}{})", + external_fmt, origin, derivation_base, xprv_str, "/0" + ), + }; + + let internal_desc = match descriptor_type { + DescriptorType::Bip49 => format!( + "{}([{}{}]{}{}))", + internal_fmt, origin, derivation_base, xprv_str, "/1" + ), + _ => format!( + "{}([{}{}]{}{})", + internal_fmt, origin, derivation_base, xprv_str, "/1" + ), + }; + + // Parse descriptors + let (ext_desc, ext_keymap) = + Descriptor::::parse_descriptor(&secp, &external_desc) + .map_err(|e| Error::DescriptorParsingError(e.to_string()))?; + + let (int_desc, int_keymap) = + Descriptor::::parse_descriptor(&secp, &internal_desc).map_err( + |e| { + Error::DescriptorParsingError(format!("Failed to parse internal descriptor: {}", e)) + }, + )?; + + Ok(serde_json::json!({ + "type": descriptor_type.to_string(), + "mnemonic": mnemonic.to_string(), + "private_descriptors": { + "external": ext_desc.to_string_with_secret(&ext_keymap), + "internal": int_desc.to_string_with_secret(&int_keymap), + }, + "public_descriptors": { + "external": ext_desc.to_string(), + "internal": int_desc.to_string(), + } + })) +} + +pub fn generate_multipath_descriptor( + network: &Network, + script_type: u8, + key: &str, +) -> Result { + use DescriptorType::*; + + let descriptor_type = match script_type { + 44 => Bip44, + 49 => Bip49, + 84 => Bip84, + 86 => Bip86, + _ => return Err(Error::UnsupportedScriptType(script_type)), + }; + + type DescriptorConstructor = + fn(DescriptorPublicKey) -> Result, Error>; + + let (derivation_base, descriptor_constructor): (&str, DescriptorConstructor) = + match descriptor_type { + Bip44 => ("/44h/1h/0h", |key| { + Descriptor::new_pkh(key).map_err(Error::from) + }), + Bip49 => ("/49h/1h/0h", |key| { + Descriptor::new_sh_wpkh(key).map_err(Error::from) + }), + Bip84 => ("/84h/1h/0h", |key| { + Descriptor::new_wpkh(key).map_err(Error::from) + }), + Bip86 => ("/86h/1h/0h", |key| { + Descriptor::new_tr(key, None).map_err(Error::from) + }), + }; + + let secp = Secp256k1::new(); + let derivation_path = DerivationPath::from_str(&format!("m{}", derivation_base)) + .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + + // Determine if it's an xprv or xpub + let is_private = key.starts_with("xprv") || key.starts_with("tprv"); + + // Use xprv or xpub accordingly + type DescriptorBuilderFn = Box Result<(String, Option), Error>>; + + let (fingerprint, make_desc): (_, DescriptorBuilderFn) = if is_private { + let xprv: Xpriv = key + .parse() + .map_err(|e| Error::InvalidKey(format!("Invalid xprv: {e}")))?; + let fingerprint = xprv.fingerprint(&secp); + + let closure = move |change: u32| -> Result<(String, Option), Error> { + let branch_path = DerivationPath::from_str(&change.to_string()) + .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + + let desc_xprv = DescriptorXKey { + origin: Some((fingerprint, derivation_path.clone())), + xkey: xprv, + derivation_path: branch_path, + wildcard: Wildcard::Unhardened, + }; + + let desc_secret = DescriptorSecretKey::XPrv(desc_xprv.clone()); + let (desc_key, keymap, _) = match descriptor_type { + DescriptorType::Bip84 | DescriptorType::Bip49 | DescriptorType::Bip44 => { + IntoDescriptorKey::::into_descriptor_key(desc_secret) + .map_err(|e| Error::DescriptorKeyError(e.to_string()))? + .extract(&secp) + .map_err(|e| Error::DescriptorKeyError(e.to_string()))? + } + DescriptorType::Bip86 => IntoDescriptorKey::::into_descriptor_key(desc_secret) + .map_err(|e| Error::DescriptorKeyError(e.to_string()))? + .extract(&secp) + .map_err(|e| Error::DescriptorKeyError(e.to_string()))?, + }; + + let public_descriptor = descriptor_constructor(desc_key.clone())?; + let private_descriptor = descriptor_constructor(desc_key)?; + + Ok(( + public_descriptor.to_string(), + Some(private_descriptor.to_string_with_secret(&keymap)), + )) + }; + + (fingerprint, Box::new(closure)) + } else { + let xpub: Xpub = key + .parse() + .map_err(|e| Error::InvalidKey(format!("Invalid xpub: {e}")))?; + let fingerprint = xpub.fingerprint(); + + let closure = move |change: u32| -> Result<(String, Option), Error> { + let branch_path = DerivationPath::from_str(&change.to_string()) + .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?; + + let desc_xpub = DescriptorXKey { + origin: Some((fingerprint, derivation_path.clone())), + xkey: xpub, + derivation_path: branch_path, + wildcard: Wildcard::Unhardened, + }; + + let desc_key = DescriptorPublicKey::XPub(desc_xpub); + let descriptor = descriptor_constructor(desc_key)?; + Ok((descriptor.to_string(), None)) + }; + + (fingerprint, Box::new(closure)) + }; + + // Build descriptors + let (external_pub, external_priv) = make_desc(0)?; + let (internal_pub, internal_priv) = make_desc(1)?; + + let mut result = json!({ + "type": format!("{}-multipath", descriptor_type), + "public_descriptors": { + "external": external_pub, + "internal": internal_pub + }, + "fingerprint": fingerprint.to_string(), + "network": network.to_string(), + }); + + if let (Some(priv_ext), Some(priv_int)) = (external_priv, internal_priv) { + result["private_descriptors"] = json!({ + "external": priv_ext, + "internal": priv_int + }); + } + + Ok(result) +} + +pub fn generate_bip_descriptor_from_key( + network: &Network, + key: &str, + derivation_path_str: &str, + descriptor_type: DescriptorType, +) -> Result { + let secp = Secp256k1::new(); + + let derivation_path: DerivationPath = derivation_path_str + .parse() + .map_err(|e| Error::InvalidDerivationPath(format!("DerivationPath Error: {e}")))?; + + let xprv: Xpriv = key + .parse() + .map_err(|e| Error::InvalidKey(format!("Invalid xprv: {e}")))?; + + let fingerprint = xprv.fingerprint(&secp); + + let make_desc_key = |branch: u32| -> Result<(String, String), Error> { + let branch_path = DerivationPath::from(vec![ChildNumber::Normal { index: branch }]); + + let desc_xprv = DescriptorXKey { + origin: Some((fingerprint, derivation_path.clone())), + xkey: xprv, + derivation_path: branch_path.clone(), + wildcard: Wildcard::Unhardened, + }; + + let desc_secret = DescriptorSecretKey::XPrv(desc_xprv.clone()); + + let (desc_key, keymap, _) = + IntoDescriptorKey::::into_descriptor_key(desc_secret.clone()) + .map_err(|e| Error::DescriptorKeyError(e.to_string()))? + .extract(&secp) + .map_err(|e| Error::DescriptorKeyError(e.to_string()))?; + + let public_descriptor = match descriptor_type { + DescriptorType::Bip84 => Descriptor::new_wpkh(desc_key.clone())?, + DescriptorType::Bip86 => Descriptor::new_tr(desc_key.clone(), None)?, + DescriptorType::Bip49 => Descriptor::new_sh_wpkh(desc_key.clone())?, + DescriptorType::Bip44 => Descriptor::new_pkh(desc_key.clone())?, + }; + + let private_descriptor = match descriptor_type { + DescriptorType::Bip84 => Descriptor::new_wpkh(desc_key)?, + DescriptorType::Bip86 => Descriptor::new_tr(desc_key, None)?, + DescriptorType::Bip49 => Descriptor::new_sh_wpkh(desc_key)?, + DescriptorType::Bip44 => Descriptor::new_pkh(desc_key)?, + }; + + Ok(( + public_descriptor.to_string(), + private_descriptor.to_string_with_secret(&keymap), + )) + }; + + let (external_pub, external_priv) = make_desc_key(0)?; + let (internal_pub, internal_priv) = make_desc_key(1)?; + + Ok(json!({ + "type": descriptor_type.to_string(), + "fingerprint": fingerprint.to_string(), + "network": network.to_string(), + "private_descriptors": { + "external": external_priv, + "internal": internal_priv + }, + "public_descriptors": { + "external": external_pub, + "internal": internal_pub + } + })) +} + +pub fn generate_descriptor_from_mnemonic_string( + mnemonic_str: &str, + network: Network, + derivation_path_str: &str, + descriptor_type: DescriptorType, +) -> Result { + let secp = Secp256k1::new(); + + let mnemonic = Mnemonic::parse_in(Language::English, mnemonic_str) + .map_err(|e| Error::Generic(e.to_string()))?; + let ext_key: ExtendedKey = mnemonic + .into_extended_key() + .map_err(|e| Error::Generic(e.to_string()))?; + let xprv = ext_key + .into_xprv(network) + .ok_or_else(|| Error::Generic("No xprv found".to_string()))?; + + let _fingerprint = xprv.fingerprint(&secp); + let derivation_path: DerivationPath = derivation_path_str + .parse() + .map_err(|e| Error::InvalidDerivationPath(format!("DerivationPath Error: {e}")))?; + + let xprv = xprv + .derive_priv(&secp, &derivation_path) + .map_err(|e| Error::InvalidKey(format!("Failed to derive xprv: {e}")))?; + + generate_bip_descriptor_from_key( + &network, + &xprv.to_string(), + derivation_path_str, + descriptor_type, + ) +} + +pub fn is_mnemonic(s: &str) -> bool { + let word_count = s.split_whitespace().count(); + (12..=24).contains(&word_count) && s.chars().all(|c| c.is_alphanumeric() || c.is_whitespace()) +} +// Enum for descriptor types +#[derive(Debug, Clone, Copy)] +pub enum DescriptorType { + Bip44, + Bip49, + Bip84, + Bip86, }