diff --git a/packages/rs-platform-wallet-ffi/Cargo.toml b/packages/rs-platform-wallet-ffi/Cargo.toml index 83cd3fbb4cd..fd8a735f80a 100644 --- a/packages/rs-platform-wallet-ffi/Cargo.toml +++ b/packages/rs-platform-wallet-ffi/Cargo.toml @@ -10,7 +10,11 @@ description = "C FFI bindings for platform-wallet" crate-type = ["staticlib", "cdylib", "rlib"] [dependencies] -platform-wallet = { path = "../rs-platform-wallet" } +# `bls`/`eddsa` are required explicitly, not just inherited from +# `platform-wallet`'s default features: `persistence.rs` calls +# `rebuild_provider_key_account` and matches on `ProviderKeyExtendedPubKey`, +# both gated behind those features in `platform-wallet`. +platform-wallet = { path = "../rs-platform-wallet", features = ["bls", "eddsa"] } dpp = { path = "../rs-dpp" } dash-sdk = { path = "../rs-sdk", features = ["wallet"] } # Needed for `SignerHandle` + `VTableSigner` so the `*_with_signer` diff --git a/packages/rs-platform-wallet-ffi/src/persistence.rs b/packages/rs-platform-wallet-ffi/src/persistence.rs index d05e7c7688e..7491999e283 100644 --- a/packages/rs-platform-wallet-ffi/src/persistence.rs +++ b/packages/rs-platform-wallet-ffi/src/persistence.rs @@ -11,7 +11,7 @@ use bincode::config; use key_wallet::account::account_collection::AccountCollection; -use key_wallet::account::{Account, AccountType, BLSAccount, EdDSAAccount, StandardAccountType}; +use key_wallet::account::{Account, AccountType, StandardAccountType}; use key_wallet::bip32::DerivationPath; use key_wallet::bip32::ExtendedPubKey; use key_wallet::derivation_bls_bip32::ExtendedBLSPubKey; @@ -27,6 +27,9 @@ use parking_lot::Mutex; use std::str::FromStr; use crate::types::{FFINetwork, Network}; +use platform_wallet::changeset::provider_key_account::{ + rebuild_provider_key_account, ProviderAccountRebuildError, +}; use platform_wallet::changeset::{ AccountAddressPoolEntry, AccountRegistrationEntry, ClientStartState, ClientWalletStartState, ListedCoreTxid, PersistenceCapabilities, PersistenceError, PersistenceErrorKind, @@ -4969,6 +4972,23 @@ impl Drop for LoadGuard { } } +/// Map a provider-account rebuild failure to a load error naming the +/// curve-specific constructor or `AccountCollection` insert that failed. +fn provider_rebuild_error( + constructor: &str, + insert: &str, + error: ProviderAccountRebuildError, +) -> PersistenceError { + match error { + ProviderAccountRebuildError::Invalid(e) => { + PersistenceError::backend(format!("{constructor} failed: {e:?}")) + } + ProviderAccountRebuildError::Rejected(e) => { + PersistenceError::backend(format!("AccountCollection::{insert} failed: {e}")) + } + } +} + /// Reconstruct an external-signable [`Wallet`] + matching start-state /// bucket from a single `WalletRestoreEntryFFI`. The mnemonic / seed /// stays in the host's keychain; signing requests route back through @@ -5029,7 +5049,7 @@ fn build_wallet_start_state( // platform node keys) live in dedicated `Option` fields on the // collection and carry a non-secp256k1 extended public key in // the same `account_xpub_bytes` slot. Rebuild them watch-only - // via the type-specific `new` + insert methods rather than the + // via the shared `rebuild_provider_key_account` rather than the // ECDSA `Account::from_xpub` / `insert` path (which would fail // to decode the bytes and reject the provider `AccountType`). // Provider xpubs are stored raw (`bincode(xpub)`), exactly like the @@ -5056,21 +5076,14 @@ fn build_wallet_start_state( .map_err(|e| { PersistenceError::backend(format!("failed to decode provider BLS xpub: {}", e)) })?; - let bls_account = BLSAccount::new( - Some(entry.wallet_id.to_vec()), - account_type, - bls_pubkey, + rebuild_provider_key_account( + &mut accounts, + entry.wallet_id, network, + account_type, + &ProviderKeyExtendedPubKey::Bls(bls_pubkey), ) - .map_err(|e| { - PersistenceError::backend(format!("BLSAccount::new failed: {:?}", e)) - })?; - accounts.insert_bls_account(bls_account).map_err(|e| { - PersistenceError::backend(format!( - "AccountCollection::insert_bls_account failed: {}", - e - )) - })?; + .map_err(|e| provider_rebuild_error("BLSAccount::new", "insert_bls_account", e))?; continue; } AccountType::ProviderPlatformKeys => { @@ -5084,20 +5097,15 @@ fn build_wallet_start_state( e )) })?; - let eddsa_account = EdDSAAccount::new( - Some(entry.wallet_id.to_vec()), - account_type, - ed_pubkey, + rebuild_provider_key_account( + &mut accounts, + entry.wallet_id, network, + account_type, + &ProviderKeyExtendedPubKey::EdDSA(ed_pubkey), ) .map_err(|e| { - PersistenceError::backend(format!("EdDSAAccount::new failed: {:?}", e)) - })?; - accounts.insert_eddsa_account(eddsa_account).map_err(|e| { - PersistenceError::backend(format!( - "AccountCollection::insert_eddsa_account failed: {}", - e - )) + provider_rebuild_error("EdDSAAccount::new", "insert_eddsa_account", e) })?; // The platform-node (Ed25519) pool is rehydrated from the // persisted core-address rows like every other pool — see @@ -9107,6 +9115,72 @@ mod tests { ); } + /// `build_wallet_start_state` rebuilds the BLS operator-key and EdDSA + /// platform-node-key accounts watch-only from their bincode-encoded specs. + #[test] + fn provider_key_accounts_survive_restore_round_trip() { + let wallet = Wallet::from_seed_bytes( + [0x42; 64], + Network::Testnet, + key_wallet::wallet::initialization::WalletAccountCreationOptions::Default, + ) + .expect("seeded wallet"); + let bls = wallet + .accounts + .bls_account_of_type(AccountType::ProviderOperatorKeys) + .expect("a Default-created wallet has a BLS provider account") + .bls_public_key + .clone(); + let eddsa = wallet + .accounts + .eddsa_account_of_type(AccountType::ProviderPlatformKeys) + .expect("a Default-created wallet has an EdDSA provider account") + .ed25519_public_key + .clone(); + let bls_bytes = bincode::encode_to_vec(&bls, config::standard()).expect("encode BLS xpub"); + let eddsa_bytes = + bincode::encode_to_vec(&eddsa, config::standard()).expect("encode EdDSA xpub"); + let specs = [ + build_account_spec_ffi(&AccountType::ProviderOperatorKeys, &bls_bytes), + build_account_spec_ffi(&AccountType::ProviderPlatformKeys, &eddsa_bytes), + ]; + let entry = WalletRestoreEntryFFI { + wallet_id: wallet.wallet_id, + accounts: specs.as_ptr(), + accounts_count: specs.len(), + ..Default::default() + }; + + let (state, _) = + build_wallet_start_state(&entry).expect("provider key accounts must restore"); + + let restored_bls = state + .wallet + .accounts + .bls_account_of_type(AccountType::ProviderOperatorKeys) + .expect("BLS provider account must be rebuilt"); + let restored_bls_bytes = + bincode::encode_to_vec(&restored_bls.bls_public_key, config::standard()) + .expect("encode restored BLS xpub"); + assert_eq!(restored_bls_bytes, bls_bytes); + assert_eq!( + restored_bls.parent_wallet_id.as_deref(), + Some(&wallet.wallet_id[..]) + ); + assert!(restored_bls.is_watch_only); + let restored_eddsa = state + .wallet + .accounts + .eddsa_account_of_type(AccountType::ProviderPlatformKeys) + .expect("EdDSA provider account must be rebuilt"); + assert_eq!(restored_eddsa.ed25519_public_key, eddsa); + assert_eq!( + restored_eddsa.parent_wallet_id.as_deref(), + Some(&wallet.wallet_id[..]) + ); + assert!(restored_eddsa.is_watch_only); + } + /// The BLS xpub decoder is the only account-xpub graph with a /// length-prefixed field (the public key bytes). A host-supplied blob /// whose prefix claims a gigabyte must fail on the decode budget, before diff --git a/packages/rs-platform-wallet-storage/Cargo.toml b/packages/rs-platform-wallet-storage/Cargo.toml index 63e7491e009..943f26ac70c 100644 --- a/packages/rs-platform-wallet-storage/Cargo.toml +++ b/packages/rs-platform-wallet-storage/Cargo.toml @@ -30,8 +30,15 @@ hex = "0.4" # (dashpay writer). `dash-sdk` is here for the `AddressFunds` re-export # in `schema/platform_addrs.rs`. Storage declares only the features it uses # directly; `platform-wallet` adds `dash-sdk/wallet` to Cargo's unified set. +# `bls`/`eddsa` are required explicitly (not just inherited from +# `platform-wallet`'s default features) because `rebuild_provider_key_account` +# and `ProviderKeyExtendedPubKey`'s variants live behind those gates — a +# `platform-wallet` built with `default-features = false` must not silently +# break this crate's compile. platform-wallet = { path = "../rs-platform-wallet", features = [ "serde", + "bls", + "eddsa", ], optional = true } serde = { version = "1", features = ["derive"], optional = true } key-wallet = { workspace = true, optional = true } @@ -154,6 +161,10 @@ apple-native-keyring-store = { version = "=1.0.0", features = ["keychain"], opti windows-native-keyring-store = { version = "=1.0.0", optional = true } [dev-dependencies] +# `test-utils` reaches `provider_key_test_wallet`, shared with +# `platform-wallet`'s own `rebuild_provider_key_account` tests — see its use +# in `sqlite/provider_accounts.rs`'s test module. +platform-wallet = { path = "../rs-platform-wallet", features = ["test-utils"] } proptest = "1" assert_cmd = "2" static_assertions = "1" diff --git a/packages/rs-platform-wallet-storage/src/sqlite/provider_accounts.rs b/packages/rs-platform-wallet-storage/src/sqlite/provider_accounts.rs index c8419d570f3..ec6fb8bdc6e 100644 --- a/packages/rs-platform-wallet-storage/src/sqlite/provider_accounts.rs +++ b/packages/rs-platform-wallet-storage/src/sqlite/provider_accounts.rs @@ -1,54 +1,6 @@ -//! Provider account and public-key pool reconstruction for SQLite load. +//! Platform-node public-key pool reconstruction for SQLite load. use key_wallet::account::AccountType; -use platform_wallet::changeset::ProviderKeyExtendedPubKey; - -/// Why a provider key-material account could not be rebuilt into an -/// [`AccountCollection`](key_wallet::account::account_collection::AccountCollection). -#[derive(Debug, thiserror::Error)] -pub(super) enum ProviderAccountRebuildError { - /// The curve-specific account constructor rejected the key. - #[error("provider key account is invalid")] - Invalid(#[from] key_wallet::error::Error), - /// The collection refused the account — its `account_type` does not match - /// the curve (e.g. a BLS key offered as `ProviderPlatformKeys`). - #[error("account collection rejected the provider key account: {0}")] - Rejected(&'static str), -} - -/// Rebuild a watch-only provider account in its curve-specific collection slot. -pub(super) fn rebuild_provider_key_account( - accounts: &mut key_wallet::account::account_collection::AccountCollection, - wallet_id: [u8; 32], - network: key_wallet::Network, - account_type: AccountType, - extended_public_key: &ProviderKeyExtendedPubKey, -) -> Result<(), ProviderAccountRebuildError> { - match extended_public_key { - ProviderKeyExtendedPubKey::Bls(key) => { - let account = key_wallet::account::BLSAccount::new( - Some(wallet_id.to_vec()), - account_type, - key.clone(), - network, - )?; - accounts - .insert_bls_account(account) - .map_err(ProviderAccountRebuildError::Rejected) - } - ProviderKeyExtendedPubKey::EdDSA(key) => { - let account = key_wallet::account::EdDSAAccount::new( - Some(wallet_id.to_vec()), - account_type, - key.clone(), - network, - )?; - accounts - .insert_eddsa_account(account) - .map_err(ProviderAccountRebuildError::Rejected) - } - } -} /// Errors while inserting a pre-derived platform-node key into its managed pool. #[derive(Debug, thiserror::Error)] @@ -149,85 +101,12 @@ pub(super) fn insert_platform_node_pool_entry( mod tests { use super::*; use key_wallet::Network; + // Shared with `platform-wallet`'s own `rebuild_provider_key_account` tests + // via its `test-utils` feature (see this crate's `[dev-dependencies]`) — + // one fixture instead of two drifting copies. + use platform_wallet::changeset::provider_key_account::provider_key_test_wallet; use platform_wallet::wallet::provider_key_at_index::derive_platform_node_public_keys; - fn provider_key_test_wallet() -> key_wallet::wallet::Wallet { - key_wallet::wallet::Wallet::from_seed_bytes( - [0x42; 64], - Network::Testnet, - key_wallet::wallet::initialization::WalletAccountCreationOptions::Default, - ) - .expect("provider key test wallet") - } - - #[test] - fn rebuild_provider_key_account_restores_bls_and_eddsa() { - let wallet = provider_key_test_wallet(); - let bls_key = wallet - .accounts - .bls_account_of_type(AccountType::ProviderOperatorKeys) - .expect("BLS provider account") - .bls_public_key - .clone(); - let eddsa_key = wallet - .accounts - .eddsa_account_of_type(AccountType::ProviderPlatformKeys) - .expect("EdDSA provider account") - .ed25519_public_key - .clone(); - let mut accounts = key_wallet::account::account_collection::AccountCollection::new(); - let wallet_id = [0x24; 32]; - - rebuild_provider_key_account( - &mut accounts, - wallet_id, - Network::Testnet, - AccountType::ProviderOperatorKeys, - &ProviderKeyExtendedPubKey::Bls(bls_key), - ) - .expect("rebuild BLS provider account"); - rebuild_provider_key_account( - &mut accounts, - wallet_id, - Network::Testnet, - AccountType::ProviderPlatformKeys, - &ProviderKeyExtendedPubKey::EdDSA(eddsa_key), - ) - .expect("rebuild EdDSA provider account"); - - assert!(accounts - .bls_account_of_type(AccountType::ProviderOperatorKeys) - .is_some()); - assert!(accounts - .eddsa_account_of_type(AccountType::ProviderPlatformKeys) - .is_some()); - } - - #[test] - fn rebuild_provider_key_account_rejects_curve_account_type_mismatch() { - let wallet = provider_key_test_wallet(); - let bls_key = wallet - .accounts - .bls_account_of_type(AccountType::ProviderOperatorKeys) - .expect("BLS provider account") - .bls_public_key - .clone(); - let mut accounts = key_wallet::account::account_collection::AccountCollection::new(); - - let error = rebuild_provider_key_account( - &mut accounts, - [0x24; 32], - Network::Testnet, - AccountType::ProviderPlatformKeys, - &ProviderKeyExtendedPubKey::Bls(bls_key), - ) - .expect_err("BLS key must not rebuild as a platform-node account"); - - assert!(matches!(error, ProviderAccountRebuildError::Rejected(_))); - assert!(accounts - .eddsa_account_of_type(AccountType::ProviderPlatformKeys) - .is_none()); - } #[test] fn insert_used_platform_node_pool_entry_restores_used_bookkeeping() { use dashcore::hashes::Hash; diff --git a/packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs b/packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs index c0be88399ac..270e0a436d0 100644 --- a/packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs +++ b/packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs @@ -11,12 +11,12 @@ use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet::wallet::Wallet; use key_wallet::Network; +use platform_wallet::changeset::provider_key_account::{ + rebuild_provider_key_account, ProviderAccountRebuildError, +}; use platform_wallet::changeset::{AccountRegistrationEntry, CoreChangeSet}; -use crate::sqlite::provider_accounts::{ - insert_platform_node_pool_entry, rebuild_provider_key_account, PlatformNodePoolError, - ProviderAccountRebuildError, -}; +use crate::sqlite::provider_accounts::{insert_platform_node_pool_entry, PlatformNodePoolError}; use crate::sqlite::load_ctx::{LoadCtx, LoadSite, SiteCoords}; use crate::sqlite::schema::accounts::{self, AccountManifest}; @@ -949,6 +949,98 @@ mod tests { assert!(matches!(err, WalletStorageError::MissingAccount { .. })); } + fn provider_keys( + w: &Wallet, + ) -> ( + key_wallet::derivation_bls_bip32::ExtendedBLSPubKey, + key_wallet::derivation_slip10::ExtendedEd25519PubKey, + ) { + let bls = w + .accounts + .bls_account_of_type(AccountType::ProviderOperatorKeys) + .expect("Default-created wallet has a BLS provider account") + .bls_public_key + .clone(); + let eddsa = w + .accounts + .eddsa_account_of_type(AccountType::ProviderPlatformKeys) + .expect("Default-created wallet has an EdDSA provider account") + .ed25519_public_key + .clone(); + (bls, eddsa) + } + + #[test] + fn watch_only_rebuild_restores_provider_key_accounts() { + use platform_wallet::changeset::{ProviderKeyAccountEntry, ProviderKeyExtendedPubKey}; + + let w = Wallet::from_seed_bytes( + [3u8; 64], + Network::Testnet, + WalletAccountCreationOptions::Default, + ) + .unwrap(); + let id = w.compute_wallet_id(); + let (bls, eddsa) = provider_keys(&w); + let manifest = AccountManifest { + ecdsa: manifest_for(&w), + provider: vec![ + ProviderKeyAccountEntry { + account_type: AccountType::ProviderOperatorKeys, + extended_public_key: ProviderKeyExtendedPubKey::Bls(bls.clone()), + }, + ProviderKeyAccountEntry { + account_type: AccountType::ProviderPlatformKeys, + extended_public_key: ProviderKeyExtendedPubKey::EdDSA(eddsa.clone()), + }, + ], + }; + + let restored = build_wallet(Network::Testnet, id, &manifest).unwrap(); + + let restored_bls = restored + .accounts + .bls_account_of_type(AccountType::ProviderOperatorKeys) + .expect("BLS provider account must be rebuilt"); + assert_eq!(restored_bls.bls_public_key.to_bytes(), bls.to_bytes()); + assert_eq!(restored_bls.parent_wallet_id.as_deref(), Some(&id[..])); + assert!(restored_bls.is_watch_only); + let restored_eddsa = restored + .accounts + .eddsa_account_of_type(AccountType::ProviderPlatformKeys) + .expect("EdDSA provider account must be rebuilt"); + assert_eq!(restored_eddsa.ed25519_public_key, eddsa); + assert_eq!(restored_eddsa.parent_wallet_id.as_deref(), Some(&id[..])); + assert!(restored_eddsa.is_watch_only); + } + + #[test] + fn watch_only_rebuild_rejects_provider_curve_type_mismatch() { + use platform_wallet::changeset::{ProviderKeyAccountEntry, ProviderKeyExtendedPubKey}; + + let w = Wallet::from_seed_bytes( + [3u8; 64], + Network::Testnet, + WalletAccountCreationOptions::Default, + ) + .unwrap(); + let (bls, _) = provider_keys(&w); + let manifest = AccountManifest { + ecdsa: manifest_for(&w), + provider: vec![ProviderKeyAccountEntry { + account_type: AccountType::ProviderPlatformKeys, + extended_public_key: ProviderKeyExtendedPubKey::Bls(bls), + }], + }; + + let err = build_wallet(Network::Testnet, w.compute_wallet_id(), &manifest) + .expect_err("a BLS key must not rebuild as the platform-node account"); + assert!(matches!( + err, + WalletStorageError::ProviderKeyAccountEntryMismatch + )); + } + /// Regression: after restart-in-place the watch-only pools eagerly /// cover only `0..gap_limit`, but persisted UTXOs can sit at deeper /// derivation indices. Rehydration must extend each chain's pool to its diff --git a/packages/rs-platform-wallet/src/changeset/core_bridge.rs b/packages/rs-platform-wallet/src/changeset/core_bridge.rs index 3c7694d2e3f..35c44ecf149 100644 --- a/packages/rs-platform-wallet/src/changeset/core_bridge.rs +++ b/packages/rs-platform-wallet/src/changeset/core_bridge.rs @@ -1792,9 +1792,10 @@ fn derive_new_utxos(record: &TransactionRecord) -> Vec { /// the script and the address as independent parameters and validates /// neither. /// -/// Height and the confirmation flags describe the *previous* transaction and -/// aren't carried in `InputDetail`, so they remain defaulted on this synthetic -/// spent record (height 0, all flags false). +/// Height and the confirmation flags describe the *previous* output and +/// aren't carried in `InputDetail`, so they default (height 0, flags +/// false); `core_utxos` has no column for either, so those defaults never +/// become durable state. fn derive_spent_utxos(record: &TransactionRecord) -> Vec { record .input_details diff --git a/packages/rs-platform-wallet/src/changeset/mod.rs b/packages/rs-platform-wallet/src/changeset/mod.rs index 052b00da3a4..4125b8df568 100644 --- a/packages/rs-platform-wallet/src/changeset/mod.rs +++ b/packages/rs-platform-wallet/src/changeset/mod.rs @@ -18,6 +18,8 @@ pub mod identity_scan_state; pub mod merge; pub mod persistence_capabilities; pub mod platform_address_sync_start_state; +#[cfg(any(feature = "bls", feature = "eddsa"))] +pub mod provider_key_account; #[cfg(feature = "serde")] pub mod serde_adapters; #[cfg(feature = "shielded")] diff --git a/packages/rs-platform-wallet/src/changeset/provider_key_account.rs b/packages/rs-platform-wallet/src/changeset/provider_key_account.rs new file mode 100644 index 00000000000..44f934f868b --- /dev/null +++ b/packages/rs-platform-wallet/src/changeset/provider_key_account.rs @@ -0,0 +1,157 @@ +//! Watch-only rebuild of provider key-material accounts, shared by every +//! persistence backend's load path. + +use key_wallet::account::account_collection::AccountCollection; +use key_wallet::account::AccountType; +use key_wallet::Network; + +use crate::changeset::ProviderKeyExtendedPubKey; + +/// Why a provider key-material account could not be rebuilt into an +/// [`AccountCollection`]. +#[derive(Debug, thiserror::Error)] +pub enum ProviderAccountRebuildError { + /// The curve-specific account constructor rejected the key. + #[error("provider key account is invalid")] + Invalid(#[from] key_wallet::error::Error), + /// The collection refused the account — its `account_type` does not match + /// the curve (e.g. a BLS key offered as `ProviderPlatformKeys`). + #[error("account collection rejected the provider key account: {0}")] + Rejected(&'static str), +} + +/// Rebuild a watch-only provider account in its curve-specific collection slot. +/// +/// A BLS key becomes a `BLSAccount`, an EdDSA key an `EdDSAAccount`, both +/// parented to `wallet_id`; the account replaces whatever occupied that slot. +/// +/// # Errors +/// +/// [`ProviderAccountRebuildError::Rejected`] when `account_type` does not match +/// the key's curve (`ProviderOperatorKeys` ⇔ BLS, `ProviderPlatformKeys` ⇔ +/// EdDSA); [`ProviderAccountRebuildError::Invalid`] when the account +/// constructor rejects the key. +pub fn rebuild_provider_key_account( + accounts: &mut AccountCollection, + wallet_id: [u8; 32], + network: Network, + account_type: AccountType, + extended_public_key: &ProviderKeyExtendedPubKey, +) -> Result<(), ProviderAccountRebuildError> { + match extended_public_key { + #[cfg(feature = "bls")] + ProviderKeyExtendedPubKey::Bls(key) => { + let account = key_wallet::account::BLSAccount::new( + Some(wallet_id.to_vec()), + account_type, + key.clone(), + network, + )?; + accounts + .insert_bls_account(account) + .map_err(ProviderAccountRebuildError::Rejected) + } + #[cfg(feature = "eddsa")] + ProviderKeyExtendedPubKey::EdDSA(key) => { + let account = key_wallet::account::EdDSAAccount::new( + Some(wallet_id.to_vec()), + account_type, + key.clone(), + network, + )?; + accounts + .insert_eddsa_account(account) + .map_err(ProviderAccountRebuildError::Rejected) + } + } +} + +/// A wallet with both a BLS `ProviderOperatorKeys` account and an EdDSA +/// `ProviderPlatformKeys` account, for exercising [`rebuild_provider_key_account`]. +/// +/// Shared across crates (not just this module's own tests) so +/// `platform-wallet-storage`'s equivalent rebuild tests don't carry a second, +/// drifting copy — see `test-utils` in this crate's `Cargo.toml`. +#[cfg(any(test, feature = "test-utils"))] +pub fn provider_key_test_wallet() -> key_wallet::wallet::Wallet { + key_wallet::wallet::Wallet::from_seed_bytes( + [0x42; 64], + Network::Testnet, + key_wallet::wallet::initialization::WalletAccountCreationOptions::Default, + ) + .expect("provider key test wallet") +} + +#[cfg(all(test, feature = "bls", feature = "eddsa"))] +mod tests { + use super::*; + + #[test] + fn rebuild_provider_key_account_restores_bls_and_eddsa() { + let wallet = provider_key_test_wallet(); + let bls_key = wallet + .accounts + .bls_account_of_type(AccountType::ProviderOperatorKeys) + .expect("BLS provider account") + .bls_public_key + .clone(); + let eddsa_key = wallet + .accounts + .eddsa_account_of_type(AccountType::ProviderPlatformKeys) + .expect("EdDSA provider account") + .ed25519_public_key + .clone(); + let mut accounts = AccountCollection::new(); + let wallet_id = [0x24; 32]; + + rebuild_provider_key_account( + &mut accounts, + wallet_id, + Network::Testnet, + AccountType::ProviderOperatorKeys, + &ProviderKeyExtendedPubKey::Bls(bls_key), + ) + .expect("rebuild BLS provider account"); + rebuild_provider_key_account( + &mut accounts, + wallet_id, + Network::Testnet, + AccountType::ProviderPlatformKeys, + &ProviderKeyExtendedPubKey::EdDSA(eddsa_key), + ) + .expect("rebuild EdDSA provider account"); + + assert!(accounts + .bls_account_of_type(AccountType::ProviderOperatorKeys) + .is_some()); + assert!(accounts + .eddsa_account_of_type(AccountType::ProviderPlatformKeys) + .is_some()); + } + + #[test] + fn rebuild_provider_key_account_rejects_curve_account_type_mismatch() { + let wallet = provider_key_test_wallet(); + let bls_key = wallet + .accounts + .bls_account_of_type(AccountType::ProviderOperatorKeys) + .expect("BLS provider account") + .bls_public_key + .clone(); + let mut accounts = AccountCollection::new(); + + let error = rebuild_provider_key_account( + &mut accounts, + [0x24; 32], + Network::Testnet, + AccountType::ProviderPlatformKeys, + &ProviderKeyExtendedPubKey::Bls(bls_key), + ) + .expect_err("BLS key must not rebuild as a platform-node account"); + + assert!(matches!(error, ProviderAccountRebuildError::Rejected(_))); + assert!(accounts + .eddsa_account_of_type(AccountType::ProviderPlatformKeys) + .is_none()); + } +} diff --git a/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs b/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs index fe6da228e4d..bc211ce76cc 100644 --- a/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs +++ b/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs @@ -220,7 +220,7 @@ impl PlatformWalletManager

