|
55 | 55 | #include <stdint.h>
|
56 | 56 |
|
57 | 57 | #include <condition_variable>
|
| 58 | +#include <iterator> |
58 | 59 | #include <memory>
|
59 | 60 | #include <mutex>
|
60 | 61 | #include <optional>
|
| 62 | +#include <vector> |
61 | 63 |
|
62 | 64 | using kernel::CCoinsStats;
|
63 | 65 | using kernel::CoinStatsHashType;
|
@@ -2586,6 +2588,235 @@ static RPCHelpMan scanblocks()
|
2586 | 2588 | };
|
2587 | 2589 | }
|
2588 | 2590 |
|
| 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 | + |
2589 | 2820 | static RPCHelpMan getblockfilter()
|
2590 | 2821 | {
|
2591 | 2822 | return RPCHelpMan{"getblockfilter",
|
@@ -3153,6 +3384,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t)
|
3153 | 3384 | {"blockchain", &preciousblock},
|
3154 | 3385 | {"blockchain", &scantxoutset},
|
3155 | 3386 | {"blockchain", &scanblocks},
|
| 3387 | + {"blockchain", &getdescriptoractivity}, |
3156 | 3388 | {"blockchain", &getblockfilter},
|
3157 | 3389 | {"blockchain", &dumptxoutset},
|
3158 | 3390 | {"blockchain", &loadtxoutset},
|
|
0 commit comments