The following Assume in the wallet will fail (triggered through rpc) under certain reorg conditions:
https://github.com/bitcoin/bitcoin/blob/114901c0655d1dea3024a1daa362716c948bbde3/src/wallet/spend.cpp#L413
<details>
<summary>Vibe coded functional test patch to demonstrate (`-DABORT_ON_FAILED_ASSUME` is required):</summary>
From 54aea28e6439be5645267f0344a6337d9798ba14 Mon Sep 17 00:00:00 2001
From: dergoegge <n.goeggi@gmail.com>
Date: Tue, 6 Jan 2026 10:49:02 +0000
Subject: [PATCH] truc wallet reorg bug
---
test/functional/mempool_truc.py | 163 +++++++++++++++++++++++++-------
1 file changed, 127 insertions(+), 36 deletions(-)
diff --git a/test/functional/mempool_truc.py b/test/functional/mempool_truc.py
index 2586308e4d..a155d57b68 100755
--- a/test/functional/mempool_truc.py
+++ b/test/functional/mempool_truc.py
@@ -47,6 +47,36 @@ class MempoolTRUC(BitcoinTestFramework):
self.extra_args = [[]]
self.setup_clean_chain = True
+ def skip_test_if_missing_module(self):
+ self.skip_if_no_wallet()
+
+ def create_tx(self, wallet, inputs, outputs, version, prevtxs=None):
+ """Create a signed transaction using the real wallet.
+
+ Args:
+ wallet: The wallet RPC to use
+ inputs: List of inputs (empty to let fundrawtransaction pick)
+ outputs: Dict of address -> amount
+ version: Transaction version
+ prevtxs: Optional list of previous transaction info for signing
+ unbroadcast transactions. Each entry should have:
+ {"txid": ..., "vout": ..., "scriptPubKey": ..., "amount": ...}
+ """
+ raw_tx = wallet.createrawtransaction(inputs=inputs, outputs=outputs, version=version)
+ if inputs == []:
+ raw_tx = wallet.fundrawtransaction(raw_tx, {'include_unsafe': True})["hex"]
+ if prevtxs:
+ signed_tx = wallet.signrawtransactionwithwallet(raw_tx, prevtxs)
+ else:
+ signed_tx = wallet.signrawtransactionwithwallet(raw_tx)
+ return signed_tx["hex"]
+
+ def send_tx(self, wallet, inputs, outputs, version, prevtxs=None):
+ """Create and broadcast a transaction using the real wallet."""
+ tx_hex = self.create_tx(wallet, inputs, outputs, version, prevtxs)
+ txid = wallet.sendrawtransaction(tx_hex)
+ return txid
+
def check_mempool(self, txids):
"""Assert exact contents of the node's mempool (by txid)."""
mempool_contents = self.nodes[0].getrawmempool()
@@ -173,41 +203,102 @@ class MempoolTRUC(BitcoinTestFramework): [@cleanup](/bitcoin-bitcoin/contributor/cleanup/)()
def test_truc_reorg(self):
node = self.nodes[0]
+ self.log.info("Test TRUC transactions survive reorg correctly")
+
+ # Create a wallet for this test
+ node.createwallet("truc_reorg_wallet")
+ wallet = node.get_wallet_rpc("truc_reorg_wallet")
+
+ # Fund the wallet
+ funding_addr = wallet.getnewaddress()
+ self.generatetoaddress(node, 1, funding_addr)
+ self.generate(node, 100) # Mature the coinbase
# Prep for fork
fork_blocks = create_empty_fork(node)
- self.log.info("Test that, during a reorg, TRUC rules are not enforced")
self.check_mempool([])
- # Testing 2<-3 versions allowed
- tx_v2_block = self.wallet.create_self_transfer(version=2)
+ # tx_chain_1: v2 transaction with 2 outputs
+ addr1 = wallet.getnewaddress()
+ addr2 = wallet.getnewaddress()
+ tx_chain_1_hex = self.create_tx(wallet, [], {addr1: 10, addr2: 10}, version=2)
+ tx_chain_1_decoded = node.decoderawtransaction(tx_chain_1_hex)
+ tx_chain_1_txid = tx_chain_1_decoded["txid"]
+
+ # Find which vout corresponds to which address and get scriptPubKey info
+ vout_for_addr1 = None
+ vout_for_addr2 = None
+ scriptpubkey_addr1 = None
+ scriptpubkey_addr2 = None
+ for vout in tx_chain_1_decoded["vout"]:
+ if vout["scriptPubKey"].get("address") == addr1:
+ vout_for_addr1 = vout["n"]
+ scriptpubkey_addr1 = vout["scriptPubKey"]["hex"]
+ elif vout["scriptPubKey"].get("address") == addr2:
+ vout_for_addr2 = vout["n"]
+ scriptpubkey_addr2 = vout["scriptPubKey"]["hex"]
+
+ # tx_chain_2: v3 transaction spending first output of tx_chain_1
+ addr3 = wallet.getnewaddress()
+ prevtxs_chain_2 = [{
+ "txid": tx_chain_1_txid,
+ "vout": vout_for_addr1,
+ "scriptPubKey": scriptpubkey_addr1,
+ "amount": Decimal("10")
+ }]
+ tx_chain_2_hex = self.create_tx(
+ wallet,
+ [{"txid": tx_chain_1_txid, "vout": vout_for_addr1}],
+ {addr3: Decimal("9.99999")},
+ version=3,
+ prevtxs=prevtxs_chain_2
+ )
+ tx_chain_2_decoded = node.decoderawtransaction(tx_chain_2_hex)
+ tx_chain_2_txid = tx_chain_2_decoded["txid"]
+
+ # Get scriptPubKey for tx_chain_2 output
+ scriptpubkey_addr3 = tx_chain_2_decoded["vout"][0]["scriptPubKey"]["hex"]
+
+ # tx_chain_3: v3 transaction spending output of tx_chain_2
+ addr4 = wallet.getnewaddress()
+ prevtxs_chain_3 = [{
+ "txid": tx_chain_2_txid,
+ "vout": 0,
+ "scriptPubKey": scriptpubkey_addr3,
+ "amount": Decimal("9.99999")
+ }]
+ tx_chain_3_hex = self.create_tx(
+ wallet,
+ [{"txid": tx_chain_2_txid, "vout": 0}],
+ {addr4: Decimal("9.99998")},
+ version=3,
+ prevtxs=prevtxs_chain_3
+ )
+ tx_chain_3_decoded = node.decoderawtransaction(tx_chain_3_hex)
+ tx_chain_3_txid = tx_chain_3_decoded["txid"]
- # Testing 3<-2 versions allowed
- tx_v3_block = self.wallet.create_self_transfer(version=3)
+ tx_to_mine = [tx_chain_1_hex, tx_chain_2_hex, tx_chain_3_hex]
+ self.generateblock(node, output="raw(42)", transactions=tx_to_mine)
- # Testing overly-large child size
- tx_v3_block2 = self.wallet.create_self_transfer(version=3)
+ self.check_mempool([])
- # Also create a linear chain of 3 TRUC transactions that will be directly mined, followed by one v2 in-mempool after block is made
- tx_chain_1 = self.wallet.create_self_transfer(version=3)
- tx_chain_2 = self.wallet.create_self_transfer(utxo_to_spend=tx_chain_1["new_utxo"], version=3)
- tx_chain_3 = self.wallet.create_self_transfer(utxo_to_spend=tx_chain_2["new_utxo"], version=3)
+ self.trigger_reorg(fork_blocks)
- tx_to_mine = [tx_v3_block["hex"], tx_v2_block["hex"], tx_v3_block2["hex"], tx_chain_1["hex"], tx_chain_2["hex"], tx_chain_3["hex"]]
- self.generateblock(node, output="raw(42)", transactions=tx_to_mine)
+ self.check_mempool([tx_chain_1_txid, tx_chain_2_txid, tx_chain_3_txid])
- self.check_mempool([])
+ # tx_chain_4: v2 transaction funded by the wallet (spending unconfirmed outputs from tx_chain_1)
+ addr5 = wallet.getnewaddress()
+ self.log.info("Creating tx_chain_4 spending from unconfirmed tx_chain_1")
+ tx_chain_4_txid = self.send_tx(
+ wallet,
+ [], # Let fundrawtransaction pick inputs
+ {addr5: Decimal("9.99")},
+ version=2
+ )
- tx_v2_from_v3 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block["new_utxo"], version=2)
- tx_v3_from_v2 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v2_block["new_utxo"], version=3)
- tx_v3_child_large = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block2["new_utxo"], target_vsize=1250, version=3)
- assert_greater_than(node.getmempoolentry(tx_v3_child_large["txid"])["vsize"], TRUC_CHILD_MAX_VSIZE)
- tx_chain_4 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_chain_3["new_utxo"], version=2)
- self.check_mempool([tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"], tx_chain_4["txid"]])
+ self.check_mempool([tx_chain_1_txid, tx_chain_2_txid, tx_chain_3_txid, tx_chain_4_txid])
- # Reorg should have all block transactions re-accepted, ignoring TRUC enforcement
- self.trigger_reorg(fork_blocks)
- self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"], tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"], tx_chain_1["txid"], tx_chain_2["txid"], tx_chain_3["txid"], tx_chain_4["txid"]])
+ wallet.unloadwallet() [@cleanup](/bitcoin-bitcoin/contributor/cleanup/)(extra_args=["-limitclustercount=1"])
def test_nondefault_package_limits(self):
@@ -658,20 +749,20 @@ class MempoolTRUC(BitcoinTestFramework):
node = self.nodes[0]
self.wallet = MiniWallet(node)
self.generate(self.wallet, 200)
- self.test_truc_max_vsize()
- self.test_truc_acceptance()
- self.test_truc_replacement()
+ #self.test_truc_max_vsize()
+ #self.test_truc_acceptance()
+ #self.test_truc_replacement()
self.test_truc_reorg()
- self.test_nondefault_package_limits()
- self.test_truc_ancestors_package()
- self.test_truc_ancestors_package_and_mempool()
- self.test_sibling_eviction_package()
- self.test_truc_package_inheritance()
- self.test_truc_in_testmempoolaccept()
- self.test_reorg_2child_rbf()
- self.test_truc_sibling_eviction()
- self.test_reorg_sibling_eviction_1p2c()
- self.test_minrelay_in_package_combos()
+ #self.test_nondefault_package_limits()
+ #self.test_truc_ancestors_package()
+ #self.test_truc_ancestors_package_and_mempool()
+ #self.test_sibling_eviction_package()
+ #self.test_truc_package_inheritance()
+ #self.test_truc_in_testmempoolaccept()
+ #self.test_reorg_2child_rbf()
+ #self.test_truc_sibling_eviction()
+ #self.test_reorg_sibling_eviction_1p2c()
+ #self.test_minrelay_in_package_combos()
if __name__ == "__main__":
--
2.50.1
</details>
Assertion was added in #32896
This was found with a test running on Antithesis.