diff --git a/src/merkleblock.h b/src/merkleblock.h --- a/src/merkleblock.h +++ b/src/merkleblock.h @@ -136,6 +136,12 @@ */ uint256 ExtractMatches(std::vector &vMatch, std::vector &vnIndex); + + /** + * Get number of transactions the merkle proof is indicating for + * cross-reference with local blockchain knowledge. + */ + uint32_t GetNumTransactions() const { return nTransactions; }; }; /** diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -371,7 +371,7 @@ "generated by gettxoutproof\n" "\nResult:\n" "[\"txid\"] (array, strings) The txid(s) which the proof " - "commits to, or empty array if the proof is invalid\n"); + "commits to, or empty array if the proof can not be validated.\n"); } CDataStream ssMB(ParseHexV(request.params[0], "proof"), SER_NETWORK, @@ -391,13 +391,16 @@ LOCK(cs_main); const CBlockIndex *pindex = LookupBlockIndex(merkleBlock.header.GetHash()); - if (!pindex || !chainActive.Contains(pindex)) { + if (!pindex || !chainActive.Contains(pindex) || pindex->nTx == 0) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found in chain"); } - for (const uint256 &hash : vMatch) { - res.push_back(hash.GetHex()); + // Check if proof is valid, only add results if so + if (pindex->nTx == merkleBlock.txn.GetNumTransactions()) { + for (const uint256 &hash : vMatch) { + res.push_back(hash.GetHex()); + } } return res; diff --git a/test/functional/rpc_txoutproof.py b/test/functional/rpc_txoutproof.py --- a/test/functional/rpc_txoutproof.py +++ b/test/functional/rpc_txoutproof.py @@ -13,6 +13,7 @@ assert_raises_rpc_error, connect_nodes, ) +from test_framework.messages import CMerkleBlock, FromHex, ToHex class MerkleBlockTest(BitcoinTestFramework): @@ -104,6 +105,28 @@ assert_raises_rpc_error(-5, "Not all transactions found in specified or retrieved block", self.nodes[2].gettxoutproof, [txid1, txid3]) + # Now we'll try tweaking a proof. + proof = self.nodes[3].gettxoutproof([txid1, txid2]) + assert txid1 in self.nodes[0].verifytxoutproof(proof) + assert txid2 in self.nodes[1].verifytxoutproof(proof) + + tweaked_proof = FromHex(CMerkleBlock(), proof) + + # Make sure that our serialization/deserialization is working + assert txid1 in self.nodes[2].verifytxoutproof(ToHex(tweaked_proof)) + + # Check to see if we can go up the merkle tree and pass this off as a + # single-transaction block + tweaked_proof.txn.nTransactions = 1 + tweaked_proof.txn.vHash = [tweaked_proof.header.hashMerkleRoot] + tweaked_proof.txn.vBits = [True] + [False]*7 + + for n in self.nodes: + assert not n.verifytxoutproof(ToHex(tweaked_proof)) + + # TODO: try more variants, eg transactions at different depths, and + # verify that the proofs are invalid + if __name__ == '__main__': MerkleBlockTest().main() diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -731,6 +731,54 @@ self.blockhash, repr(self.transactions)) +class CPartialMerkleTree(): + def __init__(self): + self.nTransactions = 0 + self.vHash = [] + self.vBits = [] + self.fBad = False + + def deserialize(self, f): + self.nTransactions = struct.unpack("