Skip to content

Commit

Permalink
Merge bitcoin/bitcoin#30708: rpc: add getdescriptoractivity
Browse files Browse the repository at this point in the history
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
  • Loading branch information
achow101 committed Nov 27, 2024
2 parents 144f98d + 37a5c5d commit b2af068
Show file tree
Hide file tree
Showing 11 changed files with 486 additions and 11 deletions.
3 changes: 3 additions & 0 deletions doc/descriptors.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ Supporting RPCs are:
- `listdescriptors` outputs descriptors imported into a descriptor wallet (since v22).
- `scanblocks` takes as input descriptors to scan for in blocks and returns the
relevant blockhashes (since v25).
- `getdescriptoractivity` takes as input descriptors and blockhashes (as output
by `scanblocks`) and returns rich event data related to spends or receives associated
with the given descriptors.

This document describes the language. For the specifics on usage, see the RPC
documentation for the functions mentioned above.
Expand Down
6 changes: 6 additions & 0 deletions doc/release-notes-30708.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
New RPCs
--------

- `getdescriptoractivity` can be used to find all spend/receive activity relevant to
a given set of descriptors within a set of specified blocks. This call can be used with
`scanblocks` to lessen the need for additional indexing programs.
1 change: 1 addition & 0 deletions src/core_io.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include <string>
#include <vector>
#include <optional>

class CBlock;
class CBlockHeader;
Expand Down
232 changes: 232 additions & 0 deletions src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@
#include <stdint.h>

#include <condition_variable>
#include <iterator>
#include <memory>
#include <mutex>
#include <optional>
#include <vector>

using kernel::CCoinsStats;
using kernel::CoinStatsHashType;
Expand Down Expand Up @@ -2586,6 +2588,235 @@ static RPCHelpMan scanblocks()
};
}

static RPCHelpMan getdescriptoractivity()
{
return RPCHelpMan{"getdescriptoractivity",
"\nGet spend and receive activity associated with a set of descriptors for a set of blocks. "
"This command pairs well with the `relevant_blocks` output of `scanblocks()`.\n"
"This call may take several minutes. If you encounter timeouts, try specifying no RPC timeout (bitcoin-cli -rpcclienttimeout=0)",
{
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", {
{"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A valid blockhash"},
}},
scan_objects_arg_desc,
{"include_mempool", RPCArg::Type::BOOL, RPCArg::Default{true}, "Whether to include unconfirmed activity"},
},
RPCResult{
RPCResult::Type::OBJ, "", "", {
{RPCResult::Type::ARR, "activity", "events", {
{RPCResult::Type::OBJ, "", "", {
{RPCResult::Type::STR, "type", "always 'spend'"},
{RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " of the spent output"},
{RPCResult::Type::STR_HEX, "blockhash", /*optional=*/true, "The blockhash this spend appears in (omitted if unconfirmed)"},
{RPCResult::Type::NUM, "height", /*optional=*/true, "Height of the spend (omitted if unconfirmed)"},
{RPCResult::Type::STR_HEX, "spend_txid", "The txid of the spending transaction"},
{RPCResult::Type::NUM, "spend_vout", "The vout of the spend"},
{RPCResult::Type::STR_HEX, "prevout_txid", "The txid of the prevout"},
{RPCResult::Type::NUM, "prevout_vout", "The vout of the prevout"},
{RPCResult::Type::OBJ, "prevout_spk", "", ScriptPubKeyDoc()},
}},
{RPCResult::Type::OBJ, "", "", {
{RPCResult::Type::STR, "type", "always 'receive'"},
{RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " of the new output"},
{RPCResult::Type::STR_HEX, "blockhash", /*optional=*/true, "The block that this receive is in (omitted if unconfirmed)"},
{RPCResult::Type::NUM, "height", /*optional=*/true, "The height of the receive (omitted if unconfirmed)"},
{RPCResult::Type::STR_HEX, "txid", "The txid of the receiving transaction"},
{RPCResult::Type::NUM, "vout", "The vout of the receiving output"},
{RPCResult::Type::OBJ, "output_spk", "", ScriptPubKeyDoc()},
}},
// TODO is the skip_type_check avoidable with a heterogeneous ARR?
}, /*skip_type_check=*/true},
},
},
RPCExamples{
HelpExampleCli("getdescriptoractivity", "'[\"000000000000000000001347062c12fded7c528943c8ce133987e2e2f5a840ee\"]' '[\"addr(bc1qzl6nsgqzu89a66l50cvwapnkw5shh23zarqkw9)\"]'")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
UniValue ret(UniValue::VOBJ);
UniValue activity(UniValue::VARR);
NodeContext& node = EnsureAnyNodeContext(request.context);
ChainstateManager& chainman = EnsureChainman(node);

struct CompareByHeightAscending {
bool operator()(const CBlockIndex* a, const CBlockIndex* b) const {
return a->nHeight < b->nHeight;
}
};

std::set<const CBlockIndex*, CompareByHeightAscending> blockindexes_sorted;

{
// Validate all given blockhashes, and ensure blocks are along a single chain.
LOCK(::cs_main);
for (const UniValue& blockhash : request.params[0].get_array().getValues()) {
uint256 bhash = ParseHashV(blockhash, "blockhash");
CBlockIndex* pindex = chainman.m_blockman.LookupBlockIndex(bhash);
if (!pindex) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
}
if (!chainman.ActiveChain().Contains(pindex)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Block is not in main chain");
}
blockindexes_sorted.insert(pindex);
}
}

