go-ethereum/eth/txtracker/tracker.go
Csaba Kiraly 9f2575efeb eth/txtracker: add minimal tracker as inclusion stats provider
Minimal txtracker that records which peer delivered each transaction
and credits peers when their transactions appear on chain. Provides
the PeerInclusionStats needed by the dropper's protection logic.

Design:
- NotifyReceived(peer, txs): records deliverer per tx hash (called
  from handler_eth.go when tx bodies arrive via P2P)
- Subscribes to ChainHeadEvent, fetches block txs, credits the
  delivering peer for each included tx
- Per-peer EMA of recent inclusions (alpha=0.05), updated every block
- LRU eviction at 262K entries to bound memory
- Mutex-based (not channel-based) for simplicity — the hot path
  (NotifyReceived) is a fast map insert

Wired into the dropper via an adapter callback in backend.go that
converts txtracker.PeerStats to the dropper's PeerInclusionStats.
2026-04-10 08:30:21 +02:00

165 lines
4 KiB
Go

// Package txtracker provides minimal per-peer transaction inclusion tracking.
// It records which peer delivered each transaction and credits peers when
// their delivered transactions are included on chain.
package txtracker
import (
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
)
const (
// Maximum number of tx→deliverer mappings to retain.
maxTracked = 262144
// EMA smoothing factor for per-block inclusion rate.
emaAlpha = 0.05
)
// PeerStats holds the per-peer inclusion data.
type PeerStats struct {
Included int64 // Cumulative on-chain inclusions attributed to this peer
RecentIncluded float64 // EMA of per-block inclusions
}
// Chain is the blockchain interface needed by the tracker.
type Chain interface {
SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription
GetBlockByNumber(number uint64) *types.Block
}
type peerStats struct {
included int64
recentIncluded float64
}
// Tracker records which peer delivered each transaction and credits peers
// when their transactions appear on chain.
type Tracker struct {
mu sync.Mutex
txs map[common.Hash]string // hash → deliverer peer ID
peers map[string]*peerStats
order []common.Hash // insertion order for LRU eviction
chain Chain
headCh chan core.ChainHeadEvent
sub event.Subscription
quit chan struct{}
wg sync.WaitGroup
}
// New creates a new tracker.
func New() *Tracker {
return &Tracker{
txs: make(map[common.Hash]string),
peers: make(map[string]*peerStats),
quit: make(chan struct{}),
}
}
// Start begins listening for chain head events.
func (t *Tracker) Start(chain Chain) {
t.chain = chain
t.headCh = make(chan core.ChainHeadEvent, 128)
t.sub = chain.SubscribeChainHeadEvent(t.headCh)
t.wg.Add(1)
go t.loop()
}
// Stop shuts down the tracker.
func (t *Tracker) Stop() {
t.sub.Unsubscribe()
close(t.quit)
t.wg.Wait()
}
// NotifyReceived records that a peer delivered transaction bodies.
// Safe to call from any goroutine.
func (t *Tracker) NotifyReceived(peer string, txs []*types.Transaction) {
t.mu.Lock()
defer t.mu.Unlock()
for _, tx := range txs {
hash := tx.Hash()
if _, ok := t.txs[hash]; ok {
continue // already tracked, keep first deliverer
}
t.txs[hash] = peer
t.order = append(t.order, hash)
}
// Evict oldest entries if over capacity.
for len(t.txs) > maxTracked {
oldest := t.order[0]
t.order = t.order[1:]
delete(t.txs, oldest)
}
}
// GetAllPeerStats returns a snapshot of per-peer inclusion statistics.
// Safe to call from any goroutine.
func (t *Tracker) GetAllPeerStats() map[string]PeerStats {
t.mu.Lock()
defer t.mu.Unlock()
result := make(map[string]PeerStats, len(t.peers))
for id, ps := range t.peers {
result[id] = PeerStats{
Included: ps.included,
RecentIncluded: ps.recentIncluded,
}
}
return result
}
func (t *Tracker) loop() {
defer t.wg.Done()
for {
select {
case ev := <-t.headCh:
t.handleChainHead(ev)
case <-t.sub.Err():
return
case <-t.quit:
return
}
}
}
func (t *Tracker) handleChainHead(ev core.ChainHeadEvent) {
block := t.chain.GetBlockByNumber(ev.Header.Number.Uint64())
if block == nil {
return
}
t.mu.Lock()
defer t.mu.Unlock()
// Credit delivering peers for each included transaction.
blockIncl := make(map[string]int)
for _, tx := range block.Transactions() {
hash := tx.Hash()
peer, ok := t.txs[hash]
if !ok || peer == "" {
continue
}
ps := t.peers[peer]
if ps == nil {
ps = &peerStats{}
t.peers[peer] = ps
}
ps.included++
blockIncl[peer]++
}
// Update per-peer recent-inclusion EMA for all tracked peers.
for peer, ps := range t.peers {
ps.recentIncluded = (1-emaAlpha)*ps.recentIncluded + emaAlpha*float64(blockIncl[peer])
}
if len(blockIncl) > 0 {
log.Trace("Credited peers for block inclusions", "block", ev.Header.Number, "peers", len(blockIncl))
}
}