From 87e668dd762322bdb6f7b6931d1c698c3002aa93 Mon Sep 17 00:00:00 2001 From: bfoss765 Date: Wed, 5 Aug 2026 07:15:12 -0400 Subject: [PATCH 1/2] chore(baseline): bump key-wallet pin to dev #916 and migrate to new API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repoints the rust-dashcore workspace pins from 70d4bf8e to the canonical dev commit 8f78baa6 (rust-dashcore #916), matching what PR #4185 already points to. That repoint unavoidably also pulls in two older, already-merged dev PRs the v4.2-dev platform baseline predates, whose key-wallet API changes the baseline still calls the old way: * #818 — AddressInfo's flat `used`/`generated_at`/`used_at` fields became a `state: AddressState { Available | Reserved { at } | Used }` enum. * #915 — build_asset_lock[_with_signer] gained an explicit AssetLockFundingAccount funding source and a `drain` flag. * #916 — TransactionBuilder::build_unsigned was replaced by build_unsigned_reserved (additionally returns a ReservationToken). This is baseline maintenance surfaced by the #916 repoint, not a feature. It unblocks #4185/#4247/#4256/#4196, which inherit the baseline break. #818 (mechanical, behavior-preserving): `used == true` -> AddressState::Used, unused -> AddressState::Available. The removed `generated_at`/`used_at` timestamps have no equivalent on the new variants and are dropped (every platform site only ever wrote synthetic 0/None or read `used`). Platform never hands out the Reserved state, so Available/Used is the full range. #915 (semantic, behavior-preserving default): the single call site funds from the standard BIP44 account (documented `account_index`), so `AssetLockFundingAccount::Bip44 { account_index }`; `drain = false` reproduces the pre-#915 non-drain behavior the #915 author documents as the historical default. Flagged with a TODO for QE confirmation. #916 (behavior-preserving): the new build_unsigned_reserved has the same body as the old build_unsigned, only additionally surfacing the token assemble_unsigned already produced. The baseline releases abandoned reservations via the unchanged unconditional release_reservation(&tx), so the token is discarded to preserve pre-#916 behavior. Owner-guarded release via the token is the TOCTOU fix threaded by #4185, not baseline work. Whole workspace compiles clean (cargo check --workspace --all-targets). platform-wallet (513) and platform-wallet-ffi (220) lib tests pass. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 50 +++++++++---------- Cargo.toml | 16 +++--- .../rs-platform-wallet-ffi/src/persistence.rs | 36 ++++++++----- .../src/changeset/changeset.rs | 12 +++-- .../src/changeset/core_bridge.rs | 11 ++-- .../src/manager/accessors.rs | 18 +++++-- .../src/wallet/asset_lock/build.rs | 34 +++++++++++-- .../src/wallet/core/transaction.rs | 15 +++++- .../src/wallet/provider_key_at_index.rs | 11 ++-- 9 files changed, 136 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9ea5c271632..0bdbd033393 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1229,7 +1229,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1662,7 +1662,7 @@ dependencies = [ [[package]] name = "dash-network" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "bincode", "bincode_derive", @@ -1673,7 +1673,7 @@ dependencies = [ [[package]] name = "dash-network-seeds" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "dash-network", ] @@ -1750,7 +1750,7 @@ dependencies = [ [[package]] name = "dash-spv" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "async-trait", "chrono", @@ -1779,7 +1779,7 @@ dependencies = [ [[package]] name = "dashcore" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "anyhow", "base64-compat", @@ -1805,12 +1805,12 @@ dependencies = [ [[package]] name = "dashcore-private" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" [[package]] name = "dashcore-rpc" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "dashcore-rpc-json", "hex", @@ -1823,7 +1823,7 @@ dependencies = [ [[package]] name = "dashcore-rpc-json" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "bincode", "dashcore", @@ -1838,7 +1838,7 @@ dependencies = [ [[package]] name = "dashcore_hashes" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "bincode", "dashcore-private", @@ -2474,7 +2474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2535,7 +2535,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2904,7 +2904,7 @@ dependencies = [ [[package]] name = "git-state" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" [[package]] name = "glob" @@ -3588,7 +3588,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.4", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -3839,7 +3839,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4095,7 +4095,7 @@ dependencies = [ [[package]] name = "key-wallet" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "aes", "async-trait", @@ -4124,7 +4124,7 @@ dependencies = [ [[package]] name = "key-wallet-ffi" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "cbindgen 0.29.4", "dash-network", @@ -4140,7 +4140,7 @@ dependencies = [ [[package]] name = "key-wallet-manager" version = "0.45.0" -source = "git+https://github.com/dashpay/rust-dashcore?rev=70d4bf8e36057c58e02d56769a6e9760f701dd06#70d4bf8e36057c58e02d56769a6e9760f701dd06" +source = "git+https://github.com/dashpay/rust-dashcore?rev=8f78baa6b7979b9bea56501ad75b5a7b7150a711#8f78baa6b7979b9bea56501ad75b5a7b7150a711" dependencies = [ "async-trait", "bincode", @@ -5709,7 +5709,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.2", "rustls", - "socket2 0.6.4", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -5747,9 +5747,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.4", + "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6556,7 +6556,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6569,7 +6569,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6628,7 +6628,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7488,7 +7488,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8937,7 +8937,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 86e5432b7ef..3c7a1ad0760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,14 +52,14 @@ members = [ ] [workspace.dependencies] -dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } -dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } -dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } -key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } -key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } -key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } -dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } -dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "70d4bf8e36057c58e02d56769a6e9760f701dd06" } +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } +dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } +dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } +key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } +key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } +key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } +dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } +dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "8f78baa6b7979b9bea56501ad75b5a7b7150a711" } tokio-metrics = "0.5" diff --git a/packages/rs-platform-wallet-ffi/src/persistence.rs b/packages/rs-platform-wallet-ffi/src/persistence.rs index be30d937dd0..90b2e88a104 100644 --- a/packages/rs-platform-wallet-ffi/src/persistence.rs +++ b/packages/rs-platform-wallet-ffi/src/persistence.rs @@ -12,7 +12,9 @@ use key_wallet::bip32::DerivationPath; use key_wallet::bip32::ExtendedPubKey; use key_wallet::derivation_bls_bip32::ExtendedBLSPubKey; use key_wallet::derivation_slip10::ExtendedEd25519PubKey; -use key_wallet::managed_account::address_pool::{AddressPool, AddressPoolType, PublicKeyType}; +use key_wallet::managed_account::address_pool::{ + AddressPool, AddressPoolType, AddressState, PublicKeyType, +}; use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet::wallet::Wallet; @@ -3125,7 +3127,8 @@ fn build_core_address_entry_ffi( key_type_tag, pool_type_tag, address_index: info.index, - is_used: info.used, + // key-wallet #818: `used` bool -> `state` enum (Used == funded). + is_used: matches!(info.state, AddressState::Used), balance: info.balance, address_base58: address_ptr, derivation_path: path_ptr, @@ -3221,9 +3224,17 @@ unsafe fn address_info_from_ffi( public_key, index: entry.address_index, path, - used: entry.is_used, - generated_at: 0, - used_at: if entry.is_used { Some(0) } else { None }, + // key-wallet #818: the flat `used`/`generated_at`/`used_at` fields + // became the `state` enum. The FFI entry only carries `is_used`, so + // it round-trips to `Used`/`Available` (platform never hands out the + // `Reserved` state across FFI). The synthetic `generated_at: 0` / + // `used_at` values had no clock backing and have no equivalent on the + // new variants, so they are dropped without loss. + state: if entry.is_used { + AddressState::Used + } else { + AddressState::Available + }, tx_count: 0, total_received: 0, total_sent: 0, @@ -3270,7 +3281,8 @@ fn restore_address_pool(pool: &mut AddressPool, infos: Vec) { pool.script_pubkey_index .insert(info.script_pubkey.clone(), idx); pool.highest_generated = Some(pool.highest_generated.map_or(idx, |h| h.max(idx))); - if info.used { + // key-wallet #818: `used` bool -> `state` enum (Used == funded). + if matches!(info.state, AddressState::Used) { pool.used_indices.insert(idx); pool.highest_used = Some(pool.highest_used.map_or(idx, |h| h.max(idx))); } @@ -6248,9 +6260,8 @@ mod tests { index, path: DerivationPath::from_str(&format!("m/9'/1'/2'/{}", index)) .expect("static derivation path must parse"), - used: false, - generated_at: 0, - used_at: None, + // key-wallet #818: `used`/`generated_at`/`used_at` -> `state` enum. + state: AddressState::Available, tx_count: 0, total_received: 0, total_sent: 0, @@ -7072,9 +7083,8 @@ mod tests { public_key: Some(PublicKeyType::ECDSA(TEST_PUBKEY_G.to_vec())), index, path, - used: true, - generated_at: 0, - used_at: None, + // key-wallet #818: `used`/`generated_at`/`used_at` -> `state`. + state: AddressState::Used, tx_count: 0, total_received: 0, total_sent: 0, @@ -7124,7 +7134,7 @@ mod tests { entries .iter() .flat_map(|e| e.addresses.iter()) - .all(|a| a.used), + .all(|a| matches!(a.state, AddressState::Used)), "every emitted marked-used address must carry used == true" ); } diff --git a/packages/rs-platform-wallet/src/changeset/changeset.rs b/packages/rs-platform-wallet/src/changeset/changeset.rs index 1e302d2ea32..3251220486a 100644 --- a/packages/rs-platform-wallet/src/changeset/changeset.rs +++ b/packages/rs-platform-wallet/src/changeset/changeset.rs @@ -2158,7 +2158,7 @@ mod tests { index: u32, ) -> key_wallet::transaction_checking::DerivedAddressInfo { use key_wallet::bip32::{ChildNumber, DerivationPath}; - use key_wallet::managed_account::address_pool::{AddressInfo, PublicKeyType}; + use key_wallet::managed_account::address_pool::{AddressInfo, AddressState, PublicKeyType}; let pubkey = dashcore::PublicKey::from_slice(&TEST_PUBKEY_G).expect("generator point is valid"); @@ -2177,9 +2177,13 @@ mod tests { public_key: Some(PublicKeyType::ECDSA(TEST_PUBKEY_G.to_vec())), index, path, - used: true, - generated_at: 0, - used_at: None, + // key-wallet #818 replaced the flat `used`/`generated_at`/ + // `used_at` fields with the `state` enum. This stub models a + // used address, so `used: true` maps to `AddressState::Used`. + // The new `Used` variant carries no timestamp, so the former + // `generated_at`/`used_at` values have no equivalent and are + // dropped (they were write-only in this test stub). + state: AddressState::Used, tx_count: 0, total_received: 0, total_sent: 0, diff --git a/packages/rs-platform-wallet/src/changeset/core_bridge.rs b/packages/rs-platform-wallet/src/changeset/core_bridge.rs index b700e73dcaf..522ff1c1d63 100644 --- a/packages/rs-platform-wallet/src/changeset/core_bridge.rs +++ b/packages/rs-platform-wallet/src/changeset/core_bridge.rs @@ -31,7 +31,7 @@ use std::sync::Arc; use dashcore::blockdata::transaction::{txout::TxOut, OutPoint}; use dashcore::ScriptBuf; use key_wallet::account::AccountType; -use key_wallet::managed_account::address_pool::{AddressPool, AddressPoolType}; +use key_wallet::managed_account::address_pool::{AddressPool, AddressPoolType, AddressState}; use key_wallet::managed_account::transaction_record::{OutputRole, TransactionRecord}; use key_wallet::transaction_checking::transaction_router::AccountTypeToCheck; use key_wallet::transaction_checking::{DerivedAddressInfo, TransactionContext}; @@ -565,7 +565,10 @@ fn collect_usage_deltas_from_accounts( touched.insert(*owner_type); if seen.insert((*owner_type, pool.pool_type, pool_info.index)) { let mut info = pool_info.clone(); - info.used = true; + // key-wallet #818: the flat `used` bool became the + // `state` enum. Marking the address used is now + // `AddressState::Used`. + info.state = AddressState::Used; marked_used.push(DerivedAddressInfo { account_type: *owner_type, pool_type: pool.pool_type, @@ -836,7 +839,7 @@ mod usage_delta_tests { assert_eq!(entry.account_type, bip44_account_0()); assert_eq!(entry.pool_type, AddressPoolType::External); assert_eq!(entry.info.index, 0); - assert!(entry.info.used); + assert!(matches!(entry.info.state, AddressState::Used)); let watermarks = highest .get(&bip44_account_0()) @@ -902,7 +905,7 @@ mod usage_delta_tests { .find(|d| d.info.address == receive_address) .expect("spent-input address must be in the marked-used delta"); assert_eq!(entry.pool_type, AddressPoolType::External); - assert!(entry.info.used); + assert!(matches!(entry.info.state, AddressState::Used)); // The foreign output must NOT resolve to any pool. assert!( marked.iter().all(|d| d.info.address != { diff --git a/packages/rs-platform-wallet/src/manager/accessors.rs b/packages/rs-platform-wallet/src/manager/accessors.rs index 7dfc444c833..39057a919b0 100644 --- a/packages/rs-platform-wallet/src/manager/accessors.rs +++ b/packages/rs-platform-wallet/src/manager/accessors.rs @@ -5,7 +5,9 @@ use std::sync::Arc; use dashcore::{OutPoint, Txid}; use dpp::prelude::Identifier; use key_wallet::account::AccountType; -use key_wallet::managed_account::address_pool::{AddressInfo, AddressPool, AddressPoolType}; +use key_wallet::managed_account::address_pool::{ + AddressInfo, AddressPool, AddressPoolType, AddressState, +}; use key_wallet::managed_account::transaction_record::TransactionRecord; use key_wallet::utxo::Utxo; use key_wallet::WalletCoreBalance; @@ -400,8 +402,15 @@ impl PlatformWalletManager

