Skip to content

Commit d280617

Browse files
sdaftuarinstagibbs
authored andcommitted
[qa] Add a test for merkle proof malleation
1 parent ed82f17 commit d280617

File tree

2 files changed

+69
-0
lines changed

2 files changed

+69
-0
lines changed

test/functional/rpc_txoutproof.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from test_framework.test_framework import BitcoinTestFramework
88
from test_framework.util import *
9+
from test_framework.mininode import FromHex, ToHex
10+
from test_framework.messages import CMerkleBlock
911

1012
class MerkleBlockTest(BitcoinTestFramework):
1113
def set_test_params(self):
@@ -78,6 +80,27 @@ def run_test(self):
7880
# We can't get a proof if we specify transactions from different blocks
7981
assert_raises_rpc_error(-5, "Not all transactions found in specified or retrieved block", self.nodes[2].gettxoutproof, [txid1, txid3])
8082

83+
# Now we'll try tweaking a proof.
84+
proof = self.nodes[3].gettxoutproof([txid1, txid2])
85+
assert txid1 in self.nodes[0].verifytxoutproof(proof)
86+
assert txid2 in self.nodes[1].verifytxoutproof(proof)
87+
88+
tweaked_proof = FromHex(CMerkleBlock(), proof)
89+
90+
# Make sure that our serialization/deserialization is working
91+
assert txid1 in self.nodes[2].verifytxoutproof(ToHex(tweaked_proof))
92+
93+
# Check to see if we can go up the merkle tree and pass this off as a
94+
# single-transaction block
95+
tweaked_proof.txn.nTransactions = 1
96+
tweaked_proof.txn.vHash = [tweaked_proof.header.hashMerkleRoot]
97+
tweaked_proof.txn.vBits = [True] + [False]*7
98+
99+
for n in self.nodes:
100+
assert not n.verifytxoutproof(ToHex(tweaked_proof))
101+
102+
# TODO: try more variants, eg transactions at different depths, and
103+
# verify that the proofs are invalid
81104

82105
if __name__ == '__main__':
83106
MerkleBlockTest().main()

test/functional/test_framework/messages.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,52 @@ def serialize(self, with_witness=True):
841841
def __repr__(self):
842842
return "BlockTransactions(hash=%064x transactions=%s)" % (self.blockhash, repr(self.transactions))
843843

844+
class CPartialMerkleTree():
845+
def __init__(self):
846+
self.nTransactions = 0
847+
self.vHash = []
848+
self.vBits = []
849+
self.fBad = False
850+
851+
def deserialize(self, f):
852+
self.nTransactions = struct.unpack("<i", f.read(4))[0]
853+
self.vHash = deser_uint256_vector(f)
854+
vBytes = deser_string(f)
855+
self.vBits = []
856+
for i in range(len(vBytes) * 8):
857+
self.vBits.append(vBytes[i//8] & (1 << (i % 8)) != 0)
858+
859+
def serialize(self):
860+
r = b""
861+
r += struct.pack("<i", self.nTransactions)
862+
r += ser_uint256_vector(self.vHash)
863+
vBytesArray = bytearray([0x00] * ((len(self.vBits) + 7)//8))
864+
for i in range(len(self.vBits)):
865+
vBytesArray[i // 8] |= self.vBits[i] << (i % 8)
866+
r += ser_string(bytes(vBytesArray))
867+
return r
868+
869+
def __repr__(self):
870+
return "CPartialMerkleTree(nTransactions=%d, vHash=%s, vBits=%s)" % (self.nTransactions, repr(self.vHash), repr(self.vBits))
871+
872+
class CMerkleBlock():
873+
def __init__(self):
874+
self.header = CBlockHeader()
875+
self.txn = CPartialMerkleTree()
876+
877+
def deserialize(self, f):
878+
self.header.deserialize(f)
879+
self.txn.deserialize(f)
880+
881+
def serialize(self):
882+
r = b""
883+
r += self.header.serialize()
884+
r += self.txn.serialize()
885+
return r
886+
887+
def __repr__(self):
888+
return "CMerkleBlock(header=%s, txn=%s)" % (repr(self.header), repr(self.txn))
889+
844890

845891
# Objects that correspond to messages on the wire
846892
class msg_version():

0 commit comments

Comments
 (0)