Skip to content

fix(key-wallet): out-of-order UTXO spend recorded nowhere — phantom unspent balance (#649)#909

Open
bfoss765 wants to merge 7 commits into
dashpay:devfrom
bfoss765:fix/wallet-utxo-spend-not-marked-649
Open

fix(key-wallet): out-of-order UTXO spend recorded nowhere — phantom unspent balance (#649)#909
bfoss765 wants to merge 7 commits into
dashpay:devfrom
bfoss765:fix/wallet-utxo-spend-not-marked-649

Conversation

@bfoss765

@bfoss765 bfoss765 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #649. When a spending transaction is processed before its funding transaction (a normal ordering during compact-filter rescans and mempool-chained activity), the wallet does not yet own the spent input, classifies the spend as irrelevant, and records it nowhere. The funding tx is then inserted later as a fresh, spendable UTXO — a phantom balance that deterministically survives full from-seed rescans.

Basis: PR #851

The core mechanism here is adopted from @lklimek's #851 (a wallet-level observed_spent_outpoints map recording every block-observed spend independent of classification) — that design is correct and this PR builds directly on it. On top of the #851 core, this PR adds what our on-device validation showed was needed:

  • A deterministic failing repro (out_of_order_spend_repro_test.rs): funds an address, then delivers the spend block before the funding block — fails on current dev, passes with the fix. Plus two regression tests through the public WalletManager API (multi-wallet, large-block stress).
  • Late-added-account rewind: accounts added after ranges were committed (e.g. DIP-15 friend chains on DashPay wallets) get a sync-checkpoint rewind so their spends aren't missed.
  • Commit-time scan-contiguity guard in dash-spv.
  • AddressPool.script_pubkey_index cleanup in prune_unused.
  • Bounding: prune_finalized_observed_spends caps the set at the finality boundary.

On-device validation (Android, testnet)

  • Bug reproduced in production use: a day-old wallet with rapid mempool-chained activity developed a deterministic phantom +0.01 DASH (outpoint 2febe5d7e8ad1dd0fb633004a82a24783d9b2e9095883541576e5f1344eb9975:0, spent per dashj, unspent per dash-spv) that survived a full wallet rebuild from seed — matching this root cause exactly.
  • Fix validated twice on device: (1) the affected wallet rebuilt clean — balance parity with dashj restored to the duff; (2) a 1,049-transaction heavy-CoinJoin wallet restored from seed under the fixed engine — final balance exact to the duff against an independently-synced dashj baseline (12.58712954 DASH), ~8-minute scan.

Tests

  • New: 1 repro + 2 regression tests (red→green).
  • Full suites: key-wallet 549 pass / 0 fail, key-wallet-manager all pass, dash-spv lib 482 pass / 0 fail (incl. 3 new contiguity-guard tests).

Relationship to #851

This PR supersedes #851 with the same core design plus the tests and hardening above; recommending #851 be closed in its favor (comment posted there). Design credit to @lklimek.

Summary by CodeRabbit

  • Bug Fixes
    • Prevented observed-spent outputs from reappearing during out-of-order rescans and noisy block processing, including funding-first ordering.
    • Improved isolation of the observed-spent guard across multiple wallets.
    • Prevented synced checkpoint advancement when wallet account sets change mid-scan, avoiding gap certification after rewinds.
    • Ensured sync certificates are invalidated when new accounts are added.
    • Cleared stale script_pubkey index entries during address pruning.
  • Tests
    • Added new integration stress/regression tests and expanded unit coverage for observed-spent outpoints and sync/filter guard behavior.

bfoss765 and others added 2 commits July 20, 2026 18:00
…efore-funding (dashpay#649)

A spend processed BEFORE the transaction that funded the UTXO it spends
(out-of-order block delivery during a cold rescan) leaves that UTXO
permanently in the wallet's tracked set, producing phantom spendable
balance.

Device evidence (testnet): output
2febe5d7e8ad1dd0fb633004a82a24783d9b2e9095883541576e5f1344eb9975:0
(1,000,000 duffs) is counted unspent by the SDK while dashj has it spent,
and the phantom +0.01 survives a full wallet rebuild from seed (fresh
re-derivation + rescan reproduces it deterministically), proving the miss
lives in the scan/processing path, not just live mempool ingestion.

This test models that scenario at the WalletManager level: fund 1,000,000
duffs to a wallet address, then deliver the spending block (height 200)
BEFORE the funding block (height 100). It asserts the funding outpoint is
NOT still tracked afterward. FAILS on the current pin (which already
contains dashpay#837/dashpay#864/dashpay#891/dashpay#893) — those do not address this defect.

Refs: dashpay#649

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ashpay#649

Root cause: the "already spent" guard in
managed_core_funds_account.rs::update_utxos keys off the ACCOUNT-LOCAL
`spent_outpoints` set, which is only populated when the account itself
processes the spending transaction. When a spend is delivered before its
funding tx (out-of-order rescan), the wallet does not yet own the input,
so the spend is classified as irrelevant, update_utxos never runs for it,
and nothing records the spend. When the funding tx is processed later, the
output is (re-)inserted as a fresh, spendable UTXO -> phantom balance that
survives a full from-seed rescan.

Fix (adapted from dashpay#851): record every spend observed
in a block into a new wallet-level `observed_spent_outpoints` map
(ManagedWalletInfo), independent of the spending tx's classification or
account attribution. update_utxos and record_transaction consult this map:

  - update_utxos skips any output already observed spent (spend-first
    ordering: funding arrives after the spend).
  - remove_spent_from_accounts drops a coin the matched-account path
    missed (funding-first ordering: spend routed to another account).
  - TransactionRecord::compensate_for_observed_spends keeps net_amount /
    output_details consistent with the observed spend (declarative, so
    it is idempotent across rescan replays).

The set is bounded-permanent: entries are evicted by
prune_finalized_observed_spends once the spend height is provably final
(<= min(chainlock height, synced_height)); add-account rewinds the sync
checkpoint so a late account gets filter coverage before pruning can run.
A dash-spv commit-time contiguity guard keeps a mid-flight account-add
rescan from being silently clobbered forward.

Also fixes AddressPool::prune_unused to clear script_pubkey_index
alongside address_index.

Adds manager-level regression tests (multi-wallet, large-block stress)
that exercise the fix through the public WalletManager API.

The repro test from the previous commit now passes; key-wallet (549),
key-wallet-manager (all) and dash-spv lib (482) suites are green. The
pre-existing masternode-network integration failures
(test_utils/masternode_network.rs:106) are unrelated and fail identically
on the clean pin.

Refs: dashpay#649
Adapted-from: dashpay#851

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3b11f8a9-645f-43ab-a9ca-54287f17f62a

📥 Commits

Reviewing files that changed from the base of the PR and between ebcd40a and 3723925.

📒 Files selected for processing (15)
  • dash-spv/src/sync/filters/batch.rs
  • dash-spv/src/sync/filters/manager.rs
  • key-wallet-manager/src/lib.rs
  • key-wallet-manager/src/process_block.rs
  • key-wallet-manager/src/test_utils/mock_wallet.rs
  • key-wallet-manager/src/wallet_interface.rs
  • key-wallet/src/managed_account/managed_account_ref.rs
  • key-wallet/src/managed_account/managed_core_funds_account.rs
  • key-wallet/src/managed_account/transaction_record.rs
  • key-wallet/src/tests/address_pool_tests.rs
  • key-wallet/src/tests/mod.rs
  • key-wallet/src/tests/observed_spent_outpoints_tests.rs
  • key-wallet/src/transaction_checking/wallet_checker.rs
  • key-wallet/src/wallet/managed_wallet_info/mod.rs
  • key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • key-wallet/src/managed_account/transaction_record.rs
  • key-wallet/src/wallet/managed_wallet_info/mod.rs
  • key-wallet/src/managed_account/managed_core_funds_account.rs
  • key-wallet/src/transaction_checking/wallet_checker.rs

📝 Walkthrough

Walkthrough

The PR records observed spent outpoints across wallet and account processing, prevents spent UTXO resurrection during out-of-order blocks, rewinds checkpoints after account additions, and guards per-wallet sync advancement with generation and contiguity checks.

Changes

Observed spend protection

Layer / File(s) Summary
Observed-spend state and compensation
key-wallet/src/wallet/managed_wallet_info/mod.rs, key-wallet/src/managed_account/transaction_record.rs, key-wallet/src/managed_account/managed_core_funds_account.rs
Wallets persist observed spends, prune finalized entries, compensate transaction records, and rebuild account spend state.
Account and wallet transaction flow
key-wallet/src/managed_account/managed_account_ref.rs, key-wallet/src/transaction_checking/wallet_checker.rs, key-wallet-manager/src/lib.rs
Observed spends propagate through transaction APIs; spent UTXOs are removed or skipped, records are rewritten, and wallet results include those updates.
Observed-spend regression coverage
key-wallet-manager/tests/*, key-wallet/src/tests/*, key-wallet/src/managed_account/transaction_record.rs
Tests cover out-of-order spends, wallet isolation, noisy blocks, persistence, pruning, compensation, and state-change behavior.

Sync checkpoint consistency

Layer / File(s) Summary
Account-add checkpoint invalidation
key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs, key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs
Managed account creation rewinds the wallet checkpoint and exposes account-generation state.
Generation-aware batch commits
dash-spv/src/sync/filters/batch.rs, dash-spv/src/sync/filters/manager.rs, key-wallet-manager/src/wallet_interface.rs, key-wallet-manager/src/process_block.rs
Scans snapshot each wallet’s account generation, and commits advance only matching wallets whose ranges are contiguous.

Address pool index cleanup

Layer / File(s) Summary
Pruned address index cleanup
key-wallet/src/managed_account/address_pool.rs, key-wallet/src/tests/address_pool_tests.rs
Pruning removes stale script public key index entries alongside address mappings.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WalletTransactionChecker
  participant ManagedWalletInfo
  participant ManagedCoreFundsAccount
  participant TransactionRecord
  WalletTransactionChecker->>ManagedWalletInfo: record block spends
  WalletTransactionChecker->>ManagedCoreFundsAccount: process transaction with observed spends
  ManagedCoreFundsAccount->>ManagedCoreFundsAccount: skip spent UTXOs
  ManagedCoreFundsAccount->>TransactionRecord: compensate spent outputs
  ManagedWalletInfo->>ManagedCoreFundsAccount: remove spent account outputs
Loading

Possibly related issues

Possibly related PRs

Suggested labels: ready-for-review

Suggested reviewers: xdustinface, quantumexplorer, zocolini

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The address_pool script_pubkey_index cleanup and its test are unrelated to #649's out-of-order spend fix. Split the address-pool cleanup into a separate PR unless it is required for the #649 fix.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and matches the main bug fix for out-of-order UTXO spend handling in #649.
Linked Issues check ✅ Passed The observed-spend tracking, late rewind guard, and tests address #649's out-of-order spend bug.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
key-wallet/src/transaction_checking/wallet_checker.rs (1)

63-93: 🚀 Performance & Scalability | 🔵 Trivial

Scalability note: per-transaction recording/removal is un-gated over the whole matched block.

Every transaction in a matched block (including thousands of wallet-irrelevant ones) runs record_observed_spends (one insert per input) and, on this path, remove_spent_from_accounts, which allocates an all_funding_accounts_mut() Vec per call and scans accounts×inputs. During a large cold rescan synced_height stays low, so prune_finalized_observed_spends defers, letting observed_spent_outpoints grow with total block inputs across all matched blocks (bounded only by the 10M deser cap). Consider short-circuiting when there are no funding accounts/UTXOs to touch, or hoisting the account-handle fetch out of the per-tx loop, to keep busy-block/rescan cost bounded. Correctness is fine; this is about steady-state cost and memory during heavy rescans.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@key-wallet/src/transaction_checking/wallet_checker.rs` around lines 63 - 93,
Bound the per-transaction work in the block-checking flow by skipping
record_observed_spends and remove_spent_from_accounts when the wallet has no
funding accounts or UTXOs that can be affected. Reuse or hoist the
funding-account handles instead of allocating all_funding_accounts_mut() for
every transaction, while preserving spend tracking and account removal whenever
relevant funding exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@key-wallet/src/transaction_checking/wallet_checker.rs`:
- Around line 63-93: Bound the per-transaction work in the block-checking flow
by skipping record_observed_spends and remove_spent_from_accounts when the
wallet has no funding accounts or UTXOs that can be affected. Reuse or hoist the
funding-account handles instead of allocating all_funding_accounts_mut() for
every transaction, while preserving spend tracking and account removal whenever
relevant funding exists.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 274499ff-97d6-4dbb-b715-d929b750a8ff

📥 Commits

Reviewing files that changed from the base of the PR and between 19690d3 and ebcd40a.

📒 Files selected for processing (13)
  • dash-spv/src/sync/filters/manager.rs
  • key-wallet-manager/tests/common/mod.rs
  • key-wallet-manager/tests/observed_spent_large_block_stress_test.rs
  • key-wallet-manager/tests/observed_spent_multi_wallet_test.rs
  • key-wallet-manager/tests/out_of_order_spend_repro_test.rs
  • key-wallet/src/managed_account/address_pool.rs
  • key-wallet/src/managed_account/managed_account_ref.rs
  • key-wallet/src/managed_account/managed_core_funds_account.rs
  • key-wallet/src/managed_account/transaction_record.rs
  • key-wallet/src/transaction_checking/wallet_checker.rs
  • key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs
  • key-wallet/src/wallet/managed_wallet_info/mod.rs
  • key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 21, 2026
@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.52542% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.66%. Comparing base (70d4bf8) to head (6a1581a).

Files with missing lines Patch % Lines
...-wallet/src/managed_account/managed_account_ref.rs 50.00% 16 Missing ⚠️
key-wallet/src/wallet/managed_wallet_info/mod.rs 89.25% 13 Missing ⚠️
...allet/managed_wallet_info/wallet_info_interface.rs 25.00% 6 Missing ⚠️
...src/wallet/managed_wallet_info/managed_accounts.rs 33.33% 4 Missing ⚠️
key-wallet-manager/src/process_block.rs 0.00% 3 Missing ⚠️
key-wallet-manager/src/wallet_interface.rs 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #909      +/-   ##
==========================================
+ Coverage   74.56%   74.66%   +0.10%     
==========================================
  Files         328      328              
  Lines       75711    76210     +499     
==========================================
+ Hits        56451    56905     +454     
- Misses      19260    19305      +45     
Flag Coverage Δ
core 77.29% <ø> (ø)
ffi 50.53% <ø> (-0.01%) ⬇️
rpc 20.00% <ø> (ø)
spv 91.15% <100.00%> (-0.03%) ⬇️
wallet 74.67% <86.15%> (+0.26%) ⬆️
Files with missing lines Coverage Δ
dash-spv/src/sync/filters/batch.rs 97.60% <100.00%> (ø)
dash-spv/src/sync/filters/manager.rs 97.96% <100.00%> (+0.21%) ⬆️
key-wallet-manager/src/lib.rs 74.82% <100.00%> (ø)
key-wallet/src/managed_account/address_pool.rs 69.39% <100.00%> (+2.63%) ⬆️
.../src/managed_account/managed_core_funds_account.rs 79.03% <100.00%> (+1.17%) ⬆️
...y-wallet/src/managed_account/transaction_record.rs 100.00% <100.00%> (ø)
...-wallet/src/transaction_checking/wallet_checker.rs 99.24% <100.00%> (+<0.01%) ⬆️
key-wallet-manager/src/process_block.rs 90.16% <0.00%> (-0.66%) ⬇️
key-wallet-manager/src/wallet_interface.rs 11.53% <0.00%> (-1.51%) ⬇️
...src/wallet/managed_wallet_info/managed_accounts.rs 35.34% <33.33%> (+6.64%) ⬆️
... and 3 more

... and 8 files with indirect coverage changes

…patch coverage

Codecov flagged the dashpay#649 fix's previously-uncovered branches: the wallet-level
`observed_spent_outpoints` serde adapter, finality-boundary pruning, the
funding-first removal guard, the account-add sync rewind, and the AddressPool
`script_pubkey_index` prune fix. The manager-level integration tests only drive
the spend-first ordering end-to-end, leaving these reachable only from the
crate-internal `pub(crate)` surface.

Add `key-wallet/src/tests/observed_spent_outpoints_tests.rs` (the sibling file
already referenced by observed_spent_large_block_stress_test.rs) with five
white-box tests, plus one AddressPool prune test:

  - observed_spent_outpoints_survive_serde_round_trip: exercises the
    (OutPoint, height) sequence serde adapter (serialize + deserialize visitor)
    and the empty-map / `#[serde(default)]` path, isolated on an account-less
    wallet so the populated-account `script_pubkey_index` JSON-key blocker does
    not apply.
  - prune_finalized_observed_spends_respects_finality_boundary: no-op without a
    chainlock; otherwise evicts exactly entries at/below
    min(chainlock height, synced_height), keeping the rescan case (chainlock
    above sync checkpoint) from over-pruning.
  - funding_first_guard_removes_held_coin_and_compensates_record: the un-gated
    remove_spent_from_accounts / finalize_guard_removed_utxo path — coin dropped,
    reservation released, funding record compensated to net 0; idempotent;
    coinbase skipped.
  - wallet_level_set_outlives_account_local_reload: the account-local
    spent_outpoints derived set (rebuilt from recorded txs via
    simulate_reload_rebuild_spent_outpoints) forgets an unrecorded spend, but the
    persisted wallet-level set still prevents resurrection on funding re-delivery.
  - adding_account_from_xpub_rewinds_sync_checkpoint: standalone-account add
    collapses synced_height to birth_height - 1; a still-behind checkpoint is
    left untouched.
  - prune_unused_clears_script_pubkey_index (address_pool_tests.rs): regression
    guard for the missing script_pubkey_index.remove in AddressPool::prune_unused.

key-wallet lib (554) and key-wallet-manager (all) suites green.

Refs: dashpay#649

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bfoss765

Copy link
Copy Markdown
Contributor Author

Preempting the "block processing is order-enforced, so this shouldn't be possible" objection

This came up on #649 back in April — @xdustinface noted that block processing is order-enforced and concluded the defect shouldn't be reachable, and @lklimek initially agreed — before @lklimek's 2026-07-07 sub-second deterministic repro on #649 reversed that read. It's worth addressing head-on, because the objection targets the wrong layer: the coin is lost during wallet-layer transaction classification, not during chain/header sequencing, and in-order block delivery does not close that gap.

The root cause is classification, not ordering. The "already spent" guard in managed_core_funds_account.rs::update_utxos keys off the account-local spent_outpoints set, which is populated only when the account itself processes the spending transaction. A pure spend of a coin the wallet has not yet funded matches none of the account's owned inputs, so it is classified irrelevant, update_utxos never runs for it, and nothing anywhere records that the outpoint was consumed; when the funding tx is processed later, the output is re-inserted as fresh spendable value. Ordered block delivery does not prevent this, because the drop fires under several ordering-compatible paths:

  1. Cross-block out-of-order application. Height-ordered header sync does not imply height-ordered block download/apply during a cold rescan or parallel fetch. Consensus topological ordering only guarantees a funding→spend pair is correctly ordered within a single block — it says nothing about a spend whose funding coin sits in an earlier block that has not yet been applied. That is exactly what the repro constructs: funding at height 100, spend at height 200, height 200 applied first.
  2. Block re-processing / replay. A rescan re-delivers already-seen blocks; to the wallet layer a re-processed funding-after-spend sequence is indistinguishable from reordering — which is precisely what @lklimek's July 7 repro surfaced deterministically in under a second.
  3. Compact-filter scan gap. A pure-spend tx pays no wallet script, so it matches no BIP158 filter, and the block carrying it may never be fetched or processed on the spend side at all. No amount of ordering among the blocks the node does fetch can help with a block it never fetches.

This is not theoretical here. The PR's out_of_order_spend_repro_test delivers the blocks through the real WalletManager path (spend block at height 200, then funding block at height 100) and still leaves the funding outpoint permanently tracked on unpatched dev; and on testnet the phantom +0.01 on output 2febe5d7e8ad1dd0fb633004a82a24783d9b2e9095883541576e5f1344eb9975:0 survives a full from-seed re-derivation + rescan, which isolates the miss to the scan/processing path rather than live mempool ingestion. The fix records every block-observed spend into a wallet-level, classification-independent observed_spent_outpoints map, so whichever order the two blocks arrive in the funding-side insert is reconciled away.

On coverage: the previously-uncovered branches of this machinery that Codecov flagged (the observed_spent_outpoints serde adapter, finality-boundary pruning, the funding-first removal guard, the account-add sync rewind, and the AddressPool::prune_unused script_pubkey_index fix) are now exercised by targeted white-box tests added in 31b9dac; the key-wallet, key-wallet-manager, and dash-spv lib suites are green.

@bfoss765

Copy link
Copy Markdown
Contributor Author

Re the wallet_checker.rs:63-93 scalability note: leaving this as-is; the cost is already bounded and the correctness-critical half can't be applied safely. record_observed_spends must stay un-gated: a spend seen while the wallet has no matching funding account (or before one is added) is exactly the #649 case, and the set is designed to self-repopulate on replay, so skipping it when there are no funding accounts/UTXOs would reopen the bug. The account-handle fetch in remove_spent_from_accounts is already hoisted to once per call; the remaining per-tx all_funding_accounts_mut() is a non-allocating empty Vec when there are no funding accounts. Growth during a low-synced-height rescan is intentional (event-driven eviction, never age/LRU — evicting there is what reopens #649), and observed_spent_large_block_stress_test (5,000-tx block) asserts bounded/linear cost. On this safety-critical path I'd rather not trade a proven-bounded, stress-tested guard for a micro-optimization that risks the invariant.

QuantumExplorer and others added 2 commits July 22, 2026 21:46
… modification, tighten deser cap

- compensate_for_observed_spends: only replace the match-derived net_amount
  when the compensation actually dropped an output detail, keeping the
  no-observed-spend path byte-identical to pre-dashpay#649 behavior; pinned by a
  new unit test.
- record_observed_spends: report whether the persisted observed-spent map
  actually changed, and surface that as state_modified in
  check_core_transaction — a consumer persisting only on reported
  modifications must not lose a recorded spend across a restart; pinned by
  a new regression test (new spend reports, unchanged redelivery and
  mempool spends do not).
- Replace a comment reference to a nonexistent test with the inline
  rationale for why input_details and account_match.sent populate together.
- Tighten MAX_OBSERVED_SPENT_OUTPOINTS 10M -> 1M (load-time allocation cap
  from a few hundred MB to a few tens of MB), still far above any
  legitimate size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…urface guard-rewritten records, restore public account API

Addresses three external review findings on top of PR dashpay#909:

1. [P1] The commit-time contiguity guard could still certify unscanned
   coverage for a newly added account: a rewind landing INSIDE a scanned
   batch's range passed the height check, and an account add that moved
   no heights (checkpoint already at the birth floor) was undetectable
   by any height comparison. ManagedWalletInfo now carries an in-memory
   account_generation counter bumped on every account add (even
   height-invisible ones); filter scan snapshots it per wallet and
   commit refuses to advance a wallet whose generation changed since
   scan. Pinned by three new dash-spv tests including the mid-batch
   rewind repro (9000 -> scan [5000..9999] -> rewind 7499 -> commit
   keeps 7499) and the unmoved-checkpoint case.

2. [P2] The funding-first guard rewrote funding records without ever
   surfacing them: remove_spent_from_accounts now returns
   post-compensation clones of every rewritten record, the checker adds
   them to updated_records on both the relevant and irrelevant paths,
   and the manager propagates updated_records independent of
   is_relevant, so consumers persisting per-record updates see the
   rewrite.

3. [P2] ManagedAccountRefMut::record_transaction/confirm_transaction had
   silently gone pub -> pub(crate) with changed signatures. The public
   methods are restored with their original signatures (recording with
   no observed-spend context, the pre-dashpay#649 behavior); the checker uses
   new pub(crate) *_with_observed_spends variants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review fixes for dashpay#909: generation guard, surfaced record rewrites, restored public API
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: out-of-order block processing causes SPV wallet to miss UTXO spends

3 participants