Skip to content

Commit 04e54fd

Browse files
committed
Merge bitcoin/bitcoin#26325: rpc: Return accurate results for scanblocks
5ca7a7b rpc: Return accurate results for scanblocks (Aurèle Oulès) Pull request description: Implements #26322. Adds a `filter_false_positives` mode to `scanblocks` to accurately verify results from blockfilters. If the option is enabled, pre-results given by blockfilters will be filtered out again by checking vouts and vins of all transactions of the relevant blocks against the given descriptors. ### Master ```bash ./src/bitcoin-cli -testnet -named scanblocks action=start scanobjects='["addr(tb1qcxf2gv93c26s6mqz7y6etpqdf70zmn67dualgr)"]' { "from_height": 0, "to_height": 2376055, "relevant_blocks": [ "000000000001bc35077dec4104e0ab1f667ae27059bd907f9a8fac55c802ae36", "00000000000120a9c50542d73248fb7c37640c252850f0cf273134ad9febaf61", "0000000000000082f7af3835da8b6146b0bfb243b8842f09c495fa1e74d454ed", "0000000000000094c32651728193bfbe91f6789683b8d6ac6ae2d22ebd3cb5d3" ] } ``` ### PR (without `filter_false_positives` mode) Same as master ```bash ./src/bitcoin-cli -testnet -named scanblocks action=start scanobjects='["addr(tb1qcxf2gv93c26s6mqz7y6etpqdf70zmn67dualgr)"]' filter_false_positives=false { "from_height": 0, "to_height": 2376055, "relevant_blocks": [ "000000000001bc35077dec4104e0ab1f667ae27059bd907f9a8fac55c802ae36", "00000000000120a9c50542d73248fb7c37640c252850f0cf273134ad9febaf61", "0000000000000082f7af3835da8b6146b0bfb243b8842f09c495fa1e74d454ed", "0000000000000094c32651728193bfbe91f6789683b8d6ac6ae2d22ebd3cb5d3" ] } ``` ### PR (with `filter_false_positives` mode) ```bash ./src/bitcoin-cli -testnet -named scanblocks action=start scanobjects='["addr(tb1qcxf2gv93c26s6mqz7y6etpqdf70zmn67dualgr)"]' filter_false_positives=true { "from_height": 0, "to_height": 2376058, "relevant_blocks": [ "0000000000000082f7af3835da8b6146b0bfb243b8842f09c495fa1e74d454ed", "0000000000000094c32651728193bfbe91f6789683b8d6ac6ae2d22ebd3cb5d3" ] } ``` Also adds a test to check that the blockhash of a transaction will be included in the `relevant_blocks` whether the `filter_false_positives` mode is enabled or not. ACKs for top commit: achow101: ACK 5ca7a7b theStack: re-ACK 5ca7a7b furszy: Code review ACK 5ca7a7b Tree-SHA512: e8f3cceddddd66f59509717b6314d89e2fef241e13cee81b18fd95e8362cbb95cc40f884342ce6cf892a86febd9e2d434afce05d51892240e67f72ae991852e7
2 parents b55b11f + 5ca7a7b commit 04e54fd

File tree

4 files changed

+72
-6
lines changed

4 files changed

+72
-6
lines changed

src/rpc/blockchain.cpp

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <net_processing.h>
2626
#include <node/blockstorage.h>
2727
#include <node/context.h>
28+
#include <node/transaction.h>
2829
#include <node/utxo_snapshot.h>
2930
#include <primitives/transaction.h>
3031
#include <rpc/server.h>
@@ -2268,17 +2269,47 @@ class BlockFiltersScanReserver
22682269
}
22692270
};
22702271

