Skip to content

Commit bc2b1f0

Browse files
committed
Merge bitcoin/bitcoin#23549: Add scanblocks RPC call (attempt 2)
626b7c8 fuzz: add scanblocks as safe for fuzzing (James O'Beirne) 94fe545 test: rpc: add scanblocks functional test (Jonas Schnelli) 6ef2566 rpc: add scanblocks - scan for relevant blocks with descriptors (Jonas Schnelli) a4258f6 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
2 parents 6912a28 + 626b7c8 commit bc2b1f0

File tree

5 files changed

+335
-21
lines changed

5 files changed

+335
-21
lines changed

src/rpc/blockchain.cpp

Lines changed: 237 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2019,6 +2019,40 @@ class CoinsViewScanReserver
20192019
}
20202020
};
20212021

2022+
static const auto scan_action_arg_desc = RPCArg{
2023+
"action", RPCArg::Type::STR, RPCArg::Optional::NO, "The action to execute\n"
2024+
"\"start\" for starting a scan\n"
2025+
"\"abort\" for aborting the current scan (returns true when abort was successful)\n"
2026+
"\"status\" for progress report (in %) of the current scan"
2027+
};
2028+
2029+
static const auto scan_objects_arg_desc = RPCArg{
2030+
"scanobjects", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "Array of scan objects. Required for \"start\" action\n"
2031+
"Every scan object is either a string descriptor or an object:",
2032+
{
2033+
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "An output descriptor"},
2034+
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "An object with output descriptor and metadata",
2035+
{
2036+
{"desc", RPCArg::Type::STR, RPCArg::Optional::NO, "An output descriptor"},
2037+
{"range", RPCArg::Type::RANGE, RPCArg::Default{1000}, "The range of HD chain indexes to explore (either end or [begin,end])"},
2038+
}},
2039+
},
2040+
RPCArgOptions{.oneline_description="[scanobjects,...]"},
2041+
};
2042+
2043+
static const auto scan_result_abort = RPCResult{
2044+
"when action=='abort'", RPCResult::Type::BOOL, "success",
2045+
"True if scan will be aborted (not necessarily before this RPC returns), or false if there is no scan to abort"
2046+
};
2047+
static const auto scan_result_status_none = RPCResult{
2048+
"when action=='status' and no scan is in progress - possibly already completed", RPCResult::Type::NONE, "", ""
2049+
};
2050+
static const auto scan_result_status_some = RPCResult{
2051+
"when action=='status' and a scan is currently in progress", RPCResult::Type::OBJ, "", "",
2052+
{{RPCResult::Type::NUM, "progress", "Approximate percent complete"},}
2053+
};
2054+
2055+
20222056
static RPCHelpMan scantxoutset()
20232057
{
20242058
// scriptPubKey corresponding to mainnet address 12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S
@@ -2038,21 +2072,8 @@ static RPCHelpMan scantxoutset()
20382072
"In the latter case, a range needs to be specified by below if different from 1000.\n"
20392073
"For more information on output descriptors, see the documentation in the doc/descriptors.md file.\n",
20402074
{
2041-
{"action", RPCArg::Type::STR, RPCArg::Optional::NO, "The action to execute\n"
2042-
"\"start\" for starting a scan\n"
2043-
"\"abort\" for aborting the current scan (returns true when abort was successful)\n"
2044-
"\"status\" for progress report (in %) of the current scan"},
2045-
{"scanobjects", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "Array of scan objects. Required for \"start\" action\n"
2046-
"Every scan object is either a string descriptor or an object:",
2047-
{
2048-
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "An output descriptor"},
2049-
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "An object with output descriptor and metadata",
2050-
{
2051-
{"desc", RPCArg::Type::STR, RPCArg::Optional::NO, "An output descriptor"},
2052-
{"range", RPCArg::Type::RANGE, RPCArg::Default{1000}, "The range of HD chain indexes to explore (either end or [begin,end])"},
2053-
}},
2054-
},
2055-
RPCArgOptions{.oneline_description="[scanobjects,...]"}},
2075+
scan_action_arg_desc,
2076+
scan_objects_arg_desc,
20562077
},
20572078
{
20582079
RPCResult{"when action=='start'; only returns after scan completes", RPCResult::Type::OBJ, "", "", {
@@ -2075,12 +2096,9 @@ static RPCHelpMan scantxoutset()
20752096
}},
20762097
{RPCResult::Type::STR_AMOUNT, "total_amount", "The total amount of all found unspent outputs in " + CURRENCY_UNIT},
20772098
}},
2078-
RPCResult{"when action=='abort'", RPCResult::Type::BOOL, "success", "True if scan will be aborted (not necessarily before this RPC returns), or false if there is no scan to abort"},
2079-
RPCResult{"when action=='status' and a scan is currently in progress", RPCResult::Type::OBJ, "", "",
2080-
{
2081-
{RPCResult::Type::NUM, "progress", "Approximate percent complete"},
2082-
}},
2083-
RPCResult{"when action=='status' and no scan is in progress - possibly already completed", RPCResult::Type::NONE, "", ""},
2099+
scan_result_abort,
2100+
scan_result_status_some,
2101+
scan_result_status_none,
20842102
},
20852103
RPCExamples{
20862104
HelpExampleCli("scantxoutset", "start \'[\"" + EXAMPLE_DESCRIPTOR_RAW + "\"]\'") +
@@ -2188,6 +2206,203 @@ static RPCHelpMan scantxoutset()
21882206
};
21892207
}
21902208

