txoutproof.cpp raw
1 // Copyright (c) 2010 Satoshi Nakamoto
2 // Copyright (c) 2009-2022 The Limenka 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 <consensus/validation.h>
10 #include <index/txindex.h>
11 #include <merkleblock.h>
12 #include <node/blockstorage.h>
13 #include <primitives/transaction.h>
14 #include <rpc/blockchain.h>
15 #include <rpc/server.h>
16 #include <rpc/server_util.h>
17 #include <rpc/util.h>
18 #include <univalue.h>
19 #include <util/strencodings.h>
20 #include <validation.h>
21
22 using node::GetTransaction;
23
24 static RPCHelpMan gettxoutproof()
25 {
26 return RPCHelpMan{"gettxoutproof",
27 "\nReturns a hex-encoded proof that \"txid\" was included in a block.\n"
28 "\nNOTE: By default this function only works sometimes. This is when there is an\n"
29 "unspent output in the utxo for this transaction. To make it always work,\n"
30 "you need to maintain a transaction index, using the -txindex command line option or\n"
31 "specify the block in which the transaction is included manually (by blockhash).\n",
32 {
33 {"txids", RPCArg::Type::ARR, RPCArg::Optional::NO, "The txids to filter",
34 {
35 {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A transaction id"},
36 },
37 },
38 {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "If specified, looks for txid in the block with this hash"},
39 {"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "",
40 {
41 {"prove_witness", RPCArg::Type::BOOL, RPCArg::Default{false}, "If true, proves the associated wtxid/hash of the specified transactions instead of txid"},
42 },
43 },
44 },
45 {
46 RPCResult{
47 "If prove_witness is false or unspecified",
48 RPCResult::Type::STR, "data", "A string that is a serialized, hex-encoded data for the proof."
49 },
50 RPCResult{
51 "If prove_witness is true", RPCResult::Type::OBJ, "", "",
52 {
53 {RPCResult::Type::STR, "proof", "The produced txout proof, hex-encoded."},
54 {RPCResult::Type::OBJ, "proven", "Information about the proof.", {
55 {RPCResult::Type::STR_HEX, "blockhash", "The block hash the proof links to"},
56 {RPCResult::Type::NUM, "blockheight", "The height of the block the proof links to"},
57 {RPCResult::Type::ARR, "tx", "Information about transactions", {
58 {RPCResult::Type::OBJ, "", "Information about a transaction", {
59 {RPCResult::Type::STR_HEX, "txid", "Transaction id this is for (parameter; NOT proven by proof)"},
60 {RPCResult::Type::STR_HEX, "wtxid", "Wtxid/hash of a transaction"},
61 {RPCResult::Type::NUM, "blockindex", "Index of transaction in block"},
62 }},
63 }},
64 }},
65 }
66 },
67 },
68 RPCExamples{""},
69 [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
70 {
71 std::set<Txid> setTxids;
72 UniValue txids = request.params[0].get_array();
73 if (txids.empty()) {
74 throw JSONRPCError(RPC_INVALID_PARAMETER, "Parameter 'txids' cannot be empty");
75 }
76 for (unsigned int idx = 0; idx < txids.size(); idx++) {
77 auto ret{setTxids.insert(Txid::FromUint256(ParseHashV(txids[idx], "txid")))};
78 if (!ret.second) {
79 throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, duplicated txid: ") + txids[idx].get_str());
80 }
81 }
82 const UniValue options{request.params[2].isNull() ? UniValue::VOBJ : request.params[2]};
83 bool prove_witness = options["prove_witness"].isNull() ? false : options["prove_witness"].get_bool();
84
85 const CBlockIndex* pblockindex = nullptr;
86 uint256 hashBlock;
87 ChainstateManager& chainman = EnsureAnyChainman(request.context);
88 if (!request.params[1].isNull()) {
89 LOCK(cs_main);
90 hashBlock = ParseHashV(request.params[1], "blockhash");
91 pblockindex = chainman.m_blockman.LookupBlockIndex(hashBlock);
92 if (!pblockindex) {
93 throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
94 }
95 } else {
96 LOCK(cs_main);
97 Chainstate& active_chainstate = chainman.ActiveChainstate();
98
99 // Loop through txids and try to find which block they're in. Exit loop once a block is found.
100 for (const auto& tx : setTxids) {
101 const Coin& coin{AccessByTxid(active_chainstate.CoinsTip(), tx)};
102 if (!coin.IsSpent()) {
103 pblockindex = active_chainstate.m_chain[coin.nHeight];
104 break;
105 }
106 }
107 }
108
109
110 // Allow txindex to catch up if we need to query it and before we acquire cs_main.
111 if (g_txindex && !pblockindex) {
112 g_txindex->BlockUntilSyncedToCurrentChain();
113 }
114
115 if (pblockindex == nullptr) {
116 const CTransactionRef tx = GetTransaction(/*block_index=*/nullptr, /*mempool=*/nullptr, *setTxids.begin(), hashBlock, chainman.m_blockman);
117 if (!tx || hashBlock.IsNull()) {
118 throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not yet in block");
119 }
120
121 LOCK(cs_main);
122 pblockindex = chainman.m_blockman.LookupBlockIndex(hashBlock);
123 if (!pblockindex) {
124 throw JSONRPCError(RPC_INTERNAL_ERROR, "Transaction index corrupt");
125 }
126 }
127
128 {
129 LOCK(cs_main);
130 CheckBlockDataAvailability(chainman.m_blockman, *pblockindex, /*check_for_undo=*/false);
131 }
132 CBlock block;
133 if (!chainman.m_blockman.ReadBlock(block, *pblockindex)) {
134 throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk");
135 }
136
137 unsigned int ntxFound = 0;
138 UniValue txs{UniValue::VARR};
139 for (size_t i{0}; i < block.vtx.size(); ++i) {
140 const auto& tx = block.vtx.at(i);
141 if (setTxids.count(tx->GetHash())) {
142 ntxFound++;
143 if (prove_witness) {
144 UniValue txinfo{UniValue::VOBJ};
145 txinfo.pushKV("txid", tx->GetHash().GetHex());
146 txinfo.pushKV("wtxid", tx->GetWitnessHash().GetHex());
147 txinfo.pushKV("blockindex", i);
148 txs.push_back(txinfo);
149 }
150 }
151 }
152 if (ntxFound != setTxids.size()) {
153 throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Not all transactions found in specified or retrieved block");
154 }
155
156 DataStream ssMB{};
157 CMerkleBlock mb(block, setTxids, /*prove_witness=*/ prove_witness);
158 if (prove_witness) {
159 mb.SerializeWithWitness(ssMB);
160 } else {
161 ssMB << mb;
162 }
163 std::string strHex = HexStr(ssMB);
164
165 if (prove_witness) {
166 UniValue proven{UniValue::VOBJ};
167 proven.pushKV("blockhash", block.GetHash().GetHex());
168 proven.pushKV("blockheight", pblockindex->nHeight);
169 proven.pushKV("tx", txs);
170 UniValue res{UniValue::VOBJ};
171 res.pushKV("proof", strHex);
172 res.pushKV("proven", proven);
173 return res;
174 }
175
176 return strHex;
177 },
178 };
179 }
180
181 static RPCHelpMan verifytxoutproof()
182 {
183 return RPCHelpMan{"verifytxoutproof",
184 "\nVerifies that a proof points to a transaction in a block, returning the transaction it commits to\n"
185 "and throwing an RPC error if the block is not in our best chain\n",
186 {
187 {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex-encoded proof generated by gettxoutproof"},
188 {"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "",
189 {
190 {"verify_witness", RPCArg::Type::BOOL, RPCArg::Default{false}, "If true, also verifies the associated wtxid/hash of the specified transactions (if included in proof)"},
191 },
192 },
193 },
194 {
195 RPCResult{
196 "If verify_witness is false or unspecified",
197 RPCResult::Type::ARR, "", "",
198 {
199 {RPCResult::Type::STR_HEX, "txid", "The txid(s) which the proof commits to, or empty array if the proof cannot be validated."},
200 }
201 },
202 RPCResult{
203 "If verify_witness is true and the proof valid", RPCResult::Type::OBJ, "", "",
204 {
205 {RPCResult::Type::STR_HEX, "blockhash", "The block hash this proof links to"},
206 {RPCResult::Type::NUM, "blockheight", "The height of the block this proof links to"},
207 {RPCResult::Type::NUM, "confirmations", /*optional=*/true, "Number of blocks (including the one with the transactions) confirming these transactions"},
208 {RPCResult::Type::NUM, "confirmations_assumed", /*optional=*/true, "The number of unverified blocks confirming these transactions (eg, in an assumed-valid UTXO set)"},
209 {RPCResult::Type::ARR, "tx", "Information about transactions", {
210 {RPCResult::Type::OBJ, "", "Information about a transaction", {
211 {RPCResult::Type::STR_HEX, "wtxid", "Wtxid/hash of a transaction"},
212 {RPCResult::Type::NUM, "blockindex", "Index of transaction in block"},
213 }},
214 }},
215 }
216 },
217 RPCResult{"If verify_witness is true and the proof invalid", RPCResult::Type::OBJ, "", ""},
218 },
219 RPCExamples{""},
220 [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
221 {
222 const UniValue options{request.params[1].isNull() ? UniValue::VOBJ : request.params[1].get_obj()};
223 const bool verify_witness = options["verify_witness"].isNull() ? false : options["verify_witness"].get_bool();
224
225 DataStream ssMB{ParseHexV(request.params[0], "proof")};
226 CMerkleBlock merkleBlock;
227 if (verify_witness) {
228 merkleBlock.UnserializeWithWitness(ssMB);
229 } else {
230 ssMB >> merkleBlock;
231 }
232
233 UniValue res(verify_witness ? UniValue::VOBJ : UniValue::VARR);
234
235 std::vector<uint256> vMatch;
236 std::vector<unsigned int> vIndex;
237 if (merkleBlock.txn.ExtractMatches(vMatch, vIndex) != merkleBlock.header.hashMerkleRoot)
238 return res;
239
240 if (vMatch.empty()) {
241 return res;
242 }
243
244 int witness_commit_outidx{NO_WITNESS_COMMITMENT};
245 if (verify_witness) {
246 if (vIndex.at(0) != 0) return res;
247 if (!merkleBlock.m_gentx) return res;
248 if (merkleBlock.m_gentx->GetHash() != vMatch[0]) return res;
249 if (!merkleBlock.m_gentx->IsCoinBase()) return res;
250 witness_commit_outidx = GetWitnessCommitmentIndex(*merkleBlock.m_gentx);
251 if (witness_commit_outidx == NO_WITNESS_COMMITMENT) {
252 if (!merkleBlock.m_prove_gentx) {
253 // We must always prove the gentx to either reveal the wtxid root or prove it has none
254 // But the user may not care to prove the gentx itself, and in this case, we need some way to disambiguate
255 vMatch.erase(vMatch.begin());
256 vIndex.erase(vIndex.begin());
257 }
258 } else {
259 vIndex.clear();
260 uint256 wtxid_root = merkleBlock.m_wtxid_tree.ExtractMatches(vMatch, vIndex);
261 if (vMatch.empty()) return res;
262 const auto& gentx_witness_stack{merkleBlock.m_gentx->vin[0].scriptWitness.stack};
263 if (gentx_witness_stack.size() != 1 || gentx_witness_stack[0].size() != 32) return res;
264 CHash256().Write(wtxid_root).Write(gentx_witness_stack[0]).Finalize(wtxid_root);
265 if (memcmp(wtxid_root.begin(), &merkleBlock.m_gentx->vout[witness_commit_outidx].scriptPubKey[6], 32)) {
266 return res;
267 }
268 if (vIndex.at(0) == 0) {
269 auto& gentx_match = vMatch[0];
270 if (!gentx_match.IsNull()) return res;
271 gentx_match = merkleBlock.m_gentx->GetHash();
272 }
273 }
274 }
275
276 {
277 ChainstateManager& chainman = EnsureAnyChainman(request.context);
278 LOCK(cs_main);
279
280 const CBlockIndex* pindex = chainman.m_blockman.LookupBlockIndex(merkleBlock.header.GetHash());
281 if (!pindex || !chainman.ActiveChain().Contains(pindex) || pindex->nTx == 0) {
282 throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found in chain");
283 }
284
285 // Check if proof is valid, only add results if so
286 if (pindex->nTx != merkleBlock.txn.GetNumTransactions()) {
287 return res;
288 }
289
290 if (verify_witness) {
291 res.pushKV("blockheight", pindex->nHeight);
292
293 const auto pindex_tip = chainman.ActiveChain().Tip();
294 CHECK_NONFATAL(pindex_tip);
295 const auto assumed_base_height = chainman.GetSnapshotBaseHeight();
296 if (assumed_base_height && pindex->nHeight < *assumed_base_height) {
297 res.pushKV("confirmations", 0);
298 res.pushKV("confirmations_assumed", (int64_t)(pindex_tip->nHeight - pindex->nHeight + 1));
299 } else {
300 res.pushKV("confirmations", (int64_t)(pindex_tip->nHeight - pindex->nHeight + 1));
301 }
302 }
303 }
304
305 if (verify_witness) {
306 res.pushKV("blockhash", merkleBlock.header.GetHash().GetHex());
307 UniValue txs{UniValue::VARR};
308 for (size_t i{0}; i < vMatch.size(); ++i) {
309 UniValue tx{UniValue::VOBJ};
310 tx.pushKV("wtxid", vMatch.at(i).GetHex());
311 tx.pushKV("blockindex", vIndex.at(i));
312 txs.push_back(tx);
313 }
314 res.pushKV("tx", txs);
315 return res;
316 }
317
318 for (const uint256& hash : vMatch) {
319 res.push_back(hash.GetHex());
320 }
321 return res;
322 },
323 };
324 }
325
326 void RegisterTxoutProofRPCCommands(CRPCTable& t)
327 {
328 static const CRPCCommand commands[]{
329 {"blockchain", &gettxoutproof},
330 {"blockchain", &verifytxoutproof},
331 };
332 for (const auto& c : commands) {
333 t.appendCommand(c.name, &c);
334 }
335 }
336