Repository navigation
Description
Expected Behavior
A deferred contact-crypto drain runs against one wallet generation. If that generation is removed or replaced while the drain is in flight, the drain stops before it registers anything in the replacement.
Current Behavior
drain_pending_contact_crypto_verified keys every step on wallet_id, not on a generation:
- it reads the queue from the wallet currently registered under that id;
- it verifies the provider against the same wallet;
- after awaiting provider derivation and contact fetches,
register_contact_accountandregister_external_contact_accountwrite to whichever wallet is registered under the id at that moment.
No generation lifecycle guard is taken first. Callers:
reserve_payment_address—self.drain_pending_contact_crypto_verified(provider, None) send_payment—self.drain_pending_contact_crypto_verified(provider, None) - startup —
.drain_pending_contact_crypto_verified( register_contact_account—platform/packages/rs-platform-wallet/src/wallet/identity/network/contacts.rs
Lines 202 to 244 in 13c8b0a
pub async fn register_contact_account( &self, our_identity_id: &Identifier, contact_identity_id: &Identifier, account_index: u32, // Our receiving (friendship) xpub, derived by the Keychain signer. There // is no resident-seed path — every caller supplies it. account_xpub: key_wallet::bip32::ExtendedPubKey, ) -> Result<(), PlatformWalletError> { let account_type = AccountType::DashpayReceivingFunds { index: account_index, user_identity_id: our_identity_id.to_buffer(), friend_identity_id: contact_identity_id.to_buffer(), }; // Derive the account xpub and add to both Wallet and ManagedWalletInfo let mut wm = self.wallet_manager.write().await; // Early-exit if the account already exists — keeps the recurring // sweep's re-registration a true no-op (no duplicate persistence // round, no managed-state churn). { use key_wallet::account::account_collection::DashpayAccountKey; let info = wm .get_wallet_info(&self.wallet_id) .ok_or_else(|| PlatformWalletError::WalletNotFound(hex::encode(self.wallet_id)))?; let key = DashpayAccountKey { index: account_index, user_identity_id: our_identity_id.to_buffer(), friend_identity_id: contact_identity_id.to_buffer(), }; if info .core_wallet .accounts .dashpay_receival_accounts .contains_key(&key) { return Ok(()); } } let wallet = wm .get_wallet(&self.wallet_id) prepare_payment, added in feat(platform-wallet)!: sign a DashPay contact payment before broadcasting it #5465 —platform/packages/rs-platform-wallet/src/wallet/identity/network/prepared_payment.rs
Line 217 in dfbeb7d
self.drain_pending_contact_crypto_verified(provider, None)
If a wallet is removed and re-registered under the same id while a drain is awaiting the provider, the drain finishes in the new generation. The payment paths reject the stale core handle only afterwards.
The impact is limited. A wallet can be re-registered under the same id only if it has the same root key (compute_wallet_id), so the accounts the drain installs are the ones the new generation would derive itself, and a registration that already exists is a no-op. The concern is lifecycle discipline: an operation that started on one generation should not write into another.
Possible Solution
Pass the originating generation into the drain, as the payment paths now do with their Option<&Arc<WalletGeneration>>. Then:
- re-check it is current before each registration and stop when it is not; or
- take the generation's lifecycle guard around each registration, not across the provider work, so a teardown never waits on a slow host resolver.
Steps to Reproduce
Needs a test hook: block the contact-crypto provider inside a drain, remove the wallet, register it again from the same seed, then release the provider. The new generation receives contact-account registrations from the old drain.
Context
Raised by the review bot on #5465. It is not specific to that PR: every caller listed above has the same ordering on v5.1-dev.
Your Environment
platform v5.1-dev @ 13c8b0a; #5465 @ dfbeb7d.
🤖 Generated with Claude Code
Activity