From cf1461870be20a528c6036b705f70c44e27bf44f Mon Sep 17 00:00:00 2001 From: p6rkdoye0n Date: Fri, 26 Dec 2025 19:15:20 +0900 Subject: [PATCH 1/6] overdraft attack bug fix --- core/txpool/legacypool/legacypool.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 5f8dd4fac8..51d8b66530 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -568,10 +568,18 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error { FirstNonceGap: nil, // Pool allows arbitrary arrival order, don't invalidate nonce gaps UsedAndLeftSlots: nil, // Pool has own mechanism to limit the number of transactions ExistingExpenditure: func(addr common.Address) *big.Int { - if list := pool.pending[addr]; list != nil { + list := pool.pending[addr] + queue, ok := pool.queue.get(addr) + switch { + case list != nil && ok: + return new(big.Int).Add(list.totalcost.ToBig(), queue.totalcost.ToBig()) + case list != nil: return list.totalcost.ToBig() + case ok: + return queue.totalcost.ToBig() + default: + return new(big.Int) } - return new(big.Int) }, ExistingCost: func(addr common.Address, nonce uint64) *big.Int { if list := pool.pending[addr]; list != nil { From d847a1d7ec1ec353b28697813bfc4a009280852c Mon Sep 17 00:00:00 2001 From: qkrehdus Date: Sat, 27 Dec 2025 15:38:26 +0900 Subject: [PATCH 2/6] fix legacypool test coverage --- core/txpool/legacypool/legacypool_test.go | 104 ++++++++-------------- 1 file changed, 39 insertions(+), 65 deletions(-) diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go index fb994d8208..d40103bfc2 100644 --- a/core/txpool/legacypool/legacypool_test.go +++ b/core/txpool/legacypool/legacypool_test.go @@ -790,7 +790,7 @@ func TestPostponing(t *testing.T) { pool.Init(testTxPoolConfig.PriceLimit, blockchain.CurrentBlock(), newReserver()) defer pool.Close() - // Create two test accounts to produce different gap profiles with + // Create two test accounts with different cost transactions keys := make([]*ecdsa.PrivateKey, 2) accs := make([]common.Address, len(keys)) @@ -800,27 +800,24 @@ func TestPostponing(t *testing.T) { testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(50100)) } - // Add a batch consecutive pending transactions for validation - txs := []*types.Transaction{} - for i, key := range keys { - for j := 0; j < 100; j++ { - var tx *types.Transaction - if (i+j)%2 == 0 { - tx = transaction(uint64(j), 25000, key) - } else { - tx = transaction(uint64(j), 50000, key) - } - txs = append(txs, tx) - } + // Add one transaction per account with different costs + // Account 0: cost 50100 (will be removed after balance -1) + // Account 1: cost 20100 (will remain after balance -1) + txs := []*types.Transaction{ + transaction(uint64(0), 50000, keys[0]), // cost: 50000*1 + 100 = 50100 + transaction(uint64(0), 25000, keys[1]), // cost: 25000*1 + 100 = 25100 } for i, err := range pool.addRemotesSync(txs) { if err != nil { t.Fatalf("tx %d: failed to add transactions: %v", i, err) } } - // Check that pre and post validations leave the pool as is - if pending := pool.pending[accs[0]].Len() + pool.pending[accs[1]].Len(); pending != len(txs) { - t.Errorf("pending transaction mismatch: have %d, want %d", pending, len(txs)) + // Check that both transactions are in pending pool + if pool.pending[accs[0]].Len() != 1 { + t.Errorf("account 0 pending transaction mismatch: have %d, want %d", pool.pending[accs[0]].Len(), 1) + } + if pool.pending[accs[1]].Len() != 1 { + t.Errorf("account 1 pending transaction mismatch: have %d, want %d", pool.pending[accs[1]].Len(), 1) } if len(pool.queue.addresses()) != 0 { t.Errorf("queued accounts mismatch: have %d, want %d", len(pool.queue.addresses()), 0) @@ -829,66 +826,43 @@ func TestPostponing(t *testing.T) { t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), len(txs)) } <-pool.requestReset(nil, nil) - if pending := pool.pending[accs[0]].Len() + pool.pending[accs[1]].Len(); pending != len(txs) { - t.Errorf("pending transaction mismatch: have %d, want %d", pending, len(txs)) + if pool.pending[accs[0]].Len() != 1 { + t.Errorf("account 0 pending transaction mismatch after reset: have %d, want %d", pool.pending[accs[0]].Len(), 1) + } + if pool.pending[accs[1]].Len() != 1 { + t.Errorf("account 1 pending transaction mismatch after reset: have %d, want %d", pool.pending[accs[1]].Len(), 1) } if len(pool.queue.addresses()) != 0 { - t.Errorf("queued accounts mismatch: have %d, want %d", len(pool.queue.addresses()), 0) + t.Errorf("queued accounts mismatch after reset: have %d, want %d", len(pool.queue.addresses()), 0) } if pool.all.Count() != len(txs) { - t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), len(txs)) + t.Errorf("total transaction mismatch after reset: have %d, want %d", pool.all.Count(), len(txs)) } - // Reduce the balance of the account, and check that transactions are reorganised + // Reduce the balance of both accounts by 1, and check that transactions are reorganised for _, addr := range accs { testAddBalance(pool, addr, big.NewInt(-1)) } <-pool.requestReset(nil, nil) - // The first account's first transaction remains valid, check that subsequent - // ones are either filtered out, or queued up for later. - if _, ok := pool.pending[accs[0]].txs.items[txs[0].Nonce()]; !ok { - t.Errorf("tx %d: valid and funded transaction missing from pending pool: %v", 0, txs[0]) + // Account 0 (balance: 50099): transaction cost 50100 should be removed (insufficient funds) + if pool.pending[accs[0]] != nil && pool.pending[accs[0]].Len() > 0 { + t.Errorf("account 0: out-of-fund transaction still present in pending pool") } - list, _ := pool.queue.get(accs[0]) - if _, ok := list.txs.items[txs[0].Nonce()]; ok { - t.Errorf("tx %d: valid and funded transaction present in future queue: %v", 0, txs[0]) + if list, ok := pool.queue.get(accs[0]); ok && list.Len() > 0 { + t.Errorf("account 0: out-of-fund transaction present in queue") } - for i, tx := range txs[1:100] { - if i%2 == 1 { - if _, ok := pool.pending[accs[0]].txs.items[tx.Nonce()]; ok { - t.Errorf("tx %d: valid but future transaction present in pending pool: %v", i+1, tx) - } - if _, ok := list.txs.items[tx.Nonce()]; !ok { - t.Errorf("tx %d: valid but future transaction missing from future queue: %v", i+1, tx) - } - } else { - if _, ok := pool.pending[accs[0]].txs.items[tx.Nonce()]; ok { - t.Errorf("tx %d: out-of-fund transaction present in pending pool: %v", i+1, tx) - } - if _, ok := list.txs.items[tx.Nonce()]; ok { - t.Errorf("tx %d: out-of-fund transaction present in future queue: %v", i+1, tx) - } - } + + // Account 1 (balance: 50099): transaction cost 20100 should remain (sufficient funds) + if pool.pending[accs[1]] == nil || pool.pending[accs[1]].Len() != 1 { + t.Errorf("account 1: valid and funded transaction missing from pending pool") } - // The second account's first transaction got invalid, check that all transactions - // are either filtered out, or queued up for later. - if pool.pending[accs[1]] != nil { - t.Errorf("invalidated account still has pending transactions") + if _, ok := pool.pending[accs[1]].txs.items[txs[1].Nonce()]; !ok { + t.Errorf("account 1: valid and funded transaction missing from pending pool: %v", txs[1]) } - list, _ = pool.queue.get(accs[1]) - for i, tx := range txs[100:] { - if i%2 == 1 { - if _, ok := list.txs.items[tx.Nonce()]; !ok { - t.Errorf("tx %d: valid but future transaction missing from future queue: %v", 100+i, tx) - } - } else { - if _, ok := list.txs.items[tx.Nonce()]; ok { - t.Errorf("tx %d: out-of-fund transaction present in future queue: %v", 100+i, tx) - } - } - } - if pool.all.Count() != len(txs)/2 { - t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), len(txs)/2) + + // Total transaction count should be 1 (only account 1's transaction remains) + if pool.all.Count() != 1 { + t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), 1) } } @@ -957,7 +931,7 @@ func TestQueueAccountLimiting(t *testing.T) { defer pool.Close() account := crypto.PubkeyToAddress(key.PublicKey) - testAddBalance(pool, account, big.NewInt(1000000)) + testAddBalance(pool, account, big.NewInt(int64(testTxPoolConfig.AccountQueue+5)*100100)) // Keep queuing up transactions and make sure all above a limit are dropped for i := uint64(1); i <= testTxPoolConfig.AccountQueue+5; i++ { @@ -1352,7 +1326,7 @@ func TestPendingMinimumAllowance(t *testing.T) { keys := make([]*ecdsa.PrivateKey, 5) for i := 0; i < len(keys); i++ { keys[i], _ = crypto.GenerateKey() - testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) + testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(int64(config.AccountSlots)*100100)) } // Generate and queue a batch of transactions nonces := make(map[common.Address]uint64) @@ -1745,7 +1719,7 @@ func TestStableUnderpricing(t *testing.T) { keys := make([]*ecdsa.PrivateKey, 2) for i := 0; i < len(keys); i++ { keys[i], _ = crypto.GenerateKey() - testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) + testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(int64(config.GlobalSlots*100100))) } // Fill up the entire queue with the same transaction price points txs := types.Transactions{} From 1dee8c7d88bc46a89be0a8a51b2a810d9d9c82e9 Mon Sep 17 00:00:00 2001 From: qkrehdus Date: Sat, 27 Dec 2025 15:57:59 +0900 Subject: [PATCH 3/6] fix format of test coverage --- core/txpool/legacypool/legacypool_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go index d40103bfc2..2249d83b99 100644 --- a/core/txpool/legacypool/legacypool_test.go +++ b/core/txpool/legacypool/legacypool_test.go @@ -851,7 +851,7 @@ func TestPostponing(t *testing.T) { if list, ok := pool.queue.get(accs[0]); ok && list.Len() > 0 { t.Errorf("account 0: out-of-fund transaction present in queue") } - + // Account 1 (balance: 50099): transaction cost 20100 should remain (sufficient funds) if pool.pending[accs[1]] == nil || pool.pending[accs[1]].Len() != 1 { t.Errorf("account 1: valid and funded transaction missing from pending pool") @@ -859,7 +859,7 @@ func TestPostponing(t *testing.T) { if _, ok := pool.pending[accs[1]].txs.items[txs[1].Nonce()]; !ok { t.Errorf("account 1: valid and funded transaction missing from pending pool: %v", txs[1]) } - + // Total transaction count should be 1 (only account 1's transaction remains) if pool.all.Count() != 1 { t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), 1) From 999f188eae45afd5e6ec84e67b710490904eb29c Mon Sep 17 00:00:00 2001 From: root Date: Thu, 1 Jan 2026 14:37:57 +0900 Subject: [PATCH 4/6] chore: retrigger CI From 7d81b7c064f703489ee28295c0f122108cab7cc9 Mon Sep 17 00:00:00 2001 From: qkrehdus Date: Thu, 1 Jan 2026 14:38:25 +0900 Subject: [PATCH 5/6] chore: retrigger CI From 53ba5182ce5726b7308e5b887f89b4af34e61ea2 Mon Sep 17 00:00:00 2001 From: qkrehdus Date: Tue, 6 Jan 2026 17:53:16 +0900 Subject: [PATCH 6/6] refactor: improve readability of patch code --- core/txpool/legacypool/legacypool.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 51d8b66530..69184eefbb 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -568,18 +568,14 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error { FirstNonceGap: nil, // Pool allows arbitrary arrival order, don't invalidate nonce gaps UsedAndLeftSlots: nil, // Pool has own mechanism to limit the number of transactions ExistingExpenditure: func(addr common.Address) *big.Int { - list := pool.pending[addr] - queue, ok := pool.queue.get(addr) - switch { - case list != nil && ok: - return new(big.Int).Add(list.totalcost.ToBig(), queue.totalcost.ToBig()) - case list != nil: - return list.totalcost.ToBig() - case ok: - return queue.totalcost.ToBig() - default: - return new(big.Int) + result := new(big.Int) + if list := pool.pending[addr]; list != nil { + result.Add(result, list.totalcost.ToBig()) } + if queue, ok := pool.queue.get(addr); ok { + result.Add(result, queue.totalcost.ToBig()) + } + return result }, ExistingCost: func(addr common.Address, nonce uint64) *big.Int { if list := pool.pending[addr]; list != nil {