mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-07-28 14:53:03 +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()
|
static RPCHelpMan getblockfilter()
|
||||||
{
|
{
|
||||||
return RPCHelpMan{"getblockfilter",
|
return RPCHelpMan{"getblockfilter",
|
||||||
@@ -2439,6 +2636,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t)
|
|||||||
{"blockchain", &verifychain},
|
{"blockchain", &verifychain},
|
||||||
{"blockchain", &preciousblock},
|
{"blockchain", &preciousblock},
|
||||||
{"blockchain", &scantxoutset},
|
{"blockchain", &scantxoutset},
|
||||||
|
{"blockchain", &scanblocks},
|
||||||
{"blockchain", &getblockfilter},
|
{"blockchain", &getblockfilter},
|
||||||
{"hidden", &invalidateblock},
|
{"hidden", &invalidateblock},
|
||||||
{"hidden", &reconsiderblock},
|
{"hidden", &reconsiderblock},
|
||||||
|
@@ -83,6 +83,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||||||
{ "sendmany", 8, "fee_rate"},
|
{ "sendmany", 8, "fee_rate"},
|
||||||
{ "sendmany", 9, "verbose" },
|
{ "sendmany", 9, "verbose" },
|
||||||
{ "deriveaddresses", 1, "range" },
|
{ "deriveaddresses", 1, "range" },
|
||||||
|
{ "scanblocks", 1, "scanobjects" },
|
||||||
|
{ "scanblocks", 2, "start_height" },
|
||||||
|
{ "scanblocks", 3, "stop_height" },
|
||||||
{ "scantxoutset", 1, "scanobjects" },
|
{ "scantxoutset", 1, "scanobjects" },
|
||||||
{ "addmultisigaddress", 0, "nrequired" },
|
{ "addmultisigaddress", 0, "nrequired" },
|
||||||
{ "addmultisigaddress", 1, "keys" },
|
{ "addmultisigaddress", 1, "keys" },
|
||||||
|
Reference in New Issue
Block a user