{ // Snapshot per-account xpubs and address-pool entries BEFORE // the wallet / managed-info are moved into insert_wallet. The // persister sees everything needed to rebuild the wallet - // watch-only (via `Wallet::new_watch_only`) plus populate + // external-signable (via `Wallet::new_external_signable`) plus populate // SwiftData's address table on next launch. let account_specs: Vec<( key_wallet::account::AccountType, diff --git a/packages/rs-platform-wallet/src/util.rs b/packages/rs-platform-wallet/src/util.rs index 32dfde95325..8cce108d660 100644 --- a/packages/rs-platform-wallet/src/util.rs +++ b/packages/rs-platform-wallet/src/util.rs @@ -11,3 +11,13 @@ pub(crate) fn now_ms() -> u64 { .map(|d| d.as_millis() as u64) .unwrap_or(0) } + +/// Current wall-clock time in seconds since the Unix epoch. +/// +/// Pre-epoch reads return `0`, which upstream never expires. +pub(crate) fn now_secs() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) +} diff --git a/packages/rs-platform-wallet/src/wallet/identity/network/contact_requests.rs b/packages/rs-platform-wallet/src/wallet/identity/network/contact_requests.rs index ab659089118..8f95fed9279 100644 --- a/packages/rs-platform-wallet/src/wallet/identity/network/contact_requests.rs +++ b/packages/rs-platform-wallet/src/wallet/identity/network/contact_requests.rs @@ -2877,10 +2877,7 @@ impl DashPayView<'_, B> { return 0; } - let now_secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); + let now_secs = crate::util::now_secs(); let mut cleared: Vec = Vec::new(); // Permanent verify failures to mark so the sync sweep's enqueue gate diff --git a/packages/rs-platform-wallet/src/wallet/platform_wallet.rs b/packages/rs-platform-wallet/src/wallet/platform_wallet.rs index 982e6b6323e..6b69ae6042e 100644 --- a/packages/rs-platform-wallet/src/wallet/platform_wallet.rs +++ b/packages/rs-platform-wallet/src/wallet/platform_wallet.rs @@ -2583,23 +2583,24 @@ mod shield_input_selection_tests { #[test] fn regression_reports_max_from_usable_suffix_not_total_account_balance() { - // Real account snapshot: the leading address is below the reserve, so + // Account snapshot whose leading address cannot pay the fee, so // capacity must come from the usable suffix, not the account total. - assert!( - 197_264_780 <= reserve(), - "regression shape requires the leading address to stay below the reserve; \ - re-seed the balances if the versioned reserve drops under 197_264_780" - ); + // The leading balance is derived from the reserve — one credit below + // the strict `> reserve` viability threshold, the largest balance that + // must still be rejected as input 0 — so the shape holds whatever the + // versioned fee schedule does next. + let dust = reserve() - 1; + let usable = 3_623_849_220; let candidates = vec![ - (addr(1), 197_264_780), + (addr(1), dust), (addr(2), 2_000_000_000), (addr(3), 1_623_849_220), ]; let plan = plan(candidates).unwrap(); - let expected_max = 3_623_849_220 - reserve(); + let expected_max = usable - reserve(); - assert_eq!(plan.preflight.account_balance_credits, 3_821_114_000); - assert_eq!(plan.preflight.usable_balance_credits, 3_623_849_220); + assert_eq!(plan.preflight.account_balance_credits, dust + usable); + assert_eq!(plan.preflight.usable_balance_credits, usable); assert_eq!(plan.preflight.fee_reserve_credits, reserve()); assert_eq!(plan.preflight.max_shieldable_credits, expected_max); assert!(plan.preflight.can_shield); @@ -2609,11 +2610,13 @@ mod shield_input_selection_tests { assert!(!chosen.contains_key(&addr(1))); assert_eq!(chosen.values().sum::(), expected_max); + // `available` reports the usable suffix, never the account total — + // the whole point of the regression. let err = plan.select_inputs(expected_max + 1).unwrap_err(); assert!(matches!( err, PlatformWalletError::PlatformShieldCapacityExceeded { available, required } - if available == 3_623_849_220 && required == 3_623_849_221 + if available == usable && required == usable + 1 )); } diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift index 540cf0adb8c..9363dbb02d4 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift @@ -651,6 +651,9 @@ public class WalletManager { /// Import a wallet from serialized bytes /// - Parameters: /// - walletBytes: The serialized wallet data + /// - birthHeight: Block height to start scanning from. Defaults to 0 + /// (genesis), a safe full rescan; pass the wallet's known birth + /// height to skip pre-birth blocks. /// - Returns: The wallet ID of the imported wallet public func importWallet(from walletBytes: Data, birthHeight: UInt32 = 0) throws -> Data { guard !walletBytes.isEmpty else { diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift index c9034521fce..77908039238 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift @@ -1629,6 +1629,10 @@ public class PlatformWalletManager: ObservableObject { for walletId in walletIds { guard walletId.count == 32 else { continue } + // A wallet Rust declined to register (corrupt/skipped row) is + // still listed by SwiftData; `get_wallet` returns NotFound for + // it, which the do/catch below logs to `lastError` and skips — + // one bad row never fails the whole restore. var walletHandle: Handle = NULL_HANDLE do { try walletId.withUnsafeBytes { idPtr in diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift index e5e67eb4ab1..dbda3c8fefe 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift @@ -6591,7 +6591,7 @@ public final class PlatformWalletPersistenceHandler: @unchecked Sendable { /// A wallet is "restorable" when it has at least one /// `PersistentAccount` row with non-empty /// `accountExtendedPubKeyBytes`. The Rust side reconstructs the - /// watch-only `Wallet` via `Wallet::new_watch_only(network, + /// external-signable `Wallet` via `Wallet::new_external_signable(network, /// wallet_id, accounts)`; accounts come directly from the spec /// array, wallet id from the top-level struct. /// diff --git a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/ErrorHandlingTests.swift b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/ErrorHandlingTests.swift index 1c48b1e0b2d..b4cf8f404f5 100644 --- a/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/ErrorHandlingTests.swift +++ b/packages/swift-sdk/SwiftTests/SwiftDashSDKTests/ErrorHandlingTests.swift @@ -687,6 +687,13 @@ final class ErrorHandlingTests: XCTestCase { // MARK: - Core broadcast outcome mapping func testCoreBroadcastOutcomeMapping() throws { + XCTAssertEqual(PlatformWalletResultCode.errorTransactionBroadcastRejected.rawValue, 26) + XCTAssertEqual( + PlatformWalletResultCode( + ffi: PLATFORM_WALLET_FFI_RESULT_CODE_ERROR_TRANSACTION_BROADCAST_REJECTED + ), + .errorTransactionBroadcastRejected + ) XCTAssertEqual( try CoreTransactionBroadcastOutcome( resultCode: .success,