{ .address_pools() .iter() .fold((0u32, 0u32), |(used, total), pool| { - let pool_used = - pool.addresses.values().filter(|info| info.used).count() as u32; + // key-wallet #818: `used` bool -> `state` enum. + // "used" counts only funded addresses, so match on + // `AddressState::Used` (a `Reserved` address is not + // used); this preserves the pre-#818 count exactly. + let pool_used = pool + .addresses + .values() + .filter(|info| matches!(info.state, AddressState::Used)) + .count() as u32; let pool_total = pool.addresses.len() as u32; (used + pool_used, total + pool_total) }); @@ -1110,7 +1119,8 @@ fn addr_info_snapshot(info: &AddressInfo) -> AccountAddressInfoSnapshot { AccountAddressInfoSnapshot { pubkey_hash, address_index: info.index, - is_used: info.used, + // key-wallet #818: `used` bool -> `state` enum (Used == funded). + is_used: matches!(info.state, AddressState::Used), address, public_key_bytes, } diff --git a/packages/rs-platform-wallet/src/wallet/asset_lock/build.rs b/packages/rs-platform-wallet/src/wallet/asset_lock/build.rs index 3fbe004aa1f..34e65693d16 100644 --- a/packages/rs-platform-wallet/src/wallet/asset_lock/build.rs +++ b/packages/rs-platform-wallet/src/wallet/asset_lock/build.rs @@ -107,13 +107,25 @@ impl AssetLockManager { }; // 3. Delegate to the key-wallet signer-driven builder. + // + // key-wallet #915 changed the builder signature: the funding + // source is now an explicit `AssetLockFundingAccount` (BIP44 vs + // CoinJoin) and a `drain` flag was added. This call site has + // always funded from the standard BIP44 account (`account_index` + // is documented as a BIP44 account index) and never drained, so + // `Bip44 { account_index }` + `drain = false` reproduces the + // pre-#915 behavior exactly. + // TODO(baseline-bump): confirm `drain` intent with QE — defaulted + // to `false` to preserve pre-#915 behavior (non-drain BIP44 funding). + use key_wallet::wallet::managed_wallet_info::asset_lock_builder::AssetLockFundingAccount; let result = info .core_wallet .build_asset_lock_with_signer( wallet, - account_index, + AssetLockFundingAccount::Bip44 { account_index }, vec![funding], DEFAULT_FEE_PER_KB, + false, signer, ) .await @@ -937,7 +949,13 @@ mod tests { let persisted_invitation_used = stored.iter().any(|cs| { cs.account_address_pools.iter().any(|entry| { matches!(entry.account_type, AccountType::IdentityInvitation) - && entry.addresses.iter().any(|a| a.used) + && entry.addresses.iter().any(|a| { + // key-wallet #818: `used` bool -> `state` enum. + matches!( + a.state, + key_wallet::managed_account::address_pool::AddressState::Used + ) + }) }) }); assert!( @@ -1343,7 +1361,17 @@ mod tests { .iter() .filter(|e| matches!(e.account_type, AccountType::IdentityInvitation)) { - let used = entry.addresses.iter().filter(|a| a.used).count(); + // key-wallet #818: `used` bool -> `state` enum. + let used = entry + .addresses + .iter() + .filter(|a| { + matches!( + a.state, + key_wallet::managed_account::address_pool::AddressState::Used + ) + }) + .count(); assert!( used >= last_used, "invitation pool snapshot rolled back: {used} used after {last_used}" diff --git a/packages/rs-platform-wallet/src/wallet/core/transaction.rs b/packages/rs-platform-wallet/src/wallet/core/transaction.rs index 81b2e8a8249..e164b30b924 100644 --- a/packages/rs-platform-wallet/src/wallet/core/transaction.rs +++ b/packages/rs-platform-wallet/src/wallet/core/transaction.rs @@ -150,10 +150,21 @@ impl CoreWallet { // `set_funding` observes ReservationSet and `build_unsigned` // records its selection. There is no await between them and the // manager write guard prevents another finalizer interleaving. - let (unsigned, fee) = builder + // + // key-wallet #916 renamed `build_unsigned` to + // `build_unsigned_reserved`, which additionally surfaces the + // `ReservationToken` stamped onto the reserved inputs. Its body is + // otherwise identical to the old `build_unsigned` (the reservation + // was always recorded inside `assemble_unsigned`). This baseline + // path releases abandoned reservations via the unconditional + // `release_reservation(&unsigned)` below, exactly as before, so the + // token is intentionally discarded here to preserve pre-#916 + // behavior. Owner-guarded release via the token is the TOCTOU fix + // threaded by dashpay/platform#4185, not baseline maintenance. + let (unsigned, fee, _reservation) = builder .set_current_height(height) .set_funding(managed, &account) - .build_unsigned() + .build_unsigned_reserved() .map_err(|error| map_builder_error(error, account_type, account_index))?; let selected: Vec = match unsigned diff --git a/packages/rs-platform-wallet/src/wallet/provider_key_at_index.rs b/packages/rs-platform-wallet/src/wallet/provider_key_at_index.rs index 1584f7eb73e..24e00f05617 100644 --- a/packages/rs-platform-wallet/src/wallet/provider_key_at_index.rs +++ b/packages/rs-platform-wallet/src/wallet/provider_key_at_index.rs @@ -204,7 +204,7 @@ pub fn populate_platform_node_pool( network: key_wallet::Network, ) -> Result<(), PlatformWalletError> { use dashcore::hashes::Hash; - use key_wallet::managed_account::address_pool::{AddressPoolType, PublicKeyType}; + use key_wallet::managed_account::address_pool::{AddressPoolType, AddressState, PublicKeyType}; use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::AddressInfo; @@ -247,9 +247,12 @@ pub fn populate_platform_node_pool( public_key: Some(PublicKeyType::EdDSA(key.public_key.to_vec())), index: key.index, path, - used: false, - generated_at: 0, - used_at: None, + // key-wallet #818: the flat `used`/`generated_at`/`used_at` + // fields became the `state` enum. A freshly derived, unfunded + // address is `AddressState::Available` (the former + // `used: false`); the removed timestamps have no equivalent on + // the new `Available` variant. + state: AddressState::Available, tx_count: 0, total_received: 0, total_sent: 0, From f49f49e01846d6d2fc700e90187f81a422370da6 Mon Sep 17 00:00:00 2001 From: bfoss765 <38437574+bfoss765@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:46:33 -0400 Subject: [PATCH 2/2] fix(swift-sdk): migrate AddressInfo off the removed generated_at FFI field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The key-wallet pin bump takes key-wallet #818, which dropped `generated_at` / `used_at` from `#[repr(C)] FFIAddressInfo`. `build_ios.sh` regenerates that crate's cbindgen header into DashSDKFFI.xcframework, so a fresh framework build left AddressPool.swift referencing a member the C struct no longer has: error: value of type 'FFIAddressInfo' has no member 'generated_at' `generatedAt` is kept (public API, no in-repo or observable change) and pinned to the Unix epoch. That is not a substituted value: every key-wallet `AddressInfo` constructor set `generated_at: 0` ("Should use actual timestamp"), so the property has always evaluated to the epoch. Both it and `used` now document the new `AddressState` model, including that a `Reserved` address reports `used == false` and that reservation state is not observable across the C surface. Also settles the persistence round-trip question raised in review. `generated_at` / `used_at` were never serialized — `CoreAddressEntryFFI` has no field for them, and `address_info_from_ffi` was the only producer, from the literals `0` / `Some(0)` — so dropping them is lossless. `Reserved { at }` genuinely has no slot in that schema and would reload as `Available`, but nothing in platform reserves addresses (no caller of `next_receive_address_and_reserve` / `next_unused_and_reserve` in the workspace), so the loss is unreachable today rather than live. The emit path now spells the `Reserved` arm out and warns instead of silently flattening it, the schema field documents the constraint, and a new test pins the behavior so a future reserving caller surfaces as a schema decision. Verified: `cargo check --workspace --all-targets` clean; platform-wallet 513 passed, platform-wallet-ffi 221 passed; rustfmt clean; scoped clippy (the CI invocation) clean. Swift verified by type-checking the whole SwiftDashSDK module under -swift-version 6 -warnings-as-errors against the cbindgen headers regenerated from the new pin — clean, and the pre-fix source reproduces the reviewer's exact error against the same headers. Co-Authored-By: Claude Opus 4.8 --- .../src/core_address_types.rs | 12 ++- .../rs-platform-wallet-ffi/src/persistence.rs | 85 +++++++++++++++++-- .../SwiftDashSDK/KeyWallet/AddressPool.swift | 29 ++++++- 3 files changed, 118 insertions(+), 8 deletions(-) diff --git a/packages/rs-platform-wallet-ffi/src/core_address_types.rs b/packages/rs-platform-wallet-ffi/src/core_address_types.rs index c82827b67d1..67348971c7a 100644 --- a/packages/rs-platform-wallet-ffi/src/core_address_types.rs +++ b/packages/rs-platform-wallet-ffi/src/core_address_types.rs @@ -76,7 +76,17 @@ pub struct CoreAddressEntryFFI { pub pool_type_tag: u8, /// Derivation index within this pool. pub address_index: u32, - /// `AddressInfo.used` at emit time. + /// Whether funds had been seen at this address at emit time, i.e. + /// `AddressInfo.state == AddressState::Used`. + /// + /// key-wallet #818 turned `AddressInfo`'s flat `used` bool into the + /// `AddressState { Available | Reserved { at } | Used }` lifecycle. + /// This schema predates that and has no slot for a reservation, so + /// `Reserved` necessarily flattens to `false` and reloads as + /// `Available`. Nothing in platform reserves addresses today, so + /// that lossy case is currently unreachable; adding a reserving + /// caller requires a schema decision here first (see + /// `persistence::build_core_address_entry_ffi`). pub is_used: bool, /// Cached balance in duffs from `AddressInfo.balance`. pub balance: u64, diff --git a/packages/rs-platform-wallet-ffi/src/persistence.rs b/packages/rs-platform-wallet-ffi/src/persistence.rs index 90b2e88a104..73bcfb16e46 100644 --- a/packages/rs-platform-wallet-ffi/src/persistence.rs +++ b/packages/rs-platform-wallet-ffi/src/persistence.rs @@ -3128,7 +3128,30 @@ fn build_core_address_entry_ffi( pool_type_tag, address_index: info.index, // key-wallet #818: `used` bool -> `state` enum (Used == funded). - is_used: matches!(info.state, AddressState::Used), + // + // `CoreAddressEntryFFI` predates that enum and carries a single + // `is_used` bool with no slot for a reservation, so `Reserved` + // can only flatten to `false` and would reload as `Available` — + // silently returning a handed-out address to the pool. Nothing + // in platform reserves addresses (no caller of + // `next_receive_address_and_reserve` / `next_unused_and_reserve` + // anywhere in the workspace), so that arm is unreachable today. + // It is spelled out rather than folded into a catch-all so the + // first reserving caller shows up as an explicit schema decision + // instead of losing state on the next reload. + is_used: match info.state { + AddressState::Used => true, + AddressState::Available => false, + AddressState::Reserved { .. } => { + tracing::warn!( + index = info.index, + "persist: address pool entry is reserved, but the persisted \ + address schema cannot represent a reservation; it will \ + reload as available and may be handed out again" + ); + false + } + }, balance: info.balance, address_base58: address_ptr, derivation_path: path_ptr, @@ -3225,11 +3248,17 @@ unsafe fn address_info_from_ffi( index: entry.address_index, path, // key-wallet #818: the flat `used`/`generated_at`/`used_at` fields - // became the `state` enum. The FFI entry only carries `is_used`, so - // it round-trips to `Used`/`Available` (platform never hands out the - // `Reserved` state across FFI). The synthetic `generated_at: 0` / - // `used_at` values had no clock backing and have no equivalent on the - // new variants, so they are dropped without loss. + // became the `state` enum. The persisted entry only carries + // `is_used`, so it round-trips to `Used`/`Available`; see + // `build_core_address_entry_ffi` for why `Reserved` is unreachable + // here. + // + // Dropping `generated_at`/`used_at` is lossless because neither was + // ever persisted: `CoreAddressEntryFFI` has no field for them, so + // this constructor was the only thing that produced them, from the + // literals `0` / `Some(0)`. Upstream never had real values either — + // every key-wallet `AddressInfo` constructor set `generated_at: 0` + // ("Should use actual timestamp"). No stored information is lost. state: if entry.is_used { AddressState::Used } else { @@ -6381,6 +6410,50 @@ mod tests { } } + /// Pin what the persisted address row does to key-wallet #818's + /// `AddressState`. `CoreAddressEntryFFI` predates the enum and carries + /// a single `is_used` bool, so only `Used` and `Available` survive a + /// save/load cycle intact; `Reserved { .. }` has no representation and + /// comes back `Available`, i.e. a reload silently frees a handed-out + /// address. Nothing in platform reserves addresses today, so this + /// records the boundary rather than a live bug: if a reserving caller + /// is ever added, this test is where the schema decision (a dedicated + /// reservation field on the row) has to be made. + #[test] + fn address_state_round_trip_keeps_used_and_flattens_reserved() { + for (state, expected) in [ + (AddressState::Available, AddressState::Available), + (AddressState::Used, AddressState::Used), + ( + AddressState::Reserved { at: 1_700_000_000 }, + AddressState::Available, + ), + ] { + let mut info = typed_key_test_address_info(11, None); + info.state = state; + + let mut owned: Vec = Vec::new(); + let entry = build_core_address_entry_ffi( + &info, + AddressPoolTypeTagFFI::AbsentHardened as u8, + false, + &mut owned, + ) + .expect("build_core_address_entry_ffi must succeed"); + // SAFETY: the address / path c-strings live in `owned`, kept + // alive until after this decode. + let restored = unsafe { address_info_from_ffi(&entry, Network::Testnet) } + .expect("address_info_from_ffi must decode the row"); + drop(owned); + + assert_eq!( + restored.state, expected, + "{:?} must restore as {:?} through the persisted row", + state, expected + ); + } + } + /// A LEGACY row (persisted before the typed-key column: empty key, /// `public_key: None` after decode) must NOT strip the typed key the /// gap-limit prederivation put at the same index — pre-typed-key diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift index 23378ea6dc6..2102c6a397c 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/AddressPool.swift @@ -79,7 +79,30 @@ public struct AddressInfo { public let publicKey: Data? public let index: UInt32 public let path: String + + /// Whether funds have been seen at this address. + /// + /// key-wallet #818 replaced `AddressInfo`'s flat `used` flag with a + /// three-state `AddressState { Available | Reserved { at } | Used }` + /// lifecycle. `FFIAddressInfo.used` is now derived as + /// `state == .used`, so an address that has been *reserved* (handed + /// out but not yet funded) still reports `used == false`. The C + /// surface carries no reservation flag, so reservation state is not + /// observable from Swift; if that is ever needed, `FFIAddressInfo` + /// has to gain a field upstream in key-wallet-ffi first. public let used: Bool + + /// Always the Unix epoch (1970-01-01). + /// + /// key-wallet #818 removed `generated_at` from `FFIAddressInfo`, and + /// none of the new `AddressState` variants carries a generation + /// timestamp, so there is nothing left to read. This is *not* a + /// substituted value: every `AddressInfo` upstream ever produced was + /// constructed with the literal placeholder `generated_at: 0` + /// (annotated "Should use actual timestamp" in key-wallet), so this + /// property has always evaluated to the epoch. It is retained only so + /// existing call sites keep compiling and observe the same value as + /// before; do not read it as a real generation time. public let generatedAt: Date init(ffiInfo: FFIAddressInfo) { @@ -114,6 +137,10 @@ public struct AddressInfo { } self.used = ffiInfo.used - self.generatedAt = Date(timeIntervalSince1970: TimeInterval(ffiInfo.generated_at)) + // key-wallet #818 dropped `generated_at` from the C struct. See the + // property doc: the field it used to read was always the literal 0, + // so pinning the epoch here reproduces the previous value exactly + // rather than inventing one. + self.generatedAt = Date(timeIntervalSince1970: 0) } }