Coverage Report

Created: 2026-09-14 20:36

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.48k
{
40
2.48k
    return RPCMethod{
41
2.48k
        "gettxoutproof",
42
2.48k
        "Returns a hex-encoded proof that \"txid\" was included in a block.\n"
43
2.48k
        "\nNOTE: By default this function only works sometimes. This is when there is an\n"
44
2.48k
        "unspent output in the utxo for this transaction. To make it always work,\n"
45
2.48k
        "you need to maintain a transaction index, using the -txindex command line option or\n"
46
2.48k
        "specify the block in which the transaction is included manually (by blockhash).\n",
47
2.48k
        {
48
2.48k
            {"txids", RPCArg::Type::ARR, RPCArg::Optional::NO, "The txids to filter",
49
2.48k
                {
50
2.48k
                    {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A transaction hash"},
51
2.48k
                },
52
2.48k
            },
53
2.48k
            {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "If specified, looks for txid in the block with this hash"},
54
2.48k
        },
55
2.48k
        RPCResult{
56
2.48k
            RPCResult::Type::STR, "data", "A string that is a serialized, hex-encoded data for the proof."
57
2.48k
        },
58
2.48k
        RPCExamples{""},
59
2.48k
        [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue
60
2.48k
        {
61
23
            std::set<Txid> setTxids;
62
23
            UniValue txids = request.params[0].get_array();
63
23
            if (txids.empty()) {
64
1
                throw JSONRPCError(RPC_INVALID_PARAMETER, "Parameter 'txids' cannot be empty");
65
1
            }
66
50
            for (unsigned int idx = 0; idx < txids.size(); idx++) {
67
29
                auto ret{setTxids.insert(Txid::FromUint256(ParseHashV(txids[idx], "txid")))};
68
29
                if (!ret.second) {
69
1
                    throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, duplicated txid: ") + txids[idx].get_str());
70
1
                }
71
29
            }
72
73
21
            const CBlockIndex* pblockindex = nullptr;
74
21
            uint256 hashBlock;
75
21
            ChainstateManager& chainman = EnsureAnyChainman(request.context);
76
21
            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
15
            } else {
84
15
                LOCK(cs_main);
85
15
                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
16
                for (const auto& tx : setTxids) {
89
16
                    const Coin& coin{AccessByTxid(active_chainstate.CoinsTip(), tx)};
90
16
                    if (!coin.IsSpent()) {
91
10
                        pblockindex = active_chainstate.m_chain[coin.nHeight];
92
10
                        break;
93
10
                    }
94
16
                }
95
15
            }
96
97
98
            // Allow txindex to catch up if we need to query it and before we acquire cs_main.
99
20
            if (g_txindex && !pblockindex) {
100
1
                g_txindex->BlockUntilSyncedToCurrentChain();
101
1
            }
102
103
20
            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
18
            {
117
18
                LOCK(cs_main);
118
18
                CheckBlockDataAvailability(chainman.m_blockman, *pblockindex, /*check_for_undo=*/false);
119
18
            }
120
18
            CBlock block;
121
18
            if (!chainman.m_blockman.ReadBlock(block, *pblockindex)) {
122
0
                throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk");
123
0
            }
124
125
18
            unsigned int ntxFound = 0;
126
36
            for (const auto& tx : block.vtx) {
127
36
                if (setTxids.contains(tx->GetHash())) {
128
18
                    ntxFound++;
129
18
                }
130
36
            }
131
18
            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
17
            DataStream ssMB{};
136
17
            CMerkleBlock mb(block, setTxids);
137
17
            ssMB << mb;
138
17
            std::string strHex = HexStr(ssMB);
139
17
            return strHex;
140
18
        },
141
2.48k
    };
142
2.48k
}
143
144
static RPCMethod verifytxoutproof()
145
2.47k
{
146
2.47k
    return RPCMethod{
147
2.47k
        "verifytxoutproof",
148
2.47k
        "Verifies that a proof points to a transaction in a block, returning the transaction it commits to\n"
149
2.47k
        "and throwing an RPC error if the block is not in our best chain\n",
150
2.47k
        {
151
2.47k
            {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex-encoded proof generated by gettxoutproof"},
152
2.47k
        },
153
2.47k
        RPCResult{
154
2.47k
            RPCResult::Type::ARR, "", "",
155
2.47k
            {
156
2.47k
                {RPCResult::Type::STR_HEX, "txid", "The txid(s) which the proof commits to, or empty array if the proof cannot be validated."},
157
2.47k
            }
158
2.47k
        },
159
2.47k
        RPCExamples{""},
160
2.47k
        [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue
161
2.47k
        {
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.47k
    };
190
2.47k
}
191
192
void RegisterTxoutProofRPCCommands(CRPCTable& t)
193
1.36k
{
194
1.36k
    static const CRPCCommand commands[]{
195
1.36k
        {"blockchain", &gettxoutproof},
196
1.36k
        {"blockchain", &verifytxoutproof},
197
1.36k
    };
198
2.72k
    for (const auto& c : commands) {
199
2.72k
        t.appendCommand(c.name, &c);
200
2.72k
    }
201
1.36k
}