From 20e64621db56a264d3291f5df1831a94a2bcee15 Mon Sep 17 00:00:00 2001 From: HalFinneyIsMyHomeBoy Date: Thu, 8 Jan 2026 16:43:01 +0000 Subject: [PATCH 01/14] manually tripped descriptor mismatch --- ddk/Cargo.toml | 4 ++ ddk/examples/descriptor_mismatch.rs | 81 +++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 ddk/examples/descriptor_mismatch.rs diff --git a/ddk/Cargo.toml b/ddk/Cargo.toml index 10c9fed..c4be1b3 100644 --- a/ddk/Cargo.toml +++ b/ddk/Cargo.toml @@ -84,3 +84,7 @@ required-features = ["nostr"] name = "postgres" path = "examples/postgres.rs" required-features = ["postgres", "lightning", "kormir"] + +[[example]] +name = "descriptor_mismatch" +path = "examples/descriptor_mismatch.rs" diff --git a/ddk/examples/descriptor_mismatch.rs b/ddk/examples/descriptor_mismatch.rs new file mode 100644 index 0000000..6901190 --- /dev/null +++ b/ddk/examples/descriptor_mismatch.rs @@ -0,0 +1,81 @@ +//! Example to trigger the descriptor mismatch error at line 262 in wallet/mod.rs +//! +//! This example creates a wallet with one seed, then tries to load it with a different seed. +//! Since the descriptors won't match, it will trigger the error at line 262. + +use ddk::chain::EsploraClient; +use ddk::error::WalletError; +use ddk::logger::{LogLevel, Logger}; +use ddk::storage::memory::MemoryStorage; +use ddk::wallet::DlcDevKitWallet; +use bitcoin::Network; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Setup logger + let logger = Arc::new(Logger::console( + "descriptor_mismatch_example".to_string(), + LogLevel::Info, + )); + + // Setup Esplora client (using regtest as default) + let esplora_host = std::env::var("ESPLORA_HOST") + .unwrap_or_else(|_| "http://localhost:3000".to_string()); + let esplora = Arc::new( + EsploraClient::new(&esplora_host, Network::Regtest, logger.clone()) + .map_err(|e| format!("Failed to create Esplora client: {}", e))?, + ); + + // Create shared storage + let storage = Arc::new(MemoryStorage::new()); + + // First seed - this will create and persist a wallet + let mut seed1 = [0u8; 64]; + seed1[0..8].copy_from_slice(b"seed_one"); + println!("Creating wallet with seed1..."); + + let _wallet1 = DlcDevKitWallet::new( + &seed1, + esplora.clone(), + Network::Regtest, + storage.clone(), + None, + logger.clone(), + ) + .await?; + + println!("✓ Wallet 1 created successfully"); + + // Second seed - this will try to load the existing wallet but with different descriptors + // This should fail at line 262 because the descriptors won't match + let mut seed2 = [0u8; 64]; + seed2[0..8].copy_from_slice(b"seed_two"); + println!("\nAttempting to load wallet with seed2 (different descriptors)..."); + + match DlcDevKitWallet::new( + &seed2, + esplora.clone(), + Network::Regtest, + storage.clone(), // Same storage, but different seed + None, + logger.clone(), + ) + .await + { + Ok(_) => { + println!("✗ Unexpected: Wallet loaded successfully (this shouldn't happen)"); + Err("Expected error but wallet loaded successfully".into()) + } + Err(WalletError::WalletPersistanceError(e)) => { + println!("✓ Successfully triggered the error at line 262!"); + println!(" Error message: {}", e); + Ok(()) + } + Err(e) => { + println!("✗ Got a different error than expected:"); + println!(" Error: {:?}", e); + Err(format!("Expected WalletPersistanceError, got: {:?}", e).into()) + } + } +} From 533de424361d539ea1aaf8f08634b786c705dc43 Mon Sep 17 00:00:00 2001 From: HalFinneyIsMyHomeBoy Date: Thu, 8 Jan 2026 16:56:37 +0000 Subject: [PATCH 02/14] add to tests instead of standalone script --- ddk/Cargo.toml | 4 -- ddk/examples/descriptor_mismatch.rs | 81 ----------------------------- ddk/tests/wallet.rs | 78 +++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 85 deletions(-) delete mode 100644 ddk/examples/descriptor_mismatch.rs create mode 100644 ddk/tests/wallet.rs diff --git a/ddk/Cargo.toml b/ddk/Cargo.toml index c4be1b3..10c9fed 100644 --- a/ddk/Cargo.toml +++ b/ddk/Cargo.toml @@ -84,7 +84,3 @@ required-features = ["nostr"] name = "postgres" path = "examples/postgres.rs" required-features = ["postgres", "lightning", "kormir"] - -[[example]] -name = "descriptor_mismatch" -path = "examples/descriptor_mismatch.rs" diff --git a/ddk/examples/descriptor_mismatch.rs b/ddk/examples/descriptor_mismatch.rs deleted file mode 100644 index 6901190..0000000 --- a/ddk/examples/descriptor_mismatch.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! Example to trigger the descriptor mismatch error at line 262 in wallet/mod.rs -//! -//! This example creates a wallet with one seed, then tries to load it with a different seed. -//! Since the descriptors won't match, it will trigger the error at line 262. - -use ddk::chain::EsploraClient; -use ddk::error::WalletError; -use ddk::logger::{LogLevel, Logger}; -use ddk::storage::memory::MemoryStorage; -use ddk::wallet::DlcDevKitWallet; -use bitcoin::Network; -use std::sync::Arc; - -#[tokio::main] -async fn main() -> Result<(), Box> { - // Setup logger - let logger = Arc::new(Logger::console( - "descriptor_mismatch_example".to_string(), - LogLevel::Info, - )); - - // Setup Esplora client (using regtest as default) - let esplora_host = std::env::var("ESPLORA_HOST") - .unwrap_or_else(|_| "http://localhost:3000".to_string()); - let esplora = Arc::new( - EsploraClient::new(&esplora_host, Network::Regtest, logger.clone()) - .map_err(|e| format!("Failed to create Esplora client: {}", e))?, - ); - - // Create shared storage - let storage = Arc::new(MemoryStorage::new()); - - // First seed - this will create and persist a wallet - let mut seed1 = [0u8; 64]; - seed1[0..8].copy_from_slice(b"seed_one"); - println!("Creating wallet with seed1..."); - - let _wallet1 = DlcDevKitWallet::new( - &seed1, - esplora.clone(), - Network::Regtest, - storage.clone(), - None, - logger.clone(), - ) - .await?; - - println!("✓ Wallet 1 created successfully"); - - // Second seed - this will try to load the existing wallet but with different descriptors - // This should fail at line 262 because the descriptors won't match - let mut seed2 = [0u8; 64]; - seed2[0..8].copy_from_slice(b"seed_two"); - println!("\nAttempting to load wallet with seed2 (different descriptors)..."); - - match DlcDevKitWallet::new( - &seed2, - esplora.clone(), - Network::Regtest, - storage.clone(), // Same storage, but different seed - None, - logger.clone(), - ) - .await - { - Ok(_) => { - println!("✗ Unexpected: Wallet loaded successfully (this shouldn't happen)"); - Err("Expected error but wallet loaded successfully".into()) - } - Err(WalletError::WalletPersistanceError(e)) => { - println!("✓ Successfully triggered the error at line 262!"); - println!(" Error message: {}", e); - Ok(()) - } - Err(e) => { - println!("✗ Got a different error than expected:"); - println!(" Error: {:?}", e); - Err(format!("Expected WalletPersistanceError, got: {:?}", e).into()) - } - } -} diff --git a/ddk/tests/wallet.rs b/ddk/tests/wallet.rs new file mode 100644 index 0000000..04803c7 --- /dev/null +++ b/ddk/tests/wallet.rs @@ -0,0 +1,78 @@ +mod test_util; + +use ddk::chain::EsploraClient; +use ddk::error::WalletError; +use ddk::logger::{LogLevel, Logger}; +use ddk::storage::memory::MemoryStorage; +use ddk::wallet::DlcDevKitWallet; +use bitcoin::Network; +use std::sync::Arc; + +/// Test to trigger the descriptor mismatch error at line 262 in wallet/mod.rs +/// +/// This test creates a wallet with one seed, then tries to load it with a different seed. +/// Since the descriptors won't match, it will trigger the error at line 262. +#[tokio::test] +async fn descriptor_mismatch_error() { + dotenv::dotenv().ok(); + + // Setup logger + let logger = Arc::new(Logger::console( + "descriptor_mismatch_test".to_string(), + LogLevel::Info, + )); + + // Setup Esplora client + let esplora_host = std::env::var("ESPLORA_HOST").expect("ESPLORA_HOST must be set"); + let esplora = Arc::new( + EsploraClient::new(&esplora_host, Network::Regtest, logger.clone()) + .expect("Failed to create Esplora client"), + ); + + // Create shared storage + let storage = Arc::new(MemoryStorage::new()); + + // First seed - this will create and persist a wallet + let mut seed1 = [0u8; 64]; + seed1[0..8].copy_from_slice(b"seed_one"); + + let _wallet1 = DlcDevKitWallet::new( + &seed1, + esplora.clone(), + Network::Regtest, + storage.clone(), + None, + logger.clone(), + ) + .await + .expect("Failed to create first wallet"); + + // Second seed - this will try to load the existing wallet but with different descriptors + // This should fail at line 262 because the descriptors won't match + let mut seed2 = [0u8; 64]; + seed2[0..8].copy_from_slice(b"seed_two"); + + let result = DlcDevKitWallet::new( + &seed2, + esplora.clone(), + Network::Regtest, + storage.clone(), // Same storage, but different seed + None, + logger.clone(), + ) + .await; + + // Verify we got the expected error + match result { + Ok(_) => panic!("Expected WalletPersistanceError but wallet loaded successfully"), + Err(WalletError::WalletPersistanceError(e)) => { + // Successfully triggered the error at line 262 + assert!( + e.contains("descriptor") || e.len() > 0, + "Expected descriptor-related error message, got: {}", + e + ); + } + Err(e) => panic!("Expected WalletPersistanceError, got: {:?}", e), + } +} From 16a1b06234ed95caecddc95b3d1a6ce4451f5e10 Mon Sep 17 00:00:00 2001 From: HalFinneyIsMyHomeBoy Date: Thu, 8 Jan 2026 17:57:17 +0000 Subject: [PATCH 03/14] chore --- ddk/Cargo.toml | 10 +++++----- ddk/tests/wallet.rs | 23 +++++++++++++++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/ddk/Cargo.toml b/ddk/Cargo.toml index 10c9fed..eb6c68b 100644 --- a/ddk/Cargo.toml +++ b/ddk/Cargo.toml @@ -24,10 +24,10 @@ sled = ["dep:sled"] postgres = ["dep:sqlx", "sqlx/postgres"] [dependencies] -ddk-manager = { version = "1.0.9", path = "../ddk-manager", features = ["use-serde"] } -ddk-dlc = { version = "1.0.9", path = "../dlc", features = ["use-serde"] } -ddk-messages = { version = "1.0.9", path = "../dlc-messages", features = [ "use-serde"] } -ddk-trie = { version = "1.0.9", path = "../dlc-trie", features = ["use-serde"] } +ddk-manager = { version = "1.0.6", path = "../ddk-manager", features = ["use-serde"] } +ddk-dlc = { version = "1.0.6", path = "../dlc", features = ["use-serde"] } +ddk-messages = { version = "1.0.6", path = "../dlc-messages", features = [ "use-serde"] } +ddk-trie = { version = "1.0.6", path = "../dlc-trie", features = ["use-serde"] } bitcoin = { version = "0.32.6", features = ["rand", "serde"] } bdk_esplora = { version = "0.22.0", features = ["blocking-https", "async-https", "tokio"] } @@ -58,7 +58,7 @@ lightning-net-tokio = { version = "0.1.0", optional = true } # oracle feature reqwest = { version = "0.12.9", features = ["json"], optional = true } -kormir = { version = "1.0.9", path = "../kormir" } +kormir = { version = "1.0.6", path = "../kormir" } hmac = "0.12.1" sha2 = "0.10" nostr-database = { version = "0.40.0", optional = true } diff --git a/ddk/tests/wallet.rs b/ddk/tests/wallet.rs index 04803c7..8477bb8 100644 --- a/ddk/tests/wallet.rs +++ b/ddk/tests/wallet.rs @@ -5,6 +5,7 @@ use ddk::error::WalletError; use ddk::logger::{LogLevel, Logger}; use ddk::storage::memory::MemoryStorage; use ddk::wallet::DlcDevKitWallet; +use ddk::Storage; use bitcoin::Network; use std::sync::Arc; @@ -30,7 +31,7 @@ async fn descriptor_mismatch_error() { ); // Create shared storage - let storage = Arc::new(MemoryStorage::new()); + let storage = Arc::new(MemoryStorage::new()) as Arc; // First seed - this will create and persist a wallet let mut seed1 = [0u8; 64]; @@ -66,13 +67,23 @@ async fn descriptor_mismatch_error() { match result { Ok(_) => panic!("Expected WalletPersistanceError but wallet loaded successfully"), Err(WalletError::WalletPersistanceError(e)) => { - // Successfully triggered the error at line 262 + + println!("\n{}", "=".repeat(70)); + println!(" {:?}", WalletError::WalletPersistanceError(e.clone())); + println!("\n{}", "=".repeat(70)); assert!( - e.contains("descriptor") || e.len() > 0, - "Expected descriptor-related error message, got: {}", - e + e.len() > 0, + "Error message should not be empty" ); } - Err(e) => panic!("Expected WalletPersistanceError, got: {:?}", e), + Err(e) => { + println!("\n{}", "=".repeat(70)); + println!("UNEXPECTED ERROR TYPE"); + println!("{}", "=".repeat(70)); + println!("Expected WalletPersistanceError, but got: {:?}", e); + println!("Full error Debug: {:?}", e); + println!("{}", "=".repeat(70)); + panic!("Expected WalletPersistanceError, got: {:?}", e); + } } } From 0f2a66b284efdd3baa5fdc26aa58befeb0c2614a Mon Sep 17 00:00:00 2001 From: HalFinneyIsMyHomeBoy Date: Fri, 9 Jan 2026 21:04:31 +0000 Subject: [PATCH 04/14] detailed descriptor mismatch error handling and checksum extraction --- ddk/src/error.rs | 6 ++ ddk/src/wallet/mod.rs | 176 +++++++++++++++++++++++++++++++++++++++++- ddk/tests/wallet.rs | 47 ++++++++--- 3 files changed, 217 insertions(+), 12 deletions(-) diff --git a/ddk/src/error.rs b/ddk/src/error.rs index 5025915..2a788a7 100644 --- a/ddk/src/error.rs +++ b/ddk/src/error.rs @@ -207,6 +207,12 @@ pub enum WalletError { InvalidDerivationIndex, #[error("Invalid secret key")] InvalidSecretKey, + #[error("Descriptor mismatch in {keychain} keychain.\nExpected descriptor checksum: {expected}\nStored descriptor checksum: {stored}")] + DescriptorMismatch { + keychain: String, + expected: String, + stored: String, + }, } /// Converts a generic error to a DLC manager storage error diff --git a/ddk/src/wallet/mod.rs b/ddk/src/wallet/mod.rs index 7711888..57938c9 100644 --- a/ddk/src/wallet/mod.rs +++ b/ddk/src/wallet/mod.rs @@ -206,6 +206,160 @@ pub struct DlcDevKitWallet { const MIN_FEERATE: u32 = 253; +/// Helper function to extract the checksum from a descriptor string. +/// +/// Descriptors typically have a checksum at the end after a '#' character. +/// Returns the checksum (usually 8 alphanumeric characters) or "unknown" if not found. +fn extract_descriptor_checksum(descriptor: &str) -> String { + if let Some(hash_pos) = descriptor.rfind('#') { + let checksum = &descriptor[hash_pos + 1..]; + // Trim whitespace and take exactly 8 characters (typical checksum length) + let trimmed = checksum.trim(); + trimmed.chars().take(8).collect() + } else { + "unknown".to_string() + } +} + +/// Helper function to parse BDK load errors and extract descriptor mismatch information. +/// +/// This function attempts to identify descriptor mismatches from BDK error messages +/// and provides detailed information about which keychain failed and what descriptors +/// were expected vs stored. Only checksums of descriptors are shown for security. +fn parse_descriptor_mismatch_error( + error: &dyn std::error::Error, + external_descriptor_str: &str, + internal_descriptor_str: &str, +) -> WalletError { + let error_msg = error.to_string(); + let error_debug = format!("{:?}", error); + let error_lower = error_msg.to_lowercase(); + + // Try to determine which keychain failed by checking error message + // Look for explicit mentions of keychain types + let (keychain, expected_descriptor) = if error_lower.contains("external") + || error_debug.contains("External") + || error_debug.contains("external") { + ("external", external_descriptor_str.to_string()) + } else if error_lower.contains("internal") + || error_debug.contains("Internal") + || error_debug.contains("internal") { + ("internal", internal_descriptor_str.to_string()) + } else { + // If we can't determine from the error message, we need to check both + // Check if either descriptor appears in the error (indicating which one was expected) + if error_msg.contains(external_descriptor_str) || error_debug.contains(external_descriptor_str) { + ("external", external_descriptor_str.to_string()) + } else if error_msg.contains(internal_descriptor_str) || error_debug.contains(internal_descriptor_str) { + ("internal", internal_descriptor_str.to_string()) + } else { + // Default to external if we can't determine + ("external", external_descriptor_str.to_string()) + } + }; + + // Try to extract the stored descriptor from the error message + // BDK errors might contain descriptor information in various formats + let stored_descriptor = extract_stored_descriptor_from_error(&error_msg, &error_debug) + .unwrap_or_else(|| { + // If we can't extract it, provide a helpful message with the original error + format!("Could not extract stored descriptor from error message. This may indicate a descriptor mismatch. Original error: {}", error_msg) + }); + + // Extract checksums from descriptors instead of showing full descriptors + let expected_checksum = extract_descriptor_checksum(&expected_descriptor); + let stored_checksum = if stored_descriptor.starts_with("Could not extract") { + // If we couldn't extract the descriptor, keep the error message as-is + stored_descriptor + } else { + extract_descriptor_checksum(&stored_descriptor) + }; + + WalletError::DescriptorMismatch { + keychain: keychain.to_string(), + expected: expected_checksum, + stored: stored_checksum, + } +} + +/// Attempts to extract the stored descriptor from BDK error messages. +/// +/// BDK error messages may contain descriptor information in various formats. +/// This function tries common patterns to extract the stored descriptor. +fn extract_stored_descriptor_from_error(error_msg: &str, error_debug: &str) -> Option { + // Common patterns BDK might use in error messages: + // - "stored: " + // - "found: " + // - "existing: " + // - "persisted: " + // - Descriptor might be in quotes or after certain keywords + + // BDK error format: "Descriptor mismatch for External keychain: loaded , expected " + // Try to extract from "loaded " pattern first (most common BDK format) + for text in [error_msg, error_debug] { + if let Some(pos) = text.find("loaded ") { + let after_loaded = &text[pos + "loaded ".len()..]; + // Find the comma that separates stored from expected + if let Some(comma_pos) = after_loaded.find(',') { + let stored_desc = after_loaded[..comma_pos].trim(); + if stored_desc.contains("wpkh") || stored_desc.contains("sh") || stored_desc.contains("tr") { + return Some(stored_desc.to_string()); + } + } else { + // No comma found, try to extract until end or newline + let trimmed = after_loaded.trim(); + if let Some(desc_end) = trimmed.find(|c: char| c == '\n' || c == '\r') { + let potential_desc = trimmed[..desc_end].trim(); + if potential_desc.contains("wpkh") || potential_desc.contains("sh") || potential_desc.contains("tr") { + return Some(potential_desc.to_string()); + } + } else if trimmed.contains("wpkh") || trimmed.contains("sh") || trimmed.contains("tr") { + // Take first reasonable chunk + let end = trimmed.len().min(200); + return Some(trimmed[..end].trim().to_string()); + } + } + } + } + + // Fallback to other patterns + let keywords = ["stored:", "found:", "existing:", "persisted:", "stored ", "found ", "existing ", "persisted "]; + + for text in [error_msg, error_debug] { + for keyword in &keywords { + if let Some(pos) = text.find(keyword) { + let after_keyword = &text[pos + keyword.len()..]; + // Try to find descriptor in quotes first + if let Some(quote_start) = after_keyword.find('"') { + if let Some(quote_end) = after_keyword[quote_start + 1..].find('"') { + let descriptor = &after_keyword[quote_start + 1..quote_start + 1 + quote_end]; + if descriptor.contains("wpkh") || descriptor.contains("sh") || descriptor.contains("tr") { + return Some(descriptor.to_string()); + } + } + } + + // Try to extract descriptor without quotes (look for common descriptor patterns) + let trimmed = after_keyword.trim(); + // Descriptors typically start with certain patterns and contain specific characters + if let Some(desc_end) = trimmed.find(|c: char| c == '\n' || c == '\r' || c == ',' || c == '}') { + let potential_desc = trimmed[..desc_end].trim(); + if potential_desc.contains("wpkh") || potential_desc.contains("sh") || potential_desc.contains("tr") + || potential_desc.starts_with("wpkh") || potential_desc.starts_with("sh") || potential_desc.starts_with("tr") { + return Some(potential_desc.to_string()); + } + } else if trimmed.contains("wpkh") || trimmed.contains("sh") || trimmed.contains("tr") { + // Take first reasonable chunk + let end = trimmed.len().min(200); // Limit length + return Some(trimmed[..end].trim().to_string()); + } + } + } + } + + None +} + impl DlcDevKitWallet { /// Creates a new DlcDevKitWallet instance. /// @@ -259,7 +413,27 @@ impl DlcDevKitWallet { .check_network(network) .load_wallet_async(&mut storage) .await - .map_err(|e| WalletError::WalletPersistanceError(e.to_string()))?; + .map_err(|e| { + // Check if this is a descriptor mismatch error + let error_msg = e.to_string(); + let error_lower = error_msg.to_lowercase(); + + // Convert descriptors to strings for the helper function + let external_desc_str = external_descriptor.0.to_string(); + let internal_desc_str = internal_descriptor.0.to_string(); + + // Common indicators of descriptor mismatch errors + if error_lower.contains("descriptor") && (error_lower.contains("mismatch") + || error_lower.contains("does not match") + || error_lower.contains("expected") + || error_lower.contains("stored") + || error_lower.contains("found")) { + parse_descriptor_mismatch_error(&e, &external_desc_str, &internal_desc_str) + } else { + // For other errors, use the generic error + WalletError::WalletPersistanceError(error_msg) + } + })?; let mut wallet = match load_wallet { Some(w) => w, diff --git a/ddk/tests/wallet.rs b/ddk/tests/wallet.rs index 8477bb8..5f00e50 100644 --- a/ddk/tests/wallet.rs +++ b/ddk/tests/wallet.rs @@ -9,10 +9,11 @@ use ddk::Storage; use bitcoin::Network; use std::sync::Arc; -/// Test to trigger the descriptor mismatch error at line 262 in wallet/mod.rs +/// Test to trigger the descriptor mismatch error. /// /// This test creates a wallet with one seed, then tries to load it with a different seed. -/// Since the descriptors won't match, it will trigger the error at line 262. +/// Since the descriptors won't match, it will trigger the error. The error message +/// will show checksums of the descriptors instead of the full descriptor strings. #[tokio::test] async fn descriptor_mismatch_error() { dotenv::dotenv().ok(); @@ -65,25 +66,49 @@ async fn descriptor_mismatch_error() { // Verify we got the expected error match result { - Ok(_) => panic!("Expected WalletPersistanceError but wallet loaded successfully"), - Err(WalletError::WalletPersistanceError(e)) => { - - println!("\n{}", "=".repeat(70)); - println!(" {:?}", WalletError::WalletPersistanceError(e.clone())); + Ok(_) => panic!("Expected DescriptorMismatch error but wallet loaded successfully"), + Err(WalletError::DescriptorMismatch { keychain, expected, stored }) => { println!("\n{}", "=".repeat(70)); + println!("SUCCESS: Descriptor mismatch error detected"); + println!("{}", "=".repeat(70)); + println!("Keychain: {}", keychain); + println!("Expected descriptor checksum: {}", expected); + println!("Stored descriptor checksum: {}", stored); + println!("{}", "=".repeat(70)); + + // Verify the error contains meaningful information + assert_eq!(keychain, "external", "Should identify external keychain"); + assert!( + expected.len() > 0, + "Expected descriptor checksum should not be empty" + ); + assert!( + stored.len() > 0, + "Stored descriptor checksum should not be empty" + ); + // Verify checksums are valid (8 alphanumeric characters or "unknown") + // Checksums should be 8 characters (typical) or "unknown" if extraction failed + assert!( + expected.len() == 8 || expected == "unknown", + "Expected checksum should be 8 characters or 'unknown', got: '{}' (length: {})", + expected, + expected.len() + ); assert!( - e.len() > 0, - "Error message should not be empty" + stored.len() == 8 || stored == "unknown" || stored.starts_with("Could not extract"), + "Stored checksum should be 8 characters, 'unknown', or an error message, got: '{}' (length: {})", + stored, + stored.len() ); } Err(e) => { println!("\n{}", "=".repeat(70)); println!("UNEXPECTED ERROR TYPE"); println!("{}", "=".repeat(70)); - println!("Expected WalletPersistanceError, but got: {:?}", e); + println!("Expected DescriptorMismatch, but got: {:?}", e); println!("Full error Debug: {:?}", e); println!("{}", "=".repeat(70)); - panic!("Expected WalletPersistanceError, got: {:?}", e); + panic!("Expected DescriptorMismatch, got: {:?}", e); } } } From f31f4b096901f9402e4ffff8559f1d3b024223f1 Mon Sep 17 00:00:00 2001 From: HalFinneyIsMyHomeBoy Date: Fri, 9 Jan 2026 22:01:20 +0000 Subject: [PATCH 05/14] Refactor descriptor mismatch error tests to support multiple storage backends --- ddk/tests/wallet.rs | 95 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 9 deletions(-) diff --git a/ddk/tests/wallet.rs b/ddk/tests/wallet.rs index 5f00e50..10f4043 100644 --- a/ddk/tests/wallet.rs +++ b/ddk/tests/wallet.rs @@ -9,13 +9,12 @@ use ddk::Storage; use bitcoin::Network; use std::sync::Arc; -/// Test to trigger the descriptor mismatch error. -/// -/// This test creates a wallet with one seed, then tries to load it with a different seed. -/// Since the descriptors won't match, it will trigger the error. The error message -/// will show checksums of the descriptors instead of the full descriptor strings. -#[tokio::test] -async fn descriptor_mismatch_error() { +/// Helper function to test descriptor mismatch error across different storage backends. +/// +/// This function creates a wallet with one seed, persists it, then tries to load it +/// with a different seed. It verifies that the error message correctly shows checksums +/// instead of full descriptors. +async fn test_descriptor_mismatch_error_with_storage(storage: Arc) { dotenv::dotenv().ok(); // Setup logger @@ -31,8 +30,6 @@ async fn descriptor_mismatch_error() { .expect("Failed to create Esplora client"), ); - // Create shared storage - let storage = Arc::new(MemoryStorage::new()) as Arc; // First seed - this will create and persist a wallet let mut seed1 = [0u8; 64]; @@ -112,3 +109,83 @@ async fn descriptor_mismatch_error() { } } } + +/// Test descriptor mismatch error with MemoryStorage backend. +/// +/// This test verifies that the descriptor mismatch error message works correctly +/// with the in-memory storage backend. +#[tokio::test] +async fn descriptor_mismatch_error_memory() { + dotenv::dotenv().ok(); + let storage = Arc::new(MemoryStorage::new()) as Arc; + test_descriptor_mismatch_error_with_storage(storage).await; +} + +/// Test descriptor mismatch error with SledStorage backend. +/// +/// This test verifies that the descriptor mismatch error message works correctly +/// with the Sled embedded database storage backend. +#[cfg(feature = "sled")] +#[tokio::test] +async fn descriptor_mismatch_error_sled() { + use ddk::storage::sled::SledStorage; + use uuid; + + dotenv::dotenv().ok(); + let logger = Arc::new(Logger::console( + "descriptor_mismatch_sled_test".to_string(), + LogLevel::Info, + )); + + // Create a temporary directory for the sled database + let temp_dir = std::env::temp_dir(); + let db_path = temp_dir.join(format!("ddk_test_sled_{}", uuid::Uuid::new_v4())); + + let storage = Arc::new( + SledStorage::new(db_path.to_str().unwrap(), logger) + .expect("Failed to create SledStorage") + ) as Arc; + + test_descriptor_mismatch_error_with_storage(storage).await; + + // Cleanup: remove the temporary database + if db_path.exists() { + std::fs::remove_dir_all(&db_path).ok(); + } +} + +/// Test descriptor mismatch error with PostgresStorage backend. +/// +/// This test verifies that the descriptor mismatch error message works correctly +/// with the PostgreSQL storage backend. +/// +/// Note: Requires DATABASE_URL environment variable to be set. +#[cfg(feature = "postgres")] +#[tokio::test] +async fn descriptor_mismatch_error_postgres() { + use ddk::storage::postgres::PostgresStore; + use uuid; + + dotenv::dotenv().ok(); + let postgres_url = std::env::var("DATABASE_URL") + .expect("DATABASE_URL must be set for postgres tests"); + + let logger = Arc::new(Logger::console( + "descriptor_mismatch_postgres_test".to_string(), + LogLevel::Info, + )); + + // Create a unique wallet name for this test to avoid conflicts + let wallet_name = format!("test_wallet_{}", uuid::Uuid::new_v4()); + + let storage = Arc::new( + PostgresStore::new(&postgres_url, true, logger, wallet_name.clone()) + .await + .expect("Failed to create PostgresStore") + ) as Arc; + + test_descriptor_mismatch_error_with_storage(storage).await; + + // Note: We don't clean up the database here as it's shared infrastructure. + // In a real scenario, you might want to clean up test data. +} From 77a1401c8d391ba7a7ac037d6e1ebf617bb6ccbe Mon Sep 17 00:00:00 2001 From: HalFinneyIsMyHomeBoy Date: Mon, 12 Jan 2026 01:21:48 +0000 Subject: [PATCH 06/14] improved and simplified extraction logic --- ddk/src/wallet/mod.rs | 130 +++++++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/ddk/src/wallet/mod.rs b/ddk/src/wallet/mod.rs index 57938c9..1c6da54 100644 --- a/ddk/src/wallet/mod.rs +++ b/ddk/src/wallet/mod.rs @@ -33,7 +33,8 @@ use bdk_chain::Balance; use bdk_wallet::coin_selection::{ BranchAndBoundCoinSelection, CoinSelectionAlgorithm, SingleRandomDraw, }; -use bdk_wallet::descriptor::IntoWalletDescriptor; +use bdk_wallet::descriptor::{Descriptor, IntoWalletDescriptor}; +use bdk_wallet::keys::DescriptorPublicKey; use bdk_wallet::AsyncWalletPersister; pub use bdk_wallet::LocalOutput; use bdk_wallet::{ @@ -221,8 +222,7 @@ fn extract_descriptor_checksum(descriptor: &str) -> String { } } -/// Helper function to parse BDK load errors and extract descriptor mismatch information. -/// + /// This function attempts to identify descriptor mismatches from BDK error messages /// and provides detailed information about which keychain failed and what descriptors /// were expected vs stored. Only checksums of descriptors are shown for security. @@ -285,79 +285,81 @@ fn parse_descriptor_mismatch_error( /// Attempts to extract the stored descriptor from BDK error messages. /// /// BDK error messages may contain descriptor information in various formats. -/// This function tries common patterns to extract the stored descriptor. +/// This function extracts potential descriptors and validates them using BDK's +/// descriptor parser (which uses the bitcoin crate internally). fn extract_stored_descriptor_from_error(error_msg: &str, error_debug: &str) -> Option { - // Common patterns BDK might use in error messages: - // - "stored: " - // - "found: " - // - "existing: " - // - "persisted: " - // - Descriptor might be in quotes or after certain keywords - - // BDK error format: "Descriptor mismatch for External keychain: loaded , expected " - // Try to extract from "loaded " pattern first (most common BDK format) + // Try both error message formats for text in [error_msg, error_debug] { - if let Some(pos) = text.find("loaded ") { - let after_loaded = &text[pos + "loaded ".len()..]; - // Find the comma that separates stored from expected - if let Some(comma_pos) = after_loaded.find(',') { - let stored_desc = after_loaded[..comma_pos].trim(); - if stored_desc.contains("wpkh") || stored_desc.contains("sh") || stored_desc.contains("tr") { - return Some(stored_desc.to_string()); - } - } else { - // No comma found, try to extract until end or newline - let trimmed = after_loaded.trim(); - if let Some(desc_end) = trimmed.find(|c: char| c == '\n' || c == '\r') { - let potential_desc = trimmed[..desc_end].trim(); - if potential_desc.contains("wpkh") || potential_desc.contains("sh") || potential_desc.contains("tr") { - return Some(potential_desc.to_string()); - } - } else if trimmed.contains("wpkh") || trimmed.contains("sh") || trimmed.contains("tr") { - // Take first reasonable chunk - let end = trimmed.len().min(200); - return Some(trimmed[..end].trim().to_string()); + // Try common BDK error patterns + if let Some(desc) = extract_after_keyword(text, "loaded ") { + if let Some(valid_desc) = validate_descriptor(&desc) { + return Some(valid_desc); + } + } + + // Try other common patterns + for keyword in ["stored:", "found:", "existing:", "persisted:", "stored ", "found ", "existing ", "persisted "] { + if let Some(desc) = extract_after_keyword(text, keyword) { + if let Some(valid_desc) = validate_descriptor(&desc) { + return Some(valid_desc); } } } } - // Fallback to other patterns - let keywords = ["stored:", "found:", "existing:", "persisted:", "stored ", "found ", "existing ", "persisted "]; + None +} + +/// Extracts a potential descriptor string after a keyword in the error text. +/// +/// Handles descriptors in quotes, separated by commas, or terminated by newlines/braces. +fn extract_after_keyword(text: &str, keyword: &str) -> Option { + let pos = text.find(keyword)?; + let after_keyword = &text[pos + keyword.len()..]; - for text in [error_msg, error_debug] { - for keyword in &keywords { - if let Some(pos) = text.find(keyword) { - let after_keyword = &text[pos + keyword.len()..]; - // Try to find descriptor in quotes first - if let Some(quote_start) = after_keyword.find('"') { - if let Some(quote_end) = after_keyword[quote_start + 1..].find('"') { - let descriptor = &after_keyword[quote_start + 1..quote_start + 1 + quote_end]; - if descriptor.contains("wpkh") || descriptor.contains("sh") || descriptor.contains("tr") { - return Some(descriptor.to_string()); - } - } - } - - // Try to extract descriptor without quotes (look for common descriptor patterns) - let trimmed = after_keyword.trim(); - // Descriptors typically start with certain patterns and contain specific characters - if let Some(desc_end) = trimmed.find(|c: char| c == '\n' || c == '\r' || c == ',' || c == '}') { - let potential_desc = trimmed[..desc_end].trim(); - if potential_desc.contains("wpkh") || potential_desc.contains("sh") || potential_desc.contains("tr") - || potential_desc.starts_with("wpkh") || potential_desc.starts_with("sh") || potential_desc.starts_with("tr") { - return Some(potential_desc.to_string()); - } - } else if trimmed.contains("wpkh") || trimmed.contains("sh") || trimmed.contains("tr") { - // Take first reasonable chunk - let end = trimmed.len().min(200); // Limit length - return Some(trimmed[..end].trim().to_string()); - } + // Try to extract from quoted string first (most reliable) + if let Some(quote_start) = after_keyword.find('"') { + if let Some(quote_end) = after_keyword[quote_start + 1..].find('"') { + let descriptor = after_keyword[quote_start + 1..quote_start + 1 + quote_end].trim(); + if !descriptor.is_empty() { + return Some(descriptor.to_string()); } } } - None + // Extract until delimiter (comma, newline, closing brace, or end of string) + let trimmed = after_keyword.trim(); + let desc_end = trimmed + .find(|c: char| matches!(c, ',' | '\n' | '\r' | '}' | ']')) + .unwrap_or_else(|| trimmed.len().min(500)); // Limit to reasonable length + + let potential_desc = trimmed[..desc_end].trim(); + if !potential_desc.is_empty() { + Some(potential_desc.to_string()) + } else { + None + } +} + +/// Validates that a string is a valid descriptor by attempting to parse it. +/// +/// Uses BDK's descriptor parser (which uses the bitcoin crate internally) +/// to ensure only valid descriptors are returned. The parser handles checksums +/// and whitespace automatically. +fn validate_descriptor(descriptor_str: &str) -> Option { + let trimmed = descriptor_str.trim(); + + // Skip empty strings + if trimmed.is_empty() { + return None; + } + + // Try to parse as a descriptor to validate it + // BDK's parser handles checksums (e.g., "wpkh(...)#abc12345") automatically + trimmed + .parse::>() + .ok() + .map(|_| trimmed.to_string()) } impl DlcDevKitWallet { From 1fb7c84e1cffbb57d22be4e3fcfb3535ecfdadc3 Mon Sep 17 00:00:00 2001 From: HalFinneyIsMyHomeBoy Date: Mon, 12 Jan 2026 21:52:04 +0000 Subject: [PATCH 07/14] chore --- ddk/src/wallet/mod.rs | 80 ++++++++++++++++++++++++++----------------- ddk/tests/wallet.rs | 51 ++++++++++++++------------- 2 files changed, 73 insertions(+), 58 deletions(-) diff --git a/ddk/src/wallet/mod.rs b/ddk/src/wallet/mod.rs index 1c6da54..44531f4 100644 --- a/ddk/src/wallet/mod.rs +++ b/ddk/src/wallet/mod.rs @@ -208,7 +208,7 @@ pub struct DlcDevKitWallet { const MIN_FEERATE: u32 = 253; /// Helper function to extract the checksum from a descriptor string. -/// +/// /// Descriptors typically have a checksum at the end after a '#' character. /// Returns the checksum (usually 8 alphanumeric characters) or "unknown" if not found. fn extract_descriptor_checksum(descriptor: &str) -> String { @@ -222,7 +222,6 @@ fn extract_descriptor_checksum(descriptor: &str) -> String { } } - /// This function attempts to identify descriptor mismatches from BDK error messages /// and provides detailed information about which keychain failed and what descriptors /// were expected vs stored. Only checksums of descriptors are shown for security. @@ -234,30 +233,36 @@ fn parse_descriptor_mismatch_error( let error_msg = error.to_string(); let error_debug = format!("{:?}", error); let error_lower = error_msg.to_lowercase(); - + // Try to determine which keychain failed by checking error message // Look for explicit mentions of keychain types - let (keychain, expected_descriptor) = if error_lower.contains("external") - || error_debug.contains("External") - || error_debug.contains("external") { + let (keychain, expected_descriptor) = if error_lower.contains("external") + || error_debug.contains("External") + || error_debug.contains("external") + { ("external", external_descriptor_str.to_string()) - } else if error_lower.contains("internal") - || error_debug.contains("Internal") - || error_debug.contains("internal") { + } else if error_lower.contains("internal") + || error_debug.contains("Internal") + || error_debug.contains("internal") + { ("internal", internal_descriptor_str.to_string()) } else { // If we can't determine from the error message, we need to check both // Check if either descriptor appears in the error (indicating which one was expected) - if error_msg.contains(external_descriptor_str) || error_debug.contains(external_descriptor_str) { + if error_msg.contains(external_descriptor_str) + || error_debug.contains(external_descriptor_str) + { ("external", external_descriptor_str.to_string()) - } else if error_msg.contains(internal_descriptor_str) || error_debug.contains(internal_descriptor_str) { + } else if error_msg.contains(internal_descriptor_str) + || error_debug.contains(internal_descriptor_str) + { ("internal", internal_descriptor_str.to_string()) } else { // Default to external if we can't determine ("external", external_descriptor_str.to_string()) } }; - + // Try to extract the stored descriptor from the error message // BDK errors might contain descriptor information in various formats let stored_descriptor = extract_stored_descriptor_from_error(&error_msg, &error_debug) @@ -265,7 +270,7 @@ fn parse_descriptor_mismatch_error( // If we can't extract it, provide a helpful message with the original error format!("Could not extract stored descriptor from error message. This may indicate a descriptor mismatch. Original error: {}", error_msg) }); - + // Extract checksums from descriptors instead of showing full descriptors let expected_checksum = extract_descriptor_checksum(&expected_descriptor); let stored_checksum = if stored_descriptor.starts_with("Could not extract") { @@ -274,7 +279,7 @@ fn parse_descriptor_mismatch_error( } else { extract_descriptor_checksum(&stored_descriptor) }; - + WalletError::DescriptorMismatch { keychain: keychain.to_string(), expected: expected_checksum, @@ -283,7 +288,7 @@ fn parse_descriptor_mismatch_error( } /// Attempts to extract the stored descriptor from BDK error messages. -/// +/// /// BDK error messages may contain descriptor information in various formats. /// This function extracts potential descriptors and validates them using BDK's /// descriptor parser (which uses the bitcoin crate internally). @@ -296,9 +301,18 @@ fn extract_stored_descriptor_from_error(error_msg: &str, error_debug: &str) -> O return Some(valid_desc); } } - + // Try other common patterns - for keyword in ["stored:", "found:", "existing:", "persisted:", "stored ", "found ", "existing ", "persisted "] { + for keyword in [ + "stored:", + "found:", + "existing:", + "persisted:", + "stored ", + "found ", + "existing ", + "persisted ", + ] { if let Some(desc) = extract_after_keyword(text, keyword) { if let Some(valid_desc) = validate_descriptor(&desc) { return Some(valid_desc); @@ -306,17 +320,17 @@ fn extract_stored_descriptor_from_error(error_msg: &str, error_debug: &str) -> O } } } - + None } /// Extracts a potential descriptor string after a keyword in the error text. -/// +/// /// Handles descriptors in quotes, separated by commas, or terminated by newlines/braces. fn extract_after_keyword(text: &str, keyword: &str) -> Option { let pos = text.find(keyword)?; let after_keyword = &text[pos + keyword.len()..]; - + // Try to extract from quoted string first (most reliable) if let Some(quote_start) = after_keyword.find('"') { if let Some(quote_end) = after_keyword[quote_start + 1..].find('"') { @@ -326,13 +340,13 @@ fn extract_after_keyword(text: &str, keyword: &str) -> Option { } } } - + // Extract until delimiter (comma, newline, closing brace, or end of string) let trimmed = after_keyword.trim(); let desc_end = trimmed .find(|c: char| matches!(c, ',' | '\n' | '\r' | '}' | ']')) .unwrap_or_else(|| trimmed.len().min(500)); // Limit to reasonable length - + let potential_desc = trimmed[..desc_end].trim(); if !potential_desc.is_empty() { Some(potential_desc.to_string()) @@ -342,18 +356,18 @@ fn extract_after_keyword(text: &str, keyword: &str) -> Option { } /// Validates that a string is a valid descriptor by attempting to parse it. -/// +/// /// Uses BDK's descriptor parser (which uses the bitcoin crate internally) /// to ensure only valid descriptors are returned. The parser handles checksums /// and whitespace automatically. fn validate_descriptor(descriptor_str: &str) -> Option { let trimmed = descriptor_str.trim(); - + // Skip empty strings if trimmed.is_empty() { return None; } - + // Try to parse as a descriptor to validate it // BDK's parser handles checksums (e.g., "wpkh(...)#abc12345") automatically trimmed @@ -419,17 +433,19 @@ impl DlcDevKitWallet { // Check if this is a descriptor mismatch error let error_msg = e.to_string(); let error_lower = error_msg.to_lowercase(); - + // Convert descriptors to strings for the helper function let external_desc_str = external_descriptor.0.to_string(); let internal_desc_str = internal_descriptor.0.to_string(); - + // Common indicators of descriptor mismatch errors - if error_lower.contains("descriptor") && (error_lower.contains("mismatch") - || error_lower.contains("does not match") - || error_lower.contains("expected") - || error_lower.contains("stored") - || error_lower.contains("found")) { + if error_lower.contains("descriptor") + && (error_lower.contains("mismatch") + || error_lower.contains("does not match") + || error_lower.contains("expected") + || error_lower.contains("stored") + || error_lower.contains("found")) + { parse_descriptor_mismatch_error(&e, &external_desc_str, &internal_desc_str) } else { // For other errors, use the generic error diff --git a/ddk/tests/wallet.rs b/ddk/tests/wallet.rs index 10f4043..b446640 100644 --- a/ddk/tests/wallet.rs +++ b/ddk/tests/wallet.rs @@ -1,22 +1,22 @@ mod test_util; +use bitcoin::Network; use ddk::chain::EsploraClient; use ddk::error::WalletError; use ddk::logger::{LogLevel, Logger}; use ddk::storage::memory::MemoryStorage; use ddk::wallet::DlcDevKitWallet; use ddk::Storage; -use bitcoin::Network; use std::sync::Arc; /// Helper function to test descriptor mismatch error across different storage backends. -/// +/// /// This function creates a wallet with one seed, persists it, then tries to load it /// with a different seed. It verifies that the error message correctly shows checksums /// instead of full descriptors. async fn test_descriptor_mismatch_error_with_storage(storage: Arc) { dotenv::dotenv().ok(); - + // Setup logger let logger = Arc::new(Logger::console( "descriptor_mismatch_test".to_string(), @@ -30,11 +30,10 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) .expect("Failed to create Esplora client"), ); - // First seed - this will create and persist a wallet let mut seed1 = [0u8; 64]; seed1[0..8].copy_from_slice(b"seed_one"); - + let _wallet1 = DlcDevKitWallet::new( &seed1, esplora.clone(), @@ -50,7 +49,7 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) // This should fail at line 262 because the descriptors won't match let mut seed2 = [0u8; 64]; seed2[0..8].copy_from_slice(b"seed_two"); - + let result = DlcDevKitWallet::new( &seed2, esplora.clone(), @@ -64,7 +63,11 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) // Verify we got the expected error match result { Ok(_) => panic!("Expected DescriptorMismatch error but wallet loaded successfully"), - Err(WalletError::DescriptorMismatch { keychain, expected, stored }) => { + Err(WalletError::DescriptorMismatch { + keychain, + expected, + stored, + }) => { println!("\n{}", "=".repeat(70)); println!("SUCCESS: Descriptor mismatch error detected"); println!("{}", "=".repeat(70)); @@ -72,7 +75,7 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) println!("Expected descriptor checksum: {}", expected); println!("Stored descriptor checksum: {}", stored); println!("{}", "=".repeat(70)); - + // Verify the error contains meaningful information assert_eq!(keychain, "external", "Should identify external keychain"); assert!( @@ -130,24 +133,23 @@ async fn descriptor_mismatch_error_memory() { async fn descriptor_mismatch_error_sled() { use ddk::storage::sled::SledStorage; use uuid; - + dotenv::dotenv().ok(); let logger = Arc::new(Logger::console( "descriptor_mismatch_sled_test".to_string(), LogLevel::Info, )); - + // Create a temporary directory for the sled database let temp_dir = std::env::temp_dir(); let db_path = temp_dir.join(format!("ddk_test_sled_{}", uuid::Uuid::new_v4())); - + let storage = Arc::new( - SledStorage::new(db_path.to_str().unwrap(), logger) - .expect("Failed to create SledStorage") + SledStorage::new(db_path.to_str().unwrap(), logger).expect("Failed to create SledStorage"), ) as Arc; - + test_descriptor_mismatch_error_with_storage(storage).await; - + // Cleanup: remove the temporary database if db_path.exists() { std::fs::remove_dir_all(&db_path).ok(); @@ -165,27 +167,24 @@ async fn descriptor_mismatch_error_sled() { async fn descriptor_mismatch_error_postgres() { use ddk::storage::postgres::PostgresStore; use uuid; - + dotenv::dotenv().ok(); - let postgres_url = std::env::var("DATABASE_URL") - .expect("DATABASE_URL must be set for postgres tests"); - + let postgres_url = + std::env::var("DATABASE_URL").expect("DATABASE_URL must be set for postgres tests"); + let logger = Arc::new(Logger::console( "descriptor_mismatch_postgres_test".to_string(), LogLevel::Info, )); - + // Create a unique wallet name for this test to avoid conflicts let wallet_name = format!("test_wallet_{}", uuid::Uuid::new_v4()); - + let storage = Arc::new( PostgresStore::new(&postgres_url, true, logger, wallet_name.clone()) .await - .expect("Failed to create PostgresStore") + .expect("Failed to create PostgresStore"), ) as Arc; - + test_descriptor_mismatch_error_with_storage(storage).await; - - // Note: We don't clean up the database here as it's shared infrastructure. - // In a real scenario, you might want to clean up test data. } From 58ce57371d182cd86617cc69945284ce5e2065e0 Mon Sep 17 00:00:00 2001 From: HalFinneyIsMyHomeBoy Date: Mon, 12 Jan 2026 22:10:29 +0000 Subject: [PATCH 08/14] chore --- ddk/Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ddk/Cargo.toml b/ddk/Cargo.toml index eb6c68b..10c9fed 100644 --- a/ddk/Cargo.toml +++ b/ddk/Cargo.toml @@ -24,10 +24,10 @@ sled = ["dep:sled"] postgres = ["dep:sqlx", "sqlx/postgres"] [dependencies] -ddk-manager = { version = "1.0.6", path = "../ddk-manager", features = ["use-serde"] } -ddk-dlc = { version = "1.0.6", path = "../dlc", features = ["use-serde"] } -ddk-messages = { version = "1.0.6", path = "../dlc-messages", features = [ "use-serde"] } -ddk-trie = { version = "1.0.6", path = "../dlc-trie", features = ["use-serde"] } +ddk-manager = { version = "1.0.9", path = "../ddk-manager", features = ["use-serde"] } +ddk-dlc = { version = "1.0.9", path = "../dlc", features = ["use-serde"] } +ddk-messages = { version = "1.0.9", path = "../dlc-messages", features = [ "use-serde"] } +ddk-trie = { version = "1.0.9", path = "../dlc-trie", features = ["use-serde"] } bitcoin = { version = "0.32.6", features = ["rand", "serde"] } bdk_esplora = { version = "0.22.0", features = ["blocking-https", "async-https", "tokio"] } @@ -58,7 +58,7 @@ lightning-net-tokio = { version = "0.1.0", optional = true } # oracle feature reqwest = { version = "0.12.9", features = ["json"], optional = true } -kormir = { version = "1.0.6", path = "../kormir" } +kormir = { version = "1.0.9", path = "../kormir" } hmac = "0.12.1" sha2 = "0.10" nostr-database = { version = "0.40.0", optional = true } From c3cbb79b161beb0415c87cf8b10e12a041ad28f0 Mon Sep 17 00:00:00 2001 From: HalFinneyIsMyHomeBoy Date: Tue, 13 Jan 2026 21:52:09 +0000 Subject: [PATCH 09/14] Refactoring, added more useful info to error message, and updated tests. --- ddk/src/error.rs | 10 ++- ddk/src/wallet/mod.rs | 205 ++++++++++++++++++------------------------ ddk/tests/wallet.rs | 117 ++++++++++++++++++------ 3 files changed, 190 insertions(+), 142 deletions(-) diff --git a/ddk/src/error.rs b/ddk/src/error.rs index 2a788a7..6924503 100644 --- a/ddk/src/error.rs +++ b/ddk/src/error.rs @@ -207,7 +207,15 @@ pub enum WalletError { InvalidDerivationIndex, #[error("Invalid secret key")] InvalidSecretKey, - #[error("Descriptor mismatch in {keychain} keychain.\nExpected descriptor checksum: {expected}\nStored descriptor checksum: {stored}")] + #[error( + "DESCRIPTOR MISMATCH DETECTED\n\n\ + {keychain} descriptor mismatch detected.\n\n\ + Expected descriptor:\n{expected}\n\n\ + Stored descriptor:\n{stored}\n\n\ + This error occurs when the wallet's stored descriptor doesn't match the descriptor\n\ + derived from the current seed. Please verify you're using the correct seed/private key\n\ + for this wallet, or reset the wallet data if needed. Please verify your backups before resetting." + )] DescriptorMismatch { keychain: String, expected: String, diff --git a/ddk/src/wallet/mod.rs b/ddk/src/wallet/mod.rs index 44531f4..175eb5c 100644 --- a/ddk/src/wallet/mod.rs +++ b/ddk/src/wallet/mod.rs @@ -222,6 +222,25 @@ fn extract_descriptor_checksum(descriptor: &str) -> String { } } +/// Extracts fingerprint and derivation path from bracketed content in descriptor. +/// +/// Descriptors typically have a derivation path in brackets, e.g., [7caf7de6/84'/1'/0']. +/// Returns (fingerprint, path) or ("unknown", "unknown") if not found. +fn extract_descriptor_fingerprint_and_path(descriptor: &str) -> (String, String) { + if let Some(bracket_start) = descriptor.find('[') { + if let Some(bracket_end) = descriptor[bracket_start..].find(']') { + let content = &descriptor[bracket_start + 1..bracket_start + bracket_end]; + if let Some(slash_pos) = content.find('/') { + return ( + content[..slash_pos].to_string(), + content[slash_pos + 1..].to_string(), + ); + } + } + } + ("unknown".to_string(), "unknown".to_string()) +} + /// This function attempts to identify descriptor mismatches from BDK error messages /// and provides detailed information about which keychain failed and what descriptors /// were expected vs stored. Only checksums of descriptors are shown for security. @@ -236,144 +255,98 @@ fn parse_descriptor_mismatch_error( // Try to determine which keychain failed by checking error message // Look for explicit mentions of keychain types - let (keychain, expected_descriptor) = if error_lower.contains("external") - || error_debug.contains("External") - || error_debug.contains("external") - { - ("external", external_descriptor_str.to_string()) - } else if error_lower.contains("internal") - || error_debug.contains("Internal") - || error_debug.contains("internal") - { - ("internal", internal_descriptor_str.to_string()) - } else { - // If we can't determine from the error message, we need to check both - // Check if either descriptor appears in the error (indicating which one was expected) - if error_msg.contains(external_descriptor_str) - || error_debug.contains(external_descriptor_str) - { + let (keychain, expected_descriptor) = + if error_lower.contains("external") || error_debug.contains("External") { ("external", external_descriptor_str.to_string()) - } else if error_msg.contains(internal_descriptor_str) - || error_debug.contains(internal_descriptor_str) - { + } else if error_lower.contains("internal") || error_debug.contains("Internal") { ("internal", internal_descriptor_str.to_string()) } else { - // Default to external if we can't determine - ("external", external_descriptor_str.to_string()) - } - }; - - // Try to extract the stored descriptor from the error message - // BDK errors might contain descriptor information in various formats - let stored_descriptor = extract_stored_descriptor_from_error(&error_msg, &error_debug) - .unwrap_or_else(|| { - // If we can't extract it, provide a helpful message with the original error - format!("Could not extract stored descriptor from error message. This may indicate a descriptor mismatch. Original error: {}", error_msg) - }); + // If we can't determine from the error message, check if either descriptor appears + // in the error (indicating which one was expected) + if error_msg.contains(external_descriptor_str) + || error_debug.contains(external_descriptor_str) + { + ("external", external_descriptor_str.to_string()) + } else if error_msg.contains(internal_descriptor_str) + || error_debug.contains(internal_descriptor_str) + { + ("internal", internal_descriptor_str.to_string()) + } else { + // Default to external if we can't determine + ("external", external_descriptor_str.to_string()) + } + }; - // Extract checksums from descriptors instead of showing full descriptors + // Extract checksums, fingerprints, and derivation paths directly from descriptors let expected_checksum = extract_descriptor_checksum(&expected_descriptor); - let stored_checksum = if stored_descriptor.starts_with("Could not extract") { - // If we couldn't extract the descriptor, keep the error message as-is - stored_descriptor - } else { - extract_descriptor_checksum(&stored_descriptor) + let (expected_fingerprint, expected_path) = + extract_descriptor_fingerprint_and_path(&expected_descriptor); + + // Extract stored checksum, fingerprint, and path directly from error message + // BDK error format: "loaded wpkh([fingerprint/path]...)#checksum, expected ..." + let (stored_checksum, stored_fingerprint, stored_path) = + extract_stored_info_from_error(&error_msg, &error_debug); + + // Format descriptor information with fingerprint below the path + let format_descriptor_info = |checksum: &str, path: &str, fingerprint: &str| { + if path != "unknown" { + format!( + " Checksum: {}\n DerivationPath: {}\n Fingerprint: {}", + checksum, path, fingerprint + ) + } else { + format!(" Checksum: {}", checksum) + } }; + let expected = + format_descriptor_info(&expected_checksum, &expected_path, &expected_fingerprint); + let stored = format_descriptor_info(&stored_checksum, &stored_path, &stored_fingerprint); + WalletError::DescriptorMismatch { keychain: keychain.to_string(), - expected: expected_checksum, - stored: stored_checksum, + expected, + stored, } } -/// Attempts to extract the stored descriptor from BDK error messages. +/// Extracts checksum, fingerprint, and derivation path from BDK error messages using descriptor parsing. /// -/// BDK error messages may contain descriptor information in various formats. -/// This function extracts potential descriptors and validates them using BDK's -/// descriptor parser (which uses the bitcoin crate internally). -fn extract_stored_descriptor_from_error(error_msg: &str, error_debug: &str) -> Option { +/// BDK error format: "loaded wpkh([fingerprint/path]...)#checksum, expected ..." +/// This function extracts the full descriptor string, parses it using BDK's parser, +/// then extracts checksum, fingerprint, and path from the parsed descriptor's string representation. +fn extract_stored_info_from_error(error_msg: &str, error_debug: &str) -> (String, String, String) { // Try both error message formats for text in [error_msg, error_debug] { - // Try common BDK error patterns - if let Some(desc) = extract_after_keyword(text, "loaded ") { - if let Some(valid_desc) = validate_descriptor(&desc) { - return Some(valid_desc); - } - } + // Look for "loaded " keyword which precedes the stored descriptor + if let Some(loaded_pos) = text.find("loaded ") { + let after_loaded = &text[loaded_pos + 7..]; // Skip "loaded " - // Try other common patterns - for keyword in [ - "stored:", - "found:", - "existing:", - "persisted:", - "stored ", - "found ", - "existing ", - "persisted ", - ] { - if let Some(desc) = extract_after_keyword(text, keyword) { - if let Some(valid_desc) = validate_descriptor(&desc) { - return Some(valid_desc); - } - } - } - } + // Extract the full descriptor string (up to the comma or end) + let desc_end = after_loaded.find(',').unwrap_or(after_loaded.len()); + let descriptor_str = after_loaded[..desc_end].trim(); - None -} + // Try to parse the descriptor using BDK's parser + if let Ok(descriptor) = descriptor_str.parse::>() { + // Get the canonical string representation (includes checksum) + let canonical_str = descriptor.to_string(); -/// Extracts a potential descriptor string after a keyword in the error text. -/// -/// Handles descriptors in quotes, separated by commas, or terminated by newlines/braces. -fn extract_after_keyword(text: &str, keyword: &str) -> Option { - let pos = text.find(keyword)?; - let after_keyword = &text[pos + keyword.len()..]; - - // Try to extract from quoted string first (most reliable) - if let Some(quote_start) = after_keyword.find('"') { - if let Some(quote_end) = after_keyword[quote_start + 1..].find('"') { - let descriptor = after_keyword[quote_start + 1..quote_start + 1 + quote_end].trim(); - if !descriptor.is_empty() { - return Some(descriptor.to_string()); + // Extract checksum, fingerprint, and path from the canonical representation + let checksum = extract_descriptor_checksum(&canonical_str); + let (fingerprint, path) = extract_descriptor_fingerprint_and_path(&canonical_str); + + if checksum != "unknown" || path != "unknown" { + return (checksum, fingerprint, path); + } } } } - // Extract until delimiter (comma, newline, closing brace, or end of string) - let trimmed = after_keyword.trim(); - let desc_end = trimmed - .find(|c: char| matches!(c, ',' | '\n' | '\r' | '}' | ']')) - .unwrap_or_else(|| trimmed.len().min(500)); // Limit to reasonable length - - let potential_desc = trimmed[..desc_end].trim(); - if !potential_desc.is_empty() { - Some(potential_desc.to_string()) - } else { - None - } -} - -/// Validates that a string is a valid descriptor by attempting to parse it. -/// -/// Uses BDK's descriptor parser (which uses the bitcoin crate internally) -/// to ensure only valid descriptors are returned. The parser handles checksums -/// and whitespace automatically. -fn validate_descriptor(descriptor_str: &str) -> Option { - let trimmed = descriptor_str.trim(); - - // Skip empty strings - if trimmed.is_empty() { - return None; - } - - // Try to parse as a descriptor to validate it - // BDK's parser handles checksums (e.g., "wpkh(...)#abc12345") automatically - trimmed - .parse::>() - .ok() - .map(|_| trimmed.to_string()) + ( + "unknown".to_string(), + "unknown".to_string(), + "unknown".to_string(), + ) } impl DlcDevKitWallet { diff --git a/ddk/tests/wallet.rs b/ddk/tests/wallet.rs index b446640..0c6c3f7 100644 --- a/ddk/tests/wallet.rs +++ b/ddk/tests/wallet.rs @@ -68,38 +68,49 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) expected, stored, }) => { - println!("\n{}", "=".repeat(70)); + println!("\n{}", "=".repeat(80)); println!("SUCCESS: Descriptor mismatch error detected"); - println!("{}", "=".repeat(70)); + println!("{}", "=".repeat(80)); println!("Keychain: {}", keychain); - println!("Expected descriptor checksum: {}", expected); - println!("Stored descriptor checksum: {}", stored); - println!("{}", "=".repeat(70)); + println!("Expected descriptor: {}", expected); + println!("Stored descriptor: {}", stored); + println!("{}", "=".repeat(80)); // Verify the error contains meaningful information assert_eq!(keychain, "external", "Should identify external keychain"); assert!( - expected.len() > 0, - "Expected descriptor checksum should not be empty" - ); - assert!( - stored.len() > 0, - "Stored descriptor checksum should not be empty" + !expected.is_empty(), + "Expected descriptor should not be empty" ); - // Verify checksums are valid (8 alphanumeric characters or "unknown") - // Checksums should be 8 characters (typical) or "unknown" if extraction failed + assert!(!stored.is_empty(), "Stored descriptor should not be empty"); + // Verify the format includes checksum, path, and fingerprint assert!( - expected.len() == 8 || expected == "unknown", - "Expected checksum should be 8 characters or 'unknown', got: '{}' (length: {})", - expected, - expected.len() + expected.contains("DerivationPath:") || expected == "unknown", + "Expected descriptor should include DerivationPath, got: '{}'", + expected ); assert!( - stored.len() == 8 || stored == "unknown" || stored.starts_with("Could not extract"), - "Stored checksum should be 8 characters, 'unknown', or an error message, got: '{}' (length: {})", - stored, - stored.len() + stored.contains("DerivationPath:") + || stored == "unknown" + || stored.starts_with("Could not extract"), + "Stored descriptor should include DerivationPath, got: '{}'", + stored ); + // Verify fingerprint is present when path is present + if expected.contains("DerivationPath:") { + assert!( + expected.contains("Fingerprint:"), + "Expected descriptor should include Fingerprint when DerivationPath is present, got: '{}'", + expected + ); + } + if stored.contains("DerivationPath:") { + assert!( + stored.contains("Fingerprint:"), + "Stored descriptor should include Fingerprint when DerivationPath is present, got: '{}'", + stored + ); + } } Err(e) => { println!("\n{}", "=".repeat(70)); @@ -157,10 +168,6 @@ async fn descriptor_mismatch_error_sled() { } /// Test descriptor mismatch error with PostgresStorage backend. -/// -/// This test verifies that the descriptor mismatch error message works correctly -/// with the PostgreSQL storage backend. -/// /// Note: Requires DATABASE_URL environment variable to be set. #[cfg(feature = "postgres")] #[tokio::test] @@ -188,3 +195,63 @@ async fn descriptor_mismatch_error_postgres() { test_descriptor_mismatch_error_with_storage(storage).await; } + +/// Test function to display the formatted error message. +/// +/// This test triggers a descriptor mismatch error and prints the full +/// formatted error message directly from the error's Display implementation. +#[tokio::test] +async fn display_error_message() { + dotenv::dotenv().ok(); + let storage = Arc::new(MemoryStorage::new()) as Arc; + + // Setup logger + let logger = Arc::new(Logger::console( + "display_error_test".to_string(), + LogLevel::Info, + )); + + // Setup Esplora client + let esplora_host = std::env::var("ESPLORA_HOST").expect("ESPLORA_HOST must be set"); + let esplora = Arc::new( + EsploraClient::new(&esplora_host, Network::Regtest, logger.clone()) + .expect("Failed to create Esplora client"), + ); + + // First seed - this will create and persist a wallet + let mut seed1 = [0u8; 64]; + seed1[0..8].copy_from_slice(b"seed_one"); + + let _wallet1 = DlcDevKitWallet::new( + &seed1, + esplora.clone(), + Network::Regtest, + storage.clone(), + None, + logger.clone(), + ) + .await + .expect("Failed to create first wallet"); + + // Second seed - this will try to load the existing wallet but with different descriptors + let mut seed2 = [0u8; 64]; + seed2[0..8].copy_from_slice(b"seed_two"); + + let result = DlcDevKitWallet::new( + &seed2, + esplora.clone(), + Network::Regtest, + storage.clone(), + None, + logger.clone(), + ) + .await; + + // Display the error message directly from the error's Display implementation + match result { + Ok(_) => panic!("Expected DescriptorMismatch error but wallet loaded successfully"), + Err(e) => { + println!("{}", e); + } + } +} From 0ad45c53835d92e17ddb9597c720bb065ea52c9d Mon Sep 17 00:00:00 2001 From: HalFinneyIsMyHomeBoy Date: Thu, 15 Jan 2026 20:13:55 +0000 Subject: [PATCH 10/14] Cleanup+adjustments --- ddk/src/error.rs | 6 +-- ddk/tests/wallet.rs | 99 +++++++++------------------------------------ 2 files changed, 22 insertions(+), 83 deletions(-) diff --git a/ddk/src/error.rs b/ddk/src/error.rs index 6924503..faf5b31 100644 --- a/ddk/src/error.rs +++ b/ddk/src/error.rs @@ -212,9 +212,9 @@ pub enum WalletError { {keychain} descriptor mismatch detected.\n\n\ Expected descriptor:\n{expected}\n\n\ Stored descriptor:\n{stored}\n\n\ - This error occurs when the wallet's stored descriptor doesn't match the descriptor\n\ - derived from the current seed. Please verify you're using the correct seed/private key\n\ - for this wallet, or reset the wallet data if needed. Please verify your backups before resetting." + The wallet's stored descriptor doesn't match the descriptor\n\ + derived from the current seed. Please verify you're using the correct seed\n\ + or reset the wallet data if needed, but verify your wallet backups before resetting." )] DescriptorMismatch { keychain: String, diff --git a/ddk/tests/wallet.rs b/ddk/tests/wallet.rs index 0c6c3f7..f97687c 100644 --- a/ddk/tests/wallet.rs +++ b/ddk/tests/wallet.rs @@ -1,6 +1,6 @@ mod test_util; -use bitcoin::Network; +use bitcoin::{key::rand::Fill, Network}; use ddk::chain::EsploraClient; use ddk::error::WalletError; use ddk::logger::{LogLevel, Logger}; @@ -32,7 +32,9 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) // First seed - this will create and persist a wallet let mut seed1 = [0u8; 64]; - seed1[0..8].copy_from_slice(b"seed_one"); + seed1 + .try_fill(&mut bitcoin::key::rand::thread_rng()) + .expect("Failed to generate random seed"); let _wallet1 = DlcDevKitWallet::new( &seed1, @@ -46,9 +48,10 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) .expect("Failed to create first wallet"); // Second seed - this will try to load the existing wallet but with different descriptors - // This should fail at line 262 because the descriptors won't match let mut seed2 = [0u8; 64]; - seed2[0..8].copy_from_slice(b"seed_two"); + seed2 + .try_fill(&mut bitcoin::key::rand::thread_rng()) + .expect("Failed to generate random seed"); let result = DlcDevKitWallet::new( &seed2, @@ -83,7 +86,7 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) "Expected descriptor should not be empty" ); assert!(!stored.is_empty(), "Stored descriptor should not be empty"); - // Verify the format includes checksum, path, and fingerprint + // Verify the format includes checksum and derivation path assert!( expected.contains("DerivationPath:") || expected == "unknown", "Expected descriptor should include DerivationPath, got: '{}'", @@ -96,21 +99,17 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) "Stored descriptor should include DerivationPath, got: '{}'", stored ); - // Verify fingerprint is present when path is present - if expected.contains("DerivationPath:") { - assert!( - expected.contains("Fingerprint:"), - "Expected descriptor should include Fingerprint when DerivationPath is present, got: '{}'", - expected - ); - } - if stored.contains("DerivationPath:") { - assert!( - stored.contains("Fingerprint:"), - "Stored descriptor should include Fingerprint when DerivationPath is present, got: '{}'", - stored - ); - } + // Verify checksum is present + assert!( + expected.contains("Checksum:"), + "Expected descriptor should include Checksum, got: '{}'", + expected + ); + assert!( + stored.contains("Checksum:"), + "Stored descriptor should include Checksum, got: '{}'", + stored + ); } Err(e) => { println!("\n{}", "=".repeat(70)); @@ -195,63 +194,3 @@ async fn descriptor_mismatch_error_postgres() { test_descriptor_mismatch_error_with_storage(storage).await; } - -/// Test function to display the formatted error message. -/// -/// This test triggers a descriptor mismatch error and prints the full -/// formatted error message directly from the error's Display implementation. -#[tokio::test] -async fn display_error_message() { - dotenv::dotenv().ok(); - let storage = Arc::new(MemoryStorage::new()) as Arc; - - // Setup logger - let logger = Arc::new(Logger::console( - "display_error_test".to_string(), - LogLevel::Info, - )); - - // Setup Esplora client - let esplora_host = std::env::var("ESPLORA_HOST").expect("ESPLORA_HOST must be set"); - let esplora = Arc::new( - EsploraClient::new(&esplora_host, Network::Regtest, logger.clone()) - .expect("Failed to create Esplora client"), - ); - - // First seed - this will create and persist a wallet - let mut seed1 = [0u8; 64]; - seed1[0..8].copy_from_slice(b"seed_one"); - - let _wallet1 = DlcDevKitWallet::new( - &seed1, - esplora.clone(), - Network::Regtest, - storage.clone(), - None, - logger.clone(), - ) - .await - .expect("Failed to create first wallet"); - - // Second seed - this will try to load the existing wallet but with different descriptors - let mut seed2 = [0u8; 64]; - seed2[0..8].copy_from_slice(b"seed_two"); - - let result = DlcDevKitWallet::new( - &seed2, - esplora.clone(), - Network::Regtest, - storage.clone(), - None, - logger.clone(), - ) - .await; - - // Display the error message directly from the error's Display implementation - match result { - Ok(_) => panic!("Expected DescriptorMismatch error but wallet loaded successfully"), - Err(e) => { - println!("{}", e); - } - } -} From 73fb15a2db81901480b00c9a420a7cd0f07f8aeb Mon Sep 17 00:00:00 2001 From: HalFinneyIsMyHomeBoy Date: Thu, 15 Jan 2026 20:56:41 +0000 Subject: [PATCH 11/14] refactoring+ using bdk parser for descriptor --- ddk/src/wallet/mod.rs | 157 ++++++++++++++++++++++-------------------- ddk/tests/wallet.rs | 50 ++++++-------- 2 files changed, 105 insertions(+), 102 deletions(-) diff --git a/ddk/src/wallet/mod.rs b/ddk/src/wallet/mod.rs index 175eb5c..a391bd0 100644 --- a/ddk/src/wallet/mod.rs +++ b/ddk/src/wallet/mod.rs @@ -208,9 +208,6 @@ pub struct DlcDevKitWallet { const MIN_FEERATE: u32 = 253; /// Helper function to extract the checksum from a descriptor string. -/// -/// Descriptors typically have a checksum at the end after a '#' character. -/// Returns the checksum (usually 8 alphanumeric characters) or "unknown" if not found. fn extract_descriptor_checksum(descriptor: &str) -> String { if let Some(hash_pos) = descriptor.rfind('#') { let checksum = &descriptor[hash_pos + 1..]; @@ -223,9 +220,6 @@ fn extract_descriptor_checksum(descriptor: &str) -> String { } /// Extracts fingerprint and derivation path from bracketed content in descriptor. -/// -/// Descriptors typically have a derivation path in brackets, e.g., [7caf7de6/84'/1'/0']. -/// Returns (fingerprint, path) or ("unknown", "unknown") if not found. fn extract_descriptor_fingerprint_and_path(descriptor: &str) -> (String, String) { if let Some(bracket_start) = descriptor.find('[') { if let Some(bracket_end) = descriptor[bracket_start..].find(']') { @@ -241,67 +235,98 @@ fn extract_descriptor_fingerprint_and_path(descriptor: &str) -> (String, String) ("unknown".to_string(), "unknown".to_string()) } -/// This function attempts to identify descriptor mismatches from BDK error messages -/// and provides detailed information about which keychain failed and what descriptors -/// were expected vs stored. Only checksums of descriptors are shown for security. -fn parse_descriptor_mismatch_error( +/// Attempts to extract structured information from the error chain. +/// +/// Walks the error source chain looking for: +/// 1. Exact descriptor strings in error messages (most reliable) +/// 2. Enum variant names in Debug format (e.g., "KeychainKind::External") +/// +/// Returns a tuple of (keychain, descriptor_string) if any matching evidence is found, or None otherwise. +fn extract_structured_error_info( error: &dyn std::error::Error, external_descriptor_str: &str, internal_descriptor_str: &str, -) -> WalletError { - let error_msg = error.to_string(); - let error_debug = format!("{:?}", error); - let error_lower = error_msg.to_lowercase(); +) -> Option<(&'static str, String)> { + let mut current: Option<&dyn std::error::Error> = Some(error); + + // Walk the error chain + while let Some(err) = current { + let error_debug = format!("{:?}", err); + let error_msg = err.to_string(); + + // Check for exact descriptor strings (most reliable indicator) + // This works even if BDK's error format changes + if error_msg.contains(external_descriptor_str) + || error_debug.contains(external_descriptor_str) + { + return Some(("external", external_descriptor_str.to_string())); + } + + if error_msg.contains(internal_descriptor_str) + || error_debug.contains(internal_descriptor_str) + { + return Some(("internal", internal_descriptor_str.to_string())); + } - // Try to determine which keychain failed by checking error message - // Look for explicit mentions of keychain types + // Try to extract keychain from Debug format enum variants + if error_debug.contains("KeychainKind::External") { + return Some(("external", external_descriptor_str.to_string())); + } + + if error_debug.contains("KeychainKind::Internal") { + return Some(("internal", internal_descriptor_str.to_string())); + } + + // Move to next error in chain + current = err.source(); + } + + None +} + +/// Returns true if the error looks like a descriptor mismatch (heuristics-based). +fn is_descriptor_mismatch( + error: &dyn std::error::Error, + external_descriptor_str: &str, + internal_descriptor_str: &str, +) -> bool { + extract_structured_error_info(error, external_descriptor_str, internal_descriptor_str).is_some() +} + +/// Identifies descriptor mismatches in BDK errors and extracts info on which keychain failed. +fn extract_descriptor_info( + error: &dyn std::error::Error, + external_descriptor_str: &str, + internal_descriptor_str: &str, +) -> WalletError { + // Extract structured information from error chain let (keychain, expected_descriptor) = - if error_lower.contains("external") || error_debug.contains("External") { - ("external", external_descriptor_str.to_string()) - } else if error_lower.contains("internal") || error_debug.contains("Internal") { - ("internal", internal_descriptor_str.to_string()) - } else { - // If we can't determine from the error message, check if either descriptor appears - // in the error (indicating which one was expected) - if error_msg.contains(external_descriptor_str) - || error_debug.contains(external_descriptor_str) - { - ("external", external_descriptor_str.to_string()) - } else if error_msg.contains(internal_descriptor_str) - || error_debug.contains(internal_descriptor_str) - { - ("internal", internal_descriptor_str.to_string()) - } else { - // Default to external if we can't determine - ("external", external_descriptor_str.to_string()) - } - }; + extract_structured_error_info(error, external_descriptor_str, internal_descriptor_str) + .unwrap_or(("external", external_descriptor_str.to_string())); - // Extract checksums, fingerprints, and derivation paths directly from descriptors + // Extract checksums and derivation paths directly from descriptors let expected_checksum = extract_descriptor_checksum(&expected_descriptor); - let (expected_fingerprint, expected_path) = + let (_expected_fingerprint, expected_path) = extract_descriptor_fingerprint_and_path(&expected_descriptor); - // Extract stored checksum, fingerprint, and path directly from error message - // BDK error format: "loaded wpkh([fingerprint/path]...)#checksum, expected ..." - let (stored_checksum, stored_fingerprint, stored_path) = - extract_stored_info_from_error(&error_msg, &error_debug); - - // Format descriptor information with fingerprint below the path - let format_descriptor_info = |checksum: &str, path: &str, fingerprint: &str| { + let format_descriptor_info = |checksum: &str, path: &str| { if path != "unknown" { - format!( - " Checksum: {}\n DerivationPath: {}\n Fingerprint: {}", - checksum, path, fingerprint - ) + format!(" Checksum: {}\n DerivationPath: {}", checksum, path) } else { format!(" Checksum: {}", checksum) } }; - let expected = - format_descriptor_info(&expected_checksum, &expected_path, &expected_fingerprint); - let stored = format_descriptor_info(&stored_checksum, &stored_path, &stored_fingerprint); + let expected = format_descriptor_info(&expected_checksum, &expected_path); + + // Extract stored descriptor info from error message + // Note: This requires parsing the error message string, but it's necessary + // to meet the requirement of showing expected vs stored descriptor for comparison + let error_msg = error.to_string(); + let error_debug = format!("{:?}", error); + let (stored_checksum, _stored_fingerprint, stored_path) = + extract_stored_descriptor_info(&error_msg, &error_debug); + let stored = format_descriptor_info(&stored_checksum, &stored_path); WalletError::DescriptorMismatch { keychain: keychain.to_string(), @@ -310,12 +335,9 @@ fn parse_descriptor_mismatch_error( } } -/// Extracts checksum, fingerprint, and derivation path from BDK error messages using descriptor parsing. -/// -/// BDK error format: "loaded wpkh([fingerprint/path]...)#checksum, expected ..." -/// This function extracts the full descriptor string, parses it using BDK's parser, -/// then extracts checksum, fingerprint, and path from the parsed descriptor's string representation. -fn extract_stored_info_from_error(error_msg: &str, error_debug: &str) -> (String, String, String) { +/// Extracts checksum, fingerprint, and derivation path from the stored descriptor +/// in BDK error messages. +fn extract_stored_descriptor_info(error_msg: &str, error_debug: &str) -> (String, String, String) { // Try both error message formats for text in [error_msg, error_debug] { // Look for "loaded " keyword which precedes the stored descriptor @@ -403,26 +425,13 @@ impl DlcDevKitWallet { .load_wallet_async(&mut storage) .await .map_err(|e| { - // Check if this is a descriptor mismatch error - let error_msg = e.to_string(); - let error_lower = error_msg.to_lowercase(); - - // Convert descriptors to strings for the helper function let external_desc_str = external_descriptor.0.to_string(); let internal_desc_str = internal_descriptor.0.to_string(); - // Common indicators of descriptor mismatch errors - if error_lower.contains("descriptor") - && (error_lower.contains("mismatch") - || error_lower.contains("does not match") - || error_lower.contains("expected") - || error_lower.contains("stored") - || error_lower.contains("found")) - { - parse_descriptor_mismatch_error(&e, &external_desc_str, &internal_desc_str) + if is_descriptor_mismatch(&e, &external_desc_str, &internal_desc_str) { + extract_descriptor_info(&e, &external_desc_str, &internal_desc_str) } else { - // For other errors, use the generic error - WalletError::WalletPersistanceError(error_msg) + WalletError::WalletPersistanceError(e.to_string()) } })?; diff --git a/ddk/tests/wallet.rs b/ddk/tests/wallet.rs index f97687c..8b75c39 100644 --- a/ddk/tests/wallet.rs +++ b/ddk/tests/wallet.rs @@ -13,7 +13,7 @@ use std::sync::Arc; /// /// This function creates a wallet with one seed, persists it, then tries to load it /// with a different seed. It verifies that the error message correctly shows checksums -/// instead of full descriptors. +/// and derivation paths for both expected and stored descriptors for comparison. async fn test_descriptor_mismatch_error_with_storage(storage: Arc) { dotenv::dotenv().ok(); @@ -30,7 +30,6 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) .expect("Failed to create Esplora client"), ); - // First seed - this will create and persist a wallet let mut seed1 = [0u8; 64]; seed1 .try_fill(&mut bitcoin::key::rand::thread_rng()) @@ -47,7 +46,6 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) .await .expect("Failed to create first wallet"); - // Second seed - this will try to load the existing wallet but with different descriptors let mut seed2 = [0u8; 64]; seed2 .try_fill(&mut bitcoin::key::rand::thread_rng()) @@ -57,13 +55,12 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) &seed2, esplora.clone(), Network::Regtest, - storage.clone(), // Same storage, but different seed + storage.clone(), None, logger.clone(), ) .await; - // Verify we got the expected error match result { Ok(_) => panic!("Expected DescriptorMismatch error but wallet loaded successfully"), Err(WalletError::DescriptorMismatch { @@ -71,40 +68,43 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) expected, stored, }) => { + let error_msg = format!( + "{}", + WalletError::DescriptorMismatch { + keychain: keychain.clone(), + expected: expected.clone(), + stored: stored.clone(), + } + ); println!("\n{}", "=".repeat(80)); - println!("SUCCESS: Descriptor mismatch error detected"); - println!("{}", "=".repeat(80)); - println!("Keychain: {}", keychain); - println!("Expected descriptor: {}", expected); - println!("Stored descriptor: {}", stored); + println!("{}", error_msg); println!("{}", "=".repeat(80)); - // Verify the error contains meaningful information assert_eq!(keychain, "external", "Should identify external keychain"); assert!( !expected.is_empty(), "Expected descriptor should not be empty" ); assert!(!stored.is_empty(), "Stored descriptor should not be empty"); - // Verify the format includes checksum and derivation path + assert!( - expected.contains("DerivationPath:") || expected == "unknown", - "Expected descriptor should include DerivationPath, got: '{}'", + expected.contains("DerivationPath:") || expected.contains("Checksum:"), + "Expected descriptor should include DerivationPath or Checksum, got: '{}'", expected ); - assert!( - stored.contains("DerivationPath:") - || stored == "unknown" - || stored.starts_with("Could not extract"), - "Stored descriptor should include DerivationPath, got: '{}'", - stored - ); - // Verify checksum is present + assert!( expected.contains("Checksum:"), "Expected descriptor should include Checksum, got: '{}'", expected ); + + assert!( + stored.contains("DerivationPath:") || stored.contains("Checksum:"), + "Stored descriptor should include DerivationPath or Checksum, got: '{}'", + stored + ); + assert!( stored.contains("Checksum:"), "Stored descriptor should include Checksum, got: '{}'", @@ -124,9 +124,6 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) } /// Test descriptor mismatch error with MemoryStorage backend. -/// -/// This test verifies that the descriptor mismatch error message works correctly -/// with the in-memory storage backend. #[tokio::test] async fn descriptor_mismatch_error_memory() { dotenv::dotenv().ok(); @@ -135,9 +132,6 @@ async fn descriptor_mismatch_error_memory() { } /// Test descriptor mismatch error with SledStorage backend. -/// -/// This test verifies that the descriptor mismatch error message works correctly -/// with the Sled embedded database storage backend. #[cfg(feature = "sled")] #[tokio::test] async fn descriptor_mismatch_error_sled() { From f2f13e703a004fd85473087677011a8050174bfd Mon Sep 17 00:00:00 2001 From: HalFinneyIsMyHomeBoy Date: Thu, 15 Jan 2026 20:58:52 +0000 Subject: [PATCH 12/14] cleanup --- ddk/src/wallet/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/ddk/src/wallet/mod.rs b/ddk/src/wallet/mod.rs index a391bd0..b55c58a 100644 --- a/ddk/src/wallet/mod.rs +++ b/ddk/src/wallet/mod.rs @@ -304,7 +304,6 @@ fn extract_descriptor_info( extract_structured_error_info(error, external_descriptor_str, internal_descriptor_str) .unwrap_or(("external", external_descriptor_str.to_string())); - // Extract checksums and derivation paths directly from descriptors let expected_checksum = extract_descriptor_checksum(&expected_descriptor); let (_expected_fingerprint, expected_path) = extract_descriptor_fingerprint_and_path(&expected_descriptor); @@ -340,7 +339,6 @@ fn extract_descriptor_info( fn extract_stored_descriptor_info(error_msg: &str, error_debug: &str) -> (String, String, String) { // Try both error message formats for text in [error_msg, error_debug] { - // Look for "loaded " keyword which precedes the stored descriptor if let Some(loaded_pos) = text.find("loaded ") { let after_loaded = &text[loaded_pos + 7..]; // Skip "loaded " @@ -353,7 +351,6 @@ fn extract_stored_descriptor_info(error_msg: &str, error_debug: &str) -> (String // Get the canonical string representation (includes checksum) let canonical_str = descriptor.to_string(); - // Extract checksum, fingerprint, and path from the canonical representation let checksum = extract_descriptor_checksum(&canonical_str); let (fingerprint, path) = extract_descriptor_fingerprint_and_path(&canonical_str); From 6c92dc5314a56f0f6428f0441aef276cd0d59aa2 Mon Sep 17 00:00:00 2001 From: HalFinneyIsMyHomeBoy Date: Thu, 15 Jan 2026 21:28:34 +0000 Subject: [PATCH 13/14] cleanup --- ddk/src/wallet/mod.rs | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/ddk/src/wallet/mod.rs b/ddk/src/wallet/mod.rs index b55c58a..1e21a3d 100644 --- a/ddk/src/wallet/mod.rs +++ b/ddk/src/wallet/mod.rs @@ -304,28 +304,20 @@ fn extract_descriptor_info( extract_structured_error_info(error, external_descriptor_str, internal_descriptor_str) .unwrap_or(("external", external_descriptor_str.to_string())); - let expected_checksum = extract_descriptor_checksum(&expected_descriptor); - let (_expected_fingerprint, expected_path) = - extract_descriptor_fingerprint_and_path(&expected_descriptor); - - let format_descriptor_info = |checksum: &str, path: &str| { - if path != "unknown" { - format!(" Checksum: {}\n DerivationPath: {}", checksum, path) - } else { - format!(" Checksum: {}", checksum) - } - }; - - let expected = format_descriptor_info(&expected_checksum, &expected_path); + // Format expected descriptor info + let expected = format!( + " Checksum: {}", + extract_descriptor_checksum(&expected_descriptor) + ); // Extract stored descriptor info from error message // Note: This requires parsing the error message string, but it's necessary // to meet the requirement of showing expected vs stored descriptor for comparison let error_msg = error.to_string(); let error_debug = format!("{:?}", error); - let (stored_checksum, _stored_fingerprint, stored_path) = + let (stored_checksum, _stored_fingerprint, _stored_path) = extract_stored_descriptor_info(&error_msg, &error_debug); - let stored = format_descriptor_info(&stored_checksum, &stored_path); + let stored = format!(" Checksum: {}", stored_checksum); WalletError::DescriptorMismatch { keychain: keychain.to_string(), From 64eb8c4d195fabe87a2193ef5ed140469e206544 Mon Sep 17 00:00:00 2001 From: HalFinneyIsMyHomeBoy Date: Sun, 18 Jan 2026 00:30:41 +0000 Subject: [PATCH 14/14] edgecase error handling --- ddk/src/error.rs | 2 +- ddk/src/wallet/mod.rs | 18 ++++++++++++++---- ddk/tests/wallet.rs | 6 +++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/ddk/src/error.rs b/ddk/src/error.rs index faf5b31..845d3de 100644 --- a/ddk/src/error.rs +++ b/ddk/src/error.rs @@ -209,7 +209,7 @@ pub enum WalletError { InvalidSecretKey, #[error( "DESCRIPTOR MISMATCH DETECTED\n\n\ - {keychain} descriptor mismatch detected.\n\n\ + {keychain}.\n\n\ Expected descriptor:\n{expected}\n\n\ Stored descriptor:\n{stored}\n\n\ The wallet's stored descriptor doesn't match the descriptor\n\ diff --git a/ddk/src/wallet/mod.rs b/ddk/src/wallet/mod.rs index 1e21a3d..0134937 100644 --- a/ddk/src/wallet/mod.rs +++ b/ddk/src/wallet/mod.rs @@ -300,9 +300,12 @@ fn extract_descriptor_info( internal_descriptor_str: &str, ) -> WalletError { // Extract structured information from error chain - let (keychain, expected_descriptor) = - extract_structured_error_info(error, external_descriptor_str, internal_descriptor_str) - .unwrap_or(("external", external_descriptor_str.to_string())); + let (keychain, expected_descriptor) = extract_structured_error_info( + error, + external_descriptor_str, + internal_descriptor_str, + ) + .unwrap_or(("unknown", external_descriptor_str.to_string())); // Format expected descriptor info let expected = format!( @@ -319,8 +322,15 @@ fn extract_descriptor_info( extract_stored_descriptor_info(&error_msg, &error_debug); let stored = format!(" Checksum: {}", stored_checksum); + // Format keychain message - indicate uncertainty if we couldn't determine which keychain + let keychain_msg = if keychain == "unknown" { + "A descriptor mismatch was detected, but the specific keychain (external/internal) could not be determined".to_string() + } else { + format!("{keychain} descriptor mismatch detected") + }; + WalletError::DescriptorMismatch { - keychain: keychain.to_string(), + keychain: keychain_msg, expected, stored, } diff --git a/ddk/tests/wallet.rs b/ddk/tests/wallet.rs index 8b75c39..7cb2083 100644 --- a/ddk/tests/wallet.rs +++ b/ddk/tests/wallet.rs @@ -80,7 +80,11 @@ async fn test_descriptor_mismatch_error_with_storage(storage: Arc) println!("{}", error_msg); println!("{}", "=".repeat(80)); - assert_eq!(keychain, "external", "Should identify external keychain"); + assert!( + keychain.contains("external") && keychain.contains("descriptor mismatch detected"), + "Should identify external keychain, got: '{}'", + keychain + ); assert!( !expected.is_empty(), "Expected descriptor should not be empty"