diff --git a/daemon/test_daemon.go b/daemon/test_daemon.go index ca4bb2acac..2827066ba7 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/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..cceeb4386b --- /dev/null +++ b/test/sequentialtest/longest_chain/03_longest_chain_invalidate_fork_test.go @@ -0,0 +1,141 @@ +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) ) + // 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)) + 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 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/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) +}