From 81675a781f3ab62a0576a9739d13b4997b63230d Mon Sep 17 00:00:00 2001 From: naiyoma Date: Wed, 23 Jul 2025 14:02:11 +0300 Subject: [PATCH 1/3] test: use pre-generated chain --- test/functional/mempool_accept_wtxid.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/functional/mempool_accept_wtxid.py b/test/functional/mempool_accept_wtxid.py index 60fa580f39f..fe46fbf7dac 100755 --- a/test/functional/mempool_accept_wtxid.py +++ b/test/functional/mempool_accept_wtxid.py @@ -22,14 +22,11 @@ from test_framework.util import ( class MempoolWtxidTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - self.setup_clean_chain = True def run_test(self): node = self.nodes[0] - - self.log.info('Start with empty mempool and 101 blocks') - # The last 100 coinbase transactions are premature - blockhash = self.generate(node, 101)[0] + self.log.info('Start with pre-generated blocks') + blockhash = self.nodes[0].getblockhash(1) txid = node.getblock(blockhash=blockhash, verbosity=2)["tx"][0]["txid"] assert_equal(node.getmempoolinfo()['size'], 0) @@ -56,7 +53,6 @@ class MempoolWtxidTest(BitcoinTestFramework): self.log.info("Submit child_one to the mempool") txid_submitted = node.sendrawtransaction(child_one.serialize().hex()) assert_equal(node.getmempoolentry(txid_submitted)['wtxid'], child_one_wtxid) - peer_wtxid_relay.wait_for_broadcast([child_one_wtxid]) assert_equal(node.getmempoolinfo()["unbroadcastcount"], 0) From 7cfe790820cf247e8a27bb8091defc54c74d6aec Mon Sep 17 00:00:00 2001 From: naiyoma Date: Wed, 23 Jul 2025 17:03:47 +0300 Subject: [PATCH 2/3] test: replace ValidWitnessMalleatedTx class with function Simplify the witness malleation test helper by converting the ValidWitnessMalleatedTx class to a standalone function build_malleated_tx_package() and updating call sites. Co-authored-by: rkrux --- test/functional/mempool_accept_wtxid.py | 31 +++++---- test/functional/p2p_private_broadcast.py | 16 ++--- test/functional/test_framework/script_util.py | 68 ++++++++++--------- 3 files changed, 61 insertions(+), 54 deletions(-) diff --git a/test/functional/mempool_accept_wtxid.py b/test/functional/mempool_accept_wtxid.py index fe46fbf7dac..e388114dc4e 100755 --- a/test/functional/mempool_accept_wtxid.py +++ b/test/functional/mempool_accept_wtxid.py @@ -7,17 +7,16 @@ Test mempool acceptance in case of an already known transaction with identical non-witness data but different witness. """ -from test_framework.messages import ( - COIN, -) from test_framework.p2p import P2PTxInvStore -from test_framework.script_util import ValidWitnessMalleatedTx +from test_framework.script_util import build_malleated_tx_package from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_not_equal, assert_equal, ) - +from test_framework.wallet import ( + MiniWallet, +) class MempoolWtxidTest(BitcoinTestFramework): def set_test_params(self): @@ -25,23 +24,27 @@ class MempoolWtxidTest(BitcoinTestFramework): def run_test(self): node = self.nodes[0] + mini_wallet = MiniWallet(node) self.log.info('Start with pre-generated blocks') - blockhash = self.nodes[0].getblockhash(1) - txid = node.getblock(blockhash=blockhash, verbosity=2)["tx"][0]["txid"] + assert_equal(node.getmempoolinfo()['size'], 0) self.log.info("Submit parent with multiple script branches to mempool") - 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'] - signed_parent_txid = node.sendrawtransaction(hexstring=raw_parent, maxfeerate=0) + parent = mini_wallet.create_self_transfer()["tx"] + parent_amount = parent.vout[0].nValue - 10000 + child_amount = parent_amount - 10000 + parent, child_one, child_two = build_malleated_tx_package( + parent=parent, + rebalance_parent_output_amount=parent_amount, + child_amount=child_amount + ) + + mini_wallet.sendrawtransaction(from_node=node, tx_hex=parent.serialize().hex()) + self.generate(node, 1) peer_wtxid_relay = node.add_p2p_connection(P2PTxInvStore()) - - 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 child_two_wtxid = child_two.wtxid_hex diff --git a/test/functional/p2p_private_broadcast.py b/test/functional/p2p_private_broadcast.py index 4c3739d8a04..7b62b3de84d 100755 --- a/test/functional/p2p_private_broadcast.py +++ b/test/functional/p2p_private_broadcast.py @@ -18,7 +18,6 @@ from test_framework.p2p import ( from test_framework.messages import ( CAddress, CInv, - COIN, MSG_WTX, malleate_tx_to_invalid_witness, msg_inv, @@ -27,7 +26,7 @@ from test_framework.messages import ( from test_framework.netutil import ( format_addr_port ) -from test_framework.script_util import ValidWitnessMalleatedTx +from test_framework.script_util import build_malleated_tx_package from test_framework.socks5 import ( Socks5Configuration, Socks5Server, @@ -400,16 +399,17 @@ class P2PPrivateBroadcast(BitcoinTestFramework): tx_originator.setmocktime(0) # Let the clock tick again (it will go backwards due to this). self.log.info("Sending a pair of transactions with the same txid but different valid wtxids via RPC") - txgen = ValidWitnessMalleatedTx() - funding = wallet.get_utxo() - fee_sat = 1000 - siblings_parent = txgen.build_parent_tx(funding["txid"], amount=funding["value"] * COIN - fee_sat) - sibling1, sibling2 = txgen.build_malleated_children(siblings_parent.txid_hex, amount=siblings_parent.vout[0].nValue - fee_sat) + parent = wallet.create_self_transfer()["tx"] + parent_amount = parent.vout[0].nValue - 10000 + child_amount = parent_amount - 10000 + siblings_parent, sibling1, sibling2 = build_malleated_tx_package( + parent=parent, + rebalance_parent_output_amount=parent_amount, + child_amount=child_amount) self.log.info(f" - sibling1: txid={sibling1.txid_hex}, wtxid={sibling1.wtxid_hex}") self.log.info(f" - sibling2: txid={sibling2.txid_hex}, wtxid={sibling2.wtxid_hex}") assert_equal(sibling1.txid_hex, sibling2.txid_hex) assert_not_equal(sibling1.wtxid_hex, sibling2.wtxid_hex) - wallet.sign_tx(siblings_parent) assert_equal(len(tx_originator.getrawmempool()), 1) tx_returner.send_without_ping(msg_tx(siblings_parent)) self.wait_until(lambda: len(tx_originator.getrawmempool()) > 1) diff --git a/test/functional/test_framework/script_util.py b/test/functional/test_framework/script_util.py index 50a185eeab9..812d9bbd0a8 100755 --- a/test/functional/test_framework/script_util.py +++ b/test/functional/test_framework/script_util.py @@ -36,7 +36,10 @@ from test_framework.script import ( hash160, ) -from test_framework.util import assert_equal +from test_framework.util import ( + assert_greater_than_or_equal, + assert_equal, +) # Maximum number of potentially executed legacy signature operations in validating a transaction. MAX_STD_LEGACY_SIGOPS = 2_500 @@ -164,43 +167,44 @@ def check_script(script): assert False -class ValidWitnessMalleatedTx: +def build_malleated_tx_package(*, parent: CTransaction, rebalance_parent_output_amount, child_amount): """ - 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 + Returns a transaction package with valid witness: + - Parent transaction whose last output contains a script that has two spending conditions + - Two malleated child transactions with same txid but different wtxids because of different witnesses + + Args: + parent: Transaction with modifiable outputs. Either unsigned (sign after + calling this function) or anyone-can-spend (e.g., MiniWallet's OP_TRUE). """ - def __init__(self): - hashlock = hash160(b'Preimage') - self.witness_script = CScript([OP_IF, OP_HASH160, hashlock, OP_EQUAL, OP_ELSE, OP_TRUE, OP_ENDIF]) + 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]) - 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]) + # Append to the transaction the vout containing the script supporting 2 spending conditions + assert_greater_than_or_equal(len(parent.vout), 1) + last_output = parent.vout[len(parent.vout) - 1] + assert_greater_than_or_equal(last_output.nValue, rebalance_parent_output_amount) + last_output.nValue -= rebalance_parent_output_amount + parent.vout.append(CTxOut(rebalance_parent_output_amount, script_pubkey)) - 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]) + # 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 = 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 + child_one.vin.append(CTxIn(COutPoint(int(parent.txid_hex, 16), len(parent.vout) - 1), b"")) + child_one.vout.append(CTxOut(child_amount, child_script_pubkey)) + child_one.wit.vtxinwit.append(CTxInWitness()) + child_one.wit.vtxinwit[0].scriptWitness.stack = [b'Preimage', b'\x01', 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'', witness_script] + return parent, child_one, child_two class TestFrameworkScriptUtil(unittest.TestCase): From 3f5211cba8e73e8eb03781e6ec32ba9c4a263782 Mon Sep 17 00:00:00 2001 From: naiyoma Date: Wed, 30 Jul 2025 13:31:41 +0300 Subject: [PATCH 3/3] test: remove child_one/child_two (w)txid variables --- test/functional/mempool_accept_wtxid.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/test/functional/mempool_accept_wtxid.py b/test/functional/mempool_accept_wtxid.py index e388114dc4e..a9edea6d646 100755 --- a/test/functional/mempool_accept_wtxid.py +++ b/test/functional/mempool_accept_wtxid.py @@ -45,31 +45,27 @@ class MempoolWtxidTest(BitcoinTestFramework): self.generate(node, 1) peer_wtxid_relay = node.add_p2p_connection(P2PTxInvStore()) - child_one_wtxid = child_one.wtxid_hex - child_one_txid = child_one.txid_hex - child_two_wtxid = child_two.wtxid_hex - child_two_txid = child_two.txid_hex - assert_equal(child_one_txid, child_two_txid) - assert_not_equal(child_one_wtxid, child_two_wtxid) + assert_equal(child_one.txid_hex, child_two.txid_hex) + assert_not_equal(child_one.wtxid_hex, child_two.wtxid_hex) self.log.info("Submit child_one to the mempool") txid_submitted = node.sendrawtransaction(child_one.serialize().hex()) - assert_equal(node.getmempoolentry(txid_submitted)['wtxid'], child_one_wtxid) - peer_wtxid_relay.wait_for_broadcast([child_one_wtxid]) + assert_equal(node.getmempoolentry(txid_submitted)['wtxid'], child_one.wtxid_hex) + peer_wtxid_relay.wait_for_broadcast([child_one.wtxid_hex]) assert_equal(node.getmempoolinfo()["unbroadcastcount"], 0) # testmempoolaccept reports the "already in mempool" error assert_equal(node.testmempoolaccept([child_one.serialize().hex()]), [{ - "txid": child_one_txid, - "wtxid": child_one_wtxid, + "txid": child_one.txid_hex, + "wtxid": child_one.wtxid_hex, "allowed": False, "reject-reason": "txn-already-in-mempool", "reject-details": "txn-already-in-mempool" }]) assert_equal(node.testmempoolaccept([child_two.serialize().hex()])[0], { - "txid": child_two_txid, - "wtxid": child_two_wtxid, + "txid": child_two.txid_hex, + "wtxid": child_two.wtxid_hex, "allowed": False, "reject-reason": "txn-same-nonwitness-data-in-mempool", "reject-details": "txn-same-nonwitness-data-in-mempool" @@ -87,7 +83,7 @@ class MempoolWtxidTest(BitcoinTestFramework): # The node should rebroadcast the transaction using the wtxid of the correct transaction # (child_one, which is in its mempool). - peer_wtxid_relay_2.wait_for_broadcast([child_one_wtxid]) + peer_wtxid_relay_2.wait_for_broadcast([child_one.wtxid_hex]) assert_equal(node.getmempoolinfo()["unbroadcastcount"], 0) if __name__ == '__main__':