Skip to content

cl: add per-peer rate limiting on P2P ReqResp handlers - #20146

Merged
domiwei merged 14 commits into
mainfrom
caplin-reqresp-rate-limiting
Mar 26, 2026
Merged

domiwei merged 14 commits into
mainfrom
caplin-reqresp-rate-limiting

Conversation

@yperbasis

@yperbasis yperbasis commented Mar 25, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Implement per-peer, per-protocol token bucket rate limiting and per-peer concurrent stream caps on all incoming Caplin ReqResp handlers
  • Rate limits are counted per response item, not per request (e.g. a blocks-by-range for 96 blocks costs 96 tokens), closing the ~96x effectiveness gap vs Lighthouse
  • Rate limits aligned with Lighthouse/Lodestar (e.g. 128 blocks/10s, 72 blobs/10s, 2 pings/10s, 1 goodbye/10s, 5 status/15s)
  • Cap concurrent in-flight requests per peer at 5; apply 30-second punishment when a peer exceeds a protocol's rate limit
  • Enable libp2p ResourceManager with per-peer inbound stream cap of 32 (matching Lighthouse's MAX_INBOUND_SUBSTREAMS)
  • Remove dead rate limiting code (RateLimits struct, checkRateLimit() method, punishmentEndTimes sync.Map) and dead comments (observeBandwidth, rcmgrObs) that were never wired up

Fixes https://github.com/ethereum-bounty/erigon/issues/3

🤖 Generated with Claude Code

Implement per-peer, per-protocol token bucket rate limiting and per-peer
concurrent stream caps on all incoming Caplin ReqResp handlers. Previously,
checkRateLimit() was defined but never called, and no concurrency limits
existed, allowing a single peer to flood unlimited requests.

Rate limits are aligned with Lighthouse/Lodestar (e.g. 128 blocks/10s,
2 pings/10s, 1 goodbye/10s). A 30-second punishment is applied when a
peer exceeds a protocol's rate limit. Concurrent in-flight requests per
peer are capped at 5.

Removes dead rate limiting code (RateLimits struct, checkRateLimit method,
punishmentEndTimes sync.Map) that was never wired up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yperbasis yperbasis added this to the 3.5.0 milestone Mar 25, 2026
yperbasis and others added 5 commits March 25, 2026 10:56
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move peerID extraction after defer recover() to avoid unrecoverable panic
- Use Load before LoadOrStore to avoid allocating a tokenBucket on every request
- Remove unused ErrRateLimited/ErrTooManyRequests exports and duplicate RateLimitedPrefix;
  use existing InvalidRequestPrefix constant instead
- Fix copyright year (2024 → 2026) and add header to test file

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add periodic cleanup of stale sync.Map entries (expired punishments, idle
token buckets, zero-count concurrency counters) to prevent unbounded memory
growth as peers connect and disconnect. Write InvalidRequestPrefix on
concurrency rejection to match rate-limit path. Remove redundant s.Close()
after s.Reset().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
hashFiles() is not available in composite action outputs.value
expressions. Use steps.restore.outputs.cache-primary-key instead,
which reflects the exact key passed to the cache/restore step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
yperbasis and others added 5 commits March 25, 2026 15:35
Write a spec-compliant SSZ-snappy error message (via EncodeAndWrite) on
rate-limit and concurrency rejections instead of a raw response-code
byte.

Remove concurrency-counter cleanup from the periodic sweep to eliminate
a TOCTOU race between cleanup's Delete and acquireConcurrency's
LoadOrStore. Each entry is a single *atomic.Int32 per peer (~40 bytes),
so the memory cost of keeping them is negligible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…omments

Enable libp2p ResourceManager with per-peer inbound stream cap of 32
(matching Lighthouse's MAX_INBOUND_SUBSTREAMS), down from the default 256.
This adds transport-level protection complementary to the application-level
token bucket rate limiter.

Remove dead comments: observeBandwidth() (function never existed) and
rcmgrObs.MustRegisterWith() (variable never existed). Bandwidth monitoring
is already enabled via metrics.BandwidthCounter in p2p.go.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change rate limiting from per-request to per-response-item counting,
aligned with Lighthouse's approach. The wrapper pre-consumes 1 token
for admission; batch handlers (blocks-by-range, blobs-by-range, etc.)
consume additional tokens proportional to the requested item count
after decoding the request.

This closes the ~96x effectiveness gap vs Lighthouse. For example,
blocks-by-range now costs min(req.Count, 96) tokens per request
instead of 1, giving an effective burst of ~128 blocks (matching
Lighthouse's 128 blocks/10s).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ng punishment

Use s.Close() instead of s.Reset() on rate-limit and concurrency
rejections so the peer can read the SSZ error response before the
stream is torn down.

When punishment is triggered, drain the token bucket to 0 and advance
lastRefill to the punishment expiry (minus a 1-token grace window).
This suppresses token accumulation during the punishment period,
preventing a repeat offender from getting a fresh full burst at
expiry. The tryConsume refill is clamped to non-negative elapsed time
so the advanced lastRefill does not subtract tokens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add end-to-end tests over real libp2p streams that verify rate limiting
works at the protocol level. TestPingRateLimit confirms burst=2 and
punishment. TestBlocksByRangeRateLimit confirms per-item token costing
rejects a second 96-block request after the first exhausts most of the
128-token burst.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yperbasis yperbasis changed the title cl/sentinel: add per-peer rate limiting on P2P ReqResp handlers cl/p2p, cl/sentinel: add per-peer rate limiting on P2P ReqResp handlers Mar 25, 2026
@yperbasis yperbasis changed the title cl/p2p, cl/sentinel: add per-peer rate limiting on P2P ReqResp handlers cl: add per-peer rate limiting on P2P ReqResp handlers Mar 25, 2026
@yperbasis
yperbasis marked this pull request as ready for review March 25, 2026 16:08
@yperbasis
yperbasis requested a review from anacrolix March 25, 2026 16:08
github-merge-queue Bot pushed a commit that referenced this pull request Mar 25, 2026
## Summary

- Use `steps.restore.outputs.cache-primary-key` instead of re-computing
the inline `hashFiles` expression in the `restore-mod-cache` composite
action's `primary-key` output. The inline expression was being evaluated
at the wrong level (composite action output context rather than step
context), causing a cache key mismatch between restore and save.

Cherry-picked from #20146 (ae57f6f) to land independently.

## Test plan

- [ ] Verify CI cache hit/miss behavior on a workflow run using this
action

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Oleksandr Lystopad <oleksandr.lystopad@erigon.tech>
yperbasis and others added 2 commits March 25, 2026 18:46
Use GetBlobParameters(epoch) instead of max(MaxBlobsPerBlock, MaxBlobsPerBlockElectra)
so Deneb requests (max 6 blobs/block) are not overcharged at the Electra rate (max 9).
Blob sidecars are deprecated at Fulu, so this only affects Deneb/Electra ranges.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@domiwei

domiwei commented Mar 26, 2026

Copy link
Copy Markdown
Member

BlobSchedule-aware lookup

domiwei
domiwei previously approved these changes Mar 26, 2026

@domiwei domiwei left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall: great PR — the two-phase token consumption (1 for admission in the wrapper, remainder after decoding) is clean, and the punishment drain + lastRefill advance to prevent burst regeneration is a nice detail. Tests are thorough (both unit and integration over real libp2p streams).

Two comments:

1. blobs.go — use GetBlobParameters() instead of max()

// current
maxBlobs := max(int(c.beaconConfig.MaxBlobsPerBlock), int(c.beaconConfig.MaxBlobsPerBlockElectra))

// suggested
startEpoch := req.StartSlot / c.beaconConfig.SlotsPerEpoch
maxBlobs := int(c.beaconConfig.GetBlobParameters(startEpoch).MaxBlobsPerBlock)

max(MaxBlobsPerBlock, MaxBlobsPerBlockElectra) always charges at the Electra rate (9), which overcharges Deneb requests by 50% (actual max is 6). GetBlobParameters(epoch) is the canonical API used elsewhere (on_block.go, beacon_block.go, operations.go) and handles BlobSchedule correctly.

Blob sidecars are deprecated at Fulu so the effective max here is always ≤ Electra's, but using the schedule-aware lookup is more precise and consistent with the rest of the codebase.

2. Nit: typo

maxBlobsThroughoutputPerRequest → maxBlobsThroughputPerRequest (Throughoutput → Throughput)

@domiwei
domiwei enabled auto-merge March 26, 2026 06:09
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@domiwei
domiwei added this pull request to the merge queue Mar 26, 2026
Merged via the queue into main with commit bfc4ce4 Mar 26, 2026
35 checks passed
@domiwei
domiwei deleted the caplin-reqresp-rate-limiting branch March 26, 2026 11:22
wmitsuda pushed a commit that referenced this pull request Apr 1, 2026
## Summary

- Use `steps.restore.outputs.cache-primary-key` instead of re-computing
the inline `hashFiles` expression in the `restore-mod-cache` composite
action's `primary-key` output. The inline expression was being evaluated
at the wrong level (composite action output context rather than step
context), causing a cache key mismatch between restore and save.

Cherry-picked from #20146 (ae57f6f) to land independently.

## Test plan

- [ ] Verify CI cache hit/miss behavior on a workflow run using this
action

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Oleksandr Lystopad <oleksandr.lystopad@erigon.tech>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants