From afaaba69eddd50bc22b94ca7c0f9195773aaa111 Mon Sep 17 00:00:00 2001 From: stratospher <44024636+stratospher@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:32:58 +0530 Subject: [PATCH] test: refactor out same-txid-diff-wtxid tx to reuse in other tests useful to easily create transactions with same txid, different wtxid and valid witness for testing scenarios in other places (ex: private broadcast connections) --- test/functional/mempool_accept_wtxid.py | 47 +++------------- test/functional/test_framework/script_util.py | 54 ++++++++++++++++++- 2 files changed, 59 insertions(+), 42 deletions(-) diff --git a/test/functional/mempool_accept_wtxid.py b/test/functional/mempool_accept_wtxid.py index 35983d8eb48..fdb88cd4a59 100755 --- a/test/functional/mempool_accept_wtxid.py +++ b/test/functional/mempool_accept_wtxid.py @@ -7,34 +7,18 @@ Test mempool acceptance in case of an already known transaction with identical non-witness data but different witness. """ -from copy import deepcopy from test_framework.messages import ( COIN, - COutPoint, - CTransaction, - CTxIn, - CTxInWitness, - CTxOut, - sha256, ) from test_framework.p2p import P2PTxInvStore -from test_framework.script import ( - CScript, - OP_0, - OP_ELSE, - OP_ENDIF, - OP_EQUAL, - OP_HASH160, - OP_IF, - OP_TRUE, - hash160, -) +from test_framework.script_util import ValidWitnessMalleatedTx from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_not_equal, assert_equal, ) + class MempoolWtxidTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 @@ -50,38 +34,19 @@ class MempoolWtxidTest(BitcoinTestFramework): assert_equal(node.getmempoolinfo()['size'], 0) self.log.info("Submit parent with multiple script branches to mempool") - hashlock = hash160(b'Preimage') - witness_script = CScript([OP_IF, OP_HASH160, hashlock, OP_EQUAL, OP_ELSE, OP_TRUE, OP_ENDIF]) - witness_program = sha256(witness_script) - script_pubkey = CScript([OP_0, witness_program]) - - parent = CTransaction() - parent.vin.append(CTxIn(COutPoint(int(txid, 16), 0), b"")) - parent.vout.append(CTxOut(int(9.99998 * COIN), script_pubkey)) + txgen = ValidWitnessMalleatedTx() + parent = txgen.build_parent_tx(txid, 9.99998 * COIN) privkeys = [node.get_deterministic_priv_key().key] raw_parent = node.signrawtransactionwithkey(hexstring=parent.serialize().hex(), privkeys=privkeys)['hex'] - parent_txid = node.sendrawtransaction(hexstring=raw_parent, maxfeerate=0) + signed_parent_txid = node.sendrawtransaction(hexstring=raw_parent, maxfeerate=0) self.generate(node, 1) peer_wtxid_relay = node.add_p2p_connection(P2PTxInvStore()) - # Create a new transaction with witness solving first branch - child_witness_script = CScript([OP_TRUE]) - child_witness_program = sha256(child_witness_script) - child_script_pubkey = CScript([OP_0, child_witness_program]) - - child_one = CTransaction() - child_one.vin.append(CTxIn(COutPoint(int(parent_txid, 16), 0), b"")) - child_one.vout.append(CTxOut(int(9.99996 * COIN), child_script_pubkey)) - child_one.wit.vtxinwit.append(CTxInWitness()) - child_one.wit.vtxinwit[0].scriptWitness.stack = [b'Preimage', b'\x01', witness_script] + child_one, child_two = txgen.build_malleated_children(signed_parent_txid, 9.99996 * COIN) child_one_wtxid = child_one.wtxid_hex child_one_txid = child_one.txid_hex - - # Create another identical transaction with witness solving second branch - child_two = deepcopy(child_one) - child_two.wit.vtxinwit[0].scriptWitness.stack = [b'', witness_script] child_two_wtxid = child_two.wtxid_hex child_two_txid = child_two.txid_hex diff --git a/test/functional/test_framework/script_util.py b/test/functional/test_framework/script_util.py index 938183ece4c..3f8e1e61906 100755 --- a/test/functional/test_framework/script_util.py +++ b/test/functional/test_framework/script_util.py @@ -5,6 +5,16 @@ """Useful Script constants and utils.""" import unittest +from copy import deepcopy + +from test_framework.messages import ( + COutPoint, + CTransaction, + CTxIn, + CTxInWitness, + CTxOut, + sha256, +) from test_framework.script import ( CScript, OP_0, @@ -14,12 +24,15 @@ from test_framework.script import ( OP_CHECKMULTISIG, OP_CHECKSIG, OP_DUP, + OP_ELSE, + OP_ENDIF, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, + OP_IF, OP_RETURN, + OP_TRUE, hash160, - sha256, ) # To prevent a "tx-size-small" policy rule error, a transaction has to have a @@ -131,6 +144,45 @@ def check_script(script): assert False +class ValidWitnessMalleatedTx: + """ + Creates a valid witness malleation transaction test case: + - Parent transaction with a script supporting 2 branches + - 2 child transactions with the same txid but different wtxids + """ + def __init__(self): + hashlock = hash160(b'Preimage') + self.witness_script = CScript([OP_IF, OP_HASH160, hashlock, OP_EQUAL, OP_ELSE, OP_TRUE, OP_ENDIF]) + + def build_parent_tx(self, funding_txid, amount): + # Create an unsigned parent transaction paying to the witness script. + witness_program = sha256(self.witness_script) + script_pubkey = CScript([OP_0, witness_program]) + + parent = CTransaction() + parent.vin.append(CTxIn(COutPoint(int(funding_txid, 16), 0), b"")) + parent.vout.append(CTxOut(int(amount), script_pubkey)) + return parent + + def build_malleated_children(self, signed_parent_txid, amount): + # Create 2 valid children that differ only in witness data. + # 1. Create a new transaction with witness solving first branch + child_witness_script = CScript([OP_TRUE]) + child_witness_program = sha256(child_witness_script) + child_script_pubkey = CScript([OP_0, child_witness_program]) + + child_one = CTransaction() + child_one.vin.append(CTxIn(COutPoint(int(signed_parent_txid, 16), 0), b"")) + child_one.vout.append(CTxOut(int(amount), child_script_pubkey)) + child_one.wit.vtxinwit.append(CTxInWitness()) + child_one.wit.vtxinwit[0].scriptWitness.stack = [b'Preimage', b'\x01', self.witness_script] + + # 2. Create another identical transaction with witness solving second branch + child_two = deepcopy(child_one) + child_two.wit.vtxinwit[0].scriptWitness.stack = [b'', self.witness_script] + return child_one, child_two + + class TestFrameworkScriptUtil(unittest.TestCase): def test_multisig(self): fake_pubkey = bytes([0]*33)