mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-24 08:49:29 +00:00
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).
This commit is contained in:
parent
58556173f6
commit
8bfddee2ea
3 changed files with 393 additions and 0 deletions
125
eth/dropper_test.go
Normal file
125
eth/dropper_test.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
package eth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
)
|
||||
|
||||
func makePeers(n int) []*p2p.Peer {
|
||||
peers := make([]*p2p.Peer, n)
|
||||
for i := range peers {
|
||||
id := enode.ID{byte(i)}
|
||||
peers[i] = p2p.NewPeer(id, fmt.Sprintf("peer%d", i), nil)
|
||||
}
|
||||
return peers
|
||||
}
|
||||
|
||||
func TestFilterProtectedNoStats(t *testing.T) {
|
||||
// When the stats func returns nil/empty, all peers remain droppable.
|
||||
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
|
||||
cm.peerStatsFunc = func() map[string]PeerInclusionStats { return nil }
|
||||
|
||||
peers := makePeers(10)
|
||||
result := cm.filterProtectedPeers(peers)
|
||||
if len(result) != len(peers) {
|
||||
t.Fatalf("expected all peers droppable with nil stats, got %d/%d", len(result), len(peers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterProtectedEmptyStats(t *testing.T) {
|
||||
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
|
||||
cm.peerStatsFunc = func() map[string]PeerInclusionStats {
|
||||
return map[string]PeerInclusionStats{}
|
||||
}
|
||||
|
||||
peers := makePeers(10)
|
||||
result := cm.filterProtectedPeers(peers)
|
||||
if len(result) != len(peers) {
|
||||
t.Fatalf("expected all peers droppable with empty stats, got %d/%d", len(result), len(peers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterProtectedTopPeer(t *testing.T) {
|
||||
// 20 peers, maxDialPeers=20, 10% = 2 protected per category.
|
||||
// NewPeer creates non-inbound peers, so all go to dialed bucket.
|
||||
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
|
||||
|
||||
peers := makePeers(20)
|
||||
stats := make(map[string]PeerInclusionStats)
|
||||
// Peer 0: top by Finalized
|
||||
stats[peers[0].ID().String()] = PeerInclusionStats{Finalized: 100}
|
||||
// Peer 1: top by RecentIncluded
|
||||
stats[peers[1].ID().String()] = PeerInclusionStats{RecentIncluded: 5.0}
|
||||
|
||||
cm.peerStatsFunc = func() map[string]PeerInclusionStats { return stats }
|
||||
|
||||
result := cm.filterProtectedPeers(peers)
|
||||
// 2 categories × 2 protected each = up to 4, but peers 0 and 1 are
|
||||
// different so both should be removed. Other peers have zero scores.
|
||||
protected := len(peers) - len(result)
|
||||
if protected != 2 {
|
||||
t.Fatalf("expected 2 protected peers, got %d", protected)
|
||||
}
|
||||
// Verify peers 0 and 1 are not in result.
|
||||
for _, p := range result {
|
||||
id := p.ID().String()
|
||||
if id == peers[0].ID().String() || id == peers[1].ID().String() {
|
||||
t.Fatalf("peer %s should be protected", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterProtectedZeroScore(t *testing.T) {
|
||||
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
|
||||
|
||||
peers := makePeers(10)
|
||||
stats := make(map[string]PeerInclusionStats)
|
||||
// All peers have zero stats.
|
||||
for _, p := range peers {
|
||||
stats[p.ID().String()] = PeerInclusionStats{}
|
||||
}
|
||||
cm.peerStatsFunc = func() map[string]PeerInclusionStats { return stats }
|
||||
|
||||
result := cm.filterProtectedPeers(peers)
|
||||
if len(result) != len(peers) {
|
||||
t.Fatalf("expected no protection with zero scores, got %d protected", len(peers)-len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterProtectedOverlap(t *testing.T) {
|
||||
// One peer is top in both categories — should only be removed once.
|
||||
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
|
||||
|
||||
peers := makePeers(20)
|
||||
stats := make(map[string]PeerInclusionStats)
|
||||
// Peer 0 is top in both.
|
||||
stats[peers[0].ID().String()] = PeerInclusionStats{Finalized: 100, RecentIncluded: 5.0}
|
||||
|
||||
cm.peerStatsFunc = func() map[string]PeerInclusionStats { return stats }
|
||||
|
||||
result := cm.filterProtectedPeers(peers)
|
||||
protected := len(peers) - len(result)
|
||||
if protected != 1 {
|
||||
t.Fatalf("expected 1 protected peer (overlap), got %d", protected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterProtectedAllProtected(t *testing.T) {
|
||||
// Only 2 droppable peers, both are top by different categories.
|
||||
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
|
||||
|
||||
peers := makePeers(2)
|
||||
stats := make(map[string]PeerInclusionStats)
|
||||
stats[peers[0].ID().String()] = PeerInclusionStats{Finalized: 100}
|
||||
stats[peers[1].ID().String()] = PeerInclusionStats{RecentIncluded: 5.0}
|
||||
|
||||
cm.peerStatsFunc = func() map[string]PeerInclusionStats { return stats }
|
||||
|
||||
result := cm.filterProtectedPeers(peers)
|
||||
if len(result) != 0 {
|
||||
t.Fatalf("expected all peers protected, got %d droppable", len(result))
|
||||
}
|
||||
}
|
||||
|
|
@ -156,6 +156,12 @@ func (t *Tracker) handleChainHead(ev core.ChainHeadEvent) {
|
|||
blockIncl[peer]++
|
||||
}
|
||||
}
|
||||
// Ensure peers with inclusions in this block have entries.
|
||||
for peer := range blockIncl {
|
||||
if t.peers[peer] == nil {
|
||||
t.peers[peer] = &peerStats{}
|
||||
}
|
||||
}
|
||||
// Update EMA for all tracked peers (decay inactive ones).
|
||||
for peer, ps := range t.peers {
|
||||
ps.recentIncluded = (1-emaAlpha)*ps.recentIncluded + emaAlpha*float64(blockIncl[peer])
|
||||
|
|
|
|||
262
eth/txtracker/tracker_test.go
Normal file
262
eth/txtracker/tracker_test.go
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue