Commit graph

13 commits

Author SHA1 Message Date
Csaba Kiraly
e8083ed0f7 eth: fix lint issues after rebase
Three files had goimports drift from resolving rebase conflicts
(eth/dropper_test.go, eth/fetcher/tx_fetcher.go, eth/handler.go) —
re-run goimports.

Also remove an unused mockConsumer.count() helper in
eth/txtracker/tracker_test.go that no test calls. The method was
left in during the peerstats split and never needed.
2026-04-20 10:01:49 +02:00
Csaba Kiraly
b6b6345be9 eth: rename NotifyRequestLatency to NotifyRequestResult, track success/timeout counts
Replace NotifyRequestLatency(peer, latency) with
NotifyRequestResult(peer, latency, timeout). The new timeout bool
tells peerstats whether the request was answered or timed out.

Per-peer RequestSuccesses and RequestTimeouts counters replace the
single RequestSamples field — any two of the three are derivable, so
we keep the two primary counters and derive the total
(successes + timeouts) where needed (e.g. the MinLatencySamples
guard in the dropper).

The latency EMA continues to use the timeout value for timed-out
requests, penalizing slow peers as before. The success/timeout
counters are exposed as statistics only — no protection category
uses them yet.
2026-04-20 09:30:52 +02:00
Csaba Kiraly
89222edba9 eth/peerstats: gate latency protection on sample freshness
The request-latency category scores peers by the reciprocal of their
RequestLatencyEMA, but that EMA is only updated by NotifyRequestLatency
— which only fires when the tx fetcher sends a request to the peer.
A peer can serve a burst of fast replies to build a strong EMA, stop
announcing transactions so we never request from them again, and
retain latency protection indefinitely with a frozen score.

Record LastLatencySample (wall-clock time) per peer alongside the EMA
update. In the dropper's scoring function, return 0 when the last
sample is older than MaxLatencyStaleness (10 minutes). Fresh samples
reset the clock, so peers that resume activity become eligible again.

Timestamps rather than block counts: real-time is what we actually
care about (10 minutes idle), not a block count that varies with
chain pace, and the EMA itself is a time.Duration so measuring
staleness in the same domain stays consistent.

Tests cover the timestamp update on NotifyRequestLatency, the timestamp
advancing on successive samples, and the dropper rejecting a stale
peer whose EMA and sample count would otherwise qualify.
2026-04-20 09:30:52 +02:00
Csaba Kiraly
b178ec9a4a eth/peerstats: bump MinLatencySamples from 10 to 100
Require substantially more samples before a peer's request-latency EMA
becomes eligible for protection. A 10-sample floor was too low: a peer
hitting 10 fast replies in a short burst could earn protection before
the slow alpha=0.01 EMA had moved meaningfully away from the bootstrap
value. At ~70-sample EMA half-life, a 100-sample floor means the EMA
has been refined through several half-lives before it can affect
dropping decisions.

Updates the dropper tests that previously used RequestSamples=50 to
use peerstats.MinLatencySamples so they stay robust to future value
changes. Design notes and a test comment reference the new value.
2026-04-20 09:30:52 +02:00
Csaba Kiraly
06c5ce8372 eth/peerstats: split peer quality aggregation out of txtracker
Introduces a new eth/peerstats package as the single home for per-peer
quality metrics consumed by the dropper. txtracker shrinks to a pure
tx-lifecycle role: it maps tx hash to deliverer, subscribes to chain
heads, computes per-block per-peer inclusion and finalization deltas,
and emits them to a StatsConsumer.

peerstats owns the aggregates: inclusion EMA, finalized counter,
latency EMA, sample counter, and the MinLatencySamples bootstrap
constant the dropper uses to filter under-sampled peers. It's a
plain struct with a mutex — no goroutine of its own, no lifecycle
management. The fetcher's onRequestLatency callback now flows to
peerStats.NotifyRequestLatency, the handler's unregisterPeer cleans
up via peerStats.NotifyPeerDrop, and the dropper reads its snapshot
via peerStats.GetAllPeerStats.

txtracker.handleChainHead computes deltas under its own lock, then
releases the lock before calling the consumer, which avoids any
cross-package lock ordering. Tests are split along the same line:
tracker tests use a mock consumer to assert what signals are emitted,
peerstats tests cover EMA math and aggregation semantics directly.
2026-04-20 09:30:52 +02:00
Csaba Kiraly
c82be6827f eth: add request-latency peer protection category
Adds a third protection category to the dropper, scoring peers by
per-peer tx-request response latency. Fast peers are harder to drop;
peers that chronically time out (their EMA drifts toward the 5s
timeout sample) score low and are normal drop candidates.

