mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-24 08:49:29 +00:00
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.
293 lines
10 KiB
Go
293 lines
10 KiB
Go
// Copyright 2026 The go-ethereum Authors
|
||
// This file is part of the go-ethereum library.
|
||
//
|
||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||
// it under the terms of the GNU Lesser General Public License as published by
|
||
// the Free Software Foundation, either version 3 of the License, or
|
||
// (at your option) any later version.
|
||
//
|
||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
// GNU Lesser General Public License for more details.
|
||
//
|
||
// You should have received a copy of the GNU Lesser General Public License
|
||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||
|
||
package peerstats
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// TestNotifyBlockBootstrapsFromInclusions verifies that a peer with a positive
|
||
// inclusion count in the first NotifyBlock gets a stats entry created.
|
||
func TestNotifyBlockBootstrapsFromInclusions(t *testing.T) {
|
||
s := New()
|
||
s.NotifyBlock(map[string]int{"peerA": 3}, nil)
|
||
|
||
stats := s.GetAllPeerStats()
|
||
if len(stats) != 1 {
|
||
t.Fatalf("expected 1 peer entry, got %d", len(stats))
|
||
}
|
||
ps, ok := stats["peerA"]
|
||
if !ok {
|
||
t.Fatal("expected peerA entry")
|
||
}
|
||
// EMA after first block: (1-0.05)*0 + 0.05*3 = 0.15
|
||
if ps.RecentIncluded <= 0 {
|
||
t.Fatalf("expected RecentIncluded > 0 after inclusion, got %f", ps.RecentIncluded)
|
||
}
|
||
}
|
||
|
||
// TestNotifyBlockDecaysKnownPeers verifies that peers already tracked get their
|
||
// RecentIncluded EMA decayed when they have no inclusions in a block.
|
||
func TestNotifyBlockDecaysKnownPeers(t *testing.T) {
|
||
s := New()
|
||
// Seed peerA with an inclusion.
|
||
s.NotifyBlock(map[string]int{"peerA": 3}, nil)
|
||
initial := s.GetAllPeerStats()["peerA"].RecentIncluded
|
||
|
||
// Empty block — peerA should decay.
|
||
s.NotifyBlock(nil, nil)
|
||
after := s.GetAllPeerStats()["peerA"].RecentIncluded
|
||
|
||
if after >= initial {
|
||
t.Fatalf("expected decay, got %f >= %f", after, initial)
|
||
}
|
||
}
|
||
|
||
// TestNotifyBlockDoesNotResurrectDroppedPeers verifies that finalization
|
||
// credits to a peer with no entry don't create one.
|
||
func TestNotifyBlockDoesNotResurrectFromFinalization(t *testing.T) {
|
||
s := New()
|
||
s.NotifyBlock(nil, map[string]int{"peerA": 5})
|
||
|
||
if stats := s.GetAllPeerStats(); len(stats) != 0 {
|
||
t.Fatalf("finalization credits must not create entries, got %d peers", len(stats))
|
||
}
|
||
}
|
||
|
||
// TestNotifyBlockDropThenFinalizeNoResurrect verifies the full drop→finalize
|
||
// sequence: a dropped peer doesn't come back via finalization credits.
|
||
func TestNotifyBlockDropThenFinalizeNoResurrect(t *testing.T) {
|
||
s := New()
|
||
s.NotifyBlock(map[string]int{"peerA": 1}, nil)
|
||
s.NotifyPeerDrop("peerA")
|
||
s.NotifyBlock(nil, map[string]int{"peerA": 10})
|
||
|
||
if stats := s.GetAllPeerStats(); len(stats) != 0 {
|
||
t.Fatalf("dropped peer must not be resurrected, got %d peers", len(stats))
|
||
}
|
||
}
|
||
|
||
// TestNotifyBlockFinalizationCredits an existing peer.
|
||
func TestNotifyBlockFinalizationCredits(t *testing.T) {
|
||
s := New()
|
||
s.NotifyBlock(map[string]int{"peerA": 1}, nil)
|
||
s.NotifyBlock(nil, map[string]int{"peerA": 3})
|
||
|
||
// RecentFinalized is a slow EMA, not a cumulative count: assert it
|
||
// moved in the positive direction, not the exact value.
|
||
if got := s.GetAllPeerStats()["peerA"].RecentFinalized; got <= 0 {
|
||
t.Fatalf("expected RecentFinalized>0 after credits, got %f", got)
|
||
}
|
||
}
|
||
|
||
// TestNotifyBlockInclusionEMAUpdate verifies the EMA formula (1-α)·old + α·count.
|
||
func TestNotifyBlockInclusionEMAUpdate(t *testing.T) {
|
||
s := New()
|
||
// Three inclusions: EMA = 0.05 * 3 = 0.15
|
||
s.NotifyBlock(map[string]int{"peerA": 3}, nil)
|
||
got := s.GetAllPeerStats()["peerA"].RecentIncluded
|
||
want := 0.15
|
||
if diff := got - want; diff < -1e-9 || diff > 1e-9 {
|
||
t.Fatalf("EMA after one sample: got %f, want %f", got, want)
|
||
}
|
||
// Next block with 10 inclusions: EMA = 0.95*0.15 + 0.05*10 = 0.6425
|
||
s.NotifyBlock(map[string]int{"peerA": 10}, nil)
|
||
got = s.GetAllPeerStats()["peerA"].RecentIncluded
|
||
want = 0.6425
|
||
if diff := got - want; diff < -1e-9 || diff > 1e-9 {
|
||
t.Fatalf("EMA after two samples: got %f, want %f", got, want)
|
||
}
|
||
}
|
||
|
||
// TestNotifyRequestResultFirstSampleBootstrap asserts that the first
|
||
// latency sample seeds the EMA directly.
|
||
func TestNotifyRequestResultFirstSampleBootstrap(t *testing.T) {
|
||
s := New()
|
||
s.NotifyRequestResult("peerA", 200*time.Millisecond, false)
|
||
|
||
ps := s.GetAllPeerStats()["peerA"]
|
||
if ps.RequestLatencyEMA != 200*time.Millisecond {
|
||
t.Fatalf("expected first sample to seed EMA at 200ms, got %v", ps.RequestLatencyEMA)
|
||
}
|
||
if ps.RequestSuccesses != 1 {
|
||
t.Fatalf("expected RequestSuccesses=1, got %d", ps.RequestSuccesses)
|
||
}
|
||
if ps.RequestTimeouts != 0 {
|
||
t.Fatalf("expected RequestTimeouts=0, got %d", ps.RequestTimeouts)
|
||
}
|
||
}
|
||
|
||
// TestNotifyRequestResultEMAUpdate verifies the EMA formula for latency.
|
||
func TestNotifyRequestResultEMAUpdate(t *testing.T) {
|
||
s := New()
|
||
s.NotifyRequestResult("peerA", 100*time.Millisecond, false)
|
||
s.NotifyRequestResult("peerA", 1000*time.Millisecond, false)
|
||
|
||
// Expected: 0.99*100ms + 0.01*1000ms = 109ms
|
||
got := s.GetAllPeerStats()["peerA"].RequestLatencyEMA
|
||
want := 109 * time.Millisecond
|
||
delta := got - want
|
||
if delta < 0 {
|
||
delta = -delta
|
||
}
|
||
if delta > 1*time.Microsecond {
|
||
t.Fatalf("EMA mismatch: got %v, want %v", got, want)
|
||
}
|
||
ps := s.GetAllPeerStats()["peerA"]
|
||
if ps.RequestSuccesses != 2 {
|
||
t.Fatalf("expected RequestSuccesses=2, got %d", ps.RequestSuccesses)
|
||
}
|
||
}
|
||
|
||
// TestNotifyRequestResultSlowConvergence verifies the slow alpha
|
||
// damps convergence under sustained timeouts.
|
||
func TestNotifyRequestResultSlowConvergence(t *testing.T) {
|
||
s := New()
|
||
s.NotifyRequestResult("peerA", 100*time.Millisecond, false)
|
||
for i := 0; i < 50; i++ {
|
||
s.NotifyRequestResult("peerA", 5*time.Second, false)
|
||
}
|
||
got := s.GetAllPeerStats()["peerA"].RequestLatencyEMA
|
||
if got < 1*time.Second {
|
||
t.Fatalf("EMA did not move enough under sustained timeouts, got %v", got)
|
||
}
|
||
if got > 3*time.Second {
|
||
t.Fatalf("EMA converged too fast for slow alpha=0.01, got %v", got)
|
||
}
|
||
}
|
||
|
||
// TestNotifyPeerDropClearsStats verifies that a dropped peer disappears
|
||
// from GetAllPeerStats.
|
||
func TestNotifyPeerDropClearsStats(t *testing.T) {
|
||
s := New()
|
||
s.NotifyRequestResult("peerA", 200*time.Millisecond, false)
|
||
s.NotifyPeerDrop("peerA")
|
||
|
||
if _, ok := s.GetAllPeerStats()["peerA"]; ok {
|
||
t.Fatal("peerA stats should be removed after NotifyPeerDrop")
|
||
}
|
||
}
|
||
|
||
// TestStaleRequestLatencyAfterDrop documents the accepted behavior: a
|
||
// late sample after NotifyPeerDrop recreates a 1-sample entry. The
|
||
// dropper's MinLatencySamples=100 guard ensures this is harmless.
|
||
func TestStaleRequestLatencyAfterDrop(t *testing.T) {
|
||
s := New()
|
||
s.NotifyRequestResult("peerA", 200*time.Millisecond, false)
|
||
s.NotifyPeerDrop("peerA")
|
||
// Late sample racing with the drop.
|
||
s.NotifyRequestResult("peerA", 50*time.Millisecond, false)
|
||
|
||
ps := s.GetAllPeerStats()["peerA"]
|
||
if ps.RequestSuccesses != 1 {
|
||
t.Fatalf("expected fresh RequestSuccesses=1, got %d", ps.RequestSuccesses)
|
||
}
|
||
if ps.RequestLatencyEMA != 50*time.Millisecond {
|
||
t.Fatalf("expected fresh bootstrap at 50ms, got %v", ps.RequestLatencyEMA)
|
||
}
|
||
// The dropper's MinLatencySamples guard (in eth/dropper.go) prevents
|
||
// this 1-sample entry from earning latency-based protection.
|
||
}
|
||
|
||
// TestMultiplePeersIsolated verifies per-peer isolation across signal types.
|
||
func TestMultiplePeersIsolated(t *testing.T) {
|
||
s := New()
|
||
s.NotifyBlock(map[string]int{"peerA": 5, "peerB": 0}, nil)
|
||
s.NotifyRequestResult("peerA", 100*time.Millisecond, false)
|
||
s.NotifyRequestResult("peerB", 5*time.Second, false)
|
||
s.NotifyBlock(nil, map[string]int{"peerA": 2})
|
||
|
||
stats := s.GetAllPeerStats()
|
||
// Only peerA receives finalization credits; peerB's EMA stays at zero
|
||
// (no credits, pure decay from zero).
|
||
if stats["peerA"].RecentFinalized <= 0 || stats["peerB"].RecentFinalized != 0 {
|
||
t.Errorf("finalization leaked: A=%f B=%f", stats["peerA"].RecentFinalized, stats["peerB"].RecentFinalized)
|
||
}
|
||
if stats["peerA"].RequestLatencyEMA != 100*time.Millisecond {
|
||
t.Errorf("peerA latency: got %v, want 100ms", stats["peerA"].RequestLatencyEMA)
|
||
}
|
||
if stats["peerB"].RequestLatencyEMA != 5*time.Second {
|
||
t.Errorf("peerB latency: got %v, want 5s", stats["peerB"].RequestLatencyEMA)
|
||
}
|
||
}
|
||
|
||
// TestLatencyTimestampSet verifies that NotifyRequestResult stamps the
|
||
// peer's LastLatencySample with approximately time.Now().
|
||
func TestLatencyTimestampSet(t *testing.T) {
|
||
s := New()
|
||
before := time.Now()
|
||
s.NotifyRequestResult("peerA", 100*time.Millisecond, false)
|
||
after := time.Now()
|
||
|
||
got := s.GetAllPeerStats()["peerA"].LastLatencySample
|
||
if got.Before(before) || got.After(after) {
|
||
t.Fatalf("LastLatencySample = %v not in [%v, %v]", got, before, after)
|
||
}
|
||
}
|
||
|
||
// TestLatencyTimestampUpdatesOnEachSample verifies that a later
|
||
// NotifyRequestResult call advances LastLatencySample.
|
||
func TestLatencyTimestampUpdatesOnEachSample(t *testing.T) {
|
||
s := New()
|
||
s.NotifyRequestResult("peerA", 100*time.Millisecond, false)
|
||
first := s.GetAllPeerStats()["peerA"].LastLatencySample
|
||
|
||
// Small sleep so the second timestamp is detectably later.
|
||
time.Sleep(2 * time.Millisecond)
|
||
s.NotifyRequestResult("peerA", 200*time.Millisecond, false)
|
||
second := s.GetAllPeerStats()["peerA"].LastLatencySample
|
||
|
||
if !second.After(first) {
|
||
t.Fatalf("expected second sample timestamp > first, got first=%v second=%v", first, second)
|
||
}
|
||
}
|
||
|
||
// TestRequestResultTimeoutCounting verifies that timeout=true increments
|
||
// RequestTimeouts (not RequestSuccesses) and still updates the EMA.
|
||
func TestRequestResultTimeoutCounting(t *testing.T) {
|
||
s := New()
|
||
s.NotifyRequestResult("peerA", 5*time.Second, true)
|
||
|
||
ps := s.GetAllPeerStats()["peerA"]
|
||
if ps.RequestTimeouts != 1 {
|
||
t.Fatalf("expected RequestTimeouts=1, got %d", ps.RequestTimeouts)
|
||
}
|
||
if ps.RequestSuccesses != 0 {
|
||
t.Fatalf("expected RequestSuccesses=0, got %d", ps.RequestSuccesses)
|
||
}
|
||
if ps.RequestLatencyEMA != 5*time.Second {
|
||
t.Fatalf("EMA should bootstrap to timeout value, got %v", ps.RequestLatencyEMA)
|
||
}
|
||
}
|
||
|
||
// TestRequestResultMixedCounting verifies that a mix of successes and
|
||
// timeouts increments the correct counters independently.
|
||
func TestRequestResultMixedCounting(t *testing.T) {
|
||
s := New()
|
||
s.NotifyRequestResult("peerA", 100*time.Millisecond, false)
|
||
s.NotifyRequestResult("peerA", 100*time.Millisecond, false)
|
||
s.NotifyRequestResult("peerA", 5*time.Second, true)
|
||
|
||
ps := s.GetAllPeerStats()["peerA"]
|
||
if ps.RequestSuccesses != 2 {
|
||
t.Fatalf("expected RequestSuccesses=2, got %d", ps.RequestSuccesses)
|
||
}
|
||
if ps.RequestTimeouts != 1 {
|
||
t.Fatalf("expected RequestTimeouts=1, got %d", ps.RequestTimeouts)
|
||
}
|
||
}
|