rpc: add getdescriptoractivity

This commit is contained in:
James O'Beirne
2024-08-21 04:23:40 -04:00
parent 25fe087de5
commit 811f76f3a5
7 changed files with 441 additions and 0 deletions

View File

@@ -10,6 +10,7 @@
#include <string>
#include <vector>
#include <optional>
class CBlock;
class CBlockHeader;

View File

@@ -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;
@@ -2585,6 +2587,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",
@@ -3152,6 +3383,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t)
{"blockchain", &preciousblock},
{"blockchain", &scantxoutset},
{"blockchain", &scanblocks},
{"blockchain", &getdescriptoractivity},
{"blockchain", &getblockfilter},
{"blockchain", &dumptxoutset},
{"blockchain", &loadtxoutset},

View File

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

View File

@@ -130,6 +130,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
"getchaintxstats",
"getconnectioncount",
"getdeploymentinfo",
"getdescriptoractivity",
"getdescriptorinfo",
"getdifficulty",
"getindexinfo",