Coverage Report

Created: 2026-10-07 13:27

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