From ef7cdf2d1840e644f7fc69ae77f306422f480f44 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:32:38 +0000 Subject: [PATCH 01/13] chore(deps): bump rust-dashcore to dev head 78d10022 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump all 8 rust-dashcore workspace dependencies (dashcore, dash-spv, key-wallet, key-wallet-ffi, key-wallet-manager, dash-network, dash-network-seeds, dashcore-rpc) from 5c0113e7 to 78d10022 (rust-dashcore `dev` head, 15 commits, 0.43.0 -> 0.45.0). key-wallet's `Signer` trait gained a required `extended_public_key` method. Implement it for MnemonicResolverCoreSigner (real BIP-32 xpub derivation with private-scalar zeroization on every return path) and add stubs to the two rs-dpp test signers. Resolver + derive boilerplate extracted into a shared `resolve_and_derive` helper (DRY); the master scalar is wiped on both the success and derive-error paths. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01MoY6vhmqZuHzNsMfJ8wakQ 🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent --- Cargo.lock | 70 +++++------ Cargo.toml | 16 +-- packages/rs-dpp/src/state_transition/mod.rs | 11 +- .../signing_tests.rs | 11 +- .../src/mnemonic_resolver_core_signer.rs | 115 ++++++++++++++---- 5 files changed, 151 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8de05bacd92..512dffc0d2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1206,7 +1206,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1636,8 +1636,8 @@ dependencies = [ [[package]] name = "dash-network" -version = "0.43.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5c0113e7901551450f6063023eec4be95beeb6b9#5c0113e7901551450f6063023eec4be95beeb6b9" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" dependencies = [ "bincode", "bincode_derive", @@ -1647,8 +1647,8 @@ dependencies = [ [[package]] name = "dash-network-seeds" -version = "0.43.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5c0113e7901551450f6063023eec4be95beeb6b9#5c0113e7901551450f6063023eec4be95beeb6b9" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" dependencies = [ "dash-network", ] @@ -1724,8 +1724,8 @@ dependencies = [ [[package]] name = "dash-spv" -version = "0.43.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5c0113e7901551450f6063023eec4be95beeb6b9#5c0113e7901551450f6063023eec4be95beeb6b9" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" dependencies = [ "async-trait", "chrono", @@ -1753,8 +1753,8 @@ dependencies = [ [[package]] name = "dashcore" -version = "0.43.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5c0113e7901551450f6063023eec4be95beeb6b9#5c0113e7901551450f6063023eec4be95beeb6b9" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" dependencies = [ "anyhow", "base64-compat", @@ -1779,13 +1779,13 @@ dependencies = [ [[package]] name = "dashcore-private" -version = "0.43.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5c0113e7901551450f6063023eec4be95beeb6b9#5c0113e7901551450f6063023eec4be95beeb6b9" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" [[package]] name = "dashcore-rpc" -version = "0.43.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5c0113e7901551450f6063023eec4be95beeb6b9#5c0113e7901551450f6063023eec4be95beeb6b9" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" dependencies = [ "dashcore-rpc-json", "hex", @@ -1797,8 +1797,8 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" -version = "0.43.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5c0113e7901551450f6063023eec4be95beeb6b9#5c0113e7901551450f6063023eec4be95beeb6b9" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" dependencies = [ "bincode", "dashcore", @@ -1812,8 +1812,8 @@ dependencies = [ [[package]] name = "dashcore_hashes" -version = "0.43.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5c0113e7901551450f6063023eec4be95beeb6b9#5c0113e7901551450f6063023eec4be95beeb6b9" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" dependencies = [ "bincode", "dashcore-private", @@ -2428,7 +2428,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2489,7 +2489,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2866,8 +2866,8 @@ dependencies = [ [[package]] name = "git-state" -version = "0.43.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5c0113e7901551450f6063023eec4be95beeb6b9#5c0113e7901551450f6063023eec4be95beeb6b9" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" [[package]] name = "glob" @@ -3803,7 +3803,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4033,8 +4033,8 @@ dependencies = [ [[package]] name = "key-wallet" -version = "0.43.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5c0113e7901551450f6063023eec4be95beeb6b9#5c0113e7901551450f6063023eec4be95beeb6b9" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" dependencies = [ "aes", "async-trait", @@ -4062,8 +4062,8 @@ dependencies = [ [[package]] name = "key-wallet-ffi" -version = "0.43.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5c0113e7901551450f6063023eec4be95beeb6b9#5c0113e7901551450f6063023eec4be95beeb6b9" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" dependencies = [ "cbindgen 0.29.4", "dash-network", @@ -4078,8 +4078,8 @@ dependencies = [ [[package]] name = "key-wallet-manager" -version = "0.43.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=5c0113e7901551450f6063023eec4be95beeb6b9#5c0113e7901551450f6063023eec4be95beeb6b9" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" dependencies = [ "async-trait", "bincode", @@ -4596,7 +4596,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5696,7 +5696,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6486,7 +6486,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6499,7 +6499,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6558,7 +6558,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7418,7 +7418,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -8867,7 +8867,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5d5f2960f1d..c36a7a9c874 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,14 +50,14 @@ members = [ ] [workspace.dependencies] -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "5c0113e7901551450f6063023eec4be95beeb6b9" } -dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "5c0113e7901551450f6063023eec4be95beeb6b9" } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "5c0113e7901551450f6063023eec4be95beeb6b9" } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "5c0113e7901551450f6063023eec4be95beeb6b9" } -key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "5c0113e7901551450f6063023eec4be95beeb6b9" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "5c0113e7901551450f6063023eec4be95beeb6b9" } -dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "5c0113e7901551450f6063023eec4be95beeb6b9" } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "5c0113e7901551450f6063023eec4be95beeb6b9" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } +dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } +key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } +dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } tokio-metrics = "0.5" diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 92bf4a4e583..74ebc13c977 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -3335,7 +3335,7 @@ mod tests { use dashcore::secp256k1::{ ecdsa, rand::rngs::OsRng, Message, PublicKey, Secp256k1, SecretKey, }; - use key_wallet::bip32::DerivationPath; + use key_wallet::bip32::{DerivationPath, ExtendedPubKey}; use key_wallet::signer::{Signer as KwSigner, SignerMethod}; /// Fixed-key in-memory signer used only by this test. Mirrors how a @@ -3370,6 +3370,15 @@ mod tests { async fn public_key(&self, _path: &DerivationPath) -> Result { Ok(self.public) } + + async fn extended_public_key( + &self, + _path: &DerivationPath, + ) -> Result { + // Test stub holds a single raw key with no chain code; extended + // public key derivation is not meaningful here. + Err("FixedKeySigner: no chain code — extended_public_key not supported".to_string()) + } } // Generate a single random key. Using the same key on both sides is diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/signing_tests.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/signing_tests.rs index 67f95088409..b2c4fb67f83 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/signing_tests.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/signing_tests.rs @@ -204,7 +204,7 @@ async fn try_from_asset_lock_with_signer_and_private_key_signs_multiple_inputs() async fn try_from_asset_lock_with_signers_produces_matching_signature() { use async_trait::async_trait; use dashcore::secp256k1::{ecdsa, Message}; - use key_wallet::bip32::DerivationPath; + use key_wallet::bip32::{DerivationPath, ExtendedPubKey}; use key_wallet::signer::{Signer as KwSigner, SignerMethod}; /// Fixed-key in-memory `key_wallet::signer::Signer`. Mirrors how the @@ -237,6 +237,15 @@ async fn try_from_asset_lock_with_signers_produces_matching_signature() { async fn public_key(&self, _path: &DerivationPath) -> Result { Ok(self.public) } + + async fn extended_public_key( + &self, + _path: &DerivationPath, + ) -> Result { + // Test stub holds a single raw key with no chain code; extended + // public key derivation is not meaningful here. + Err("FixedKeySigner: no chain code — extended_public_key not supported".to_string()) + } } let secp = Secp256k1::new(); diff --git a/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs b/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs index ade11828594..bda1d3aa00f 100644 --- a/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs +++ b/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs @@ -64,7 +64,7 @@ use std::ffi::c_void; use std::os::raw::c_char; use async_trait::async_trait; -use key_wallet::bip32::{DerivationPath, ExtendedPrivKey}; +use key_wallet::bip32::{DerivationPath, ExtendedPrivKey, ExtendedPubKey}; use key_wallet::dashcore::secp256k1::{self, Secp256k1}; use key_wallet::signer::{Signer, SignerMethod}; use key_wallet::Network; @@ -222,18 +222,32 @@ impl MnemonicResolverCoreSigner { } } - /// Resolve the mnemonic from the Swift-side callback, then - /// derive the secp256k1 private key at `path`. Returns the raw - /// 32-byte scalar in a `Zeroizing` wrapper so the caller's last - /// drop point zeros it. + /// Resolve the mnemonic from the Swift-side callback, then derive the + /// BIP-32 extended private key at `path`. /// - /// All other intermediate buffers (mnemonic, seed) are dropped - /// (and zeroed) before this method returns — only the final - /// derived scalar leaks out, and even that is `Zeroizing`-wrapped. - fn derive_priv( + /// This is the single entry-point for all private-key material in this + /// signer. It handles the full stack: resolver FFI call → result-code + /// mapping → UTF-8 + word-list validation → BIP-39 seed → master + /// `ExtendedPrivKey` → child `ExtendedPrivKey` at `path`. + /// + /// # Zeroization contract + /// + /// - The `master` scalar is wiped inside this helper before returning. + /// - The *caller* is responsible for wiping `derived.private_key` after + /// extracting whatever it needs — `ExtendedPrivKey` carries no + /// `Drop`-based zeroization (see the `TODO(upstream)` below). + /// - All other intermediate buffers (mnemonic, seed) are `Zeroizing`- + /// wrapped and wiped on drop before this method returns. + /// + /// # Errors + /// + /// Propagates [`MnemonicResolverSignerError`] for every failure mode: + /// null handle, resolver FFI errors, encoding/parse failures, and BIP-32 + /// derivation errors. + fn resolve_and_derive( &self, path: &DerivationPath, - ) -> Result, MnemonicResolverSignerError> { + ) -> Result { if self.resolver_addr == 0 { return Err(MnemonicResolverSignerError::NullHandle); } @@ -293,27 +307,53 @@ impl MnemonicResolverCoreSigner { let secp = Secp256k1::new(); let mut master = ExtendedPrivKey::new_master(self.network, seed.as_ref()) .map_err(|e| MnemonicResolverSignerError::DerivationFailed(format!("master: {e}")))?; - let mut derived = master - .derive_priv(&secp, path) - .map_err(|e| MnemonicResolverSignerError::DerivationFailed(format!("path: {e}")))?; - - // `secret_bytes()` returns a plain `[u8; 32]`; wrap in - // `Zeroizing` so the caller (and any panic-unwind path) - // wipes it on drop. - let bytes = Zeroizing::new(derived.private_key.secret_bytes()); - // TODO(upstream): `key_wallet::bip32::ExtendedPrivKey` has no // `Drop` / `Zeroize` impl — the inner `secp256k1::SecretKey` - // scalars on `master` and `derived` would otherwise drop - // un-wiped. Mirrors the SecretKey-copy hole CodeRabbit R7 - // flagged at the sign-site. Proper fix is a `Zeroize` / - // `ZeroizeOnDrop` impl in `dashpay/rust-dashcore`'s - // `key-wallet/src/bip32.rs`; until that lands, wipe the two - // SecretKey fields explicitly here. Mirrored in the sibling - // FFI at `rs-platform-wallet-ffi/src/sign_with_mnemonic_resolver.rs`. + // scalar on `master` would otherwise drop un-wiped. The `derived` + // scalar is the caller's responsibility (see zeroization contract + // above). Proper fix is a `Zeroize` / `ZeroizeOnDrop` impl in + // `dashpay/rust-dashcore`'s `key-wallet/src/bip32.rs`; until + // that lands, wipe the master scalar explicitly on BOTH paths: + // the error arm below (early return) and the success path after + // the match. Mirrored in the sibling FFI at + // `rs-platform-wallet-ffi/src/sign_with_mnemonic_resolver.rs`. + let derived = match master.derive_priv(&secp, path) { + Ok(d) => d, + Err(e) => { + // Wipe master before returning — the success path below + // never runs, so line 323's erase would be skipped without + // this explicit call. + master.private_key.non_secure_erase(); + return Err(MnemonicResolverSignerError::DerivationFailed(format!( + "path: {e}" + ))); + } + }; + + // Success path: master scalar wiped here; derived is the caller's + // responsibility (documented in the zeroization contract above). master.private_key.non_secure_erase(); - derived.private_key.non_secure_erase(); + Ok(derived) + } + + /// Resolve the mnemonic and derive the raw 32-byte scalar at `path`. + /// + /// Returns the scalar in a `Zeroizing` wrapper so the caller's last + /// drop point wipes it. All other intermediate key material is zeroed + /// before this method returns (see [`Self::resolve_and_derive`]). + fn derive_priv( + &self, + path: &DerivationPath, + ) -> Result, MnemonicResolverSignerError> { + let mut derived = self.resolve_and_derive(path)?; + // `secret_bytes()` copies the 32-byte scalar; wrap in `Zeroizing` + // so the caller's drop point wipes it. + let bytes = Zeroizing::new(derived.private_key.secret_bytes()); + // Wipe the derived extended key's scalar — the extracted `bytes` + // carry it from here. Mirrors the SecretKey-copy hole noted in + // `resolve_and_derive`'s TODO(upstream). + derived.private_key.non_secure_erase(); Ok(bytes) } } @@ -362,6 +402,27 @@ impl Signer for MnemonicResolverCoreSigner { secret.non_secure_erase(); Ok(pubkey) } + + /// Derive the BIP-32 extended public key at `path`. + /// + /// Returns the full [`ExtendedPubKey`] (public point + chain code) so + /// callers can perform non-hardened child derivation locally without + /// additional round-trips to the resolver. All intermediate private-key + /// material is zeroized before this method returns (see + /// [`Self::resolve_and_derive`]); `ExtendedPubKey` carries only public + /// information and requires no further wiping. + async fn extended_public_key( + &self, + path: &DerivationPath, + ) -> Result { + let mut derived = self.resolve_and_derive(path)?; + let secp = Secp256k1::new(); + // Capture the extended public key *before* wiping the private scalar. + // `ExtendedPubKey` carries only public material (chain code + point). + let xpub = ExtendedPubKey::from_priv(&secp, &derived); + derived.private_key.non_secure_erase(); + Ok(xpub) + } } #[cfg(test)] From 27d235fe437a99331c2b9b2812d5e5efbbfb18ec Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:24:24 +0000 Subject: [PATCH 02/13] fix(dpp): implement extended_public_key on shielded FixedKeySigner test stub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rust-dashcore bump added `Signer::extended_public_key` as a required trait method. The shielded `shield_from_asset_lock_transition` signing test's `FixedKeySigner` stub was missed: it is feature-gated behind `all(test, state-transition-signing, core_key_wallet, shielded-client)` and is not compiled under default features, so the local workspace check passed while CI (which enables those features) failed with E0046. Add the stub matching the two sibling test signers (returns Err — the fixed-key stub carries no chain code). Verified with `cargo check -p dpp --tests --features state-transition-signing,core_key_wallet,shielded-client`. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01MoY6vhmqZuHzNsMfJ8wakQ 🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent --- .../signing_tests.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs index c31e3b1fc1e..5ea8e633b3a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs @@ -33,7 +33,7 @@ use platform_version::version::PlatformVersion; use async_trait::async_trait; use dashcore::secp256k1::{ecdsa, Message, PublicKey, Secp256k1, SecretKey}; -use key_wallet::bip32::DerivationPath; +use key_wallet::bip32::{DerivationPath, ExtendedPubKey}; use key_wallet::signer::{Signer as KwSigner, SignerMethod}; /// Fixed-key in-memory `key_wallet::signer::Signer`. Mirrors how a @@ -77,6 +77,15 @@ impl KwSigner for FixedKeySigner { async fn public_key(&self, _path: &DerivationPath) -> Result { Ok(self.public) } + + async fn extended_public_key( + &self, + _path: &DerivationPath, + ) -> Result { + // Test stub holds a single raw key with no chain code; extended + // public key derivation is not meaningful here. + Err("FixedKeySigner: no chain code — extended_public_key not supported".to_string()) + } } fn make_chain_asset_lock_proof() -> AssetLockProof { From 36e2fed2289c19195a8ccc517f433321ea12be10 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:15:26 +0000 Subject: [PATCH 03/13] chore(deps): bump rust-dashcore to PR #833 (Zeroize for ExtendedPrivKey) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the 8 rust-dashcore workspace deps (dashcore, dash-spv, key-wallet, key-wallet-ffi, key-wallet-manager, dash-network, dash-network-seeds, dashcore-rpc) from rev 78d10022 to f42498e0 — the head of rust-dashcore PR #833, which adds `impl zeroize::Zeroize for ExtendedPrivKey` in key-wallet/src/bip32.rs. Package versions stay at 0.45.0; Cargo.lock change is rev-only. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 16 ++++++++-------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 512dffc0d2d..1a0a3f2d44c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1637,7 +1637,7 @@ dependencies = [ [[package]] name = "dash-network" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" +source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" dependencies = [ "bincode", "bincode_derive", @@ -1648,7 +1648,7 @@ dependencies = [ [[package]] name = "dash-network-seeds" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" +source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" dependencies = [ "dash-network", ] @@ -1725,7 +1725,7 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" +source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" dependencies = [ "async-trait", "chrono", @@ -1754,7 +1754,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" +source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" dependencies = [ "anyhow", "base64-compat", @@ -1780,12 +1780,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" +source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" [[package]] name = "dashcore-rpc" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" +source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" dependencies = [ "dashcore-rpc-json", "hex", @@ -1798,7 +1798,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" +source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" dependencies = [ "bincode", "dashcore", @@ -1813,7 +1813,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" +source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" dependencies = [ "bincode", "dashcore-private", @@ -2867,7 +2867,7 @@ dependencies = [ [[package]] name = "git-state" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" +source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" [[package]] name = "glob" @@ -4034,7 +4034,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" +source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" dependencies = [ "aes", "async-trait", @@ -4063,7 +4063,7 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" +source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" dependencies = [ "cbindgen 0.29.4", "dash-network", @@ -4079,7 +4079,7 @@ dependencies = [ [[package]] name = "key-wallet-manager" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=78d10022c92384affe9d71be3266ac7bb4710291#78d10022c92384affe9d71be3266ac7bb4710291" +source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" dependencies = [ "async-trait", "bincode", diff --git a/Cargo.toml b/Cargo.toml index c36a7a9c874..3561c08beb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,14 +50,14 @@ members = [ ] [workspace.dependencies] -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } -dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } -key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } -dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "78d10022c92384affe9d71be3266ac7bb4710291" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } +dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } +key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } +dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } tokio-metrics = "0.5" From 3be6885df8ed67a73e24ae8da7aeb63fb6f18b8c Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:15:37 +0000 Subject: [PATCH 04/13] refactor(rs-sdk-ffi): wrap ExtendedPrivKey in Zeroizing, drop manual erase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that key-wallet's ExtendedPrivKey implements Zeroize (rust-dashcore PR #833), hold the master and derived extended keys in Zeroizing<_> and let RAII wipe them on every exit path — success, ?-early-return, and panic-unwind. This removes the 4 manual `.private_key.non_secure_erase()` calls in resolve_and_derive / derive_priv / extended_public_key that only covered the paths they were hand-placed on. resolve_and_derive now returns Zeroizing; its two callers are updated. The two `secret.non_secure_erase()` calls in sign_ecdsa / public_key stay: secp256k1::SecretKey has no Zeroize impl, so those raw SecretKey copies still need an explicit wipe. Module and per-fn zeroization docs rewritten to describe the current Zeroizing approach. Co-Authored-By: Claude Opus 4.8 --- .../src/mnemonic_resolver_core_signer.rs | 104 +++++++----------- 1 file changed, 40 insertions(+), 64 deletions(-) diff --git a/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs b/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs index bda1d3aa00f..6c4f73ab924 100644 --- a/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs +++ b/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs @@ -42,20 +42,20 @@ //! method returns. Two mechanisms cover the different ownership //! shapes: //! -//! - **`Zeroizing` wrappers** scrub on `Drop` for the byte-buffer -//! intermediates: the resolver mnemonic buffer, the BIP-39 seed, -//! and the final derived 32-byte scalar. -//! - **Explicit `non_secure_erase` calls** scrub the -//! [`secp256k1::SecretKey`] scalars inside the two intermediate +//! - **`Zeroizing` wrappers** scrub on `Drop`. This covers the +//! byte-buffer intermediates (resolver mnemonic buffer, BIP-39 seed, +//! final derived 32-byte scalar) and the two intermediate //! [`ExtendedPrivKey`] values (master + derived). `ExtendedPrivKey` -//! has no `Drop` / `Zeroize` impl in `key-wallet`, so falling out -//! of scope alone would leave those scalars resident; the explicit -//! wipe at the bottom of `derive_priv` closes the gap. Same -//! defense is applied at the sign-site for the `SecretKey` copy -//! `from_slice` creates. A proper fix is a `Zeroize` / -//! `ZeroizeOnDrop` impl in `dashpay/rust-dashcore`'s -//! `key-wallet/src/bip32.rs`; until that ships, the local wipes -//! keep the no-residue invariant true. +//! is `Copy` with no `Drop`, so the wipe fires through its +//! `Zeroizing` wrapper on every exit path — success, `?`-early-return, +//! and panic-unwind. `ExtendedPrivKey: Zeroize` comes from +//! rust-dashcore PR #833 (rev +//! `f42498e0d04257e28b4e457c16629904a872ab61`). +//! - **Explicit `non_secure_erase` calls** scrub the raw +//! [`secp256k1::SecretKey`] copies at the two sign sites, where the +//! scalar comes back out of `SecretKey::from_slice`. `SecretKey` has +//! no `Zeroize` impl (only `non_secure_erase()`), so it can't ride a +//! `Zeroizing` wrapper. //! //! Combined, no private key bytes survive past the trait-method //! boundary. @@ -232,12 +232,12 @@ impl MnemonicResolverCoreSigner { /// /// # Zeroization contract /// - /// - The `master` scalar is wiped inside this helper before returning. - /// - The *caller* is responsible for wiping `derived.private_key` after - /// extracting whatever it needs — `ExtendedPrivKey` carries no - /// `Drop`-based zeroization (see the `TODO(upstream)` below). - /// - All other intermediate buffers (mnemonic, seed) are `Zeroizing`- - /// wrapped and wiped on drop before this method returns. + /// Both the `master` and returned `derived` extended keys are held in + /// [`Zeroizing`], so every `ExtendedPrivKey` scalar is wiped on drop: + /// `master` when this helper returns, `derived` when the caller's + /// binding drops. The mnemonic and seed buffers are likewise + /// `Zeroizing`-wrapped. `ExtendedPrivKey: Zeroize` comes from + /// rust-dashcore PR #833 (rev `f42498e0d04257e28b4e457c16629904a872ab61`). /// /// # Errors /// @@ -247,7 +247,7 @@ impl MnemonicResolverCoreSigner { fn resolve_and_derive( &self, path: &DerivationPath, - ) -> Result { + ) -> Result, MnemonicResolverSignerError> { if self.resolver_addr == 0 { return Err(MnemonicResolverSignerError::NullHandle); } @@ -305,56 +305,33 @@ impl MnemonicResolverCoreSigner { drop(mnemonic); let secp = Secp256k1::new(); - let mut master = ExtendedPrivKey::new_master(self.network, seed.as_ref()) - .map_err(|e| MnemonicResolverSignerError::DerivationFailed(format!("master: {e}")))?; - // TODO(upstream): `key_wallet::bip32::ExtendedPrivKey` has no - // `Drop` / `Zeroize` impl — the inner `secp256k1::SecretKey` - // scalar on `master` would otherwise drop un-wiped. The `derived` - // scalar is the caller's responsibility (see zeroization contract - // above). Proper fix is a `Zeroize` / `ZeroizeOnDrop` impl in - // `dashpay/rust-dashcore`'s `key-wallet/src/bip32.rs`; until - // that lands, wipe the master scalar explicitly on BOTH paths: - // the error arm below (early return) and the success path after - // the match. Mirrored in the sibling FFI at - // `rs-platform-wallet-ffi/src/sign_with_mnemonic_resolver.rs`. - let derived = match master.derive_priv(&secp, path) { - Ok(d) => d, - Err(e) => { - // Wipe master before returning — the success path below - // never runs, so line 323's erase would be skipped without - // this explicit call. - master.private_key.non_secure_erase(); - return Err(MnemonicResolverSignerError::DerivationFailed(format!( - "path: {e}" - ))); - } - }; - - // Success path: master scalar wiped here; derived is the caller's - // responsibility (documented in the zeroization contract above). - master.private_key.non_secure_erase(); + let master = Zeroizing::new( + ExtendedPrivKey::new_master(self.network, seed.as_ref()).map_err(|e| { + MnemonicResolverSignerError::DerivationFailed(format!("master: {e}")) + })?, + ); + let derived = master + .derive_priv(&secp, path) + .map_err(|e| MnemonicResolverSignerError::DerivationFailed(format!("path: {e}")))?; - Ok(derived) + Ok(Zeroizing::new(derived)) } /// Resolve the mnemonic and derive the raw 32-byte scalar at `path`. /// /// Returns the scalar in a `Zeroizing` wrapper so the caller's last - /// drop point wipes it. All other intermediate key material is zeroed - /// before this method returns (see [`Self::resolve_and_derive`]). + /// drop point wipes it. All other intermediate key material — the two + /// [`ExtendedPrivKey`] values, mnemonic, and seed — is `Zeroizing`- + /// wrapped and wiped before this method returns (see + /// [`Self::resolve_and_derive`]). fn derive_priv( &self, path: &DerivationPath, ) -> Result, MnemonicResolverSignerError> { - let mut derived = self.resolve_and_derive(path)?; - // `secret_bytes()` copies the 32-byte scalar; wrap in `Zeroizing` - // so the caller's drop point wipes it. - let bytes = Zeroizing::new(derived.private_key.secret_bytes()); - // Wipe the derived extended key's scalar — the extracted `bytes` - // carry it from here. Mirrors the SecretKey-copy hole noted in - // `resolve_and_derive`'s TODO(upstream). - derived.private_key.non_secure_erase(); - Ok(bytes) + let derived = self.resolve_and_derive(path)?; + // `secret_bytes()` copies the 32-byte scalar out of the `Zeroizing`- + // wrapped `derived`, which wipes on drop at end of scope. + Ok(Zeroizing::new(derived.private_key.secret_bytes())) } } @@ -415,12 +392,11 @@ impl Signer for MnemonicResolverCoreSigner { &self, path: &DerivationPath, ) -> Result { - let mut derived = self.resolve_and_derive(path)?; + let derived = self.resolve_and_derive(path)?; let secp = Secp256k1::new(); - // Capture the extended public key *before* wiping the private scalar. - // `ExtendedPubKey` carries only public material (chain code + point). + // `ExtendedPubKey` carries only public material (chain code + point); + // `derived` wipes its private scalar on drop at end of scope. let xpub = ExtendedPubKey::from_priv(&secp, &derived); - derived.private_key.non_secure_erase(); Ok(xpub) } } From b175d5083233f80a6d6e1be63f05857e5074aa44 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:40:03 +0000 Subject: [PATCH 05/13] refactor(rs-sdk-ffi): confine ExtendedPrivKey to resolve_and_derive via extract closure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolve_and_derive now takes `extract: impl FnOnce(&ExtendedPrivKey) -> T` and returns T, instead of handing back Zeroizing. The derived key is wrapped in Zeroizing the instant derive_priv returns and is only ever borrowed by the closure, so no raw or wrapped ExtendedPrivKey crosses the function boundary — closing the Copy stack-residue gap flagged in review. The one remaining by-value copy is derive_priv's own return slot, which is upstream's (key-wallet) API to own. derive_priv and extended_public_key become thin closure callers. Also add a #[tokio::test] driving extended_public_key against an independently derived ExtendedPubKey from the same BIP-39 vector / network / path, asserting full-struct equality plus explicit chain_code / depth / network spot-checks so metadata loss is caught, not just the public point (fixes codecov/patch gap). Co-Authored-By: Claude Opus 4.8 --- .../src/mnemonic_resolver_core_signer.rs | 91 +++++++++++++------ 1 file changed, 65 insertions(+), 26 deletions(-) diff --git a/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs b/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs index 6c4f73ab924..1078802c248 100644 --- a/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs +++ b/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs @@ -222,8 +222,9 @@ impl MnemonicResolverCoreSigner { } } - /// Resolve the mnemonic from the Swift-side callback, then derive the - /// BIP-32 extended private key at `path`. + /// Resolve the mnemonic from the Swift-side callback, derive the BIP-32 + /// extended private key at `path`, and hand it *by reference* to + /// `extract`, returning whatever `extract` produces. /// /// This is the single entry-point for all private-key material in this /// signer. It handles the full stack: resolver FFI call → result-code @@ -232,22 +233,25 @@ impl MnemonicResolverCoreSigner { /// /// # Zeroization contract /// - /// Both the `master` and returned `derived` extended keys are held in - /// [`Zeroizing`], so every `ExtendedPrivKey` scalar is wiped on drop: - /// `master` when this helper returns, `derived` when the caller's - /// binding drops. The mnemonic and seed buffers are likewise - /// `Zeroizing`-wrapped. `ExtendedPrivKey: Zeroize` comes from - /// rust-dashcore PR #833 (rev `f42498e0d04257e28b4e457c16629904a872ab61`). + /// Both the `master` and `derived` extended keys are held in [`Zeroizing`] + /// and wiped when this method returns. The `ExtendedPrivKey` never crosses + /// the call boundary — `extract` only borrows it — so no raw or wrapped + /// key can outlive the derivation. `extract` returns public material + /// (`ExtendedPubKey`) or a `Zeroizing` scalar copy; the caller wipes the + /// latter on its own drop. The mnemonic and seed buffers are likewise + /// `Zeroizing`-wrapped. `ExtendedPrivKey: Zeroize` comes from rust-dashcore + /// PR #833 (rev `f42498e0d04257e28b4e457c16629904a872ab61`). /// /// # Errors /// /// Propagates [`MnemonicResolverSignerError`] for every failure mode: /// null handle, resolver FFI errors, encoding/parse failures, and BIP-32 /// derivation errors. - fn resolve_and_derive( + fn resolve_and_derive( &self, path: &DerivationPath, - ) -> Result, MnemonicResolverSignerError> { + extract: impl FnOnce(&ExtendedPrivKey) -> T, + ) -> Result { if self.resolver_addr == 0 { return Err(MnemonicResolverSignerError::NullHandle); } @@ -310,28 +314,29 @@ impl MnemonicResolverCoreSigner { MnemonicResolverSignerError::DerivationFailed(format!("master: {e}")) })?, ); - let derived = master - .derive_priv(&secp, path) - .map_err(|e| MnemonicResolverSignerError::DerivationFailed(format!("path: {e}")))?; + let derived = + Zeroizing::new(master.derive_priv(&secp, path).map_err(|e| { + MnemonicResolverSignerError::DerivationFailed(format!("path: {e}")) + })?); - Ok(Zeroizing::new(derived)) + Ok(extract(&derived)) } /// Resolve the mnemonic and derive the raw 32-byte scalar at `path`. /// /// Returns the scalar in a `Zeroizing` wrapper so the caller's last - /// drop point wipes it. All other intermediate key material — the two - /// [`ExtendedPrivKey`] values, mnemonic, and seed — is `Zeroizing`- - /// wrapped and wiped before this method returns (see - /// [`Self::resolve_and_derive`]). + /// drop point wipes it. The intermediate `ExtendedPrivKey` values, + /// mnemonic, and seed are wiped inside [`Self::resolve_and_derive`] + /// before this returns. fn derive_priv( &self, path: &DerivationPath, ) -> Result, MnemonicResolverSignerError> { - let derived = self.resolve_and_derive(path)?; - // `secret_bytes()` copies the 32-byte scalar out of the `Zeroizing`- - // wrapped `derived`, which wipes on drop at end of scope. - Ok(Zeroizing::new(derived.private_key.secret_bytes())) + // `secret_bytes()` copies the scalar out of the borrowed key; the + // `ExtendedPrivKey` itself never leaves `resolve_and_derive`. + self.resolve_and_derive(path, |derived| { + Zeroizing::new(derived.private_key.secret_bytes()) + }) } } @@ -392,12 +397,10 @@ impl Signer for MnemonicResolverCoreSigner { &self, path: &DerivationPath, ) -> Result { - let derived = self.resolve_and_derive(path)?; let secp = Secp256k1::new(); // `ExtendedPubKey` carries only public material (chain code + point); - // `derived` wipes its private scalar on drop at end of scope. - let xpub = ExtendedPubKey::from_priv(&secp, &derived); - Ok(xpub) + // the borrowed private key never leaves `resolve_and_derive`. + self.resolve_and_derive(path, |derived| ExtendedPubKey::from_priv(&secp, derived)) } } @@ -493,6 +496,42 @@ mod tests { unsafe { dash_sdk_mnemonic_resolver_destroy(resolver) }; } + #[tokio::test] + async fn extended_public_key_matches_independent_derivation() { + let resolver = make_resolver(english_resolve); + let signer = + unsafe { MnemonicResolverCoreSigner::new(resolver, [0u8; 32], Network::Testnet) }; + + let path = test_path(); + let xpub = signer + .extended_public_key(&path) + .await + .expect("extended_public_key succeeds"); + + // Independently derive the expected xpub straight from the known + // BIP-39 vector — same network + path, no resolver in the loop. + let secp = Secp256k1::new(); + let mnemonic = parse_mnemonic_any_language(ENGLISH_PHRASE).expect("valid phrase"); + let master = ExtendedPrivKey::new_master(Network::Testnet, &mnemonic.to_seed("")) + .expect("master derivation"); + let derived = master.derive_priv(&secp, &path).expect("path derivation"); + let expected = ExtendedPubKey::from_priv(&secp, &derived); + + // Full-struct equality catches any field regression; the explicit + // metadata asserts keep the guarantee even if `ExtendedPubKey`'s + // `PartialEq` ever narrows, and document that chain code / depth / + // network are deliberately covered — not just the public point. + assert_eq!(xpub, expected, "xpub must match independent derivation"); + assert_eq!( + xpub.chain_code, expected.chain_code, + "chain code must match" + ); + assert_eq!(xpub.depth, expected.depth, "depth must match"); + assert_eq!(xpub.network, expected.network, "network must match"); + + unsafe { dash_sdk_mnemonic_resolver_destroy(resolver) }; + } + #[tokio::test] async fn missing_resolver_surfaces_not_found_error() { let resolver = make_resolver(missing_resolve); From 5a2b2b5019ba67a8156b3c9de0aafd258196dfc8 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:47:29 +0000 Subject: [PATCH 06/13] test(rs-sdk-ffi): make extended_public_key metadata asserts load-bearing; doc irreducible xpriv transient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorder the extended_public_key test so each field-level assert (public_key, chain_code, depth, network) runs before the full-struct assert_eq!, which otherwise short-circuits and leaves the per-field metadata checks unreachable on a regression (i.e. vacuous). Proven non-vacuous: temporarily corrupting only `depth` in the impl fails the test at "depth must match", where a pubkey-only check would have passed. Also document the one irreducible transient in resolve_and_derive: key-wallet's ExtendedPrivKey::derive_priv returns by value (Copy), so its return slot holds an un-wiped copy until the same-line Zeroizing::new takes ownership — noted so the zeroization contract doesn't over-promise "zero copies ever". Co-Authored-By: Claude Opus 4.8 --- .../src/mnemonic_resolver_core_signer.rs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs b/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs index 1078802c248..80bc8b3b891 100644 --- a/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs +++ b/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs @@ -242,6 +242,13 @@ impl MnemonicResolverCoreSigner { /// `Zeroizing`-wrapped. `ExtendedPrivKey: Zeroize` comes from rust-dashcore /// PR #833 (rev `f42498e0d04257e28b4e457c16629904a872ab61`). /// + /// One transient is irreducible: `ExtendedPrivKey::derive_priv` returns the + /// child key *by value* (`ExtendedPrivKey` is `Copy`), so its return slot + /// briefly holds an un-wiped copy until the same-line `Zeroizing::new` + /// takes ownership. Closing that would require an upstream signature change + /// in key-wallet; it is not "zero copies ever", just the tightest this + /// layer can guarantee. + /// /// # Errors /// /// Propagates [`MnemonicResolverSignerError`] for every failure mode: @@ -517,17 +524,26 @@ mod tests { let derived = master.derive_priv(&secp, &path).expect("path derivation"); let expected = ExtendedPubKey::from_priv(&secp, &derived); - // Full-struct equality catches any field regression; the explicit - // metadata asserts keep the guarantee even if `ExtendedPubKey`'s - // `PartialEq` ever narrows, and document that chain code / depth / - // network are deliberately covered — not just the public point. - assert_eq!(xpub, expected, "xpub must match independent derivation"); + // Field-level checks run first so a silently-dropped BIP-32 metadatum + // fails here with a precise message — not just the public point. The + // final full-struct assert then catches the remaining fields + // (parent_fingerprint, child_number). Ordering matters: a leading + // full-struct `assert_eq!` would short-circuit and make these + // per-field asserts unreachable (i.e. vacuous) on a metadata regression. + assert_eq!( + xpub.public_key, expected.public_key, + "public key must match" + ); assert_eq!( xpub.chain_code, expected.chain_code, "chain code must match" ); assert_eq!(xpub.depth, expected.depth, "depth must match"); assert_eq!(xpub.network, expected.network, "network must match"); + assert_eq!( + xpub, expected, + "full xpub must match independent derivation" + ); unsafe { dash_sdk_mnemonic_resolver_destroy(resolver) }; } From 56612fd529d40268d4f290232f62bab312c2d950 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:48:28 +0000 Subject: [PATCH 07/13] chore(deps): bump rust-dashcore to a8c57fe (drop Copy, zeroize ExtendedPrivKey on Drop) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Advances the 8 rust-dashcore workspace deps from rev f42498e0d04257e28b4e457c16629904a872ab61 to a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e (PR #833 head). The new commit drops `Copy` from `ExtendedPrivKey` and gives it a `Drop` impl that zeroizes its secret material on scope exit — so the key wipes itself automatically instead of relying on caller-side wrappers, and non-`Copy` moves leave no stray bitwise duplicate behind. No workspace call sites relied on `ExtendedPrivKey: Copy`: the Copy-removal impact is absorbed upstream in `bip32::derive_priv` (clones `self` instead of `*self`), so `cargo check --workspace --all-targets` is clean with no source changes. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 16 ++++++++-------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a0a3f2d44c..6a71df9fafd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1637,7 +1637,7 @@ dependencies = [ [[package]] name = "dash-network" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e#a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" dependencies = [ "bincode", "bincode_derive", @@ -1648,7 +1648,7 @@ dependencies = [ [[package]] name = "dash-network-seeds" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e#a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" dependencies = [ "dash-network", ] @@ -1725,7 +1725,7 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e#a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" dependencies = [ "async-trait", "chrono", @@ -1754,7 +1754,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e#a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" dependencies = [ "anyhow", "base64-compat", @@ -1780,12 +1780,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e#a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" [[package]] name = "dashcore-rpc" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e#a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" dependencies = [ "dashcore-rpc-json", "hex", @@ -1798,7 +1798,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e#a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" dependencies = [ "bincode", "dashcore", @@ -1813,7 +1813,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e#a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" dependencies = [ "bincode", "dashcore-private", @@ -2867,7 +2867,7 @@ dependencies = [ [[package]] name = "git-state" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e#a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" [[package]] name = "glob" @@ -4034,7 +4034,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e#a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" dependencies = [ "aes", "async-trait", @@ -4063,7 +4063,7 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e#a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" dependencies = [ "cbindgen 0.29.4", "dash-network", @@ -4079,7 +4079,7 @@ dependencies = [ [[package]] name = "key-wallet-manager" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=f42498e0d04257e28b4e457c16629904a872ab61#f42498e0d04257e28b4e457c16629904a872ab61" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e#a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" dependencies = [ "async-trait", "bincode", diff --git a/Cargo.toml b/Cargo.toml index 3561c08beb9..746d23ddeb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,14 +50,14 @@ members = [ ] [workspace.dependencies] -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } -dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } -key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } -dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "f42498e0d04257e28b4e457c16629904a872ab61" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" } +dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" } +key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" } +dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e" } tokio-metrics = "0.5" From 374df98771712ec3858dde230762fbe689f22308 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:48:43 +0000 Subject: [PATCH 08/13] refactor(rs-sdk-ffi): rely on ExtendedPrivKey Drop, drop redundant Zeroizing wrap `ExtendedPrivKey` now zeroizes on `Drop` (rust-dashcore rev a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e), so wrapping the master and derived keys in `Zeroizing` inside `resolve_and_derive` only bought a harmless double-wipe. Unwrap both and let the type's own `Drop` do the work; `Zeroizing` stays on the plain byte buffers that have no `Drop` of their own (mnemonic buffer, BIP-39 seed, final 32-byte scalar). Rewrites the module `# Zeroization` block and the `resolve_and_derive` contract to the present three-mechanism reality (self-wiping key + Zeroizing buffers + non_secure_erase on SecretKey). The previously documented "irreducible transient" caveat is fully closed: a non-`Copy` move leaves no bitwise duplicate, so there is no un-wiped return slot. Co-Authored-By: Claude Opus 4.8 --- .../src/mnemonic_resolver_core_signer.rs | 57 ++++++++----------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs b/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs index 80bc8b3b891..97ce91cd0a3 100644 --- a/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs +++ b/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs @@ -39,18 +39,18 @@ //! # Zeroization //! //! Every intermediate that carries key material is wiped before the -//! method returns. Two mechanisms cover the different ownership +//! method returns. Three mechanisms cover the different ownership //! shapes: //! -//! - **`Zeroizing` wrappers** scrub on `Drop`. This covers the -//! byte-buffer intermediates (resolver mnemonic buffer, BIP-39 seed, -//! final derived 32-byte scalar) and the two intermediate -//! [`ExtendedPrivKey`] values (master + derived). `ExtendedPrivKey` -//! is `Copy` with no `Drop`, so the wipe fires through its -//! `Zeroizing` wrapper on every exit path — success, `?`-early-return, -//! and panic-unwind. `ExtendedPrivKey: Zeroize` comes from -//! rust-dashcore PR #833 (rev -//! `f42498e0d04257e28b4e457c16629904a872ab61`). +//! - **[`ExtendedPrivKey`] self-wipes on `Drop`.** The master and +//! derived extended keys zero their secret material when they leave +//! scope, on every exit path — success, `?`-early-return, and +//! panic-unwind. The type is no longer `Copy` as of rust-dashcore rev +//! `a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e`, so each move is a real +//! move that leaves no stray bitwise duplicate behind. +//! - **`Zeroizing` wrappers** scrub the plain byte buffers that carry +//! no `Drop` of their own: the resolver mnemonic buffer, the BIP-39 +//! seed, and the final derived 32-byte scalar. //! - **Explicit `non_secure_erase` calls** scrub the raw //! [`secp256k1::SecretKey`] copies at the two sign sites, where the //! scalar comes back out of `SecretKey::from_slice`. `SecretKey` has @@ -233,21 +233,16 @@ impl MnemonicResolverCoreSigner { /// /// # Zeroization contract /// - /// Both the `master` and `derived` extended keys are held in [`Zeroizing`] - /// and wiped when this method returns. The `ExtendedPrivKey` never crosses - /// the call boundary — `extract` only borrows it — so no raw or wrapped - /// key can outlive the derivation. `extract` returns public material + /// Both the `master` and `derived` extended keys wipe their secret + /// material when they leave this scope — [`ExtendedPrivKey`] zeroizes on + /// `Drop` as of rust-dashcore rev + /// `a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e`, and is no longer `Copy`, so + /// each move is a real move that leaves no bitwise duplicate behind. The + /// key never crosses the call boundary — `extract` only borrows it — so it + /// cannot outlive the derivation. `extract` returns public material /// (`ExtendedPubKey`) or a `Zeroizing` scalar copy; the caller wipes the - /// latter on its own drop. The mnemonic and seed buffers are likewise - /// `Zeroizing`-wrapped. `ExtendedPrivKey: Zeroize` comes from rust-dashcore - /// PR #833 (rev `f42498e0d04257e28b4e457c16629904a872ab61`). - /// - /// One transient is irreducible: `ExtendedPrivKey::derive_priv` returns the - /// child key *by value* (`ExtendedPrivKey` is `Copy`), so its return slot - /// briefly holds an un-wiped copy until the same-line `Zeroizing::new` - /// takes ownership. Closing that would require an upstream signature change - /// in key-wallet; it is not "zero copies ever", just the tightest this - /// layer can guarantee. + /// latter on its own drop. The mnemonic and seed buffers are plain arrays + /// and ride [`Zeroizing`] wrappers for the same guarantee. /// /// # Errors /// @@ -316,15 +311,11 @@ impl MnemonicResolverCoreSigner { drop(mnemonic); let secp = Secp256k1::new(); - let master = Zeroizing::new( - ExtendedPrivKey::new_master(self.network, seed.as_ref()).map_err(|e| { - MnemonicResolverSignerError::DerivationFailed(format!("master: {e}")) - })?, - ); - let derived = - Zeroizing::new(master.derive_priv(&secp, path).map_err(|e| { - MnemonicResolverSignerError::DerivationFailed(format!("path: {e}")) - })?); + let master = ExtendedPrivKey::new_master(self.network, seed.as_ref()) + .map_err(|e| MnemonicResolverSignerError::DerivationFailed(format!("master: {e}")))?; + let derived = master + .derive_priv(&secp, path) + .map_err(|e| MnemonicResolverSignerError::DerivationFailed(format!("path: {e}")))?; Ok(extract(&derived)) } From 1aac44a0abfb2ca93872218d09b25640e4a38939 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 2 Jul 2026 08:29:01 +0000 Subject: [PATCH 09/13] fix(deps): stage regenerated Cargo.lock for rust-dashcore a8a0968 bump The prior merge commit updated Cargo.toml's rust-dashcore rev pins to the current dev HEAD but left Cargo.lock staged at the pre-cargo-update state (still pinned to v0.44.0/991c6ebe), an oversight caught during PR comment verification. Stage the already-regenerated lockfile so both files agree on the a8a096838b829cf5bec3c2374a23511640a0c35c pin. Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 70 +++++++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5148cee60c1..c7ae962cba6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1206,7 +1206,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1636,8 +1636,8 @@ dependencies = [ [[package]] name = "dash-network" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "bincode", "bincode_derive", @@ -1647,8 +1647,8 @@ dependencies = [ [[package]] name = "dash-network-seeds" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "dash-network", ] @@ -1724,8 +1724,8 @@ dependencies = [ [[package]] name = "dash-spv" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "async-trait", "chrono", @@ -1753,8 +1753,8 @@ dependencies = [ [[package]] name = "dashcore" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "anyhow", "base64-compat", @@ -1779,13 +1779,13 @@ dependencies = [ [[package]] name = "dashcore-private" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" [[package]] name = "dashcore-rpc" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "dashcore-rpc-json", "hex", @@ -1797,8 +1797,8 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "bincode", "dashcore", @@ -1812,8 +1812,8 @@ dependencies = [ [[package]] name = "dashcore_hashes" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "bincode", "dashcore-private", @@ -2428,7 +2428,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2489,7 +2489,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2866,8 +2866,8 @@ dependencies = [ [[package]] name = "git-state" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" [[package]] name = "glob" @@ -3803,7 +3803,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4033,8 +4033,8 @@ dependencies = [ [[package]] name = "key-wallet" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "aes", "async-trait", @@ -4062,8 +4062,8 @@ dependencies = [ [[package]] name = "key-wallet-ffi" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "cbindgen 0.29.4", "dash-network", @@ -4078,8 +4078,8 @@ dependencies = [ [[package]] name = "key-wallet-manager" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "async-trait", "bincode", @@ -4596,7 +4596,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5696,7 +5696,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6486,7 +6486,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6499,7 +6499,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6558,7 +6558,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -7418,7 +7418,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -8867,7 +8867,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] From defa2491495a5499c0d97c9dfe6f327d8fb4f6b5 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 2 Jul 2026 08:31:25 +0000 Subject: [PATCH 10/13] build: sync Cargo.lock to rust-dashcore a8a0968 (0.45.0) The committed lockfile still pinned all 12 rust-dashcore workspace crates at the stale rev 991c6eb (v0.44.0), even though root Cargo.toml was already re-pinned to a8a096838b829cf5bec3c2374a23511640a0c35c (v0.45.0). Cargo silently re-resolves the lock on the next build, so --locked builds (CI) would fail against the mismatch. Regenerate the lock to match: dashcore, dashcore-private, dashcore_hashes, dashcore-rpc, dashcore-rpc-json, dash-network, dash-network-seeds, dash-spv, git-state, key-wallet, key-wallet-ffi, key-wallet-manager all move 991c6eb -> a8a0968. No other crate churn. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5148cee60c1..c177bf2960a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1636,8 +1636,8 @@ dependencies = [ [[package]] name = "dash-network" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "bincode", "bincode_derive", @@ -1647,8 +1647,8 @@ dependencies = [ [[package]] name = "dash-network-seeds" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "dash-network", ] @@ -1724,8 +1724,8 @@ dependencies = [ [[package]] name = "dash-spv" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "async-trait", "chrono", @@ -1753,8 +1753,8 @@ dependencies = [ [[package]] name = "dashcore" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "anyhow", "base64-compat", @@ -1779,13 +1779,13 @@ dependencies = [ [[package]] name = "dashcore-private" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" [[package]] name = "dashcore-rpc" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "dashcore-rpc-json", "hex", @@ -1797,8 +1797,8 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "bincode", "dashcore", @@ -1812,8 +1812,8 @@ dependencies = [ [[package]] name = "dashcore_hashes" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "bincode", "dashcore-private", @@ -2866,8 +2866,8 @@ dependencies = [ [[package]] name = "git-state" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" [[package]] name = "glob" @@ -4033,8 +4033,8 @@ dependencies = [ [[package]] name = "key-wallet" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "aes", "async-trait", @@ -4062,8 +4062,8 @@ dependencies = [ [[package]] name = "key-wallet-ffi" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "cbindgen 0.29.4", "dash-network", @@ -4078,8 +4078,8 @@ dependencies = [ [[package]] name = "key-wallet-manager" -version = "0.44.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +version = "0.45.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" dependencies = [ "async-trait", "bincode", From f669691f7a22f50f4fab7a056036a53ebf781b9b Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 2 Jul 2026 08:31:37 +0000 Subject: [PATCH 11/13] docs(rs-sdk-ffi): correct ExtendedPrivKey zeroization comments for a8a0968 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two accuracy fixes forced by the rust-dashcore pin move to a8a0968 (squashed PR #833 form), verified against key-wallet/src/bip32.rs at that rev — ExtendedPrivKey now derives Clone (not Copy) and has an explicit `impl Drop` that calls `self.zeroize()`: - signer_simple.rs: the `dash_sdk_sign_with_mnemonic_and_path` comment still claimed "the ExtendedPrivKey itself doesn't zeroize — derived falls out of scope intact". That was true when written (PR #3541, pre-Zeroize), but is now false: `derived` self-wipes on Drop. Reword to state present reality and clarify the Zeroizing wrapper is there because `secret_bytes()` copies the scalar into a fresh array with no Drop of its own. - mnemonic_resolver_core_signer.rs: re-point two rev citations from a8c57fe (rebased away upstream, no longer reachable from dev) to the current reachable pin a8a0968. The behavioral claims are unchanged and still hold. Comment-only; no functional change. Co-Authored-By: Claude Opus 4.8 --- packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs | 4 ++-- packages/rs-sdk-ffi/src/signer_simple.rs | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs b/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs index 97ce91cd0a3..6bedd522d8f 100644 --- a/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs +++ b/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs @@ -46,7 +46,7 @@ //! derived extended keys zero their secret material when they leave //! scope, on every exit path — success, `?`-early-return, and //! panic-unwind. The type is no longer `Copy` as of rust-dashcore rev -//! `a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e`, so each move is a real +//! `a8a096838b829cf5bec3c2374a23511640a0c35c`, so each move is a real //! move that leaves no stray bitwise duplicate behind. //! - **`Zeroizing` wrappers** scrub the plain byte buffers that carry //! no `Drop` of their own: the resolver mnemonic buffer, the BIP-39 @@ -236,7 +236,7 @@ impl MnemonicResolverCoreSigner { /// Both the `master` and `derived` extended keys wipe their secret /// material when they leave this scope — [`ExtendedPrivKey`] zeroizes on /// `Drop` as of rust-dashcore rev - /// `a8c57fe863c96ac9c7e33833549e7a4f75ac9b5e`, and is no longer `Copy`, so + /// `a8a096838b829cf5bec3c2374a23511640a0c35c`, and is no longer `Copy`, so /// each move is a real move that leaves no bitwise duplicate behind. The /// key never crosses the call boundary — `extract` only borrows it — so it /// cannot outlive the derivation. `extract` returns public material diff --git a/packages/rs-sdk-ffi/src/signer_simple.rs b/packages/rs-sdk-ffi/src/signer_simple.rs index 9b5ecd9ac90..a93571f0fc9 100644 --- a/packages/rs-sdk-ffi/src/signer_simple.rs +++ b/packages/rs-sdk-ffi/src/signer_simple.rs @@ -414,9 +414,11 @@ pub unsafe extern "C" fn dash_sdk_sign_with_mnemonic_and_path( Ok(d) => d, Err(_) => return fail(SIGN_WITH_MNEMONIC_ERR_DERIVATION), }; - // Pull the 32 secret bytes into a `Zeroizing` so they're scrubbed - // when this function returns (the `ExtendedPrivKey` itself doesn't - // zeroize — `derived` falls out of scope intact). + // Copy the 32 secret bytes into a `Zeroizing` so this fresh array — + // which has no `Drop` of its own — is scrubbed when the function + // returns. `derived` self-wipes separately: `ExtendedPrivKey` + // zeroizes on `Drop` as of rust-dashcore rev + // a8a096838b829cf5bec3c2374a23511640a0c35c. let secret_bytes: zeroize::Zeroizing<[u8; 32]> = zeroize::Zeroizing::new(derived.private_key.secret_bytes()); From 0259bed39dc2e179fd356b21768786220410dbdc Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:26:44 +0000 Subject: [PATCH 12/13] chore(deps): bump rust-dashcore to afcff156 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move all eight rust-dashcore workspace deps (dashcore, dash-spv, key-wallet, key-wallet-ffi, key-wallet-manager, dash-network, dash-network-seeds, dashcore-rpc) from a8a09683 to dev HEAD afcff156, and regenerate Cargo.lock so every rust-dashcore-sourced crate (including the transitive dashcore_hashes, dashcore-private, dashcore-rpc-json, git-state) resolves to the new rev. Upstream refactors the signing API: `extended_public_key` moves out of the `Signer` trait into a dedicated `ExtendedPubKeySigner: Signer` trait, so implementing an extended-pubkey export is now opt-in rather than a required `Signer` method. `ExtendedPrivKey` keeps its zeroize-on-`Drop`, non-`Copy` shape. The three rs-dpp test-signer stubs added earlier in this branch (`FixedKeySigner` in `state_transition/mod.rs` and its two `signing_tests.rs` call sites) only existed to satisfy the previously required `Signer::extended_public_key` method. With that method moved to the opt-in trait, the stubs no longer compile and nothing needs them as `ExtendedPubKeySigner` -- revert those three files to their v4.1-dev content. Co-Authored-By: Claude Opus 4.8 🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent --- Cargo.lock | 46 +++++++++---------- Cargo.toml | 16 +++---- packages/rs-dpp/src/state_transition/mod.rs | 11 +---- .../signing_tests.rs | 11 +---- .../signing_tests.rs | 11 +---- 5 files changed, 34 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7ae962cba6..6e400baaa77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1206,7 +1206,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1637,7 +1637,7 @@ dependencies = [ [[package]] name = "dash-network" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=afcff1566c81abb0a9cf64b99b0ee675169fddfc#afcff1566c81abb0a9cf64b99b0ee675169fddfc" dependencies = [ "bincode", "bincode_derive", @@ -1648,7 +1648,7 @@ dependencies = [ [[package]] name = "dash-network-seeds" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=afcff1566c81abb0a9cf64b99b0ee675169fddfc#afcff1566c81abb0a9cf64b99b0ee675169fddfc" dependencies = [ "dash-network", ] @@ -1725,7 +1725,7 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=afcff1566c81abb0a9cf64b99b0ee675169fddfc#afcff1566c81abb0a9cf64b99b0ee675169fddfc" dependencies = [ "async-trait", "chrono", @@ -1754,7 +1754,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=afcff1566c81abb0a9cf64b99b0ee675169fddfc#afcff1566c81abb0a9cf64b99b0ee675169fddfc" dependencies = [ "anyhow", "base64-compat", @@ -1780,12 +1780,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=afcff1566c81abb0a9cf64b99b0ee675169fddfc#afcff1566c81abb0a9cf64b99b0ee675169fddfc" [[package]] name = "dashcore-rpc" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=afcff1566c81abb0a9cf64b99b0ee675169fddfc#afcff1566c81abb0a9cf64b99b0ee675169fddfc" dependencies = [ "dashcore-rpc-json", "hex", @@ -1798,7 +1798,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=afcff1566c81abb0a9cf64b99b0ee675169fddfc#afcff1566c81abb0a9cf64b99b0ee675169fddfc" dependencies = [ "bincode", "dashcore", @@ -1813,7 +1813,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=afcff1566c81abb0a9cf64b99b0ee675169fddfc#afcff1566c81abb0a9cf64b99b0ee675169fddfc" dependencies = [ "bincode", "dashcore-private", @@ -2428,7 +2428,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2489,7 +2489,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2867,7 +2867,7 @@ dependencies = [ [[package]] name = "git-state" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=afcff1566c81abb0a9cf64b99b0ee675169fddfc#afcff1566c81abb0a9cf64b99b0ee675169fddfc" [[package]] name = "glob" @@ -3803,7 +3803,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4034,7 +4034,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=afcff1566c81abb0a9cf64b99b0ee675169fddfc#afcff1566c81abb0a9cf64b99b0ee675169fddfc" dependencies = [ "aes", "async-trait", @@ -4063,7 +4063,7 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=afcff1566c81abb0a9cf64b99b0ee675169fddfc#afcff1566c81abb0a9cf64b99b0ee675169fddfc" dependencies = [ "cbindgen 0.29.4", "dash-network", @@ -4079,7 +4079,7 @@ dependencies = [ [[package]] name = "key-wallet-manager" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=a8a096838b829cf5bec3c2374a23511640a0c35c#a8a096838b829cf5bec3c2374a23511640a0c35c" +source = "git+https://github.com/dashpay/rust-dashcore?rev=afcff1566c81abb0a9cf64b99b0ee675169fddfc#afcff1566c81abb0a9cf64b99b0ee675169fddfc" dependencies = [ "async-trait", "bincode", @@ -4596,7 +4596,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5696,7 +5696,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6486,7 +6486,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6499,7 +6499,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6558,7 +6558,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7418,7 +7418,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -8867,7 +8867,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 84d8a4f2b94..da7ec6e8986 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,14 +50,14 @@ members = [ ] [workspace.dependencies] -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8a096838b829cf5bec3c2374a23511640a0c35c" } -dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8a096838b829cf5bec3c2374a23511640a0c35c" } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8a096838b829cf5bec3c2374a23511640a0c35c" } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8a096838b829cf5bec3c2374a23511640a0c35c" } -key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8a096838b829cf5bec3c2374a23511640a0c35c" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8a096838b829cf5bec3c2374a23511640a0c35c" } -dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8a096838b829cf5bec3c2374a23511640a0c35c" } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "a8a096838b829cf5bec3c2374a23511640a0c35c" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "afcff1566c81abb0a9cf64b99b0ee675169fddfc" } +dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "afcff1566c81abb0a9cf64b99b0ee675169fddfc" } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "afcff1566c81abb0a9cf64b99b0ee675169fddfc" } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "afcff1566c81abb0a9cf64b99b0ee675169fddfc" } +key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "afcff1566c81abb0a9cf64b99b0ee675169fddfc" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "afcff1566c81abb0a9cf64b99b0ee675169fddfc" } +dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "afcff1566c81abb0a9cf64b99b0ee675169fddfc" } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "afcff1566c81abb0a9cf64b99b0ee675169fddfc" } tokio-metrics = "0.5" diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 74ebc13c977..92bf4a4e583 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -3335,7 +3335,7 @@ mod tests { use dashcore::secp256k1::{ ecdsa, rand::rngs::OsRng, Message, PublicKey, Secp256k1, SecretKey, }; - use key_wallet::bip32::{DerivationPath, ExtendedPubKey}; + use key_wallet::bip32::DerivationPath; use key_wallet::signer::{Signer as KwSigner, SignerMethod}; /// Fixed-key in-memory signer used only by this test. Mirrors how a @@ -3370,15 +3370,6 @@ mod tests { async fn public_key(&self, _path: &DerivationPath) -> Result { Ok(self.public) } - - async fn extended_public_key( - &self, - _path: &DerivationPath, - ) -> Result { - // Test stub holds a single raw key with no chain code; extended - // public key derivation is not meaningful here. - Err("FixedKeySigner: no chain code — extended_public_key not supported".to_string()) - } } // Generate a single random key. Using the same key on both sides is diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/signing_tests.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/signing_tests.rs index b2c4fb67f83..67f95088409 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/signing_tests.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_funding_from_asset_lock_transition/signing_tests.rs @@ -204,7 +204,7 @@ async fn try_from_asset_lock_with_signer_and_private_key_signs_multiple_inputs() async fn try_from_asset_lock_with_signers_produces_matching_signature() { use async_trait::async_trait; use dashcore::secp256k1::{ecdsa, Message}; - use key_wallet::bip32::{DerivationPath, ExtendedPubKey}; + use key_wallet::bip32::DerivationPath; use key_wallet::signer::{Signer as KwSigner, SignerMethod}; /// Fixed-key in-memory `key_wallet::signer::Signer`. Mirrors how the @@ -237,15 +237,6 @@ async fn try_from_asset_lock_with_signers_produces_matching_signature() { async fn public_key(&self, _path: &DerivationPath) -> Result { Ok(self.public) } - - async fn extended_public_key( - &self, - _path: &DerivationPath, - ) -> Result { - // Test stub holds a single raw key with no chain code; extended - // public key derivation is not meaningful here. - Err("FixedKeySigner: no chain code — extended_public_key not supported".to_string()) - } } let secp = Secp256k1::new(); diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs index 5ea8e633b3a..c31e3b1fc1e 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs @@ -33,7 +33,7 @@ use platform_version::version::PlatformVersion; use async_trait::async_trait; use dashcore::secp256k1::{ecdsa, Message, PublicKey, Secp256k1, SecretKey}; -use key_wallet::bip32::{DerivationPath, ExtendedPubKey}; +use key_wallet::bip32::DerivationPath; use key_wallet::signer::{Signer as KwSigner, SignerMethod}; /// Fixed-key in-memory `key_wallet::signer::Signer`. Mirrors how a @@ -77,15 +77,6 @@ impl KwSigner for FixedKeySigner { async fn public_key(&self, _path: &DerivationPath) -> Result { Ok(self.public) } - - async fn extended_public_key( - &self, - _path: &DerivationPath, - ) -> Result { - // Test stub holds a single raw key with no chain code; extended - // public key derivation is not meaningful here. - Err("FixedKeySigner: no chain code — extended_public_key not supported".to_string()) - } } fn make_chain_asset_lock_proof() -> AssetLockProof { From 0582e3c7f42818e219f14d219cbe1b2e9d7e67aa Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:26:56 +0000 Subject: [PATCH 13/13] feat(rs-sdk-ffi): export extended pubkey via ExtendedPubKeySigner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement `ExtendedPubKeySigner::extended_public_key` on `MnemonicResolverCoreSigner`, tracking the upstream split of that method out of the base `Signer` trait. Callers that need offline non-hardened descendant derivation (e.g. DashPay contact-payment addresses) can now obtain the full BIP-32 extended public key -- public point plus chain code -- from the mnemonic-resolver signer in one call. Funnel all private-key material through a single `resolve_and_derive` helper that owns the mnemonic -> seed -> master -> child `ExtendedPrivKey` stack and hands the derived key to a caller-supplied closure by reference. The extended key never crosses the call boundary and self-wipes on `Drop` (`ExtendedPrivKey` zeroizes and is not `Copy`); the mnemonic and seed byte buffers ride `Zeroizing` wrappers. Both `derive_priv` (raw scalar for signing) and `extended_public_key` (xpub) share this one entry point. Add a unit test asserting the exported xpub matches an independent BIP-39 derivation field-by-field (public key, chain code, depth, network) before the full-struct equality, so a dropped BIP-32 metadatum fails with a precise message rather than a vacuous assert. Also correct the `signer_simple.rs` zeroization comment, now that `ExtendedPrivKey` self-wipes on `Drop` -- the `Zeroizing` wrap there guards only the separate `secret_bytes()` array copy. Co-Authored-By: Claude Opus 4.8 🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent --- .../src/mnemonic_resolver_core_signer.rs | 15 ++++++++------- packages/rs-sdk-ffi/src/signer_simple.rs | 3 +-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs b/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs index 6bedd522d8f..623272b8534 100644 --- a/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs +++ b/packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs @@ -45,9 +45,8 @@ //! - **[`ExtendedPrivKey`] self-wipes on `Drop`.** The master and //! derived extended keys zero their secret material when they leave //! scope, on every exit path — success, `?`-early-return, and -//! panic-unwind. The type is no longer `Copy` as of rust-dashcore rev -//! `a8a096838b829cf5bec3c2374a23511640a0c35c`, so each move is a real -//! move that leaves no stray bitwise duplicate behind. +//! panic-unwind. The type is not `Copy`, so each move is a real move +//! that leaves no stray bitwise duplicate behind. //! - **`Zeroizing` wrappers** scrub the plain byte buffers that carry //! no `Drop` of their own: the resolver mnemonic buffer, the BIP-39 //! seed, and the final derived 32-byte scalar. @@ -66,7 +65,7 @@ use std::os::raw::c_char; use async_trait::async_trait; use key_wallet::bip32::{DerivationPath, ExtendedPrivKey, ExtendedPubKey}; use key_wallet::dashcore::secp256k1::{self, Secp256k1}; -use key_wallet::signer::{Signer, SignerMethod}; +use key_wallet::signer::{ExtendedPubKeySigner, Signer, SignerMethod}; use key_wallet::Network; use thiserror::Error; use zeroize::Zeroizing; @@ -235,9 +234,8 @@ impl MnemonicResolverCoreSigner { /// /// Both the `master` and `derived` extended keys wipe their secret /// material when they leave this scope — [`ExtendedPrivKey`] zeroizes on - /// `Drop` as of rust-dashcore rev - /// `a8a096838b829cf5bec3c2374a23511640a0c35c`, and is no longer `Copy`, so - /// each move is a real move that leaves no bitwise duplicate behind. The + /// `Drop` and is not `Copy`, so each move is a real move that leaves no + /// bitwise duplicate behind. The /// key never crosses the call boundary — `extract` only borrows it — so it /// cannot outlive the derivation. `extract` returns public material /// (`ExtendedPubKey`) or a `Zeroizing` scalar copy; the caller wipes the @@ -382,7 +380,10 @@ impl Signer for MnemonicResolverCoreSigner { secret.non_secure_erase(); Ok(pubkey) } +} +#[async_trait] +impl ExtendedPubKeySigner for MnemonicResolverCoreSigner { /// Derive the BIP-32 extended public key at `path`. /// /// Returns the full [`ExtendedPubKey`] (public point + chain code) so diff --git a/packages/rs-sdk-ffi/src/signer_simple.rs b/packages/rs-sdk-ffi/src/signer_simple.rs index a93571f0fc9..1eb97522e5a 100644 --- a/packages/rs-sdk-ffi/src/signer_simple.rs +++ b/packages/rs-sdk-ffi/src/signer_simple.rs @@ -417,8 +417,7 @@ pub unsafe extern "C" fn dash_sdk_sign_with_mnemonic_and_path( // Copy the 32 secret bytes into a `Zeroizing` so this fresh array — // which has no `Drop` of its own — is scrubbed when the function // returns. `derived` self-wipes separately: `ExtendedPrivKey` - // zeroizes on `Drop` as of rust-dashcore rev - // a8a096838b829cf5bec3c2374a23511640a0c35c. + // zeroizes on `Drop`. let secret_bytes: zeroize::Zeroizing<[u8; 32]> = zeroize::Zeroizing::new(derived.private_key.secret_bytes());