2272+
static bool CheckBlockFilterMatches(BlockManager& blockman, const CBlockIndex& blockindex, const GCSFilter::ElementSet& needles)
2273+
{
2274+
const CBlock block{GetBlockChecked(blockman, &blockindex)};
2275+
const CBlockUndo block_undo{GetUndoChecked(blockman, &blockindex)};
2276+
2277+
// Check if any of the outputs match the scriptPubKey
2278+
for (const auto& tx : block.vtx) {
2279+
if (std::any_of(tx->vout.cbegin(), tx->vout.cend(), [&](const auto& txout) {
2280+
return needles.count(std::vector<unsigned char>(txout.scriptPubKey.begin(), txout.scriptPubKey.end())) != 0;
2281+
})) {
2282+
return true;
2283+
}
2284+
}
2285+
// Check if any of the inputs match the scriptPubKey
2286+
for (const auto& txundo : block_undo.vtxundo) {
2287+
if (std::any_of(txundo.vprevout.cbegin(), txundo.vprevout.cend(), [&](const auto& coin) {
2288+
return needles.count(std::vector<unsigned char>(coin.out.scriptPubKey.begin(), coin.out.scriptPubKey.end())) != 0;
2289+
})) {
2290+
return true;
2291+
}
2292+
}
2293+
2294+
return false;
2295+
}
2296+
22712297
static RPCHelpMan scanblocks()
22722298
{
22732299
return RPCHelpMan{"scanblocks",
2274-
"\nReturn relevant blockhashes for given descriptors.\n"
2300+
"\nReturn relevant blockhashes for given descriptors (requires blockfilterindex).\n"
22752301
"This call may take several minutes. Make sure to use no RPC timeout (bitcoin-cli -rpcclienttimeout=0)",
22762302
{
22772303
scan_action_arg_desc,
22782304
scan_objects_arg_desc,
22792305
RPCArg{"start_height", RPCArg::Type::NUM, RPCArg::Default{0}, "Height to start to scan from"},
22802306
RPCArg{"stop_height", RPCArg::Type::NUM, RPCArg::DefaultHint{"chain tip"}, "Height to stop to scan"},
2281-
RPCArg{"filtertype", RPCArg::Type::STR, RPCArg::Default{BlockFilterTypeName(BlockFilterType::BASIC)}, "The type name of the filter"}
2307+
RPCArg{"filtertype", RPCArg::Type::STR, RPCArg::Default{BlockFilterTypeName(BlockFilterType::BASIC)}, "The type name of the filter"},
2308+
RPCArg{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "",
2309+
{
2310+
{"filter_false_positives", RPCArg::Type::BOOL, RPCArg::Default{false}, "Filter false positives (slower and may fail on pruned nodes). Otherwise they may occur at a rate of 1/M"},
2311+
},
2312+
RPCArgOptions{.oneline_description="\"options\""}},
22822313
},
22832314
{
22842315
scan_result_status_none,
@@ -2338,6 +2369,9 @@ static RPCHelpMan scanblocks()
23382369
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unknown filtertype");
23392370
}
23402371

2372+
UniValue options{request.params[5].isNull() ? UniValue::VOBJ : request.params[5]};
2373+
bool filter_false_positives{options.exists("filter_false_positives") ? options["filter_false_positives"].get_bool() : false};
2374+
23412375
BlockFilterIndex* index = GetBlockFilterIndex(filtertype);
23422376
if (!index) {
23432377
throw JSONRPCError(RPC_MISC_ERROR, "Index is not enabled for filtertype " + filtertype_name);
@@ -2408,6 +2442,15 @@ static RPCHelpMan scanblocks()
24082442
for (const BlockFilter& filter : filters) {
24092443
// compare the elements-set with each filter
24102444
if (filter.GetFilter().MatchAny(needle_set)) {
2445+
if (filter_false_positives) {
2446+
// Double check the filter matches by scanning the block
2447+
const CBlockIndex& blockindex = *CHECK_NONFATAL(WITH_LOCK(cs_main, return chainman.m_blockman.LookupBlockIndex(filter.GetBlockHash())));
2448+
2449+
if (!CheckBlockFilterMatches(chainman.m_blockman, blockindex, needle_set)) {
2450+
continue;
2451+
}
2452+
}
2453+
24112454
blocks.push_back(filter.GetBlockHash().GetHex());
24122455
LogPrint(BCLog::RPC, "scanblocks: found match in %s\n", filter.GetBlockHash().GetHex());
24132456
}

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
8686
{ "scanblocks", 1, "scanobjects" },
8787
{ "scanblocks", 2, "start_height" },
8888
{ "scanblocks", 3, "stop_height" },
89+
{ "scanblocks", 5, "options" },
8990
{ "scantxoutset", 1, "scanobjects" },
9091
{ "addmultisigaddress", 0, "nrequired" },
9192
{ "addmultisigaddress", 1, "keys" },

test/functional/feature_pruning.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def set_test_params(self):
8484
["-maxreceivebuffer=20000", "-prune=550"],
8585
["-maxreceivebuffer=20000"],
8686
["-maxreceivebuffer=20000"],
87-
["-prune=550"],
87+
["-prune=550", "-blockfilterindex=1"],
8888
]
8989
self.rpc_timeout = 120
9090

@@ -356,7 +356,7 @@ def wallet_test(self):
356356
self.connect_nodes(0, 5)
357357
nds = [self.nodes[0], self.nodes[5]]
358358
self.sync_blocks(nds, wait=5, timeout=300)
359-
self.restart_node(5, extra_args=["-prune=550"]) # restart to trigger rescan
359+
self.restart_node(5, extra_args=["-prune=550", "-blockfilterindex=1"]) # restart to trigger rescan
360360
self.log.info("Success")
361361

362362
def run_test(self):
@@ -472,7 +472,20 @@ def run_test(self):
472472
self.log.info("Test invalid pruning command line options")
473473
self.test_invalid_command_line_options()
474474

475+
self.test_scanblocks_pruned()
476+
475477
self.log.info("Done")
476478

479+
def test_scanblocks_pruned(self):
480+
node = self.nodes[5]
481+
genesis_blockhash = node.getblockhash(0)
482+
false_positive_spk = bytes.fromhex("001400000000000000000000000000000000000cadcb")
483+
484+
assert genesis_blockhash in node.scanblocks(
485+
"start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0)['relevant_blocks']
486+
487+
assert_raises_rpc_error(-1, "Block not available (pruned data)", node.scanblocks,
488+
"start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0, "basic", {"filter_false_positives": True})
489+
477490
if __name__ == '__main__':
478491
PruneTest().main()

test/functional/rpc_scanblocks.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ def run_test(self):
6262
# make sure the blockhash is present when using the first mined block as start_height
6363
assert blockhash in node.scanblocks(
6464
"start", [f"addr({addr_1})"], height)['relevant_blocks']
65+
for v in [False, True]:
66+
assert blockhash in node.scanblocks(
67+
action="start",
68+
scanobjects=[f"addr({addr_1})"],
69+
start_height=height,
70+
options={"filter_false_positives": v})['relevant_blocks']
6571

6672
# also test the stop height
6773
assert blockhash in node.scanblocks(
@@ -94,8 +100,11 @@ def run_test(self):
94100
assert genesis_blockhash in node.scanblocks(
95101
"start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0)['relevant_blocks']
96102

97-
# TODO: after an "accurate" mode for scanblocks is implemented (e.g. PR #26325)
98-
# check here that it filters out the false-positive
103+
# check that the filter_false_positives option works
104+
assert genesis_blockhash in node.scanblocks(
105+
"start", [{"desc": f"raw({genesis_coinbase_spk.hex()})"}], 0, 0, "basic", {"filter_false_positives": True})['relevant_blocks']
106+
assert genesis_blockhash not in node.scanblocks(
107+
"start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0, "basic", {"filter_false_positives": True})['relevant_blocks']
99108

100109
# test node with disabled blockfilterindex
101110
assert_raises_rpc_error(-1, "Index is not enabled for filtertype basic",

0 commit comments

Comments
 (0)