std::set<CScript> scripts_to_watch;

// Determine scripts to watch.
for (const UniValue& scanobject : request.params[1].get_array().getValues()) {
FlatSigningProvider provider;
std::vector<CScript> scripts = EvalDescriptorStringOrObject(scanobject, provider);

for (const CScript& script : scripts) {
scripts_to_watch.insert(script);
}
}

const auto AddSpend = [&](
const CScript& spk,
const CAmount val,
const CTransactionRef& tx,
int vin,
const CTxIn& txin,
const CBlockIndex* index
) {
UniValue event(UniValue::VOBJ);
UniValue spkUv(UniValue::VOBJ);
ScriptToUniv(spk, /*out=*/spkUv, /*include_hex=*/true, /*include_address=*/true);

event.pushKV("type", "spend");
event.pushKV("amount", ValueFromAmount(val));
if (index) {
event.pushKV("blockhash", index->GetBlockHash().ToString());
event.pushKV("height", index->nHeight);
}
event.pushKV("spend_txid", tx->GetHash().ToString());
event.pushKV("spend_vin", vin);
event.pushKV("prevout_txid", txin.prevout.hash.ToString());
event.pushKV("prevout_vout", txin.prevout.n);
event.pushKV("prevout_spk", spkUv);

return event;
};

const auto AddReceive = [&](const CTxOut& txout, const CBlockIndex* index, int vout, const CTransactionRef& tx) {
UniValue event(UniValue::VOBJ);
UniValue spkUv(UniValue::VOBJ);
ScriptToUniv(txout.scriptPubKey, /*out=*/spkUv, /*include_hex=*/true, /*include_address=*/true);

event.pushKV("type", "receive");
event.pushKV("amount", ValueFromAmount(txout.nValue));
if (index) {
event.pushKV("blockhash", index->GetBlockHash().ToString());
event.pushKV("height", index->nHeight);
}
event.pushKV("txid", tx->GetHash().ToString());
event.pushKV("vout", vout);
event.pushKV("output_spk", spkUv);

return event;
};

BlockManager* blockman;
Chainstate& active_chainstate = chainman.ActiveChainstate();
{
LOCK(::cs_main);
blockman = CHECK_NONFATAL(&active_chainstate.m_blockman);
}

for (const CBlockIndex* blockindex : blockindexes_sorted) {
const CBlock block{GetBlockChecked(chainman.m_blockman, *blockindex)};
const CBlockUndo block_undo{GetUndoChecked(*blockman, *blockindex)};

for (size_t i = 0; i < block.vtx.size(); ++i) {
const auto& tx = block.vtx.at(i);

if (!tx->IsCoinBase()) {
// skip coinbase; spends can't happen there.
const auto& txundo = block_undo.vtxundo.at(i - 1);

for (size_t vin_idx = 0; vin_idx < tx->vin.size(); ++vin_idx) {
const auto& coin = txundo.vprevout.at(vin_idx);
const auto& txin = tx->vin.at(vin_idx);
if (scripts_to_watch.contains(coin.out.scriptPubKey)) {
activity.push_back(AddSpend(
coin.out.scriptPubKey, coin.out.nValue, tx, vin_idx, txin, blockindex));
}
}
}

for (size_t vout_idx = 0; vout_idx < tx->vout.size(); ++vout_idx) {
const auto& vout = tx->vout.at(vout_idx);
if (scripts_to_watch.contains(vout.scriptPubKey)) {
activity.push_back(AddReceive(vout, blockindex, vout_idx, tx));
}
}
}
}

bool search_mempool = true;
if (!request.params[2].isNull()) {
search_mempool = request.params[2].get_bool();
}

