From 55d241b6c972887b985e20a8dbf8e83fea549afe Mon Sep 17 00:00:00 2001 From: xdustinface Date: Thu, 2 Jul 2026 21:21:06 +1000 Subject: [PATCH] fix(key-wallet): preserve UTXO flags across reprocessing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `update_utxos` rebuilds each output with a fresh `Utxo::new` on every reprocess, so a mempool→block confirmation silently reset `is_instantlocked` (and `is_trusted` and any coin reservation) back to false. An InstantSend lock is permanent for a txid per DIP-0010 and trust only ever settles, so both now latch monotonically when an entry for the outpoint already exists, and the coin-reservation flag is carried through unchanged. `is_confirmed` stays freshly derived so a reorg can still downgrade it. Addresses CodeRabbit review comment on PR #836 https://github.com/dashpay/rust-dashcore/pull/836#discussion_r3512506949 --- .../managed_account/managed_core_funds_account.rs | 12 ++++++++++++ .../src/transaction_checking/wallet_checker.rs | 2 ++ 2 files changed, 14 insertions(+) diff --git a/key-wallet/src/managed_account/managed_core_funds_account.rs b/key-wallet/src/managed_account/managed_core_funds_account.rs index cf95d78d1..b073dc2c9 100644 --- a/key-wallet/src/managed_account/managed_core_funds_account.rs +++ b/key-wallet/src/managed_account/managed_core_funds_account.rs @@ -202,6 +202,18 @@ impl ManagedCoreFundsAccount { utxo.is_instantlocked = matches!(context, TransactionContext::InstantSend(_)); utxo.is_trusted = is_trusted_output; + // Reprocessing (e.g. mempool→block) rebuilds this UTXO from + // scratch, so carry forward flags that must not regress. An + // InstantSend lock is permanent for a txid (DIP-0010) and + // trust only ever settles, so both latch monotonically. A + // coin reservation is orthogonal to chain context and is kept + // as-is. `is_confirmed` stays freshly derived so a reorg can + // still downgrade it. + if let Some(prior) = self.utxos.get(&outpoint) { + utxo.is_instantlocked |= prior.is_instantlocked; + utxo.is_trusted |= prior.is_trusted; + utxo.is_locked = prior.is_locked; + } self.utxos.insert(outpoint, utxo); utxos_changed = true; } diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index b62def243..6d8c7591f 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -958,6 +958,8 @@ mod tests { assert!(ctx.transaction(&txid).is_confirmed()); assert_eq!(ctx.transaction(&txid).height(), Some(1000)); assert!(ctx.first_utxo().is_confirmed); + // The earlier IS lock must survive the mempool→block reprocess. + assert!(ctx.first_utxo().is_instantlocked, "IS-lock flag must not be lost on confirmation"); assert_eq!(ctx.managed_wallet.balance.spendable(), 200_000); // Stage 4: chain-locked block (rescan with stronger context)