rpc: add scanblocks - scan for relevant blocks with descriptors

Co-authored-by: James O'Beirne <james.obeirne@gmail.com>
This commit is contained in:
Jonas Schnelli
2021-01-08 11:40:49 +01:00
committed by James O'Beirne
parent a4258f6e81
commit 6ef2566b68
2 changed files with 201 additions and 0 deletions

View File

@ -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},

View File

@ -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" },