Skip to content

Commit 811f76f

Browse files
committed
rpc: add getdescriptoractivity
1 parent 25fe087 commit 811f76f

File tree

7 files changed

+441
-0
lines changed

7 files changed

+441
-0
lines changed

doc/release-notes-30708.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
New RPCs
2+
--------
3+
4+
- `getdescriptoractivity` can be used to find all spend/receive activity relevant to
5+
a given set of descriptors within a set of specified blocks. This call can be used with
6+
`scanblocks` to lessen the need for additional indexing programs.

src/core_io.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#include <string>
1212
#include <vector>
13+
#include <optional>
1314

1415
class CBlock;
1516
class CBlockHeader;

src/rpc/blockchain.cpp

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@
5555
#include <stdint.h>
5656

5757
#include <condition_variable>
58+
#include <iterator>
5859
#include <memory>
5960
#include <mutex>
6061
#include <optional>
62+
#include <vector>
6163

6264
using kernel::CCoinsStats;
6365
using kernel::CoinStatsHashType;
@@ -2585,6 +2587,235 @@ static RPCHelpMan scanblocks()
25852587
};
25862588
}
25872589

2590+
static RPCHelpMan getdescriptoractivity()
2591+
{
2592+
return RPCHelpMan{"getdescriptoractivity",
2593+
"\nGet spend and receive activity associated with a set of descriptors for a set of blocks. "
2594+
"This command pairs well with the `relevant_blocks` output of `scanblocks()`.\n"
2595+
"This call may take several minutes. If you encounter timeouts, try specifying no RPC timeout (bitcoin-cli -rpcclienttimeout=0)",
2596+
{
2597+
RPCArg{"blockhashes", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "The list of blockhashes to examine for activity. Order doesn't matter. Must be along main chain or an error is thrown.\n", {
2598+
{"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A valid blockhash"},
2599+
}},
2600+
scan_objects_arg_desc,
2601+
{"include_mempool", RPCArg::Type::BOOL, RPCArg::Default{true}, "Whether to include unconfirmed activity"},
2602+
},
2603+
RPCResult{
2604+
RPCResult::Type::OBJ, "", "", {
2605+
{RPCResult::Type::ARR, "activity", "events", {
2606+
{RPCResult::Type::OBJ, "", "", {
2607+
{RPCResult::Type::STR, "type", "always 'spend'"},
2608+
{RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " of the spent output"},
2609+
{RPCResult::Type::STR_HEX, "blockhash", /*optional=*/true, "The blockhash this spend appears in (omitted if unconfirmed)"},
2610+
{RPCResult::Type::NUM, "height", /*optional=*/true, "Height of the spend (omitted if unconfirmed)"},
2611+
{RPCResult::Type::STR_HEX, "spend_txid", "The txid of the spending transaction"},
2612+
{RPCResult::Type::NUM, "spend_vout", "The vout of the spend"},
2613+
{RPCResult::Type::STR_HEX, "prevout_txid", "The txid of the prevout"},
2614+
{RPCResult::Type::NUM, "prevout_vout", "The vout of the prevout"},
2615+
{RPCResult::Type::OBJ, "prevout_spk", "", ScriptPubKeyDoc()},
2616+
}},
2617+
{RPCResult::Type::OBJ, "", "", {
2618+
{RPCResult::Type::STR, "type", "always 'receive'"},
2619+
{RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " of the new output"},
2620+
{RPCResult::Type::STR_HEX, "blockhash", /*optional=*/true, "The block that this receive is in (omitted if unconfirmed)"},
2621+
{RPCResult::Type::NUM, "height", /*optional=*/true, "The height of the receive (omitted if unconfirmed)"},
2622+
{RPCResult::Type::STR_HEX, "txid", "The txid of the receiving transaction"},
2623+
{RPCResult::Type::NUM, "vout", "The vout of the receiving output"},
2624+
{RPCResult::Type::OBJ, "output_spk", "", ScriptPubKeyDoc()},
2625+
}},
2626+
// TODO is the skip_type_check avoidable with a heterogeneous ARR?
2627+
}, /*skip_type_check=*/true},
2628+
},
2629+
},
2630+
RPCExamples{
2631+
HelpExampleCli("getdescriptoractivity", "'[\"000000000000000000001347062c12fded7c528943c8ce133987e2e2f5a840ee\"]' '[\"addr(bc1qzl6nsgqzu89a66l50cvwapnkw5shh23zarqkw9)\"]'")
2632+
},
2633+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
2634+
{
2635+
UniValue ret(UniValue::VOBJ);
2636+
UniValue activity(UniValue::VARR);
2637+
NodeContext& node = EnsureAnyNodeContext(request.context);
2638+
ChainstateManager& chainman = EnsureChainman(node);
2639+
2640+
struct CompareByHeightAscending {
2641+
bool operator()(const CBlockIndex* a, const CBlockIndex* b) const {
2642+
return a->nHeight < b->nHeight;
2643+
}
2644+
};
2645+
2646+
std::set<const CBlockIndex*, CompareByHeightAscending> blockindexes_sorted;
2647+
2648+
{
2649+
// Validate all given blockhashes, and ensure blocks are along a single chain.
2650+
LOCK(::cs_main);
2651+
for (const UniValue& blockhash : request.params[0].get_array().getValues()) {
2652+
uint256 bhash = ParseHashV(blockhash, "blockhash");
2653+
CBlockIndex* pindex = chainman.m_blockman.LookupBlockIndex(bhash);
2654+
if (!pindex) {
2655+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
2656+
}
2657+
if (!chainman.ActiveChain().Contains(pindex)) {
2658+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Block is not in main chain");
2659+
}
2660+
blockindexes_sorted.insert(pindex);
2661+
}
2662+
}
2663+
2664+
std::set<CScript> scripts_to_watch;
2665+
2666+
// Determine scripts to watch.
2667+
for (const UniValue& scanobject : request.params[1].get_array().getValues()) {
2668+
FlatSigningProvider provider;
2669+
std::vector<CScript> scripts = EvalDescriptorStringOrObject(scanobject, provider);
2670+
2671+
for (const CScript& script : scripts) {
2672+
scripts_to_watch.insert(script);
2673+
}
2674+
}
2675+
2676+
const auto AddSpend = [&](
2677+
const CScript& spk,
2678+
const CAmount val,
2679+
const CTransactionRef& tx,
2680+
int vin,
2681+
const CTxIn& txin,
2682+
const CBlockIndex* index
2683+
) {
2684+
UniValue event(UniValue::VOBJ);
2685+
UniValue spkUv(UniValue::VOBJ);
2686+
ScriptToUniv(spk, /*out=*/spkUv, /*include_hex=*/true, /*include_address=*/true);
2687+
2688+
event.pushKV("type", "spend");
2689+
event.pushKV("amount", ValueFromAmount(val));
2690+
if (index) {
2691+
event.pushKV("blockhash", index->GetBlockHash().ToString());
2692+
event.pushKV("height", index->nHeight);
2693+
}
2694+
event.pushKV("spend_txid", tx->GetHash().ToString());
2695+
event.pushKV("spend_vin", vin);
2696+
event.pushKV("prevout_txid", txin.prevout.hash.ToString());
2697+
event.pushKV("prevout_vout", txin.prevout.n);
2698+
event.pushKV("prevout_spk", spkUv);
2699+
2700+
return event;
2701+
};
2702+
2703+
const auto AddReceive = [&](const CTxOut& txout, const CBlockIndex* index, int vout, const CTransactionRef& tx) {
2704+
UniValue event(UniValue::VOBJ);
2705+
UniValue spkUv(UniValue::VOBJ);
2706+
ScriptToUniv(txout.scriptPubKey, /*out=*/spkUv, /*include_hex=*/true, /*include_address=*/true);
2707+
2708+
event.pushKV("type", "receive");
2709+
event.pushKV("amount", ValueFromAmount(txout.nValue));
2710+
if (index) {
2711+
event.pushKV("blockhash", index->GetBlockHash().ToString());
2712+
event.pushKV("height", index->nHeight);
2713+
}
2714+
event.pushKV("txid", tx->GetHash().ToString());
2715+
event.pushKV("vout", vout);
2716+
event.pushKV("output_spk", spkUv);
2717+
2718+
return event;
2719+
};
2720+
2721+
BlockManager* blockman;
2722+
Chainstate& active_chainstate = chainman.ActiveChainstate();
2723+
{
2724+
LOCK(::cs_main);
2725+
blockman = CHECK_NONFATAL(&active_chainstate.m_blockman);
2726+
}
2727+
2728+
for (const CBlockIndex* blockindex : blockindexes_sorted) {
2729+
const CBlock block{GetBlockChecked(chainman.m_blockman, *blockindex)};
2730+
const CBlockUndo block_undo{GetUndoChecked(*blockman, *blockindex)};
2731+
2732+
for (size_t i = 0; i < block.vtx.size(); ++i) {
2733+
const auto& tx = block.vtx.at(i);
2734+
2735+
if (!tx->IsCoinBase()) {
2736+
// skip coinbase; spends can't happen there.
2737+
const auto& txundo = block_undo.vtxundo.at(i - 1);
2738+
2739+
for (size_t vin_idx = 0; vin_idx < tx->vin.size(); ++vin_idx) {
2740+
const auto& coin = txundo.vprevout.at(vin_idx);
2741+
const auto& txin = tx->vin.at(vin_idx);
2742+
if (scripts_to_watch.contains(coin.out.scriptPubKey)) {
2743+
activity.push_back(AddSpend(
2744+
coin.out.scriptPubKey, coin.out.nValue, tx, vin_idx, txin, blockindex));
2745+
}
2746+
}
2747+
}
2748+
2749+
for (size_t vout_idx = 0; vout_idx < tx->vout.size(); ++vout_idx) {
2750+
const auto& vout = tx->vout.at(vout_idx);
2751+
if (scripts_to_watch.contains(vout.scriptPubKey)) {
2752+
activity.push_back(AddReceive(vout, blockindex, vout_idx, tx));
2753+
}
2754+
}
2755+
}
2756+
}
2757+
2758+
bool search_mempool = true;
2759+
if (!request.params[2].isNull()) {
2760+
search_mempool = request.params[2].get_bool();
2761+
}
2762+
2763+
if (search_mempool) {
2764+
const CTxMemPool& mempool = EnsureMemPool(node);
2765+
LOCK(::cs_main);
2766+
LOCK(mempool.cs);
2767+
const CCoinsViewCache& coins_view = &active_chainstate.CoinsTip();
2768+
2769+
for (const CTxMemPoolEntry& e : mempool.entryAll()) {
2770+
const auto& tx = e.GetSharedTx();
2771+
2772+
for (size_t vin_idx = 0; vin_idx < tx->vin.size(); ++vin_idx) {
2773+
CScript scriptPubKey;
2774+
CAmount value;
2775+
const auto& txin = tx->vin.at(vin_idx);
2776+
std::optional<Coin> coin = coins_view.GetCoin(txin.prevout);
2777+
2778+
// Check if the previous output is in the chain
2779+
if (!coin) {
2780+
// If not found in the chain, check the mempool. Likely, this is a
2781+
// child transaction of another transaction in the mempool.
2782+
CTransactionRef prev_tx = CHECK_NONFATAL(mempool.get(txin.prevout.hash));
2783+
2784+
if (txin.prevout.n >= prev_tx->vout.size()) {
2785+
throw std::runtime_error("Invalid output index");
2786+
}
2787+
const CTxOut& out = prev_tx->vout[txin.prevout.n];
2788+
scriptPubKey = out.scriptPubKey;
2789+
value = out.nValue;
2790+
} else {
2791+
// Coin found in the chain
2792+
const CTxOut& out = coin->out;
2793+
scriptPubKey = out.scriptPubKey;
2794+
value = out.nValue;
2795+
}
2796+
2797+
if (scripts_to_watch.contains(scriptPubKey)) {
2798+
UniValue event(UniValue::VOBJ);
2799+
activity.push_back(AddSpend(
2800+
scriptPubKey, value, tx, vin_idx, txin, nullptr));
2801+
}
2802+
}
2803+
2804+
for (size_t vout_idx = 0; vout_idx < tx->vout.size(); ++vout_idx) {
2805+
const auto& vout = tx->vout.at(vout_idx);
2806+
if (scripts_to_watch.contains(vout.scriptPubKey)) {
2807+
activity.push_back(AddReceive(vout, nullptr, vout_idx, tx));
2808+
}
2809+
}
2810+
}
2811+
}
2812+
2813+
ret.pushKV("activity", activity);
2814+
return ret;
2815+
},
2816+
};
2817+
}
2818+
25882819
static RPCHelpMan getblockfilter()
25892820
{
25902821
return RPCHelpMan{"getblockfilter",
@@ -3152,6 +3383,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t)
31523383
{"blockchain", &preciousblock},
31533384
{"blockchain", &scantxoutset},
31543385
{"blockchain", &scanblocks},
3386+
{"blockchain", &getdescriptoractivity},
31553387
{"blockchain", &getblockfilter},
31563388
{"blockchain", &dumptxoutset},
31573389
{"blockchain", &loadtxoutset},

src/rpc/client.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
9292
{ "scanblocks", 3, "stop_height" },
9393
{ "scanblocks", 5, "options" },
9494
{ "scanblocks", 5, "filter_false_positives" },
95+
{ "getdescriptoractivity", 0, "blockhashes" },
96+
{ "getdescriptoractivity", 1, "scanobjects" },
97+
{ "getdescriptoractivity", 2, "include_mempool" },
9598
{ "scantxoutset", 1, "scanobjects" },
9699
{ "addmultisigaddress", 0, "nrequired" },
97100
{ "addmultisigaddress", 1, "keys" },

src/test/fuzz/rpc.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
130130
"getchaintxstats",
131131
"getconnectioncount",
132132
"getdeploymentinfo",
133+
"getdescriptoractivity",
133134
"getdescriptorinfo",
134135
"getdifficulty",
135136
"getindexinfo",

0 commit comments

Comments
 (0)