The issue
scanblocks reads block filters over a range via LookupFilterRange. When the
block filter index is behind the active chain (e.g. right after startup, while
it is still syncing in the background), the lookup for the not yet indexed range
fails. The old code ignored that failure, advanced start_index to the end of
the chunk, and kept going - so the scan skipped every unindexed block and still
returned "completed": true.
The result is a silent gap: a caller gets a relevant_blocks list that looks
fine but is missing any match in the unindexed range, with no way to
distinguish "no matches" from "range was never scanned".
Note that "completed" only means the scan was not aborted - it does not mean
every filter in the requested range was actually read.
Steps to reproduce
- Start a node with
-blockfilterindex=1and let it mine/receive some blocks. - Stop the node, delete
<datadir>/<chain>/indexes/blockfilter, restart with-blockfilterindex=1. - Immediately (before the index finishes rebuilding) call
scanblocks start '["addr(<addr>)"]'.
Before this change the call returns "completed": true with an empty/partial
relevant_blocks, silently skipping the range the index had not rebuilt yet.
How it is fixed and why
Mirror getblockfilter's error classification:
- Call
BlockUntilSyncedToCurrentChain()only to obtain anindex_readyflag. Do not fail unconditionally when the index is still behind the tip - the requested range may already be available. - If
LookupFilterRangefails and the index is not ready, throw the same "still in the process of being indexed"RPC_MISC_ERRORthatgetblockfilteruses. - If
LookupFilterRangefails after the index is ready, throwRPC_INTERNAL_ERROR(unexpected / corruption), instead of skipping the chunk.
This closes the silent gap without rejecting scans whose range is already covered while the index catches up to the tip.
The diff also reindents the match loop: inverting if (LookupFilterRange(...))
into an early throw removes one nesting level from the existing body. That
reindentation is a consequence of the bug fix, not a standalone style change.
Tests
test/functional/rpc_scanblocks.py- newtest_scanblocks_unindexed_rangecovers the modified code: it pads the chain, wipes the filter index, restarts, and while the index is behind the tip asserts that a scan to the tip returns the indexing error, that a scan over an already-written prefix (genesis) still succeeds, and that after sync the match is found.- The existing
run_testcases (whichwait_until(... synced ...)before scanning) continue to exercise the normal synced path.
AI / tooling note
I found this regression while reviewing the RPC with an AI-assisted tool.
I reproduced the unsynced-index behavior myself, chose to mirror
getblockfilter's existing guard, and verified the fix with the
functional test above. This PR text and any review replies are my own.