From c2d66e03326d5b399f4b10d5eab7f1f75d973e81 Mon Sep 17 00:00:00 2001 From: sugh01 <19183308+sugh01@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:45:19 +0100 Subject: [PATCH 1/4] test: added a test for checking double spend transaction states on longest chain --- daemon/test_daemon.go | 10 +- settings.conf | 2 +- test/sequentialtest/longest_chain/helpers.go | 2 +- .../longest_chain/longest_chain_test.go | 327 ++++++++++++++++++ 4 files changed, 338 insertions(+), 3 deletions(-) diff --git a/daemon/test_daemon.go b/daemon/test_daemon.go index a423902818..64fedbe717 100644 --- a/daemon/test_daemon.go +++ b/daemon/test_daemon.go @@ -757,7 +757,7 @@ func (td *TestDaemon) VerifyOnLongestChainInUtxoStore(t *testing.T, tx *bt.Tx) { func (td *TestDaemon) VerifyNotOnLongestChainInUtxoStore(t *testing.T, tx *bt.Tx) { readTx, err := td.UtxoStore.Get(td.Ctx, tx.TxIDChainHash(), fields.UnminedSince) require.NoError(t, err, "Failed to get transaction %s", tx.String()) - assert.Greater(t, readTx.UnminedSince, uint32(0), "Expected transaction %s to be on the longest chain", tx.TxIDChainHash().String()) + assert.Greater(t, readTx.UnminedSince, uint32(0), "Expected transaction %s to be not on the longest chain", tx.TxIDChainHash().String()) } // VerifyNotInUtxoStore verifies that the transaction does not exist in the UTXO store. @@ -1273,6 +1273,13 @@ finished: } func (td *TestDaemon) WaitForBlockStateChange(t *testing.T, expectedBlock *model.Block, timeout time.Duration) { + // First check if the expected block is already the current best block + state, err := td.BlockAssemblyClient.GetBlockAssemblyState(td.Ctx) + if err == nil && state.CurrentHash == expectedBlock.Header.Hash().String() { + t.Logf("Block %s (height %d) is already the current best block", expectedBlock.Header.Hash().String(), expectedBlock.Height) + return + } + stateChangeCh := make(chan blockassembly.BestBlockInfo) td.BlockAssembler.SetStateChangeCh(stateChangeCh) @@ -1290,6 +1297,7 @@ func (td *TestDaemon) WaitForBlockStateChange(t *testing.T, expectedBlock *model t.Fatalf("Timeout waiting for block assembly to reach block %s", expectedBlock.Header.Hash().String()) case bestBlockInfo := <-stateChangeCh: t.Logf("Received BestBlockInfo: Height=%d, Hash=%s", bestBlockInfo.Height, bestBlockInfo.Header.Hash().String()) + t.Logf("Expected block: Height=%d, Hash=%s", expectedBlock.Height, expectedBlock.Header.Hash().String()) if bestBlockInfo.Header.Hash().IsEqual(expectedBlock.Header.Hash()) { return } diff --git a/settings.conf b/settings.conf index 4fdf0649cb..9fab5438d5 100644 --- a/settings.conf +++ b/settings.conf @@ -338,7 +338,7 @@ blockchainDBUserPwd.docker.teranode3 = miner3 blockchain_store = sqlite:///blockchain blockchain_store.dev = postgres://teranode:teranode@localhost:${POSTGRES_PORT}/teranode -blockchain_store.dev.system.test = sqlitememory:///blockchain +blockchain_store.dev.system.test = sqlite:///blockchain blockchain_store.docker.ci.chainintegrity.teranode1 = postgres://miner1:miner1@localhost:${POSTGRES_PORT}/teranode1 blockchain_store.docker.ci.chainintegrity.teranode2 = postgres://miner2:miner2@localhost:${POSTGRES_PORT}/teranode2 blockchain_store.docker.ci.chainintegrity.teranode3 = postgres://miner3:miner3@localhost:${POSTGRES_PORT}/teranode3 diff --git a/test/sequentialtest/longest_chain/helpers.go b/test/sequentialtest/longest_chain/helpers.go index ac66cb28ff..0ac5dcecc7 100644 --- a/test/sequentialtest/longest_chain/helpers.go +++ b/test/sequentialtest/longest_chain/helpers.go @@ -13,7 +13,7 @@ import ( ) var ( - blockWait = 5 * time.Second + blockWait = 30 * time.Second ) func setupLongestChainTest(t *testing.T, utxoStoreOverride string) (td *daemon.TestDaemon, block3 *model.Block) { diff --git a/test/sequentialtest/longest_chain/longest_chain_test.go b/test/sequentialtest/longest_chain/longest_chain_test.go index d761899d56..103371f334 100644 --- a/test/sequentialtest/longest_chain/longest_chain_test.go +++ b/test/sequentialtest/longest_chain/longest_chain_test.go @@ -6,6 +6,7 @@ import ( "github.com/bsv-blockchain/teranode/test/utils/aerospike" "github.com/bsv-blockchain/teranode/test/utils/postgres" + "github.com/bsv-blockchain/teranode/test/utils/transactions" "github.com/stretchr/testify/require" ) @@ -23,6 +24,18 @@ func TestLongestChainSQLite(t *testing.T) { t.Run("invalid block with old tx", func(t *testing.T) { testLongestChainInvalidateBlockWithOldTx(t, utxoStore) }) + + t.Run("fork with different tx inclusion", func(t *testing.T) { + testLongestChainForkDifferentTxInclusion(t, utxoStore) + }) + + t.Run("transaction chain dependency", func(t *testing.T) { + testLongestChainTransactionChainDependency(t, utxoStore) + }) + + t.Run("partial output consumption", func(t *testing.T) { + testLongestChainWithDoubleSpendTransaction(t, utxoStore) + }) } func TestLongestChainPostgres(t *testing.T) { @@ -45,6 +58,18 @@ func TestLongestChainPostgres(t *testing.T) { t.Run("invalid block with old tx", func(t *testing.T) { testLongestChainInvalidateBlockWithOldTx(t, utxoStore) }) + + t.Run("fork with different tx inclusion", func(t *testing.T) { + testLongestChainForkDifferentTxInclusion(t, utxoStore) + }) + + t.Run("transaction chain dependency", func(t *testing.T) { + testLongestChainTransactionChainDependency(t, utxoStore) + }) + + t.Run("partial output consumption", func(t *testing.T) { + testLongestChainWithDoubleSpendTransaction(t, utxoStore) + }) } func TestLongestChainAerospike(t *testing.T) { @@ -67,6 +92,14 @@ func TestLongestChainAerospike(t *testing.T) { t.Run("invalid block with old tx", func(t *testing.T) { testLongestChainInvalidateBlockWithOldTx(t, utxoStore) }) + + t.Run("fork with different tx inclusion", func(t *testing.T) { + testLongestChainForkDifferentTxInclusion(t, utxoStore) + }) + + t.Run("transaction chain dependency", func(t *testing.T) { + testLongestChainWithDoubleSpendTransaction(t, utxoStore) + }) } func testLongestChainSimple(t *testing.T, utxoStore string) { @@ -243,3 +276,297 @@ func testLongestChainInvalidateBlockWithOldTx(t *testing.T, utxoStore string) { td.VerifyNotOnLongestChainInUtxoStore(t, tx1) td.VerifyOnLongestChainInUtxoStore(t, tx2) } + +func testLongestChainForkDifferentTxInclusion(t *testing.T, utxoStore string) { + // Setup test environment + td, block3 := setupLongestChainTest(t, utxoStore) + defer func() { + td.Stop(t) + }() + + block1, err := td.BlockchainClient.GetBlockByHeight(td.Ctx, 1) + require.NoError(t, err) + + block2, err := td.BlockchainClient.GetBlockByHeight(td.Ctx, 2) + require.NoError(t, err) + + // Create two transactions + tx1 := td.CreateTransaction(t, block1.CoinbaseTx, 0) + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, tx1)) + + tx2 := td.CreateTransaction(t, block2.CoinbaseTx, 0) + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, tx2)) + + td.VerifyInBlockAssembly(t, tx1) + td.VerifyInBlockAssembly(t, tx2) + + // Fork A: Create block4a with only tx1 + _, block4a := td.CreateTestBlock(t, block3, 4001, tx1) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block4a, "legacy", nil, false), "Failed to process block") + td.WaitForBlock(t, block4a, blockWait) + td.WaitForBlockBeingMined(t, block4a) + + // 0 -> 1 ... 2 -> 3 -> 4a (*) + + td.VerifyNotInBlockAssembly(t, tx1) // mined and removed from block assembly + td.VerifyInBlockAssembly(t, tx2) // not mined yet + td.VerifyOnLongestChainInUtxoStore(t, tx1) + td.VerifyNotOnLongestChainInUtxoStore(t, tx2) + + // Fork B: Create block4b with both tx1 and tx2 + _, block4b := td.CreateTestBlock(t, block3, 4002, tx1, tx2) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block4b, "legacy", nil, false), "Failed to process block") + td.WaitForBlockBeingMined(t, block4b) + + time.Sleep(1 * time.Second) // give some time for the block to be processed + + // / 4a (*) + // 0 -> 1 ... 2 -> 3 + // \ 4b + + // Still on fork A, so tx1 is mined, tx2 is not + td.VerifyNotInBlockAssembly(t, tx1) // mined in fork A + td.VerifyInBlockAssembly(t, tx2) // not on longest chain yet + td.VerifyOnLongestChainInUtxoStore(t, tx1) + td.VerifyNotOnLongestChainInUtxoStore(t, tx2) + + // Make fork B longer by adding block5b + _, block5b := td.CreateTestBlock(t, block4b, 5002) // empty block + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block5b, "legacy", nil, false, false), "Failed to process block") + td.WaitForBlock(t, block5b, blockWait) + td.WaitForBlockBeingMined(t, block5b) + + // / 4a + // 0 -> 1 ... 2 -> 3 + // \ 4b -> 5b (*) + + // Now fork B is longest, both tx1 and tx2 are mined in block4b + td.VerifyNotInBlockAssembly(t, tx1) // mined in fork B (block4b) + td.VerifyNotInBlockAssembly(t, tx2) // mined in fork B (block4b) + td.VerifyOnLongestChainInUtxoStore(t, tx1) + td.VerifyOnLongestChainInUtxoStore(t, tx2) +} + +func testLongestChainTransactionChainDependency(t *testing.T, utxoStore string) { + // Scenario: Parent-child transaction chain where parent gets invalidated in reorg + // Fork A: Block4a contains tx1 (creates multiple outputs) + // Mempool: tx2 spends output from tx1, tx3 spends output from tx2 + // Fork B becomes longest without tx1 + // All dependent transactions (tx2, tx3) should be removed from mempool + + td, block3 := setupLongestChainTest(t, utxoStore) + defer func() { + td.Stop(t) + }() + + block1, err := td.BlockchainClient.GetBlockByHeight(td.Ctx, 1) + require.NoError(t, err) + + block2, err := td.BlockchainClient.GetBlockByHeight(td.Ctx, 2) + require.NoError(t, err) + + // Create parent transaction with multiple outputs (explicitly 5 outputs) + // This ensures we have enough outputs for child and grandchild transactions to spend + tx1, err := td.CreateParentTransactionWithNOutputs(t, block1.CoinbaseTx, 5) + require.NoError(t, err) + td.VerifyInBlockAssembly(t, tx1) + t.Logf("tx1 created with %d outputs", len(tx1.Outputs)) + + // Mine tx1 in Fork A + _, block4a := td.CreateTestBlock(t, block3, 4001, tx1) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block4a, "legacy", nil, false), "Failed to process block") + td.WaitForBlock(t, block4a, blockWait) + td.WaitForBlockBeingMined(t, block4a) + + // 0 -> 1 ... 2 -> 3 -> 4a (*) + + td.VerifyNotInBlockAssembly(t, tx1) + td.VerifyOnLongestChainInUtxoStore(t, tx1) + + // Create child transaction (tx2) spending output from tx1 + tx2 := td.CreateTransaction(t, tx1, 0) + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, tx2)) + td.VerifyInBlockAssembly(t, tx2) + + // Create grandchild transaction (tx3) spending output from tx2 + tx3 := td.CreateTransaction(t, tx2, 0) + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, tx3)) + td.VerifyInBlockAssembly(t, tx3) + + // Create competing Fork B without tx1 + altTx := td.CreateTransaction(t, block2.CoinbaseTx, 0) + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, altTx)) + + _, block4b := td.CreateTestBlock(t, block3, 4002, altTx) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block4b, "legacy", nil, false), "Failed to process block") + td.WaitForBlockBeingMined(t, block4b) + + time.Sleep(1 * time.Second) + + // / 4a (*) [contains tx1] + // 0 -> 1 ... 2 -> 3 + // \ 4b [contains altTx, no tx1] + + // Still on Fork A, all transactions should be in expected state + td.VerifyNotInBlockAssembly(t, tx1) + td.VerifyInBlockAssembly(t, tx2) + td.VerifyInBlockAssembly(t, tx3) + + // Make Fork B longer + _, block5b := td.CreateTestBlock(t, block4b, 5002) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block5b, "legacy", nil, false, false), "Failed to process block") + td.WaitForBlock(t, block5b, blockWait) + td.WaitForBlockBeingMined(t, block5b) + + // / 4a [contains tx1] + // 0 -> 1 ... 2 -> 3 + // \ 4b -> 5b (*) [no tx1] + + // Now Fork B is longest, tx1 should return to mempool + // tx2 and tx3 should be removed as their parent (tx1) outputs are not on longest chain + td.VerifyInBlockAssembly(t, tx1) // back in mempool + + // tx2 and tx3 depend on tx1's outputs which are not on the longest chain + // They should NOT be in block assembly as they're invalid + td.VerifyInBlockAssembly(t, tx2) + td.VerifyInBlockAssembly(t, tx3) + + td.VerifyNotOnLongestChainInUtxoStore(t, tx1) + td.VerifyNotOnLongestChainInUtxoStore(t, tx2) + td.VerifyNotOnLongestChainInUtxoStore(t, tx3) +} + +func testLongestChainWithDoubleSpendTransaction(t *testing.T, utxoStore string) { + // Scenario: Transaction with multiple outputs gets consumed differently across forks + // Parent tx creates multiple outputs [O1, O2, O3] + // Fork A: Contains tx1 spending O1 and tx2 spending O2 + // Fork B: Contains tx3 spending all outputs [O2, O3] + + td, block3 := setupLongestChainTest(t, utxoStore) + defer func() { + td.Stop(t) + }() + + block1, err := td.BlockchainClient.GetBlockByHeight(td.Ctx, 1) + require.NoError(t, err) + + block2, err := td.BlockchainClient.GetBlockByHeight(td.Ctx, 2) + require.NoError(t, err) + + // Create parent transaction with multiple outputs (at least 3) + parentTx, err := td.CreateParentTransactionWithNOutputs(t, block1.CoinbaseTx, 4) + require.NoError(t, err) + + // Mine parentTx first so we have confirmed UTXOs + _, block4 := td.CreateTestBlock(t, block3, 4000, parentTx) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block4, "legacy", nil, false), "Failed to process block") + t.Logf("WaitForBlockBeingMined(t, block4): %s", block4.Hash().String()) + td.WaitForBlockBeingMined(t, block4) + t.Logf("WaitForBlock(t, block4, blockWait): %s", block4.Hash().String()) + td.WaitForBlock(t, block4, blockWait) + t.Logf("VerifyNotInBlockAssembly(t, parentTx): %s", parentTx.TxIDChainHash().String()) + td.VerifyNotInBlockAssembly(t, parentTx) + t.Logf("VerifyOnLongestChainInUtxoStore(t, parentTx): %s", parentTx.TxIDChainHash().String()) + td.VerifyOnLongestChainInUtxoStore(t, parentTx) + + // 0 -> 1 ... 2 -> 3 -> 4 (*) + + // Create transactions spending individual outputs + tx1 := td.CreateTransaction(t, parentTx, 0) // spends output 0 + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, tx1)) + + tx2 := td.CreateTransaction(t, parentTx, 1) // spends output 1 + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, tx2)) + + td.VerifyInBlockAssembly(t, tx1) + td.VerifyInBlockAssembly(t, tx2) + + // Fork A: Mine tx1 and tx2 separately + _, block5a := td.CreateTestBlock(t, block4, 5001, tx1, tx2) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block5a, "legacy", nil, false), "Failed to process block") + t.Logf("WaitForBlockBeingMined(t, block5a): %s", block5a.Hash().String()) + td.WaitForBlockBeingMined(t, block5a) + t.Logf("WaitForBlock(t, block5a, blockWait): %s", block5a.Hash().String()) + td.WaitForBlock(t, block5a, blockWait) + + // 0 -> 1 ... 2 -> 3 -> 4 -> 5a (*) + + t.Logf("VerifyNotInBlockAssembly(t, tx1): %s", tx1.TxIDChainHash().String()) + td.VerifyNotInBlockAssembly(t, tx1) + t.Logf("VerifyNotInBlockAssembly(t, tx2): %s", tx2.TxIDChainHash().String()) + td.VerifyNotInBlockAssembly(t, tx2) + t.Logf("VerifyOnLongestChainInUtxoStore(t, tx1): %s", tx1.TxIDChainHash().String()) + td.VerifyOnLongestChainInUtxoStore(t, tx1) + t.Logf("VerifyOnLongestChainInUtxoStore(t, tx2): %s", tx2.TxIDChainHash().String()) + td.VerifyOnLongestChainInUtxoStore(t, tx2) + + // Fork B: Create a transaction that spends output 2 from parentTx along with output 0 from parentTx2 + // This creates a conflict with tx2 which spend those outputs individually + + parentTx2 := td.CreateTransaction(t, block2.CoinbaseTx, 0) + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, parentTx2)) + + // Wait for it to be in block assembly + td.VerifyInBlockAssembly(t, parentTx2) + + tx3 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTx, 3), transactions.WithInput(parentTx, 1), transactions.WithInput(parentTx2, 0), transactions.WithP2PKHOutputs(1, 100000)) + + _, block5b := td.CreateTestBlock(t, block4, 5002, parentTx2, tx3) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block5b, "legacy", nil, false), "Failed to process block") + t.Logf("WaitForBlockBeingMined(t, block5b): %s", block5b.Hash().String()) + td.WaitForBlockBeingMined(t, block5b) + + // / 5a (*) [tx1, tx2] + // 0 -> 1 ... 2 -> 3 -> 4 + // \ 5b [tx3 consumes same outputs, altTx] + + // Make Fork B longer + _, block6b := td.CreateTestBlock(t, block5b, 6002) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block6b, "legacy", nil, false, false), "Failed to process block") + t.Logf("WaitForBlockBeingMined(t, block6b): %s", block6b.Hash().String()) + td.WaitForBlockBeingMined(t, block6b) + t.Logf("WaitForBlock(t, block6b, blockWait): %s", block6b.Hash().String()) + td.WaitForBlock(t, block6b, blockWait) + + // / 5a [tx1, tx2] + // 0 -> 1 ... 2 -> 3 -> 4 + // \ 5b -> 6b (*) [tx3, altTx] + + t.Logf("VerifyInBlockAssembly(t, tx1): %s", tx1.TxIDChainHash().String()) + td.VerifyInBlockAssembly(t, tx1) + t.Logf("VerifyNotInBlockAssembly(t, tx2): %s", tx2.TxIDChainHash().String()) + td.VerifyNotInBlockAssembly(t, tx2) + t.Logf("VerifyNotInBlockAssembly(t, tx3): %s", tx3.TxIDChainHash().String()) + td.VerifyNotInBlockAssembly(t, tx3) // mined in block5b + t.Logf("VerifyNotOnLongestChainInUtxoStore(t, tx1): %s", tx1.TxIDChainHash().String()) + td.VerifyNotOnLongestChainInUtxoStore(t, tx1) + t.Logf("VerifyNotOnLongestChainInUtxoStore(t, tx2): %s", tx2.TxIDChainHash().String()) + td.VerifyNotOnLongestChainInUtxoStore(t, tx2) + t.Logf("VerifyOnLongestChainInUtxoStore(t, tx3): %s", tx3.TxIDChainHash().String()) + td.VerifyOnLongestChainInUtxoStore(t, tx3) + + // mine a block and verify if tx3 is mined + _, block6a := td.CreateTestBlock(t, block5a, 6001, tx3) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block6a, "legacy", nil, false), "Failed to process block") + td.WaitForBlockBeingMined(t, block6a) + + _, block7a := td.CreateTestBlock(t, block6a, 7001) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block7a, "legacy", nil, false), "Failed to process block") + td.WaitForBlockBeingMined(t, block7a) + + td.VerifyInBlockAssembly(t, tx3) + td.VerifyNotOnLongestChainInUtxoStore(t, tx3) + + t.Logf("VerifyNotInBlockAssembly(t, tx1): %s", tx1.TxIDChainHash().String()) + td.VerifyNotInBlockAssembly(t, tx1) + t.Logf("VerifyNotInBlockAssembly(t, tx2): %s", tx2.TxIDChainHash().String()) + td.VerifyNotInBlockAssembly(t, tx2) + t.Logf("VerifyNotInBlockAssembly(t, tx3): %s", tx3.TxIDChainHash().String()) + td.VerifyNotInBlockAssembly(t, tx3) // mined in block5b + t.Logf("VerifyOnLongestChainInUtxoStore(t, tx1): %s", tx1.TxIDChainHash().String()) + td.VerifyOnLongestChainInUtxoStore(t, tx1) + t.Logf("VerifyOnLongestChainInUtxoStore(t, tx2): %s", tx2.TxIDChainHash().String()) + td.VerifyOnLongestChainInUtxoStore(t, tx2) + t.Logf("VerifyNotOnLongestChainInUtxoStore(t, tx3): %s", tx3.TxIDChainHash().String()) + td.VerifyNotOnLongestChainInUtxoStore(t, tx3) +} From 516b7558e4d450980395955fb909931cd828df31 Mon Sep 17 00:00:00 2001 From: sugh01 <19183308+sugh01@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:49:12 +0100 Subject: [PATCH 2/4] test: adds invalid block test --- .../03_longest_chain_invalidate_fork_test.go | 142 ++++++++++++++++++ .../03_longest_chain_multirecords_test.go | 142 ++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 test/sequentialtest/longest_chain/03_longest_chain_invalidate_fork_test.go create mode 100644 test/sequentialtest/longest_chain/03_longest_chain_multirecords_test.go diff --git a/test/sequentialtest/longest_chain/03_longest_chain_invalidate_fork_test.go b/test/sequentialtest/longest_chain/03_longest_chain_invalidate_fork_test.go new file mode 100644 index 0000000000..2e994dcff3 --- /dev/null +++ b/test/sequentialtest/longest_chain/03_longest_chain_invalidate_fork_test.go @@ -0,0 +1,142 @@ +package longest_chain + +import ( + "testing" + + "github.com/bsv-blockchain/teranode/test/utils/aerospike" + "github.com/bsv-blockchain/teranode/test/utils/transactions" + "github.com/stretchr/testify/require" +) + +func TestLongestChainAerospikeInvalidateFork(t *testing.T) { + // start an aerospike container + utxoStore, teardown, err := aerospike.InitAerospikeContainer() + require.NoError(t, err) + + t.Cleanup(func() { + _ = teardown() + }) + + t.Run("invalid block with old tx", func(t *testing.T) { + testLongestChainInvalidateFork(t, utxoStore) + }) +} + + +func testLongestChainInvalidateFork(t *testing.T, utxoStore string) { + // Setup test environment + td, block3 := setupLongestChainTest(t, utxoStore) + defer func() { + td.Stop(t) + }() + + td.Settings.BlockValidation.OptimisticMining = true + + block1, err := td.BlockchainClient.GetBlockByHeight(td.Ctx, 1) + require.NoError(t, err) + + parentTxWith3Outputs := td.CreateTransactionWithOptions(t, transactions.WithInput(block1.CoinbaseTx, 0), transactions.WithP2PKHOutputs(3, 100000) ) + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, parentTxWith3Outputs)) + + childTx1 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 0), transactions.WithP2PKHOutputs(1, 100000) ) + childTx2 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 1), transactions.WithP2PKHOutputs(1, 100000) ) + childTx3 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 2), transactions.WithP2PKHOutputs(1, 100000) ) + + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, childTx1)) + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, childTx2)) + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, childTx3)) + + _, block4a := td.CreateTestBlock(t, block3, 4001, parentTxWith3Outputs, childTx1, childTx2) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block4a, "legacy", nil, false, true), "Failed to process block") + td.WaitForBlockBeingMined(t, block4a) + t.Logf("WaitForBlock(t, block4a, blockWait): %s", block4a.Hash().String()) + td.WaitForBlock(t, block4a, blockWait) + + + // 0 -> 1 ... 2 -> 3 -> 4a (*) + + td.VerifyNotInBlockAssembly(t, parentTxWith3Outputs) + td.VerifyNotInBlockAssembly(t, childTx1) + td.VerifyNotInBlockAssembly(t, childTx2) + td.VerifyInBlockAssembly(t, childTx3) + td.VerifyOnLongestChainInUtxoStore(t, parentTxWith3Outputs) + td.VerifyOnLongestChainInUtxoStore(t, childTx1) + td.VerifyOnLongestChainInUtxoStore(t, childTx2) + td.VerifyNotOnLongestChainInUtxoStore(t, childTx3) + + // create a block with tx1 and tx2 that will be invalid as tx2 is already on block4a + _, block4b := td.CreateTestBlock(t, block3, 4002, parentTxWith3Outputs, childTx2, childTx3) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block4b, "legacy", nil, true), "Failed to process block") + t.Logf("WaitForBlockBeingMined(t, block4b): %s", block4b.Hash().String()) + td.WaitForBlockBeingMined(t, block4b) + + _, block5b := td.CreateTestBlock(t, block4b, 5001) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block5b, "legacy", nil, true), "Failed to process block") + t.Logf("WaitForBlockBeingMined(t, block5b): %s", block5b.Hash().String()) + td.WaitForBlockBeingMined(t, block5b) + t.Logf("WaitForBlock(t, block5b, blockWait): %s", block5b.Hash().String()) + td.WaitForBlock(t, block5b, blockWait) + + // 0 -> 1 ... 2 -> 3 -> 4b -> 5b (*) + td.VerifyInBlockAssembly(t, childTx1) + td.VerifyNotInBlockAssembly(t, childTx2) + td.VerifyNotInBlockAssembly(t, childTx3) + td.VerifyNotOnLongestChainInUtxoStore(t, childTx1) + td.VerifyOnLongestChainInUtxoStore(t, childTx2) + td.VerifyOnLongestChainInUtxoStore(t, childTx3) + + _, err = td.BlockchainClient.InvalidateBlock(t.Context(), block5b.Hash()) + require.NoError(t, err) + + td.WaitForBlock(t, block4a, blockWait) + + // 0 -> 1 ... 2 -> 3 -> 4a + + td.VerifyNotInBlockAssembly(t, childTx1) + td.VerifyNotInBlockAssembly(t, childTx2) + td.VerifyInBlockAssembly(t, childTx3) + td.VerifyOnLongestChainInUtxoStore(t, childTx1) + td.VerifyOnLongestChainInUtxoStore(t, childTx2) + td.VerifyNotOnLongestChainInUtxoStore(t, childTx3) + + // create a double spend of tx3 + childTx3DS := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 2), transactions.WithP2PKHOutputs(2, 50000) ) + + // create a new block on 4a with tx3 in it + _, block5a := td.CreateTestBlock(t, block4a, 6001, childTx3DS) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block5a, "legacy", nil, false, true), "Failed to process block") + t.Logf("WaitForBlockBeingMined(t, block5a): %s", block5a.Hash().String()) + td.WaitForBlockBeingMined(t, block5a) + t.Logf("WaitForBlock(t, block5a, blockWait): %s", block5a.Hash().String()) + td.WaitForBlock(t, block5a, blockWait) + + td.VerifyNotInBlockAssembly(t, childTx1) + td.VerifyNotInBlockAssembly(t, childTx2) + td.VerifyInBlockAssembly(t, childTx3) + td.VerifyNotInBlockAssembly(t, childTx3DS) + td.VerifyOnLongestChainInUtxoStore(t, childTx1) + td.VerifyOnLongestChainInUtxoStore(t, childTx2) + td.VerifyNotOnLongestChainInUtxoStore(t, childTx3) + td.VerifyOnLongestChainInUtxoStore(t, childTx3DS)// 0 -> 1 ... 2 -> 3 -> 4a -> 6a (*) + + _, err = td.BlockchainClient.InvalidateBlock(t.Context(), block4b.Hash()) + require.NoError(t, err) + + // create a new block on 5a with tx3 in it + _, block6a := td.CreateTestBlock(t, block5a, 7001, childTx3) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block6a, "legacy", nil, false, true), "Failed to process block") + t.Logf("WaitForBlockBeingMined(t, block6a): %s", block6a.Hash().String()) + td.WaitForBlockBeingMined(t, block6a) + t.Logf("WaitForBlock(t, block6a, blockWait): %s", block6a.Hash().String()) + td.WaitForBlock(t, block6a, blockWait) + + t.Logf("FINAL VERIFICATIONS:") + td.VerifyNotInBlockAssembly(t, childTx1) + td.VerifyNotInBlockAssembly(t, childTx2) + td.VerifyNotInBlockAssembly(t, childTx3) + td.VerifyNotInBlockAssembly(t, childTx3DS) + td.VerifyOnLongestChainInUtxoStore(t, childTx1) + td.VerifyOnLongestChainInUtxoStore(t, childTx2) + td.VerifyOnLongestChainInUtxoStore(t, childTx3) + td.VerifyOnLongestChainInUtxoStore(t, childTx3DS) +} diff --git a/test/sequentialtest/longest_chain/03_longest_chain_multirecords_test.go b/test/sequentialtest/longest_chain/03_longest_chain_multirecords_test.go new file mode 100644 index 0000000000..1522a2ae12 --- /dev/null +++ b/test/sequentialtest/longest_chain/03_longest_chain_multirecords_test.go @@ -0,0 +1,142 @@ +package longest_chain + +import ( + "testing" + + "github.com/bsv-blockchain/teranode/test/utils/aerospike" + "github.com/bsv-blockchain/teranode/test/utils/transactions" + "github.com/stretchr/testify/require" +) + +func TestLongestChainAerospikeMultirecordsInvalidateWinningFork(t *testing.T) { + // start an aerospike container + utxoStore, teardown, err := aerospike.InitAerospikeContainer() + require.NoError(t, err) + + t.Cleanup(func() { + _ = teardown() + }) + + t.Run("invalid block with old tx", func(t *testing.T) { + testLongestChainInvalidateWinningFork(t, utxoStore) + }) +} + + +func testLongestChainInvalidateWinningFork(t *testing.T, utxoStore string) { + // Setup test environment + td, block3 := setupLongestChainTest(t, utxoStore) + defer func() { + td.Stop(t) + }() + + td.Settings.BlockValidation.OptimisticMining = true + + block1, err := td.BlockchainClient.GetBlockByHeight(td.Ctx, 1) + require.NoError(t, err) + + parentTxWith3Outputs := td.CreateTransactionWithOptions(t, transactions.WithInput(block1.CoinbaseTx, 0), transactions.WithP2PKHOutputs(3, 100000) ) + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, parentTxWith3Outputs)) + + childTx1 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 0), transactions.WithP2PKHOutputs(1, 100000) ) + childTx2 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 1), transactions.WithP2PKHOutputs(1, 100000) ) + childTx3 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 2), transactions.WithP2PKHOutputs(1, 100000) ) + + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, childTx1)) + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, childTx2)) + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, childTx3)) + + _, block4a := td.CreateTestBlock(t, block3, 4001, parentTxWith3Outputs, childTx1, childTx2) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block4a, "legacy", nil, false, true), "Failed to process block") + td.WaitForBlockBeingMined(t, block4a) + t.Logf("WaitForBlock(t, block4a, blockWait): %s", block4a.Hash().String()) + td.WaitForBlock(t, block4a, blockWait) + + + // 0 -> 1 ... 2 -> 3 -> 4a (*) + + td.VerifyNotInBlockAssembly(t, parentTxWith3Outputs) + td.VerifyNotInBlockAssembly(t, childTx1) + td.VerifyNotInBlockAssembly(t, childTx2) + td.VerifyInBlockAssembly(t, childTx3) + td.VerifyOnLongestChainInUtxoStore(t, parentTxWith3Outputs) + td.VerifyOnLongestChainInUtxoStore(t, childTx1) + td.VerifyOnLongestChainInUtxoStore(t, childTx2) + td.VerifyNotOnLongestChainInUtxoStore(t, childTx3) + + // create a block with tx1 and tx2 that will be invalid as tx2 is already on block4a + _, block4b := td.CreateTestBlock(t, block3, 4002, parentTxWith3Outputs, childTx2, childTx3) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block4b, "legacy", nil, true), "Failed to process block") + t.Logf("WaitForBlockBeingMined(t, block4b): %s", block4b.Hash().String()) + td.WaitForBlockBeingMined(t, block4b) + + _, block5b := td.CreateTestBlock(t, block4b, 5001) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block5b, "legacy", nil, true), "Failed to process block") + t.Logf("WaitForBlockBeingMined(t, block5b): %s", block5b.Hash().String()) + td.WaitForBlockBeingMined(t, block5b) + t.Logf("WaitForBlock(t, block5b, blockWait): %s", block5b.Hash().String()) + td.WaitForBlock(t, block5b, blockWait) + + // 0 -> 1 ... 2 -> 3 -> 4b -> 5b (*) + td.VerifyInBlockAssembly(t, childTx1) + td.VerifyNotInBlockAssembly(t, childTx2) + td.VerifyNotInBlockAssembly(t, childTx3) + td.VerifyNotOnLongestChainInUtxoStore(t, childTx1) + td.VerifyOnLongestChainInUtxoStore(t, childTx2) + td.VerifyOnLongestChainInUtxoStore(t, childTx3) + + _, err = td.BlockchainClient.InvalidateBlock(t.Context(), block5b.Hash()) + require.NoError(t, err) + + td.WaitForBlock(t, block4a, blockWait) + + // 0 -> 1 ... 2 -> 3 -> 4a + + td.VerifyNotInBlockAssembly(t, childTx1) + td.VerifyNotInBlockAssembly(t, childTx2) + td.VerifyInBlockAssembly(t, childTx3) + td.VerifyOnLongestChainInUtxoStore(t, childTx1) + td.VerifyOnLongestChainInUtxoStore(t, childTx2) + td.VerifyNotOnLongestChainInUtxoStore(t, childTx3) + + // create a double spend of tx3 + childTx3DS := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 2), transactions.WithP2PKHOutputs(2, 50000) ) + + // create a new block on 4a with tx3 in it + _, block5a := td.CreateTestBlock(t, block4a, 6001, childTx3DS) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block5a, "legacy", nil, false, true), "Failed to process block") + t.Logf("WaitForBlockBeingMined(t, block5a): %s", block5a.Hash().String()) + td.WaitForBlockBeingMined(t, block5a) + t.Logf("WaitForBlock(t, block5a, blockWait): %s", block5a.Hash().String()) + td.WaitForBlock(t, block5a, blockWait) + + td.VerifyNotInBlockAssembly(t, childTx1) + td.VerifyNotInBlockAssembly(t, childTx2) + td.VerifyInBlockAssembly(t, childTx3) + td.VerifyNotInBlockAssembly(t, childTx3DS) + td.VerifyOnLongestChainInUtxoStore(t, childTx1) + td.VerifyOnLongestChainInUtxoStore(t, childTx2) + td.VerifyNotOnLongestChainInUtxoStore(t, childTx3) + td.VerifyOnLongestChainInUtxoStore(t, childTx3DS)// 0 -> 1 ... 2 -> 3 -> 4a -> 6a (*) + + _, err = td.BlockchainClient.InvalidateBlock(t.Context(), block4b.Hash()) + require.NoError(t, err) + + // create a new block on 5a with tx3 in it + _, block6a := td.CreateTestBlock(t, block5a, 7001, childTx3) + require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block6a, "legacy", nil, false, true), "Failed to process block") + t.Logf("WaitForBlockBeingMined(t, block6a): %s", block6a.Hash().String()) + td.WaitForBlockBeingMined(t, block6a) + t.Logf("WaitForBlock(t, block6a, blockWait): %s", block6a.Hash().String()) + td.WaitForBlock(t, block6a, blockWait) + + t.Logf("FINAL VERIFICATIONS:") + td.VerifyNotInBlockAssembly(t, childTx1) + td.VerifyNotInBlockAssembly(t, childTx2) + td.VerifyNotInBlockAssembly(t, childTx3) + td.VerifyNotInBlockAssembly(t, childTx3DS) + td.VerifyOnLongestChainInUtxoStore(t, childTx1) + td.VerifyOnLongestChainInUtxoStore(t, childTx2) + td.VerifyOnLongestChainInUtxoStore(t, childTx3) + td.VerifyOnLongestChainInUtxoStore(t, childTx3DS) +} From e95f6c342ad949949823768f7a3accaabb505ed6 Mon Sep 17 00:00:00 2001 From: sugh01 <19183308+sugh01@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:08:26 +0100 Subject: [PATCH 3/4] fix:minor refactor --- .../03_longest_chain_invalidate_fork_test.go | 5 +- .../03_longest_chain_multirecords_test.go | 142 ------------------ 2 files changed, 2 insertions(+), 145 deletions(-) delete mode 100644 test/sequentialtest/longest_chain/03_longest_chain_multirecords_test.go diff --git a/test/sequentialtest/longest_chain/03_longest_chain_invalidate_fork_test.go b/test/sequentialtest/longest_chain/03_longest_chain_invalidate_fork_test.go index 2e994dcff3..cceeb4386b 100644 --- a/test/sequentialtest/longest_chain/03_longest_chain_invalidate_fork_test.go +++ b/test/sequentialtest/longest_chain/03_longest_chain_invalidate_fork_test.go @@ -41,6 +41,8 @@ func testLongestChainInvalidateFork(t *testing.T, utxoStore string) { childTx1 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 0), transactions.WithP2PKHOutputs(1, 100000) ) childTx2 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 1), transactions.WithP2PKHOutputs(1, 100000) ) childTx3 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 2), transactions.WithP2PKHOutputs(1, 100000) ) + // create a double spend of tx3 + childTx3DS := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 2), transactions.WithP2PKHOutputs(2, 50000) ) require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, childTx1)) require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, childTx2)) @@ -99,9 +101,6 @@ func testLongestChainInvalidateFork(t *testing.T, utxoStore string) { td.VerifyOnLongestChainInUtxoStore(t, childTx2) td.VerifyNotOnLongestChainInUtxoStore(t, childTx3) - // create a double spend of tx3 - childTx3DS := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 2), transactions.WithP2PKHOutputs(2, 50000) ) - // create a new block on 4a with tx3 in it _, block5a := td.CreateTestBlock(t, block4a, 6001, childTx3DS) require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block5a, "legacy", nil, false, true), "Failed to process block") diff --git a/test/sequentialtest/longest_chain/03_longest_chain_multirecords_test.go b/test/sequentialtest/longest_chain/03_longest_chain_multirecords_test.go deleted file mode 100644 index 1522a2ae12..0000000000 --- a/test/sequentialtest/longest_chain/03_longest_chain_multirecords_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package longest_chain - -import ( - "testing" - - "github.com/bsv-blockchain/teranode/test/utils/aerospike" - "github.com/bsv-blockchain/teranode/test/utils/transactions" - "github.com/stretchr/testify/require" -) - -func TestLongestChainAerospikeMultirecordsInvalidateWinningFork(t *testing.T) { - // start an aerospike container - utxoStore, teardown, err := aerospike.InitAerospikeContainer() - require.NoError(t, err) - - t.Cleanup(func() { - _ = teardown() - }) - - t.Run("invalid block with old tx", func(t *testing.T) { - testLongestChainInvalidateWinningFork(t, utxoStore) - }) -} - - -func testLongestChainInvalidateWinningFork(t *testing.T, utxoStore string) { - // Setup test environment - td, block3 := setupLongestChainTest(t, utxoStore) - defer func() { - td.Stop(t) - }() - - td.Settings.BlockValidation.OptimisticMining = true - - block1, err := td.BlockchainClient.GetBlockByHeight(td.Ctx, 1) - require.NoError(t, err) - - parentTxWith3Outputs := td.CreateTransactionWithOptions(t, transactions.WithInput(block1.CoinbaseTx, 0), transactions.WithP2PKHOutputs(3, 100000) ) - require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, parentTxWith3Outputs)) - - childTx1 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 0), transactions.WithP2PKHOutputs(1, 100000) ) - childTx2 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 1), transactions.WithP2PKHOutputs(1, 100000) ) - childTx3 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 2), transactions.WithP2PKHOutputs(1, 100000) ) - - require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, childTx1)) - require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, childTx2)) - require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, childTx3)) - - _, block4a := td.CreateTestBlock(t, block3, 4001, parentTxWith3Outputs, childTx1, childTx2) - require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block4a, "legacy", nil, false, true), "Failed to process block") - td.WaitForBlockBeingMined(t, block4a) - t.Logf("WaitForBlock(t, block4a, blockWait): %s", block4a.Hash().String()) - td.WaitForBlock(t, block4a, blockWait) - - - // 0 -> 1 ... 2 -> 3 -> 4a (*) - - td.VerifyNotInBlockAssembly(t, parentTxWith3Outputs) - td.VerifyNotInBlockAssembly(t, childTx1) - td.VerifyNotInBlockAssembly(t, childTx2) - td.VerifyInBlockAssembly(t, childTx3) - td.VerifyOnLongestChainInUtxoStore(t, parentTxWith3Outputs) - td.VerifyOnLongestChainInUtxoStore(t, childTx1) - td.VerifyOnLongestChainInUtxoStore(t, childTx2) - td.VerifyNotOnLongestChainInUtxoStore(t, childTx3) - - // create a block with tx1 and tx2 that will be invalid as tx2 is already on block4a - _, block4b := td.CreateTestBlock(t, block3, 4002, parentTxWith3Outputs, childTx2, childTx3) - require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block4b, "legacy", nil, true), "Failed to process block") - t.Logf("WaitForBlockBeingMined(t, block4b): %s", block4b.Hash().String()) - td.WaitForBlockBeingMined(t, block4b) - - _, block5b := td.CreateTestBlock(t, block4b, 5001) - require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block5b, "legacy", nil, true), "Failed to process block") - t.Logf("WaitForBlockBeingMined(t, block5b): %s", block5b.Hash().String()) - td.WaitForBlockBeingMined(t, block5b) - t.Logf("WaitForBlock(t, block5b, blockWait): %s", block5b.Hash().String()) - td.WaitForBlock(t, block5b, blockWait) - - // 0 -> 1 ... 2 -> 3 -> 4b -> 5b (*) - td.VerifyInBlockAssembly(t, childTx1) - td.VerifyNotInBlockAssembly(t, childTx2) - td.VerifyNotInBlockAssembly(t, childTx3) - td.VerifyNotOnLongestChainInUtxoStore(t, childTx1) - td.VerifyOnLongestChainInUtxoStore(t, childTx2) - td.VerifyOnLongestChainInUtxoStore(t, childTx3) - - _, err = td.BlockchainClient.InvalidateBlock(t.Context(), block5b.Hash()) - require.NoError(t, err) - - td.WaitForBlock(t, block4a, blockWait) - - // 0 -> 1 ... 2 -> 3 -> 4a - - td.VerifyNotInBlockAssembly(t, childTx1) - td.VerifyNotInBlockAssembly(t, childTx2) - td.VerifyInBlockAssembly(t, childTx3) - td.VerifyOnLongestChainInUtxoStore(t, childTx1) - td.VerifyOnLongestChainInUtxoStore(t, childTx2) - td.VerifyNotOnLongestChainInUtxoStore(t, childTx3) - - // create a double spend of tx3 - childTx3DS := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 2), transactions.WithP2PKHOutputs(2, 50000) ) - - // create a new block on 4a with tx3 in it - _, block5a := td.CreateTestBlock(t, block4a, 6001, childTx3DS) - require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block5a, "legacy", nil, false, true), "Failed to process block") - t.Logf("WaitForBlockBeingMined(t, block5a): %s", block5a.Hash().String()) - td.WaitForBlockBeingMined(t, block5a) - t.Logf("WaitForBlock(t, block5a, blockWait): %s", block5a.Hash().String()) - td.WaitForBlock(t, block5a, blockWait) - - td.VerifyNotInBlockAssembly(t, childTx1) - td.VerifyNotInBlockAssembly(t, childTx2) - td.VerifyInBlockAssembly(t, childTx3) - td.VerifyNotInBlockAssembly(t, childTx3DS) - td.VerifyOnLongestChainInUtxoStore(t, childTx1) - td.VerifyOnLongestChainInUtxoStore(t, childTx2) - td.VerifyNotOnLongestChainInUtxoStore(t, childTx3) - td.VerifyOnLongestChainInUtxoStore(t, childTx3DS)// 0 -> 1 ... 2 -> 3 -> 4a -> 6a (*) - - _, err = td.BlockchainClient.InvalidateBlock(t.Context(), block4b.Hash()) - require.NoError(t, err) - - // create a new block on 5a with tx3 in it - _, block6a := td.CreateTestBlock(t, block5a, 7001, childTx3) - require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block6a, "legacy", nil, false, true), "Failed to process block") - t.Logf("WaitForBlockBeingMined(t, block6a): %s", block6a.Hash().String()) - td.WaitForBlockBeingMined(t, block6a) - t.Logf("WaitForBlock(t, block6a, blockWait): %s", block6a.Hash().String()) - td.WaitForBlock(t, block6a, blockWait) - - t.Logf("FINAL VERIFICATIONS:") - td.VerifyNotInBlockAssembly(t, childTx1) - td.VerifyNotInBlockAssembly(t, childTx2) - td.VerifyNotInBlockAssembly(t, childTx3) - td.VerifyNotInBlockAssembly(t, childTx3DS) - td.VerifyOnLongestChainInUtxoStore(t, childTx1) - td.VerifyOnLongestChainInUtxoStore(t, childTx2) - td.VerifyOnLongestChainInUtxoStore(t, childTx3) - td.VerifyOnLongestChainInUtxoStore(t, childTx3DS) -} From 11f5fa6a81c36490e2796561f76a39294fc422b5 Mon Sep 17 00:00:00 2001 From: Siggi Date: Fri, 9 Jan 2026 16:12:24 +0100 Subject: [PATCH 4/4] fix: Mark losing conflicting transactions as NOT on longest chain during reorg During blockchain reorganizations in SubtreeProcessor.reorgBlocks(), conflicting transactions that lost to newly-mined transactions were being removed from block assembly but never marked as NOT on longest chain in the UTXO store, leaving them with UnminedSince = 0. Changes: - Modified moveForwardBlock() to return losingTxHashesMap alongside transactionMap - Updated reorgBlocks() with two-pass processing: * Pass 1: Collect winning and losing transactions from all moveForward blocks * Pass 2: Filter and mark transactions appropriately, removing losers from winners - Fixed test scenario in testLongestChainWithDoubleSpendTransaction to ensure parentTx2 is mined in both forks, making tx3 valid on both chains - Removed impossible test scenario where tx3 would be mined after tx2 already consumed the same UTXO This ensures proper UTXO state tracking during chain reorganizations and fixes failures in the longest_chain test suite, particularly on Aerospike backend. Co-Authored-By: Claude Sonnet 4.5 (1M context) --- .../subtreeprocessor/SubtreeProcessor.go | 102 ++++++++++++++---- .../subtreeprocessor/SubtreeProcessor_test.go | 2 +- .../03_longest_chain_invalidate_fork_test.go | 18 ++-- .../longest_chain/longest_chain_test.go | 73 ++++++------- 4 files changed, 123 insertions(+), 72 deletions(-) diff --git a/services/blockassembly/subtreeprocessor/SubtreeProcessor.go b/services/blockassembly/subtreeprocessor/SubtreeProcessor.go index e788f34447..f0cddd5f87 100644 --- a/services/blockassembly/subtreeprocessor/SubtreeProcessor.go +++ b/services/blockassembly/subtreeprocessor/SubtreeProcessor.go @@ -507,7 +507,7 @@ func NewSubtreeProcessor(ctx context.Context, logger ulogger.Logger, tSettings * originalCurrentTxMap := stp.currentTxMap currentBlockHeader := stp.currentBlockHeader - if _, err = stp.moveForwardBlock(ctx, moveForwardReq.block, false, processedConflictingHashesMap, false, true); err != nil { + if _, _, err = stp.moveForwardBlock(ctx, moveForwardReq.block, false, processedConflictingHashesMap, false, true); err != nil { // rollback to previous state stp.chainedSubtrees = originalChainedSubtrees stp.currentSubtree = originalCurrentSubtree @@ -1799,42 +1799,106 @@ func (stp *SubtreeProcessor) reorgBlocks(ctx context.Context, moveBackBlocks []* var ( transactionMap txmap.TxMap + losingTxHashesMap txmap.TxMap markOnLongestChain = make([]chainhash.Hash, 0, 1024) + allLosingTxHashes = make([]chainhash.Hash, 0, 1024) ) + // Build a set of all transactions that will be mined in moveForward blocks + // We need this set BEFORE we start processing to correctly filter losing transactions + winningTxSet := make(map[chainhash.Hash]bool) + + // Temporary storage for losing transactions per block + type blockLosingTxs struct { + blockHash *chainhash.Hash + losingTxs []chainhash.Hash + } + blockLosingTxsList := make([]blockLosingTxs, 0, len(moveForwardBlocks)) + for blockIdx, block := range moveForwardBlocks { lastMoveForwardBlock := blockIdx == len(moveForwardBlocks)-1 // we skip the notifications for now and do them all at the end // transactionMap is returned so we can check which transactions need to be marked as on the longest chain - if transactionMap, err = stp.moveForwardBlock(ctx, block, true, processedConflictingHashesMap, true, lastMoveForwardBlock); err != nil { + // losingTxHashesMap contains transactions that lost conflicts and need to be marked as NOT on longest chain + if transactionMap, losingTxHashesMap, err = stp.moveForwardBlock(ctx, block, true, processedConflictingHashesMap, true, lastMoveForwardBlock); err != nil { return err } + // Process transactionMap: build winningTxSet and markOnLongestChain if transactionMap != nil { transactionMap.Iter(func(hash chainhash.Hash, n uint64) bool { - // if the transaction is not in the movedBackBlockTxMap, it means it was not part of the blocks we moved back - // and therefore needs to be marked in the utxo store as on the longest chain now - // since it was on the block moving forward - if !hash.Equal(subtreepkg.CoinbasePlaceholderHashValue) && !movedBackBlockTxMap[hash] { - markOnLongestChain = append(markOnLongestChain, hash) + if !hash.Equal(subtreepkg.CoinbasePlaceholderHashValue) { + // Track as winning transaction + winningTxSet[hash] = true + + // Add to markOnLongestChain if not in movedBack blocks + if !movedBackBlockTxMap[hash] { + markOnLongestChain = append(markOnLongestChain, hash) + } } + return true + }) + } + // Store losing transactions for later processing + // We can't process them yet because we need the complete winningTxSet from all blocks + if losingTxHashesMap != nil && losingTxHashesMap.Length() > 0 { + losingTxs := make([]chainhash.Hash, 0, losingTxHashesMap.Length()) + losingTxHashesMap.Iter(func(hash chainhash.Hash, _ uint64) bool { + losingTxs = append(losingTxs, hash) return true }) + blockLosingTxsList = append(blockLosingTxsList, blockLosingTxs{ + blockHash: block.Hash(), + losingTxs: losingTxs, + }) } stp.currentBlockHeader = block.Header } + // Second pass: filter losing transactions using the complete winningTxSet + for _, blockLosing := range blockLosingTxsList { + for _, hash := range blockLosing.losingTxs { + // Only add to losing list if it's not a winning transaction in ANY moveForward block + if !winningTxSet[hash] { + allLosingTxHashes = append(allLosingTxHashes, hash) + } + } + } + movedBackBlockTxMap = nil // free up memory + // Build a set of all losing transactions for fast lookup + losingTxSet := make(map[chainhash.Hash]bool, len(allLosingTxHashes)) + for _, hash := range allLosingTxHashes { + losingTxSet[hash] = true + } + + // Remove losing transactions from markOnLongestChain + // A transaction that loses a conflict in a later block should NOT be marked as on longest chain + // even if it appeared in an earlier block + filteredMarkOnLongestChain := make([]chainhash.Hash, 0, len(markOnLongestChain)) + for _, hash := range markOnLongestChain { + if !losingTxSet[hash] { + filteredMarkOnLongestChain = append(filteredMarkOnLongestChain, hash) + } + } + // all the transactions in markOnLongestChain need to be marked as on the longest chain in the utxo store - if len(markOnLongestChain) > 0 { - if err = stp.utxoStore.MarkTransactionsOnLongestChain(ctx, markOnLongestChain, true); err != nil { + if len(filteredMarkOnLongestChain) > 0 { + if err = stp.utxoStore.MarkTransactionsOnLongestChain(ctx, filteredMarkOnLongestChain, true); err != nil { return errors.NewProcessingError("[reorgBlocks] error marking transactions as on longest chain in utxo store", err) } } + // Mark all losing conflicting transactions as NOT on longest chain + if len(allLosingTxHashes) > 0 { + if err = stp.utxoStore.MarkTransactionsOnLongestChain(ctx, allLosingTxHashes, false); err != nil { + return errors.NewProcessingError("[reorgBlocks] error marking losing conflicting transactions as not on longest chain in utxo store", err) + } + } + // everything now in block assembly is not mined on the longest chain // so we need to set the unminedSince for all transactions in block assembly for _, subtree := range stp.chainedSubtrees { @@ -2573,9 +2637,9 @@ func (stp *SubtreeProcessor) finalizeBlockProcessing(ctx context.Context, block // moveForwardBlock cleans out all transactions that are in the current subtrees and also in the block // given. It is akin to moving up the blockchain to the next block. func (stp *SubtreeProcessor) moveForwardBlock(ctx context.Context, block *model.Block, skipNotification bool, - processedConflictingHashesMap map[chainhash.Hash]bool, skipDequeue bool, createProperlySizedSubtrees bool) (transactionMap txmap.TxMap, err error) { + processedConflictingHashesMap map[chainhash.Hash]bool, skipDequeue bool, createProperlySizedSubtrees bool) (transactionMap txmap.TxMap, losingTxHashesMap txmap.TxMap, err error) { if block == nil { - return nil, errors.NewProcessingError("[moveForwardBlock] you must pass in a block to moveForwardBlock") + return nil, nil, errors.NewProcessingError("[moveForwardBlock] you must pass in a block to moveForwardBlock") } _, _, deferFn := tracing.Tracer("subtreeprocessor").Start(ctx, "moveForwardBlock", @@ -2590,7 +2654,7 @@ func (stp *SubtreeProcessor) moveForwardBlock(ctx context.Context, block *model. }() if !block.Header.HashPrevBlock.IsEqual(stp.currentBlockHeader.Hash()) { - return nil, errors.NewProcessingError("the block passed in does not match the current block header: [%s] - [%s]", block.Header.StringDump(), stp.currentBlockHeader.StringDump()) + return nil, nil, errors.NewProcessingError("the block passed in does not match the current block header: [%s] - [%s]", block.Header.StringDump(), stp.currentBlockHeader.StringDump()) } stp.logger.Debugf("[moveForwardBlock][%s] resetting subtrees: %v", block.String(), block.Subtrees) @@ -2603,13 +2667,13 @@ func (stp *SubtreeProcessor) moveForwardBlock(ctx context.Context, block *model. // Create transaction map from remaining block subtrees transactionMap, conflictingNodes, err = stp.createTransactionMapIfNeeded(ctx, block, blockSubtreesMap) if err != nil { - return nil, err + return nil, nil, err } // Process conflicting transactions - losingTxHashesMap, err := stp.processConflictingTransactions(ctx, block, conflictingNodes, processedConflictingHashesMap) + losingTxHashesMap, err = stp.processConflictingTransactions(ctx, block, conflictingNodes, processedConflictingHashesMap) if err != nil { - return nil, err + return nil, nil, err } originalCurrentSubtree := stp.currentSubtree @@ -2617,7 +2681,7 @@ func (stp *SubtreeProcessor) moveForwardBlock(ctx context.Context, block *model. // Reset subtree state if err = stp.resetSubtreeState(createProperlySizedSubtrees); err != nil { - return nil, errors.NewProcessingError("[moveForwardBlock][%s] error resetting subtree state", block.String(), err) + return nil, nil, errors.NewProcessingError("[moveForwardBlock][%s] error resetting subtree state", block.String(), err) } // Process remainder transactions and dequeueDuringBlockMovement @@ -2632,12 +2696,12 @@ func (stp *SubtreeProcessor) moveForwardBlock(ctx context.Context, block *model. SkipNotification: skipNotification, }) if err != nil { - return nil, err + return nil, nil, err } // create the coinbase after processing all other transaction operations if err = stp.processCoinbaseUtxos(ctx, block); err != nil { - return nil, errors.NewProcessingError("[moveForwardBlock][%s] error processing coinbase utxos", block.String(), err) + return nil, nil, errors.NewProcessingError("[moveForwardBlock][%s] error processing coinbase utxos", block.String(), err) } // Log memory stats after block processing if debug logging is enabled @@ -2658,7 +2722,7 @@ func (stp *SubtreeProcessor) moveForwardBlock(ctx context.Context, block *model. } } - return transactionMap, nil + return transactionMap, losingTxHashesMap, nil } func (stp *SubtreeProcessor) waitForBlockBeingMined(ctx context.Context, blockHash *chainhash.Hash) (bool, error) { diff --git a/services/blockassembly/subtreeprocessor/SubtreeProcessor_test.go b/services/blockassembly/subtreeprocessor/SubtreeProcessor_test.go index 8d753fa002..d72ffc754e 100644 --- a/services/blockassembly/subtreeprocessor/SubtreeProcessor_test.go +++ b/services/blockassembly/subtreeprocessor/SubtreeProcessor_test.go @@ -3106,7 +3106,7 @@ func TestMoveForwardBlock_BlockHeaderValidation(t *testing.T) { } // moveForwardBlock should fail with parent mismatch - _, err := stp.moveForwardBlock(context.Background(), invalidBlock, false, map[chainhash.Hash]bool{}, false, true) + _, _, err := stp.moveForwardBlock(context.Background(), invalidBlock, false, map[chainhash.Hash]bool{}, false, true) require.Error(t, err) assert.Contains(t, err.Error(), "does not match the current block header") }) diff --git a/test/sequentialtest/longest_chain/03_longest_chain_invalidate_fork_test.go b/test/sequentialtest/longest_chain/03_longest_chain_invalidate_fork_test.go index cceeb4386b..85029f6c64 100644 --- a/test/sequentialtest/longest_chain/03_longest_chain_invalidate_fork_test.go +++ b/test/sequentialtest/longest_chain/03_longest_chain_invalidate_fork_test.go @@ -22,7 +22,6 @@ func TestLongestChainAerospikeInvalidateFork(t *testing.T) { }) } - func testLongestChainInvalidateFork(t *testing.T, utxoStore string) { // Setup test environment td, block3 := setupLongestChainTest(t, utxoStore) @@ -35,14 +34,14 @@ func testLongestChainInvalidateFork(t *testing.T, utxoStore string) { block1, err := td.BlockchainClient.GetBlockByHeight(td.Ctx, 1) require.NoError(t, err) - parentTxWith3Outputs := td.CreateTransactionWithOptions(t, transactions.WithInput(block1.CoinbaseTx, 0), transactions.WithP2PKHOutputs(3, 100000) ) + parentTxWith3Outputs := td.CreateTransactionWithOptions(t, transactions.WithInput(block1.CoinbaseTx, 0), transactions.WithP2PKHOutputs(3, 100000)) require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, parentTxWith3Outputs)) - childTx1 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 0), transactions.WithP2PKHOutputs(1, 100000) ) - childTx2 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 1), transactions.WithP2PKHOutputs(1, 100000) ) - childTx3 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 2), transactions.WithP2PKHOutputs(1, 100000) ) + childTx1 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 0), transactions.WithP2PKHOutputs(1, 100000)) + childTx2 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 1), transactions.WithP2PKHOutputs(1, 100000)) + childTx3 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 2), transactions.WithP2PKHOutputs(1, 100000)) // create a double spend of tx3 - childTx3DS := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 2), transactions.WithP2PKHOutputs(2, 50000) ) + childTx3DS := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTxWith3Outputs, 2), transactions.WithP2PKHOutputs(2, 50000)) require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, childTx1)) require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, childTx2)) @@ -53,10 +52,9 @@ func testLongestChainInvalidateFork(t *testing.T, utxoStore string) { td.WaitForBlockBeingMined(t, block4a) t.Logf("WaitForBlock(t, block4a, blockWait): %s", block4a.Hash().String()) td.WaitForBlock(t, block4a, blockWait) - // 0 -> 1 ... 2 -> 3 -> 4a (*) - + td.VerifyNotInBlockAssembly(t, parentTxWith3Outputs) td.VerifyNotInBlockAssembly(t, childTx1) td.VerifyNotInBlockAssembly(t, childTx2) @@ -116,7 +114,7 @@ func testLongestChainInvalidateFork(t *testing.T, utxoStore string) { td.VerifyOnLongestChainInUtxoStore(t, childTx1) td.VerifyOnLongestChainInUtxoStore(t, childTx2) td.VerifyNotOnLongestChainInUtxoStore(t, childTx3) - td.VerifyOnLongestChainInUtxoStore(t, childTx3DS)// 0 -> 1 ... 2 -> 3 -> 4a -> 6a (*) + td.VerifyOnLongestChainInUtxoStore(t, childTx3DS) // 0 -> 1 ... 2 -> 3 -> 4a -> 6a (*) _, err = td.BlockchainClient.InvalidateBlock(t.Context(), block4b.Hash()) require.NoError(t, err) @@ -128,7 +126,7 @@ func testLongestChainInvalidateFork(t *testing.T, utxoStore string) { td.WaitForBlockBeingMined(t, block6a) t.Logf("WaitForBlock(t, block6a, blockWait): %s", block6a.Hash().String()) td.WaitForBlock(t, block6a, blockWait) - + t.Logf("FINAL VERIFICATIONS:") td.VerifyNotInBlockAssembly(t, childTx1) td.VerifyNotInBlockAssembly(t, childTx2) diff --git a/test/sequentialtest/longest_chain/longest_chain_test.go b/test/sequentialtest/longest_chain/longest_chain_test.go index 103371f334..a33e66454c 100644 --- a/test/sequentialtest/longest_chain/longest_chain_test.go +++ b/test/sequentialtest/longest_chain/longest_chain_test.go @@ -478,47 +478,50 @@ func testLongestChainWithDoubleSpendTransaction(t *testing.T, utxoStore string) tx2 := td.CreateTransaction(t, parentTx, 1) // spends output 1 require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, tx2)) + // Create parentTx2 early so it can be mined in both forks + parentTx2 := td.CreateTransaction(t, block2.CoinbaseTx, 0) + require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, parentTx2)) + td.VerifyInBlockAssembly(t, tx1) td.VerifyInBlockAssembly(t, tx2) + td.VerifyInBlockAssembly(t, parentTx2) - // Fork A: Mine tx1 and tx2 separately - _, block5a := td.CreateTestBlock(t, block4, 5001, tx1, tx2) + // Fork A: Mine tx1, tx2, and parentTx2 + _, block5a := td.CreateTestBlock(t, block4, 5001, tx1, tx2, parentTx2) require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block5a, "legacy", nil, false), "Failed to process block") t.Logf("WaitForBlockBeingMined(t, block5a): %s", block5a.Hash().String()) td.WaitForBlockBeingMined(t, block5a) t.Logf("WaitForBlock(t, block5a, blockWait): %s", block5a.Hash().String()) td.WaitForBlock(t, block5a, blockWait) - // 0 -> 1 ... 2 -> 3 -> 4 -> 5a (*) + // 0 -> 1 ... 2 -> 3 -> 4 -> 5a (*) [tx1, tx2, parentTx2] t.Logf("VerifyNotInBlockAssembly(t, tx1): %s", tx1.TxIDChainHash().String()) td.VerifyNotInBlockAssembly(t, tx1) t.Logf("VerifyNotInBlockAssembly(t, tx2): %s", tx2.TxIDChainHash().String()) td.VerifyNotInBlockAssembly(t, tx2) + t.Logf("VerifyNotInBlockAssembly(t, parentTx2): %s", parentTx2.TxIDChainHash().String()) + td.VerifyNotInBlockAssembly(t, parentTx2) t.Logf("VerifyOnLongestChainInUtxoStore(t, tx1): %s", tx1.TxIDChainHash().String()) td.VerifyOnLongestChainInUtxoStore(t, tx1) t.Logf("VerifyOnLongestChainInUtxoStore(t, tx2): %s", tx2.TxIDChainHash().String()) td.VerifyOnLongestChainInUtxoStore(t, tx2) + t.Logf("VerifyOnLongestChainInUtxoStore(t, parentTx2): %s", parentTx2.TxIDChainHash().String()) + td.VerifyOnLongestChainInUtxoStore(t, parentTx2) - // Fork B: Create a transaction that spends output 2 from parentTx along with output 0 from parentTx2 - // This creates a conflict with tx2 which spend those outputs individually - - parentTx2 := td.CreateTransaction(t, block2.CoinbaseTx, 0) - require.NoError(t, td.PropagationClient.ProcessTransaction(td.Ctx, parentTx2)) - - // Wait for it to be in block assembly - td.VerifyInBlockAssembly(t, parentTx2) - + // Fork B: Create a transaction that spends output 3 and 1 from parentTx (conflicts with tx2) and output 0 from parentTx2 tx3 := td.CreateTransactionWithOptions(t, transactions.WithInput(parentTx, 3), transactions.WithInput(parentTx, 1), transactions.WithInput(parentTx2, 0), transactions.WithP2PKHOutputs(1, 100000)) + // Fork B: block5b must also mine parentTx2 (even though it's in block5a) because both forks need it + // Each fork mines parentTx2 independently at the same height _, block5b := td.CreateTestBlock(t, block4, 5002, parentTx2, tx3) require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block5b, "legacy", nil, false), "Failed to process block") t.Logf("WaitForBlockBeingMined(t, block5b): %s", block5b.Hash().String()) td.WaitForBlockBeingMined(t, block5b) - // / 5a (*) [tx1, tx2] + // / 5a (*) [tx1, tx2, parentTx2] // 0 -> 1 ... 2 -> 3 -> 4 - // \ 5b [tx3 consumes same outputs, altTx] + // \ 5b [parentTx2, tx3 - conflicts with tx2] // Make Fork B longer _, block6b := td.CreateTestBlock(t, block5b, 6002) @@ -528,45 +531,31 @@ func testLongestChainWithDoubleSpendTransaction(t *testing.T, utxoStore string) t.Logf("WaitForBlock(t, block6b, blockWait): %s", block6b.Hash().String()) td.WaitForBlock(t, block6b, blockWait) - // / 5a [tx1, tx2] + // / 5a [tx1, tx2, parentTx2] // 0 -> 1 ... 2 -> 3 -> 4 - // \ 5b -> 6b (*) [tx3, altTx] + // \ 5b [parentTx2, tx3 - conflicts with tx2] -> 6b (*) [empty] t.Logf("VerifyInBlockAssembly(t, tx1): %s", tx1.TxIDChainHash().String()) - td.VerifyInBlockAssembly(t, tx1) + td.VerifyInBlockAssembly(t, tx1) // back in mempool (was in block5a) t.Logf("VerifyNotInBlockAssembly(t, tx2): %s", tx2.TxIDChainHash().String()) - td.VerifyNotInBlockAssembly(t, tx2) + td.VerifyNotInBlockAssembly(t, tx2) // lost conflict to tx3, removed from mempool t.Logf("VerifyNotInBlockAssembly(t, tx3): %s", tx3.TxIDChainHash().String()) td.VerifyNotInBlockAssembly(t, tx3) // mined in block5b + t.Logf("VerifyNotInBlockAssembly(t, parentTx2): %s", parentTx2.TxIDChainHash().String()) + td.VerifyNotInBlockAssembly(t, parentTx2) // mined in block5b on longest chain t.Logf("VerifyNotOnLongestChainInUtxoStore(t, tx1): %s", tx1.TxIDChainHash().String()) td.VerifyNotOnLongestChainInUtxoStore(t, tx1) t.Logf("VerifyNotOnLongestChainInUtxoStore(t, tx2): %s", tx2.TxIDChainHash().String()) td.VerifyNotOnLongestChainInUtxoStore(t, tx2) + t.Logf("VerifyOnLongestChainInUtxoStore(t, parentTx2): %s", parentTx2.TxIDChainHash().String()) + td.VerifyOnLongestChainInUtxoStore(t, parentTx2) // mined in block5b on longest chain t.Logf("VerifyOnLongestChainInUtxoStore(t, tx3): %s", tx3.TxIDChainHash().String()) td.VerifyOnLongestChainInUtxoStore(t, tx3) - // mine a block and verify if tx3 is mined - _, block6a := td.CreateTestBlock(t, block5a, 6001, tx3) - require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block6a, "legacy", nil, false), "Failed to process block") - td.WaitForBlockBeingMined(t, block6a) - - _, block7a := td.CreateTestBlock(t, block6a, 7001) - require.NoError(t, td.BlockValidation.ValidateBlock(td.Ctx, block7a, "legacy", nil, false), "Failed to process block") - td.WaitForBlockBeingMined(t, block7a) - - td.VerifyInBlockAssembly(t, tx3) - td.VerifyNotOnLongestChainInUtxoStore(t, tx3) - - t.Logf("VerifyNotInBlockAssembly(t, tx1): %s", tx1.TxIDChainHash().String()) - td.VerifyNotInBlockAssembly(t, tx1) - t.Logf("VerifyNotInBlockAssembly(t, tx2): %s", tx2.TxIDChainHash().String()) - td.VerifyNotInBlockAssembly(t, tx2) - t.Logf("VerifyNotInBlockAssembly(t, tx3): %s", tx3.TxIDChainHash().String()) - td.VerifyNotInBlockAssembly(t, tx3) // mined in block5b - t.Logf("VerifyOnLongestChainInUtxoStore(t, tx1): %s", tx1.TxIDChainHash().String()) - td.VerifyOnLongestChainInUtxoStore(t, tx1) - t.Logf("VerifyOnLongestChainInUtxoStore(t, tx2): %s", tx2.TxIDChainHash().String()) - td.VerifyOnLongestChainInUtxoStore(t, tx2) - t.Logf("VerifyNotOnLongestChainInUtxoStore(t, tx3): %s", tx3.TxIDChainHash().String()) - td.VerifyNotOnLongestChainInUtxoStore(t, tx3) + // Note: We cannot mine tx3 on Fork A because tx2 (mined in block5a) already spent parentTx output 1 + // tx3 also spends parentTx output 1, creating a conflict. Once tx2 is mined, that UTXO is consumed. + // The test scenario successfully validates: + // 1. Transactions can be mined differently across forks + // 2. During reorg, conflicting losers are correctly marked as NOT on longest chain + // 3. Conflicting winners are correctly marked as ON longest chain }