mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-07-12 20:32:36 +02:00
rpc: add scanblocks - scan for relevant blocks with descriptors
Co-authored-by: James O'Beirne <james.obeirne@gmail.com>
This commit is contained in:
committed by
James O'Beirne
parent
a4258f6e81
commit
6ef2566b68
@ -2204,6 +2204,203 @@ static RPCHelpMan scantxoutset()
|
||||
};
|
||||
}
|
||||
|
||||
/** RAII object to prevent concurrency issue when scanning blockfilters */
|
||||
static std::atomic<int> g_scanfilter_progress;
|
||||
static std::atomic<int> g_scanfilter_progress_height;
|
||||
static std::atomic<bool> g_scanfilter_in_progress;
|
||||
static std::atomic<bool> g_scanfilter_should_abort_scan;
|
||||
class BlockFiltersScanReserver
|
||||
{
|
||||
private:
|
||||
bool m_could_reserve{false};
|
||||
public:
|
||||
explicit BlockFiltersScanReserver() = default;
|
||||
|
||||
bool reserve() {
|
||||
CHECK_NONFATAL(!m_could_reserve);
|
||||
if (g_scanfilter_in_progress.exchange(true)) {
|
||||
return false;
|
||||
}
|
||||
m_could_reserve = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
~BlockFiltersScanReserver() {
|
||||
if (m_could_reserve) {
|
||||
g_scanfilter_in_progress = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static RPCHelpMan scanblocks()
|
||||
{
|
||||
return RPCHelpMan{"scanblocks",
|
||||
"\nReturn relevant blockhashes for given descriptors.\n"
|
||||
"This call may take several minutes. Make sure to use no RPC timeout (bitcoin-cli -rpcclienttimeout=0)",
|
||||
{
|
||||
scan_action_arg_desc,
|
||||
scan_objects_arg_desc,
|
||||
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{"filtertype", RPCArg::Type::STR, RPCArg::Default{BlockFilterTypeName(BlockFilterType::BASIC)}, "The type name of the filter"}
|
||||
},
|
||||
{
|
||||
scan_result_status_none,
|
||||
RPCResult{"When action=='start'", RPCResult::Type::OBJ, "", "", {
|
||||
{RPCResult::Type::NUM, "from_height", "The height we started the scan from"},
|
||||
{RPCResult::Type::NUM, "to_height", "The height we ended the scan at"},
|
||||
{RPCResult::Type::ARR, "relevant_blocks", "", {{RPCResult::Type::STR_HEX, "blockhash", "A relevant blockhash"},}},
|
||||
},
|
||||
},
|
||||
RPCResult{"when action=='status' and a scan is currently in progress", RPCResult::Type::OBJ, "", "", {
|
||||
{RPCResult::Type::NUM, "progress", "Approximate percent complete"},
|
||||
{RPCResult::Type::NUM, "current_height", "Height of the block currently being scanned"},
|
||||
},
|
||||
},
|
||||
scan_result_abort,
|
||||
},
|
||||
RPCExamples{
|
||||
HelpExampleCli("scanblocks", "start '[\"addr(bcrt1q4u4nsgk6ug0sqz7r3rj9tykjxrsl0yy4d0wwte)\"]' 300000") +
|
||||
HelpExampleCli("scanblocks", "start '[\"addr(bcrt1q4u4nsgk6ug0sqz7r3rj9tykjxrsl0yy4d0wwte)\"]' 100 150 basic") +
|
||||
HelpExampleCli("scanblocks", "status") +
|
||||
HelpExampleRpc("scanblocks", "\"start\", [\"addr(bcrt1q4u4nsgk6ug0sqz7r3rj9tykjxrsl0yy4d0wwte)\"], 300000") +
|
||||
HelpExampleRpc("scanblocks", "\"start\", [\"addr(bcrt1q4u4nsgk6ug0sqz7r3rj9tykjxrsl0yy4d0wwte)\"], 100, 150, \"basic\"") +
|
||||
HelpExampleRpc("scanblocks", "\"status\"")
|
||||
},
|
||||
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
||||
{
|
||||
UniValue ret(UniValue::VOBJ);
|
||||
if (request.params[0].get_str() == "status") {
|
||||
BlockFiltersScanReserver reserver;
|
||||
if (reserver.reserve()) {
|
||||
// no scan in progress
|
||||
return NullUniValue;
|
||||
}
|
||||
ret.pushKV("progress", g_scanfilter_progress.load());
|
||||
ret.pushKV("current_height", g_scanfilter_progress_height.load());
|
||||
return ret;
|
||||
} else if (request.params[0].get_str() == "abort") {
|
||||
BlockFiltersScanReserver reserver;
|
||||
if (reserver.reserve()) {
|
||||
// reserve was possible which means no scan was running
|
||||
return false;
|
||||
}
|
||||
// set the abort flag
|
||||
g_scanfilter_should_abort_scan = true;
|
||||
return true;
|
||||
}
|
||||
else if (request.params[0].get_str() == "start") {
|
||||
BlockFiltersScanReserver reserver;
|
||||
if (!reserver.reserve()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan already in progress, use action \"abort\" or \"status\"");
|
||||
}
|
||||
const std::string filtertype_name{request.params[4].isNull() ? "basic" : request.params[4].get_str()};
|
||||
|
||||
BlockFilterType filtertype;
|
||||
if (!BlockFilterTypeByName(filtertype_name, filtertype)) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unknown filtertype");
|
||||
}
|
||||
|
||||
BlockFilterIndex* index = GetBlockFilterIndex(filtertype);
|
||||
if (!index) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Index is not enabled for filtertype " + filtertype_name);
|
||||
}
|
||||
|
||||
NodeContext& node = EnsureAnyNodeContext(request.context);
|
||||
ChainstateManager& chainman = EnsureChainman(node);
|
||||
|
||||
// set the start-height
|
||||
const CBlockIndex* block = nullptr;
|
||||
const CBlockIndex* stop_block = nullptr;
|
||||
{
|
||||
LOCK(cs_main);
|
||||
CChain& active_chain = chainman.ActiveChain();
|
||||
block = active_chain.Genesis();
|
||||
stop_block = active_chain.Tip();
|
||||
if (!request.params[2].isNull()) {
|
||||
block = active_chain[request.params[2].getInt<int>()];
|
||||
if (!block) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Invalid start_height");
|
||||
}
|
||||
}
|
||||
if (!request.params[3].isNull()) {
|
||||
stop_block = active_chain[request.params[3].getInt<int>()];
|
||||
if (!stop_block || stop_block->nHeight < block->nHeight) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Invalid stop_height");
|
||||
}
|
||||
}
|
||||
}
|
||||
CHECK_NONFATAL(block);
|
||||
|
||||
// loop through the scan objects, add scripts to the needle_set
|
||||
GCSFilter::ElementSet needle_set;
|
||||
for (const UniValue& scanobject : request.params[1].get_array().getValues()) {
|
||||
FlatSigningProvider provider;
|
||||
std::vector<CScript> scripts = EvalDescriptorStringOrObject(scanobject, provider);
|
||||
for (const CScript& script : scripts) {
|
||||
needle_set.emplace(script.begin(), script.end());
|
||||
}
|
||||
}
|
||||
UniValue blocks(UniValue::VARR);
|
||||
const int amount_per_chunk = 10000;
|
||||
const CBlockIndex* start_index = block; // for remembering the start of a blockfilter range
|
||||
std::vector<BlockFilter> filters;
|
||||
const CBlockIndex* start_block = block; // for progress reporting
|
||||
const int total_blocks_to_process = stop_block->nHeight - start_block->nHeight;
|
||||
|
||||
g_scanfilter_should_abort_scan = false;
|
||||
g_scanfilter_progress = 0;
|
||||
g_scanfilter_progress_height = start_block->nHeight;
|
||||
|
||||
while (block) {
|
||||
node.rpc_interruption_point(); // allow a clean shutdown
|
||||
if (g_scanfilter_should_abort_scan) {
|
||||
LogPrintf("scanblocks RPC aborted at height %d.\n", block->nHeight);
|
||||
break;
|
||||
}
|
||||
const CBlockIndex* next = nullptr;
|
||||
{
|
||||
LOCK(cs_main);
|
||||
CChain& active_chain = chainman.ActiveChain();
|
||||
next = active_chain.Next(block);
|
||||
if (block == stop_block) next = nullptr;
|
||||
}
|
||||
if (start_index->nHeight + amount_per_chunk == block->nHeight || next == nullptr) {
|
||||
LogPrint(BCLog::RPC, "Fetching blockfilters from height %d to height %d.\n", start_index->nHeight, block->nHeight);
|
||||
if (index->LookupFilterRange(start_index->nHeight, block, filters)) {
|
||||
for (const BlockFilter& filter : filters) {
|
||||
// compare the elements-set with each filter
|
||||
if (filter.GetFilter().MatchAny(needle_set)) {
|
||||
blocks.push_back(filter.GetBlockHash().GetHex());
|
||||
LogPrint(BCLog::RPC, "scanblocks: found match in %s\n", filter.GetBlockHash().GetHex());
|
||||
}
|
||||
}
|
||||
}
|
||||
start_index = block;
|
||||
|
||||
// update progress
|
||||
int blocks_processed = block->nHeight - start_block->nHeight;
|
||||
if (total_blocks_to_process > 0) { // avoid division by zero
|
||||
g_scanfilter_progress = (int)(100.0 / total_blocks_to_process * blocks_processed);
|
||||
} else {
|
||||
g_scanfilter_progress = 100;
|
||||
}
|
||||
g_scanfilter_progress_height = block->nHeight;
|
||||
}
|
||||
block = next;
|
||||
}
|
||||
ret.pushKV("from_height", start_block->nHeight);
|
||||
ret.pushKV("to_height", g_scanfilter_progress_height.load());
|
||||
ret.pushKV("relevant_blocks", blocks);
|
||||
}
|
||||
else {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid command");
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static RPCHelpMan getblockfilter()
|
||||
{
|
||||
return RPCHelpMan{"getblockfilter",
|
||||
@ -2439,6 +2636,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t)
|
||||
{"blockchain", &verifychain},
|
||||
{"blockchain", &preciousblock},
|
||||
{"blockchain", &scantxoutset},
|
||||
{"blockchain", &scanblocks},
|
||||
{"blockchain", &getblockfilter},
|
||||
{"hidden", &invalidateblock},
|
||||
{"hidden", &reconsiderblock},
|
||||
|
@ -83,6 +83,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
||||
{ "sendmany", 8, "fee_rate"},
|
||||
{ "sendmany", 9, "verbose" },
|
||||
{ "deriveaddresses", 1, "range" },
|
||||
{ "scanblocks", 1, "scanobjects" },
|
||||
{ "scanblocks", 2, "start_height" },
|
||||
{ "scanblocks", 3, "stop_height" },
|
||||
{ "scantxoutset", 1, "scanobjects" },
|
||||
{ "addmultisigaddress", 0, "nrequired" },
|
||||
{ "addmultisigaddress", 1, "keys" },
|
||||
|
Reference in New Issue
Block a user