From 848fdc82c957311eae4ed1895edf229b488560fe Mon Sep 17 00:00:00 2001 From: xdustinface Date: Sun, 26 Apr 2026 00:44:28 +1000 Subject: [PATCH] feat(key-wallet): seed sync checkpoint from `birth_height` Move sync checkpoint seeding into `ManagedWalletInfo` construction so it can't be forgotten, and let callers control the scan range when importing. - `WalletInfoInterface::from_wallet` now takes `birth_height` and seeds `synced_height` and `last_processed_height` to `birth_height.saturating_sub(1)`. Previously every wallet-add path had to remember to call `set_birth_height` separately, and forgetting it dragged `WalletManager::synced_height` (a min across wallets) back to genesis on every add. - `WalletManager::import_wallet_from_extended_priv_key`, `import_wallet_from_xpub`, and `import_wallet_from_bytes` (plus the matching FFI) gain a `birth_height` parameter so previously-used keys can rescan from a chosen height instead of being silently anchored at the chain tip. - Unused `ManagedWalletInfo::with_birth_height` removed. --- dash-spv-ffi/tests/test_wallet_manager.rs | 1 + key-wallet-ffi/FFI_API.md | 2 +- key-wallet-ffi/src/wallet_manager.rs | 5 ++++- .../src/wallet_manager_serialization_tests.rs | 1 + key-wallet-ffi/src/wallet_manager_tests.rs | 2 ++ key-wallet-ffi/tests/test_import_wallet.rs | 1 + key-wallet-manager/src/lib.rs | 21 +++++++++++++------ .../tests/test_serialized_wallets.rs | 11 +++++++--- 8 files changed, 33 insertions(+), 11 deletions(-) diff --git a/dash-spv-ffi/tests/test_wallet_manager.rs b/dash-spv-ffi/tests/test_wallet_manager.rs index b6ecb0d7a..9e04908ff 100644 --- a/dash-spv-ffi/tests/test_wallet_manager.rs +++ b/dash-spv-ffi/tests/test_wallet_manager.rs @@ -93,6 +93,7 @@ mod tests { wallet_manager_ptr, serialized_wallet.as_ptr(), serialized_wallet.len(), + 0, imported_wallet_id.as_mut_ptr(), &mut error as *mut FFIError, ); diff --git a/key-wallet-ffi/FFI_API.md b/key-wallet-ffi/FFI_API.md index 60018fdfd..4cd96c44a 100644 --- a/key-wallet-ffi/FFI_API.md +++ b/key-wallet-ffi/FFI_API.md @@ -678,7 +678,7 @@ Get wallet IDs # Safety - `manager` must be a valid pointer to an FFIWalletMan #### `wallet_manager_import_wallet_from_bytes` ```c -wallet_manager_import_wallet_from_bytes(manager: *mut FFIWalletManager, wallet_bytes: *const u8, wallet_bytes_len: usize, wallet_id_out: *mut u8, error: *mut FFIError,) -> bool +wallet_manager_import_wallet_from_bytes(manager: *mut FFIWalletManager, wallet_bytes: *const u8, wallet_bytes_len: usize, birth_height: u32, wallet_id_out: *mut u8, error: *mut FFIError,) -> bool ``` **Module:** `wallet_manager` diff --git a/key-wallet-ffi/src/wallet_manager.rs b/key-wallet-ffi/src/wallet_manager.rs index e2736b26b..2cb96add8 100644 --- a/key-wallet-ffi/src/wallet_manager.rs +++ b/key-wallet-ffi/src/wallet_manager.rs @@ -278,6 +278,8 @@ pub unsafe extern "C" fn wallet_manager_free_wallet_bytes(wallet_bytes: *mut u8, /// - `manager` must be a valid pointer to an FFIWalletManager instance /// - `wallet_bytes` must be a valid pointer to bincode-serialized wallet bytes /// - `wallet_bytes_len` must be the exact length of the wallet bytes +/// - `birth_height` is the birth height to seed the wallet's sync checkpoint +/// (0 to rescan from genesis) /// - `wallet_id_out` must be a valid pointer to a 32-byte array that will receive the wallet ID /// - `error` must be a valid pointer to an FFIError structure /// - The caller must ensure all pointers remain valid for the duration of this call @@ -287,6 +289,7 @@ pub unsafe extern "C" fn wallet_manager_import_wallet_from_bytes( manager: *mut FFIWalletManager, wallet_bytes: *const u8, wallet_bytes_len: usize, + birth_height: u32, wallet_id_out: *mut u8, error: *mut FFIError, ) -> bool { @@ -299,7 +302,7 @@ pub unsafe extern "C" fn wallet_manager_import_wallet_from_bytes( // Import the wallet using async runtime let result = manager_ref.runtime.block_on(async { let mut manager_guard = manager_ref.manager.write().await; - manager_guard.import_wallet_from_bytes(wallet_bytes_slice) + manager_guard.import_wallet_from_bytes(wallet_bytes_slice, birth_height) }); let wallet_id = unwrap_or_return!(result, error); diff --git a/key-wallet-ffi/src/wallet_manager_serialization_tests.rs b/key-wallet-ffi/src/wallet_manager_serialization_tests.rs index df78f06a6..7f0ac983d 100644 --- a/key-wallet-ffi/src/wallet_manager_serialization_tests.rs +++ b/key-wallet-ffi/src/wallet_manager_serialization_tests.rs @@ -196,6 +196,7 @@ mod tests { manager2, wallet_bytes_out, wallet_bytes_len_out, + 0, imported_wallet_id.as_mut_ptr(), error, ) diff --git a/key-wallet-ffi/src/wallet_manager_tests.rs b/key-wallet-ffi/src/wallet_manager_tests.rs index 76abf7e48..7a8239582 100644 --- a/key-wallet-ffi/src/wallet_manager_tests.rs +++ b/key-wallet-ffi/src/wallet_manager_tests.rs @@ -925,6 +925,7 @@ mod tests { manager3, wallet_bytes_slice.as_ptr(), wallet_bytes_slice.len(), + 0, import_wallet_id_out.as_mut_ptr(), error, ) @@ -1081,6 +1082,7 @@ mod tests { manager2, wallet_bytes_copy.as_ptr(), wallet_bytes_copy.len(), + 0, import_wallet_id_out.as_mut_ptr(), error, ) diff --git a/key-wallet-ffi/tests/test_import_wallet.rs b/key-wallet-ffi/tests/test_import_wallet.rs index 33a4c2103..ee5fc3ecf 100644 --- a/key-wallet-ffi/tests/test_import_wallet.rs +++ b/key-wallet-ffi/tests/test_import_wallet.rs @@ -58,6 +58,7 @@ mod tests { manager2, ptr::null(), 0, + 0, imported_wallet_id.as_mut_ptr(), &mut error, ); diff --git a/key-wallet-manager/src/lib.rs b/key-wallet-manager/src/lib.rs index 874d39020..facf471d6 100644 --- a/key-wallet-manager/src/lib.rs +++ b/key-wallet-manager/src/lib.rs @@ -307,6 +307,7 @@ impl WalletManager { /// /// # Arguments /// * `xprv` - The extended private key string (base58check encoded) + /// * `birth_height` - Birth height for wallet scanning (0 to sync from genesis) /// * `account_creation_options` - Specifies which accounts to create during initialization /// /// # Returns @@ -315,6 +316,7 @@ impl WalletManager { pub fn import_wallet_from_extended_priv_key( &mut self, xprv: &str, + birth_height: CoreBlockHeight, account_creation_options: key_wallet::wallet::initialization::WalletAccountCreationOptions, ) -> Result { // Parse the extended private key @@ -334,7 +336,7 @@ impl WalletManager { } // Create managed wallet info - let managed_info = T::from_wallet(&wallet, self.last_processed_height()); + let managed_info = T::from_wallet(&wallet, birth_height); self.wallets.insert(wallet_id, wallet); self.wallet_infos.insert(wallet_id, managed_info); @@ -349,6 +351,7 @@ impl WalletManager { /// /// # Arguments /// * `xpub` - The extended public key string (base58check encoded) + /// * `birth_height` - Birth height for wallet scanning (0 to sync from genesis) /// * `can_sign_externally` - If true, creates an externally signable wallet (e.g., for hardware wallets). /// If false, creates a pure watch-only wallet. /// @@ -358,6 +361,7 @@ impl WalletManager { pub fn import_wallet_from_xpub( &mut self, xpub: &str, + birth_height: CoreBlockHeight, can_sign_externally: bool, ) -> Result { // Parse the extended public key @@ -380,7 +384,7 @@ impl WalletManager { } // Create managed wallet info - let managed_info = T::from_wallet(&wallet, self.last_processed_height()); + let managed_info = T::from_wallet(&wallet, birth_height); self.wallets.insert(wallet_id, wallet); self.wallet_infos.insert(wallet_id, managed_info); @@ -394,8 +398,14 @@ impl WalletManager { /// This is useful for restoring wallets from backups or transferring wallets /// between systems. /// + /// The serialized form contains only the immutable `Wallet`, not the mutable + /// `ManagedWalletInfo`, so the caller must supply a `birth_height` to seed the + /// new wallet's sync checkpoint. Pass 0 to rescan from genesis, or the chain + /// tip if no historical scan is desired. + /// /// # Arguments /// * `wallet_bytes` - The bincode-serialized wallet bytes + /// * `birth_height` - Birth height for wallet scanning (0 to sync from genesis) /// /// # Returns /// * `Ok(WalletId)` - The computed wallet ID of the imported wallet @@ -404,6 +414,7 @@ impl WalletManager { pub fn import_wallet_from_bytes( &mut self, wallet_bytes: &[u8], + birth_height: CoreBlockHeight, ) -> Result { // Deserialize the wallet from bincode let wallet: Wallet = bincode::decode_from_slice(wallet_bytes, bincode::config::standard()) @@ -420,10 +431,8 @@ impl WalletManager { return Err(WalletError::WalletExists(wallet_id)); } - // Create managed wallet info from the imported wallet, using the manager's - // current aggregated last-processed height as the fallback birth height - // since the serialized form does not preserve it. - let managed_info = T::from_wallet(&wallet, self.last_processed_height()); + // Create managed wallet info from the imported wallet + let managed_info = T::from_wallet(&wallet, birth_height); self.wallets.insert(wallet_id, wallet); self.wallet_infos.insert(wallet_id, managed_info); diff --git a/key-wallet-manager/tests/test_serialized_wallets.rs b/key-wallet-manager/tests/test_serialized_wallets.rs index edcdab694..1bc7742b9 100644 --- a/key-wallet-manager/tests/test_serialized_wallets.rs +++ b/key-wallet-manager/tests/test_serialized_wallets.rs @@ -64,10 +64,15 @@ mod tests { assert_eq!(wallet_id, wallet_id3); println!("Externally signable wallet ID: {}", hex::encode(wallet_id3)); - // Test 4: Import the serialized wallet back + // Test 4: Import the serialized wallet back with a specific birth height let mut manager4 = WalletManager::::new(Network::Testnet); - let import_result = manager4.import_wallet_from_bytes(&bytes); + let import_result = manager4.import_wallet_from_bytes(&bytes, 50_000); assert!(import_result.is_ok()); - assert_eq!(import_result.unwrap(), wallet_id); + let imported_id = import_result.unwrap(); + assert_eq!(imported_id, wallet_id); + let imported = manager4.get_wallet_info(&imported_id).unwrap(); + assert_eq!(imported.birth_height(), 50_000); + assert_eq!(imported.synced_height(), 49_999); + assert_eq!(imported.last_processed_height(), 49_999); } }