2209+
/** RAII object to prevent concurrency issue when scanning blockfilters */
2210+
static std::atomic<int> g_scanfilter_progress;
2211+
static std::atomic<int> g_scanfilter_progress_height;
2212+
static std::atomic<bool> g_scanfilter_in_progress;
2213+
static std::atomic<bool> g_scanfilter_should_abort_scan;
2214+
class BlockFiltersScanReserver
2215+
{
2216+
private:
2217+
bool m_could_reserve{false};
2218+
public:
2219+
explicit BlockFiltersScanReserver() = default;
2220+
2221+
bool reserve() {
2222+
CHECK_NONFATAL(!m_could_reserve);
2223+
if (g_scanfilter_in_progress.exchange(true)) {
2224+
return false;
2225+
}
2226+
m_could_reserve = true;
2227+
return true;
2228+
}
2229+
2230+
~BlockFiltersScanReserver() {
2231+
if (m_could_reserve) {
2232+
g_scanfilter_in_progress = false;
2233+
}
2234+
}
2235+
};
2236+
2237+
static RPCHelpMan scanblocks()
2238+
{
2239+
return RPCHelpMan{"scanblocks",
2240+
"\nReturn relevant blockhashes for given descriptors.\n"
2241+
"This call may take several minutes. Make sure to use no RPC timeout (bitcoin-cli -rpcclienttimeout=0)",
2242+
{
2243+
scan_action_arg_desc,
2244+
scan_objects_arg_desc,
2245+
RPCArg{"start_height", RPCArg::Type::NUM, RPCArg::Default{0}, "Height to start to scan from"},
2246+
RPCArg{"stop_height", RPCArg::Type::NUM, RPCArg::DefaultHint{"chain tip"}, "Height to stop to scan"},
2247+
RPCArg{"filtertype", RPCArg::Type::STR, RPCArg::Default{BlockFilterTypeName(BlockFilterType::BASIC)}, "The type name of the filter"}
2248+
},
2249+
{
2250+
scan_result_status_none,
2251+
RPCResult{"When action=='start'", RPCResult::Type::OBJ, "", "", {
2252+
{RPCResult::Type::NUM, "from_height", "The height we started the scan from"},
2253+
{RPCResult::Type::NUM, "to_height", "The height we ended the scan at"},
2254+
{RPCResult::Type::ARR, "relevant_blocks", "", {{RPCResult::Type::STR_HEX, "blockhash", "A relevant blockhash"},}},
2255+
},
2256+
},
2257+
RPCResult{"when action=='status' and a scan is currently in progress", RPCResult::Type::OBJ, "", "", {
2258+
{RPCResult::Type::NUM, "progress", "Approximate percent complete"},
2259+
{RPCResult::Type::NUM, "current_height", "Height of the block currently being scanned"},
2260+
},
2261+
},
2262+
scan_result_abort,
2263+
},
2264+
RPCExamples{
2265+
HelpExampleCli("scanblocks", "start '[\"addr(bcrt1q4u4nsgk6ug0sqz7r3rj9tykjxrsl0yy4d0wwte)\"]' 300000") +
2266+
HelpExampleCli("scanblocks", "start '[\"addr(bcrt1q4u4nsgk6ug0sqz7r3rj9tykjxrsl0yy4d0wwte)\"]' 100 150 basic") +
2267+
HelpExampleCli("scanblocks", "status") +
2268+
HelpExampleRpc("scanblocks", "\"start\", [\"addr(bcrt1q4u4nsgk6ug0sqz7r3rj9tykjxrsl0yy4d0wwte)\"], 300000") +
2269+
HelpExampleRpc("scanblocks", "\"start\", [\"addr(bcrt1q4u4nsgk6ug0sqz7r3rj9tykjxrsl0yy4d0wwte)\"], 100, 150, \"basic\"") +
2270+
HelpExampleRpc("scanblocks", "\"status\"")
2271+
},
2272+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
2273+
{
2274+
UniValue ret(UniValue::VOBJ);
2275+
if (request.params[0].get_str() == "status") {
2276+
BlockFiltersScanReserver reserver;
2277+
if (reserver.reserve()) {
2278+
// no scan in progress
2279+
return NullUniValue;
2280+
}
2281+
ret.pushKV("progress", g_scanfilter_progress.load());
2282+
ret.pushKV("current_height", g_scanfilter_progress_height.load());
2283+
return ret;
2284+
} else if (request.params[0].get_str() == "abort") {
2285+
BlockFiltersScanReserver reserver;
2286+
if (reserver.reserve()) {
2287+
// reserve was possible which means no scan was running
2288+
return false;
2289+
}
2290+
// set the abort flag
2291+
g_scanfilter_should_abort_scan = true;
2292+
return true;
2293+
}
2294+
else if (request.params[0].get_str() == "start") {
2295+
BlockFiltersScanReserver reserver;
2296+
if (!reserver.reserve()) {
2297+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan already in progress, use action \"abort\" or \"status\"");
2298+
}
2299+
const std::string filtertype_name{request.params[4].isNull() ? "basic" : request.params[4].get_str()};
2300+
2301+
BlockFilterType filtertype;
2302+
if (!BlockFilterTypeByName(filtertype_name, filtertype)) {
2303+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unknown filtertype");
2304+
}
2305+
2306+
BlockFilterIndex* index = GetBlockFilterIndex(filtertype);
2307+
if (!index) {
2308+
throw JSONRPCError(RPC_MISC_ERROR, "Index is not enabled for filtertype " + filtertype_name);
2309+
}
2310+
2311+
NodeContext& node = EnsureAnyNodeContext(request.context);
2312+
ChainstateManager& chainman = EnsureChainman(node);
2313+
2314+
// set the start-height
2315+
const CBlockIndex* block = nullptr;
2316+
const CBlockIndex* stop_block = nullptr;
2317+
{
2318+
LOCK(cs_main);
2319+
CChain& active_chain = chainman.ActiveChain();
2320+
block = active_chain.Genesis();
2321+
stop_block = active_chain.Tip();
2322+
if (!request.params[2].isNull()) {
2323+
block = active_chain[request.params[2].getInt<int>()];
2324+
if (!block) {
2325+
throw JSONRPCError(RPC_MISC_ERROR, "Invalid start_height");
2326+
}
2327+
}
2328+
if (!request.params[3].isNull()) {
2329+
stop_block = active_chain[request.params[3].getInt<int>()];
2330+
if (!stop_block || stop_block->nHeight < block->nHeight) {
2331+
throw JSONRPCError(RPC_MISC_ERROR, "Invalid stop_height");
2332+
}
2333+
}
2334+
}
2335+
CHECK_NONFATAL(block);
2336+
2337+
// loop through the scan objects, add scripts to the needle_set
2338+
GCSFilter::ElementSet needle_set;
2339+
for (const UniValue& scanobject : request.params[1].get_array().getValues()) {
2340+
FlatSigningProvider provider;
2341+
std::vector<CScript> scripts = EvalDescriptorStringOrObject(scanobject, provider);
2342+
for (const CScript& script : scripts) {
2343+
needle_set.emplace(script.begin(), script.end());
2344+
}
2345+
}
2346+
UniValue blocks(UniValue::VARR);
2347+
const int amount_per_chunk = 10000;
2348+
const CBlockIndex* start_index = block; // for remembering the start of a blockfilter range
2349+
std::vector<BlockFilter> filters;
2350+
const CBlockIndex* start_block = block; // for progress reporting
2351+
const int total_blocks_to_process = stop_block->nHeight - start_block->nHeight;
2352+
2353+
g_scanfilter_should_abort_scan = false;
2354+
g_scanfilter_progress = 0;
2355+
g_scanfilter_progress_height = start_block->nHeight;
2356+
2357+
while (block) {
2358+
node.rpc_interruption_point(); // allow a clean shutdown
2359+
if (g_scanfilter_should_abort_scan) {
2360+
LogPrintf("scanblocks RPC aborted at height %d.\n", block->nHeight);
2361+
break;
2362+
}
2363+
const CBlockIndex* next = nullptr;
2364+
{
2365+
LOCK(cs_main);
2366+
CChain& active_chain = chainman.ActiveChain();
2367+
next = active_chain.Next(block);
2368+
if (block == stop_block) next = nullptr;
2369+
}
2370+
if (start_index->nHeight + amount_per_chunk == block->nHeight || next == nullptr) {
2371+
LogPrint(BCLog::RPC, "Fetching blockfilters from height %d to height %d.\n", start_index->nHeight, block->nHeight);
2372+
if (index->LookupFilterRange(start_index->nHeight, block, filters)) {
2373+
for (const BlockFilter& filter : filters) {
2374+
// compare the elements-set with each filter
2375+
if (filter.GetFilter().MatchAny(needle_set)) {
2376+
blocks.push_back(filter.GetBlockHash().GetHex());
2377+
LogPrint(BCLog::RPC, "scanblocks: found match in %s\n", filter.GetBlockHash().GetHex());
2378+
}
2379+
}
2380+
}
2381+
start_index = block;
2382+
2383+
// update progress
2384+
int blocks_processed = block->nHeight - start_block->nHeight;
2385+
if (total_blocks_to_process > 0) { // avoid division by zero
2386+
g_scanfilter_progress = (int)(100.0 / total_blocks_to_process * blocks_processed);
2387+
} else {
2388+
g_scanfilter_progress = 100;
2389+
}
2390+
g_scanfilter_progress_height = block->nHeight;
2391+
}
2392+
block = next;
2393+
}
2394+
ret.pushKV("from_height", start_block->nHeight);
2395+
ret.pushKV("to_height", g_scanfilter_progress_height.load());
2396+
ret.pushKV("relevant_blocks", blocks);
2397+
}
2398+
else {
2399+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid command");
2400+
}
2401+
return ret;
2402+
},
2403+
};
2404+
}
2405+
21912406
static RPCHelpMan getblockfilter()
21922407
{
21932408
return RPCHelpMan{"getblockfilter",
@@ -2423,6 +2638,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t)
24232638
{"blockchain", &verifychain},
24242639
{"blockchain", &preciousblock},
24252640
{"blockchain", &scantxoutset},
2641+
{"blockchain", &scanblocks},
24262642
{"blockchain", &getblockfilter},
24272643
{"hidden", &invalidateblock},
24282644
{"hidden", &reconsiderblock},

src/rpc/client.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
8383
{ "sendmany", 8, "fee_rate"},
8484
{ "sendmany", 9, "verbose" },
8585
{ "deriveaddresses", 1, "range" },
86+
{ "scanblocks", 1, "scanobjects" },
87+
{ "scanblocks", 2, "start_height" },
88+
{ "scanblocks", 3, "stop_height" },
8689
{ "scantxoutset", 1, "scanobjects" },
8790
{ "addmultisigaddress", 0, "nrequired" },
8891
{ "addmultisigaddress", 1, "keys" },

src/test/fuzz/rpc.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
151151
"preciousblock",
152152
"pruneblockchain",
153153
"reconsiderblock",
154+
"scanblocks",
154155
"scantxoutset",
155156
"sendrawtransaction",
156157
"setmocktime",

0 commit comments

Comments
 (0)