diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1763e2963..c130ee1a2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -27,6 +27,8 @@ jobs: - name: Spell check uses: crate-ci/typos@v1 + with: + config: ./_typos.toml - name: Run cargo fmt run: cargo +nightly fmt --all --check @@ -140,8 +142,6 @@ jobs: include: - crate: floresta-common feature_args: "" - - crate: floresta-common - feature_args: "--features descriptors-no-std" steps: - uses: actions/checkout@v4 diff --git a/Cargo.lock b/Cargo.lock index 083584cfd..8d7843cbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -964,7 +964,6 @@ version = "0.4.0" dependencies = [ "bitcoin", "hashbrown", - "miniscript", "sha2", "spin", ] @@ -1078,6 +1077,7 @@ dependencies = [ "floresta-chain", "floresta-common", "kv", + "miniscript", "rand", "serde", "serde_json", diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 000000000..b98e270e4 --- /dev/null +++ b/_typos.toml @@ -0,0 +1,9 @@ +[default] +extend-ignore-re = [ + # This regex is used to prevent https://github.com/crate-ci/typos from analyzing words + # with lengths between 32 and 150 characters. These lengths correspond to hashes, + # public keys, private keys, XPUBs, descriptors, and Bitcoin addresses. + # These items are usually randomly generated for testing and may contain patterns + # that would be flagged as errors but should not be. + "[0-9A-Za-z]{32,150}+", +] \ No newline at end of file diff --git a/bin/florestad/src/cli.rs b/bin/florestad/src/cli.rs index 0a607be2f..137711d8b 100644 --- a/bin/florestad/src/cli.rs +++ b/bin/florestad/src/cli.rs @@ -63,14 +63,13 @@ pub struct Cli { pub wallet_xpub: Option>, #[arg(long, value_name = "DESCRIPTOR")] - /// Add an output descriptor to our wallet + /// Add an output descriptor to our wallet. /// - /// This option can be passed many times, and will accept any valid output descriptor. - /// You only need to pass this once, but there's no harm in passing it more than once. - /// After you start florestad at least once, passing some xpub, florestad - /// will follow the first 100 addresses derived from this xpub on each keychain and - /// cache any transactions where those addresses appear. You can use either the integrated - /// json-rpc or electrum server to fetch an address's history, balance and utxos. + /// This option can be passed multiple times, as long as each descriptor is valid. + /// For each valid descriptor, the node will derive the first 100 addresses and cache any + /// transactions related to those addresses. + /// You can use the integrated JSON-RPC or Electrum server to fetch the transaction history, + /// balance, and UTXOs for these addresses. pub wallet_descriptor: Option>, #[arg(long, value_name = "BLOCK_HASH|0", default_value = "hardcoded", value_parser = parse_assume_valid)] diff --git a/crates/floresta-common/Cargo.toml b/crates/floresta-common/Cargo.toml index c8119aad2..2440e05e0 100644 --- a/crates/floresta-common/Cargo.toml +++ b/crates/floresta-common/Cargo.toml @@ -11,16 +11,12 @@ readme.workspace = true # Floresta/README.md [dependencies] bitcoin = { workspace = true } hashbrown = { version = "0.16" } -miniscript = { workspace = true, optional = true, default-features = false } sha2 = { workspace = true } spin = { workspace = true } [features] -default = ["std", "descriptors-std"] +default = ["std"] std = ["bitcoin/std", "sha2/std"] -# Both features can be set at the same time, but for `no_std` only the latter should be used -descriptors-std = ["miniscript/std"] -descriptors-no-std = ["miniscript/no-std"] [lints] workspace = true diff --git a/crates/floresta-common/src/lib.rs b/crates/floresta-common/src/lib.rs index b10e4142a..f31b91b45 100644 --- a/crates/floresta-common/src/lib.rs +++ b/crates/floresta-common/src/lib.rs @@ -18,10 +18,6 @@ use bitcoin::hashes::sha256; use bitcoin::hashes::Hash; use bitcoin::ScriptBuf; use bitcoin::VarInt; -#[cfg(any(feature = "descriptors-std", feature = "descriptors-no-std"))] -use miniscript::Descriptor; -#[cfg(any(feature = "descriptors-std", feature = "descriptors-no-std"))] -use miniscript::DescriptorPublicKey; use sha2::Digest; #[cfg(feature = "std")] @@ -33,8 +29,6 @@ pub mod spsc; #[cfg(feature = "std")] pub use ema::Ema; -#[cfg(any(feature = "descriptors-std", feature = "descriptors-no-std"))] -use prelude::*; pub use spsc::Channel; /// Computes the SHA-256 digest of the byte slice data and returns a [Hash] from `bitcoin_hashes`. @@ -86,24 +80,41 @@ pub mod service_flags { pub const UTREEXO_FILTER: u64 = 1 << 25; } -#[cfg(any(feature = "descriptors-std", feature = "descriptors-no-std"))] -/// Takes an array of descriptors as `String`, performs sanity checks on each one -/// and returns list of parsed descriptors. -pub fn parse_descriptors( - descriptors: &[String], -) -> Result>, miniscript::Error> { - let descriptors = descriptors - .iter() - .map(|descriptor| { - let descriptor = Descriptor::::from_str(descriptor.as_str())?; - descriptor.sanity_check()?; - descriptor.into_single_descriptors() - }) - .collect::>, _>>()? - .into_iter() - .flatten() - .collect::>(); - Ok(descriptors) +#[derive(Debug, Clone)] +/// A simple fraction struct that allows adding numbers to the numerator and denominator +/// +/// If we want compute a rolling-average, we would naively hold all elements in a list and +/// compute the average from it. This is not efficient, as it requires O(n) memory and O(n) +/// time to compute the average. Instead, we can use a fraction to compute the average in O(1) +/// time and O(1) memory, by keeping track of the sum of all elements and the number of elements. +pub struct FractionAvg { + numerator: u64, + denominator: u64, +} + +impl FractionAvg { + /// Creates a new fraction with the given numerator and denominator + pub fn new(numerator: u64, denominator: u64) -> Self { + Self { + numerator, + denominator, + } + } + + /// Adds a number to the numerator and increments the denominator + pub fn add(&mut self, other: u64) { + self.numerator += other; + self.denominator += 1; + } + + /// Returns the average of the fraction + pub fn value(&self) -> f64 { + if self.denominator == 0 { + return 0.0; + } + + self.numerator as f64 / self.denominator as f64 + } } #[cfg(not(feature = "std"))] diff --git a/crates/floresta-node/src/error.rs b/crates/floresta-node/src/error.rs index 02b582bb1..a2a2e3cf2 100644 --- a/crates/floresta-node/src/error.rs +++ b/crates/floresta-node/src/error.rs @@ -6,11 +6,11 @@ use floresta_chain::BlockchainError; use floresta_chain::FlatChainstoreError; #[cfg(feature = "compact-filters")] use floresta_compact_filters::IterableFilterStoreError; +use floresta_watch_only::descriptor::DescriptorError; use floresta_watch_only::kv_database::KvDatabaseError; use floresta_watch_only::WatchOnlyError; use tokio_rustls::rustls::pki_types; -use crate::slip132; #[derive(Debug)] pub enum FlorestadError { /// Encoding/decoding error. @@ -41,7 +41,7 @@ pub enum FlorestadError { TomlParsing(toml::de::Error), /// Parsing registered HD version bytes from slip132. - WalletInput(slip132::Error), + WalletInput(DescriptorError), /// Parsing a bitcoin address. AddressParsing(bitcoin::address::ParseError), @@ -98,9 +98,6 @@ pub enum FlorestadError { /// Failed to create the TLS data directory. CouldNotCreateTLSDataDir(String, std::io::Error), - /// Failed to provide a valid xpub. - InvalidProvidedXpub(String, slip132::Error), - /// Failed to obtain the wallet cache. CouldNotObtainWalletCache(WatchOnlyError), @@ -198,9 +195,6 @@ impl std::fmt::Display for FlorestadError { FlorestadError::CouldNotCreateTLSDataDir(path, err) => { write!(f, "Could not create TLS data directory {path}: {err}") } - FlorestadError::InvalidProvidedXpub(xpub, err) => { - write!(f, "Invalid provided xpub {xpub}: {err:?}") - } FlorestadError::CouldNotObtainWalletCache(err) => { write!(f, "Could not obtain wallet cache: {err}") } @@ -248,7 +242,7 @@ impl_from_error!(Io, std::io::Error); impl_from_error!(ScriptValidation, bitcoin::blockdata::script::Error); impl_from_error!(Blockchain, BlockchainError); impl_from_error!(SerdeJson, serde_json::Error); -impl_from_error!(WalletInput, slip132::Error); +impl_from_error!(WalletInput, DescriptorError); impl_from_error!(TomlParsing, toml::de::Error); impl_from_error!(BlockValidation, BlockValidationErrors); impl_from_error!(AddressParsing, bitcoin::address::ParseError); diff --git a/crates/floresta-node/src/florestad.rs b/crates/floresta-node/src/florestad.rs index 351674019..dc5412e17 100644 --- a/crates/floresta-node/src/florestad.rs +++ b/crates/floresta-node/src/florestad.rs @@ -4,12 +4,15 @@ use std::net::Ipv4Addr; use std::net::SocketAddr; use std::path::Path; use std::path::PathBuf; +use std::str::FromStr; use std::sync::Arc; use std::sync::Mutex; #[cfg(feature = "json-rpc")] use std::sync::OnceLock; +use bitcoin::Address; pub use bitcoin::Network; +use bitcoin::ScriptBuf; #[cfg(feature = "zmq-server")] use floresta_chain::pruned_utreexo::BlockchainInterface; pub use floresta_chain::AssumeUtreexoValue; @@ -27,6 +30,7 @@ use floresta_electrum::electrum_protocol::ElectrumServer; use floresta_mempool::Mempool; use floresta_watch_only::kv_database::KvDatabase; use floresta_watch_only::AddressCache; +use floresta_watch_only::WatchOnlyError; use floresta_wire::address_man::AddressMan; use floresta_wire::node::running_ctx::RunningNode; use floresta_wire::node::UtreexoNode; @@ -57,7 +61,6 @@ use crate::error::FlorestadError; use crate::florestad::fs::OpenOptions; #[cfg(feature = "json-rpc")] use crate::json_rpc; -use crate::wallet_input::InitialWalletSetup; #[cfg(feature = "zmq-server")] use crate::zmq::ZMQServer; @@ -741,45 +744,71 @@ impl Florestad { Self::get_config_file(&default_path) } }; - let setup = self.prepare_wallet_setup(config_file)?; // Add the configured descriptors and addresses to the wallet - for descriptor in setup.descriptors { - let descriptor = descriptor.to_string(); - let is_cached = wallet.is_cached(&descriptor)?; + for descriptor in self.get_descriptors(&config_file) { + if let Err(e) = wallet.push_descriptor(&descriptor) { + if let WatchOnlyError::DescriptorDuplicate = e { + warn!("Descriptor already exists in wallet, skipping: {descriptor}"); + } else { + return Err(FlorestadError::from(e)); + } + } + } - if !is_cached { - wallet.push_descriptor(&descriptor)?; + for xpub in self.get_xpubs(&config_file) { + if let Err(e) = wallet.push_xpub(&xpub, self.config.network) { + if let WatchOnlyError::DescriptorDuplicate = e { + warn!("Descriptor for the provided XPUB already exists in the wallet. Skipping: {xpub}"); + } else { + return Err(FlorestadError::from(e)); + } } } - for addresses in setup.addresses { - wallet.cache_address(addresses.script_pubkey()); + + for address in self.get_addresses(&config_file)? { + wallet.cache_address(address); } info!("Wallet setup completed!"); Ok(()) } - /// Parses the configured list of xpubs, output descriptors and addresses to watch for, and - /// returns the constructed `InitialWalletSetup`. - fn prepare_wallet_setup( - &self, - config_file: ConfigFile, - ) -> Result { - let config = &self.config; + /// Get the wallet descriptors from the config file and the environment. + fn get_descriptors(&self, config_file: &ConfigFile) -> Vec { + let mut descriptors = Vec::new(); + descriptors.extend(self.config.wallet_descriptor.clone().unwrap_or_default()); + descriptors.extend(config_file.wallet.descriptors.clone().unwrap_or_default()); + + descriptors + } + /// Get the wallet xpubs from the config file and the environment + fn get_xpubs(&self, config_file: &ConfigFile) -> Vec { let mut xpubs = Vec::new(); - xpubs.extend(config.wallet_xpub.clone().unwrap_or_default()); - xpubs.extend(config_file.wallet.xpubs.unwrap_or_default()); + xpubs.extend(self.config.wallet_xpub.clone().unwrap_or_default()); + xpubs.extend(config_file.wallet.xpubs.clone().unwrap_or_default()); xpubs.extend(Self::get_key_from_env()); - let mut descriptors = Vec::new(); - descriptors.extend(config.wallet_descriptor.clone().unwrap_or_default()); - descriptors.extend(config_file.wallet.descriptors.unwrap_or_default()); + xpubs + } - let addresses = config_file.wallet.addresses.unwrap_or_default(); + /// Get the wallet addresses from the config file + fn get_addresses(&self, config_file: &ConfigFile) -> Result, FlorestadError> { + let addresses_string = config_file.wallet.addresses.clone().unwrap_or_default(); + + let addresses = addresses_string + .iter() + .map(|address| match Address::from_str(address) { + Ok(address) => Ok(address.assume_checked().script_pubkey()), + Err(e) => { + error!("Invalid address provided: {address} \nReason: {e:?}"); + Err(e) + } + }) + .collect::, _>>()?; - InitialWalletSetup::build(&xpubs, &descriptors, &addresses, config.network, 100) + Ok(addresses) } /// Get the default Electrum port for the Network and TLS combination. diff --git a/crates/floresta-node/src/json_rpc/res.rs b/crates/floresta-node/src/json_rpc/res.rs index bb475e86e..6773ddf01 100644 --- a/crates/floresta-node/src/json_rpc/res.rs +++ b/crates/floresta-node/src/json_rpc/res.rs @@ -5,6 +5,7 @@ use corepc_types::v30::GetBlockVerboseOne; use floresta_chain::extensions::HeaderExtError; use floresta_common::impl_error_from; use floresta_mempool::mempool::AcceptToMempoolError; +use floresta_watch_only::descriptor::DescriptorError; use serde::Deserialize; use serde::Serialize; @@ -163,7 +164,7 @@ pub enum JsonRpcError { InvalidScript, /// The provided descriptor is invalid, e.g., if it does not match the expected format - InvalidDescriptor(miniscript::Error), + InvalidDescriptor(DescriptorError), /// The requested block is not found in the blockchain BlockNotFound, @@ -283,7 +284,7 @@ impl From for JsonRpcError { } } -impl_error_from!(JsonRpcError, miniscript::Error, InvalidDescriptor); +impl_error_from!(JsonRpcError, DescriptorError, InvalidDescriptor); impl From> for JsonRpcError { fn from(e: floresta_watch_only::WatchOnlyError) -> Self { diff --git a/crates/floresta-node/src/json_rpc/server.rs b/crates/floresta-node/src/json_rpc/server.rs index 22118068e..a02ebfa34 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; use std::net::SocketAddr; -use std::slice; use std::sync::Arc; use std::time::Instant; @@ -23,7 +22,6 @@ use bitcoin::TxIn; use bitcoin::TxOut; use bitcoin::Txid; use floresta_chain::ThreadSafeChain; -use floresta_common::parse_descriptors; use floresta_compact_filters::flat_filters_store::FlatFiltersStore; use floresta_compact_filters::network_filters::NetworkFilters; use floresta_watch_only::kv_database::KvDatabase; @@ -99,22 +97,8 @@ impl RpcImpl { } fn load_descriptor(&self, descriptor: String) -> Result { - let desc = slice::from_ref(&descriptor); - let mut parsed = parse_descriptors(desc)?; - - // It's ok to unwrap because we know there is at least one element in the vector - let addresses = parsed.pop().unwrap(); - let addresses = (0..100) - .map(|index| { - let address = addresses - .at_derivation_index(index) - .unwrap() - .script_pubkey(); - self.wallet.cache_address(address.clone()); - address - }) - .collect::>(); - + let addresses = self.wallet.push_descriptor(&descriptor)?; + debug!("Descriptor pushed: {descriptor}"); debug!("Rescanning with block filters for addresses: {addresses:?}"); let addresses = self.wallet.get_cached_addresses(); @@ -131,9 +115,6 @@ impl RpcImpl { addresses, chain, wallet, cfilters, node, None, None, )); - self.wallet.push_descriptor(&descriptor)?; - debug!("Descriptor pushed: {descriptor}"); - Ok(true) } diff --git a/crates/floresta-node/src/lib.rs b/crates/floresta-node/src/lib.rs index d4366d021..89f6094b6 100644 --- a/crates/floresta-node/src/lib.rs +++ b/crates/floresta-node/src/lib.rs @@ -12,8 +12,6 @@ mod error; mod florestad; #[cfg(feature = "json-rpc")] mod json_rpc; -mod slip132; -mod wallet_input; #[cfg(feature = "zmq-server")] mod zmq; diff --git a/crates/floresta-node/src/slip132.rs b/crates/floresta-node/src/slip132.rs deleted file mode 100644 index 9b0bf28d4..000000000 --- a/crates/floresta-node/src/slip132.rs +++ /dev/null @@ -1,198 +0,0 @@ -// Based on slip132 from LNP/BP Descriptor Wallet library by: -// Dr. Maxim Orlovsky -// -// Adapted for Floresta by: -// Davidson Sousa - -//! Bitcoin SLIP-132 standard implementation for parsing custom xpub/xpriv key -//! formats - -use std::fmt::Debug; - -use bitcoin::base58; -use bitcoin::bip32; -use bitcoin::bip32::Xpriv; -use bitcoin::bip32::Xpub; - -/// Magical version bytes for xpub: bitcoin mainnet public key for P2PKH or P2SH -pub const VERSION_MAGIC_XPUB: [u8; 4] = [0x04, 0x88, 0xB2, 0x1E]; -/// Magical version bytes for xprv: bitcoin mainnet private key for P2PKH or -/// P2SH -pub const VERSION_MAGIC_XPRV: [u8; 4] = [0x04, 0x88, 0xAD, 0xE4]; -/// Magical version bytes for ypub: bitcoin mainnet public key for P2WPKH in -/// P2SH -pub const VERSION_MAGIC_YPUB: [u8; 4] = [0x04, 0x9D, 0x7C, 0xB2]; -/// Magical version bytes for yprv: bitcoin mainnet private key for P2WPKH in -/// P2SH -pub const VERSION_MAGIC_YPRV: [u8; 4] = [0x04, 0x9D, 0x78, 0x78]; -/// Magical version bytes for zpub: bitcoin mainnet public key for P2WPKH -pub const VERSION_MAGIC_ZPUB: [u8; 4] = [0x04, 0xB2, 0x47, 0x46]; -/// Magical version bytes for zprv: bitcoin mainnet private key for P2WPKH -pub const VERSION_MAGIC_ZPRV: [u8; 4] = [0x04, 0xB2, 0x43, 0x0C]; -/// Magical version bytes for Ypub: bitcoin mainnet public key for -/// multi-signature P2WSH in P2SH -pub const VERSION_MAGIC_YPUB_MULTISIG: [u8; 4] = [0x02, 0x95, 0xb4, 0x3f]; -/// Magical version bytes for Yprv: bitcoin mainnet private key for -/// multi-signature P2WSH in P2SH -pub const VERSION_MAGIC_YPRV_MULTISIG: [u8; 4] = [0x02, 0x95, 0xb0, 0x05]; -/// Magical version bytes for Zpub: bitcoin mainnet public key for -/// multi-signature P2WSH -pub const VERSION_MAGIC_ZPUB_MULTISIG: [u8; 4] = [0x02, 0xaa, 0x7e, 0xd3]; -/// Magical version bytes for Zprv: bitcoin mainnet private key for -/// multi-signature P2WSH -pub const VERSION_MAGIC_ZPRV_MULTISIG: [u8; 4] = [0x02, 0xaa, 0x7a, 0x99]; - -/// Magical version bytes for tpub: bitcoin testnet/regtest public key for -/// P2PKH or P2SH -pub const VERSION_MAGIC_TPUB: [u8; 4] = [0x04, 0x35, 0x87, 0xCF]; -/// Magical version bytes for tprv: bitcoin testnet/regtest private key for -/// P2PKH or P2SH -pub const VERSION_MAGIC_TPRV: [u8; 4] = [0x04, 0x35, 0x83, 0x94]; -/// Magical version bytes for upub: bitcoin testnet/regtest public key for -/// P2WPKH in P2SH -pub const VERSION_MAGIC_UPUB: [u8; 4] = [0x04, 0x4A, 0x52, 0x62]; -/// Magical version bytes for uprv: bitcoin testnet/regtest private key for -/// P2WPKH in P2SH -pub const VERSION_MAGIC_UPRV: [u8; 4] = [0x04, 0x4A, 0x4E, 0x28]; -/// Magical version bytes for vpub: bitcoin testnet/regtest public key for -/// P2WPKH -pub const VERSION_MAGIC_VPUB: [u8; 4] = [0x04, 0x5F, 0x1C, 0xF6]; -/// Magical version bytes for vprv: bitcoin testnet/regtest private key for -/// P2WPKH -pub const VERSION_MAGIC_VPRV: [u8; 4] = [0x04, 0x5F, 0x18, 0xBC]; -/// Magical version bytes for Upub: bitcoin testnet/regtest public key for -/// multi-signature P2WSH in P2SH -pub const VERSION_MAGIC_UPUB_MULTISIG: [u8; 4] = [0x02, 0x42, 0x89, 0xef]; -/// Magical version bytes for Uprv: bitcoin testnet/regtest private key for -/// multi-signature P2WSH in P2SH -pub const VERSION_MAGIC_UPRV_MULTISIG: [u8; 4] = [0x02, 0x42, 0x85, 0xb5]; -/// Magical version bytes for Zpub: bitcoin testnet/regtest public key for -/// multi-signature P2WSH -pub const VERSION_MAGIC_VPUB_MULTISIG: [u8; 4] = [0x02, 0x57, 0x54, 0x83]; -/// Magical version bytes for Zprv: bitcoin testnet/regtest private key for -/// multi-signature P2WSH -pub const VERSION_MAGIC_VPRV_MULTISIG: [u8; 4] = [0x02, 0x57, 0x50, 0x48]; - -/// Extended public and private key processing errors -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum Error { - /// error in BASE58 key encoding. Details: {0} - Base58(base58::Error), - - /// error in hex key encoding. Details: {0} - Hex(bitcoin::hashes::hex::HexToArrayError), - - /// pk->pk derivation was attempted on a hardened key. - CannotDeriveFromHardenedKey, - - /// child number {0} is out of range. - InvalidChildNumber(u32), - - /// invalid child number format. - InvalidChildNumberFormat, - - /// invalid derivation path format. - InvalidDerivationPathFormat, - - /// unknown version magic bytes {0:#06X?} - UnknownVersion([u8; 4]), - - /// encoded extended key data has wrong length {0} - WrongExtendedKeyLength(usize), - - /// unrecognized or unsupported extended key prefix (please check SLIP 32 - /// for possible values) - UnknownSlip32Prefix, - - /// failure in rust bitcoin library - InternalFailure, -} - -impl From for Error { - fn from(err: bip32::Error) -> Self { - match err { - bip32::Error::CannotDeriveFromHardenedKey => Error::CannotDeriveFromHardenedKey, - bip32::Error::InvalidChildNumber(no) => Error::InvalidChildNumber(no), - bip32::Error::InvalidChildNumberFormat => Error::InvalidChildNumberFormat, - bip32::Error::InvalidDerivationPathFormat => Error::InvalidDerivationPathFormat, - bip32::Error::Secp256k1(_) => Error::InternalFailure, - bip32::Error::UnknownVersion(ver) => Error::UnknownVersion(ver), - bip32::Error::WrongExtendedKeyLength(len) => Error::WrongExtendedKeyLength(len), - bip32::Error::Base58(err) => Error::Base58(err), - bip32::Error::Hex(err) => Error::Hex(err), - _ => Error::InternalFailure, - } - } -} - -impl From for Error { - fn from(err: base58::Error) -> Self { - Error::Base58(err) - } -} - -/// Trait for building standard BIP32 extended keys from SLIP132 variant. -pub trait FromSlip132 { - /// Constructs standard BIP32 extended key from SLIP132 string. - fn from_slip132_str(s: &str) -> Result - where - Self: Sized; -} - -impl FromSlip132 for Xpub { - fn from_slip132_str(s: &str) -> Result { - let mut data = base58::decode_check(s)?; - - let mut prefix = [0u8; 4]; - prefix.copy_from_slice(&data[0..4]); - let slice = match prefix { - VERSION_MAGIC_XPUB - | VERSION_MAGIC_YPUB - | VERSION_MAGIC_ZPUB - | VERSION_MAGIC_YPUB_MULTISIG - | VERSION_MAGIC_ZPUB_MULTISIG => VERSION_MAGIC_XPUB, - - VERSION_MAGIC_TPUB - | VERSION_MAGIC_UPUB - | VERSION_MAGIC_VPUB - | VERSION_MAGIC_UPUB_MULTISIG - | VERSION_MAGIC_VPUB_MULTISIG => VERSION_MAGIC_TPUB, - - _ => return Err(Error::UnknownSlip32Prefix), - }; - data[0..4].copy_from_slice(&slice); - - let xpub = Xpub::decode(&data)?; - - Ok(xpub) - } -} - -impl FromSlip132 for Xpriv { - fn from_slip132_str(s: &str) -> Result { - let mut data = base58::decode_check(s)?; - - let mut prefix = [0u8; 4]; - prefix.copy_from_slice(&data[0..4]); - let slice = match prefix { - VERSION_MAGIC_XPRV - | VERSION_MAGIC_YPRV - | VERSION_MAGIC_ZPRV - | VERSION_MAGIC_YPRV_MULTISIG - | VERSION_MAGIC_ZPRV_MULTISIG => VERSION_MAGIC_XPRV, - - VERSION_MAGIC_TPRV - | VERSION_MAGIC_UPRV - | VERSION_MAGIC_VPRV - | VERSION_MAGIC_UPRV_MULTISIG - | VERSION_MAGIC_VPRV_MULTISIG => VERSION_MAGIC_TPRV, - - _ => return Err(Error::UnknownSlip32Prefix), - }; - data[0..4].copy_from_slice(&slice); - - let xprv = Xpriv::decode(&data)?; - - Ok(xprv) - } -} diff --git a/crates/floresta-node/src/wallet_input.rs b/crates/floresta-node/src/wallet_input.rs deleted file mode 100644 index 9ce5c9abd..000000000 --- a/crates/floresta-node/src/wallet_input.rs +++ /dev/null @@ -1,260 +0,0 @@ -//! Handles different inputs, try to make sense out of it and store a sane descriptor at the end - -use std::str::FromStr; - -use bitcoin::Address; -use bitcoin::Network; -use miniscript::Descriptor; -use miniscript::DescriptorPublicKey; -use tracing::error; - -use crate::error::FlorestadError; - -pub mod extended_pub_key { - use bitcoin::bip32::Xpub; - - use crate::slip132; - - pub fn from_str(s: &str) -> Result { - slip132::FromSlip132::from_slip132_str(s) - } -} - -fn parse_xpubs(xpubs: &[String]) -> Result>, FlorestadError> { - let mut descriptors = Vec::new(); - for key in xpubs { - // Parses the descriptor and get an external and change descriptors - let xpub = extended_pub_key::from_str(key.as_str())?; - let main_desc = format!("wpkh({xpub}/0/*)"); - let change_desc = format!("wpkh({xpub}/1/*)"); - descriptors.push(Descriptor::::from_str(&main_desc)?); - descriptors.push(Descriptor::::from_str(&change_desc)?); - } - Ok(descriptors) -} - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct InitialWalletSetup { - pub(crate) descriptors: Vec>, - pub(crate) addresses: Vec
, -} - -impl InitialWalletSetup { - pub(crate) fn build( - xpubs: &[String], - initial_descriptors: &[String], - addresses: &[String], - network: Network, - addresses_per_descriptor: u32, - ) -> Result { - let mut descriptors = parse_xpubs(xpubs)?; - descriptors.extend(parse_descriptors(initial_descriptors)?); - descriptors.sort(); - descriptors.dedup(); - let mut addresses = addresses - .iter() - .flat_map(|address| match Address::from_str(address) { - Ok(address) => Ok(address.require_network(network)), - Err(e) => { - error!("Invalid address provided: {address} \nReason: {e:?}"); - Err(e) - } - }) - .collect::, _>>()?; - addresses.extend(descriptors.iter().flat_map(|descriptor| { - (0..addresses_per_descriptor).map(|index| { - descriptor - .at_derivation_index(index) - .expect("Error while deriving address") - .address(network) - .expect("Error while deriving address. Is this an active descriptor?") - }) - })); - addresses.sort(); - addresses.dedup(); - Ok(Self { - descriptors, - addresses, - }) - } -} - -pub fn parse_descriptors( - descriptors: &[String], -) -> Result>, FlorestadError> { - let descriptors = descriptors - .iter() - .map(|descriptor| { - let descriptor = Descriptor::::from_str(descriptor.as_str())?; - descriptor.sanity_check()?; - descriptor.into_single_descriptors() - }) - .collect::>, _>>()? - .into_iter() - .flatten() - .collect::>(); - Ok(descriptors) -} - -#[cfg(test)] -pub mod test { - use bitcoin::bip32::ChildNumber; - use bitcoin::secp256k1::Secp256k1; - use bitcoin::Network; - - use super::*; - - #[test] - fn test_xpub_parsing() { - // Test cases from https://github.com/satoshilabs/slips/blob/master/slip-0132.md - const XPUB: &str = "xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj"; - const YPUB: &str = "ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP"; - const ZPUB: &str = "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs"; - - let secp = Secp256k1::new(); - - let xpub: bitcoin::bip32::Xpub = super::extended_pub_key::from_str(XPUB) - .expect("Parsing failed") - .ckd_pub(&secp, ChildNumber::Normal { index: 0 }) - .and_then(|key| key.ckd_pub(&secp, ChildNumber::Normal { index: 0 })) - .unwrap(); - let ypub = super::extended_pub_key::from_str(YPUB) - .expect("Parsing failed") - .ckd_pub(&secp, ChildNumber::Normal { index: 0 }) - .and_then(|key| key.ckd_pub(&secp, ChildNumber::Normal { index: 0 })) - .unwrap(); - let zpub = super::extended_pub_key::from_str(ZPUB) - .expect("Parsing failed") - .ckd_pub(&secp, ChildNumber::Normal { index: 0 }) - .and_then(|key| key.ckd_pub(&secp, ChildNumber::Normal { index: 0 })) - .unwrap(); - // Old p2pkh - assert_eq!( - Address::p2pkh(xpub.to_pub(), Network::Bitcoin) - .to_string() - .as_str(), - "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA" - ); - - // p2wpkh-p2pkh - let script = Address::p2wpkh(&ypub.to_pub(), Network::Bitcoin).script_pubkey(); - - assert_eq!( - Address::p2sh(&script, Network::Bitcoin) - .unwrap() - .to_string() - .as_str(), - "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf" - ); - - // p2wpkh - assert_eq!( - Address::p2wpkh(&zpub.to_pub(), Network::Bitcoin) - .to_string() - .as_str(), - "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu" - ) - } - - #[test] - fn test_descriptor_parsing() { - // singlesig - assert_eq!( - parse_descriptors(&[ - "wpkh([a5b13c0e/84h/0h/0h]xpub6CFy3kRXorC3NMTt8qrsY9ucUfxVLXyFQ49JSLm3iEG5gfAmWewYFzjNYFgRiCjoB9WWEuJQiyYGCdZvUTwPEUPL9pPabT8bkbiD9Po47XG/<0;1>/*)#n8sgapuv".to_owned() - ]).unwrap(), - parse_descriptors(&[ - "wpkh([a5b13c0e/84'/0'/0']xpub6CFy3kRXorC3NMTt8qrsY9ucUfxVLXyFQ49JSLm3iEG5gfAmWewYFzjNYFgRiCjoB9WWEuJQiyYGCdZvUTwPEUPL9pPabT8bkbiD9Po47XG/0/*)#wg8dh3s7".to_owned(), - "wpkh([a5b13c0e/84'/0'/0']xpub6CFy3kRXorC3NMTt8qrsY9ucUfxVLXyFQ49JSLm3iEG5gfAmWewYFzjNYFgRiCjoB9WWEuJQiyYGCdZvUTwPEUPL9pPabT8bkbiD9Po47XG/1/*)#luzv2yqx".to_owned() - ]).unwrap() - ); - // multisig - assert_eq!( - parse_descriptors(&[ - "wsh(sortedmulti(1,[6f826a6a/48h/0h/0h/2h]xpub6DsY48BAsvEMTRPbeSTu9jZXqEsTKr5T86WbRbXHp2gEVCNR3hALnMorFawVwnnHMMfjbyY8We9B4beh1fxqhcv6kgSeLgQxeXDqv3DaW7m/<0;1>/*,[a5b13c0e/48h/0h/0h/2h]xpub6Eqj1Hj3RezebC6cKiYYN2sAc1Wu33BWoaafnNgAbQwDkJdy7aXCYCmaMzb8rCpmh919UsehyV5Ywjo62hG4R2G2PGv4uqEDTUhYQw26BDJ/<0;1>/*))#nykmcu2v".to_owned() - ]).unwrap(), - parse_descriptors(&[ - "wsh(sortedmulti(1,[6f826a6a/48'/0'/0'/2']xpub6DsY48BAsvEMTRPbeSTu9jZXqEsTKr5T86WbRbXHp2gEVCNR3hALnMorFawVwnnHMMfjbyY8We9B4beh1fxqhcv6kgSeLgQxeXDqv3DaW7m/0/*,[a5b13c0e/48'/0'/0'/2']xpub6Eqj1Hj3RezebC6cKiYYN2sAc1Wu33BWoaafnNgAbQwDkJdy7aXCYCmaMzb8rCpmh919UsehyV5Ywjo62hG4R2G2PGv4uqEDTUhYQw26BDJ/0/*))#sw68w95x".to_owned(), - "wsh(sortedmulti(1,[6f826a6a/48'/0'/0'/2']xpub6DsY48BAsvEMTRPbeSTu9jZXqEsTKr5T86WbRbXHp2gEVCNR3hALnMorFawVwnnHMMfjbyY8We9B4beh1fxqhcv6kgSeLgQxeXDqv3DaW7m/1/*,[a5b13c0e/48'/0'/0'/2']xpub6Eqj1Hj3RezebC6cKiYYN2sAc1Wu33BWoaafnNgAbQwDkJdy7aXCYCmaMzb8rCpmh919UsehyV5Ywjo62hG4R2G2PGv4uqEDTUhYQw26BDJ/1/*))#fafrqkpn".to_owned() - ]).unwrap() - ); - } - - #[test] - fn test_initial_wallet_build() { - use pretty_assertions::assert_eq; - let addresses_per_descriptor = 1; - let network = Network::Bitcoin; - // Build wallet from xpub (in this case a zpub from slip132 standard) - let w1_xpub = InitialWalletSetup::build(&[ - "zpub6qvVf5mN7DH14wr7oZS7xL6cpcFPDmxFEHBk18YpUF1qnroE1yGfW83eafbbi23dzRk7jrVXeJFMyCo3urmQpwkXtVnRmGmaJ3qVvdwx4mB".to_owned() - ], &[], &[], network, addresses_per_descriptor).unwrap(); - // Build same wallet from output descriptor - let w1_descriptor = InitialWalletSetup::build(&[], &[ - "wpkh(xpub6CFy3kRXorC3NMTt8qrsY9ucUfxVLXyFQ49JSLm3iEG5gfAmWewYFzjNYFgRiCjoB9WWEuJQiyYGCdZvUTwPEUPL9pPabT8bkbiD9Po47XG/<0;1>/*)".to_owned() - ], &[], network, addresses_per_descriptor).unwrap(); - // Using both methods the result should be the same - assert_eq!(w1_xpub, w1_descriptor); - // Both normal receiving descriptor and change descriptor should be present - assert_eq!( - w1_descriptor.descriptors - .iter() - .map(ToString::to_string) - .collect::>(), - vec![ - "wpkh(xpub6CFy3kRXorC3NMTt8qrsY9ucUfxVLXyFQ49JSLm3iEG5gfAmWewYFzjNYFgRiCjoB9WWEuJQiyYGCdZvUTwPEUPL9pPabT8bkbiD9Po47XG/0/*)#qua4l7ct", - "wpkh(xpub6CFy3kRXorC3NMTt8qrsY9ucUfxVLXyFQ49JSLm3iEG5gfAmWewYFzjNYFgRiCjoB9WWEuJQiyYGCdZvUTwPEUPL9pPabT8bkbiD9Po47XG/1/*)#3gc5ztgn" - ] - ); - // Receiving and change addresses - let addresses = vec![ - "bc1q88guum89mxwszau37m3y4p24renwlwgtkscl6x".to_owned(), - "bc1q24629yendf7q0dxnw362dqccn52vuz9s0z59hr".to_owned(), - ]; - assert_eq!( - w1_descriptor - .addresses - .iter() - .map(ToString::to_string) - .collect::>(), - addresses - ); - // We can build from these addresses - let w1_addresses = - InitialWalletSetup::build(&[], &[], &addresses, network, addresses_per_descriptor) - .unwrap(); - // And the result will be the same as from xpub/descriptor - assert_eq!(w1_descriptor.addresses, w1_addresses.addresses); - // We can also build from xpub, descriptor and addresses, at same time - let w1_all = - InitialWalletSetup::build(&[ - "zpub6qvVf5mN7DH14wr7oZS7xL6cpcFPDmxFEHBk18YpUF1qnroE1yGfW83eafbbi23dzRk7jrVXeJFMyCo3urmQpwkXtVnRmGmaJ3qVvdwx4mB".to_owned() - ], &[ - "wpkh(xpub6CFy3kRXorC3NMTt8qrsY9ucUfxVLXyFQ49JSLm3iEG5gfAmWewYFzjNYFgRiCjoB9WWEuJQiyYGCdZvUTwPEUPL9pPabT8bkbiD9Po47XG/<0;1>/*)".to_owned() - ], &addresses, network, addresses_per_descriptor).unwrap(); - // And the result should be the same, no duplication will happen - assert_eq!(w1_descriptor, w1_all); - } - - #[test] - fn test_initial_wallet_build_multisig_testnet() { - use pretty_assertions::assert_eq; - let addresses_per_descriptor = 1; - let network = Network::Testnet; - let w1_descriptor = InitialWalletSetup::build(&[], &[ - "wsh(sortedmulti(1,[54ff5a12/48h/1h/0h/2h]tpubDDw6pwZA3hYxcSN32q7a5ynsKmWr4BbkBNHydHPKkM4BZwUfiK7tQ26h7USm8kA1E2FvCy7f7Er7QXKF8RNptATywydARtzgrxuPDwyYv4x/<0;1>/*,[bcf969c0/48h/1h/0h/2h]tpubDEFdgZdCPgQBTNtGj4h6AehK79Jm4LH54JrYBJjAtHMLEAth7LuY87awx9ZMiCURFzFWhxToRJK6xp39aqeJWrG5nuW3eBnXeMJcvDeDxfp/<0;1>/*))#fuw35j0q".to_owned() - ], &[], network, addresses_per_descriptor).unwrap(); - let addresses = vec![ - "tb1q2eeqw57e7pmrh5w3wkrshctx2qk80vf4mu7l7ek3ne4hg3lmcrnqcwejgj".to_owned(), - "tb1q6dpyc3jyqelgfwksedef0k2244rcg4gf6wvqm463lk907es2m08qnrfky7".to_owned(), - ]; - assert_eq!( - w1_descriptor - .addresses - .iter() - .map(ToString::to_string) - .collect::>(), - addresses - ); - } -} diff --git a/crates/floresta-watch-only/Cargo.toml b/crates/floresta-watch-only/Cargo.toml index b25bf4511..028965d53 100644 --- a/crates/floresta-watch-only/Cargo.toml +++ b/crates/floresta-watch-only/Cargo.toml @@ -16,10 +16,11 @@ kv = { workspace = true } serde = { workspace = true } serde_json = { workspace = true, features = ["alloc"] } tracing = { workspace = true } +miniscript = { workspace = true, features = ["compiler", "no-std"] } # Local dependencies floresta-chain = { workspace = true } -floresta-common = { workspace = true, features = ["descriptors-no-std"] } +floresta-common = { workspace = true } [dev-dependencies] rand = { workspace = true } diff --git a/crates/floresta-watch-only/src/descriptor/mod.rs b/crates/floresta-watch-only/src/descriptor/mod.rs new file mode 100644 index 000000000..7d254d9f3 --- /dev/null +++ b/crates/floresta-watch-only/src/descriptor/mod.rs @@ -0,0 +1,475 @@ +use core::fmt; +use core::str::FromStr; + +use bitcoin::Network; +use bitcoin::ScriptBuf; +use floresta_common::impl_error_from; +use miniscript::descriptor::ConversionError; +use miniscript::Descriptor; +use miniscript::DescriptorPublicKey; +use miniscript::Error as MiniscriptError; + +mod slip132; + +use crate::descriptor::slip132::generate_descriptor_from_xpub; +use crate::descriptor::slip132::is_xpub_mainnet; +use crate::descriptor::slip132::Error as Slip132Error; + +#[derive(Debug)] +pub enum DescriptorError { + /// Error parsing xpub + XpubParseError(Slip132Error), + + /// Error xpub network mismatch + XpubNetworkMismatch(String), + + /// Error in miniscript + MiniscriptError(MiniscriptError), + + DeriveDescriptorError(ConversionError), +} + +impl_error_from!(DescriptorError, Slip132Error, XpubParseError); +impl_error_from!(DescriptorError, MiniscriptError, MiniscriptError); +impl_error_from!(DescriptorError, ConversionError, DeriveDescriptorError); + +impl fmt::Display for DescriptorError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DescriptorError::XpubParseError(err) => write!(f, "Xpub parse error: {}", err), + DescriptorError::XpubNetworkMismatch(key) => { + write!( + f, + "The inserted Xpub does not operate in this network: {}", + key + ) + } + DescriptorError::MiniscriptError(err) => write!(f, "Miniscript error: {}", err), + DescriptorError::DeriveDescriptorError(err) => { + write!(f, "Derive descriptor error: {}", err) + } + } + } +} + +pub fn parse_xpub(xpub: &str, network: Network) -> Result, DescriptorError> { + // Check if the xpub network matches the expected network + let is_mainnet = is_xpub_mainnet(xpub)?; + if (is_mainnet && network != Network::Bitcoin) || (!is_mainnet && network == Network::Bitcoin) { + return Err(DescriptorError::XpubNetworkMismatch(xpub.to_string())); + } + + // Parses the descriptor and get an external and change descriptors + let main_desc = generate_descriptor_from_xpub(xpub, false)?; + let change_desc = generate_descriptor_from_xpub(xpub, true)?; + + Ok(vec![ + Descriptor::::from_str(&main_desc)?.to_string(), + Descriptor::::from_str(&change_desc)?.to_string(), + ]) +} + +/// Takes an array of descriptors as `String`, performs sanity checks on each one +/// and returns list of parsed descriptors. +pub fn parse_descriptors( + descriptors: &[String], +) -> Result>, DescriptorError> { + let descriptors = descriptors + .iter() + .map(|descriptor| parse_and_split_descriptor(descriptor)) + .collect::>, _>>()? + .into_iter() + .flatten() + .collect::>(); + Ok(descriptors) +} + +/// Parses a descriptor string, validates it, and splits it into single descriptors. +pub fn parse_and_split_descriptor( + descriptor: &str, +) -> Result>, DescriptorError> { + let descriptor = Descriptor::::from_str(descriptor)?; + descriptor.sanity_check()?; + + let descriptors = descriptor.into_single_descriptors()?; + + Ok(descriptors) +} + +/// Derives addresses from a list of descriptors. +/// Parses each descriptor, validates it, and derives the specified number of addresses +/// starting from the given index. +pub fn derive_addresses_from_list_descriptors( + descriptors: &[String], + index: u32, + quantity: u32, +) -> Result, DescriptorError> { + let mut addresses = Vec::new(); + for desc in descriptors { + addresses.extend_from_slice(&derive_addresses_from_descriptor(desc, index, quantity)?); + } + + Ok(addresses) +} + +/// Derives addresses from a single descriptor string. +/// Splits the descriptor into single descriptors and derives addresses for each one. +pub fn derive_addresses_from_descriptor( + descriptor: &str, + index: u32, + quantity: u32, +) -> Result, DescriptorError> { + let descriptors = parse_and_split_descriptor(descriptor)?; + + let mut addresses = Vec::with_capacity(descriptors.len() * quantity as usize); + for desc in descriptors { + addresses.extend_from_slice(&derive_addresses_from_parsed_descriptor( + desc, index, quantity, + )?); + } + + Ok(addresses) +} + +/// Derives addresses from a parsed descriptor. +/// Generates the specified number of addresses starting from the given index. +pub fn derive_addresses_from_parsed_descriptor( + descriptor: Descriptor, + index: u32, + quantity: u32, +) -> Result, DescriptorError> { + let mut addresses = Vec::with_capacity(quantity as usize); + for i in index..index + quantity { + let address = descriptor.at_derivation_index(i)?.script_pubkey(); + addresses.push(address); + } + + Ok(addresses) +} + +#[cfg(test)] +mod test { + use std::vec; + + use bitcoin::Network; + + use super::*; + + struct TestCase { + xpub: &'static str, + default_descriptor: &'static str, + main_descriptor: &'static str, + change_descriptor: &'static str, + main_address: &'static str, + change_address: &'static str, + main_script: &'static str, + change_script: &'static str, + network: Network, + } + + const TEST_CASE_XPUB: TestCase = TestCase { + xpub: "xpub6CPimhNogJosVzpueNmrWEfSHc2YTXG1ZyE6TBV4Nx6UxZ7zKSGYv9hKxNjiFY5o1vz7QeZa2m6vQmyndDrkECk8cShWYWxe1gqa1xJEkgs", + default_descriptor: "pkh(xpub6CPimhNogJosVzpueNmrWEfSHc2YTXG1ZyE6TBV4Nx6UxZ7zKSGYv9hKxNjiFY5o1vz7QeZa2m6vQmyndDrkECk8cShWYWxe1gqa1xJEkgs/<0;1>/*)", + main_descriptor: "pkh(xpub6CPimhNogJosVzpueNmrWEfSHc2YTXG1ZyE6TBV4Nx6UxZ7zKSGYv9hKxNjiFY5o1vz7QeZa2m6vQmyndDrkECk8cShWYWxe1gqa1xJEkgs/0/*)#32jmvyn7", + change_descriptor: "pkh(xpub6CPimhNogJosVzpueNmrWEfSHc2YTXG1ZyE6TBV4Nx6UxZ7zKSGYv9hKxNjiFY5o1vz7QeZa2m6vQmyndDrkECk8cShWYWxe1gqa1xJEkgs/1/*)#q7h633rx", + main_address: "1JHazecJrjbxBMQgRcyV3JCQJwVbHBjH5t", + change_address: "1JbCXSeZHizJDQANsgtLBjo5y24JNMyGTB", + main_script: "OP_DUP OP_HASH160 OP_PUSHBYTES_20 bd9d2ba0e12d433a4b3c81fbf6457f41a4b37ffe OP_EQUALVERIFY OP_CHECKSIG", + change_script: "OP_DUP OP_HASH160 OP_PUSHBYTES_20 c0f1e6c8977d40f9a8ffe9b06120ae4c2833e9ef OP_EQUALVERIFY OP_CHECKSIG", + network: Network::Bitcoin, + }; + + const TEST_CASE_YPUB: TestCase = TestCase { + xpub: "ypub6XmBfjfmuYD1bjv5RCEHU8jD1NPGZh6NRTGDB8ndQsd7MPnzhDhAsdrF9sK8Z4G9FvcFBHoGsZqhsDHtenca3K5QigYWVKXvkAx6HBxVGYM", + default_descriptor: "sh(wpkh(xpub6CvvN4zrkrfXkSixaqSfG3dhqQEpd56sWLjzPjtk2sFEJHymSZXcFaC78fMYZ9cDrHVSRpCiQuV9yvgKw6CZF5PorLr5uQiSUStStZjpSSV/<0;1>/*))", + main_descriptor: "sh(wpkh(xpub6CvvN4zrkrfXkSixaqSfG3dhqQEpd56sWLjzPjtk2sFEJHymSZXcFaC78fMYZ9cDrHVSRpCiQuV9yvgKw6CZF5PorLr5uQiSUStStZjpSSV/0/*))#657qlqhe", + change_descriptor: "sh(wpkh(xpub6CvvN4zrkrfXkSixaqSfG3dhqQEpd56sWLjzPjtk2sFEJHymSZXcFaC78fMYZ9cDrHVSRpCiQuV9yvgKw6CZF5PorLr5uQiSUStStZjpSSV/1/*))#uhk9ydud", + main_address: "31sQy1RG4Y6sCtCpmXrtiJooqzBozRUTU6", + change_address: "33kzJbaR4EDzEoigsKuLata1svSqNGsdSo", + main_script: "OP_HASH160 OP_PUSHBYTES_20 01f764ff1e1f27740b0b638b0251bec1bece0964 OP_EQUAL", + change_script: "OP_HASH160 OP_PUSHBYTES_20 16b0903438a739fc09bb7e894895df291bb8ee19 OP_EQUAL", + network: Network::Bitcoin, + }; + + const TEST_CASE_ZPUB: TestCase = TestCase { + xpub: "zpub6rFvSvP5VbpXwej2L5WseLfxfdUzSczs9DK9v9mpXgXNqjFhtfUTRGkQKr7sXKNyrrzhd2LCysGqts1oT3b1PJji16xWzcmNMfhmZ8kkLZ1", + default_descriptor: "wpkh(xpub6CbPqb3FCEjaF4LnfMwdEAUxKhC6ZP1sJzGiMMz3mfmcjXdFPM9LB9S8HSChXW593am685964YZk8Hng1ekynqNWGRZfpo8PpDaUmyvQqvY/<0;1>/*)", + main_descriptor: "wpkh(xpub6CbPqb3FCEjaF4LnfMwdEAUxKhC6ZP1sJzGiMMz3mfmcjXdFPM9LB9S8HSChXW593am685964YZk8Hng1ekynqNWGRZfpo8PpDaUmyvQqvY/0/*)#z2djk607", + change_descriptor: "wpkh(xpub6CbPqb3FCEjaF4LnfMwdEAUxKhC6ZP1sJzGiMMz3mfmcjXdFPM9LB9S8HSChXW593am685964YZk8Hng1ekynqNWGRZfpo8PpDaUmyvQqvY/1/*)#n7gnt0lx", + main_address: "bc1qz4ta3h4ga6hdqa090wfpr83asyz5z40t272wez", + change_address: "bc1qjeq39p3mpvmwqwkpaqe9hdjgfhfa8w5z87tnp4", + main_script: "OP_0 OP_PUSHBYTES_20 1557d8dea8eeaed075e57b92119e3d81054155eb", + change_script: "OP_0 OP_PUSHBYTES_20 964112863b0b36e03ac1e8325bb6484dd3d3ba82", + network: Network::Bitcoin, + }; + + const TEST_CASE_TPUB: TestCase = TestCase { + xpub: "tpubDC73PMTHeKDXnFwNFz8CLBy2VVx4D85WW2vbzwVLwCD9zkQ6Vj97muhLRTbKvmue1PyVQLwizvBW6v2SD1LnzbeuHnRsDYQZGE8urTZHMn5", + default_descriptor: "pkh(tpubDC73PMTHeKDXnFwNFz8CLBy2VVx4D85WW2vbzwVLwCD9zkQ6Vj97muhLRTbKvmue1PyVQLwizvBW6v2SD1LnzbeuHnRsDYQZGE8urTZHMn5/<0;1>/*)", + main_descriptor: "pkh(tpubDC73PMTHeKDXnFwNFz8CLBy2VVx4D85WW2vbzwVLwCD9zkQ6Vj97muhLRTbKvmue1PyVQLwizvBW6v2SD1LnzbeuHnRsDYQZGE8urTZHMn5/0/*)#8zp7ryrl", + change_descriptor: "pkh(tpubDC73PMTHeKDXnFwNFz8CLBy2VVx4D85WW2vbzwVLwCD9zkQ6Vj97muhLRTbKvmue1PyVQLwizvBW6v2SD1LnzbeuHnRsDYQZGE8urTZHMn5/1/*)#kkyl73n8", + main_address: "mhk8YjtyHigqGMiEGaf8cnNW9Game9exC6", + change_address: "mmuYagUFFQtAzw8Ts7afED6HFboCy4e8WR", + main_script: "OP_DUP OP_HASH160 OP_PUSHBYTES_20 186e37d051208d814da8988b596e515ac79c0336 OP_EQUALVERIFY OP_CHECKSIG", + change_script: "OP_DUP OP_HASH160 OP_PUSHBYTES_20 461686e57db4157808a9d4e935ae35d60fae0676 OP_EQUALVERIFY OP_CHECKSIG", + network: Network::Testnet, + }; + + const TEST_CASE_UPUB: TestCase = TestCase { + xpub: "upub5E3Vhaq9uVmz426B5FME1csAY8tvQ8vRqt7WnGyiJ4CoknpyM2WJk4B6uSh2kud3r8RJHTzS5jLFnWNRThKZyew6tDX2eXGMyTvfa8AVwyK", + default_descriptor: "sh(wpkh(tpubDCuv8pfb4pMsshrP2WhBqoV3PARvDPPz8rGUV1iWmz6LfNwNBDr5kgpMD6eaH8Y3rxJd9UHyzpDx8Yhj1eQrFoSCYqMc5nP4Nbi1VvJmNco/<0;1>/*))", + main_descriptor: "sh(wpkh(tpubDCuv8pfb4pMsshrP2WhBqoV3PARvDPPz8rGUV1iWmz6LfNwNBDr5kgpMD6eaH8Y3rxJd9UHyzpDx8Yhj1eQrFoSCYqMc5nP4Nbi1VvJmNco/0/*))#sh4fvsj4", + change_descriptor: "sh(wpkh(tpubDCuv8pfb4pMsshrP2WhBqoV3PARvDPPz8rGUV1iWmz6LfNwNBDr5kgpMD6eaH8Y3rxJd9UHyzpDx8Yhj1eQrFoSCYqMc5nP4Nbi1VvJmNco/1/*))#k5avhaep", + main_address: "2NBfJvMZadWb8mwtV3F4FXTqAJs3pkYNdn8", + change_address: "2MznomgtTHMBvsMqPwwE3sSLzj6F8w3Mnyi", + main_script: "OP_HASH160 OP_PUSHBYTES_20 ca005d4bd7b470a9e12710789cac3812c16146a4 OP_EQUAL", + change_script: "OP_HASH160 OP_PUSHBYTES_20 52c1f9cdaa84c6552678051e6322a8f5ff6687ae OP_EQUAL", + network: Network::Testnet, + }; + + const TEST_CASE_VPUB: TestCase = TestCase { + xpub: "vpub5Zrsj9pYeJLwTfggbSQYZDdpEpZ4M1qB1EUKfXB9bjsookSNjM6c6eFTYfjb8KcGJV4ZqAYScBvC7hyDbbWKCHVcC6RETNJUfwUFvnHJM8Y", + default_descriptor: "wpkh(tpubDDu2riz4ewPMS4FmiLxtBKABuswcDeKEP674as24hfPTfEjYJtGpVDEZq7jYedsLufq5whFS4cTLaTgxRrBagCK6zNZPJibgoMBxTvUcVFf/<0;1>/*)", + main_descriptor: "wpkh(tpubDDu2riz4ewPMS4FmiLxtBKABuswcDeKEP674as24hfPTfEjYJtGpVDEZq7jYedsLufq5whFS4cTLaTgxRrBagCK6zNZPJibgoMBxTvUcVFf/0/*)#f8w55tty", + change_descriptor: "wpkh(tpubDDu2riz4ewPMS4FmiLxtBKABuswcDeKEP674as24hfPTfEjYJtGpVDEZq7jYedsLufq5whFS4cTLaTgxRrBagCK6zNZPJibgoMBxTvUcVFf/1/*)#cnt4f7mu", + main_address: "tb1q7e5q2y0mpvesst3jxhe45q0e2q9gdkfd6zxzqa", + change_address: "tb1qzplphjt68gs0lwvxrq70t9j9cva8ky7r7ucz2g", + main_script: "OP_0 OP_PUSHBYTES_20 f6680511fb0b33082e3235f35a01f9500a86d92d", + change_script: "OP_0 OP_PUSHBYTES_20 107e1bc97a3a20ffb986183cf59645c33a7b13c3", + network: Network::Testnet, + }; + + const TEST_CASE_VPUB_REGTEST: TestCase = TestCase { + xpub: "vpub5Zrsj9pYeJLwTfggbSQYZDdpEpZ4M1qB1EUKfXB9bjsookSNjM6c6eFTYfjb8KcGJV4ZqAYScBvC7hyDbbWKCHVcC6RETNJUfwUFvnHJM8Y", + default_descriptor: "wpkh(tpubDDu2riz4ewPMS4FmiLxtBKABuswcDeKEP674as24hfPTfEjYJtGpVDEZq7jYedsLufq5whFS4cTLaTgxRrBagCK6zNZPJibgoMBxTvUcVFf/<0;1>/*)", + main_descriptor: "wpkh(tpubDDu2riz4ewPMS4FmiLxtBKABuswcDeKEP674as24hfPTfEjYJtGpVDEZq7jYedsLufq5whFS4cTLaTgxRrBagCK6zNZPJibgoMBxTvUcVFf/0/*)#f8w55tty", + change_descriptor: "wpkh(tpubDDu2riz4ewPMS4FmiLxtBKABuswcDeKEP674as24hfPTfEjYJtGpVDEZq7jYedsLufq5whFS4cTLaTgxRrBagCK6zNZPJibgoMBxTvUcVFf/1/*)#cnt4f7mu", + main_address: "bcrt1q7e5q2y0mpvesst3jxhe45q0e2q9gdkfdctl0h5", + change_address: "bcrt1qzplphjt68gs0lwvxrq70t9j9cva8ky7ru4p0ap", + main_script: "OP_0 OP_PUSHBYTES_20 f6680511fb0b33082e3235f35a01f9500a86d92d", + change_script: "OP_0 OP_PUSHBYTES_20 107e1bc97a3a20ffb986183cf59645c33a7b13c3", + network: Network::Regtest, + }; + + const TEST_CASES: [&TestCase; 7] = [ + &TEST_CASE_XPUB, + &TEST_CASE_YPUB, + &TEST_CASE_ZPUB, + &TEST_CASE_TPUB, + &TEST_CASE_UPUB, + &TEST_CASE_VPUB, + &TEST_CASE_VPUB_REGTEST, + ]; + + #[test] + fn test_parse_xpub_valid_cases() { + let cases = TEST_CASES; + + for &tc in &cases { + let descriptors_string = parse_xpub(tc.xpub, tc.network).unwrap(); + assert_eq!(descriptors_string.len(), 2); + assert_eq!(descriptors_string[0], tc.main_descriptor); + assert_eq!(descriptors_string[1], tc.change_descriptor); + + let descriptors = parse_descriptors(&descriptors_string).unwrap(); + assert_eq!(descriptors.len(), 2); + + let main_desc = descriptors[0].clone(); + let main_address = main_desc + .at_derivation_index(0) + .unwrap() + .address(tc.network) + .unwrap(); + assert_eq!(main_address.to_string(), tc.main_address); + + let change_desc = descriptors[1].clone(); + let change_address = change_desc + .at_derivation_index(0) + .unwrap() + .address(tc.network) + .unwrap(); + assert_eq!(change_address.to_string(), tc.change_address); + } + } + + #[test] + fn test_parse_xpub_with_correct_network() { + fn check(xpub: &str, network: Network) { + let parsed = parse_xpub(xpub, network); + assert!(parsed.is_ok()); + } + + let cases = TEST_CASES; + + for &tc in &cases { + check(tc.xpub, tc.network); + } + } + + #[test] + fn test_parse_xpub_with_wrong_network() { + fn check(xpub: &str, network: Network) { + let wrong_network = if network == Network::Bitcoin { + vec![Network::Testnet, Network::Regtest, Network::Signet] + } else { + vec![Network::Bitcoin] + }; + + for net in wrong_network { + let parsed = parse_xpub(xpub, net); + let err = parsed.err().unwrap(); + + if let DescriptorError::XpubNetworkMismatch(actual) = err { + assert_eq!(actual, xpub.to_string()); + } else { + panic!("Expected XpubNetworkMismatch error"); + } + } + } + + let cases = TEST_CASES; + + for &tc in &cases { + check(tc.xpub, tc.network); + } + } + + #[test] + fn test_parse_descriptors_valid_cases() { + // singlesig + for cases in TEST_CASES { + assert_eq!( + parse_descriptors(&[cases.default_descriptor.to_owned()]).unwrap(), + parse_descriptors(&[ + cases.main_descriptor.to_owned(), + cases.change_descriptor.to_owned() + ]) + .unwrap() + ); + } + + // multisig + assert_eq!( + parse_descriptors(&[ + "wsh(sortedmulti(1,[6f826a6a/48h/0h/0h/2h]xpub6DsY48BAsvEMTRPbeSTu9jZXqEsTKr5T86WbRbXHp2gEVCNR3hALnMorFawVwnnHMMfjbyY8We9B4beh1fxqhcv6kgSeLgQxeXDqv3DaW7m/<0;1>/*,[a5b13c0e/48h/0h/0h/2h]xpub6Eqj1Hj3RezebC6cKiYYN2sAc1Wu33BWoaafnNgAbQwDkJdy7aXCYCmaMzb8rCpmh919UsehyV5Ywjo62hG4R2G2PGv4uqEDTUhYQw26BDJ/<0;1>/*))#nykmcu2v".to_owned() + ]).unwrap(), + parse_descriptors(&[ + "wsh(sortedmulti(1,[6f826a6a/48'/0'/0'/2']xpub6DsY48BAsvEMTRPbeSTu9jZXqEsTKr5T86WbRbXHp2gEVCNR3hALnMorFawVwnnHMMfjbyY8We9B4beh1fxqhcv6kgSeLgQxeXDqv3DaW7m/0/*,[a5b13c0e/48'/0'/0'/2']xpub6Eqj1Hj3RezebC6cKiYYN2sAc1Wu33BWoaafnNgAbQwDkJdy7aXCYCmaMzb8rCpmh919UsehyV5Ywjo62hG4R2G2PGv4uqEDTUhYQw26BDJ/0/*))#sw68w95x".to_owned(), + "wsh(sortedmulti(1,[6f826a6a/48'/0'/0'/2']xpub6DsY48BAsvEMTRPbeSTu9jZXqEsTKr5T86WbRbXHp2gEVCNR3hALnMorFawVwnnHMMfjbyY8We9B4beh1fxqhcv6kgSeLgQxeXDqv3DaW7m/1/*,[a5b13c0e/48'/0'/0'/2']xpub6Eqj1Hj3RezebC6cKiYYN2sAc1Wu33BWoaafnNgAbQwDkJdy7aXCYCmaMzb8rCpmh919UsehyV5Ywjo62hG4R2G2PGv4uqEDTUhYQw26BDJ/1/*))#fafrqkpn".to_owned() + ]).unwrap() + ); + } + + #[test] + fn test_parse_and_split_descriptor_valid_cases() { + for cases in TEST_CASES { + let descriptors = parse_and_split_descriptor(cases.default_descriptor).unwrap(); + let expected_descriptor = [cases.main_descriptor, cases.change_descriptor] + .iter() + .map(|d| Descriptor::::from_str(d).unwrap()) + .collect::>(); + + assert_eq!(descriptors, expected_descriptor); + } + } + + #[test] + fn test_derive_addresses_from_list_descriptors_valid_cases() { + let cases = TEST_CASES; + let all_default_descriptors: Vec = cases + .iter() + .map(|tc| tc.default_descriptor.to_string()) + .collect(); + + let all_script_buff: Vec = cases + .iter() + .flat_map(|tc| vec![tc.main_script.to_string(), tc.change_script.to_string()]) + .collect(); + + let addresses_derived = + derive_addresses_from_list_descriptors(&all_default_descriptors, 0, 1).unwrap(); + + assert_eq!(addresses_derived.len(), all_script_buff.len()); + assert_eq!( + addresses_derived + .iter() + .map(|script| script.to_string()) + .collect::>(), + all_script_buff + ); + } + + #[test] + fn test_derive_addresses_from_descriptor_valid_cases() { + let cases = TEST_CASES; + for &tc in &cases { + let list_script_buff = + derive_addresses_from_descriptor(tc.default_descriptor, 0, 1).unwrap(); + assert_eq!(list_script_buff.len(), 2); + assert_eq!(list_script_buff[0].to_string(), tc.main_script); + assert_eq!(list_script_buff[1].to_string(), tc.change_script); + + let main_script_buff = + derive_addresses_from_descriptor(tc.main_descriptor, 0, 1).unwrap(); + assert_eq!(main_script_buff.len(), 1); + assert_eq!(main_script_buff[0].to_string(), tc.main_script); + + let change_script_buff = + derive_addresses_from_descriptor(tc.change_descriptor, 0, 1).unwrap(); + assert_eq!(change_script_buff.len(), 1); + assert_eq!(change_script_buff[0].to_string(), tc.change_script); + } + } + + #[test] + fn test_derive_addresses_from_parsed_descriptor_valid_cases() { + for &tc in &TEST_CASES { + let descriptor = parse_and_split_descriptor(tc.main_descriptor).unwrap()[0].clone(); + let derived_addresses = + derive_addresses_from_parsed_descriptor(descriptor, 0, 1).unwrap(); + + assert_eq!(derived_addresses.len(), 1); + assert_eq!(derived_addresses[0].to_string(), tc.main_script); + } + } + + #[test] + fn test_invalid_descriptor_parsing() { + fn check(result: Result>, DescriptorError>) { + assert!(result.is_err()); + if let Err(DescriptorError::MiniscriptError(_)) = result { + // Expected error + } else { + panic!("Expected MiniscriptError"); + } + } + + let invalid_descriptor = "invalid(descriptor)"; + + let result = parse_descriptors(&[invalid_descriptor.to_string()]); + check(result); + + let result = parse_and_split_descriptor(invalid_descriptor); + check(result); + } + + #[test] + fn test_derive_addresses_with_invalid_descriptor() { + fn check(result: Result, DescriptorError>) { + assert!(result.is_err()); + if let Err(DescriptorError::MiniscriptError(_)) = result { + // Expected error + } else { + panic!("Expected MiniscriptError"); + } + } + let invalid_descriptor = "invalid(descriptor)"; + + let result = derive_addresses_from_descriptor(invalid_descriptor, 0, 1); + check(result); + + let result = + derive_addresses_from_list_descriptors(&[invalid_descriptor.to_string()], 0, 1); + check(result); + } +} diff --git a/crates/floresta-watch-only/src/descriptor/slip132.rs b/crates/floresta-watch-only/src/descriptor/slip132.rs new file mode 100644 index 000000000..6cabc6b68 --- /dev/null +++ b/crates/floresta-watch-only/src/descriptor/slip132.rs @@ -0,0 +1,376 @@ +// Based on slip132 from LNP/BP Descriptor Wallet library by: +// Dr. Maxim Orlovsky +// +// Adapted for Floresta by: +// Davidson Sousa + +//! Bitcoin SLIP-132 standard implementation for parsing custom xpub/xpriv key +//! formats + +use core::fmt; +use core::fmt::Debug; + +use bitcoin::base58; +use bitcoin::bip32; +use bitcoin::bip32::Xpub; + +/// Magical version bytes for xpub: bitcoin mainnet public key for P2PKH or P2SH +pub const VERSION_MAGIC_XPUB: [u8; 4] = [0x04, 0x88, 0xB2, 0x1E]; +/// Magical version bytes for xprv: bitcoin mainnet private key for P2PKH or +/// P2SH +pub const VERSION_MAGIC_XPRV: [u8; 4] = [0x04, 0x88, 0xAD, 0xE4]; +/// Magical version bytes for ypub: bitcoin mainnet public key for P2WPKH in +/// P2SH +pub const VERSION_MAGIC_YPUB: [u8; 4] = [0x04, 0x9D, 0x7C, 0xB2]; +/// Magical version bytes for yprv: bitcoin mainnet private key for P2WPKH in +/// P2SH +pub const VERSION_MAGIC_YPRV: [u8; 4] = [0x04, 0x9D, 0x78, 0x78]; +/// Magical version bytes for zpub: bitcoin mainnet public key for P2WPKH +pub const VERSION_MAGIC_ZPUB: [u8; 4] = [0x04, 0xB2, 0x47, 0x46]; +/// Magical version bytes for zprv: bitcoin mainnet private key for P2WPKH +pub const VERSION_MAGIC_ZPRV: [u8; 4] = [0x04, 0xB2, 0x43, 0x0C]; +/// Magical version bytes for Ypub: bitcoin mainnet public key for +/// multi-signature P2WSH in P2SH +pub const VERSION_MAGIC_YPUB_MULTISIG: [u8; 4] = [0x02, 0x95, 0xb4, 0x3f]; +/// Magical version bytes for Yprv: bitcoin mainnet private key for +/// multi-signature P2WSH in P2SH +pub const VERSION_MAGIC_YPRV_MULTISIG: [u8; 4] = [0x02, 0x95, 0xb0, 0x05]; +/// Magical version bytes for Zpub: bitcoin mainnet public key for +/// multi-signature P2WSH +pub const VERSION_MAGIC_ZPUB_MULTISIG: [u8; 4] = [0x02, 0xaa, 0x7e, 0xd3]; +/// Magical version bytes for Zprv: bitcoin mainnet private key for +/// multi-signature P2WSH +pub const VERSION_MAGIC_ZPRV_MULTISIG: [u8; 4] = [0x02, 0xaa, 0x7a, 0x99]; + +/// Magical version bytes for tpub: bitcoin testnet/regtest public key for +/// P2PKH or P2SH +pub const VERSION_MAGIC_TPUB: [u8; 4] = [0x04, 0x35, 0x87, 0xCF]; +/// Magical version bytes for tprv: bitcoin testnet/regtest private key for +/// P2PKH or P2SH +pub const VERSION_MAGIC_TPRV: [u8; 4] = [0x04, 0x35, 0x83, 0x94]; +/// Magical version bytes for upub: bitcoin testnet/regtest public key for +/// P2WPKH in P2SH +pub const VERSION_MAGIC_UPUB: [u8; 4] = [0x04, 0x4A, 0x52, 0x62]; +/// Magical version bytes for uprv: bitcoin testnet/regtest private key for +/// P2WPKH in P2SH +pub const VERSION_MAGIC_UPRV: [u8; 4] = [0x04, 0x4A, 0x4E, 0x28]; +/// Magical version bytes for vpub: bitcoin testnet/regtest public key for +/// P2WPKH +pub const VERSION_MAGIC_VPUB: [u8; 4] = [0x04, 0x5F, 0x1C, 0xF6]; +/// Magical version bytes for vprv: bitcoin testnet/regtest private key for +/// P2WPKH +pub const VERSION_MAGIC_VPRV: [u8; 4] = [0x04, 0x5F, 0x18, 0xBC]; +/// Magical version bytes for Upub: bitcoin testnet/regtest public key for +/// multi-signature P2WSH in P2SH +pub const VERSION_MAGIC_UPUB_MULTISIG: [u8; 4] = [0x02, 0x42, 0x89, 0xef]; +/// Magical version bytes for Uprv: bitcoin testnet/regtest private key for +/// multi-signature P2WSH in P2SH +pub const VERSION_MAGIC_UPRV_MULTISIG: [u8; 4] = [0x02, 0x42, 0x85, 0xb5]; +/// Magical version bytes for Zpub: bitcoin testnet/regtest public key for +/// multi-signature P2WSH +pub const VERSION_MAGIC_VPUB_MULTISIG: [u8; 4] = [0x02, 0x57, 0x54, 0x83]; +/// Magical version bytes for Zprv: bitcoin testnet/regtest private key for +/// multi-signature P2WSH +pub const VERSION_MAGIC_VPRV_MULTISIG: [u8; 4] = [0x02, 0x57, 0x50, 0x48]; + +/// Extended public and private key processing errors +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum Error { + /// error in BASE58 key encoding. Details: {0} + Base58(base58::Error), + + /// error in hex key encoding. Details: {0} + Hex(bitcoin::hashes::hex::HexToArrayError), + + /// pk->pk derivation was attempted on a hardened key. + CannotDeriveFromHardenedKey, + + /// child number {0} is out of range. + InvalidChildNumber(u32), + + /// invalid child number format. + InvalidChildNumberFormat, + + /// invalid derivation path format. + InvalidDerivationPathFormat, + + /// unknown version magic bytes {0:#06X?} + UnknownVersion([u8; 4]), + + /// encoded extended key data has wrong length {0} + WrongExtendedKeyLength(usize), + + /// unrecognized or unsupported extended key prefix (please check SLIP 32 + /// for possible values) + UnknownSlip32Prefix, + + /// failure in rust bitcoin library + InternalFailure, + + /// No support for xpriv + NoSupportXpriv, + + /// No multisig support via xpub. + NoSupportXpubMultisig, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Base58(e) => write!(f, "Base58 error: {}", e), + Error::Hex(e) => write!(f, "Hex error: {}", e), + Error::CannotDeriveFromHardenedKey => { + write!(f, "Cannot derive from hardened key") + } + Error::InvalidChildNumber(no) => write!(f, "Invalid child number: {}", no), + Error::InvalidChildNumberFormat => write!(f, "Invalid child number format"), + Error::InvalidDerivationPathFormat => write!(f, "Invalid derivation path format"), + Error::UnknownVersion(ver) => write!(f, "Unknown version bytes: {:02X?}", ver), + Error::WrongExtendedKeyLength(len) => { + write!(f, "Wrong extended key length: {}", len) + } + Error::UnknownSlip32Prefix => write!(f, "Unknown SLIP-132 prefix"), + Error::InternalFailure => write!(f, "Internal failure"), + Error::NoSupportXpriv => write!(f, "No support for xpriv keys"), + Error::NoSupportXpubMultisig => write!(f, "No support for xpub multisig keys"), + } + } +} + +fn extract_slip132_prefix(s: &str) -> Result<[u8; 4], Error> { + let data = base58::decode_check(s)?; + let mut prefix = [0u8; 4]; + prefix.copy_from_slice(&data[0..4]); + + validate_slip132_prefix(prefix)?; + + Ok(prefix) +} + +fn validate_slip132_prefix(prefix: [u8; 4]) -> Result<(), Error> { + match prefix { + VERSION_MAGIC_XPUB | VERSION_MAGIC_YPUB | VERSION_MAGIC_ZPUB | VERSION_MAGIC_TPUB + | VERSION_MAGIC_UPUB | VERSION_MAGIC_VPUB => Ok(()), + + VERSION_MAGIC_XPRV | VERSION_MAGIC_YPRV | VERSION_MAGIC_ZPRV | VERSION_MAGIC_TPRV + | VERSION_MAGIC_UPRV | VERSION_MAGIC_VPRV => Err(Error::NoSupportXpriv), + + VERSION_MAGIC_YPUB_MULTISIG + | VERSION_MAGIC_ZPUB_MULTISIG + | VERSION_MAGIC_UPUB_MULTISIG + | VERSION_MAGIC_VPUB_MULTISIG + | VERSION_MAGIC_YPRV_MULTISIG + | VERSION_MAGIC_ZPRV_MULTISIG + | VERSION_MAGIC_UPRV_MULTISIG + | VERSION_MAGIC_VPRV_MULTISIG => Err(Error::NoSupportXpubMultisig), + + _ => Err(Error::UnknownSlip32Prefix), + } +} + +impl From for Error { + fn from(err: bip32::Error) -> Self { + match err { + bip32::Error::CannotDeriveFromHardenedKey => Error::CannotDeriveFromHardenedKey, + bip32::Error::InvalidChildNumber(no) => Error::InvalidChildNumber(no), + bip32::Error::InvalidChildNumberFormat => Error::InvalidChildNumberFormat, + bip32::Error::InvalidDerivationPathFormat => Error::InvalidDerivationPathFormat, + bip32::Error::Secp256k1(_) => Error::InternalFailure, + bip32::Error::UnknownVersion(ver) => Error::UnknownVersion(ver), + bip32::Error::WrongExtendedKeyLength(len) => Error::WrongExtendedKeyLength(len), + bip32::Error::Base58(err) => Error::Base58(err), + bip32::Error::Hex(err) => Error::Hex(err), + _ => Error::InternalFailure, + } + } +} + +impl From for Error { + fn from(err: base58::Error) -> Self { + Error::Base58(err) + } +} + +/// Trait for building standard BIP32 extended keys from SLIP132 variant. +pub trait FromSlip132 { + /// Constructs standard BIP32 extended key from SLIP132 string. + fn from_slip132_str(s: &str) -> Result + where + Self: Sized; +} + +impl FromSlip132 for Xpub { + fn from_slip132_str(s: &str) -> Result { + let mut data = base58::decode_check(s)?; + + let prefix: [u8; 4] = extract_slip132_prefix(s)?; + let slice = match prefix { + VERSION_MAGIC_XPUB | VERSION_MAGIC_YPUB | VERSION_MAGIC_ZPUB => VERSION_MAGIC_XPUB, + + VERSION_MAGIC_TPUB | VERSION_MAGIC_UPUB | VERSION_MAGIC_VPUB => VERSION_MAGIC_TPUB, + + _ => return Err(Error::UnknownSlip32Prefix), + }; + data[0..4].copy_from_slice(&slice); + + let xpub = Xpub::decode(&data)?; + + Ok(xpub) + } +} + +/// Generates a descriptor based on the provided xpub. +/// The descriptor type is determined by the xpub's prefix: +/// - P2PKH for xpub/tpub (Legacy addresses) +/// - P2WPKH-P2SH for ypub/upub (SegWit nested in P2SH) +/// - P2WPKH for zpub/vpub (Native SegWit) +pub fn generate_descriptor_from_xpub(s: &str, change: bool) -> Result { + let index = if change { 1 } else { 0 }; + let xpub = Xpub::from_slip132_str(s)?; + + let prefix = extract_slip132_prefix(s)?; + + match prefix { + VERSION_MAGIC_XPUB | VERSION_MAGIC_TPUB => Ok(format!("pkh({xpub}/{index}/*)")), + VERSION_MAGIC_YPUB | VERSION_MAGIC_UPUB => Ok(format!("sh(wpkh({xpub}/{index}/*))")), + VERSION_MAGIC_ZPUB | VERSION_MAGIC_VPUB => Ok(format!("wpkh({xpub}/{index}/*)")), + + _ => Err(Error::UnknownSlip32Prefix), + } +} + +/// Checks if the xpub belongs to the mainnet based on its prefix. +pub fn is_xpub_mainnet(s: &str) -> Result { + let prefix = extract_slip132_prefix(s)?; + match prefix { + VERSION_MAGIC_XPUB | VERSION_MAGIC_YPUB | VERSION_MAGIC_ZPUB => Ok(true), + + VERSION_MAGIC_TPUB | VERSION_MAGIC_UPUB | VERSION_MAGIC_VPUB => Ok(false), + + _ => Err(Error::UnknownSlip32Prefix), + } +} + +#[cfg(test)] +mod test { + use super::*; + + const XPUB: &str = "xpub6CPimhNogJosVzpueNmrWEfSHc2YTXG1ZyE6TBV4Nx6UxZ7zKSGYv9hKxNjiFY5o1vz7QeZa2m6vQmyndDrkECk8cShWYWxe1gqa1xJEkgs"; + const XPRIV: &str = "xprv9yQNNBquqwFaHWkSYMEr96ihjaC444YACkJVeo5SpcZW5knqmtxJNMNr781jwtB2PRV1BJMdwAMrQt7DHUumiA7h1BGp2C4h2C1geFtYMzs"; + const YPUB: &str = "ypub6XmBfjfmuYD1bjv5RCEHU8jD1NPGZh6NRTGDB8ndQsd7MPnzhDhAsdrF9sK8Z4G9FvcFBHoGsZqhsDHtenca3K5QigYWVKXvkAx6HBxVGYM"; + const YPRIV: &str = "yprvAJmqGE8t5AeiPFqcKAhH6znUTLYnAENX4ELcNkP1rY68UbTr9gNvKqXmJZe1RsaVZquKb9UR2KkwUTGcN337oFKkyqFRmPKhpdcLLiVQMZ6"; + const ZPUB: &str = "zpub6rFvSvP5VbpXwej2L5WseLfxfdUzSczs9DK9v9mpXgXNqjFhtfUTRGkQKr7sXKNyrrzhd2LCysGqts1oT3b1PJji16xWzcmNMfhmZ8kkLZ1"; + const ZPRIV: &str = "zprvAdGa3QrBfEGEjAeZE3ysHCjE7beW3AH1mzPZ7mNCyLzPxvvZM8ACsURvUbcnW5hV91XfdZNzi8jvPLZ4fubdG4qqKkFFojgd6JN67Hyy8xT"; + + const TPUB: &str = "tpubDC73PMTHeKDXnFwNFz8CLBy2VVx4D85WW2vbzwVLwCD9zkQ6Vj97muhLRTbKvmue1PyVQLwizvBW6v2SD1LnzbeuHnRsDYQZGE8urTZHMn5"; + const TPRIV: &str = "tprv8fR1EwR3VwXrtnuaNLTbvnJuvUS83ntbvjKpiRT3WvQmAG9KsLKXbR5UFKPAiRJKyDxiLd2uovStSJ3Qnov7SgkKK4mUcghwX7KHwjhSEFi"; + const UPUB: &str = "upub5E3Vhaq9uVmz426B5FME1csAY8tvQ8vRqt7WnGyiJ4CoknpyM2WJk4B6uSh2kud3r8RJHTzS5jLFnWNRThKZyew6tDX2eXGMyTvfa8AVwyK"; + const UPRIV: &str = "uprv9149J5JG58DgqY1hyDpDeUvRz74RzgCaUfBuyta6jifpszVpoVC4CFrd4D7E5QFmzdYJ1EuLFq9Ge4TvCAymG8cPt3QLz7UJ8Fpsiwgg7Lg"; + const VPUB: &str = "vpub5Zrsj9pYeJLwTfggbSQYZDdpEpZ4M1qB1EUKfXB9bjsookSNjM6c6eFTYfjb8KcGJV4ZqAYScBvC7hyDbbWKCHVcC6RETNJUfwUFvnHJM8Y"; + const VPRIV: &str = "vprv9LsXKeHeovneFBcDVQsYC5h5gniZwZ7Ke1Yis8mY3QLpvx7EBonMYqvyhMwbmAfR5SXLA2byEyPbB3uAajJhAr2NNeM91WfAwRS1KcjCeFo"; + + const YPRIV_MULTISIG: &str = "YprvARd5Qbw8ES2RHXALKfJ8v2hnKe3iDZNuG6kupDzPxXJicdKJ4Mc3TN8D1GNHsQsNTS7wGPts88TJ8pjKKCYLon9GopLzQ6fr9VcTWRUxbLq"; + const YPUB_MULTISIG: &str = "Ypub6ecRp7U24oaiW1EoRgq9HAeWsftCd26kdKgWccQ1WrqhVReSbtvJ1ASgrXLA4CSKw11yatzpmyYy3LraoW2E7kd7X32fTnwdqHnESpnyrKb"; + const ZPRIV_MULTISIG: &str = "ZprvAkTLiGc3P7Zu8pMTA25m87oHVcCAABNQBDH8bctHLXgbfj8XK1mc5RnM2UKssKXHs5Ek1sVRanor27Lt2txMc1psgA3Qz1VLRDg6u4gAyFo"; + const ZPUB_MULTISIG: &str = "Zpub6ySh7n8wDV8CMJRvG3cmVFk23e2eZe6FYSCjQ1HttsDaYXTfrZ5rdE6psjHk476FLe8nLNbPEduWvdU9XCSEuzJiPNj63hm871qsqUVx7kC"; + const UPRIV_MULTISIG: &str = "Uprv98J2BwFTdhrVtLPrzE9e5gKmdmTvT5Qubeg2geQrSVoCQE4P3iwny7VevSXwsnFgpseiGVWdHV36bgH4SQtHcqQsLTZJ4TPu4bMsx8v4vMj"; + const UPUB_MULTISIG: &str = "Upub5MHNbSnMU5Qo6pUL6FgeSpGWBoJQrY8kxsbdV2pTzqLBH2PXbGG3Wup8mhVp4ZpeJSYkazcawL8mWCQKviNAvoti3gEy89fgkPXetXJzNZt"; + const VPRIV_MULTSIG: &str = "Vprv19RbkhR2UPGc4edf2xogFVhtnjvvdpwMJgX4yug3xZufVdwzDXc1xUrQcnaQMZLgp3mPQVD5NQ9QFdwktTo1LgumfqiidCHCfTaY2MtjLrZ"; + const VPUB_MULTSIG: &str = "Vpub5g7du7TGckxGx7fSvcUGeuN1MmSroA8Fsz7rGRiMNqi4L8CkqvRc8yUGnuTQ4UUZi5fZLUD9PzVKPV1teQnBj3aJv1wPi4VB27bJH5b5suR"; + + fn create_invalid_slip32_base58(reference: &str) -> String { + let mut data = base58::decode_check(reference).unwrap(); + // Change prefix + data[3] = 0x21; + + // Remove checksum + data.truncate(data.len() - 4); + + base58::encode_check(&data) + } + + #[test] + fn test_check_unknown_slip32_prefix_error() { + let cases = [ + create_invalid_slip32_base58(XPUB), + create_invalid_slip32_base58(YPUB), + create_invalid_slip32_base58(ZPUB), + create_invalid_slip32_base58(TPUB), + create_invalid_slip32_base58(UPUB), + create_invalid_slip32_base58(VPUB), + ]; + + for key in cases.iter() { + let result = extract_slip132_prefix(key); + assert_eq!(result.err().unwrap(), Error::UnknownSlip32Prefix); + } + } + + #[test] + fn test_verify_multisig_prefix_error() { + let cases = &[ + YPRIV_MULTISIG, + YPUB_MULTISIG, + ZPRIV_MULTISIG, + ZPUB_MULTISIG, + UPRIV_MULTISIG, + UPUB_MULTISIG, + VPRIV_MULTSIG, + VPUB_MULTSIG, + ]; + for key in cases.iter() { + let result = extract_slip132_prefix(key); + assert_eq!(result.err().unwrap(), Error::NoSupportXpubMultisig); + } + } + + #[test] + fn test_check_xpriv_support_error() { + let cases = &[(XPRIV), (YPRIV), (ZPRIV), (TPRIV), (UPRIV), (VPRIV)]; + + for key in cases.iter() { + let result = extract_slip132_prefix(key); + assert_eq!(result.err().unwrap(), Error::NoSupportXpriv); + } + } + + #[test] + fn test_validate_network_xpub() { + let cases = &[ + (XPUB, true), + (YPUB, true), + (ZPUB, true), + (TPUB, false), + (UPUB, false), + (VPUB, false), + ]; + + for &(key, change) in cases.iter() { + let result = is_xpub_mainnet(key); + assert_eq!(result.unwrap(), change); + } + } + + #[test] + fn test_descriptor_generation_for_xpub() { + let cases: &[(&str, bool, &str); 12] = &[ + (XPUB, true, "pkh(xpub6CPimhNogJosVzpueNmrWEfSHc2YTXG1ZyE6TBV4Nx6UxZ7zKSGYv9hKxNjiFY5o1vz7QeZa2m6vQmyndDrkECk8cShWYWxe1gqa1xJEkgs/1/*)"), + (XPUB, false, "pkh(xpub6CPimhNogJosVzpueNmrWEfSHc2YTXG1ZyE6TBV4Nx6UxZ7zKSGYv9hKxNjiFY5o1vz7QeZa2m6vQmyndDrkECk8cShWYWxe1gqa1xJEkgs/0/*)"), + (YPUB, true, "sh(wpkh(xpub6CvvN4zrkrfXkSixaqSfG3dhqQEpd56sWLjzPjtk2sFEJHymSZXcFaC78fMYZ9cDrHVSRpCiQuV9yvgKw6CZF5PorLr5uQiSUStStZjpSSV/1/*))"), + (YPUB, false, "sh(wpkh(xpub6CvvN4zrkrfXkSixaqSfG3dhqQEpd56sWLjzPjtk2sFEJHymSZXcFaC78fMYZ9cDrHVSRpCiQuV9yvgKw6CZF5PorLr5uQiSUStStZjpSSV/0/*))"), + (ZPUB, true, "wpkh(xpub6CbPqb3FCEjaF4LnfMwdEAUxKhC6ZP1sJzGiMMz3mfmcjXdFPM9LB9S8HSChXW593am685964YZk8Hng1ekynqNWGRZfpo8PpDaUmyvQqvY/1/*)"), + (ZPUB, false, "wpkh(xpub6CbPqb3FCEjaF4LnfMwdEAUxKhC6ZP1sJzGiMMz3mfmcjXdFPM9LB9S8HSChXW593am685964YZk8Hng1ekynqNWGRZfpo8PpDaUmyvQqvY/0/*)"), + (TPUB, true, "pkh(tpubDC73PMTHeKDXnFwNFz8CLBy2VVx4D85WW2vbzwVLwCD9zkQ6Vj97muhLRTbKvmue1PyVQLwizvBW6v2SD1LnzbeuHnRsDYQZGE8urTZHMn5/1/*)"), + (TPUB, false, "pkh(tpubDC73PMTHeKDXnFwNFz8CLBy2VVx4D85WW2vbzwVLwCD9zkQ6Vj97muhLRTbKvmue1PyVQLwizvBW6v2SD1LnzbeuHnRsDYQZGE8urTZHMn5/0/*)"), + (UPUB, true, "sh(wpkh(tpubDCuv8pfb4pMsshrP2WhBqoV3PARvDPPz8rGUV1iWmz6LfNwNBDr5kgpMD6eaH8Y3rxJd9UHyzpDx8Yhj1eQrFoSCYqMc5nP4Nbi1VvJmNco/1/*))"), + (UPUB, false, "sh(wpkh(tpubDCuv8pfb4pMsshrP2WhBqoV3PARvDPPz8rGUV1iWmz6LfNwNBDr5kgpMD6eaH8Y3rxJd9UHyzpDx8Yhj1eQrFoSCYqMc5nP4Nbi1VvJmNco/0/*))"), + (VPUB, true , "wpkh(tpubDDu2riz4ewPMS4FmiLxtBKABuswcDeKEP674as24hfPTfEjYJtGpVDEZq7jYedsLufq5whFS4cTLaTgxRrBagCK6zNZPJibgoMBxTvUcVFf/1/*)"), + (VPUB, false, "wpkh(tpubDDu2riz4ewPMS4FmiLxtBKABuswcDeKEP674as24hfPTfEjYJtGpVDEZq7jYedsLufq5whFS4cTLaTgxRrBagCK6zNZPJibgoMBxTvUcVFf/0/*)"), + ]; + + for &(key, change, expect) in cases.iter() { + let result = generate_descriptor_from_xpub(key, change).unwrap(); + assert_eq!(result, expect); + } + } +} diff --git a/crates/floresta-watch-only/src/lib.rs b/crates/floresta-watch-only/src/lib.rs index d53a9170d..034e4693b 100644 --- a/crates/floresta-watch-only/src/lib.rs +++ b/crates/floresta-watch-only/src/lib.rs @@ -12,12 +12,13 @@ use core::cmp::Ordering; use core::fmt::Debug; use bitcoin::hashes::sha256; +use bitcoin::Network; use bitcoin::ScriptBuf; use floresta_chain::BlockConsumer; use floresta_chain::UtxoData; use floresta_common::get_spk_hash; -use floresta_common::parse_descriptors; +pub mod descriptor; pub mod kv_database; #[cfg(any(test, feature = "memory-database"))] pub mod memory_database; @@ -40,11 +41,18 @@ use serde::Serialize; use sync::RwLock; use tracing::error; +use crate::descriptor::derive_addresses_from_descriptor; +use crate::descriptor::derive_addresses_from_list_descriptors; +use crate::descriptor::parse_xpub; +use crate::descriptor::DescriptorError; + #[derive(Debug)] pub enum WatchOnlyError { WalletNotInitialized, TransactionNotFound, DatabaseError(DatabaseError), + DescriptorDuplicate, + InvalidDescriptor(DescriptorError), } impl Display for WatchOnlyError { @@ -59,6 +67,12 @@ impl Display for WatchOnlyError { WatchOnlyError::DatabaseError(e) => { write!(f, "Database error: {e:?}") } + WatchOnlyError::DescriptorDuplicate => { + write!(f, "Descriptor is already cached") + } + WatchOnlyError::InvalidDescriptor(e) => { + write!(f, "Invalid descriptor: {e:?}") + } } } } @@ -346,17 +360,15 @@ impl AddressCacheInner { fn derive_addresses(&mut self) -> Result<(), WatchOnlyError> { let mut stats = self.database.get_stats()?; let descriptors = self.database.descs_get()?; - let descriptors = parse_descriptors(&descriptors).expect("We validate those descriptors"); - for desc in descriptors { - let index = stats.derivation_index; - for idx in index..(index + 100) { - let script = desc - .at_derivation_index(idx) - .expect("We validate those descriptors before saving") - .script_pubkey(); - self.cache_address(script); - } - } + + let addresses = + derive_addresses_from_list_descriptors(&descriptors, stats.derivation_index, 100) + .map_err(WatchOnlyError::InvalidDescriptor)?; + + addresses.iter().for_each(|address| { + self.cache_address(address.clone()); + }); + stats.derivation_index += 100; Ok(self.database.save_stats(&stats)?) } @@ -626,10 +638,10 @@ impl AddressCache { } /// Tells whether or not a descriptor is already cached - pub fn is_cached(&self, desc: &String) -> Result> { + pub fn is_cached(&self, desc: &str) -> Result> { let inner = self.inner.read().expect("poisoned lock"); let known_descs = inner.database.descs_get()?; - Ok(known_descs.contains(desc)) + Ok(known_descs.contains(&String::from(desc))) } /// Tells whether an address is already cached @@ -638,9 +650,38 @@ impl AddressCache { inner.address_map.contains_key(script_hash) } - pub fn push_descriptor(&self, descriptor: &str) -> Result<(), WatchOnlyError> { + /// Adds a descriptor to the wallet, derives addresses, and caches them. + pub fn push_descriptor( + &self, + descriptor: &str, + ) -> Result, WatchOnlyError> { + if self.is_cached(&String::from(descriptor))? { + return Err(WatchOnlyError::DescriptorDuplicate); + } + + let address_descriptors = derive_addresses_from_descriptor(descriptor, 0, 100) + .map_err(WatchOnlyError::InvalidDescriptor)?; + + for address in address_descriptors.clone() { + self.cache_address(address); + } + let inner = self.inner.write().expect("poisoned lock"); - Ok(inner.database.desc_save(descriptor)?) + inner.database.desc_save(descriptor)?; + + Ok(address_descriptors) + } + + /// Adds an XPUB to the wallet, derives descriptors from it, saves these descriptors persistently, + /// derives addresses, and caches them. + pub fn push_xpub(&self, xpub: &str, network: Network) -> Result<(), WatchOnlyError> { + let descriptors = parse_xpub(xpub, network).map_err(WatchOnlyError::InvalidDescriptor)?; + + for descriptor in descriptors { + self.push_descriptor(&descriptor)?; + } + + Ok(()) } pub fn get_position(&self, txid: &Txid) -> Option { @@ -975,7 +1016,7 @@ mod test { // [is_cached], [push_descriptor] let desc = "wsh(sortedmulti(1,[54ff5a12/48h/1h/0h/2h]tpubDDw6pwZA3hYxcSN32q7a5ynsKmWr4BbkBNHydHPKkM4BZwUfiK7tQ26h7USm8kA1E2FvCy7f7Er7QXKF8RNptATywydARtzgrxuPDwyYv4x/<0;1>/*,[bcf969c0/48h/1h/0h/2h]tpubDEFdgZdCPgQBTNtGj4h6AehK79Jm4LH54JrYBJjAtHMLEAth7LuY87awx9ZMiCURFzFWhxToRJK6xp39aqeJWrG5nuW3eBnXeMJcvDeDxfp/<0;1>/*))#fuw35j0q"; cache.push_descriptor(desc).unwrap(); - assert!(cache.is_cached(&desc.to_string()).unwrap()); + assert!(cache.is_cached(desc).unwrap()); // [derive_addresses] cache.derive_addresses().unwrap(); diff --git a/justfile b/justfile index 9800ebf14..bc8134cdf 100644 --- a/justfile +++ b/justfile @@ -148,7 +148,7 @@ gen-manpages path="": # Run typos spell-check: @just check-command typos spell-check "cargo +nightly install typos-cli --locked" - typos + typos --config _typos.toml # Usage: # just install # installs both florestad and floresta-cli diff --git a/tests/example/bitcoin.py b/tests/example/bitcoin.py index 6c9f32abe..25e546381 100644 --- a/tests/example/bitcoin.py +++ b/tests/example/bitcoin.py @@ -7,6 +7,11 @@ from test_framework import FlorestaTestFramework from test_framework.node import NodeType +from test_framework.constants import ( + GENESIS_BLOCK_HASH, + GENESIS_BLOCK_DIFFICULTY_FLOAT, + TEST_CHAIN, +) class BitcoindTest(FlorestaTestFramework): @@ -17,14 +22,6 @@ class BitcoindTest(FlorestaTestFramework): the test do and the expected result in the docstrings """ - expected_chain = "regtest" - expected_height = 0 - expected_headers = 0 - expected_blockhash = ( - "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" - ) - expected_difficulty = 1 - def set_test_params(self): """ Here we define setup for test adding a node definition @@ -54,9 +51,9 @@ def run_test(self): # to perform some kind of action response = self.bitcoind.rpc.get_blockchain_info() - self.assertEqual(response["chain"], BitcoindTest.expected_chain) - self.assertEqual(response["bestblockhash"], BitcoindTest.expected_blockhash) - self.assertTrue(response["difficulty"] > 0) + self.assertEqual(response["chain"], TEST_CHAIN) + self.assertEqual(response["bestblockhash"], GENESIS_BLOCK_HASH) + self.assertEqual(response["difficulty"], GENESIS_BLOCK_DIFFICULTY_FLOAT) self.stop() diff --git a/tests/example/functional.py b/tests/example/functional.py index 50bfa4d0c..26db37771 100644 --- a/tests/example/functional.py +++ b/tests/example/functional.py @@ -7,6 +7,12 @@ from test_framework import FlorestaTestFramework from test_framework.node import NodeType +from test_framework.constants import ( + GENESIS_BLOCK_HEIGHT, + GENESIS_BLOCK_HASH, + GENESIS_BLOCK_DIFFICULTY_INT, + GENESIS_BLOCK_LEAF_COUNT, +) class FunctionalTest(FlorestaTestFramework): @@ -17,11 +23,6 @@ class FunctionalTest(FlorestaTestFramework): the test do and the expected result in the docstrings """ - expected_height = 0 - expected_block = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" - expected_difficulty = 1 - expected_leaf_count = 0 - def set_test_params(self): """ Here we define setup for test adding a node definition @@ -54,10 +55,10 @@ def run_test(self): # Make assertions with our framework. Avoid usage of # native `assert` clauses. For more information, see # https://github.com/getfloresta/Floresta/issues/426 - self.assertEqual(inf_response["height"], FunctionalTest.expected_height) - self.assertEqual(inf_response["best_block"], FunctionalTest.expected_block) - self.assertEqual(inf_response["difficulty"], FunctionalTest.expected_difficulty) - self.assertEqual(inf_response["leaf_count"], FunctionalTest.expected_leaf_count) + self.assertEqual(inf_response["height"], GENESIS_BLOCK_HEIGHT) + self.assertEqual(inf_response["best_block"], GENESIS_BLOCK_HASH) + self.assertEqual(inf_response["difficulty"], GENESIS_BLOCK_DIFFICULTY_INT) + self.assertEqual(inf_response["leaf_count"], GENESIS_BLOCK_LEAF_COUNT) # stop nodes self.stop() diff --git a/tests/example/utreexod.py b/tests/example/utreexod.py index 41940e865..46b433ff0 100644 --- a/tests/example/utreexod.py +++ b/tests/example/utreexod.py @@ -7,6 +7,11 @@ from test_framework import FlorestaTestFramework from test_framework.node import NodeType +from test_framework.constants import ( + TEST_CHAIN, + GENESIS_BLOCK_HASH, + GENESIS_BLOCK_DIFFICULTY_INT, +) class UtreexodTest(FlorestaTestFramework): @@ -17,14 +22,6 @@ class UtreexodTest(FlorestaTestFramework): the test do and the expected result in the docstrings """ - expected_chain = "regtest" - expected_height = 0 - expected_headers = 0 - expected_blockhash = ( - "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" - ) - expected_difficulty = 1 - def set_test_params(self): """ Here we define setup for test adding a node definition @@ -54,13 +51,9 @@ def run_test(self): # to perform some kind of action utreexo_response = self.utreexod.rpc.get_blockchain_info() - self.assertEqual(utreexo_response["chain"], UtreexodTest.expected_chain) - self.assertEqual( - utreexo_response["bestblockhash"], UtreexodTest.expected_blockhash - ) - self.assertEqual( - utreexo_response["difficulty"], UtreexodTest.expected_difficulty - ) + self.assertEqual(utreexo_response["chain"], TEST_CHAIN) + self.assertEqual(utreexo_response["bestblockhash"], GENESIS_BLOCK_HASH) + self.assertEqual(utreexo_response["difficulty"], GENESIS_BLOCK_DIFFICULTY_INT) self.stop() diff --git a/tests/floresta-cli/getbestblockhash.py b/tests/floresta-cli/getbestblockhash.py index a8e860f82..ff73fb5ed 100644 --- a/tests/floresta-cli/getbestblockhash.py +++ b/tests/floresta-cli/getbestblockhash.py @@ -9,6 +9,7 @@ import time from test_framework import FlorestaTestFramework from test_framework.node import NodeType +from test_framework.constants import WALLET_ADDRESS class GetBestblockhashTest(FlorestaTestFramework): @@ -24,8 +25,6 @@ class GetBestblockhashTest(FlorestaTestFramework): of floresta and utreexod, respectively. """ - best_block = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" - def set_test_params(self): """ Setup a florestad node and a utreexod mining node @@ -35,7 +34,7 @@ def set_test_params(self): self.utreexod = self.add_node_extra_args( variant=NodeType.UTREEXOD, extra_args=[ - "--miningaddr=bcrt1q4gfcga7jfjmm02zpvrh4ttc5k7lmnq2re52z2y", + f"--miningaddr={WALLET_ADDRESS}", "--prune=0", ], ) diff --git a/tests/floresta-cli/getblockchaininfo.py b/tests/floresta-cli/getblockchaininfo.py index 04695b9b2..242cfcc99 100644 --- a/tests/floresta-cli/getblockchaininfo.py +++ b/tests/floresta-cli/getblockchaininfo.py @@ -6,6 +6,11 @@ from test_framework import FlorestaTestFramework from test_framework.node import NodeType +from test_framework.constants import ( + GENESIS_BLOCK_HASH, + GENESIS_BLOCK_DIFFICULTY_INT, + GENESIS_BLOCK_HEIGHT, +) class GetBlockchaininfoTest(FlorestaTestFramework): @@ -14,9 +19,6 @@ class GetBlockchaininfoTest(FlorestaTestFramework): """ nodes = [-1] - best_block = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" - difficulty = 1 - height = 0 ibd = True latest_block_time = 1296688602 latest_work = "0000000000000000000000000000000000000000000000000000000000000002" @@ -41,9 +43,9 @@ def run_test(self): # Test assertions response = self.florestad.rpc.get_blockchain_info() - self.assertEqual(response["best_block"], GetBlockchaininfoTest.best_block) - self.assertEqual(response["difficulty"], GetBlockchaininfoTest.difficulty) - self.assertEqual(response["height"], GetBlockchaininfoTest.height) + self.assertEqual(response["best_block"], GENESIS_BLOCK_HASH) + self.assertEqual(response["difficulty"], GENESIS_BLOCK_DIFFICULTY_INT) + self.assertEqual(response["height"], GENESIS_BLOCK_HEIGHT) self.assertEqual(response["ibd"], GetBlockchaininfoTest.ibd) self.assertEqual( response["latest_block_time"], GetBlockchaininfoTest.latest_block_time diff --git a/tests/floresta-cli/getblockcount.py b/tests/floresta-cli/getblockcount.py index ae18700b7..2f7f0c96f 100644 --- a/tests/floresta-cli/getblockcount.py +++ b/tests/floresta-cli/getblockcount.py @@ -8,6 +8,7 @@ import time from test_framework import FlorestaTestFramework from test_framework.node import NodeType +from test_framework.constants import WALLET_ADDRESS class GetBlockCountTest(FlorestaTestFramework): @@ -30,7 +31,7 @@ def set_test_params(self): self.utreexod = self.add_node_extra_args( variant=NodeType.UTREEXOD, extra_args=[ - "--miningaddr=bcrt1q4gfcga7jfjmm02zpvrh4ttc5k7lmnq2re52z2y", + f"--miningaddr={WALLET_ADDRESS}", "--prune=0", ], ) diff --git a/tests/floresta-cli/getblockhash.py b/tests/floresta-cli/getblockhash.py index f09e34473..5f3d6f313 100644 --- a/tests/floresta-cli/getblockhash.py +++ b/tests/floresta-cli/getblockhash.py @@ -8,6 +8,7 @@ import time from test_framework import FlorestaTestFramework from test_framework.node import NodeType +from test_framework.constants import WALLET_ADDRESS class GetBlockhashTest(FlorestaTestFramework): @@ -17,8 +18,6 @@ class GetBlockhashTest(FlorestaTestFramework): the blockhashes match between floresta, utreexod, and bitcoind. """ - best_block = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" - def set_test_params(self): """ Setup a single node @@ -31,7 +30,7 @@ def set_test_params(self): self.utreexod = self.add_node_extra_args( variant=NodeType.UTREEXOD, extra_args=[ - "--miningaddr=bcrt1q4gfcga7jfjmm02zpvrh4ttc5k7lmnq2re52z2y", + f"--miningaddr={WALLET_ADDRESS}", "--prune=0", ], ) diff --git a/tests/floresta-cli/getblockheader.py b/tests/floresta-cli/getblockheader.py index b7527875f..f477904a7 100644 --- a/tests/floresta-cli/getblockheader.py +++ b/tests/floresta-cli/getblockheader.py @@ -6,6 +6,7 @@ from test_framework import FlorestaTestFramework from test_framework.node import NodeType +from test_framework.constants import GENESIS_BLOCK_HASH class GetBlockheaderHeightZeroTest(FlorestaTestFramework): @@ -28,7 +29,6 @@ class GetBlockheaderHeightZeroTest(FlorestaTestFramework): nodes = [-1] version = 1 - blockhash = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" prev_blockhash = "0000000000000000000000000000000000000000000000000000000000000000" merkle_root = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b" time = 1296688602 @@ -49,9 +49,7 @@ def run_test(self): self.run_node(self.florestad) # Test assertions - response = self.florestad.rpc.get_blockheader( - GetBlockheaderHeightZeroTest.blockhash - ) + response = self.florestad.rpc.get_blockheader(GENESIS_BLOCK_HASH) self.assertEqual(response["version"], GetBlockheaderHeightZeroTest.version) self.assertEqual( response["prev_blockhash"], GetBlockheaderHeightZeroTest.prev_blockhash diff --git a/tests/floresta-cli/gettxout.py b/tests/floresta-cli/gettxout.py index f704c875e..a2684bb98 100644 --- a/tests/floresta-cli/gettxout.py +++ b/tests/floresta-cli/gettxout.py @@ -9,12 +9,13 @@ import os from test_framework import FlorestaTestFramework from test_framework.node import NodeType +from test_framework.constants import WALLET_ADDRESS # TODO Use many addresses types as possible to test the gettxout command WALLET_CONFIG = "\n".join( [ "[wallet]", - 'addresses = [ "bcrt1q4gfcga7jfjmm02zpvrh4ttc5k7lmnq2re52z2y" ]', + f'addresses = [ "{WALLET_ADDRESS}" ]', ] ) @@ -46,7 +47,7 @@ def set_test_params(self): self.utreexod = self.add_node_extra_args( variant=NodeType.UTREEXOD, extra_args=[ - "--miningaddr=bcrt1q4gfcga7jfjmm02zpvrh4ttc5k7lmnq2re52z2y", + f"--miningaddr={WALLET_ADDRESS}", "--prune=0", ], ) diff --git a/tests/floresta-cli/listdescriptors.py b/tests/floresta-cli/listdescriptors.py new file mode 100644 index 000000000..64eb1e28b --- /dev/null +++ b/tests/floresta-cli/listdescriptors.py @@ -0,0 +1,41 @@ +""" +floresta_cli_listdescriptor.py + +This functional test cli utility to interact with a Floresta node with `listdescriptor` +""" + +from test_framework import FlorestaTestFramework +from test_framework.node import NodeType +from test_framework.constants import WALLET_DESCRIPTOR_EXTERNAL + + +class ListDescriptorTest(FlorestaTestFramework): + """ + Test the `listdescriptors` RPC command with a fresh node. + """ + + def set_test_params(self): + """ + Setup a single node + """ + self.florestad = self.add_node_default_args(variant=NodeType.FLORESTAD) + + def run_test(self): + self.run_node(self.florestad) + + self.log("Checking initial descriptors...") + descriptors = self.florestad.rpc.list_descriptors() + self.assertEqual(len(descriptors), 0) + + self.log("Loading external wallet descriptor...") + result = self.florestad.rpc.load_descriptor(WALLET_DESCRIPTOR_EXTERNAL) + self.assertTrue(result) + + self.log("Checking loaded descriptors...") + descriptors = self.florestad.rpc.list_descriptors() + self.assertEqual(len(descriptors), 1) + self.assertEqual(descriptors[0], WALLET_DESCRIPTOR_EXTERNAL) + + +if __name__ == "__main__": + ListDescriptorTest().main() diff --git a/tests/floresta-cli/loaddescriptor.py b/tests/floresta-cli/loaddescriptor.py new file mode 100644 index 000000000..cde041677 --- /dev/null +++ b/tests/floresta-cli/loaddescriptor.py @@ -0,0 +1,60 @@ +""" +floresta_cli_loaddescriptor.py + +Functional test for the `loaddescriptor` CLI utility to interact with a Floresta node. +""" + +from requests.exceptions import HTTPError + +from test_framework import FlorestaTestFramework +from test_framework.node import NodeType +from test_framework.constants import ( + WALLET_DESCRIPTOR_EXTERNAL, + WALLET_DESCRIPTOR_PRIV_EXTERNAL, +) + + +class LoadDescriptorTest(FlorestaTestFramework): + """ + Test the `loaddescriptor` RPC command with a fresh node. + """ + + def set_test_params(self): + """ + Setup a single node + """ + self.florestad = self.add_node_default_args(variant=NodeType.FLORESTAD) + + def check_descriptors(self): + """ + Check the descriptors loaded in the node. + """ + self.log("Checking loaded descriptors...") + descriptors = self.florestad.rpc.list_descriptors() + self.assertEqual(len(descriptors), 1) + self.assertEqual(descriptors[0], WALLET_DESCRIPTOR_EXTERNAL) + + def run_test(self): + self.run_node(self.florestad) + + self.log("Loading external wallet descriptor...") + result = self.florestad.rpc.load_descriptor(WALLET_DESCRIPTOR_EXTERNAL) + self.assertTrue(result) + + self.check_descriptors() + + self.log("Loading private key wallet descriptor (should fail)...") + with self.assertRaises(HTTPError): + result = self.florestad.rpc.load_descriptor(WALLET_DESCRIPTOR_PRIV_EXTERNAL) + + self.check_descriptors() + + self.log("Loading external wallet descriptor again (should fail)...") + with self.assertRaises(HTTPError): + result = self.florestad.rpc.load_descriptor(WALLET_DESCRIPTOR_EXTERNAL) + + self.check_descriptors() + + +if __name__ == "__main__": + LoadDescriptorTest().main() diff --git a/tests/florestad/reorg-chain.py b/tests/florestad/reorg-chain.py index 3ddc2e806..7c1194a72 100644 --- a/tests/florestad/reorg-chain.py +++ b/tests/florestad/reorg-chain.py @@ -12,6 +12,7 @@ from test_framework import FlorestaTestFramework from test_framework.node import NodeType +from test_framework.constants import WALLET_ADDRESS class ChainReorgTest(FlorestaTestFramework): @@ -25,7 +26,7 @@ def set_test_params(self): self.utreexod = self.add_node_extra_args( variant=NodeType.UTREEXOD, extra_args=[ - "--miningaddr=bcrt1q4gfcga7jfjmm02zpvrh4ttc5k7lmnq2re52z2y", + f"--miningaddr={WALLET_ADDRESS}", "--utreexoproofindex", "--prune=0", ], diff --git a/tests/florestad/wallet_conf.py b/tests/florestad/wallet_conf.py new file mode 100644 index 000000000..d6d9c32b2 --- /dev/null +++ b/tests/florestad/wallet_conf.py @@ -0,0 +1,120 @@ +""" +Test the wallet configuration using configuration files for the Floresta node. +""" + +import os + +from test_framework import FlorestaTestFramework +from test_framework.node import Node, NodeType +from test_framework.constants import ( + WALLET_ADDRESS, + WALLET_DESCRIPTOR_EXTERNAL, + WALLET_DESCRIPTOR_INTERNAL, + WALLET_DESCRIPTOR_PRIV_EXTERNAL, + WALLET_DESCRIPTOR_PRIV_INTERNAL, + WALLET_XPUB_BIP_84, + WALLET_XPRIV, +) + +WALLET_CONFIG_ADDRESS = "\n".join( + [ + "[wallet]", + f'addresses = [ "{WALLET_ADDRESS}" ]', + ] +) +WALLET_CONFIG_XPUB = "\n".join( + [ + "[wallet]", + f'xpubs = [ "{WALLET_XPUB_BIP_84}" ]', + ] +) + +WALLET_CONFIG_DESCRIPTOR = "\n".join( + [ + "[wallet]", + f'descriptors = [ "{WALLET_DESCRIPTOR_EXTERNAL}", "{WALLET_DESCRIPTOR_INTERNAL}" ]', + ] +) + +WALLET_CONFIG_XPRIV = "\n".join( + [ + "[wallet]", + f'xpubs = [ "{WALLET_XPRIV}" ]', + ] +) + +WALLET_CONFIG_DESCRIPTOR_PRIV = "\n".join( + [ + "[wallet]", + f'descriptors = [ "{WALLET_DESCRIPTOR_PRIV_EXTERNAL}", "{WALLET_DESCRIPTOR_PRIV_INTERNAL}" ]', + ] +) + + +class WalletConfTest(FlorestaTestFramework): + """ + Test the wallet configuration using configuration files for the Floresta node. + + This class tests the behavior of different wallet configurations, ensuring + that the node handles them correctly. + """ + + def set_test_params(self): + """ + Set up five nodes with different wallet configurations. + """ + self.florestad_addr = self.create_floresta_node(WALLET_CONFIG_ADDRESS) + self.florestad_xpub = self.create_floresta_node(WALLET_CONFIG_XPUB) + self.florestad_desc = self.create_floresta_node(WALLET_CONFIG_DESCRIPTOR) + self.florestad_xpriv = self.create_floresta_node(WALLET_CONFIG_XPRIV) + self.florestad_desc_priv = self.create_floresta_node( + WALLET_CONFIG_DESCRIPTOR_PRIV + ) + + def create_floresta_node(self, config): + """ + Create Floresta nodes with the given configuration. + """ + floresta_node = self.add_node_default_args(variant=NodeType.FLORESTAD) + config_dir = os.path.join(floresta_node.daemon.data_dir, "config.toml") + with open(config_dir, "w") as f: + f.write(config) + floresta_node.set_extra_args([f"--config-file={config_dir}"]) + + return floresta_node + + def run_test(self): + """ + Run the test cases for each node configuration. + """ + self.run_node(self.florestad_addr) + self.run_node(self.florestad_xpub) + self.run_node(self.florestad_desc) + + self.log("Checking descriptors for each wallet(addr)") + descriptors = self.florestad_addr.rpc.list_descriptors() + self.assertEqual(len(descriptors), 0) + + self.log("Checking descriptors for each wallet(xpub)") + descriptors = self.florestad_xpub.rpc.list_descriptors() + self.assertEqual(len(descriptors), 2) + self.assertEqual(descriptors[0], WALLET_DESCRIPTOR_EXTERNAL) + self.assertEqual(descriptors[1], WALLET_DESCRIPTOR_INTERNAL) + + self.log("Checking descriptors for each wallet(descriptor)") + descriptors = self.florestad_desc.rpc.list_descriptors() + self.assertEqual(len(descriptors), 2) + self.assertEqual(descriptors[0], WALLET_DESCRIPTOR_EXTERNAL) + self.assertEqual(descriptors[1], WALLET_DESCRIPTOR_INTERNAL) + + self.log("Checking descriptors for each wallet(xpriv)") + with self.assertRaises(Exception): + self.run_node(self.florestad_xpriv) + + self.log("Checking descriptors for each wallet(descriptor with privkey)") + with self.assertRaises(Exception): + self.run_node(self.florestad_desc_priv) + + +if __name__ == "__main__": + WalletConfTest().main() diff --git a/tests/florestad/wallet_flag.py b/tests/florestad/wallet_flag.py new file mode 100644 index 000000000..1c5cbbf7b --- /dev/null +++ b/tests/florestad/wallet_flag.py @@ -0,0 +1,88 @@ +""" +Test the wallet configuration flags for the Floresta node. + +This script tests the behavior of the `--wallet-xpub` and `--wallet-descriptor` +flags, ensuring that the node handles them correctly. +""" + +import os + +from test_framework import FlorestaTestFramework +from test_framework.node import NodeType +from test_framework.constants import ( + WALLET_ADDRESS, + WALLET_DESCRIPTOR_EXTERNAL, + WALLET_DESCRIPTOR_INTERNAL, + WALLET_DESCRIPTOR_PRIV_EXTERNAL, + WALLET_DESCRIPTOR_PRIV_INTERNAL, + WALLET_XPUB_BIP_84, + WALLET_XPRIV, +) + + +class WalletFlagTest(FlorestaTestFramework): + """ + Test the wallet configuration flags for the Floresta node. + """ + + def set_test_params(self): + """ + Set up four nodes with different wallet configurations. + """ + self.florestad_xpub = self.add_node_extra_args( + variant=NodeType.FLORESTAD, + extra_args=[ + f"--wallet-xpub={WALLET_XPUB_BIP_84}", + ], + ) + self.florestad_desc = self.add_node_extra_args( + variant=NodeType.FLORESTAD, + extra_args=[ + f"--wallet-descriptor={WALLET_DESCRIPTOR_EXTERNAL}", + f"--wallet-descriptor={WALLET_DESCRIPTOR_INTERNAL}", + ], + ) + self.florestad_xpriv = self.add_node_extra_args( + variant=NodeType.FLORESTAD, + extra_args=[ + f"--wallet-xpub={WALLET_XPRIV}", + ], + ) + self.florestad_desc_priv = self.add_node_extra_args( + variant=NodeType.FLORESTAD, + extra_args=[ + f"--wallet-descriptor={WALLET_DESCRIPTOR_PRIV_EXTERNAL}", + f"--wallet-descriptor={WALLET_DESCRIPTOR_PRIV_INTERNAL}", + ], + ) + + def run_test(self): + """ + Run the test cases for each node configuration. + """ + self.run_node(self.florestad_xpub) + self.run_node(self.florestad_desc) + + self.log("Checking descriptors for each wallet(xpub)") + descriptors = self.florestad_xpub.rpc.list_descriptors() + self.assertEqual(len(descriptors), 2) + self.assertEqual(descriptors[0], WALLET_DESCRIPTOR_EXTERNAL) + self.assertEqual(descriptors[1], WALLET_DESCRIPTOR_INTERNAL) + + self.log("Checking descriptors for each wallet(descriptor)") + descriptors = self.florestad_desc.rpc.list_descriptors() + self.assertEqual(len(descriptors), 2) + self.assertEqual(descriptors[0], WALLET_DESCRIPTOR_EXTERNAL) + self.assertEqual(descriptors[1], WALLET_DESCRIPTOR_INTERNAL) + + self.log("Checking descriptors for each wallet(xpriv)") + with self.assertRaises(Exception): + self.run_node(self.florestad_xpriv) + + self.log("Checking descriptors for each wallet(descriptor with privkey)") + with self.assertRaises(Exception): + self.run_node(self.florestad_desc_priv) + + +if __name__ == "__main__": + WalletFlagTest().main() diff --git a/tests/test_framework/constants.py b/tests/test_framework/constants.py new file mode 100644 index 000000000..f026d7423 --- /dev/null +++ b/tests/test_framework/constants.py @@ -0,0 +1,26 @@ +""" +This module contains constants used throughout the Floresta tests. +""" + +import os + +GENESIS_BLOCK_HEIGHT = 0 +GENESIS_BLOCK_HASH = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" +GENESIS_BLOCK_DIFFICULTY_INT = 1 +GENESIS_BLOCK_DIFFICULTY_FLOAT = 4.656542373906925e-10 +GENESIS_BLOCK_LEAF_COUNT = 0 +TEST_CHAIN = "regtest" +FLORESTA_TEMP_DIR = os.getenv("FLORESTA_TEMP_DIR") + +# Mnemonics = useless ritual arm slow mention dog force almost sudden pulp rude eager +# pylint: disable = line-too-long +WALLET_XPRIV = "tprv8hCwaWbnCTeqSXMmEgtYqC3tjCHQTKphfXBG5MfWgcA6pif3fAUqCuqwphSyXmVFhd8b5ep5krkRxF6YkuQfxSAhHMTGeRA8rKPzQd9BMre" +WALLET_DESCRIPTOR_PRIV_EXTERNAL = f"wpkh({WALLET_XPRIV}/0/*)#amzqvgzd" +WALLET_DESCRIPTOR_PRIV_INTERNAL = f"wpkh({WALLET_XPRIV}/1/*)#v08p3aj4" +# pylint: disable = line-too-long +WALLET_XPUB = "tpubDDtyive2LqLWKzPZ8LZ9Ebi1JDoLcf1cEpn3Mshp6sxVfCupHZJRPQTozp2EpTF76vJcyQBN7VP7CjUntEJxeADnuTMNTYKoSWNae8soVyv" +WALLET_DESCRIPTOR_EXTERNAL = f"wpkh({WALLET_XPUB}/0/*)#7h6kdtnk" +WALLET_DESCRIPTOR_INTERNAL = f"wpkh({WALLET_XPUB}/1/*)#0rlhs7rw" +# pylint: disable = line-too-long +WALLET_XPUB_BIP_84 = "vpub5ZrpbMUWLCJ6MbpU1RzocWBddAQnk2XYry9JSXrtzxSqoicei28CzqUhiN2HJ8z2VjY6rsUNf4qxjym43ydhAFQJ7BDDcC2bK6et6x9hc4D" +WALLET_ADDRESS = "bcrt1q427ze5mrzqupzyfmqsx9gxh7xav538yk2j4cft" diff --git a/tests/test_framework/rpc/bitcoin.py b/tests/test_framework/rpc/bitcoin.py index 3165c6faa..d1b3b88a8 100644 --- a/tests/test_framework/rpc/bitcoin.py +++ b/tests/test_framework/rpc/bitcoin.py @@ -5,6 +5,7 @@ """ from test_framework.rpc.base import BaseRPC +from test_framework.constants import WALLET_ADDRESS class BitcoinRPC(BaseRPC): @@ -33,7 +34,7 @@ def generate_block_to_address(self, nblocks: int, address: str) -> list: def generate_block(self, nblocks: int) -> list: """ - Mine blocks immediately to a address(bcrt1q3ml87jemlfvk7lq8gfs7pthvj5678ndnxnw9ch) using + Mine blocks immediately to a address(WALLET_ADDRESS) using `generate_block_to_address(nblocks, address)` Args: @@ -42,5 +43,4 @@ def generate_block(self, nblocks: int) -> list: Returns: A list of block hashes of the newly mined blocks """ - address = "bcrt1q3ml87jemlfvk7lq8gfs7pthvj5678ndnxnw9ch" - return self.generate_block_to_address(nblocks, address) + return self.generate_block_to_address(nblocks, WALLET_ADDRESS) diff --git a/tests/test_framework/rpc/floresta.py b/tests/test_framework/rpc/floresta.py index b4ba4fc18..3932f4003 100644 --- a/tests/test_framework/rpc/floresta.py +++ b/tests/test_framework/rpc/floresta.py @@ -32,3 +32,15 @@ def get_memoryinfo(self, mode: str): raise ValueError(f"Invalid getmemoryinfo mode: '{mode}'") return self.perform_request("getmemoryinfo", params=[mode]) + + def list_descriptors(self): + """ + List all loaded descriptors + """ + return self.perform_request("listdescriptors") + + def load_descriptor(self, descriptor: str) -> dict: + """ + Load a descriptor into the node performing + """ + return self.perform_request("loaddescriptor", params=[descriptor]) diff --git a/tests/test_framework/util.py b/tests/test_framework/util.py index 01cf9b562..10bbb6566 100644 --- a/tests/test_framework/util.py +++ b/tests/test_framework/util.py @@ -9,6 +9,7 @@ create_pkcs8_private_key, create_pkcs8_self_signed_certificate, ) +from test_framework.constants import FLORESTA_TEMP_DIR class Utility: @@ -22,12 +23,12 @@ def get_integration_test_dir(): Get path for florestad used in integration tests, generally set on $FLORESTA_TEMP_DIR/binaries """ - if os.getenv("FLORESTA_TEMP_DIR") is None: + if FLORESTA_TEMP_DIR is None: raise RuntimeError( "FLORESTA_TEMP_DIR not set. " + " Please set it to the path of the integration test directory." ) - return os.getenv("FLORESTA_TEMP_DIR") + return FLORESTA_TEMP_DIR @staticmethod def get_logs_dir(): diff --git a/tests/test_runner.py b/tests/test_runner.py index c4441539b..9a641ca0a 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -71,6 +71,10 @@ ("example", "bitcoin"), ("example", "utreexod"), ("florestad", "node-info"), + ("florestad", "wallet_conf"), + ("florestad", "wallet_flag"), + ("floresta-cli", "loaddescriptor"), + ("floresta-cli", "listdescriptors"), ] # Before running the tests, we check if the number of tests