]> Untitled Git - bdk/commit
fix(chain): let the indexer decide how far to rescan
authorLLFourn <lloyd.fourn@gmail.com>
Wed, 19 Aug 2026 05:15:39 +0000 (15:15 +1000)
committer志宇 <hello@evanlinjin.me>
Wed, 2 Sep 2026 00:42:30 +0000 (00:42 +0000)
commitf08eeac1547e4a4585933989573ede4db4dcb9ba
treef25f7dcc05609df8cca1393f9bb52fd0f7379e51
parent6d03fc3b4b4aa73f44f4ff1d87dcdb484f99dba8
fix(chain): let the indexer decide how far to rescan

`IndexedTxGraph::reindex` was a single pass over `TxGraph::full_txs`,
which iterates a `HashMap`, so the same graph reindexed twice could
produce two different `last_revealed` results. Each match inside
`KeychainTxOutIndex::_index_txout` bumps `last_revealed` and replenishes
the lookahead, widening the derived window the remaining outputs are
judged against. An output far enough out to need that widening was
skipped whenever it happened to be visited first, and no pass revisited
it.

This is not merely misuse of the API. The lookahead exists precisely to
catch indices the persisted frontier does not know about -- a restored
wallet, another signer on the same descriptor, an externally built PSBT
paying one of our far indices -- so whenever it does its job the frontier
moves mid-walk, and the order dependence is present in the intended use.

Add `Indexer::rescan`, which is handed the whole graph and returns what
indexing it produced. The default implementation offers every full
transaction and floating output once, which is all an indexer needs when
what it recognizes is fixed up front; `reindex` becomes a call to it.
`KeychainTxOutIndex` overrides it and looks repeatedly, stopping when a
pass leaves its revealed frontier unmoved.

Putting the loop behind the trait rather than in `IndexedTxGraph` keeps
the decision where the knowledge is. An indexer that does not widen
what it matches is walked exactly once, as before, so the looping
cannot leak into implementations that have no use for it. And
`KeychainTxOutIndex` can key the loop on its own frontier instead of on
whether a changeset came back empty: the changeset also carries staged
spk cache entries, which move without the frontier moving, so an
emptiness test spends an extra full walk on the ordinary restore path.

The regression test uses a single transaction with both outputs rather
than two transactions, because the graph's walk order is a `HashMap`
order: a two-transaction test would pass a single-pass implementation
about half the time. `index_tx` walks `tx.output` in vout order, so the
far index at vout 0 is always judged against the initial window and
always missed until the near index at vout 1 lifts the frontier.
crates/chain/src/indexed_tx_graph.rs
crates/chain/src/indexer.rs
crates/chain/src/indexer/keychain_txout.rs
crates/chain/tests/test_indexed_tx_graph.rs