docs/FSD_RATE_LIMIT.md §5 lists four migrations onto rate_limit::RateLimiter. Three landed in CIRISEdge#558:
InviteGate — had two real bugs (unbounded under identity rotation, then an O(cap) scan per rejected invite). Its original abuse tests were the acceptance criterion and failed on first migration, catching two design errors in the shared limiter itself.
- The missing-signer queue — the fourth appearance of the O(cap)-per-rejection class, which is what prompted extracting
SourceLedger.
- The identifier-lookup budget (new) — the reason the limiter work started.
Two remain, and they are deliberately deferred rather than forgotten.
Why they were held back
LogThrottle and RefusalBackoff are, per the FSD's own table, the two implementations that never shipped a bug — they are the reference implementations for bounded keys and for backoff respectively. Migrating them is uniformity, not defect reduction.
And the FSD's claim that each is "this module with different settings" turns out to be only partly true. Absorbing them requires extending the shared module:
| caller |
what it needs that the limiter lacks |
RefusalBackoff |
per-class backoff parameters (transient 60s/300s vs terminal 1800s/21600s; Policy has one Backoff for all classes) · front-drop eviction — its keys are content hashes with no source attribution, so fair-by-source does not apply at all · record_at returns the window |
LogThrottle |
Instant-based, sub-second capable; the limiter is whole-second Ts |
So the shape is: make the shared primitive more complex, to take on two callers that are currently correct — inside a PR that has already absorbed ~35 findings across nine rounds. That trade is worth making deliberately, not by momentum.
One precondition already landed in #558: the limiter is now generic over its key. RefusalBackoff is consulted once per wanted hash per round, so a String-keyed limiter would have cost a heap allocation and a hex encoding per hash per round — the #547 shape exactly. A Copy key now costs nothing.
The work
- Move
Backoff into Quota, so each class carries its own base/cap.
- Add an eviction mode:
FairBySource (today's behaviour) | Oldest (front-drop, for source-less key spaces).
- Migrate
RefusalBackoff internals; keep record_at/suppressed_at signatures so its ~70 call sites do not move.
- Migrate
LogThrottle internals; keep check/check_at and ThrottleDecision.
Acceptance criterion, same as before: each caller's existing tests stay green untouched. They were written against real bugs, so they are the regression suite — and on InviteGate they were what caught the shared limiter's own errors.
docs/FSD_RATE_LIMIT.md§5 lists four migrations ontorate_limit::RateLimiter. Three landed in CIRISEdge#558:InviteGate— had two real bugs (unbounded under identity rotation, then an O(cap) scan per rejected invite). Its original abuse tests were the acceptance criterion and failed on first migration, catching two design errors in the shared limiter itself.SourceLedger.Two remain, and they are deliberately deferred rather than forgotten.
Why they were held back
LogThrottleandRefusalBackoffare, per the FSD's own table, the two implementations that never shipped a bug — they are the reference implementations for bounded keys and for backoff respectively. Migrating them is uniformity, not defect reduction.And the FSD's claim that each is "this module with different settings" turns out to be only partly true. Absorbing them requires extending the shared module:
RefusalBackoffPolicyhas oneBackofffor all classes) · front-drop eviction — its keys are content hashes with no source attribution, so fair-by-source does not apply at all ·record_atreturns the windowLogThrottleInstant-based, sub-second capable; the limiter is whole-secondTsSo the shape is: make the shared primitive more complex, to take on two callers that are currently correct — inside a PR that has already absorbed ~35 findings across nine rounds. That trade is worth making deliberately, not by momentum.
One precondition already landed in #558: the limiter is now generic over its key.
RefusalBackoffis consulted once per wanted hash per round, so aString-keyed limiter would have cost a heap allocation and a hex encoding per hash per round — the #547 shape exactly. ACopykey now costs nothing.The work
BackoffintoQuota, so each class carries its own base/cap.FairBySource(today's behaviour) |Oldest(front-drop, for source-less key spaces).RefusalBackoffinternals; keeprecord_at/suppressed_atsignatures so its ~70 call sites do not move.LogThrottleinternals; keepcheck/check_atandThrottleDecision.Acceptance criterion, same as before: each caller's existing tests stay green untouched. They were written against real bugs, so they are the regression suite — and on
InviteGatethey were what caught the shared limiter's own errors.