Merge bitcoin/bitcoin#23549: Add scanblocks RPC call (attempt 2)

626b7c8493 fuzz: add scanblocks as safe for fuzzing (James O'Beirne)
94fe5453c7 test: rpc: add scanblocks functional test (Jonas Schnelli)
6ef2566b68 rpc: add scanblocks - scan for relevant blocks with descriptors (Jonas Schnelli)
a4258f6e81 rpc: move-only: consolidate blockchain scan args (James O'Beirne)

Pull request description:

  Revives #20664. All feedback from the previous PR has either been responded to inline or incorporated here.

  ---

  Major changes from Jonas' PR:
  - consolidated arguments for scantxoutset/scanblocks
  - substantial cleanup of the functional test

  Here's the range-diff (`git range-diff master jonasschnelli/2020/12/filterblocks_rpc jamesob/2021-11-scanblocks`): https://gist.github.com/jamesob/aa4a975344209f0316444b8de2ec1d18

  ### Original PR description

  > The `scanblocks` RPC call allows one to get relevant blockhashes from a set of descriptors by scanning all blockfilters in a given range.
  >
  > **Example:**
  >
  > `scanblocks start '["addr(<bitcoin_address>)"]' 661000` (returns relevant blockhashes for `<bitcoin_address>` from blockrange 661000->tip)
  >
  > ## Why is this useful?
  > **Fast wallet rescans**: get the relevant blocks and only rescan those via `rescanblockchain getblockheader(<hash>)[height] getblockheader(<hash>)[height])`. A future PR may add an option to allow to provide an array of blockhashes to `rescanblockchain`.
  >
  > **prune wallet rescans**: (_needs additional changes_): together with a call to fetch blocks from the p2p network if they have been pruned, it would allow to rescan wallets back to the genesis block in pruned mode (relevant #15946).
  >
  > **SPV mode** (_needs additional changes_): it would be possible to build the blockfilterindex from the p2p network (rather then deriving them from the blocks) and thus allow some sort of hybrid-SPV mode with moderate bandwidth consumption (related #9483)

ACKs for top commit:
  furszy:
    diff re-ACK 626b7c8

Tree-SHA512: f84e4dcb851b122b39e9700c58fbc31e899cdcf9b587df9505eaf1f45578cc4253e89ce2a45d1ff21bd213e31ddeedbbcad2c80810f46755b30acc17b07e2873
This commit is contained in:
Andrew Chow
2022-10-13 10:43:08 -04:00
5 changed files with 335 additions and 21 deletions

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python3
# Copyright (c) 2021 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test the scanblocks RPC call."""
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, assert_raises_rpc_error
class ScanblocksTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 2
self.extra_args = [["-blockfilterindex=1"], []]
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def run_test(self):
node = self.nodes[0]
# send 1.0, mempool only
addr_1 = node.getnewaddress()
node.sendtoaddress(addr_1, 1.0)
parent_key = "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B"
# send 1.0, mempool only
# childkey 5 of `parent_key`
node.sendtoaddress("mkS4HXoTYWRTescLGaUTGbtTTYX5EjJyEE", 1.0)
# mine a block and assure that the mined blockhash is in the filterresult
blockhash = self.generate(node, 1)[0]
height = node.getblockheader(blockhash)['height']
self.wait_until(lambda: all(i["synced"] for i in node.getindexinfo().values()))
out = node.scanblocks("start", [f"addr({addr_1})"])
assert(blockhash in out['relevant_blocks'])
assert_equal(height, out['to_height'])
assert_equal(0, out['from_height'])
# mine another block
blockhash_new = self.generate(node, 1)[0]
height_new = node.getblockheader(blockhash_new)['height']
# make sure the blockhash is not in the filter result if we set the start_height
# to the just mined block (unlikely to hit a false positive)
assert(blockhash not in node.scanblocks(
"start", [f"addr({addr_1})"], height_new)['relevant_blocks'])
# make sure the blockhash is present when using the first mined block as start_height
assert(blockhash in node.scanblocks(
"start", [f"addr({addr_1})"], height)['relevant_blocks'])
# also test the stop height
assert(blockhash in node.scanblocks(
"start", [f"addr({addr_1})"], height, height)['relevant_blocks'])
# use the stop_height to exclude the relevant block
assert(blockhash not in node.scanblocks(
"start", [f"addr({addr_1})"], 0, height - 1)['relevant_blocks'])
# make sure the blockhash is present when using the first mined block as start_height
assert(blockhash in node.scanblocks(
"start", [{"desc": f"pkh({parent_key}/*)", "range": [0, 100]}], height)['relevant_blocks'])
# test node with disabled blockfilterindex
assert_raises_rpc_error(-1, "Index is not enabled for filtertype basic",
self.nodes[1].scanblocks, "start", [f"addr({addr_1})"])
# test unknown filtertype
assert_raises_rpc_error(-5, "Unknown filtertype",
node.scanblocks, "start", [f"addr({addr_1})"], 0, 10, "extended")
# test invalid start_height
assert_raises_rpc_error(-1, "Invalid start_height",
node.scanblocks, "start", [f"addr({addr_1})"], 100000000)
# test invalid stop_height
assert_raises_rpc_error(-1, "Invalid stop_height",
node.scanblocks, "start", [f"addr({addr_1})"], 10, 0)
assert_raises_rpc_error(-1, "Invalid stop_height",
node.scanblocks, "start", [f"addr({addr_1})"], 10, 100000000)
# test accessing the status (must be empty)
assert_equal(node.scanblocks("status"), None)
# test aborting the current scan (there is no, must return false)
assert_equal(node.scanblocks("abort"), False)
# test invalid command
assert_raises_rpc_error(-8, "Invalid command", node.scanblocks, "foobar")
if __name__ == '__main__':
ScanblocksTest().main()

View File

@@ -316,6 +316,7 @@ BASE_SCRIPTS = [
'rpc_deriveaddresses.py',
'rpc_deriveaddresses.py --usecli',
'p2p_ping.py',
'rpc_scanblocks.py',
'rpc_scantxoutset.py',
'feature_txindex_compatibility.py',
'feature_unsupported_utxo_db.py',