mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-24 18:23:26 -03:00
Merge bitcoin/bitcoin#30708: rpc: add getdescriptoractivity
37a5c5d836
doc: update descriptors.md for getdescriptoractivity (James O'Beirne)ee3ce6a4f4
test: rpc: add no address case for getdescriptoractivity (James O'Beirne)811f76f3a5
rpc: add getdescriptoractivity (James O'Beirne)25fe087de5
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: reACK37a5c5d836
achow101: ACK37a5c5d836
tdb3: Code review and light retest ACK37a5c5d836
rkrux: re-ACK37a5c5d836
Tree-SHA512: 04aa51e329c6c2ed72464b9886281d5ebd7511a8a8e184ea81249033a4dad535a12829b1010afc2da79b344ea8b5ab8ed47e426d0bf2eb78ab395d20b1da8dbb
This commit is contained in:
commit
b2af068825
11 changed files with 486 additions and 11 deletions
|
@ -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.
|
||||
|
|
6
doc/release-notes-30708.md
Normal file
6
doc/release-notes-30708.md
Normal file
|
@ -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.
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
|
||||
class CBlock;
|
||||
class CBlockHeader;
|
||||
|
|
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -3153,6 +3384,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t)
|
|||
{"blockchain", &preciousblock},
|
||||
{"blockchain", &scantxoutset},
|
||||
{"blockchain", &scanblocks},
|
||||
{"blockchain", &getdescriptoractivity},
|
||||
{"blockchain", &getblockfilter},
|
||||
{"blockchain", &dumptxoutset},
|
||||
{"blockchain", &loadtxoutset},
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() + ")"},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -514,4 +514,6 @@ private:
|
|||
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
|
||||
|
|
|
@ -130,6 +130,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
|
|||
"getchaintxstats",
|
||||
"getconnectioncount",
|
||||
"getdeploymentinfo",
|
||||
"getdescriptoractivity",
|
||||
"getdescriptorinfo",
|
||||
"getdifficulty",
|
||||
"getindexinfo",
|
||||
|
|
226
test/functional/rpc_getdescriptoractivity.py
Executable file
226
test/functional/rpc_getdescriptoractivity.py
Executable file
|
@ -0,0 +1,226 @@
|
|||
#!/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, MiniWalletMode, 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)
|
||||
self.test_no_address(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))
|
||||
|
||||
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()
|
|
@ -378,6 +378,7 @@ BASE_SCRIPTS = [
|
|||
'rpc_deriveaddresses.py --usecli',
|
||||
'p2p_ping.py',
|
||||
'p2p_tx_privacy.py',
|
||||
'rpc_getdescriptoractivity.py',
|
||||
'rpc_scanblocks.py',
|
||||
'p2p_sendtxrcncl.py',
|
||||
'rpc_scantxoutset.py',
|
||||
|
|
Loading…
Add table
Reference in a new issue