Skip to content

Commit b2af068

Browse files
committed
Merge bitcoin/bitcoin#30708: rpc: add getdescriptoractivity
37a5c5d doc: update descriptors.md for getdescriptoractivity (James O'Beirne) ee3ce6a test: rpc: add no address case for getdescriptoractivity (James O'Beirne) 811f76f rpc: add getdescriptoractivity (James O'Beirne) 25fe087 rpc: move-only: move ScriptPubKeyDoc to utils (James O'Beirne) Pull request description: The RPC command `scanblocks` provides a useful way to get a set of blockhashes that have activity relevant to a set of descriptors (`relevant_blocks`). However actually extracting the activity from those blocks is left as an exercise to the end user. This process involves not only generating the (potentially ranged) set of scripts for the descriptor set on the client side (maybe via `deriveaddresses`), but then the user must retrieve each block's contents one-by-one using `getblock <hash>`, which is transmitted over a network link. And that's all before they perform the actual search over block content. There's even more work required to incorporate unconfirmed transactions. This PR introduces an RPC `getdescriptoractivity` that [dovetails](https://bitcoin-irc.chaincode.com/bitcoin-core-dev/2024-08-16#1046393;) with `scanblocks` output, handling the process described above. Users specify the blockhashes (perhaps from `relevant_blocks`) and a set of descriptors; they are then given all spend/receive activity in that set of blocks. This is a very useful tool when implementing lightweight wallets that want neither to require a third-party indexer like electrs, nor the overhead of creating and managing watch-only wallets in Core. This allows Core to be more easily used in a "stateless" manner by wallets, with potentially many nodes interchangeably acting as backends. ### Example usage ``` % ./src/bitcoin-cli scanblocks start \ '["addr(bc1p0cp0vyag6snlta2l7c4am3rue7eef9f72l7uhx52m4v27vfydx9s8tfs7t)"]' \ 857263 { "from_height": 857263, "to_height": 858263, "relevant_blocks": [ "00000000000000000002bc5cc78f5b0913a5230a8f4b0d5060bc9a60900a5a88", "00000000000000000001c5291ed6a40c06d3db5c8fb738567654b24a14b24ecb" ], "completed": true } % ./src/bitcoin-cli getdescriptoractivity \ '["00000000000000000002bc5cc78f5b0913a5230a8f4b0d5060bc9a60900a5a88", "00000000000000000001c5291ed6a40c06d3db5c8fb738567654b24a14b24ecb"]' \ '["addr(bc1p0cp0vyag6snlta2l7c4am3rue7eef9f72l7uhx52m4v27vfydx9s8tfs7t)"]' { "activity": [ { "type": "receive", "amount": 0.00002900, "blockhash": "00000000000000000002bc5cc78f5b0913a5230a8f4b0d5060bc9a60900a5a88", "height": 857907, "txid": "c9d34f202c1f66d80cae76f305350f5fdde910b97cf6ae6bf79f5bcf2a337d06", "vout": 254, "output_spk": { "asm": "1 7e02f613a8d427f5f55ff62bddc47ccfb394953e57fdcb9a8add58af3124698b", "desc": "rawtr(7e02f613a8d427f5f55ff62bddc47ccfb394953e57fdcb9a8add58af3124698b)#yewcd80j", "hex": "51207e02f613a8d427f5f55ff62bddc47ccfb394953e57fdcb9a8add58af3124698b", "address": "bc1p0cp0vyag6snlta2l7c4am3rue7eef9f72l7uhx52m4v27vfydx9s8tfs7t", "type": "witness_v1_taproot" } }, { "type": "spend", "amount": 0.00002900, "blockhash": "00000000000000000001c5291ed6a40c06d3db5c8fb738567654b24a14b24ecb", "height": 858260, "spend_txid": "7f61d1b248d4ee46376f9c6df272f63fbb0c17039381fb23ca5d90473b823c36", "spend_vin": 0, "prevout_txid": "c9d34f202c1f66d80cae76f305350f5fdde910b97cf6ae6bf79f5bcf2a337d06", "prevout_vout": 254, "prevout_spk": { "asm": "1 7e02f613a8d427f5f55ff62bddc47ccfb394953e57fdcb9a8add58af3124698b", "desc": "rawtr(7e02f613a8d427f5f55ff62bddc47ccfb394953e57fdcb9a8add58af3124698b)#yewcd80j", "hex": "51207e02f613a8d427f5f55ff62bddc47ccfb394953e57fdcb9a8add58af3124698b", "address": "bc1p0cp0vyag6snlta2l7c4am3rue7eef9f72l7uhx52m4v27vfydx9s8tfs7t", "type": "witness_v1_taproot" } } ] } ``` ACKs for top commit: instagibbs: reACK 37a5c5d achow101: ACK 37a5c5d tdb3: Code review and light retest ACK 37a5c5d rkrux: re-ACK 37a5c5d Tree-SHA512: 04aa51e329c6c2ed72464b9886281d5ebd7511a8a8e184ea81249033a4dad535a12829b1010afc2da79b344ea8b5ab8ed47e426d0bf2eb78ab395d20b1da8dbb
2 parents 144f98d + 37a5c5d commit b2af068

