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
0From 54aea28e6439be5645267f0344a6337d9798ba14 Mon Sep 17 00:00:00 2001
1From: dergoegge <n.goeggi@gmail.com>
2Date: Tue, 6 Jan 2026 10:49:02 +0000
3Subject: [PATCH] truc wallet reorg bug
4
5---
6 test/functional/mempool_truc.py | 163 +++++++++++++++++++++++++-------
7 1 file changed, 127 insertions(+), 36 deletions(-)
8
9diff --git a/test/functional/mempool_truc.py b/test/functional/mempool_truc.py
10index 2586308e4d..a155d57b68 100755
11--- a/test/functional/mempool_truc.py
12+++ b/test/functional/mempool_truc.py
13@@ -47,6 +47,36 @@ class MempoolTRUC(BitcoinTestFramework):
14 self.extra_args = [[]]
15 self.setup_clean_chain = True
16
17+ def skip_test_if_missing_module(self):
18+ self.skip_if_no_wallet()
19+
20+ def create_tx(self, wallet, inputs, outputs, version, prevtxs=None):
21+ """Create a signed transaction using the real wallet.
22+
23+ Args:
24+ wallet: The wallet RPC to use
25+ inputs: List of inputs (empty to let fundrawtransaction pick)
26+ outputs: Dict of address -> amount
27+ version: Transaction version
28+ prevtxs: Optional list of previous transaction info for signing
29+ unbroadcast transactions. Each entry should have:
30+ {"txid": ..., "vout": ..., "scriptPubKey": ..., "amount": ...}
31+ """
32+ raw_tx = wallet.createrawtransaction(inputs=inputs, outputs=outputs, version=version)
33+ if inputs == []:
34+ raw_tx = wallet.fundrawtransaction(raw_tx, {'include_unsafe': True})["hex"]
35+ if prevtxs:
36+ signed_tx = wallet.signrawtransactionwithwallet(raw_tx, prevtxs)
37+ else:
38+ signed_tx = wallet.signrawtransactionwithwallet(raw_tx)
39+ return signed_tx["hex"]
40+
41+ def send_tx(self, wallet, inputs, outputs, version, prevtxs=None):
42+ """Create and broadcast a transaction using the real wallet."""
43+ tx_hex = self.create_tx(wallet, inputs, outputs, version, prevtxs)
44+ txid = wallet.sendrawtransaction(tx_hex)
45+ return txid
46+
47 def check_mempool(self, txids):
48 """Assert exact contents of the node's mempool (by txid)."""
49 mempool_contents = self.nodes[0].getrawmempool()
50@@ -173,41 +203,102 @@ class MempoolTRUC(BitcoinTestFramework): [@cleanup](/bitcoin-bitcoin/contributor/cleanup/)()
51 def test_truc_reorg(self):
52 node = self.nodes[0]
53+ self.log.info("Test TRUC transactions survive reorg correctly")
54+
55+ # Create a wallet for this test
56+ node.createwallet("truc_reorg_wallet")
57+ wallet = node.get_wallet_rpc("truc_reorg_wallet")
58+
59+ # Fund the wallet
60+ funding_addr = wallet.getnewaddress()
61+ self.generatetoaddress(node, 1, funding_addr)
62+ self.generate(node, 100) # Mature the coinbase
63
64 # Prep for fork
65 fork_blocks = create_empty_fork(node)
66- self.log.info("Test that, during a reorg, TRUC rules are not enforced")
67 self.check_mempool([])
68
69- # Testing 2<-3 versions allowed
70- tx_v2_block = self.wallet.create_self_transfer(version=2)
71+ # tx_chain_1: v2 transaction with 2 outputs
72+ addr1 = wallet.getnewaddress()
73+ addr2 = wallet.getnewaddress()
74+ tx_chain_1_hex = self.create_tx(wallet, [], {addr1: 10, addr2: 10}, version=2)
75+ tx_chain_1_decoded = node.decoderawtransaction(tx_chain_1_hex)
76+ tx_chain_1_txid = tx_chain_1_decoded["txid"]
77+
78+ # Find which vout corresponds to which address and get scriptPubKey info
79+ vout_for_addr1 = None
80+ vout_for_addr2 = None
81+ scriptpubkey_addr1 = None
82+ scriptpubkey_addr2 = None
83+ for vout in tx_chain_1_decoded["vout"]:
84+ if vout["scriptPubKey"].get("address") == addr1:
85+ vout_for_addr1 = vout["n"]
86+ scriptpubkey_addr1 = vout["scriptPubKey"]["hex"]
87+ elif vout["scriptPubKey"].get("address") == addr2:
88+ vout_for_addr2 = vout["n"]
89+ scriptpubkey_addr2 = vout["scriptPubKey"]["hex"]
90+
91+ # tx_chain_2: v3 transaction spending first output of tx_chain_1
92+ addr3 = wallet.getnewaddress()
93+ prevtxs_chain_2 = [{
94+ "txid": tx_chain_1_txid,
95+ "vout": vout_for_addr1,
96+ "scriptPubKey": scriptpubkey_addr1,
97+ "amount": Decimal("10")
98+ }]
99+ tx_chain_2_hex = self.create_tx(
100+ wallet,
101+ [{"txid": tx_chain_1_txid, "vout": vout_for_addr1}],
102+ {addr3: Decimal("9.99999")},
103+ version=3,
104+ prevtxs=prevtxs_chain_2
105+ )
106+ tx_chain_2_decoded = node.decoderawtransaction(tx_chain_2_hex)
107+ tx_chain_2_txid = tx_chain_2_decoded["txid"]
108+
109+ # Get scriptPubKey for tx_chain_2 output
110+ scriptpubkey_addr3 = tx_chain_2_decoded["vout"][0]["scriptPubKey"]["hex"]
111+
112+ # tx_chain_3: v3 transaction spending output of tx_chain_2
113+ addr4 = wallet.getnewaddress()
114+ prevtxs_chain_3 = [{
115+ "txid": tx_chain_2_txid,
116+ "vout": 0,
117+ "scriptPubKey": scriptpubkey_addr3,
118+ "amount": Decimal("9.99999")
119+ }]
120+ tx_chain_3_hex = self.create_tx(
121+ wallet,
122+ [{"txid": tx_chain_2_txid, "vout": 0}],
123+ {addr4: Decimal("9.99998")},
124+ version=3,
125+ prevtxs=prevtxs_chain_3
126+ )
127+ tx_chain_3_decoded = node.decoderawtransaction(tx_chain_3_hex)
128+ tx_chain_3_txid = tx_chain_3_decoded["txid"]
129
130- # Testing 3<-2 versions allowed
131- tx_v3_block = self.wallet.create_self_transfer(version=3)
132+ tx_to_mine = [tx_chain_1_hex, tx_chain_2_hex, tx_chain_3_hex]
133+ self.generateblock(node, output="raw(42)", transactions=tx_to_mine)
134
135- # Testing overly-large child size
136- tx_v3_block2 = self.wallet.create_self_transfer(version=3)
137+ self.check_mempool([])
138
139- # Also create a linear chain of 3 TRUC transactions that will be directly mined, followed by one v2 in-mempool after block is made
140- tx_chain_1 = self.wallet.create_self_transfer(version=3)
141- tx_chain_2 = self.wallet.create_self_transfer(utxo_to_spend=tx_chain_1["new_utxo"], version=3)
142- tx_chain_3 = self.wallet.create_self_transfer(utxo_to_spend=tx_chain_2["new_utxo"], version=3)
143+ self.trigger_reorg(fork_blocks)
144
145- 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"]]
146- self.generateblock(node, output="raw(42)", transactions=tx_to_mine)
147+ self.check_mempool([tx_chain_1_txid, tx_chain_2_txid, tx_chain_3_txid])
148
149- self.check_mempool([])
150+ # tx_chain_4: v2 transaction funded by the wallet (spending unconfirmed outputs from tx_chain_1)
151+ addr5 = wallet.getnewaddress()
152+ self.log.info("Creating tx_chain_4 spending from unconfirmed tx_chain_1")
153+ tx_chain_4_txid = self.send_tx(
154+ wallet,
155+ [], # Let fundrawtransaction pick inputs
156+ {addr5: Decimal("9.99")},
157+ version=2
158+ )
159
160- tx_v2_from_v3 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block["new_utxo"], version=2)
161- tx_v3_from_v2 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v2_block["new_utxo"], version=3)
162- 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)
163- assert_greater_than(node.getmempoolentry(tx_v3_child_large["txid"])["vsize"], TRUC_CHILD_MAX_VSIZE)
164- tx_chain_4 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_chain_3["new_utxo"], version=2)
165- self.check_mempool([tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"], tx_chain_4["txid"]])
166+ self.check_mempool([tx_chain_1_txid, tx_chain_2_txid, tx_chain_3_txid, tx_chain_4_txid])
167
168- # Reorg should have all block transactions re-accepted, ignoring TRUC enforcement
169- self.trigger_reorg(fork_blocks)
170- 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"]])
171+ wallet.unloadwallet() [@cleanup](/bitcoin-bitcoin/contributor/cleanup/)(extra_args=["-limitclustercount=1"])
172 def test_nondefault_package_limits(self):
173@@ -658,20 +749,20 @@ class MempoolTRUC(BitcoinTestFramework):
174 node = self.nodes[0]
175 self.wallet = MiniWallet(node)
176 self.generate(self.wallet, 200)
177- self.test_truc_max_vsize()
178- self.test_truc_acceptance()
179- self.test_truc_replacement()
180+ #self.test_truc_max_vsize()
181+ #self.test_truc_acceptance()
182+ #self.test_truc_replacement()
183 self.test_truc_reorg()
184- self.test_nondefault_package_limits()
185- self.test_truc_ancestors_package()
186- self.test_truc_ancestors_package_and_mempool()
187- self.test_sibling_eviction_package()
188- self.test_truc_package_inheritance()
189- self.test_truc_in_testmempoolaccept()
190- self.test_reorg_2child_rbf()
191- self.test_truc_sibling_eviction()
192- self.test_reorg_sibling_eviction_1p2c()
193- self.test_minrelay_in_package_combos()
194+ #self.test_nondefault_package_limits()
195+ #self.test_truc_ancestors_package()
196+ #self.test_truc_ancestors_package_and_mempool()
197+ #self.test_sibling_eviction_package()
198+ #self.test_truc_package_inheritance()
199+ #self.test_truc_in_testmempoolaccept()
200+ #self.test_reorg_2child_rbf()
201+ #self.test_truc_sibling_eviction()
202+ #self.test_reorg_sibling_eviction_1p2c()
203+ #self.test_minrelay_in_package_combos()
204
205
206 if __name__ == "__main__":
207--
2082.50.1
Assertion was added in #32896
This was found with a test running on Antithesis.