Commit 697bfb7
fix(dash-spv): stop losing derived scripts, and close the loop on wallet state
Two syncs of the same wallet against the same chain returned balances 2 000 020
sat apart, about half the time each. The money was one block, 2 429 667, which
the runs that ended high never processed.
Scripts derived while applying a block were handed to the batch named by the
block's in-flight record. That record is keyed by block hash and consumed by
the first delivery, and a block is delivered more than once — a rescan
re-queues what the forward scan already handed over, and on a mainnet restore
2 904 of 3 039 relevant heights arrive twice or more. Every later delivery
found no record, so its scripts reached no batch, no later batch, and no
backward sweep.
What made it invisible is that the derivation itself was not lost: the wallet
kept the addresses. So no later block reported them as new, no rescan carried
them, and the filter layer went on matching a query it did not know was
incomplete. Measured: a second delivery of the block at 2 429 637 derived 27
scripts covering the mixing session that owns 2 429 667, the batch holding that
block rescanned four times without them, and the block was never matched.
`collect_new_scripts` now routes by height instead — to the batch whose range
contains the block, which still holds that range's filters, falling back to the
backward accumulator only when no active batch covers the height.
That alone would still rest on a one-shot notification arriving, and 23.6% of
blocks are applied out of order, so notification-shaped invariants are not
worth much here. `reconcile_untested_scripts` therefore closes the loop on
state: each batch records which scripts have been matched against its filters,
and before committing it asks the wallet what it watches now and re-tests the
difference. It also gives the lower active batches the scripts a higher one
derived, which nothing did before.
Ten full mainnet restores across five configurations now return the same 13 876
outputs and the same balance, where the same wallet previously split roughly
50/50 between two answers. Two of the first three runs exercised the routing
path (92 and 27 scripts rescued), so the agreement is not luck.
Two consequences of the new routing, both handled here. `rescan_batch` marks
scripts tested before its empty-filters return, as `scan_batch` already did:
otherwise a batch with no filters is handed the same set by every commit
attempt and never converges.
And `backward_scripts` can now be non-empty with no active batch, when a block
is delivered after its batch committed — so the assertion in `try_process_batch`
that it is empty no longer holds. Those scripts cannot be left to a next commit
that may never come: the accumulator is in-memory only, nothing looks below the
committed frontier again, and a shutdown at the tip loses them for good, since
the restart resumes with `committed_height` already at the tip and no batch to
reconcile them against. The completion branch therefore sweeps them itself over
the whole committed range, and holds `FiltersSyncComplete` while the blocks that
sweep found are still in flight. The gate is the tracker, not the commit gate:
blocks a tip sweep queues are charged to no batch, so no batch can hold the
completion for them. Their `BlockProcessed` re-enters the branch, and a round
that derives no new scripts is the fixpoint. Processed records left by blocks
applied after the last commit are pruned there too, since no commit will.
Four regression tests, each failing without the fix: a `BlockProcessed` with
no in-flight record whose scripts must reach the batch covering the height (and
the backward accumulator when none does); a batch scanned with a query missing
one address, whose commit must find that address's block without ever being
told; a rescan of an empty batch, which must still record what it was handed;
and a tip with scripts stranded in the accumulator, which must sweep them and
withhold completion until the block it finds has been applied.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K92DcuiKs8UdWkrhyfghqX1 parent 93260bf commit 697bfb7
4 files changed
Lines changed: 364 additions & 22 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| 40 | + | |
| 41 | + | |
40 | 42 | | |
41 | 43 | | |
42 | 44 | | |
| |||
56 | 58 | | |
57 | 59 | | |
58 | 60 | | |
| 61 | + | |
59 | 62 | | |
60 | 63 | | |
61 | 64 | | |
| |||
119 | 122 | | |
120 | 123 | | |
121 | 124 | | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
122 | 144 | | |
123 | 145 | | |
124 | 146 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
148 | 148 | | |
149 | 149 | | |
150 | 150 | | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
151 | 156 | | |
152 | 157 | | |
153 | 158 | | |
| |||
0 commit comments