mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-04-29 23:09:44 -04:00
rpc: Return accurate results for scanblocks
This makes use of undo data to accurately verify results from blockfilters.
This commit is contained in:
parent
911a40ead2
commit
5ca7a7be76
4 changed files with 72 additions and 6 deletions
|
@ -25,6 +25,7 @@
|
||||||
#include <net_processing.h>
|
#include <net_processing.h>
|
||||||
#include <node/blockstorage.h>
|
#include <node/blockstorage.h>
|
||||||
#include <node/context.h>
|
#include <node/context.h>
|
||||||
|
#include <node/transaction.h>
|
||||||
#include <node/utxo_snapshot.h>
|
#include <node/utxo_snapshot.h>
|
||||||
#include <primitives/transaction.h>
|
#include <primitives/transaction.h>
|
||||||
#include <rpc/server.h>
|
#include <rpc/server.h>
|
||||||
|
@ -2268,17 +2269,47 @@ public:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static bool CheckBlockFilterMatches(BlockManager& blockman, const CBlockIndex& blockindex, const GCSFilter::ElementSet& needles)
|
||||||
|
{
|
||||||
|
const CBlock block{GetBlockChecked(blockman, &blockindex)};
|
||||||
|
const CBlockUndo block_undo{GetUndoChecked(blockman, &blockindex)};
|
||||||
|
|
||||||
|
// Check if any of the outputs match the scriptPubKey
|
||||||
|
for (const auto& tx : block.vtx) {
|
||||||
|
if (std::any_of(tx->vout.cbegin(), tx->vout.cend(), [&](const auto& txout) {
|
||||||
|
return needles.count(std::vector<unsigned char>(txout.scriptPubKey.begin(), txout.scriptPubKey.end())) != 0;
|
||||||
|
})) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check if any of the inputs match the scriptPubKey
|
||||||
|
for (const auto& txundo : block_undo.vtxundo) {
|
||||||
|
if (std::any_of(txundo.vprevout.cbegin(), txundo.vprevout.cend(), [&](const auto& coin) {
|
||||||
|
return needles.count(std::vector<unsigned char>(coin.out.scriptPubKey.begin(), coin.out.scriptPubKey.end())) != 0;
|
||||||
|
})) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static RPCHelpMan scanblocks()
|
static RPCHelpMan scanblocks()
|
||||||
{
|
{
|
||||||
return RPCHelpMan{"scanblocks",
|
return RPCHelpMan{"scanblocks",
|
||||||
"\nReturn relevant blockhashes for given descriptors.\n"
|
"\nReturn relevant blockhashes for given descriptors (requires blockfilterindex).\n"
|
||||||
"This call may take several minutes. Make sure to use no RPC timeout (bitcoin-cli -rpcclienttimeout=0)",
|
"This call may take several minutes. Make sure to use no RPC timeout (bitcoin-cli -rpcclienttimeout=0)",
|
||||||
{
|
{
|
||||||
scan_action_arg_desc,
|
scan_action_arg_desc,
|
||||||
scan_objects_arg_desc,
|
scan_objects_arg_desc,
|
||||||
RPCArg{"start_height", RPCArg::Type::NUM, RPCArg::Default{0}, "Height to start to scan from"},
|
RPCArg{"start_height", RPCArg::Type::NUM, RPCArg::Default{0}, "Height to start to scan from"},
|
||||||
RPCArg{"stop_height", RPCArg::Type::NUM, RPCArg::DefaultHint{"chain tip"}, "Height to stop to scan"},
|
RPCArg{"stop_height", RPCArg::Type::NUM, RPCArg::DefaultHint{"chain tip"}, "Height to stop to scan"},
|
||||||
RPCArg{"filtertype", RPCArg::Type::STR, RPCArg::Default{BlockFilterTypeName(BlockFilterType::BASIC)}, "The type name of the filter"}
|
RPCArg{"filtertype", RPCArg::Type::STR, RPCArg::Default{BlockFilterTypeName(BlockFilterType::BASIC)}, "The type name of the filter"},
|
||||||
|
RPCArg{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "",
|
||||||
|
{
|
||||||
|
{"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"},
|
||||||
|
},
|
||||||
|
RPCArgOptions{.oneline_description="\"options\""}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scan_result_status_none,
|
scan_result_status_none,
|
||||||
|
@ -2338,6 +2369,9 @@ static RPCHelpMan scanblocks()
|
||||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unknown filtertype");
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unknown filtertype");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UniValue options{request.params[5].isNull() ? UniValue::VOBJ : request.params[5]};
|
||||||
|
bool filter_false_positives{options.exists("filter_false_positives") ? options["filter_false_positives"].get_bool() : false};
|
||||||
|
|
||||||
BlockFilterIndex* index = GetBlockFilterIndex(filtertype);
|
BlockFilterIndex* index = GetBlockFilterIndex(filtertype);
|
||||||
if (!index) {
|
if (!index) {
|
||||||
throw JSONRPCError(RPC_MISC_ERROR, "Index is not enabled for filtertype " + filtertype_name);
|
throw JSONRPCError(RPC_MISC_ERROR, "Index is not enabled for filtertype " + filtertype_name);
|
||||||
|
@ -2408,6 +2442,15 @@ static RPCHelpMan scanblocks()
|
||||||
for (const BlockFilter& filter : filters) {
|
for (const BlockFilter& filter : filters) {
|
||||||
// compare the elements-set with each filter
|
// compare the elements-set with each filter
|
||||||
if (filter.GetFilter().MatchAny(needle_set)) {
|
if (filter.GetFilter().MatchAny(needle_set)) {
|
||||||
|
if (filter_false_positives) {
|
||||||
|
// Double check the filter matches by scanning the block
|
||||||
|
const CBlockIndex& blockindex = *CHECK_NONFATAL(WITH_LOCK(cs_main, return chainman.m_blockman.LookupBlockIndex(filter.GetBlockHash())));
|
||||||
|
|
||||||
|
if (!CheckBlockFilterMatches(chainman.m_blockman, blockindex, needle_set)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
blocks.push_back(filter.GetBlockHash().GetHex());
|
blocks.push_back(filter.GetBlockHash().GetHex());
|
||||||
LogPrint(BCLog::RPC, "scanblocks: found match in %s\n", filter.GetBlockHash().GetHex());
|
LogPrint(BCLog::RPC, "scanblocks: found match in %s\n", filter.GetBlockHash().GetHex());
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
||||||
{ "scanblocks", 1, "scanobjects" },
|
{ "scanblocks", 1, "scanobjects" },
|
||||||
{ "scanblocks", 2, "start_height" },
|
{ "scanblocks", 2, "start_height" },
|
||||||
{ "scanblocks", 3, "stop_height" },
|
{ "scanblocks", 3, "stop_height" },
|
||||||
|
{ "scanblocks", 5, "options" },
|
||||||
{ "scantxoutset", 1, "scanobjects" },
|
{ "scantxoutset", 1, "scanobjects" },
|
||||||
{ "addmultisigaddress", 0, "nrequired" },
|
{ "addmultisigaddress", 0, "nrequired" },
|
||||||
{ "addmultisigaddress", 1, "keys" },
|
{ "addmultisigaddress", 1, "keys" },
|
||||||
|
|
|
@ -84,7 +84,7 @@ class PruneTest(BitcoinTestFramework):
|
||||||
["-maxreceivebuffer=20000", "-prune=550"],
|
["-maxreceivebuffer=20000", "-prune=550"],
|
||||||
["-maxreceivebuffer=20000"],
|
["-maxreceivebuffer=20000"],
|
||||||
["-maxreceivebuffer=20000"],
|
["-maxreceivebuffer=20000"],
|
||||||
["-prune=550"],
|
["-prune=550", "-blockfilterindex=1"],
|
||||||
]
|
]
|
||||||
self.rpc_timeout = 120
|
self.rpc_timeout = 120
|
||||||
|
|
||||||
|
@ -356,7 +356,7 @@ class PruneTest(BitcoinTestFramework):
|
||||||
self.connect_nodes(0, 5)
|
self.connect_nodes(0, 5)
|
||||||
nds = [self.nodes[0], self.nodes[5]]
|
nds = [self.nodes[0], self.nodes[5]]
|
||||||
self.sync_blocks(nds, wait=5, timeout=300)
|
self.sync_blocks(nds, wait=5, timeout=300)
|
||||||
self.restart_node(5, extra_args=["-prune=550"]) # restart to trigger rescan
|
self.restart_node(5, extra_args=["-prune=550", "-blockfilterindex=1"]) # restart to trigger rescan
|
||||||
self.log.info("Success")
|
self.log.info("Success")
|
||||||
|
|
||||||
def run_test(self):
|
def run_test(self):
|
||||||
|
@ -472,7 +472,20 @@ class PruneTest(BitcoinTestFramework):
|
||||||
self.log.info("Test invalid pruning command line options")
|
self.log.info("Test invalid pruning command line options")
|
||||||
self.test_invalid_command_line_options()
|
self.test_invalid_command_line_options()
|
||||||
|
|
||||||
|
self.test_scanblocks_pruned()
|
||||||
|
|
||||||
self.log.info("Done")
|
self.log.info("Done")
|
||||||
|
|
||||||
|
def test_scanblocks_pruned(self):
|
||||||
|
node = self.nodes[5]
|
||||||
|
genesis_blockhash = node.getblockhash(0)
|
||||||
|
false_positive_spk = bytes.fromhex("001400000000000000000000000000000000000cadcb")
|
||||||
|
|
||||||
|
assert genesis_blockhash in node.scanblocks(
|
||||||
|
"start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0)['relevant_blocks']
|
||||||
|
|
||||||
|
assert_raises_rpc_error(-1, "Block not available (pruned data)", node.scanblocks,
|
||||||
|
"start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0, "basic", {"filter_false_positives": True})
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
PruneTest().main()
|
PruneTest().main()
|
||||||
|
|
|
@ -62,6 +62,12 @@ class ScanblocksTest(BitcoinTestFramework):
|
||||||
# make sure the blockhash is present when using the first mined block as start_height
|
# make sure the blockhash is present when using the first mined block as start_height
|
||||||
assert blockhash in node.scanblocks(
|
assert blockhash in node.scanblocks(
|
||||||
"start", [f"addr({addr_1})"], height)['relevant_blocks']
|
"start", [f"addr({addr_1})"], height)['relevant_blocks']
|
||||||
|
for v in [False, True]:
|
||||||
|
assert blockhash in node.scanblocks(
|
||||||
|
action="start",
|
||||||
|
scanobjects=[f"addr({addr_1})"],
|
||||||
|
start_height=height,
|
||||||
|
options={"filter_false_positives": v})['relevant_blocks']
|
||||||
|
|
||||||
# also test the stop height
|
# also test the stop height
|
||||||
assert blockhash in node.scanblocks(
|
assert blockhash in node.scanblocks(
|
||||||
|
@ -94,8 +100,11 @@ class ScanblocksTest(BitcoinTestFramework):
|
||||||
assert genesis_blockhash in node.scanblocks(
|
assert genesis_blockhash in node.scanblocks(
|
||||||
"start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0)['relevant_blocks']
|
"start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0)['relevant_blocks']
|
||||||
|
|
||||||
# TODO: after an "accurate" mode for scanblocks is implemented (e.g. PR #26325)
|
# check that the filter_false_positives option works
|
||||||
# check here that it filters out the false-positive
|
assert genesis_blockhash in node.scanblocks(
|
||||||
|
"start", [{"desc": f"raw({genesis_coinbase_spk.hex()})"}], 0, 0, "basic", {"filter_false_positives": True})['relevant_blocks']
|
||||||
|
assert genesis_blockhash not in node.scanblocks(
|
||||||
|
"start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0, "basic", {"filter_false_positives": True})['relevant_blocks']
|
||||||
|
|
||||||
# test node with disabled blockfilterindex
|
# test node with disabled blockfilterindex
|
||||||
assert_raises_rpc_error(-1, "Index is not enabled for filtertype basic",
|
assert_raises_rpc_error(-1, "Index is not enabled for filtertype basic",
|
||||||
|
|
Loading…
Add table
Reference in a new issue