mirror of
https://github.com/logos-messaging/logos-delivery.git
synced 2026-06-06 22:19:30 +00:00
Combines five dep-and-build changes that all flow from the libp2p v2.0.0
upgrade and the move to the extracted libp2p_mix / mix-rln plugin stack:
waku.nimble:
* libp2p: ff8d51857 -> c43199378 (release/v2.0.0 tip; sha-pinned until
vacp2p cuts a v2.0.0 tag).
* Drop the bare `zlib < 0.2` cap — no longer needed by the upgraded
libp2p.
* websock: bare ">= 0.4.0" — replaces the d4cd68b URL+SHA workaround
that pinned through a libp2p commit-specific websock SHA.
* nim-json-rpc: switch to chaitanyaprem/nim-json-rpc#f05fad25 — relaxes
websock cap to allow >=0.4.0. TODO: revert to status-im/nim-json-rpc
once status-im/nim-json-rpc#277 merges and a tag is cut.
* lsquic: bare ">= 0.4.1" (drops URL form).
* Add mix-rln-spam-protection-plugin pin (23b278b4) and nim-libp2p-mix
pin (50c4ab4f — PR #14 HEAD); the plugin pins the same libp2p_mix
SHA so the diamond dep collapses to a single source.
waku/factory/waku.nim:
* Explicit HPService.setup(switch) / AutonatService.setup(switch)
calls. libp2p v2.0.0's Service lifecycle refactor (libp2p#2462)
removed switch.start's auto-setup loop, so any caller that assigns
directly to switch.services (we do) is responsible for calling
setup() themselves. Without it, AutonatService.addressMapper stays
nil and peerInfo.expandAddrs SIGSEGVs during start(). Wrapped in
try/except for ServiceSetupError so a setup failure surfaces as a
logged error rather than a crash.
Build / scripts:
* scripts/build_rln_mix.sh removed and Makefile simplified — librln
is now a single shared archive built from zerokit's `stateless`
features (no separate librln_mix archive).
* simulations/mixnet/build_setup.sh + setup_credentials.nim updated
to use librln_v2.0.2.a directly and run RLN keystore setup before
nodes start.
Validated:
* Cold local-cache nimble setup --localdeps -y.
* wakunode2 and chat2mix link cleanly.
* Mixnet roundtrip sim: [PASS] bob received message from alice.
* RLN proof generation + verification on every in-path mix node:
5 gen_called == 5 verified, 0 SPAM_PROOF_* errors.
199 lines
6.6 KiB
Nim
199 lines
6.6 KiB
Nim
## RequestRateLimiter
|
|
##
|
|
## RequestRateLimiter is a general service protection mechanism.
|
|
## While applies an overall rate limit, it also ensure fair usage among peers.
|
|
##
|
|
## This is reached by reject peers that are constantly over using the service while allowing others to use it
|
|
## within the global limit set.
|
|
## Punished peers will also be recovered after a certain time period if not violating the limit.
|
|
##
|
|
## This is reached by calculating a ratio of the global limit and applying it to each peer.
|
|
## This ratio is applied to the allowed tokens within a ratio * the global time period.
|
|
## The allowed tokens for peers are limited to 75% of ratio * global token volume.
|
|
##
|
|
## This needs to be taken into account when setting the global limit for the specific service type and use cases.
|
|
|
|
{.push raises: [].}
|
|
|
|
import
|
|
std/[options, math],
|
|
chronicles,
|
|
chronos/timer,
|
|
libp2p/stream/connection,
|
|
libp2p/utility
|
|
|
|
import std/times except TimeInterval, Duration, seconds, minutes
|
|
|
|
import ../option_shims
|
|
import ./[single_token_limiter, service_metrics, timed_map]
|
|
|
|
export token_bucket, setting, service_metrics
|
|
|
|
logScope:
|
|
topics = "waku ratelimit"
|
|
|
|
const PER_PEER_ALLOWED_PERCENT_OF_VOLUME = 0.75
|
|
const UNLIMITED_RATIO = 0
|
|
const UNLIMITED_TIMEOUT = 0.seconds
|
|
const MILISECONDS_RATIO = 10
|
|
const SECONDS_RATIO = 3
|
|
const MINUTES_RATIO = 2
|
|
|
|
type RequestRateLimiter* = ref object of RootObj
|
|
tokenBucket: TokenBucket
|
|
setting*: Option[RateLimitSetting]
|
|
mainBucketSetting: RateLimitSetting
|
|
ratio: int
|
|
peerBucketSetting*: RateLimitSetting
|
|
peerUsage: TimedMap[PeerId, TokenBucket]
|
|
checkUsageImpl: proc(
|
|
t: var RequestRateLimiter, proto: string, conn: Connection, now: Moment
|
|
): bool {.gcsafe, raises: [].}
|
|
|
|
proc newMainTokenBucket(
|
|
setting: RateLimitSetting, ratio: int, startTime: Moment
|
|
): TokenBucket =
|
|
## RequestRateLimiter's global bucket should keep the *rate* of the configured
|
|
## setting while allowing a larger burst window. We achieve this by scaling
|
|
## both capacity and fillDuration by the same ratio.
|
|
##
|
|
## This matches previous behavior where unused tokens could effectively
|
|
## accumulate across multiple periods.
|
|
let burstCapacity = setting.volume * ratio
|
|
var bucket = TokenBucket.new(
|
|
capacity = burstCapacity,
|
|
fillDuration = setting.period * ratio,
|
|
startTime = startTime,
|
|
mode = Continuous,
|
|
)
|
|
|
|
# Start with the configured volume (not the burst capacity) so that the
|
|
# initial burst behavior matches the raw setting, while still allowing
|
|
# accumulation up to `burstCapacity` over time.
|
|
let excess = burstCapacity - setting.volume
|
|
if excess > 0:
|
|
discard bucket.tryConsume(excess, startTime)
|
|
|
|
return bucket
|
|
|
|
proc mgetOrPut(
|
|
requestRateLimiter: var RequestRateLimiter, peerId: PeerId, now: Moment
|
|
): var TokenBucket =
|
|
let bucketForNew = newTokenBucket(
|
|
some(requestRateLimiter.peerBucketSetting), Discrete, now
|
|
).valueOr:
|
|
raiseAssert "This branch is not allowed to be reached as it will not be called if the setting is None."
|
|
|
|
return requestRateLimiter.peerUsage.mgetOrPut(peerId, bucketForNew)
|
|
|
|
proc checkUsageUnlimited(
|
|
t: var RequestRateLimiter, proto: string, conn: Connection, now: Moment
|
|
): bool {.gcsafe, raises: [].} =
|
|
true
|
|
|
|
proc checkUsageLimited(
|
|
t: var RequestRateLimiter, proto: string, conn: Connection, now: Moment
|
|
): bool {.gcsafe, raises: [].} =
|
|
# Lazy-init the main bucket using the first observed request time. This makes
|
|
# refill behavior deterministic under tests where `now` is controlled.
|
|
if isNil(t.tokenBucket):
|
|
t.tokenBucket = newMainTokenBucket(t.mainBucketSetting, t.ratio, now)
|
|
|
|
let peerBucket = t.mgetOrPut(conn.peerId, now)
|
|
## check requesting peer's usage is not over the calculated ratio and let that peer go which not requested much/or this time...
|
|
if not peerBucket.tryConsume(1, now):
|
|
trace "peer usage limit reached", peer = conn.peerId
|
|
return false
|
|
|
|
# Ok if the peer can consume, check the overall budget we have left
|
|
if not t.tokenBucket.tryConsume(1, now):
|
|
return false
|
|
|
|
return true
|
|
|
|
proc checkUsage*(
|
|
t: var RequestRateLimiter, proto: string, conn: Connection, now = Moment.now()
|
|
): bool {.raises: [].} =
|
|
t.checkUsageImpl(t, proto, conn, now)
|
|
|
|
template checkUsageLimit*(
|
|
t: var RequestRateLimiter,
|
|
proto: string,
|
|
conn: Connection,
|
|
bodyWithinLimit, bodyRejected: untyped,
|
|
) =
|
|
if t.checkUsage(proto, conn):
|
|
let requestStartTime = Moment.now()
|
|
waku_service_requests.inc(labelValues = [proto, "served"])
|
|
|
|
bodyWithinLimit
|
|
|
|
let requestDuration = Moment.now() - requestStartTime
|
|
waku_service_request_handling_duration_seconds.observe(
|
|
requestDuration.milliseconds.float / 1000, labelValues = [proto]
|
|
)
|
|
else:
|
|
waku_service_requests.inc(labelValues = [proto, "rejected"])
|
|
bodyRejected
|
|
|
|
# TODO: review these ratio assumptions! Debatable!
|
|
func calcPeriodRatio(settingOpt: Option[RateLimitSetting]): int =
|
|
settingOpt.withValue(setting):
|
|
if setting.isUnlimited():
|
|
return UNLIMITED_RATIO
|
|
|
|
if setting.period <= 1.seconds:
|
|
return MILISECONDS_RATIO
|
|
|
|
if setting.period <= 1.minutes:
|
|
return SECONDS_RATIO
|
|
|
|
return MINUTES_RATIO
|
|
do:
|
|
# when setting is none
|
|
return UNLIMITED_RATIO
|
|
|
|
# calculates peer cache items timeout
|
|
# effectively if a peer does not issue any requests for this amount of time will be forgotten.
|
|
func calcCacheTimeout(settingOpt: Option[RateLimitSetting], ratio: int): Duration =
|
|
settingOpt.withValue(setting):
|
|
if setting.isUnlimited():
|
|
return UNLIMITED_TIMEOUT
|
|
|
|
# CacheTimout for peers is double the replensih period for peers
|
|
return setting.period * ratio * 2
|
|
do:
|
|
# when setting is none
|
|
return UNLIMITED_TIMEOUT
|
|
|
|
func calcPeerTokenSetting(
|
|
setting: Option[RateLimitSetting], ratio: int
|
|
): RateLimitSetting =
|
|
let s = setting.valueOr:
|
|
return (0, 0.minutes)
|
|
|
|
let peerVolume =
|
|
trunc((s.volume * ratio).float * PER_PEER_ALLOWED_PERCENT_OF_VOLUME).int
|
|
let peerPeriod = s.period * ratio
|
|
|
|
return (peerVolume, peerPeriod)
|
|
|
|
proc newRequestRateLimiter*(setting: Option[RateLimitSetting]): RequestRateLimiter =
|
|
let ratio = calcPeriodRatio(setting)
|
|
let isLimited = setting.isSome() and not setting.get().isUnlimited()
|
|
let mainBucketSetting =
|
|
if isLimited:
|
|
setting.get()
|
|
else:
|
|
(0, 0.minutes)
|
|
|
|
return RequestRateLimiter(
|
|
tokenBucket: nil,
|
|
setting: setting,
|
|
mainBucketSetting: mainBucketSetting,
|
|
ratio: ratio,
|
|
peerBucketSetting: calcPeerTokenSetting(setting, ratio),
|
|
peerUsage: init(TimedMap[PeerId, TokenBucket], calcCacheTimeout(setting, ratio)),
|
|
checkUsageImpl: (if isLimited: checkUsageLimited else: checkUsageUnlimited),
|
|
)
|