go-ethereum/eth/txtracker/tracker_test.go
Csaba Kiraly 8bfddee2ea eth: add tests for txtracker and dropper peer protection
txtracker tests (7 tests):
- NotifyReceived: stats empty before chain events
- InclusionEMA: EMA increases on inclusion, decays on empty blocks
- Finalization: Finalized counter credited after finalization
- MultiplePeers: each peer credited for own txs only
- FirstDelivererWins: duplicate delivery ignored
- NoFinalizationCredit: no credit without finalization
- EMADecay: EMA approaches zero after 30 empty blocks

dropper tests (6 tests):
- FilterProtectedNoStats: nil stats → all droppable
- FilterProtectedEmptyStats: empty map → all droppable
- FilterProtectedTopPeer: top-scored peers removed from droppable
- FilterProtectedZeroScore: zero scores → no protection
- FilterProtectedOverlap: peer top in both categories → counted once
- FilterProtectedAllProtected: all droppable protected → empty list

Also fix: create peer entries during EMA update for peers with
inclusions in the current block (previously only created during
finalization, so EMA was not tracked before first finalization).
2026-04-10 09:07:38 +02:00

262 lines
6.2 KiB
Go

package txtracker
import (
"math/big"
"sync"
"testing"
"time"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/trie"
)
// mockChain implements the Chain interface for testing.
type mockChain struct {
mu sync.Mutex
headFeed event.Feed
blocks map[uint64]*types.Block
finalNum uint64
}
func newMockChain() *mockChain {
return &mockChain{blocks: make(map[uint64]*types.Block)}
}
func (c *mockChain) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription {
return c.headFeed.Subscribe(ch)
}
func (c *mockChain) GetBlockByNumber(number uint64) *types.Block {
c.mu.Lock()
defer c.mu.Unlock()
return c.blocks[number]
}
func (c *mockChain) CurrentFinalBlock() *types.Header {
c.mu.Lock()
defer c.mu.Unlock()
if c.finalNum == 0 {
return nil
}
return &types.Header{Number: new(big.Int).SetUint64(c.finalNum)}
}
func (c *mockChain) addBlock(num uint64, txs []*types.Transaction) {
c.mu.Lock()
defer c.mu.Unlock()
header := &types.Header{Number: new(big.Int).SetUint64(num)}
c.blocks[num] = types.NewBlock(header, &types.Body{Transactions: txs}, nil, trie.NewListHasher())
}
func (c *mockChain) setFinalBlock(num uint64) {
c.mu.Lock()
defer c.mu.Unlock()
c.finalNum = num
}
func (c *mockChain) sendHead(num uint64) {
c.headFeed.Send(core.ChainHeadEvent{
Header: &types.Header{Number: new(big.Int).SetUint64(num)},
})
}
func makeTx(nonce uint64) *types.Transaction {
return types.NewTx(&types.LegacyTx{Nonce: nonce, GasPrice: big.NewInt(1), Gas: 21000})
}
// waitForHead gives the tracker time to process a chain head event.
func waitForHead() {
time.Sleep(50 * time.Millisecond)
}
func TestNotifyReceived(t *testing.T) {
tr := New()
chain := newMockChain()
tr.Start(chain)
defer tr.Stop()
txs := []*types.Transaction{makeTx(1), makeTx(2), makeTx(3)}
tr.NotifyReceived("peerA", txs)
// No chain events yet — stats should be empty.
stats := tr.GetAllPeerStats()
if len(stats) != 0 {
t.Fatalf("expected empty stats before any chain events, got %d peers", len(stats))
}
}
func TestInclusionEMA(t *testing.T) {
tr := New()
chain := newMockChain()
tr.Start(chain)
defer tr.Stop()
tx := makeTx(1)
tr.NotifyReceived("peerA", []*types.Transaction{tx})
// Block 1 includes peerA's tx.
chain.addBlock(1, []*types.Transaction{tx})
chain.sendHead(1)
waitForHead()
stats := tr.GetAllPeerStats()
if stats["peerA"].RecentIncluded <= 0 {
t.Fatalf("expected RecentIncluded > 0 after inclusion, got %f", stats["peerA"].RecentIncluded)
}
ema1 := stats["peerA"].RecentIncluded
// Block 2 has no txs from peerA — EMA should decay.
chain.addBlock(2, nil)
chain.sendHead(2)
waitForHead()
stats = tr.GetAllPeerStats()
if stats["peerA"].RecentIncluded >= ema1 {
t.Fatalf("expected EMA to decay, got %f >= %f", stats["peerA"].RecentIncluded, ema1)
}
}
func TestFinalization(t *testing.T) {
tr := New()
chain := newMockChain()
tr.Start(chain)
defer tr.Stop()
tx := makeTx(1)
tr.NotifyReceived("peerA", []*types.Transaction{tx})
// Include in block 1.
chain.addBlock(1, []*types.Transaction{tx})
chain.sendHead(1)
waitForHead()
// Not finalized yet.
stats := tr.GetAllPeerStats()
if stats["peerA"].Finalized != 0 {
t.Fatalf("expected Finalized=0 before finalization, got %d", stats["peerA"].Finalized)
}
// Finalize block 1, then send head 2 to trigger checkFinalization.
chain.setFinalBlock(1)
chain.addBlock(2, nil)
chain.sendHead(2)
waitForHead()
stats = tr.GetAllPeerStats()
if stats["peerA"].Finalized != 1 {
t.Fatalf("expected Finalized=1 after finalization, got %d", stats["peerA"].Finalized)
}
}
func TestMultiplePeers(t *testing.T) {
tr := New()
chain := newMockChain()
tr.Start(chain)
defer tr.Stop()
tx1 := makeTx(1)
tx2 := makeTx(2)
tr.NotifyReceived("peerA", []*types.Transaction{tx1})
tr.NotifyReceived("peerB", []*types.Transaction{tx2})
// Both included in block 1.
chain.addBlock(1, []*types.Transaction{tx1, tx2})
chain.sendHead(1)
waitForHead()
// Finalize.
chain.setFinalBlock(1)
chain.addBlock(2, nil)
chain.sendHead(2)
waitForHead()
stats := tr.GetAllPeerStats()
if stats["peerA"].Finalized != 1 {
t.Fatalf("peerA: expected Finalized=1, got %d", stats["peerA"].Finalized)
}
if stats["peerB"].Finalized != 1 {
t.Fatalf("peerB: expected Finalized=1, got %d", stats["peerB"].Finalized)
}
}
func TestFirstDelivererWins(t *testing.T) {
tr := New()
chain := newMockChain()
tr.Start(chain)
defer tr.Stop()
tx := makeTx(1)
tr.NotifyReceived("peerA", []*types.Transaction{tx})
tr.NotifyReceived("peerB", []*types.Transaction{tx}) // duplicate, should be ignored
chain.addBlock(1, []*types.Transaction{tx})
chain.sendHead(1)
waitForHead()
chain.setFinalBlock(1)
chain.addBlock(2, nil)
chain.sendHead(2)
waitForHead()
stats := tr.GetAllPeerStats()
if stats["peerA"].Finalized != 1 {
t.Fatalf("peerA should be credited, got Finalized=%d", stats["peerA"].Finalized)
}
if stats["peerB"].Finalized != 0 {
t.Fatalf("peerB should NOT be credited, got Finalized=%d", stats["peerB"].Finalized)
}
}
func TestNoFinalizationCredit(t *testing.T) {
tr := New()
chain := newMockChain()
tr.Start(chain)
defer tr.Stop()
tx := makeTx(1)
tr.NotifyReceived("peerA", []*types.Transaction{tx})
// Include but don't finalize.
chain.addBlock(1, []*types.Transaction{tx})
chain.sendHead(1)
waitForHead()
// Send more heads without finalization.
chain.addBlock(2, nil)
chain.sendHead(2)
waitForHead()
stats := tr.GetAllPeerStats()
if stats["peerA"].Finalized != 0 {
t.Fatalf("expected Finalized=0 without finalization, got %d", stats["peerA"].Finalized)
}
}
func TestEMADecay(t *testing.T) {
tr := New()
chain := newMockChain()
tr.Start(chain)
defer tr.Stop()
tx := makeTx(1)
tr.NotifyReceived("peerA", []*types.Transaction{tx})
// Include in block 1.
chain.addBlock(1, []*types.Transaction{tx})
chain.sendHead(1)
waitForHead()
// Send 30 empty blocks — EMA should decay close to zero.
for i := uint64(2); i <= 31; i++ {
chain.addBlock(i, nil)
chain.sendHead(i)
waitForHead()
}
stats := tr.GetAllPeerStats()
if stats["peerA"].RecentIncluded > 0.02 {
t.Fatalf("expected RecentIncluded near zero after 30 empty blocks, got %f", stats["peerA"].RecentIncluded)
}
}