What changed, and why it matters
This commit only adds a new regression test to Bitcoin Core. It does not change any production wallet, consensus, or mempool code. The test exercises a scenario involving v2 and v3 (TRUC) transactions across a blockchain reorganization, likely to ensure a previously reported issue stays fixed. There is no patch to runtime behavior here, so it does not introduce or fix an exploitable vulnerability by itself.
No immediate action required. Treat as a test-only change. If investigating issue 34206, review the underlying production fix that this test is meant to protect, but that fix is not present in this commit.
Security signals we found
Regression test added for issue 34206
Test covers reorg handling for v2 parent + TRUC child clusters
No production code changes
Evidence from the diff
The diff adds a single functional test, test_coins_availability_reorg, in test/functional/wallet_v3_txs.py. It creates a v2 parent transaction, chains a version-3 (TRUC) child, mines both, triggers a reorg with create_empty_fork/submitblock, and then verifies wallet coin availability and mempool cluster behavior after the reorg. The test references GitHub issue 34206 in its title. No C++, Python, or RPC implementation code is modified.
Changed components
test/functional/wallet_v3_txs.pyInspect captured patch +106 / −0
diff --git a/test/functional/wallet_v3_txs.py b/test/functional/wallet_v3_txs.py
index 6b3b40d5..20fd0ebb 100755
--- a/test/functional/wallet_v3_txs.py
+++ b/test/functional/wallet_v3_txs.py
@@ -19,6 +19,10 @@ from test_framework.script import (
from test_framework.script_util import bulk_vout
+from test_framework.blocktools import (
+ create_empty_fork,
+)
+
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
@@ -84,6 +88,12 @@ class WalletV3Test(BitcoinTestFramework):
test_func(2, 3)
test_func(3, 2)
+ def trigger_reorg(self, fork_blocks):
+ """Trigger reorg of the fork blocks."""
+ for block in fork_blocks:
+ self.nodes[0].submitblock(block.serialize().hex())
+ assert_equal(self.nodes[0].getbestblockhash(), fork_blocks[-1].hash_hex)
+
def run_test(self):
self.nodes[0].createwallet("alice")
self.alice = self.nodes[0].get_wallet_rpc("alice")
@@ -120,6 +130,7 @@ class WalletV3Test(BitcoinTestFramework):
self.mix_non_truc_versions()
self.cant_spend_multiple_unconfirmed_truc_outputs()
self.test_spend_third_generation()
+ self.test_coins_availability_reorg()
@cleanup
def tx_spends_unconfirmed_tx_with_wrong_version(self, version_a, version_b):
@@ -630,5 +641,100 @@ class WalletV3Test(BitcoinTestFramework):
version=3
)
+ @cleanup
+ def test_coins_availability_reorg(self):
+ self.log.info("Test coin availability after reorg with v2 parent and truc child")
+
+ # Prep fork blocks
+ fork_blocks = create_empty_fork(self.nodes[0])
+
+ # Send funds to alice so she can create transactions
+ outputs = {self.alice.getnewaddress(): 5.0}
+ self.send_tx(self.charlie, [], outputs, 2)
+ self.generate(self.nodes[0], 1)
+
+ # Alice creates a v2 transaction with 2 outputs
+ alice_unspent = self.alice.listunspent()[0]
+ v2_outputs = [
+ {self.alice.getnewaddress(): 2.5},
+ {self.alice.getnewaddress(): 2.4999},
+ ]
+ v2_txid = self.send_tx(self.alice, [alice_unspent], v2_outputs, 2)
+
+ # Mine the v2 transaction in one block
+ self.generate(self.nodes[0], 1)
+
+ # Get the output from the v2 transaction for chaining
+ v2_utxo = self.alice.listunspent(minconf=1)[0]
+ assert_equal(v2_utxo["txid"], v2_txid)
+
+ # Alice creates a truc child chaining from the v2 utxo
+ truc_outputs = {self.alice.getnewaddress(): v2_utxo["amount"] - Decimal("0.0001")}
+ truc_txid = self.send_tx(self.alice, [v2_utxo], truc_outputs, 3)
+
+ # Mine the truc transaction in a second block
+ self.generate(self.nodes[0], 1)
+
+ # Verify both transactions are confirmed
+ wallet_tx_v2 = self.alice.gettransaction(v2_txid)
+ wallet_tx_truc = self.alice.gettransaction(truc_txid)
+
+ assert_equal(wallet_tx_v2["confirmations"], 2)
+ assert_equal(wallet_tx_truc["confirmations"], 1)
+
+ # Check that their versions are correct
+ assert_equal(self.alice.decoderawtransaction(wallet_tx_v2["hex"])["version"], 2)
+ assert_equal(self.alice.decoderawtransaction(wallet_tx_truc["hex"])["version"], 3)
+
+ # Check listunspent before reorg - should have the truc output
+ unspent_before = self.alice.listunspent()
+ truc_output_txids = [u["txid"] for u in unspent_before]
+ assert truc_txid in truc_output_txids
+
+ # Trigger the reorg
+ self.trigger_reorg(fork_blocks)
+ # The TRUC transaction is now in a cluster of size 3, which is only permitted in a reorg.
+ assert_equal(self.nodes[0].getmempoolcluster(truc_txid)["txcount"], 3)
+
+ # After reorg, both transactions should be back in mempool
+ mempool = self.nodes[0].getrawmempool()
+ assert v2_txid in mempool
+ assert truc_txid in mempool
+
+ # Check listunspent after reorg - the truc output should still appear
+ # as unconfirmed since the transaction is in the mempool
+ unspent_after = self.alice.listunspent(minconf=0)
+ unspent_txids_after = [u["txid"] for u in unspent_after]
+ assert truc_txid in unspent_txids_after
+
+ total_unconfirmed_amount = sum([u["amount"] for u in self.alice.listunspent(minconf=0)])
+ # We cannot create a transaction spending both outputs, regardless of version.
+ output_too_high = {self.bob.getnewaddress(): total_unconfirmed_amount - Decimal("1")}
+ for version in [2, 3]:
+ raw_output_too_high = self.alice.createrawtransaction(inputs=[], outputs=output_too_high, version=version)
+ assert_raises_rpc_error(
+ -4,
+ "Insufficient funds",
+ self.alice.fundrawtransaction,
+ raw_output_too_high,
+ {'include_unsafe' : True}
+ )
+
+ # Now try to create a v2 transaction - this triggers AvailableCoins with
+ # check_version_trucness=true. The v2 parent has truc_child_in_mempool set
+ # because the truc child is in mempool.
+ new_v2_outputs = {self.bob.getnewaddress(): 0.1}
+ raw_v2_child = self.alice.createrawtransaction(inputs=[], outputs=new_v2_outputs, version=2)
+ raw_v2_with_v3_sibling = self.alice.fundrawtransaction(raw_v2_child, {'include_unsafe': True})
+
+ # See that this transaction can be added to mempool
+ signed_raw_v2_with_v3_sibling = self.alice.signrawtransactionwithwallet(raw_v2_with_v3_sibling["hex"])
+ self.alice.sendrawtransaction(signed_raw_v2_with_v3_sibling["hex"])
+
+ # This TRUC transaction is in a cluster of size 4
+ assert_equal(self.nodes[0].getmempoolcluster(truc_txid)["txcount"], 4)
+
+
+
if __name__ == '__main__':
WalletV3Test(__file__).main()
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.