PeerInclusionStats gains RequestLatencyEMA (time.Duration) and
RequestSamples (int64). The stats adapter in backend.go copies them
from txtracker.PeerStats. The scoring function returns 1/EMA once
the peer has >= MinLatencySamples (10) recorded samples — an
under-sampled peer scores 0 and is filtered by the existing
"score <= 0" rule, preventing a single lucky-fast reply from
displacing established peers.

Adds three unit tests via protectedPeersByPool for the basic
top-N selection, the bootstrap guard, and per-pool independence.
2026-04-20 09:07:04 +02:00
Csaba Kiraly
f24161de71 eth/txtracker: replace cumulative Finalized with slow RecentFinalized EMA
The total-finalized protection category ranked peers by a monotonic
cumulative count, so a peer that had been productive in the past kept
a high score forever — even if they had since gone silent — and held
a protected slot without contributing.

Replace txtracker.PeerStats.Finalized (int64 cumulative) with
RecentFinalized (float64 EMA). On each chain head, finalization
credits accumulated over the newly-finalized range are folded into a
slow EMA (alpha=0.0001, half-life ~6930 blocks ≈ 23 hours on 12s
mainnet blocks). Peers that continue contributing keep a high score;
peers that stop decay toward zero over roughly a day.

The dropper category renames to "recent-finalized" accordingly. The
type's docstring is rewritten to describe both categories as EMAs
with different time horizons (slow finalized, fast included).

Refactors checkFinalization to return a per-peer credits map rather
than mutating state directly, so both EMAs update in the same loop
over tracked peers.
2026-04-19 12:14:23 +02:00
Csaba Kiraly
1f2ebc5d59 eth: drop PeerInclusionStats wrapper and use txtracker.PeerStats directly
PeerInclusionStats was declared identically to txtracker.PeerStats as a
decoupling abstraction: any stats provider could implement the dropper's
callback by returning this shape. In practice there's one provider and
the two types were kept in sync by a rote copy adapter in backend.go.

Delete PeerInclusionStats, have the dropper consume txtracker.PeerStats
directly via getPeerStatsFunc. backend.go now passes
txTracker.GetAllPeerStats as the callback with no adapter.

If a second stats provider ever appears, the abstraction can come back;
until then, one fewer type and 8 fewer lines of ceremony.
2026-04-15 14:35:37 +02:00
Csaba Kiraly
a7ce1e2ad8 eth: test per-pool top-N selection in dropper peer protection
The protection feature promises top-N per inbound/dialed pool, but
every existing test constructed peers via p2p.NewPeer (which produces
no-flag peers), so all test peers landed in the dialed pool and the
per-pool split was never validated.

Extract the selection logic from protectedPeers into a pure helper
protectedPeersByPool(inbound, dialed, stats) that accepts pre-split
pools. This sidesteps the unexported p2p.connFlag types and makes the
interesting behavior directly testable. Add three tests covering:

  - exact top-N selected independently in each pool
  - cross-category union with overlap deduplication
  - per-pool independence: top dialed peers stay protected even when
    every inbound peer scores higher globally
2026-04-13 16:56:53 +02:00
Csaba Kiraly
1c518be79f eth: simplify peer protection — compute protected set upfront
Compute the protected peer set once in dropRandomPeer via
protectedPeers(), then include protection as a condition in
selectDoNotDrop alongside trusted/static/recent checks. This
eliminates the separate filterProtectedPeers post-pass and the
awkward "all protected → skip" branch.

Rename filterProtectedPeers to protectedPeers, returning
map[*p2p.Peer]bool instead of filtering a slice. The map is
checked directly in selectDoNotDrop via protected[p].
2026-04-10 12:18:56 +02:00
Csaba Kiraly
f66323d768 eth: add LGPL copyright headers to new files
Add the standard go-ethereum LGPL header to tracker.go,
tracker_test.go, and dropper_test.go.
2026-04-10 12:18:56 +02:00
Csaba Kiraly
44c8a5b7f4 eth: base protection quota on current peer count, not max capacity
protectTopN used maxPeers (configured capacity) to compute the
number of peers to protect. With small droppable sets this could
protect everyone, permanently disabling churn.

Use len(entries) (current droppable count in each category) instead.
With 20 droppable dialed peers and 10% fraction, 2 are protected.
With 3 droppable peers, 0 are protected — churn is never blocked.
2026-04-10 10:36:59 +02:00
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