From 25fe087de59e967ce968d35ed77138325eb9a9fa Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Fri, 15 Nov 2024 13:58:53 -0500 Subject: [PATCH 1/4] rpc: move-only: move ScriptPubKeyDoc to utils --- src/rpc/rawtransaction.cpp | 11 ----------- src/rpc/util.cpp | 11 +++++++++++ src/rpc/util.h | 2 ++ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 65e6e40b0dc..8f20e61fae0 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -82,17 +82,6 @@ static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& } } -static std::vector 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 DecodeTxDoc(const std::string& txid_field_doc) { return { diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp index d71d7d737b0..9a658e3f62d 100644 --- a/src/rpc/util.cpp +++ b/src/rpc/util.cpp @@ -1404,3 +1404,14 @@ void PushWarnings(const std::vector& warnings, UniValue& obj) if (warnings.empty()) return; obj.pushKV("warnings", BilingualStringsToUniValue(warnings)); } + +std::vector 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() + ")"}, + }; +} diff --git a/src/rpc/util.h b/src/rpc/util.h index b8e67597688..cd120ce2e2c 100644 --- a/src/rpc/util.h +++ b/src/rpc/util.h @@ -512,4 +512,6 @@ class RPCHelpMan void PushWarnings(const UniValue& warnings, UniValue& obj); void PushWarnings(const std::vector& warnings, UniValue& obj); +std::vector ScriptPubKeyDoc(); + #endif // BITCOIN_RPC_UTIL_H From 811f76f3a511d20750046319b390e225a1151caa Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Wed, 21 Aug 2024 04:23:40 -0400 Subject: [PATCH 2/4] rpc: add getdescriptoractivity --- doc/release-notes-30708.md | 6 + src/core_io.h | 1 + src/rpc/blockchain.cpp | 232 +++++++++++++++++++ src/rpc/client.cpp | 3 + src/test/fuzz/rpc.cpp | 1 + test/functional/rpc_getdescriptoractivity.py | 197 ++++++++++++++++ test/functional/test_runner.py | 1 + 7 files changed, 441 insertions(+) create mode 100644 doc/release-notes-30708.md create mode 100755 test/functional/rpc_getdescriptoractivity.py diff --git a/doc/release-notes-30708.md b/doc/release-notes-30708.md new file mode 100644 index 00000000000..5cf17c7b650 --- /dev/null +++ b/doc/release-notes-30708.md @@ -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. diff --git a/src/core_io.h b/src/core_io.h index 9305bb72393..ce2e8f67128 100644 --- a/src/core_io.h +++ b/src/core_io.h @@ -10,6 +10,7 @@ #include #include +#include class CBlock; class CBlockHeader; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 4894cecfbda..edf738dd1d8 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -55,9 +55,11 @@ #include #include +#include #include #include #include +#include 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 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 scripts_to_watch; + + // Determine scripts to watch. + for (const UniValue& scanobject : request.params[1].get_array().getValues()) { + FlatSigningProvider provider; + std::vector 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 = 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}, diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 601e4fa7bf5..23461a0cfa6 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -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" }, diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index 4db37ab7b7a..9be21677937 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -130,6 +130,7 @@ const std::vector RPC_COMMANDS_SAFE_FOR_FUZZING{ "getchaintxstats", "getconnectioncount", "getdeploymentinfo", + "getdescriptoractivity", "getdescriptorinfo", "getdifficulty", "getindexinfo", diff --git a/test/functional/rpc_getdescriptoractivity.py b/test/functional/rpc_getdescriptoractivity.py new file mode 100755 index 00000000000..62b98a4ba2b --- /dev/null +++ b/test/functional/rpc_getdescriptoractivity.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# Copyright (c) 2024-present The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from decimal import Decimal + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error +from test_framework.messages import COIN +from test_framework.wallet import MiniWallet, getnewdestination + + +class GetBlocksActivityTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + + def run_test(self): + node = self.nodes[0] + wallet = MiniWallet(node) + node.setmocktime(node.getblockheader(node.getbestblockhash())['time']) + wallet.generate(200, invalid_call=False) + + self.test_no_activity(node) + self.test_activity_in_block(node, wallet) + self.test_no_mempool_inclusion(node, wallet) + self.test_multiple_addresses(node, wallet) + self.test_invalid_blockhash(node, wallet) + self.test_invalid_descriptor(node, wallet) + self.test_confirmed_and_unconfirmed(node, wallet) + self.test_receive_then_spend(node, wallet) + + def test_no_activity(self, node): + _, _, addr_1 = getnewdestination() + result = node.getdescriptoractivity([], [f"addr({addr_1})"], True) + assert_equal(len(result['activity']), 0) + + def test_activity_in_block(self, node, wallet): + _, spk_1, addr_1 = getnewdestination(address_type='bech32m') + txid = wallet.send_to(from_node=node, scriptPubKey=spk_1, amount=1 * COIN)['txid'] + blockhash = self.generate(node, 1)[0] + + # Test getdescriptoractivity with the specific blockhash + result = node.getdescriptoractivity([blockhash], [f"addr({addr_1})"], True) + assert_equal(list(result.keys()), ['activity']) + [activity] = result['activity'] + + for k, v in { + 'amount': Decimal('1.00000000'), + 'blockhash': blockhash, + 'height': 201, + 'txid': txid, + 'type': 'receive', + 'vout': 1, + }.items(): + assert_equal(activity[k], v) + + outspk = activity['output_spk'] + + assert_equal(outspk['asm'][:2], '1 ') + assert_equal(outspk['desc'].split('(')[0], 'rawtr') + assert_equal(outspk['hex'], spk_1.hex()) + assert_equal(outspk['address'], addr_1) + assert_equal(outspk['type'], 'witness_v1_taproot') + + + def test_no_mempool_inclusion(self, node, wallet): + _, spk_1, addr_1 = getnewdestination() + wallet.send_to(from_node=node, scriptPubKey=spk_1, amount=1 * COIN) + + _, spk_2, addr_2 = getnewdestination() + wallet.send_to( + from_node=node, scriptPubKey=spk_2, amount=1 * COIN) + + # Do not generate a block to keep the transaction in the mempool + + result = node.getdescriptoractivity([], [f"addr({addr_1})", f"addr({addr_2})"], False) + + assert_equal(len(result['activity']), 0) + + def test_multiple_addresses(self, node, wallet): + _, spk_1, addr_1 = getnewdestination() + _, spk_2, addr_2 = getnewdestination() + wallet.send_to(from_node=node, scriptPubKey=spk_1, amount=1 * COIN) + wallet.send_to(from_node=node, scriptPubKey=spk_2, amount=2 * COIN) + + blockhash = self.generate(node, 1)[0] + + result = node.getdescriptoractivity([blockhash], [f"addr({addr_1})", f"addr({addr_2})"], True) + + assert_equal(len(result['activity']), 2) + + # Duplicate address specification is fine. + assert_equal( + result, + node.getdescriptoractivity([blockhash], [ + f"addr({addr_1})", f"addr({addr_1})", f"addr({addr_2})"], True)) + + # Flipping descriptor order doesn't affect results. + result_flipped = node.getdescriptoractivity( + [blockhash], [f"addr({addr_2})", f"addr({addr_1})"], True) + assert_equal(result, result_flipped) + + [a1] = [a for a in result['activity'] if a['output_spk']['address'] == addr_1] + [a2] = [a for a in result['activity'] if a['output_spk']['address'] == addr_2] + + assert a1['blockhash'] == blockhash + assert a1['amount'] == 1.0 + + assert a2['blockhash'] == blockhash + assert a2['amount'] == 2.0 + + def test_invalid_blockhash(self, node, wallet): + self.generate(node, 20) # Generate to get more fees + + _, spk_1, addr_1 = getnewdestination() + wallet.send_to(from_node=node, scriptPubKey=spk_1, amount=1 * COIN) + + invalid_blockhash = "0000000000000000000000000000000000000000000000000000000000000000" + + assert_raises_rpc_error( + -5, "Block not found", + node.getdescriptoractivity, [invalid_blockhash], [f"addr({addr_1})"], True) + + def test_invalid_descriptor(self, node, wallet): + blockhash = self.generate(node, 1)[0] + _, _, addr_1 = getnewdestination() + + assert_raises_rpc_error( + -5, "is not a valid descriptor", + node.getdescriptoractivity, [blockhash], [f"addrx({addr_1})"], True) + + def test_confirmed_and_unconfirmed(self, node, wallet): + self.generate(node, 20) # Generate to get more fees + + _, spk_1, addr_1 = getnewdestination() + txid_1 = wallet.send_to( + from_node=node, scriptPubKey=spk_1, amount=1 * COIN)['txid'] + blockhash = self.generate(node, 1)[0] + + _, spk_2, to_addr = getnewdestination() + txid_2 = wallet.send_to( + from_node=node, scriptPubKey=spk_2, amount=1 * COIN)['txid'] + + result = node.getdescriptoractivity( + [blockhash], [f"addr({addr_1})", f"addr({to_addr})"], True) + + activity = result['activity'] + assert_equal(len(activity), 2) + + [confirmed] = [a for a in activity if a.get('blockhash') == blockhash] + assert confirmed['txid'] == txid_1 + assert confirmed['height'] == node.getblockchaininfo()['blocks'] + + [unconfirmed] = [a for a in activity if not a.get('blockhash')] + assert 'blockhash' not in unconfirmed + assert 'height' not in unconfirmed + + assert any(a['txid'] == txid_2 for a in activity if not a.get('blockhash')) + + def test_receive_then_spend(self, node, wallet): + """Also important because this tests multiple blockhashes.""" + self.generate(node, 20) # Generate to get more fees + + sent1 = wallet.send_self_transfer(from_node=node) + utxo = sent1['new_utxo'] + blockhash_1 = self.generate(node, 1)[0] + + sent2 = wallet.send_self_transfer(from_node=node, utxo_to_spend=utxo) + blockhash_2 = self.generate(node, 1)[0] + + result = node.getdescriptoractivity( + [blockhash_1, blockhash_2], [wallet.get_descriptor()], True) + + assert_equal(len(result['activity']), 4) + + assert result['activity'][1]['type'] == 'receive' + assert result['activity'][1]['txid'] == sent1['txid'] + assert result['activity'][1]['blockhash'] == blockhash_1 + + assert result['activity'][2]['type'] == 'spend' + assert result['activity'][2]['spend_txid'] == sent2['txid'] + assert result['activity'][2]['prevout_txid'] == sent1['txid'] + assert result['activity'][2]['blockhash'] == blockhash_2 + + # Test that reversing the blockorder yields the same result. + assert_equal(result, node.getdescriptoractivity( + [blockhash_1, blockhash_2], [wallet.get_descriptor()], True)) + + # Test that duplicating a blockhash yields the same result. + assert_equal(result, node.getdescriptoractivity( + [blockhash_1, blockhash_2, blockhash_2], [wallet.get_descriptor()], True)) + + +if __name__ == '__main__': + GetBlocksActivityTest(__file__).main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 3d8c2300663..0233209c0b4 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -378,6 +378,7 @@ 'rpc_deriveaddresses.py --usecli', 'p2p_ping.py', 'p2p_tx_privacy.py', + 'rpc_getdescriptoractivity.py', 'rpc_scanblocks.py', 'p2p_sendtxrcncl.py', 'rpc_scantxoutset.py', From ee3ce6a4f4d35afe7fcab16eff419a6788b02170 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Mon, 18 Nov 2024 15:32:52 -0500 Subject: [PATCH 3/4] test: rpc: add no address case for getdescriptoractivity Co-authored-by: Greg Sanders --- test/functional/rpc_getdescriptoractivity.py | 31 +++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/test/functional/rpc_getdescriptoractivity.py b/test/functional/rpc_getdescriptoractivity.py index 62b98a4ba2b..3efa1948b76 100755 --- a/test/functional/rpc_getdescriptoractivity.py +++ b/test/functional/rpc_getdescriptoractivity.py @@ -8,7 +8,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error from test_framework.messages import COIN -from test_framework.wallet import MiniWallet, getnewdestination +from test_framework.wallet import MiniWallet, MiniWalletMode, getnewdestination class GetBlocksActivityTest(BitcoinTestFramework): @@ -30,6 +30,7 @@ def run_test(self): self.test_invalid_descriptor(node, wallet) self.test_confirmed_and_unconfirmed(node, wallet) self.test_receive_then_spend(node, wallet) + self.test_no_address(node, wallet) def test_no_activity(self, node): _, _, addr_1 = getnewdestination() @@ -192,6 +193,34 @@ def test_receive_then_spend(self, node, wallet): assert_equal(result, node.getdescriptoractivity( [blockhash_1, blockhash_2, blockhash_2], [wallet.get_descriptor()], True)) + def test_no_address(self, node, wallet): + raw_wallet = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_P2PK) + raw_wallet.generate(100, invalid_call=False) + + no_addr_tx = raw_wallet.send_self_transfer(from_node=node) + raw_desc = raw_wallet.get_descriptor() + + blockhash = self.generate(node, 1)[0] + + result = node.getdescriptoractivity([blockhash], [raw_desc], False) + + assert_equal(len(result['activity']), 2) + + a1 = result['activity'][0] + a2 = result['activity'][1] + + assert a1['type'] == "spend" + assert a1['blockhash'] == blockhash + # sPK lacks address. + assert_equal(list(a1['prevout_spk'].keys()), ['asm', 'desc', 'hex', 'type']) + assert a1['amount'] == no_addr_tx["fee"] + Decimal(no_addr_tx["tx"].vout[0].nValue) / COIN + + assert a2['type'] == "receive" + assert a2['blockhash'] == blockhash + # sPK lacks address. + assert_equal(list(a2['output_spk'].keys()), ['asm', 'desc', 'hex', 'type']) + assert a2['amount'] == Decimal(no_addr_tx["tx"].vout[0].nValue) / COIN + if __name__ == '__main__': GetBlocksActivityTest(__file__).main() From 37a5c5d83664c31d83fc649d3c8c858bd5f10f21 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Mon, 18 Nov 2024 15:38:01 -0500 Subject: [PATCH 4/4] doc: update descriptors.md for getdescriptoractivity --- doc/descriptors.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/descriptors.md b/doc/descriptors.md index 8c55d4ae37f..115ca5e61ae 100644 --- a/doc/descriptors.md +++ b/doc/descriptors.md @@ -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.