if (search_mempool) {
const CTxMemPool& mempool = EnsureMemPool(node);
LOCK(::cs_main);
LOCK(mempool.cs);
const CCoinsViewCache& coins_view = &active_chainstate.CoinsTip();

for (const CTxMemPoolEntry& e : mempool.entryAll()) {
const auto& tx = e.GetSharedTx();

for (size_t vin_idx = 0; vin_idx < tx->vin.size(); ++vin_idx) {
CScript scriptPubKey;
CAmount value;
const auto& txin = tx->vin.at(vin_idx);
std::optional<Coin> coin = coins_view.GetCoin(txin.prevout);

// Check if the previous output is in the chain
if (!coin) {
// If not found in the chain, check the mempool. Likely, this is a
// child transaction of another transaction in the mempool.
CTransactionRef prev_tx = CHECK_NONFATAL(mempool.get(txin.prevout.hash));

if (txin.prevout.n >= prev_tx->vout.size()) {
throw std::runtime_error("Invalid output index");
}
const CTxOut& out = prev_tx->vout[txin.prevout.n];
scriptPubKey = out.scriptPubKey;
value = out.nValue;
} else {
// Coin found in the chain
const CTxOut& out = coin->out;
scriptPubKey = out.scriptPubKey;
value = out.nValue;
}

if (scripts_to_watch.contains(scriptPubKey)) {
UniValue event(UniValue::VOBJ);
activity.push_back(AddSpend(
scriptPubKey, value, tx, vin_idx, txin, nullptr));
}
}

for (size_t vout_idx = 0; vout_idx < tx->vout.size(); ++vout_idx) {
const auto& vout = tx->vout.at(vout_idx);
if (scripts_to_watch.contains(vout.scriptPubKey)) {
activity.push_back(AddReceive(vout, nullptr, vout_idx, tx));
}
}
}
}

ret.pushKV("activity", activity);
return ret;
},
};
}

static RPCHelpMan getblockfilter()
{
return RPCHelpMan{"getblockfilter",
Expand Down Expand Up @@ -3153,6 +3384,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t)
{"blockchain", &preciousblock},
{"blockchain", &scantxoutset},
{"blockchain", &scanblocks},
{"blockchain", &getdescriptoractivity},
{"blockchain", &getblockfilter},
{"blockchain", &dumptxoutset},
{"blockchain", &loadtxoutset},
Expand Down
3 changes: 3 additions & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "scanblocks", 3, "stop_height" },
{ "scanblocks", 5, "options" },
{ "scanblocks", 5, "filter_false_positives" },
{ "getdescriptoractivity", 0, "blockhashes" },
{ "getdescriptoractivity", 1, "scanobjects" },
{ "getdescriptoractivity", 2, "include_mempool" },
{ "scantxoutset", 1, "scanobjects" },
{ "addmultisigaddress", 0, "nrequired" },
{ "addmultisigaddress", 1, "keys" },
Expand Down
11 changes: 0 additions & 11 deletions src/rpc/rawtransaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,6 @@ static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue&
}
}

static std::vector<RPCResult> ScriptPubKeyDoc() {
return
{
{RPCResult::Type::STR, "asm", "Disassembly of the output script"},
{RPCResult::Type::STR, "desc", "Inferred descriptor for the output"},
{RPCResult::Type::STR_HEX, "hex", "The raw output script bytes, hex-encoded"},
{RPCResult::Type::STR, "address", /*optional=*/true, "The Bitcoin address (only if a well-defined address exists)"},
{RPCResult::Type::STR, "type", "The type (one of: " + GetAllOutputTypes() + ")"},
};
}

static std::vector<RPCResult> DecodeTxDoc(const std::string& txid_field_doc)
{
return {
Expand Down
11 changes: 11 additions & 0 deletions src/rpc/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1407,3 +1407,14 @@ void PushWarnings(const std::vector<bilingual_str>& warnings, UniValue& obj)
if (warnings.empty()) return;
obj.pushKV("warnings", BilingualStringsToUniValue(warnings));
}

std::vector<RPCResult> ScriptPubKeyDoc() {
return
{
{RPCResult::Type::STR, "asm", "Disassembly of the output script"},
{RPCResult::Type::STR, "desc", "Inferred descriptor for the output"},
{RPCResult::Type::STR_HEX, "hex", "The raw output script bytes, hex-encoded"},
{RPCResult::Type::STR, "address", /*optional=*/true, "The Bitcoin address (only if a well-defined address exists)"},
{RPCResult::Type::STR, "type", "The type (one of: " + GetAllOutputTypes() + ")"},
};
}
2 changes: 2 additions & 0 deletions src/rpc/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -514,4 +514,6 @@ class RPCHelpMan
void PushWarnings(const UniValue& warnings, UniValue& obj);
void PushWarnings(const std::vector<bilingual_str>& warnings, UniValue& obj);

std::vector<RPCResult> ScriptPubKeyDoc();

#endif // BITCOIN_RPC_UTIL_H
1 change: 1 addition & 0 deletions src/test/fuzz/rpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
"getchaintxstats",
"getconnectioncount",
"getdeploymentinfo",
"getdescriptoractivity",
"getdescriptorinfo",
"getdifficulty",
"getindexinfo",
Expand Down
Loading

0 comments on commit b2af068

Please sign in to comment.