|
| 1 | +package subtreevalidation |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "testing" |
| 6 | + |
| 7 | + "github.com/bsv-blockchain/go-bt/v2" |
| 8 | + "github.com/stretchr/testify/require" |
| 9 | +) |
| 10 | + |
| 11 | +// TestPrepareTxsPerLevel_ExternalParents tests that transactions with parents |
| 12 | +// outside the subtree are not incorrectly assigned to level 0. |
| 13 | +// |
| 14 | +// This test demonstrates the bug where transactions with external parents |
| 15 | +// (parents not in the current subtree) are assigned level 0, causing them |
| 16 | +// to be validated before their parents are available, resulting in TX_NOT_FOUND errors. |
| 17 | +func TestPrepareTxsPerLevel_ExternalParents(t *testing.T) { |
| 18 | + t.Run("transaction with external parent should not be level 0", func(t *testing.T) { |
| 19 | + s := &Server{} |
| 20 | + |
| 21 | + // Create a simple chain: |
| 22 | + // parentTx (external, not in subtree) -> childTx (in subtree) |
| 23 | + parentTx := tx1.Clone() |
| 24 | + childTx := bt.NewTx() |
| 25 | + |
| 26 | + // Make childTx spend from parentTx (which is NOT in the subtree) |
| 27 | + err := childTx.FromUTXOs(&bt.UTXO{ |
| 28 | + TxIDHash: parentTx.TxIDChainHash(), |
| 29 | + Vout: 0, |
| 30 | + LockingScript: parentTx.Outputs[0].LockingScript, |
| 31 | + Satoshis: parentTx.Outputs[0].Satoshis, |
| 32 | + }) |
| 33 | + require.NoError(t, err) |
| 34 | + |
| 35 | + err = childTx.AddP2PKHOutputFromScript(parentTx.Outputs[0].LockingScript, 1000) |
| 36 | + require.NoError(t, err) |
| 37 | + |
| 38 | + // Create transactions slice with ONLY childTx (parentTx is external) |
| 39 | + transactions := []missingTx{ |
| 40 | + { |
| 41 | + tx: childTx, |
| 42 | + idx: 0, |
| 43 | + }, |
| 44 | + } |
| 45 | + |
| 46 | + // Call prepareTxsPerLevel |
| 47 | + maxLevel, txsPerLevel, err := s.prepareTxsPerLevel(context.Background(), transactions) |
| 48 | + require.NoError(t, err) |
| 49 | + |
| 50 | + // BUG DEMONSTRATION: Currently, childTx is assigned to level 0 because its parent |
| 51 | + // is not in the subtree, so len(dependencies[childTx]) == 0 |
| 52 | + // |
| 53 | + // This is INCORRECT because childTx has a parent (just not in the subtree). |
| 54 | + // When childTx is validated at level 0, it will fail with TX_NOT_FOUND |
| 55 | + // because parentTx is not available yet. |
| 56 | + |
| 57 | + // Verify the current buggy behaviour exists |
| 58 | + currentMaxLevel := maxLevel |
| 59 | + currentLevel0Count := len(txsPerLevel[0]) |
| 60 | + |
| 61 | + // Document what currently happens (will pass, showing the bug exists) |
| 62 | + t.Logf("Current buggy behaviour: maxLevel=%d, level0_count=%d", currentMaxLevel, currentLevel0Count) |
| 63 | + |
| 64 | + // This assertion will FAIL when the bug is fixed |
| 65 | + // The bug is that transactions with external parents are assigned to level 0 |
| 66 | + if currentMaxLevel == 0 && currentLevel0Count == 1 { |
| 67 | + t.Fatal("BUG CONFIRMED: Transaction with external parent is incorrectly assigned to level 0. " + |
| 68 | + "This will cause TX_NOT_FOUND errors during validation because the parent transaction " + |
| 69 | + "is not in the subtree and won't be available when this transaction is validated.") |
| 70 | + } |
| 71 | + }) |
| 72 | + |
| 73 | + t.Run("mixed transactions with internal and external parents", func(t *testing.T) { |
| 74 | + s := &Server{} |
| 75 | + |
| 76 | + // Create a more complex scenario: |
| 77 | + // externalParent (not in subtree) -> tx1 (in subtree) -> tx2 (in subtree) |
| 78 | + externalParent := parentTx1.Clone() |
| 79 | + tx1InSubtree := bt.NewTx() |
| 80 | + tx2InSubtree := bt.NewTx() |
| 81 | + |
| 82 | + // tx1 spends from external parent |
| 83 | + err := tx1InSubtree.FromUTXOs(&bt.UTXO{ |
| 84 | + TxIDHash: externalParent.TxIDChainHash(), |
| 85 | + Vout: 0, |
| 86 | + LockingScript: externalParent.Outputs[0].LockingScript, |
| 87 | + Satoshis: externalParent.Outputs[0].Satoshis, |
| 88 | + }) |
| 89 | + require.NoError(t, err) |
| 90 | + |
| 91 | + err = tx1InSubtree.AddP2PKHOutputFromScript(externalParent.Outputs[0].LockingScript, 5000) |
| 92 | + require.NoError(t, err) |
| 93 | + |
| 94 | + // tx2 spends from tx1 (both in subtree) |
| 95 | + err = tx2InSubtree.FromUTXOs(&bt.UTXO{ |
| 96 | + TxIDHash: tx1InSubtree.TxIDChainHash(), |
| 97 | + Vout: 0, |
| 98 | + LockingScript: tx1InSubtree.Outputs[0].LockingScript, |
| 99 | + Satoshis: tx1InSubtree.Outputs[0].Satoshis, |
| 100 | + }) |
| 101 | + require.NoError(t, err) |
| 102 | + |
| 103 | + err = tx2InSubtree.AddP2PKHOutputFromScript(tx1InSubtree.Outputs[0].LockingScript, 4000) |
| 104 | + require.NoError(t, err) |
| 105 | + |
| 106 | + // Only tx1 and tx2 are in the subtree (externalParent is not) |
| 107 | + transactions := []missingTx{ |
| 108 | + { |
| 109 | + tx: tx1InSubtree, |
| 110 | + idx: 0, |
| 111 | + }, |
| 112 | + { |
| 113 | + tx: tx2InSubtree, |
| 114 | + idx: 1, |
| 115 | + }, |
| 116 | + } |
| 117 | + |
| 118 | + maxLevel, txsPerLevel, err := s.prepareTxsPerLevel(context.Background(), transactions) |
| 119 | + require.NoError(t, err) |
| 120 | + |
| 121 | + // Find which level each tx is at |
| 122 | + tx1Level := -1 |
| 123 | + tx2Level := -1 |
| 124 | + |
| 125 | + for level, txs := range txsPerLevel { |
| 126 | + for _, mtx := range txs { |
| 127 | + if mtx.tx.TxID() == tx1InSubtree.TxID() { |
| 128 | + tx1Level = level |
| 129 | + } |
| 130 | + if mtx.tx.TxID() == tx2InSubtree.TxID() { |
| 131 | + tx2Level = level |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + t.Logf("Current behaviour: tx1_level=%d, tx2_level=%d, maxLevel=%d", tx1Level, tx2Level, maxLevel) |
| 137 | + |
| 138 | + // BUG CONFIRMED: tx1 is at level 0 even though it has an external parent |
| 139 | + // When tx1 is validated at level 0, it will try to look up externalParent |
| 140 | + // and fail with TX_NOT_FOUND, exactly like the error in the bug report. |
| 141 | + if tx1Level == 0 && tx2Level == 1 { |
| 142 | + t.Fatal("BUG CONFIRMED: tx1 is at level 0 despite having an external parent. " + |
| 143 | + "This causes TX_NOT_FOUND when trying to validate tx1 before its parent is available. " + |
| 144 | + "tx2 is correctly at level 1 (depends on tx1 which is in subtree).") |
| 145 | + } |
| 146 | + }) |
| 147 | + |
| 148 | + t.Run("all transactions have external parents", func(t *testing.T) { |
| 149 | + s := &Server{} |
| 150 | + |
| 151 | + // Scenario: Multiple independent transactions, all with external parents |
| 152 | + // None of them depend on each other, but all depend on external txs |
| 153 | + externalParent1 := parentTx1.Clone() |
| 154 | + externalParent2 := tx1.Clone() |
| 155 | + |
| 156 | + childTx1 := bt.NewTx() |
| 157 | + childTx2 := bt.NewTx() |
| 158 | + |
| 159 | + // childTx1 spends from externalParent1 (not in subtree) |
| 160 | + err := childTx1.FromUTXOs(&bt.UTXO{ |
| 161 | + TxIDHash: externalParent1.TxIDChainHash(), |
| 162 | + Vout: 0, |
| 163 | + LockingScript: externalParent1.Outputs[0].LockingScript, |
| 164 | + Satoshis: externalParent1.Outputs[0].Satoshis, |
| 165 | + }) |
| 166 | + require.NoError(t, err) |
| 167 | + |
| 168 | + err = childTx1.AddP2PKHOutputFromScript(externalParent1.Outputs[0].LockingScript, 3000) |
| 169 | + require.NoError(t, err) |
| 170 | + |
| 171 | + // childTx2 spends from externalParent2 (not in subtree) |
| 172 | + err = childTx2.FromUTXOs(&bt.UTXO{ |
| 173 | + TxIDHash: externalParent2.TxIDChainHash(), |
| 174 | + Vout: 0, |
| 175 | + LockingScript: externalParent2.Outputs[0].LockingScript, |
| 176 | + Satoshis: externalParent2.Outputs[0].Satoshis, |
| 177 | + }) |
| 178 | + require.NoError(t, err) |
| 179 | + |
| 180 | + err = childTx2.AddP2PKHOutputFromScript(externalParent2.Outputs[0].LockingScript, 2000) |
| 181 | + require.NoError(t, err) |
| 182 | + |
| 183 | + // Both child transactions are in the subtree, but their parents are not |
| 184 | + transactions := []missingTx{ |
| 185 | + { |
| 186 | + tx: childTx1, |
| 187 | + idx: 0, |
| 188 | + }, |
| 189 | + { |
| 190 | + tx: childTx2, |
| 191 | + idx: 1, |
| 192 | + }, |
| 193 | + } |
| 194 | + |
| 195 | + maxLevel, txsPerLevel, err := s.prepareTxsPerLevel(context.Background(), transactions) |
| 196 | + require.NoError(t, err) |
| 197 | + |
| 198 | + t.Logf("Current behaviour: maxLevel=%d, level0_count=%d", maxLevel, len(txsPerLevel[0])) |
| 199 | + |
| 200 | + // BUG CONFIRMED: Both transactions are assigned to level 0 |
| 201 | + // This is problematic because when these transactions are validated, |
| 202 | + // they will all fail with TX_NOT_FOUND errors for their parent transactions. |
| 203 | + if maxLevel == 0 && len(txsPerLevel[0]) == 2 { |
| 204 | + t.Fatal("BUG CONFIRMED: Both transactions are at level 0 despite having external parents. " + |
| 205 | + "During validation, both will fail with TX_NOT_FOUND when trying to look up their " + |
| 206 | + "parent transactions that are not in the subtree.") |
| 207 | + } |
| 208 | + }) |
| 209 | +} |
0 commit comments