diff --git a/doc/release-notes-xxxxx.md b/doc/release-notes-xxxxx.md new file mode 100644 index 000000000000..cebe64d33f9a --- /dev/null +++ b/doc/release-notes-xxxxx.md @@ -0,0 +1,6 @@ +RPC +--- + +The new `gettxproof` and `verifytxproof` RPCs can produce and verify transaction +inclusion proofs for a single witness transaction id in a specified block. +(#xxxxx) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 524c28165cca..6b1fd7c373e1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -106,6 +106,7 @@ add_library(bitcoin_common STATIC EXCLUDE_FROM_ALL common/init.cpp common/interfaces.cpp common/license_info.cpp + common/merkle.cpp common/messages.cpp common/netif.cpp common/pcp.cpp @@ -241,6 +242,7 @@ add_library(bitcoin_node STATIC EXCLUDE_FROM_ALL node/transaction.cpp node/txdownloadman_impl.cpp node/txorphanage.cpp + node/txproof.cpp node/txreconciliation.cpp node/utxo_snapshot.cpp node/warnings.cpp diff --git a/src/common/merkle.cpp b/src/common/merkle.cpp new file mode 100644 index 000000000000..7845bf5d8a63 --- /dev/null +++ b/src/common/merkle.cpp @@ -0,0 +1,137 @@ +// Copyright (c) The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include + +std::optional ComputeMerkleRootFromBranch(const uint256& leaf, const std::vector& branch, uint32_t position, uint32_t leaf_count) +{ + if (leaf_count == 0 || position >= leaf_count) return std::nullopt; + + uint256 hash{leaf}; + uint32_t count{leaf_count}; + uint32_t index{position}; + for (const uint256& sibling : branch) { + if (count <= 1) return std::nullopt; + + const bool duplicate{(index ^ 1U) >= count}; + if (duplicate && sibling != hash) return std::nullopt; + + hash = (index & 1) ? Hash(sibling, hash) : Hash(hash, sibling); + index >>= 1; + count = (count + 1) / 2; + } + if (count != 1) return std::nullopt; + return hash; +} + +/* This implements a constant-space merkle path calculator, limited to 2^32 leaves. */ +static void MerkleComputation(const std::vector& leaves, uint32_t leaf_pos, std::vector& path) +{ + path.clear(); + Assume(leaves.size() <= UINT32_MAX); + if (leaves.size() == 0) { + return; + } + // count is the number of leaves processed so far. + uint32_t count = 0; + // inner is an array of eagerly computed subtree hashes, indexed by tree + // level (0 being the leaves). + // For example, when count is 25 (11001 in binary), inner[4] is the hash of + // the first 16 leaves, inner[3] of the next 8 leaves, and inner[0] equal to + // the last leaf. The other inner entries are undefined. + uint256 inner[32]; + // Which position in inner is a hash that depends on the matching leaf. + int matchlevel = -1; + // First process all leaves into 'inner' values. + while (count < leaves.size()) { + uint256 h = leaves[count]; + bool matchh = count == leaf_pos; + count++; + int level; + // For each of the lower bits in count that are 0, do 1 step. Each + // corresponds to an inner value that existed before processing the + // current leaf, and each needs a hash to combine it. + for (level = 0; !(count & ((uint32_t{1}) << level)); level++) { + if (matchh) { + path.push_back(inner[level]); + } else if (matchlevel == level) { + path.push_back(h); + matchh = true; + } + h = Hash(inner[level], h); + } + // Store the resulting hash at inner position level. + inner[level] = h; + if (matchh) { + matchlevel = level; + } + } + // Do a final 'sweep' over the rightmost branch of the tree to process + // odd levels, and reduce everything to a single top value. + // Level is the level (counted from the bottom) up to which we've sweeped. + int level = 0; + // As long as bit number level in count is zero, skip it. It means there + // is nothing left at this level. + while (!(count & ((uint32_t{1}) << level))) { + level++; + } + uint256 h = inner[level]; + bool matchh = matchlevel == level; + while (count != ((uint32_t{1}) << level)) { + // If we reach this point, h is an inner value that is not the top. + // We combine it with itself (Bitcoin's special rule for odd levels in + // the tree) to produce a higher level one. + if (matchh) { + path.push_back(h); + } + h = Hash(h, h); + // Increment count to the value it would have if two entries at this + // level had existed. + count += ((uint32_t{1}) << level); + level++; + // And propagate the result upwards accordingly. + while (!(count & ((uint32_t{1}) << level))) { + if (matchh) { + path.push_back(inner[level]); + } else if (matchlevel == level) { + path.push_back(h); + matchh = true; + } + h = Hash(inner[level], h); + level++; + } + } +} + +static std::vector ComputeMerklePath(const std::vector& leaves, uint32_t position) { + std::vector ret; + MerkleComputation(leaves, position, ret); + return ret; +} + +std::vector TransactionMerklePath(const CBlock& block, uint32_t position) +{ + std::vector leaves; + leaves.resize(block.vtx.size()); + for (size_t s = 0; s < block.vtx.size(); s++) { + leaves[s] = block.vtx[s]->GetHash().ToUint256(); + } + return ComputeMerklePath(leaves, position); +} + +std::vector WitnessMerklePath(const CBlock& block, uint32_t position) +{ + std::vector leaves; + leaves.resize(block.vtx.size()); + if (!leaves.empty()) { + leaves[0] = uint256{}; + } + for (size_t s = 1; s < block.vtx.size(); s++) { + leaves[s] = block.vtx[s]->GetWitnessHash().ToUint256(); + } + return ComputeMerklePath(leaves, position); +} diff --git a/src/common/merkle.h b/src/common/merkle.h new file mode 100644 index 000000000000..2f79645d18e1 --- /dev/null +++ b/src/common/merkle.h @@ -0,0 +1,42 @@ +// Copyright (c) The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_COMMON_MERKLE_H +#define BITCOIN_COMMON_MERKLE_H + +#include +#include + +#include +#include +#include + +/** + * Compute the Merkle root for a leaf from a merkle branch and a known tree + * shape, or nullopt if the branch is inconsistent with the supplied position + * and leaf count. + */ +std::optional ComputeMerkleRootFromBranch(const uint256& leaf, const std::vector& branch, uint32_t position, uint32_t leaf_count); + +/** + * Compute merkle path to the specified transaction + * + * @param[in] block the block + * @param[in] position transaction for which to calculate the merkle path (0 is the coinbase) + * + * @return merkle path ordered from the deepest + */ +std::vector TransactionMerklePath(const CBlock& block, uint32_t position); + +/** + * Compute merkle path to the specified transaction's witness transaction id. + * + * @param[in] block the block + * @param[in] position transaction for which to calculate the merkle path (0 is the coinbase's null wtxid) + * + * @return merkle path ordered from the deepest + */ +std::vector WitnessMerklePath(const CBlock& block, uint32_t position); + +#endif // BITCOIN_COMMON_MERKLE_H diff --git a/src/consensus/merkle.cpp b/src/consensus/merkle.cpp index c6ae81c61420..d9fa6418f0bf 100644 --- a/src/consensus/merkle.cpp +++ b/src/consensus/merkle.cpp @@ -4,7 +4,6 @@ #include #include -#include /* WARNING! If you're reading this because you're learning about crypto and/or designing a new system that will use merkle trees, keep in mind @@ -62,7 +61,6 @@ uint256 ComputeMerkleRoot(std::vector hashes, bool* mutated) { return hashes[0]; } - uint256 BlockMerkleRoot(const CBlock& block, bool* mutated) { std::vector leaves; @@ -83,98 +81,3 @@ uint256 BlockWitnessMerkleRoot(const CBlock& block) } return ComputeMerkleRoot(std::move(leaves)); } - -/* This implements a constant-space merkle path calculator, limited to 2^32 leaves. */ -static void MerkleComputation(const std::vector& leaves, uint32_t leaf_pos, std::vector& path) -{ - path.clear(); - Assume(leaves.size() <= UINT32_MAX); - if (leaves.size() == 0) { - return; - } - // count is the number of leaves processed so far. - uint32_t count = 0; - // inner is an array of eagerly computed subtree hashes, indexed by tree - // level (0 being the leaves). - // For example, when count is 25 (11001 in binary), inner[4] is the hash of - // the first 16 leaves, inner[3] of the next 8 leaves, and inner[0] equal to - // the last leaf. The other inner entries are undefined. - uint256 inner[32]; - // Which position in inner is a hash that depends on the matching leaf. - int matchlevel = -1; - // First process all leaves into 'inner' values. - while (count < leaves.size()) { - uint256 h = leaves[count]; - bool matchh = count == leaf_pos; - count++; - int level; - // For each of the lower bits in count that are 0, do 1 step. Each - // corresponds to an inner value that existed before processing the - // current leaf, and each needs a hash to combine it. - for (level = 0; !(count & ((uint32_t{1}) << level)); level++) { - if (matchh) { - path.push_back(inner[level]); - } else if (matchlevel == level) { - path.push_back(h); - matchh = true; - } - h = Hash(inner[level], h); - } - // Store the resulting hash at inner position level. - inner[level] = h; - if (matchh) { - matchlevel = level; - } - } - // Do a final 'sweep' over the rightmost branch of the tree to process - // odd levels, and reduce everything to a single top value. - // Level is the level (counted from the bottom) up to which we've sweeped. - int level = 0; - // As long as bit number level in count is zero, skip it. It means there - // is nothing left at this level. - while (!(count & ((uint32_t{1}) << level))) { - level++; - } - uint256 h = inner[level]; - bool matchh = matchlevel == level; - while (count != ((uint32_t{1}) << level)) { - // If we reach this point, h is an inner value that is not the top. - // We combine it with itself (Bitcoin's special rule for odd levels in - // the tree) to produce a higher level one. - if (matchh) { - path.push_back(h); - } - h = Hash(h, h); - // Increment count to the value it would have if two entries at this - // level had existed. - count += ((uint32_t{1}) << level); - level++; - // And propagate the result upwards accordingly. - while (!(count & ((uint32_t{1}) << level))) { - if (matchh) { - path.push_back(inner[level]); - } else if (matchlevel == level) { - path.push_back(h); - matchh = true; - } - h = Hash(inner[level], h); - level++; - } - } -} - -static std::vector ComputeMerklePath(const std::vector& leaves, uint32_t position) { - std::vector ret; - MerkleComputation(leaves, position, ret); - return ret; -} - -std::vector TransactionMerklePath(const CBlock& block, uint32_t position) -{ - std::vector leaves; - leaves.resize(block.vtx.size()); - for (size_t s = 0; s < block.vtx.size(); s++) { - leaves[s] = block.vtx[s]->GetHash().ToUint256(); - } - return ComputeMerklePath(leaves, position); -} diff --git a/src/consensus/merkle.h b/src/consensus/merkle.h index 03b5a1b5aac7..f79756e3cfe1 100644 --- a/src/consensus/merkle.h +++ b/src/consensus/merkle.h @@ -5,11 +5,11 @@ #ifndef BITCOIN_CONSENSUS_MERKLE_H #define BITCOIN_CONSENSUS_MERKLE_H -#include - #include #include +#include + uint256 ComputeMerkleRoot(std::vector hashes, bool* mutated = nullptr); /* @@ -23,14 +23,4 @@ uint256 BlockMerkleRoot(const CBlock& block, bool* mutated = nullptr); */ uint256 BlockWitnessMerkleRoot(const CBlock& block); -/** - * Compute merkle path to the specified transaction - * - * @param[in] block the block - * @param[in] position transaction for which to calculate the merkle path (0 is the coinbase) - * - * @return merkle path ordered from the deepest - */ -std::vector TransactionMerklePath(const CBlock& block, uint32_t position); - #endif // BITCOIN_CONSENSUS_MERKLE_H diff --git a/src/consensus/validation.h b/src/consensus/validation.h index 37a40e767e2d..91eafc23b781 100644 --- a/src/consensus/validation.h +++ b/src/consensus/validation.h @@ -144,12 +144,12 @@ static inline int64_t GetTransactionInputWeight(const CTxIn& txin) } /** Compute at which vout of the block's coinbase transaction the witness commitment occurs, or -1 if not found */ -inline int GetWitnessCommitmentIndex(const CBlock& block) +inline int GetWitnessCommitmentIndex(const CTransaction& coinbase_tx) { int commitpos = NO_WITNESS_COMMITMENT; - if (!block.vtx.empty()) { - for (size_t o = 0; o < block.vtx[0]->vout.size(); o++) { - const CTxOut& vout = block.vtx[0]->vout[o]; + { + for (size_t o = 0; o < coinbase_tx.vout.size(); ++o) { + const CTxOut& vout = coinbase_tx.vout[o]; if (vout.scriptPubKey.size() >= MINIMUM_WITNESS_COMMITMENT && vout.scriptPubKey[0] == OP_RETURN && vout.scriptPubKey[1] == 0x24 && @@ -164,4 +164,12 @@ inline int GetWitnessCommitmentIndex(const CBlock& block) return commitpos; } +inline int GetWitnessCommitmentIndex(const CBlock& block) +{ + if (block.vtx.empty()) { + return NO_WITNESS_COMMITMENT; + } + return GetWitnessCommitmentIndex(*block.vtx[0]); +} + #endif // BITCOIN_CONSENSUS_VALIDATION_H diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 3e34c3abf398..fb642400810b 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -10,9 +10,9 @@ #include #include #include +#include #include #include -#include #include #include #include diff --git a/src/node/txproof.cpp b/src/node/txproof.cpp new file mode 100644 index 000000000000..8f9011a5caad --- /dev/null +++ b/src/node/txproof.cpp @@ -0,0 +1,143 @@ +// Copyright (c) 2026-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://opensource.org/license/mit. + +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace node { +namespace { + +bool VerifyMerkleBranch(const uint256& root, const uint256& leaf, const std::vector& branch, uint32_t pos, uint32_t tx_count) +{ + const auto computed_root{ComputeMerkleRootFromBranch(leaf, branch, pos, tx_count)}; + return computed_root && *computed_root == root; +} + +bool VerifyWitnessCommitment(const CTransaction& coinbase, const uint256& witness_root) +{ + const int witness_commitment_index{GetWitnessCommitmentIndex(coinbase)}; + if (witness_commitment_index == NO_WITNESS_COMMITMENT) return false; + + const auto& witness_stack{coinbase.vin[0].scriptWitness.stack}; + if (witness_stack.size() != 1 || witness_stack[0].size() != 32) return false; + + uint256 commitment{witness_root}; + CHash256().Write(commitment).Write(witness_stack[0]).Finalize(commitment); + return memcmp(commitment.begin(), &coinbase.vout[witness_commitment_index].scriptPubKey[6], 32) == 0; +} + +} // namespace + +std::optional MakeTxProof(const CBlock& block, const std::optional& wtxid) +{ + if (block.vtx.empty() || block.vtx.size() > std::numeric_limits::max()) return std::nullopt; + + TxProof proof; + proof.block_hash = block.GetHash(); + proof.block_tx_count = static_cast(block.vtx.size()); + proof.coinbase = block.vtx[0]; + + TxProofTransaction coinbase; + coinbase.txid = proof.coinbase->GetHash(); + coinbase.wtxid = proof.coinbase->GetWitnessHash(); + coinbase.pos = 0; + coinbase.txid_branch = TransactionMerklePath(block, /*position=*/0); + proof.transactions.push_back(std::move(coinbase)); + + if (!wtxid || *wtxid == proof.coinbase->GetWitnessHash()) return proof; + + const bool has_witness_commitment{GetWitnessCommitmentIndex(block) != NO_WITNESS_COMMITMENT}; + for (size_t i{1}; i < block.vtx.size(); ++i) { + const Txid txid{block.vtx[i]->GetHash()}; + const Wtxid block_wtxid{block.vtx[i]->GetWitnessHash()}; + if (*wtxid != block_wtxid) continue; + + TxProofTransaction transaction; + transaction.txid = txid; + transaction.wtxid = block_wtxid; + transaction.pos = static_cast(i); + if (has_witness_commitment) { + transaction.wtxid_branch = WitnessMerklePath(block, i); + } else { + transaction.txid_branch = TransactionMerklePath(block, i); + } + proof.transactions.push_back(std::move(transaction)); + return proof; + } + + return std::nullopt; +} + +std::optional VerifyTxProof(const TxProof& proof, const CBlockHeader& header, uint32_t block_tx_count) +{ + if (block_tx_count == 0) return std::nullopt; + if (!proof.coinbase || !proof.coinbase->IsCoinBase()) return std::nullopt; + if (proof.block_hash != header.GetHash()) return std::nullopt; + if (proof.block_tx_count != block_tx_count) return std::nullopt; + if (proof.transactions.empty()) return std::nullopt; + + const bool has_witness_commitment{GetWitnessCommitmentIndex(*proof.coinbase) != NO_WITNESS_COMMITMENT}; + TxProofVerificationResult result; + result.block_hash = proof.block_hash; + result.block_tx_count = proof.block_tx_count; + result.transactions.reserve(proof.transactions.size()); + + bool has_coinbase{false}; + std::set positions; + for (const auto& transaction : proof.transactions) { + if (transaction.pos >= proof.block_tx_count) return std::nullopt; + if (!positions.insert(transaction.pos).second) return std::nullopt; + + TxProofVerificationTransaction verified; + verified.pos = transaction.pos; + + if (transaction.pos == 0) { + if (has_coinbase) return std::nullopt; + if (!transaction.txid_branch || transaction.wtxid_branch) return std::nullopt; + if (transaction.txid != proof.coinbase->GetHash()) return std::nullopt; + if (transaction.wtxid != proof.coinbase->GetWitnessHash()) return std::nullopt; + if (!VerifyMerkleBranch(header.hashMerkleRoot, transaction.txid.ToUint256(), *transaction.txid_branch, transaction.pos, proof.block_tx_count)) { + return std::nullopt; + } + verified.txid = transaction.txid; + if (!has_witness_commitment || transaction.txid.ToUint256() == transaction.wtxid.ToUint256()) { + verified.wtxid = transaction.wtxid; + } + has_coinbase = true; + result.transactions.push_back(std::move(verified)); + continue; + } + + if (has_witness_commitment) { + if (!transaction.wtxid_branch || transaction.txid_branch) return std::nullopt; + const auto witness_root{ComputeMerkleRootFromBranch(transaction.wtxid.ToUint256(), *transaction.wtxid_branch, transaction.pos, proof.block_tx_count)}; + if (!witness_root || !VerifyWitnessCommitment(*proof.coinbase, *witness_root)) return std::nullopt; + verified.wtxid = transaction.wtxid; + result.transactions.push_back(std::move(verified)); + continue; + } + + if (!transaction.txid_branch || transaction.wtxid_branch) return std::nullopt; + if (transaction.txid.ToUint256() != transaction.wtxid.ToUint256()) return std::nullopt; + if (!VerifyMerkleBranch(header.hashMerkleRoot, transaction.txid.ToUint256(), *transaction.txid_branch, transaction.pos, proof.block_tx_count)) { + return std::nullopt; + } + verified.txid = transaction.txid; + verified.wtxid = transaction.wtxid; + result.transactions.push_back(std::move(verified)); + } + + if (!has_coinbase) return std::nullopt; + return result; +} + +} // namespace node diff --git a/src/node/txproof.h b/src/node/txproof.h new file mode 100644 index 000000000000..0b53874a352d --- /dev/null +++ b/src/node/txproof.h @@ -0,0 +1,51 @@ +// Copyright (c) 2026-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://opensource.org/license/mit. + +#ifndef BITCOIN_NODE_TXPROOF_H +#define BITCOIN_NODE_TXPROOF_H + +#include +#include +#include +#include + +#include +#include +#include + +namespace node { + +struct TxProofTransaction { + Txid txid; + Wtxid wtxid; + uint32_t pos{0}; + std::optional> txid_branch; + std::optional> wtxid_branch; +}; + +struct TxProof { + uint256 block_hash; + uint32_t block_tx_count{0}; + CTransactionRef coinbase; + std::vector transactions; +}; + +struct TxProofVerificationTransaction { + uint32_t pos{0}; + std::optional txid; + std::optional wtxid; +}; + +struct TxProofVerificationResult { + uint256 block_hash; + uint32_t block_tx_count{0}; + std::vector transactions; +}; + +std::optional MakeTxProof(const CBlock& block, const std::optional& wtxid); +std::optional VerifyTxProof(const TxProof& proof, const CBlockHeader& header, uint32_t block_tx_count); + +} // namespace node + +#endif // BITCOIN_NODE_TXPROOF_H diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index a28543fb3bbc..48d48abdc86f 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -234,6 +234,10 @@ static const CRPCConvertParam vRPCConvertParams[] = { "converttopsbt", 3, "psbt_version"}, { "gettxout", 1, "n" }, { "gettxout", 2, "include_mempool" }, + { "gettxproof", 0, "wtxids" }, + { "verifytxproof", 0, "txproof" }, + { "verifytxproof", 1, "options" }, + { "verifytxproof", 1, "require_active_chain" }, { "gettxoutproof", 0, "txids" }, { "gettxoutsetinfo", 1, "hash_or_height", ParamFormat::JSON_OR_STRING }, { "gettxoutsetinfo", 2, "use_index"}, diff --git a/src/rpc/txoutproof.cpp b/src/rpc/txoutproof.cpp index 628825fc80e6..80129258cce5 100644 --- a/src/rpc/txoutproof.cpp +++ b/src/rpc/txoutproof.cpp @@ -6,8 +6,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -18,7 +20,277 @@ #include #include +#include +#include + using node::GetTransaction; +using node::MakeTxProof; +using node::TxProof; +using node::TxProofVerificationResult; +using node::VerifyTxProof; + +static UniValue MerklePathToUniv(const std::vector& path) +{ + UniValue result{UniValue::VARR}; + for (const uint256& hash : path) { + result.push_back(hash.GetHex()); + } + return result; +} + +static std::optional> ParseMerklePath(const UniValue& proofs, const std::string& path, const std::string& key) +{ + const UniValue& merkle_path{proofs.find_value(key)}; + if (merkle_path.isNull()) return std::nullopt; + if (!merkle_path.isArray()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("%s.%s must be an array", path, key)); + } + std::vector result; + result.reserve(merkle_path.size()); + for (unsigned int i{0}; i < merkle_path.size(); ++i) { + result.push_back(ParseHashV(merkle_path[i], strprintf("%s.%s[%u]", path, key, i))); + } + return result; +} + +static UniValue TxProofTransactionToUniv(const node::TxProofTransaction& transaction) +{ + UniValue proof{UniValue::VOBJ}; + if (transaction.txid_branch) { + proof.pushKV("txid", MerklePathToUniv(*transaction.txid_branch)); + } + if (transaction.wtxid_branch) { + proof.pushKV("wtxid", MerklePathToUniv(*transaction.wtxid_branch)); + } + + UniValue result{UniValue::VOBJ}; + result.pushKV("txid", transaction.txid.GetHex()); + result.pushKV("wtxid", transaction.wtxid.GetHex()); + result.pushKV("pos", transaction.pos); + result.pushKV("proof", std::move(proof)); + return result; +} + +static UniValue TxProofTransactionsToUniv(const std::vector& transactions) +{ + UniValue result{UniValue::VARR}; + for (const auto& transaction : transactions) { + result.push_back(TxProofTransactionToUniv(transaction)); + } + return result; +} + +static UniValue TxProofToUniv(const TxProof& proof) +{ + UniValue result{UniValue::VOBJ}; + result.pushKV("blockhash", proof.block_hash.GetHex()); + result.pushKV("block_tx_count", proof.block_tx_count); + result.pushKV("coinbase_tx", EncodeHexTx(*proof.coinbase)); + result.pushKV("transactions", TxProofTransactionsToUniv(proof.transactions)); + return result; +} + +static std::optional ParseTxProof(const UniValue& proof) +{ + TxProof result; + result.block_hash = ParseHashO(proof, "blockhash"); + result.block_tx_count = proof.find_value("block_tx_count").getInt(); + + CMutableTransaction coinbase_mut; + if (!DecodeHexTx(coinbase_mut, proof.find_value("coinbase_tx").get_str())) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "coinbase_tx must be a serialized transaction"); + } + result.coinbase = MakeTransactionRef(std::move(coinbase_mut)); + + const UniValue& transactions{proof.find_value("transactions")}; + if (!transactions.isArray()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "transactions must be an array"); + } + if (transactions.size() > 2) { + return std::nullopt; + } + result.transactions.reserve(transactions.size()); + for (unsigned int i{0}; i < transactions.size(); ++i) { + const UniValue& transaction{transactions[i]}; + if (!transaction.isObject()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("transactions[%u] must be an object", i)); + } + + auto& entry{result.transactions.emplace_back()}; + entry.txid = Txid::FromUint256(ParseHashO(transaction, "txid")); + entry.wtxid = Wtxid::FromUint256(ParseHashO(transaction, "wtxid")); + entry.pos = transaction.find_value("pos").getInt(); + + const UniValue& branches{transaction.find_value("proof")}; + if (!branches.isObject()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("transactions[%u].proof must be an object", i)); + } + const std::string path{strprintf("transactions[%u].proof", i)}; + entry.txid_branch = ParseMerklePath(branches, path, "txid"); + entry.wtxid_branch = ParseMerklePath(branches, path, "wtxid"); + if (!entry.txid_branch && !entry.wtxid_branch) return std::nullopt; + } + return result; +} + +static UniValue TxProofVerificationResultToUniv(const TxProofVerificationResult& proof) +{ + UniValue transactions{UniValue::VARR}; + for (const auto& transaction : proof.transactions) { + UniValue entry{UniValue::VOBJ}; + entry.pushKV("pos", transaction.pos); + if (transaction.txid) { + entry.pushKV("txid", transaction.txid->GetHex()); + } + if (transaction.wtxid) { + entry.pushKV("wtxid", transaction.wtxid->GetHex()); + } + transactions.push_back(std::move(entry)); + } + + UniValue result{UniValue::VOBJ}; + result.pushKV("blockhash", proof.block_hash.GetHex()); + result.pushKV("block_tx_count", proof.block_tx_count); + result.pushKV("transactions", std::move(transactions)); + return result; +} + +static const CBlockIndex* GetTxProofBlockIndex(ChainstateManager& chainman, const std::set& txids, const UniValue& blockhash_param) +{ + const CBlockIndex* pblockindex{nullptr}; + uint256 hash_block; + if (!blockhash_param.isNull()) { + LOCK(cs_main); + hash_block = ParseHashV(blockhash_param, "blockhash"); + pblockindex = chainman.m_blockman.LookupBlockIndex(hash_block); + if (!pblockindex) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); + } + return pblockindex; + } + + { + LOCK(cs_main); + Chainstate& active_chainstate = chainman.ActiveChainstate(); + + // Loop through txids and try to find which block they're in. Exit loop once a block is found. + for (const auto& txid : txids) { + const Coin& coin{AccessByTxid(active_chainstate.CoinsTip(), txid)}; + if (!coin.IsSpent()) { + pblockindex = active_chainstate.m_chain[coin.nHeight]; + break; + } + } + } + + // Allow txindex to catch up if we need to query it and before we acquire cs_main. + if (g_txindex && !pblockindex) { + g_txindex->BlockUntilSyncedToCurrentChain(); + } + + if (pblockindex == nullptr) { + const CTransactionRef tx = GetTransaction(/*block_index=*/nullptr, /*mempool=*/nullptr, *txids.begin(), chainman.m_blockman, hash_block); + if (!tx || hash_block.IsNull()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not yet in block"); + } + + LOCK(cs_main); + pblockindex = chainman.m_blockman.LookupBlockIndex(hash_block); + if (!pblockindex) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "Transaction index corrupt"); + } + } + + return pblockindex; +} + +static RPCMethod gettxproof() +{ + return RPCMethod{ + "gettxproof", + "Returns a transaction inclusion proof for a transaction in a block.\n" + "\nThe proof uses plain merkle branches rather than the legacy BIP37 merkleblock encoding.\n" + "Target transactions are selected by witness transaction id. If the block has a witness\n" + "commitment, non-coinbase target transactions are proven by wtxid; otherwise they are\n" + "proven by txid, which is equal to wtxid for those transactions. The coinbase transaction\n" + "is always included at position 0 so verifiers can prove the presence or absence of the\n" + "witness commitment. Passing an empty wtxids array returns only this coinbase proof, which\n" + "is equivalent to requesting the coinbase transaction's wtxid. The wtxids array is reserved\n" + "for future expansion, but currently accepts at most one element.\n", + { + {"wtxids", RPCArg::Type::ARR, RPCArg::Optional::NO, "The witness transaction id to prove. Accepts at most one element. Empty array returns only the coinbase proof", + { + {"wtxid", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A witness transaction id"}, + }, + }, + {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The block hash"}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "blockhash", "The block hash the proof links to"}, + {RPCResult::Type::NUM, "block_tx_count", "The number of transactions in the block"}, + {RPCResult::Type::STR_HEX, "coinbase_tx", "The serialized coinbase transaction, hex-encoded with witness data"}, + {RPCResult::Type::ARR, "transactions", "The coinbase proof followed by the target transaction if one was requested", { + {RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::STR_HEX, "txid", "The transaction id"}, + {RPCResult::Type::STR_HEX, "wtxid", "The witness transaction id"}, + {RPCResult::Type::NUM, "pos", "The transaction position in the block"}, + {RPCResult::Type::OBJ, "proof", "Merkle branches ordered from leaf to root", { + {RPCResult::Type::ARR, "txid", /*optional=*/true, "Branch proving txid against the block header merkle root; present for the coinbase and for non-coinbase targets when the block has no witness commitment", { + {RPCResult::Type::STR_HEX, "", "A sibling hash"}, + }}, + {RPCResult::Type::ARR, "wtxid", /*optional=*/true, "Branch proving wtxid against the witness merkle root; present for non-coinbase targets when the block has a witness commitment", { + {RPCResult::Type::STR_HEX, "", "A sibling hash"}, + }}, + }}, + }}, + }}, + } + }, + RPCExamples{""}, + [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue + { + std::set wtxids; + const UniValue wtxid_values{request.params[0].get_array()}; + for (unsigned int i{0}; i < wtxid_values.size(); ++i) { + auto inserted{wtxids.insert(Wtxid::FromUint256(ParseHashV(wtxid_values[i], strprintf("wtxids[%u]", i))))}; + if (!inserted.second) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, duplicated wtxid: %s", wtxid_values[i].get_str())); + } + } + if (wtxids.size() > 1) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Parameter 'wtxids' accepts at most one element"); + } + std::optional wtxid; + if (!wtxids.empty()) { + wtxid = *wtxids.begin(); + } + + const uint256 block_hash{ParseHashV(request.params[1], "blockhash")}; + ChainstateManager& chainman = EnsureAnyChainman(request.context); + const CBlockIndex* pblockindex{nullptr}; + { + LOCK(cs_main); + pblockindex = chainman.m_blockman.LookupBlockIndex(block_hash); + if (!pblockindex) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); + } + CheckBlockDataAvailability(chainman.m_blockman, *pblockindex, /*check_for_undo=*/false); + } + CBlock block; + if (!chainman.m_blockman.ReadBlock(block, *pblockindex)) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk"); + } + + const auto proof{MakeTxProof(block, wtxid)}; + if (!proof) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Not all transactions found in specified block"); + } + return TxProofToUniv(*proof); + }, + }; +} static RPCMethod gettxoutproof() { @@ -55,48 +327,8 @@ static RPCMethod gettxoutproof() } } - const CBlockIndex* pblockindex = nullptr; - uint256 hashBlock; ChainstateManager& chainman = EnsureAnyChainman(request.context); - if (!request.params[1].isNull()) { - LOCK(cs_main); - hashBlock = ParseHashV(request.params[1], "blockhash"); - pblockindex = chainman.m_blockman.LookupBlockIndex(hashBlock); - if (!pblockindex) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); - } - } else { - LOCK(cs_main); - Chainstate& active_chainstate = chainman.ActiveChainstate(); - - // Loop through txids and try to find which block they're in. Exit loop once a block is found. - for (const auto& tx : setTxids) { - const Coin& coin{AccessByTxid(active_chainstate.CoinsTip(), tx)}; - if (!coin.IsSpent()) { - pblockindex = active_chainstate.m_chain[coin.nHeight]; - break; - } - } - } - - - // Allow txindex to catch up if we need to query it and before we acquire cs_main. - if (g_txindex && !pblockindex) { - g_txindex->BlockUntilSyncedToCurrentChain(); - } - - if (pblockindex == nullptr) { - const CTransactionRef tx = GetTransaction(/*block_index=*/nullptr, /*mempool=*/nullptr, *setTxids.begin(), chainman.m_blockman, hashBlock); - if (!tx || hashBlock.IsNull()) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not yet in block"); - } - - LOCK(cs_main); - pblockindex = chainman.m_blockman.LookupBlockIndex(hashBlock); - if (!pblockindex) { - throw JSONRPCError(RPC_INTERNAL_ERROR, "Transaction index corrupt"); - } - } + const CBlockIndex* pblockindex{GetTxProofBlockIndex(chainman, setTxids, request.params[1])}; { LOCK(cs_main); @@ -126,6 +358,101 @@ static RPCMethod gettxoutproof() }; } +static RPCMethod verifytxproof() +{ + return RPCMethod{ + "verifytxproof", + "Verifies a transaction inclusion proof produced by gettxproof.\n" + "\nReturns an object with the proven transaction identifiers if the proof is valid, or an empty\n" + "object if the proof is well-formed but invalid. For non-coinbase targets, blocks with a witness\n" + "commitment prove the wtxid, and blocks without a witness commitment prove the txid.\n" + "\nBy default, the block must be in the active chain. Set require_active_chain=false to verify\n" + "proofs for known stale blocks.\n", + { + {"txproof", RPCArg::Type::OBJ, RPCArg::Optional::NO, "The proof object generated by gettxproof", + { + {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The block hash the proof links to"}, + {"block_tx_count", RPCArg::Type::NUM, RPCArg::Optional::NO, "The number of transactions in the block"}, + {"coinbase_tx", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The serialized coinbase transaction, hex-encoded with witness data"}, + {"transactions", RPCArg::Type::ARR, RPCArg::Optional::NO, "The coinbase proof followed by the target transaction if one was requested", + { + {"transaction", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", + { + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, + {"wtxid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The witness transaction id"}, + {"pos", RPCArg::Type::NUM, RPCArg::Optional::NO, "The transaction position in the block"}, + {"proof", RPCArg::Type::OBJ, RPCArg::Optional::NO, "Merkle branches ordered from leaf to root", + { + {"txid", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "Branch proving txid against the block header merkle root; present for the coinbase and for non-coinbase targets when the block has no witness commitment", + { + {"hash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A sibling hash"}, + }, + }, + {"wtxid", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "Branch proving wtxid against the witness merkle root; present for non-coinbase targets when the block has a witness commitment", + { + {"hash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A sibling hash"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + RPCArgOptions{.oneline_description = "txproof"}}, + {"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "", + { + {"require_active_chain", RPCArg::Type::BOOL, RPCArg::Default{true}, "If true, require the proof's block to be in the active chain"}, + }, + }, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "The proven transaction information if the proof is valid, or an empty object if invalid", + { + {RPCResult::Type::STR_HEX, "blockhash", /*optional=*/true, "The block hash this proof links to"}, + {RPCResult::Type::NUM, "block_tx_count", /*optional=*/true, "The number of transactions in the block"}, + {RPCResult::Type::ARR, "transactions", /*optional=*/true, "The proven transaction identifiers", { + {RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::NUM, "pos", "The transaction position in the block"}, + {RPCResult::Type::STR_HEX, "txid", /*optional=*/true, "The proven transaction id"}, + {RPCResult::Type::STR_HEX, "wtxid", /*optional=*/true, "The proven witness transaction id"}, + }}, + }}, + } + }, + RPCExamples{""}, + [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue + { + const auto proof{ParseTxProof(request.params[0].get_obj())}; + if (!proof) return UniValue{UniValue::VOBJ}; + const UniValue options{request.params[1].isNull() ? UniValue::VOBJ : request.params[1].get_obj()}; + const bool require_active_chain{options["require_active_chain"].isNull() ? true : options["require_active_chain"].get_bool()}; + + const CBlockIndex* pindex{nullptr}; + CBlockHeader header; + uint32_t block_tx_count{0}; + ChainstateManager& chainman = EnsureAnyChainman(request.context); + { + LOCK(cs_main); + pindex = chainman.m_blockman.LookupBlockIndex(proof->block_hash); + if (!pindex || pindex->nTx == 0) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found in chain"); + } + if (require_active_chain && !chainman.ActiveChain().Contains(*pindex)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found in active chain"); + } + if (pindex->nTx != proof->block_tx_count) return UniValue{UniValue::VOBJ}; + header = pindex->GetBlockHeader(); + block_tx_count = pindex->nTx; + } + + const auto verification{VerifyTxProof(*proof, header, block_tx_count)}; + return verification ? TxProofVerificationResultToUniv(*verification) : UniValue{UniValue::VOBJ}; + }, + }; +} + static RPCMethod verifytxoutproof() { return RPCMethod{ @@ -177,6 +504,8 @@ static RPCMethod verifytxoutproof() void RegisterTxoutProofRPCCommands(CRPCTable& t) { static const CRPCCommand commands[]{ + {"blockchain", &gettxproof}, + {"blockchain", &verifytxproof}, {"blockchain", &gettxoutproof}, {"blockchain", &verifytxoutproof}, }; diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index a25d9f900a62..2dc67a888988 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -121,6 +121,7 @@ add_executable(test_bitcoin txindex_tests.cpp txospenderindex_tests.cpp txpackage_tests.cpp + txproof_tests.cpp txreconciliation_tests.cpp txrequest_tests.cpp txvalidation_tests.cpp @@ -150,6 +151,7 @@ target_json_data_sources(test_bitcoin data/key_io_valid.json data/script_tests.json data/sighash.json + data/txproof.json data/tx_invalid.json data/tx_valid.json ) diff --git a/src/test/data/txproof.json b/src/test/data/txproof.json new file mode 100644 index 000000000000..485ca9524d6d --- /dev/null +++ b/src/test/data/txproof.json @@ -0,0 +1,246 @@ +[ + { + "description": "mainnet block 1", + "height": 1, + "block_hex": "010000006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e362990101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000", + "cases": [ + { + "description": "empty_no_witness", + "txid": "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098", + "proof": { + "blockhash": "00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048", + "block_tx_count": 1, + "coinbase_tx": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000", + "transactions": [ + { + "txid": "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098", + "wtxid": "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098", + "pos": 0, + "proof": { + "txid": [] + } + } + ] + } + } + ] + }, + { + "description": "mainnet block 586", + "height": 586, + "block_hex": "0100000038babc9586a5fcd60713573494f4377e7c401c33aa24729a4f6cff46000000004d5969c0d10dcce60868fee4d4de80ba5ef38abaeed8a75daa63e48c963d7b1950476f49ffff001d2d9791370301000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0804ffff001d025d06ffffffff0100f2052a0100000043410410daf049ef402de0b6adba8b0f7c392bcf9a6385116efc8b4143b8b7a7841e7de73b478ffe13b60c50ea01e24b4b48c24f5e0fbc5d6c8433c7ca7c3ed3ab8173ac0000000001000000050f40f5e65e115eb4bdb3007f0fb8beaa404cf7ae45de16074e8acc9b69bbf0c3000000004847304402201092da40af6dea8abcbeefb8586335b26d39d36be9b6c38d6c9cc18f20dd5886022045964de79a9008f68d53fc9bc58f9e30b224a1b98dbfda5c7b7b860f32c6aef101ffffffff1bb875b247332e558731c2c510f611d3dde991ea9fe69365bf445a0ccd513b190000000049483045022100b0a1d0a00251c56809a5ab5d7ba6cbe68b82c9bf4f806ee39c568ae537572c840220781ce69017ec3b2d6f96ffff4d19c80c224f40c73b8c26cba4b30e7f4171579b01ffffffff2099e1a92d94c35f0645683257c4c255165385f3e9129a85fed5a3f3d867c9b60000000049483045022100c8e980f43c616232e2d59dce08a5edb84aaa0915ea49780a8af367330216084a02203cc2628f16f995c7aaf6104cba64971963a4e084e4fbd0b6bcf825b47a09f8e301ffffffff5fb770c4de700aca7f74f5e6295f248edafa9423e446d76f4650df9b90f939a700000000494830450220745a8d99c51f98f5c93b8d2f5f14a1f2d8cc42ff7329645681bcafe846cbf50d022100b24e31186129f3ae6cc8a226d1eda389373652a9cf2095631fcc4345067c1ff301ffffffff968d4c096ee861307935d21d797a902b647dc970d3c8374cc13551f8397abbd80000000049483045022100ca65b3f290724d6c56fc333570fa342f2477f34b2a6c93c2e2d7216d9fe9088e022077e259a29ed1f988fab2b9f2ce17a4a56a20c188cadc72bca94e06a73826966501ffffffff0100ba1dd20500000043410497304efd3ab14d0dcbf1e901045a25f4b5dbaf576d074506fd8ded4122ba6f6bec0ed4698ce0e7928c0eaf9ddfb5387929b5d697e82e7aabebe04c10e5c87164ac0000000001000000010d26ba57ff82fefcb43826b45019043e2b6ef9aa8118b7f743167584a7f9cae70000000049483045022024fd7345df2b2bd0e6f8416529046b7d52bda5ffdb70146bc6d72b1ba73cabcd022100ff99c03006cc8f28d92e686f0ae640d20395177f329d0a9dbd560fd2a55aeee701ffffffff0100f2052a01000000434104888d890e1bd84c9e2ac363a9774414a081eb805cd2c0d52e49efc7170ebf342f1cdb284a2e2eb754fc8dd4525fe0caa3d3a525214d0b504dd75376b2f63804a8ac00000000", + "cases": [ + { + "description": "pre_segwit_non_coinbase_odd_tx_count", + "txid": "4d6edbeb62735d45ff1565385a8b0045f066055c9425e21540ea7a8060f08bf2", + "proof": { + "blockhash": "000000000d0d23516c5efd3af4eb951603bb30b2c93884b522a318b30e918ee7", + "block_tx_count": 3, + "coinbase_tx": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0804ffff001d025d06ffffffff0100f2052a0100000043410410daf049ef402de0b6adba8b0f7c392bcf9a6385116efc8b4143b8b7a7841e7de73b478ffe13b60c50ea01e24b4b48c24f5e0fbc5d6c8433c7ca7c3ed3ab8173ac00000000", + "transactions": [ + { + "txid": "d45724bacd1480b0c94d363ebf59f844fb54e60cdfda0cd38ef67154e9d0bc43", + "wtxid": "d45724bacd1480b0c94d363ebf59f844fb54e60cdfda0cd38ef67154e9d0bc43", + "pos": 0, + "proof": { + "txid": [ + "4d6edbeb62735d45ff1565385a8b0045f066055c9425e21540ea7a8060f08bf2", + "ca851cc1acd01b82667c83fd88914b9a3e1a0a99fbf2b83c89cc79ea29d0c5f0" + ] + } + }, + { + "txid": "4d6edbeb62735d45ff1565385a8b0045f066055c9425e21540ea7a8060f08bf2", + "wtxid": "4d6edbeb62735d45ff1565385a8b0045f066055c9425e21540ea7a8060f08bf2", + "pos": 1, + "proof": { + "txid": [ + "d45724bacd1480b0c94d363ebf59f844fb54e60cdfda0cd38ef67154e9d0bc43", + "ca851cc1acd01b82667c83fd88914b9a3e1a0a99fbf2b83c89cc79ea29d0c5f0" + ] + } + } + ] + } + }, + { + "description": "odd_node_duplication_target", + "txid": "6bf363548b08aa8761e278be802a2d84b8e40daefe8150f9af7dd7b65a0de49f", + "proof": { + "blockhash": "000000000d0d23516c5efd3af4eb951603bb30b2c93884b522a318b30e918ee7", + "block_tx_count": 3, + "coinbase_tx": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0804ffff001d025d06ffffffff0100f2052a0100000043410410daf049ef402de0b6adba8b0f7c392bcf9a6385116efc8b4143b8b7a7841e7de73b478ffe13b60c50ea01e24b4b48c24f5e0fbc5d6c8433c7ca7c3ed3ab8173ac00000000", + "transactions": [ + { + "txid": "d45724bacd1480b0c94d363ebf59f844fb54e60cdfda0cd38ef67154e9d0bc43", + "wtxid": "d45724bacd1480b0c94d363ebf59f844fb54e60cdfda0cd38ef67154e9d0bc43", + "pos": 0, + "proof": { + "txid": [ + "4d6edbeb62735d45ff1565385a8b0045f066055c9425e21540ea7a8060f08bf2", + "ca851cc1acd01b82667c83fd88914b9a3e1a0a99fbf2b83c89cc79ea29d0c5f0" + ] + } + }, + { + "txid": "6bf363548b08aa8761e278be802a2d84b8e40daefe8150f9af7dd7b65a0de49f", + "wtxid": "6bf363548b08aa8761e278be802a2d84b8e40daefe8150f9af7dd7b65a0de49f", + "pos": 2, + "proof": { + "txid": [ + "6bf363548b08aa8761e278be802a2d84b8e40daefe8150f9af7dd7b65a0de49f", + "baf459e5571e1003a0eb9cf7dc472329b1687579be4a27fbfa9347930bb1cc94" + ] + } + } + ] + } + }, + { + "description": "pre_segwit_coinbase", + "txid": "d45724bacd1480b0c94d363ebf59f844fb54e60cdfda0cd38ef67154e9d0bc43", + "proof": { + "blockhash": "000000000d0d23516c5efd3af4eb951603bb30b2c93884b522a318b30e918ee7", + "block_tx_count": 3, + "coinbase_tx": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0804ffff001d025d06ffffffff0100f2052a0100000043410410daf049ef402de0b6adba8b0f7c392bcf9a6385116efc8b4143b8b7a7841e7de73b478ffe13b60c50ea01e24b4b48c24f5e0fbc5d6c8433c7ca7c3ed3ab8173ac00000000", + "transactions": [ + { + "txid": "d45724bacd1480b0c94d363ebf59f844fb54e60cdfda0cd38ef67154e9d0bc43", + "wtxid": "d45724bacd1480b0c94d363ebf59f844fb54e60cdfda0cd38ef67154e9d0bc43", + "pos": 0, + "proof": { + "txid": [ + "4d6edbeb62735d45ff1565385a8b0045f066055c9425e21540ea7a8060f08bf2", + "ca851cc1acd01b82667c83fd88914b9a3e1a0a99fbf2b83c89cc79ea29d0c5f0" + ] + } + } + ] + } + } + ] + }, + { + "description": "mainnet block 481828", + "height": 481828, + "block_hex": "02000020b4747817e93fab4d169f61911d81ece2db528ad563985000000000000000000077a8959a0fc500b3bed63e381ad001543468df754ba85ddf7b57e293fccd784302409e59e93c0118311e5f4601010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff3203245a07254d696e656420627920416e74506f6f6c6d2f4542312f4144362f4e59412f5720599e400201ae3700003b7c0000ffffffff02807c814a000000001976a9140801bee4ed1e0ad6c0c4a5a7205142a8e0b2ce5388ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", + "cases": [ + { + "description": "empty_with_coinbase_witness_commitment", + "txid": "4378cdfc93e2577bdf5da84b75df68345401d01a383ed6beb300c50f9a95a877", + "proof": { + "blockhash": "000000000000000000bc331eb461d522cb3673b3881217dc30c6a5cf91710f23", + "block_tx_count": 1, + "coinbase_tx": "010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff3203245a07254d696e656420627920416e74506f6f6c6d2f4542312f4144362f4e59412f5720599e400201ae3700003b7c0000ffffffff02807c814a000000001976a9140801bee4ed1e0ad6c0c4a5a7205142a8e0b2ce5388ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", + "transactions": [ + { + "txid": "4378cdfc93e2577bdf5da84b75df68345401d01a383ed6beb300c50f9a95a877", + "wtxid": "f5083f5f0533eed905307655d38197c6d967fec0d7c472cb30d4b09ea0fc8181", + "pos": 0, + "proof": { + "txid": [] + } + } + ] + } + } + ] + }, + { + "description": "mainnet block 484645", + "height": 484645, + "block_hex": "00000020f47ab17703c494aa3f34369662ca83b24108a4266805370000000000000000006a03f20b033af2d4f29330839ff42cc573a6dce39de033d63f5f9a3e16ba12d57765b6590b3101185768eafa1d010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff610325650741d66d9956bac3f841d66d99557a463b2f4254432e544f502f4e59412f4542312f4144362ffabe6d6d4a18b6e7c11bbc6ef6a16fb291914805ccf4babbc562509d61dc01f5ec9df79f8000000000000000083daee9bf01004000000000ffffffff021b348d4a000000001976a914ba507bae8f1643d2556000ca26b9301b9069dc6b88ac0000000000000000266a24aa21a9ed7e08dbed03e0265fa0a96c32c027184310cc30d160850839e4344fa695a35a1c0120000000000000000000000000000000000000000000000000000000000000000000000000010000000157bf13aa09c2db16121d54cb1d089dc5fd390c6313d0ac8737dc77eb986bf4e2010000008b483045022100f2cd29259722d5e2fa3bed2b35024ea110f3bf9922e5efe7ac5f35d5bde6bccf022073191a8bdd461edf00a1e6050a00f464fa599b9068e73aa3aea1c3dadf2a63f2014104453b0932ee82b613d438f8fe301538f44d95f7cba887cc876be031161ca9d1d9752a84f20124089d4fcfd08b7fd3a7d2dd1d83f5b26c3d1175d5c8f7b3243564ffffffff02801d2c04000000001976a914e2812286bfbe4b9aa0bdd1681b9a947fda3d72f988ac80969800000000001976a91424f680d24674804f4e0441ab1a91c1d50d6cd23888ac000000000100000005f47f0ce5aef653fa86203f64c612501ac7dafcb67d4f27eff518d8a071dd50d40e0000006a47304402206d776ee018c459a1cd154c0b176a725145377c2be87f040af19fbaf653d1f1c7022043b4c4ccb9e2703ff4bfeb6618389c4ce0507e6afba279119ef54f6188c6a1da012103b479ce493ea76bb435091d33008fdb6a81ae2ac0ee12393791c00a6a991441b7ffffffffcf4c63e8afefa1252107fbc8eab0514fe809751d7844db36b3ed6e38dbb73897010000006a4730440220775b4dbdac6e95a9464164d4bf5018b3421d587965ca42aa1ee6ab9411ecd3cf02202aec4be661379ec68da05c37c99e8a7408af64a0f2df978378c104e38fa51677012103b479ce493ea76bb435091d33008fdb6a81ae2ac0ee12393791c00a6a991441b7ffffffffd428602ed293175afc5eb6657911d13edeb004866c320932ffc9cb6c6f41ba67220000006b4830450221008674365af63da68ff73939ed72a54ecce9cc6a7d0f382102530a97fca604a76d022041ac881b7614e2a81248ca34a8783cc5214e3da95d088e059090e31aa512b3f9012103b479ce493ea76bb435091d33008fdb6a81ae2ac0ee12393791c00a6a991441b7ffffffffe974c293901452292782d5783afb98949e615d920294de24ce86b00ac18931731a0000006b4830450221008e1a9c393a0473da882081cbf0a115529942325e0c6f7f4b88cd4f1b6bf3349a02202ea7dc42daa865398be527e39a6cb6ca07d6d847b352b889c6be69450ef756220121039479be8da4523fdb3955438a80e6d280916830fe01bf07244906884a49c89e55ffffffff1a311ef60c5b8558db076bbb896aec71e081cfeb09a500f1f34b96df2bbc3a84530000006a47304402203b99fc5e3f5dacb71bbd939e85c030583246875f56ba88d944d16a1b7de04c47022027c727f9ea31292b9d4483d0b32ce5d2a53496b100ef90f364d3c25da5797cd7012103b479ce493ea76bb435091d33008fdb6a81ae2ac0ee12393791c00a6a991441b7ffffffff0140787d01000000001976a914c8926826397f5a0eb7a5965fe5c57e67e9d4f91e88ac00000000010000000001011c14db0d49dc6eb6f94109f2d7a8016b13188fb010a4d5c982eb5f57d27fd442000000001716001425225ca6ddcfb58c94f3551f7d392f5d5c74326fffffffff0275aee2510500000017a914bda19bee58b3c72611e4a04c3a9cdd1c695a462e8790628d00000000001976a9140476cfef62d41dcf675a53f089e1eb6f084a298188ac0247304402207de377aadaa2a9ec45737b0c23e36261034ff4aa61b4032612f65e6b075dce4b02205f20c6e7f11e44be2875f67496eb2cd28f9446855a49dd6d79e5cbfc59f3d35f012102a73b6ea2341009ad144ff2f334157699c2969eb701b7bbb3ca8131895760db06000000000100000002957c93f866da8b2ae1c6024dc9b16dcdafa33e8ea3adf7bb0cc7a671d83fdedd010000006b483045022100b509fd07f7344da975fe64d5e56113de555f660381690d50e0c6870c71984a0a02200df3f6d6502f2abed6ebbdd0e5898df83b6916d518044db89da08c62ad30ee5c012103ffa9d831a9a21e861a879baa0ec76d98336b11919243f32444070e0757c31925ffffffff133ff64b2337107d0f5c27b833a19ac3e128358665ddce1d0a48f794a0818247010000006b483045022100d7db63c42d52ef372dbb4f303f475129c71745ec04d680ae3dd83c57769b641b0220254e67a7842fb69c38f74d8fdf95f91e38b3ceb73919c200121b4dee773dc20501210286f098a125cb9a28e7fa3e8840066ba1d97a7c33e9e9ec46026e4fd3f320041dffffffff0200c2eb0b0000000017a91462d6422c294c1c87407f809f13f7960aec3cc4c58764bad017000000001976a914c6bc67b820ba4662dc395cbaeab61ada238b91bc88ac0000000001000000015f5cb75ec88c55f1caacecd13253b1e8e9ea633f34e44a7e28dc03b32952c69700000000fd3d0100483045022100de07b0235c3ab2b469706fc771b67c4ef5054571bafff41d74b5218a40dd6bcf022064ff2a5ebee2cc95a46b9c9df03c1791369e7b08fa621d70b27e016ac63cf7500147304402204a114e251bd6ef1f513aeb3ddfac0bee05396b1b543e42916b605441c43f4b2a0220365aade982d807dc7e7c8b8aca87d2e0eae9a150cb8748c8ac5a8d582109db1a014ca9522103ec23a502731ca6ad17358a7b011b52d16a894db7a193360064da79ef55dbe976410471b85da68c48db5941ac0172efd463bf730890f727ad90501444460470afc7b591b82a86f607c392c58250ccfb7222599d83e8f4e8b918edb8ff2373a818ebe9410434b4076879b6ab817a35397ecc58a26d23826d207e99f092d9ea80eea21ef4b18d74ac76fd8a127beafc1990eb9caf3573d8ef839fcb218e8b01ea4e1061b87753aeffffffff01e4779b210000000017a91462b68738e8867cd7bb6d41aca069c4a3b873ad8787000000000100000001f5529f2a0b96ff9a08b1b62f428f9aef5d6961d2c8851a293abe5c03df86d7cf010000006a47304402202ff714c41bbab4fa4d144887e49636ec8351bc4c66be3b9215b0a66f60b91d3b02200a0e9cec6fa074d8c9398bda9c9860146f65bfe04b8028f974e80683273a53770121037a1d025f3f6eb8c55dc7e70a95c535fd335762151ba7e14c5eb50eab96d1ff74ffffffff02c0a00800000000001976a91446f2d815bda65a255c4d664e72464fb5cff049db88ac572cbb11000000001976a914eb96ab576cd90e80eabe6bf6314d463a0a25d46a88ac0000000001000000014d33d77ca9cb96da752ada62bf775d350e6f25620317d55015c792d70213e4e9080000008a47304402205d28a937a6e6b2b549d956e5b2803c38b1ceed305aed363ab63e718d2d49104c0220053ab7f24054689ddf72320fb078ec0ffa7e9cc03b4c94bae48b52e0b7c38aee014104fcf07bb1222f7925f2b7cc15183a40443c578e62ea17100aa3b44ba66905c95d4980aec4cd2f6eb426d1b1ec45d76724f26901099416b9265b76ba67c8b0b73dffffffff020000000000000000166a146f6d6e690000000000000001000000004aa8d0b1aa0a0000000000001976a914ef5bf97440d8d275cd9f4fd3c13c520061efddf088ac00000000020000000184cc8925321f3d784dcaf97aece52c3f8b8023135bd9b2b9724ff8889f905a87000000006a473044022011e56c5c3050b12f480e2c69750a5dfe7df44c843c6d9320ecf91e03adca0ae30220567a02ebdf7a89427b6de6c13c04779c49813de68e198e7a0da4fe8b008b50a20121039ee474ed25964ba615c3eec11ea5ac4d63d1ea874d7c6b7cd7d799c02cecbac2feffffff0272c61c00000000001976a9145161ee88833a709683781ff042a7fbfab6de21aa88aca29ded8f050000001976a9146795bc669de295d9cc8512137df380892ab61c8488ac2365070002000000018885f00accf2339c98ba1f223a162b0490cc2aa2611faa605e8ad7ca53a265c4010000006a4730440220603312ddad9fb0ecb7b91ce209f69c0cda4d8cca19de54c28ff07f91df33537d02206aba64831372912cba786987d623b834ff74ea2b5574f210dcf19b884204033201210298913980abfad5f132efe7d5c3d0bfb84887dd1f4351cfca6ce9fc1b7166b5fbfeffffff02e3523300000000001976a91408c2ca30170eeb9d1b791ccc2d46aecc0ed3442e88ac00093d00000000001976a91424499d22150273705e443cbb90ba0664fc1121ad88ac046507000100000002c4f4bd8f362de4d69565bd599333a602b4d01c09dd2c1f93a695ed9ac3e67997000000006b483045022100e59342598917fee5f67369938193e6f27183d18685c6c8dbbf58738cdcba560a0220056569f90d8ae47f78740b1b22e923a61e19632fdc70ed6ad82baf8365614337012103ef46899c589f53bec1f6af593711e0e4ca0ee368c189f495e1efc09e0aaf94d2feffffff59982a069fd678ffe8f4a3a5b66270a2da5e244550096dd86b228fca2d19a7c5000000006b48304502210098735e3f91c8f7f0d7d46276677ed5b1771f132252a72f13d94c554ac41246c4022020cc805965936408b74e9ce90de65e1bb064c1176fe370a0f2ec798844b6fcea012103516cebd3712c1e3040342abb7ecc1b60a5cb3e6ef653a2201a5909abd3ecaf2dfeffffff02eb430f00000000001976a914c9c2c6220c8e034a8481537858be00095ca1f1dd88acb0ee1200000000001976a914f9c7c66c49bb9d8d1f535d1c6628abb1cdca209188ac2365070002000000018bff29e94e898fd4f33cf8051d6ea1fe4162fe705e346eb0426f10f2bbdcbb610c0000006a4730440220072d02d3441ab905d1d1bfb386f34549f52bea3f42defe8dad48d559c07b1b3c0220699e32a31cc83389dd61bda3d64711c21400314b8fb1ddf44c9926b9d86e32a6012102b4f4890802802d3ebfef7509eba0fa07160db8a4fc3f638061c5e5e34e5e397efeffffff025a931000000000001976a9141736d14917e05ad172e635ce74976e5837f7c85988acced70e00000000001976a914a5f7049ce5eaa113d7e0e34205c402827bc3cac088ac23650700010000000207377d11a9d3833e0646e299941514c195860cb54280bccc10ac260c56d09e9e0d0000006a4730440220109dd294b41df3c31b84b3bc2f42b2973a8cef66fca5d93b040b4420c18acade022028fdec6e776e4e8bc72ef8199da448a180ebfed5010042f207af3621f80349a10121023f129d9e5a83aeff751af4aadadb7273e470e779c72ef2f18cb5e278eab92749feffffffef25064c08737b50a69e7e680af5497accfcd028491f07d3981a8dbc0451ad88010000006b483045022100cda2faba37f8fb148579491031a07d2d3def54af3a62ef595634df464059c05202205cee5c7ca8873a41cc080cce3a2a222b1e6616afbcb904904b7731072ea0782a0121027ce204dc91487153b3add2d6767388802922a26b45b0a2c72faae753177a21dbfeffffff0298091d00000000001976a91496c29f75c436dc542f21a9a61a3b0db08369f72d88ac81440f00000000001976a9147a35b0754c6b6ab31d8db8a259ee2116b0fcaeb988ac23650700010000000182ceec3b7534d86f116f8f155e023eaea37b0d56a15e260a2845e5d0c2182341010000006a473044022009bed8dc9612d9a51adefa872481f73bbf3007375a2a9b690e19def274571fd0022031987adda7d83754d1e0366b6aec1459f66ae152817e92fbbe7ccf1c25cb88db012103c1ee16ba3b762b6bfc2a578f2ea620fbfda5914256a4756156d5afd5940c9356ffffffff0279f20a00000000001976a9146ae8cac8d5509647e924e84f2b06c7c8eda635c988ac0adf6001000000001976a914095d0e3fb27a36278ad8ba4adbdd508de301595488ac000000000200000003603d33c8757739c5ed75f5f01cbeeaaf163995f532a560eff2fba8ab5aead61f01000000fdfe0000483045022100e67bca494ab38a19d04643b92b0932744763bb8a60c0723fdc6fbd9d9048c27e02205c3c369491bfc13e20037f32753e0ef581535dbab7ed30db196ce4551383281601483045022100bba78f1b8b6765283dce433131d3799b66f954d28c3ff90927a093439693473f02207b1757b5e4859970544e1adc5ed174a5409b02c605ed2b564e6704c11a13631c014c69522103f6519855bcb81e20e8ffe4735e6e57d19fe5450361b78a5b61f24e00b48af8c42102d197170e00ec0f858d1a444d4790cae9b231a0aa7105c8fc9d81256addf601fa210353cba08bdd49a62c2b77fcda3f11226499fd31d6d4b7cc959bb23c5b7997d17b53aeffffffff835afcf7a96c5a77fddb212ad2fced391085903d410569d21e04f09cb5a58c4012000000fdfe0000483045022100d00f0545a03df196aed57286581d2112205a78b926b7f70913ea1e50baa001b402207e3a58f1cbd8998b89376372ecfc22972cf9d68f5b2931cf912af44ec78e4a81014830450221009ec4b66593e1752ab1256b9fe5359127bd410a6872e87ac5b7c6ec2181fd633a022016b0bdc624e0a9399ff8afb251cb66b3811ee4044bd12319dc06b01bef80ff79014c695221031d60453ae3583d107ffe6f46d29a141938330703ec858c7dc869589f3538b15a2103684f9be5bc8078fe2bf13bfbf04ae409c8013d94b6705794dcb45c58171cbe76210254d83a9d3b59e869a95d8d9891de5c6933e4c23d8bc8e04709b854866a61152a53aeffffffff273e2fcf9191837e54be274872e6601727055102b1263e4692e18465eb37efd102000000fc0047304402201e0ff407e3a9f9a09adb1f0bf50733ce6b0adef38566cf0efc8da5aa4125e3230220350c92e71d63ddf1351feaf68ee4742d2b77135f94c37bb2e7a94976626e10670147304402202900d5fe89a02817413141b20b58e9f0626a9b73c2d6c709133f0f08c3aab5a102201b2ff5cac850e10a9fdc17373887edbc0569ca41997be52fe0e732d775d9906f014c6952210385e99327047037127d4af9c9925e63679e2f28e70fcbcdf73e27d9efd917edc521036230236deb29982e55ae33015be58e6d455f27f91c2480e71da9f74537f8f2572103d36bf416f077c4bb7779648b696fbe72dae5d4b450b0f4fb18ed818f5bb828f553aeffffffff03a08601000000000017a914ccc71e96ed178d480ed688431414e6fb3361ec1f8790761200000000001976a9144f4a48a403479d1b22117412961e319e814d383288ac9b946c100000000017a9140f606c810438483741114224ab25bd9e17e13c0387000000000200000001d8034d6bd16ae9024da766de5b5df9f32e4563543fda1fbd7bef4e3222cc0f07000000006b483045022100f888422b21ceb536ece5236a4ee6170c7cd3f52b77e1029872061232ad5afe5002200e1362ea0febc004ebb0f9981123599facc06a3e5a685680d3aa6afa73ef187e0121021d172f7db4f2ead3f9f652b7c4e7785d20fb0e7e57e5f8292dc587b8c5c479bbfeffffff0287e54402000000001976a9147d0091d8388b94090a7a259b9ca7281a2df7790988accdb57e00000000001976a914be742266f467c368a39b884515bfbee180341d0488ac236507000100000001ca8eadbe8705b960d18ae3eb5934298665033db7c50e0689937c2117795fd51e00000000fc004730440220752bef2fb8e65aeb4975f5a4a6e969bd72b680503df6e8e99d86a2236cde53a902203029fd5efeae766ceba07b23fc0b8cea5c6eb53c2a6273cc4cc17922f5f36e6a0147304402201c28cbd72d7d7788d3526af932031d6d49aaa9e6283a160015593307d6fe2938022017d4c9aec87d2b44d1644f8a5571aa85d6874729bfbf4fa81f7eac77ef8a4548014c69522102f9555d57012bb26a3e5880e71cf725facfd1bff855b710f834b1e9cf4e49fba7210332042300f63e04f2dc3aa8aff00df70253cbc95f902144227df1691f4441be212103896683c093e7e2ebbe4b50bdc8b9cb0c3c124803dfde0b91ed28e67ce44f5b9a53aeffffffff02400d0300000000001976a9146442fcf24af6374f448418c1fb8f72dad7bc53bf88ac05caff000000000017a9147f4663896598a92f32e3e64f6d75357100fe536a8700000000020000000117cd5a5b7d9a95d791a7eee0f0b1c141667e5ad7a5aa3deedd8c212f73cebda5010000006a4730440220425e40cacaccd6c2184380f870b28b7350965fb873d7e9e62fe05f00dd7ddddf022049487a7602c5ae5a5d908178bc8b90355194688b1c0d3e8d186b57b1c3659a77012103218e8230b9e1d5e73083bba4c890aead39bb8ea65166c0604b7ee1facbef6bc9feffffff02dbf20100000000001976a914e409126642490e652e70711e16e5c2de7983315288ace0b72900000000001976a9148e0e331f9031bce0eeac4ec4a32bb97a1c96915b88ac236507000200000001b3d1abac97951a76a27c848e029bcfe4aa557ddc8d2fd5bf5d1d033467257426010000006a47304402201d6b63dff103d2d6c54aa04967194c6c0b95d013926e46c3c26904881b1bfda0022039d7327d155eb49103e347b0447f718ee3500cd4dd5ab1e42c49bbf56e49d79f01210256e10b6a73be5045e74d34961b9992ed8e7a356d82132553eed8ae96aa9bb01afeffffff0240420f00000000001976a914fd683b0e96573b728fc56150cad65913824da19188ac45521c00000000001976a9146049980546873c46de6b5e41bf8401c0ad73d30688ac2365070001000000014fac515f802a2b6338779d6e1277fb9b4e7b3bf953796f4d1dd4d40397fcae7f01000000da0047304402207d98233657af8d765e962e6911edafdf46ff3dec87590c695551be7933520afb02203e2da910c2ec037491b33b96dc11bfa55f44cc6d9b1e69863ab1a3cfed85c6d701483045022100b2aac2bb800a59766a831b4e9def53eacd201c2502f37f2f15d2346208c71e57022025b145c3c16013b821c09d7bb2aeb19a0d29df21e76090513bfe395396f4bcbc01475221027d8561fdebe84da8d6f986b36c3f7e77df1dc5ff5c5cd78402e744ec570b635f21035837eb1d7f94331efdf224b879f8ec2a2ab953717b8b841003cbcd781a57a45e52aeffffffff0250f80c00000000001976a914492abf1ec3550bf943a15b5bfbeb247f80809ccf88ac0091fc000000000017a914a452b47ef4ffd79653bf2b9f0283f99e7ed64a5287000000000100000001f87087626e9b713d8f04cc3b3165ebde19b07b5b9fb3e20bac29530e749f5ab7010000006b4830450221008cc323ff48977f96a4191330b7401d72d6b4bf3ab26793204b38b5200227acd302203fc13099f76de39fc8588beef8562d0f5906cd95eb4ac9f9f9a036511ff0efe40121029b2a119af0ef5a960955371c74d34ebabbe76496f2dc0e056c4cb8483ca2a2c8ffffffff01800d35000000000017a9148178902be035e41899809c8fb216ea60ff8da7fe87000000000100000001f9a532e0a0a09faa458cd79597851dda240c819e676847e194835272dbad2fc35c0000006b483045022100b4b3993580300f9aa9e0bb031e5e6f40932b204d10a761a70d5a75cf55c6870902201ba8372c9c1f2c55d24f2593bff04f4b70ce4616a565ddda4af319cf2f4984b1012102e7d345a10772801cca875a4c1cfa21845c0c87e0638e6801bc7735796a4b694bffffffff02e80300000000000017a914b765a06993ad8a3cb4a018813d5d655dd3d08322874f320000000000001976a914a2d5eec06f8254649edce0593f632cf32edd826f88ac000000000100000001798bbf0b635b1e089b3cf9cc373478b71736e1fd5eb9f26f5974d6cb3a5b63b5010000006a473044022021652b16bd76f1e79c7f8e82604506a8bfc7e34cbf8671c96955a832a9946acf02203a9f996138104ff5deb206d797e2ae0efb8695a7c8846c717ffaaa566c12c20701210315f3c5e42964d5ae3d1b66e39be96bf9ac8877df262cbbaca8e7a287b502962bffffffff02afab6b01000000001976a9141547b355b743d06e23a119e9d6c6ead9ccab0ca188ac9a636f1b000000001976a914d9a62c731516a6fd72ae7959105e946b8e81343c88ac000000000100000002bb13820221fb162c3d9fb12c7849447ee392e0b76dee0101f81e9b4af6093434000000006b483045022100eabf52ccf75044249d81f203a0e936aec9c932dc17d0dae554ad53e2c435c0f4022066ca00a4eca82d301b7a966fbb538d1c27d33cef3e3493e1a93260da7ab355f60121028fe33f52c56d3550135456bc2fa185a58941579e13ab778c120d4ac284aa06cbffffffff107b55eb172d9d7d6a737e9b45be17741ce2b5ba5403359bc2f550f0f56c5fdb000000006a47304402200c2014f0150f2e50769d36503836e5a5450d44273695dbb423b61651a0230943022062f94c66f045c573b2846aeff49d6bc83e27c87e4e6af3c691b4fceaf9155111012102c61f7bab1ae9f25b24232034a71397d3e7bb3bc2343d5899c5020c03c0f138c6ffffffff02618b0000000000001976a91408221315628ab6abefb330995549b70433cd726088ac77450d00000000001976a9147ed8ed0b718df91913551b15f5c8eda04822d9bc88ac0000000001000000010f10d144f28918ad1dc5952bb4c59828c7f1674447cbe8234138be88af1a306f010000006b483045022100b4f142e3b6622df6b831ae1c578b1148da71658a8f6f15b5b8c45b2b295f649602206160f454d16fb54b9229ea54a6cb93108f87747914abcb04066f18a76c655bfa0121030367157d567c94aa358f593d485c2803c43014dac740644ed8145ac8a2467324ffffffff022aa40b00000000001976a914c9c735b25fc45f775a6b206687156c78d79a7ba488ac016e1d00000000001976a9140a6ba6719cdb027112d1f599db02059deeb7f5bd88ac000000000100000001161ca3147abbeaabbb48856c83110890709d659882729c30d304f7fd475ce255010000006b483045022100ab52bd52c1de135d963667193de07e1fde4572ce50be3401e79b5d7fea358bf402200c786472db4b866ef5c80f7ae2530d68bd3676b493ef02c4c9c4194ccb5d741a0121030605c22a5dbb8997b5c609364bf7394bb25f512dadbe24217768665e3345ac9bffffffff027c056100000000001976a914f61ae0754d691658bf2c41d032b281ae5b228a7d88acc07b9100000000001976a914929278ba2c0a24fbf1c560ed60b8e2e15afd435f88ac00000000010000000119ef2ccec5af94349ccd2934af1d57002d590513f85cbb76189f7920c68fdd29000000008a473044022054e1e8f124bad00ee56a0aeefdddab036a7386d76d2c8daa8b08e0d5eae00559022077fc276ccd8b3133891117b936785dd876ff398af728b9953bf4297612ff62e00141049a2b81b4283915fd518b8a7ea573f7842fec89d4602a127374358747f5c5b37d1e48f368193a3bfd61b7a92016a79e632c2dc1dcf509317b858073c8ffbed4aaffffffff02445d00000000000017a91496624919c67d6a0a678db52d7d421c388e160e168793e80400000000001976a914e5038fc9c68236d5df425a6ab317c836831f57df88ac00000000010000000112d625141fcac4d3f1d23346511790b5d7bc763ad0f98b95431b3958734a5048010000006a473044022024021fd182bcc745d8d39c1802e847c4f71ea62a7ac4f71c46c2d85fc7c4b996022079fa0f0025419950a624822a158859f50f174b8ad30546b1b00be68fa1265b470121033c18dfac7f0a9e082f9a1e24963de92bf96744644e93c6ab792fea193b98ce66ffffffff02ca090000000000001976a9149c8bce1226540cd2a6d233df62b34c9b7dfcfd7188ac23f8eb0c000000001976a914b270d2969994814bebd6a0486f899941c38bb13e88ac000000000100000001f60acaaff22c0795e80668158f40d227de292a4ac19444e916dee59496be6c85010000006a47304402200a949219b654a3d2e4a38ec0f1c963c96045026caaa44c03e4fb799f84989fc502205fc10db59b2da3b46911f9f628a67a0b4e926c8115cd00b7e9998d497543d5d90121028ad1a0235ca8ad9f029dac2c2d075a6d5ec6b8077c89dd929392199cf87a6fa1ffffffff02b9310001000000001976a914a2bb4dbdc9791e7895c86244ef36959830be6e7c88ac00c2eb0b000000001976a9144404677f4cb899635ba60880762d8f99b270a7d188ac00000000", + "cases": [ + { + "description": "segwit_witness_spending_tx", + "txid": "4ec907e083e0b9246a9d06192deba822457fab514baa6c5e7239ce0bc614f7bf", + "proof": { + "blockhash": "0000000000000000003f26ada5c8c1c32082a97414c38beb6bc0fd6444dbd65f", + "block_tx_count": 29, + "coinbase_tx": "010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff610325650741d66d9956bac3f841d66d99557a463b2f4254432e544f502f4e59412f4542312f4144362ffabe6d6d4a18b6e7c11bbc6ef6a16fb291914805ccf4babbc562509d61dc01f5ec9df79f8000000000000000083daee9bf01004000000000ffffffff021b348d4a000000001976a914ba507bae8f1643d2556000ca26b9301b9069dc6b88ac0000000000000000266a24aa21a9ed7e08dbed03e0265fa0a96c32c027184310cc30d160850839e4344fa695a35a1c0120000000000000000000000000000000000000000000000000000000000000000000000000", + "transactions": [ + { + "txid": "e599804f69e489edba07e6c6c2121bd82c48f963bba3b3b9ae177cbbe4d09342", + "wtxid": "a41ccb36b9c60687674028f7bc4e33bf35dd5d78ed87e8d9b69dc2857c72a9da", + "pos": 0, + "proof": { + "txid": [ + "fa0ca110ceee03d27cc000f5ef5166c031749a1adb5a06247bebf9ca70c61fc0", + "75e92f22d0836f554759bc727752e011acc71629e2a4195e2dfb848fe484b587", + "35c49fdd46c3af9a8947b0b663d933deb21b2bcfab61cc5d55c43f4ea668caf1", + "5e82e22ebef30bef9161441fddf019522a1fb209f86302a9f4a657ec033a790c", + "2464b4ce2e2af692a59afbb03c30bd415521341c793689b4ed5ab400870db91d" + ] + } + }, + { + "txid": "4ec907e083e0b9246a9d06192deba822457fab514baa6c5e7239ce0bc614f7bf", + "wtxid": "509f29cf2fe5e695152bfd61d743d63ccca885ac49f09990c04ba0fd8dcfc8fd", + "pos": 3, + "proof": { + "wtxid": [ + "fe7f7ee45bb56d1579fad8b9d05d13946f6647e9ca8342b227438479b8589309", + "5ee08c1c64d7ed382dcc42ec589adfc8e1368868bca65884e8ec82c760f8258f", + "35c49fdd46c3af9a8947b0b663d933deb21b2bcfab61cc5d55c43f4ea668caf1", + "5e82e22ebef30bef9161441fddf019522a1fb209f86302a9f4a657ec033a790c", + "2464b4ce2e2af692a59afbb03c30bd415521341c793689b4ed5ab400870db91d" + ] + } + } + ] + } + }, + { + "description": "segwit_coinbase", + "txid": "e599804f69e489edba07e6c6c2121bd82c48f963bba3b3b9ae177cbbe4d09342", + "proof": { + "blockhash": "0000000000000000003f26ada5c8c1c32082a97414c38beb6bc0fd6444dbd65f", + "block_tx_count": 29, + "coinbase_tx": "010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff610325650741d66d9956bac3f841d66d99557a463b2f4254432e544f502f4e59412f4542312f4144362ffabe6d6d4a18b6e7c11bbc6ef6a16fb291914805ccf4babbc562509d61dc01f5ec9df79f8000000000000000083daee9bf01004000000000ffffffff021b348d4a000000001976a914ba507bae8f1643d2556000ca26b9301b9069dc6b88ac0000000000000000266a24aa21a9ed7e08dbed03e0265fa0a96c32c027184310cc30d160850839e4344fa695a35a1c0120000000000000000000000000000000000000000000000000000000000000000000000000", + "transactions": [ + { + "txid": "e599804f69e489edba07e6c6c2121bd82c48f963bba3b3b9ae177cbbe4d09342", + "wtxid": "a41ccb36b9c60687674028f7bc4e33bf35dd5d78ed87e8d9b69dc2857c72a9da", + "pos": 0, + "proof": { + "txid": [ + "fa0ca110ceee03d27cc000f5ef5166c031749a1adb5a06247bebf9ca70c61fc0", + "75e92f22d0836f554759bc727752e011acc71629e2a4195e2dfb848fe484b587", + "35c49fdd46c3af9a8947b0b663d933deb21b2bcfab61cc5d55c43f4ea668caf1", + "5e82e22ebef30bef9161441fddf019522a1fb209f86302a9f4a657ec033a790c", + "2464b4ce2e2af692a59afbb03c30bd415521341c793689b4ed5ab400870db91d" + ] + } + } + ] + } + } + ] + }, + { + "description": "mainnet block 489125", + "height": 489125, + "block_hex": "0000002042ffee3f675f70d56d92d9d2f7ef23779fc24ca2a8e45000000000000000000075a22252c11bf574122044b1a808bc8979e7dd9eb59ebb36a7f48702c4ec50fe9813dc5973fa00189e8ab38d0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff3e03a576070559dc139801fabe6d6d226fb3d7315fac0ffc7133fd8e83ac80c0716687186ac022d9aa44b0b34299010800000000000000210000007a040000ffffffff01807c814a000000001976a914cebd1b27e747a51df13b36fc9f7c5eac3169c5bc88ac00000000", + "cases": [ + { + "description": "empty_post_segwit_no_commitment", + "txid": "fe50ecc40287f4a736bb9eb59edde77989bc08a8b144201274f51bc15222a275", + "proof": { + "blockhash": "000000000000000000f54159c9550bec373b27c1df71a2c573e16cc799d127dd", + "block_tx_count": 1, + "coinbase_tx": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff3e03a576070559dc139801fabe6d6d226fb3d7315fac0ffc7133fd8e83ac80c0716687186ac022d9aa44b0b34299010800000000000000210000007a040000ffffffff01807c814a000000001976a914cebd1b27e747a51df13b36fc9f7c5eac3169c5bc88ac00000000", + "transactions": [ + { + "txid": "fe50ecc40287f4a736bb9eb59edde77989bc08a8b144201274f51bc15222a275", + "wtxid": "fe50ecc40287f4a736bb9eb59edde77989bc08a8b144201274f51bc15222a275", + "pos": 0, + "proof": { + "txid": [] + } + } + ] + } + } + ] + } +] diff --git a/src/test/fuzz/merkle.cpp b/src/test/fuzz/merkle.cpp index 0de1c0279554..f2ec5a401217 100644 --- a/src/test/fuzz/merkle.cpp +++ b/src/test/fuzz/merkle.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include #include #include diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index e3d3e540dcb5..03094a4ba811 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -81,6 +81,7 @@ const std::vector RPC_COMMANDS_NOT_SAFE_FOR_FUZZING{ "exportasmap", // avoid writing to disk "generatetoaddress", // avoid prohibitively slow execution (when `num_blocks` is large) "generatetodescriptor", // avoid prohibitively slow execution (when `nblocks` is large) + "gettxproof", // avoid reading blocks from disk "gettxoutproof", // avoid prohibitively slow execution "importmempool", // avoid reading from disk "loadtxoutset", // avoid reading from disk @@ -187,6 +188,7 @@ const std::vector RPC_COMMANDS_SAFE_FOR_FUZZING{ "validateaddress", "verifychain", "verifymessage", + "verifytxproof", "verifytxoutproof", "waitforblock", "waitforblockheight", diff --git a/src/test/merkle_tests.cpp b/src/test/merkle_tests.cpp index a6dd23adb17e..70624216708e 100644 --- a/src/test/merkle_tests.cpp +++ b/src/test/merkle_tests.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include #include #include @@ -162,6 +163,60 @@ BOOST_AUTO_TEST_CASE(merkle_test_empty_block) BOOST_CHECK(merkle_path.empty()); } +BOOST_AUTO_TEST_CASE(merkle_branch_shape_test) +{ + CBlock block; + block.vtx.resize(5); + for (std::size_t pos = 0; pos < block.vtx.size(); pos++) { + CMutableTransaction mtx; + mtx.nLockTime = pos; + block.vtx[pos] = MakeTransactionRef(std::move(mtx)); + } + + const uint32_t pos{4}; + const auto path{TransactionMerklePath(block, pos)}; + const auto root{::ComputeMerkleRootFromBranch(block.vtx[pos]->GetHash().ToUint256(), path, pos, block.vtx.size())}; + BOOST_REQUIRE(root); + BOOST_CHECK_EQUAL(*root, BlockMerkleRoot(block)); + + // The last leaf in a five transaction tree is duplicated at the bottom + // level. A verifier that knows the tree shape can reject a different + // sibling there. + auto invalid_path{path}; + invalid_path[0] = uint256::ONE; + BOOST_CHECK(!::ComputeMerkleRootFromBranch(block.vtx[pos]->GetHash().ToUint256(), invalid_path, pos, block.vtx.size())); + + BOOST_CHECK(!::ComputeMerkleRootFromBranch(block.vtx[pos]->GetHash().ToUint256(), path, pos, block.vtx.size() - 1)); + BOOST_CHECK(!::ComputeMerkleRootFromBranch(block.vtx[pos]->GetHash().ToUint256(), path, block.vtx.size(), block.vtx.size())); + + invalid_path = path; + invalid_path.push_back(uint256::ONE); + BOOST_CHECK(!::ComputeMerkleRootFromBranch(block.vtx[pos]->GetHash().ToUint256(), invalid_path, pos, block.vtx.size())); +} + +BOOST_AUTO_TEST_CASE(witness_merkle_path_test) +{ + CBlock block; + block.vtx.resize(3); + for (std::size_t pos = 0; pos < block.vtx.size(); pos++) { + CMutableTransaction mtx; + mtx.nLockTime = pos; + if (pos > 0) { + mtx.vin.resize(1); + mtx.vin[0].scriptWitness.stack.emplace_back(std::vector{uint8_t(pos)}); + } + block.vtx[pos] = MakeTransactionRef(std::move(mtx)); + } + + for (uint32_t pos = 0; pos < block.vtx.size(); ++pos) { + const uint256 leaf{pos == 0 ? uint256{} : block.vtx[pos]->GetWitnessHash().ToUint256()}; + const auto path{WitnessMerklePath(block, pos)}; + const auto root{::ComputeMerkleRootFromBranch(leaf, path, pos, block.vtx.size())}; + BOOST_REQUIRE(root); + BOOST_CHECK_EQUAL(*root, BlockWitnessMerkleRoot(block)); + } +} + BOOST_AUTO_TEST_CASE(merkle_test_oneTx_block) { bool mutated = false; diff --git a/src/test/txproof_tests.cpp b/src/test/txproof_tests.cpp new file mode 100644 index 000000000000..2b484961799e --- /dev/null +++ b/src/test/txproof_tests.cpp @@ -0,0 +1,197 @@ +// Copyright (c) 2026-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://opensource.org/license/mit. + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace { + +uint256 HashFromHex(const std::string& hex) +{ + const auto hash{uint256::FromHex(hex)}; + BOOST_REQUIRE(hash); + return *hash; +} + +std::vector BranchFromJson(const UniValue& proof, const std::string& key) +{ + std::vector branch; + const UniValue& hashes{proof.find_value(key)}; + BOOST_REQUIRE(hashes.isArray()); + branch.reserve(hashes.size()); + for (unsigned int i{0}; i < hashes.size(); ++i) { + branch.push_back(HashFromHex(hashes[i].get_str())); + } + return branch; +} + +std::optional> OptionalBranchFromJson(const UniValue& proof, const std::string& key) +{ + const UniValue& hashes{proof.find_value(key)}; + if (hashes.isNull()) return std::nullopt; + return BranchFromJson(proof, key); +} + +node::TxProofTransaction TransactionFromJson(const UniValue& transaction) +{ + node::TxProofTransaction result; + result.txid = Txid::FromUint256(HashFromHex(transaction["txid"].get_str())); + result.wtxid = Wtxid::FromUint256(HashFromHex(transaction["wtxid"].get_str())); + result.pos = transaction["pos"].getInt(); + + const UniValue& proof{transaction["proof"]}; + BOOST_REQUIRE(proof.isObject()); + result.txid_branch = OptionalBranchFromJson(proof, "txid"); + result.wtxid_branch = OptionalBranchFromJson(proof, "wtxid"); + return result; +} + +node::TxProof ProofFromJson(const UniValue& proof) +{ + node::TxProof result; + result.block_hash = HashFromHex(proof["blockhash"].get_str()); + result.block_tx_count = proof["block_tx_count"].getInt(); + + CMutableTransaction coinbase_mut; + BOOST_REQUIRE(DecodeHexTx(coinbase_mut, proof["coinbase_tx"].get_str())); + result.coinbase = MakeTransactionRef(std::move(coinbase_mut)); + + const UniValue& transactions{proof["transactions"]}; + BOOST_REQUIRE(transactions.isArray()); + result.transactions.reserve(transactions.size()); + for (const UniValue& transaction : transactions.getValues()) { + result.transactions.push_back(TransactionFromJson(transaction)); + } + return result; +} + +UniValue BranchToJson(const std::vector& branch) +{ + UniValue result{UniValue::VARR}; + for (const auto& hash : branch) { + result.push_back(hash.GetHex()); + } + return result; +} + +UniValue TransactionToJson(const node::TxProofTransaction& transaction) +{ + UniValue proof{UniValue::VOBJ}; + if (transaction.txid_branch) { + proof.pushKV("txid", BranchToJson(*transaction.txid_branch)); + } + if (transaction.wtxid_branch) { + proof.pushKV("wtxid", BranchToJson(*transaction.wtxid_branch)); + } + + UniValue result{UniValue::VOBJ}; + result.pushKV("txid", transaction.txid.GetHex()); + result.pushKV("wtxid", transaction.wtxid.GetHex()); + result.pushKV("pos", transaction.pos); + result.pushKV("proof", std::move(proof)); + return result; +} + +UniValue ProofToJson(const node::TxProof& proof) +{ + UniValue transactions{UniValue::VARR}; + for (const auto& transaction : proof.transactions) { + transactions.push_back(TransactionToJson(transaction)); + } + + UniValue result{UniValue::VOBJ}; + result.pushKV("blockhash", proof.block_hash.GetHex()); + result.pushKV("block_tx_count", proof.block_tx_count); + result.pushKV("coinbase_tx", EncodeHexTx(*proof.coinbase)); + result.pushKV("transactions", std::move(transactions)); + return result; +} + +void CheckInvalid(const node::TxProof& proof, const CBlock& block) +{ + BOOST_CHECK(!node::VerifyTxProof(proof, static_cast(block), static_cast(block.vtx.size()))); +} + +} // namespace + +BOOST_AUTO_TEST_SUITE(txproof_tests) + +BOOST_AUTO_TEST_CASE(mainnet_vectors) +{ + const UniValue blocks{read_json(json_tests::txproof)}; + for (const UniValue& block_vector : blocks.getValues()) { + CBlock block; + BOOST_REQUIRE(DecodeHexBlk(block, block_vector["block_hex"].get_str())); + + for (const UniValue& case_vector : block_vector["cases"].getValues()) { + const UniValue& proof_json{case_vector["proof"]}; + const UniValue& transactions{proof_json["transactions"]}; + BOOST_REQUIRE(transactions.isArray()); + BOOST_REQUIRE(!transactions.empty()); + const Wtxid wtxid{Wtxid::FromUint256(HashFromHex(transactions[transactions.size() - 1]["wtxid"].get_str()))}; + + const auto generated_proof{node::MakeTxProof(block, wtxid)}; + BOOST_REQUIRE(generated_proof); + BOOST_CHECK_EQUAL(ProofToJson(*generated_proof).write(), proof_json.write()); + + const node::TxProof proof{ProofFromJson(proof_json)}; + const auto verification{node::VerifyTxProof(proof, static_cast(block), static_cast(block.vtx.size()))}; + BOOST_REQUIRE(verification); + BOOST_CHECK_EQUAL(verification->block_hash.GetHex(), proof.block_hash.GetHex()); + BOOST_CHECK_EQUAL(verification->block_tx_count, proof.block_tx_count); + BOOST_CHECK_EQUAL(verification->transactions.size(), proof.transactions.size()); + for (const auto& transaction : verification->transactions) { + BOOST_CHECK(transaction.txid || transaction.wtxid); + } + + auto invalid{proof}; + invalid.block_tx_count += 1; + CheckInvalid(invalid, block); + + invalid = proof; + invalid.transactions.front().pos = proof.block_tx_count; + CheckInvalid(invalid, block); + + if (proof.transactions.front().txid_branch && !proof.transactions.front().txid_branch->empty()) { + invalid = proof; + (*invalid.transactions.front().txid_branch)[0] = uint256::ONE; + CheckInvalid(invalid, block); + } + + if (proof.transactions.size() > 1) { + const auto& target{proof.transactions.back()}; + if (target.txid_branch && !target.txid_branch->empty()) { + invalid = proof; + (*invalid.transactions.back().txid_branch)[0] = uint256::ONE; + CheckInvalid(invalid, block); + } + if (target.wtxid_branch && !target.wtxid_branch->empty()) { + invalid = proof; + (*invalid.transactions.back().wtxid_branch)[0] = uint256::ONE; + CheckInvalid(invalid, block); + } + } else { + invalid = proof; + invalid.transactions.front().wtxid_branch = std::vector{}; + CheckInvalid(invalid, block); + } + } + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/functional/data/rpc_txproof.json b/test/functional/data/rpc_txproof.json new file mode 100644 index 000000000000..5ac6217fe3d1 --- /dev/null +++ b/test/functional/data/rpc_txproof.json @@ -0,0 +1,48 @@ +{ + "no_witness_commitment": { + "blockhash": "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206", + "block_tx_count": 1, + "coinbase_tx": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000", + "transactions": [ + { + "txid": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "wtxid": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "pos": 0, + "proof": { + "txid": [] + } + } + ] + }, + "witness_commitment": { + "blockhash": "74592269268bd87f7f66d33f38184de2fe369954e027ea2aface5b142f604442", + "block_tx_count": 5, + "coinbase_tx": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0302c900feffffff0280e00495000000001976a9142b4569203694fc997e13f2c0a1383b9e16c77a0d88ac0000000000000000266a24aa21a9edb66d6a85405f8ccaf49c941bb60745fa044bfa1041838cc7d57cbb51f13984ff01200000000000000000000000000000000000000000000000000000000000000000c8000000", + "transactions": [ + { + "txid": "bf12d96a84ee6a423b4688e13c5a81dddb4b89d2f0b8d9094d8ab6400d7b25b7", + "wtxid": "715dc5940e7fb9afddd625dcb4aad838afa1daceabf0cd34652c7022c84dd16a", + "pos": 0, + "proof": { + "txid": [ + "d69ed2b46f08ef108444eaaf3ddc7ae86a86d4fec5bfc3b6a24d036e7acabe54", + "3d1527ed52d2961408f310adf5f74e61203a1627becf81e14a9d007ee354dfc1", + "ac3f1c6d3198024de7311e4207cb142d0eef44fb0d1715f4788fdee36a1cd9b5" + ] + } + }, + { + "txid": "d23e7caa4ecf2a77fd20cb987877438562c93d9f7a110405c2697433fac3b0d8", + "wtxid": "a5cd77266b4f3e1d317165a9cb6fb07973c41155df5356611e92c9b0c767af00", + "pos": 4, + "proof": { + "wtxid": [ + "a5cd77266b4f3e1d317165a9cb6fb07973c41155df5356611e92c9b0c767af00", + "983dfad563484da08d96815f3bc66f27f8b22bd0fbdd21660649819dffb7ee4c", + "31d3f3120fca95c8fa5eb6530bf8677afad871556a21628dd967fe081afe575d" + ] + } + } + ] + } +} diff --git a/test/functional/rpc_txproof.py b/test/functional/rpc_txproof.py new file mode 100755 index 000000000000..cae430d89310 --- /dev/null +++ b/test/functional/rpc_txproof.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026-present The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit. +"""Test gettxproof and verifytxproof RPCs.""" + +from copy import deepcopy +import json +from pathlib import Path + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) +from test_framework.wallet import MiniWallet + + +ZERO_HASH = "00" * 32 +ONE_HASH = "01" + "00" * 31 + + +class TxProofTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + + def _verify(self, proof): + return self.nodes[0].verifytxproof(proof) + + def _invalid(self, proof): + assert_equal(self._verify(proof), {}) + + def _mutate(self, proof, mutator): + proof = deepcopy(proof) + mutator(proof) + return proof + + def _check_no_commitment_proof(self, proof): + coinbase = proof["transactions"][0] + assert_equal(proof["block_tx_count"], 1) + assert_equal(len(proof["transactions"]), 1) + assert_equal(coinbase["pos"], 0) + assert "wtxid" not in coinbase["proof"] + assert_equal(coinbase["proof"]["txid"], []) + + assert_equal(self._verify(proof), { + "blockhash": proof["blockhash"], + "block_tx_count": proof["block_tx_count"], + "transactions": [{ + "pos": coinbase["pos"], + "txid": coinbase["txid"], + "wtxid": coinbase["wtxid"], + }], + }) + + self._invalid(self._mutate(proof, lambda p: p.__setitem__("block_tx_count", 2))) + self._invalid(self._mutate(proof, lambda p: p["transactions"][0].__setitem__("pos", 1))) + self._invalid(self._mutate(proof, lambda p: p["transactions"][0].__setitem__("wtxid", ZERO_HASH))) + self._invalid(self._mutate(proof, lambda p: p["transactions"][0]["proof"].__setitem__("wtxid", []))) + + def _check_witness_commitment_proof(self, proof, *, expected_target_positions): + coinbase = proof["transactions"][0] + targets = proof["transactions"][1:] + assert_equal(proof["block_tx_count"], 5) + assert_equal(coinbase["pos"], 0) + assert "txid" in coinbase["proof"] + assert "wtxid" not in coinbase["proof"] + assert_equal([target["pos"] for target in targets], expected_target_positions) + for target in targets: + assert "wtxid" in target["proof"] + assert "txid" not in target["proof"] + + assert_equal(self._verify(proof), { + "blockhash": proof["blockhash"], + "block_tx_count": proof["block_tx_count"], + "transactions": [{ + "pos": coinbase["pos"], + "txid": coinbase["txid"], + }] + [{ + "pos": target["pos"], + "wtxid": target["wtxid"], + } for target in targets], + }) + + self._invalid(self._mutate(proof, lambda p: p.__setitem__("block_tx_count", 4))) + self._invalid(self._mutate(proof, lambda p: p["transactions"][-1].__setitem__("pos", 2))) + self._invalid(self._mutate(proof, lambda p: p["transactions"][0]["proof"]["txid"].__setitem__(0, ONE_HASH))) + self._invalid(self._mutate(proof, lambda p: p["transactions"][-1]["proof"]["wtxid"].__setitem__(0, ZERO_HASH))) + self._invalid(self._mutate(proof, lambda p: p["transactions"][-1]["proof"].pop("wtxid"))) + self._invalid(self._mutate(proof, lambda p: p["transactions"][-1]["proof"].__setitem__("txid", []))) + + assert_raises_rpc_error( + -8, + "transactions[0].proof.txid must be an array", + self._verify, + self._mutate(proof, lambda p: p["transactions"][0]["proof"].__setitem__("txid", "not an array")), + ) + + def run_test(self): + node = self.nodes[0] + miniwallet = MiniWallet(node) + vectors_path = Path(__file__).with_name("data") / "rpc_txproof.json" + genesis_hash = node.getblockhash(0) + + node.setmocktime(node.getblockheader(genesis_hash)["time"]) + self.generatetodescriptor(node, 200, miniwallet.get_descriptor()) + miniwallet.rescan_utxos() + + self.log.info("Check a no-witness-commitment proof against the JSON vector") + genesis_wtxid = node.getblock(genesis_hash)["tx"][0] + no_commitment_proof = node.gettxproof([genesis_wtxid], genesis_hash) + self._check_no_commitment_proof(no_commitment_proof) + + self.log.info("Check a witness-commitment proof against the JSON vector") + tx_chain = miniwallet.send_self_transfer_chain(from_node=node, chain_length=4) + assert_raises_rpc_error(-5, "Not all transactions found in specified block", node.gettxproof, [tx_chain[-1]["wtxid"]], genesis_hash) + blockhash = self.generate(node, 1)[0] + witness_proof = node.gettxproof([tx_chain[-1]["wtxid"]], blockhash) + self._check_witness_commitment_proof(witness_proof, expected_target_positions=[4]) + + self.log.info("Check a coinbase-only proof") + coinbase_wtxid = witness_proof["transactions"][0]["wtxid"] + coinbase_only_proof = { + **witness_proof, + "transactions": [witness_proof["transactions"][0]], + } + assert_equal(node.gettxproof([], blockhash), coinbase_only_proof) + assert_equal(node.gettxproof([coinbase_wtxid], blockhash), coinbase_only_proof) + + with vectors_path.open(encoding="utf8") as f: + vectors = json.load(f) + assert_equal(vectors, { + "no_witness_commitment": no_commitment_proof, + "witness_commitment": witness_proof, + }) + + self.log.info("Check invalid lookup arguments") + assert_raises_rpc_error(-8, "blockhash must be of length 64", node.gettxproof, [tx_chain[-1]["wtxid"]], "00000000") + assert_raises_rpc_error(-8, "wtxids[0] must be of length 64", node.gettxproof, ["00000000"], blockhash) + assert_raises_rpc_error(-8, "Invalid parameter, duplicated wtxid", node.gettxproof, [tx_chain[-1]["wtxid"], tx_chain[-1]["wtxid"]], blockhash) + assert_raises_rpc_error(-8, "Parameter 'wtxids' accepts at most one element", node.gettxproof, [tx_chain[-2]["wtxid"], tx_chain[-1]["wtxid"]], blockhash) + assert_raises_rpc_error(-5, "Block not found", node.gettxproof, [tx_chain[-1]["wtxid"]], ZERO_HASH) + + self.log.info("Check invalid multi-transaction proof") + self._invalid(self._mutate(witness_proof, lambda p: p["transactions"].append(deepcopy(p["transactions"][-1])))) + + self.log.info("Check active-chain verification") + node.invalidateblock(blockhash) + assert_raises_rpc_error(-5, "Block not found in active chain", node.verifytxproof, witness_proof) + assert_equal(node.verifytxproof(witness_proof, require_active_chain=False), { + "blockhash": witness_proof["blockhash"], + "block_tx_count": witness_proof["block_tx_count"], + "transactions": [{ + "pos": witness_proof["transactions"][0]["pos"], + "txid": witness_proof["transactions"][0]["txid"], + }, { + "pos": witness_proof["transactions"][1]["pos"], + "wtxid": witness_proof["transactions"][1]["wtxid"], + }], + }) + + +if __name__ == "__main__": + TxProofTest(__file__).main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 5bfc7d86239b..7ba3e0ca00f1 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -159,6 +159,7 @@ 'rpc_signer.py', 'wallet_signer.py', 'mempool_limit.py', + 'rpc_txproof.py', 'rpc_txoutproof.py', 'rpc_orphans.py', 'wallet_listreceivedby.py',