Skip to content

Commit 6ef2566

Browse files
rpc: add scanblocks - scan for relevant blocks with descriptors
Co-authored-by: James O'Beirne <[email protected]>
1 parent a4258f6 commit 6ef2566

File tree

2 files changed

+201
-0
lines changed

2 files changed

+201
-0
lines changed

src/rpc/blockchain.cpp

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2204,6 +2204,203 @@ static RPCHelpMan scantxoutset()
22042204
};
22052205
}
22062206

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

0 commit comments

Comments
 (0)