Coverage Report

Created: 2026-07-14 18:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/bitcoin/src/rpc/txoutproof.cpp
Line
Count
Source
1
// Copyright (c) 2010 Satoshi Nakamoto
2
// Copyright (c) 2009-present The Bitcoin Core developers
3
// Distributed under the MIT software license, see the accompanying
4
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
5
6
#include <chain.h>
7
#include <chainparams.h>
8
#include <coins.h>
9
#include <index/txindex.h>
10
#include <merkleblock.h>
11
#include <node/blockstorage.h>
12
#include <primitives/transaction.h>
13
#include <rpc/blockchain.h>
14
#include <rpc/server.h>
15
#include <rpc/server_util.h>
16
#include <rpc/util.h>
17
#include <univalue.h>
18
#include <util/strencodings.h>
19
#include <validation.h>
20
21
using node::GetTransaction;
22
23
static RPCMethod gettxoutproof()
24
54
{
25
54
    return RPCMethod{
26
54
        "gettxoutproof",
27
54
        "Returns a hex-encoded proof that \"txid\" was included in a block.\n"
28
54
        "\nNOTE: By default this function only works sometimes. This is when there is an\n"
29
54
        "unspent output in the utxo for this transaction. To make it always work,\n"
30
54
        "you need to maintain a transaction index, using the -txindex command line option or\n"
31
54
        "specify the block in which the transaction is included manually (by blockhash).\n",
32
54
        {
33
54
            {"txids", RPCArg::Type::ARR, RPCArg::Optional::NO, "The txids to filter",
34
54
                {
35
54
                    {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A transaction hash"},
36
54
                },
37
54
            },
38
54
            {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "If specified, looks for txid in the block with this hash"},
39
54
        },
40
54
        RPCResult{
41
54
            RPCResult::Type::STR, "data", "A string that is a serialized, hex-encoded data for the proof."
42
54
        },
43
54
        RPCExamples{""},
44
54
        [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue
45
54
        {
46
0
            std::set<Txid> setTxids;
47
0
            UniValue txids = request.params[0].get_array();
48
0
            if (txids.empty()) {
  Branch (48:17): [True: 0, False: 0]
49
0
                throw JSONRPCError(RPC_INVALID_PARAMETER, "Parameter 'txids' cannot be empty");
50
0
            }
51
0
            for (unsigned int idx = 0; idx < txids.size(); idx++) {
  Branch (51:40): [True: 0, False: 0]
52
0
                auto ret{setTxids.insert(Txid::FromUint256(ParseHashV(txids[idx], "txid")))};
53
0
                if (!ret.second) {
  Branch (53:21): [True: 0, False: 0]
54
0
                    throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, duplicated txid: ") + txids[idx].get_str());
55
0
                }
56
0
            }
57
58
0
            const CBlockIndex* pblockindex = nullptr;
59
0
            uint256 hashBlock;
60
0
            ChainstateManager& chainman = EnsureAnyChainman(request.context);
61
0
            if (!request.params[1].isNull()) {
  Branch (61:17): [True: 0, False: 0]
62
0
                LOCK(cs_main);
63
0
                hashBlock = ParseHashV(request.params[1], "blockhash");
64
0
                pblockindex = chainman.m_blockman.LookupBlockIndex(hashBlock);
65
0
                if (!pblockindex) {
  Branch (65:21): [True: 0, False: 0]
66
0
                    throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
67
0
                }
68
0
            } else {
69
0
                LOCK(cs_main);
70
0
                Chainstate& active_chainstate = chainman.ActiveChainstate();
71
72
                // Loop through txids and try to find which block they're in. Exit loop once a block is found.
73
0
                for (const auto& tx : setTxids) {
  Branch (73:37): [True: 0, False: 0]
74
0
                    const Coin& coin{AccessByTxid(active_chainstate.CoinsTip(), tx)};
75
0
                    if (!coin.IsSpent()) {
  Branch (75:25): [True: 0, False: 0]
76
0
                        pblockindex = active_chainstate.m_chain[coin.nHeight];
77
0
                        break;
78
0
                    }
79
0
                }
80
0
            }
81
82
83
            // Allow txindex to catch up if we need to query it and before we acquire cs_main.
84
0
            if (g_txindex && !pblockindex) {
  Branch (84:17): [True: 0, False: 0]
  Branch (84:30): [True: 0, False: 0]
85
0
                g_txindex->BlockUntilSyncedToCurrentChain();
86
0
            }
87
88
0
            if (pblockindex == nullptr) {
  Branch (88:17): [True: 0, False: 0]
89
0
                const CTransactionRef tx = GetTransaction(/*block_index=*/nullptr, /*mempool=*/nullptr, *setTxids.begin(), chainman.m_blockman, hashBlock);
90
0
                if (!tx || hashBlock.IsNull()) {
  Branch (90:21): [True: 0, False: 0]
  Branch (90:28): [True: 0, False: 0]
91
0
                    throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not yet in block");
92
0
                }
93
94
0
                LOCK(cs_main);
95
0
                pblockindex = chainman.m_blockman.LookupBlockIndex(hashBlock);
96
0
                if (!pblockindex) {
  Branch (96:21): [True: 0, False: 0]
97
0
                    throw JSONRPCError(RPC_INTERNAL_ERROR, "Transaction index corrupt");
98
0
                }
99
0
            }
100
101
0
            {
102
0
                LOCK(cs_main);
103
0
                CheckBlockDataAvailability(chainman.m_blockman, *pblockindex, /*check_for_undo=*/false);
104
0
            }
105
0
            CBlock block;
106
0
            if (!chainman.m_blockman.ReadBlock(block, *pblockindex)) {
  Branch (106:17): [True: 0, False: 0]
107
0
                throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk");
108
0
            }
109
110
0
            unsigned int ntxFound = 0;
111
0
            for (const auto& tx : block.vtx) {
  Branch (111:33): [True: 0, False: 0]
112
0
                if (setTxids.contains(tx->GetHash())) {
  Branch (112:21): [True: 0, False: 0]
113
0
                    ntxFound++;
114
0
                }
115
0
            }
116
0
            if (ntxFound != setTxids.size()) {
  Branch (116:17): [True: 0, False: 0]
117
0
                throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Not all transactions found in specified or retrieved block");
118
0
            }
119
120
0
            DataStream ssMB{};
121
0
            CMerkleBlock mb(block, setTxids);
122
0
            ssMB << mb;
123
0
            std::string strHex = HexStr(ssMB);
124
0
            return strHex;
125
0
        },
126
54
    };
127
54
}
128
129
static RPCMethod verifytxoutproof()
130
54
{
131
54
    return RPCMethod{
132
54
        "verifytxoutproof",
133
54
        "Verifies that a proof points to a transaction in a block, returning the transaction it commits to\n"
134
54
        "and throwing an RPC error if the block is not in our best chain\n",
135
54
        {
136
54
            {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex-encoded proof generated by gettxoutproof"},
137
54
        },
138
54
        RPCResult{
139
54
            RPCResult::Type::ARR, "", "",
140
54
            {
141
54
                {RPCResult::Type::STR_HEX, "txid", "The txid(s) which the proof commits to, or empty array if the proof cannot be validated."},
142
54
            }
143
54
        },
144
54
        RPCExamples{""},
145
54
        [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue
146
54
        {
147
0
            CMerkleBlock merkleBlock;
148
0
            SpanReader{ParseHexV(request.params[0], "proof")} >> merkleBlock;
149
150
0
            UniValue res(UniValue::VARR);
151
152
0
            std::vector<Txid> vMatch;
153
0
            std::vector<unsigned int> vIndex;
154
0
            if (merkleBlock.txn.ExtractMatches(vMatch, vIndex) != merkleBlock.header.hashMerkleRoot)
  Branch (154:17): [True: 0, False: 0]
155
0
                return res;
156
157
0
            ChainstateManager& chainman = EnsureAnyChainman(request.context);
158
0
            LOCK(cs_main);
159
160
0
            const CBlockIndex* pindex = chainman.m_blockman.LookupBlockIndex(merkleBlock.header.GetHash());
161
0
            if (!pindex || !chainman.ActiveChain().Contains(*pindex) || pindex->nTx == 0) {
  Branch (161:17): [True: 0, False: 0]
  Branch (161:28): [True: 0, False: 0]
  Branch (161:73): [True: 0, False: 0]
162
0
                throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found in chain");
163
0
            }
164
165
            // Check if proof is valid, only add results if so
166
0
            if (pindex->nTx == merkleBlock.txn.GetNumTransactions()) {
  Branch (166:17): [True: 0, False: 0]
167
0
                for (const auto& txid : vMatch) {
  Branch (167:39): [True: 0, False: 0]
168
0
                    res.push_back(txid.GetHex());
169
0
                }
170
0
            }
171
172
0
            return res;
173
0
        },
174
54
    };
175
54
}
176
177
void RegisterTxoutProofRPCCommands(CRPCTable& t)
178
27
{
179
27
    static const CRPCCommand commands[]{
180
27
        {"blockchain", &gettxoutproof},
181
27
        {"blockchain", &verifytxoutproof},
182
27
    };
183
54
    for (const auto& c : commands) {
  Branch (183:24): [True: 54, False: 27]
184
54
        t.appendCommand(c.name, &c);
185
54
    }
186
27
}