File tree

11 files changed

+486
-11
lines changed

11 files changed

+486
-11
lines changed

doc/descriptors.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ Supporting RPCs are:
2323
- `listdescriptors` outputs descriptors imported into a descriptor wallet (since v22).
2424
- `scanblocks` takes as input descriptors to scan for in blocks and returns the
2525
relevant blockhashes (since v25).
26+
- `getdescriptoractivity` takes as input descriptors and blockhashes (as output
27+
by `scanblocks`) and returns rich event data related to spends or receives associated
28+
with the given descriptors.
2629

2730
This document describes the language. For the specifics on usage, see the RPC
2831
documentation for the functions mentioned above.

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;
@@ -2586,6 +2588,235 @@ static RPCHelpMan scanblocks()
25862588
};
25872589
}
25882590

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

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,6 @@ static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue&
8282
}
8383
}
8484

85-
static std::vector<RPCResult> ScriptPubKeyDoc() {
86-
return
87-
{
88-
{RPCResult::Type::STR, "asm", "Disassembly of the output script"},
89-
{RPCResult::Type::STR, "desc", "Inferred descriptor for the output"},
90-
{RPCResult::Type::STR_HEX, "hex", "The raw output script bytes, hex-encoded"},
91-
{RPCResult::Type::STR, "address", /*optional=*/true, "The Bitcoin address (only if a well-defined address exists)"},
92-
{RPCResult::Type::STR, "type", "The type (one of: " + GetAllOutputTypes() + ")"},
93-
};
94-
}
95-
9685
static std::vector<RPCResult> DecodeTxDoc(const std::string& txid_field_doc)
9786
{
9887
return {

src/rpc/util.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,3 +1407,14 @@ void PushWarnings(const std::vector<bilingual_str>& warnings, UniValue& obj)
14071407
if (warnings.empty()) return;
14081408
obj.pushKV("warnings", BilingualStringsToUniValue(warnings));
14091409
}
1410+
1411+
std::vector<RPCResult> ScriptPubKeyDoc() {
1412+
return
1413+
{
1414+
{RPCResult::Type::STR, "asm", "Disassembly of the output script"},
1415+
{RPCResult::Type::STR, "desc", "Inferred descriptor for the output"},
1416+
{RPCResult::Type::STR_HEX, "hex", "The raw output script bytes, hex-encoded"},
1417+
{RPCResult::Type::STR, "address", /*optional=*/true, "The Bitcoin address (only if a well-defined address exists)"},
1418+
{RPCResult::Type::STR, "type", "The type (one of: " + GetAllOutputTypes() + ")"},
1419+
};
1420+
}

src/rpc/util.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,4 +514,6 @@ class RPCHelpMan
514514
void PushWarnings(const UniValue& warnings, UniValue& obj);
515515
void PushWarnings(const std::vector<bilingual_str>& warnings, UniValue& obj);
516516

517+
std::vector<RPCResult> ScriptPubKeyDoc();
518+
517519
#endif // BITCOIN_RPC_UTIL_H

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)