Skip to content

Commit 9bf7e57

Browse files
stubbstaclaude
andcommitted
feat: meter SendService transmissions through RateLimitManager
Wires the relocated RateLimitManager into the messaging layer at the transmission stage rather than the API entry point: - MessagingClientConf gains a rateLimit: RateLimitConfig field (defaulting to DefaultEpochPeriodSec / DefaultMessagesPerEpoch), and MessagingClient.new hands the constructed manager to SendService. - SendService consults admit() before the first transmission of a task, both in send() and in the retry loop. Re-publishes of an already-propagated message (firstPropagatedTime set) skip admission: they resend the same bytes, which reuse the same RLN proof/nullifier and consume no fresh epoch slot. - An over-budget task parks in the task cache as NextRoundRetry; the service loop re-admits it as the epoch budget frees up. The skeleton admit() is a pass-through, so behaviour is unchanged today. Gating transmissions instead of MessagingClient.send keeps SDS repair rebroadcasts free of API-entry rejection (SDS decides that a repair is needed; the transmission scheduler decides when it fits the budget) while every wire transmission still draws from one node-wide budget -- which network-side RLN enforcement applies to repairs regardless of any local bypass. It is also where RLN proof attachment must happen, since proofs bind to the epoch current at transmission time. Adds tests/messaging/test_rate_limit_manager.nim covering the current disabled + enabled pass-through behaviour, wired into all_tests_waku.nim. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9985460 commit 9bf7e57

6 files changed

Lines changed: 64 additions & 5 deletions

File tree

logos_delivery/api/conf/messaging_conf.nim

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import libp2p/crypto/crypto
66
import logos_delivery/api/conf/kernel_conf
77
import logos_delivery/waku/common/logging
88
import logos_delivery/waku/factory/networks_config
9+
import logos_delivery/messaging/rate_limit_manager/rate_limit_manager
910

10-
export kernel_conf
11+
export kernel_conf, rate_limit_manager
1112

1213
type LogosDeliveryMode* {.pure.} = enum
1314
Edge # client-only node
@@ -54,6 +55,9 @@ type MessagingClientConf* = object
5455
## Process log format (TEXT or JSON); applied by the kernel on node creation.
5556
nodeKey* {.name: "nodekey".}: Option[crypto.PrivateKey]
5657
## P2P node private key (64-char hex): stable identity / peerId across restarts.
58+
rateLimit*: RateLimitConfig = RateLimitConfig(
59+
epochPeriodSec: DefaultEpochPeriodSec, messagesPerEpoch: DefaultMessagesPerEpoch
60+
) ## RLN-epoch transmission budget enforced by the send service.
5761

5862
proc applyMode*(conf: var WakuNodeConf, mode: LogosDeliveryMode): ConfResult[void] =
5963
## Sets the protocol flags implied by the mode.

logos_delivery/messaging/delivery_service/send_service/send_service.nim

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import
99
./[send_processor, relay_processor, lightpush_processor, delivery_task],
1010
logos_delivery/waku/[waku_core, waku_store/common],
1111
logos_delivery/waku/waku,
12-
logos_delivery/waku/api/[store, subscriptions, publish]
12+
logos_delivery/waku/api/[store, subscriptions, publish],
13+
logos_delivery/messaging/rate_limit_manager/rate_limit_manager
1314
import logos_delivery/api/events/messaging_client_events
1415

1516
logScope:
@@ -47,6 +48,9 @@ type SendService* = ref object of RootObj
4748

4849
serviceLoopHandle: Future[void] ## handle that allows to stop the async task
4950
sendProcessor: BaseSendProcessor
51+
rateLimitManager: RateLimitManager
52+
## Meters first transmissions against the per-epoch budget; re-publishes
53+
## of an already-propagated message resend the same bytes and are free.
5054

5155
waku: Waku
5256
checkStoreForMessages: bool
@@ -78,7 +82,10 @@ proc setupSendProcessorChain(
7882
return ok(processors[0])
7983

8084
proc new*(
81-
T: typedesc[SendService], preferP2PReliability: bool, waku: Waku
85+
T: typedesc[SendService],
86+
preferP2PReliability: bool,
87+
waku: Waku,
88+
rateLimitManager: RateLimitManager,
8289
): Result[T, string] =
8390
if not waku.hasRelay() and not waku.hasLightpush():
8491
return err(
@@ -95,6 +102,7 @@ proc new*(
95102
taskCache: newSeq[DeliveryTask](),
96103
serviceLoopHandle: nil,
97104
sendProcessor: sendProcessorChain,
105+
rateLimitManager: rateLimitManager,
98106
waku: waku,
99107
checkStoreForMessages: checkStoreForMessages,
100108
lastStoreCheckTime: Moment.now(),
@@ -246,6 +254,12 @@ proc trySendMessages(self: SendService) {.async.} =
246254

247255
for task in tasksToSend:
248256
# Todo, check if it has any perf gain to run them concurrent...
257+
if task.firstPropagatedTime.isNone():
258+
## Never propagated: this transmission consumes a fresh epoch slot, so
259+
## it must be admitted. Tasks that stay over budget remain in
260+
## `NextRoundRetry` and are retried as the epoch rolls over.
261+
(await self.rateLimitManager.admit(task.msg.payload)).isOkOr:
262+
continue
249263
await self.sendProcessor.process(task)
250264

251265
proc serviceLoop(self: SendService) {.async.} =
@@ -275,6 +289,13 @@ proc send*(self: SendService, task: DeliveryTask) {.async.} =
275289
error "SendService.send: failed to subscribe to content topic",
276290
contentTopic = task.msg.contentTopic, error = error
277291

292+
(await self.rateLimitManager.admit(task.msg.payload)).isOkOr:
293+
info "SendService.send: over rate-limit budget, parking task",
294+
requestId = task.requestId, msgHash = task.msgHash.to0xHex()
295+
task.state = DeliveryState.NextRoundRetry
296+
self.addTask(task)
297+
return
298+
278299
await self.sendProcessor.process(task)
279300
reportTaskResult(self, task)
280301
if task.state != DeliveryState.FailedToDeliver:

logos_delivery/messaging/messaging_client.nim

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import
88
logos_delivery/api/messaging_client_api,
99
logos_delivery/waku/waku,
1010
logos_delivery/waku/factory/conf_builder/waku_conf_builder,
11-
logos_delivery/messaging/delivery_service/[recv_service, send_service]
11+
logos_delivery/messaging/delivery_service/[recv_service, send_service],
12+
logos_delivery/messaging/rate_limit_manager/rate_limit_manager
1213

1314
export messaging_client_api, messaging_conf
1415

@@ -25,7 +26,8 @@ proc new*(
2526
## The messaging layer chains onto Waku: it drives the underlying Waku kernel
2627
## for transport while exposing its own send/recv API.
2728
let reliability = conf.reliabilityEnabled.get(DefaultP2pReliability)
28-
let sendService = ?SendService.new(reliability, waku)
29+
let sendService =
30+
?SendService.new(reliability, waku, RateLimitManager.new(conf.rateLimit))
2931
let recvService = RecvService.new(waku)
3032
return ok(
3133
T(

tests/all_tests_waku.nim

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,8 @@ import ./tools/test_all
8888
# Persistency library tests
8989
import ./persistency/test_all
9090

91+
# Messaging API tests
92+
import ./messaging/test_all
93+
9194
# Reliable Channel API tests
9295
import ./channels/test_all

tests/messaging/test_all.nim

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{.used.}
2+
3+
import ./test_rate_limit_manager
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{.used.}
2+
3+
import chronos, testutils/unittests, stew/byteutils
4+
5+
import logos_delivery/messaging/rate_limit_manager/rate_limit_manager
6+
7+
suite "RateLimitManager - admission":
8+
asyncTest "admit is a pass-through when disabled":
9+
let rl = RateLimitManager.new(
10+
RateLimitConfig(enabled: false, epochPeriodSec: 600, messagesPerEpoch: 1)
11+
)
12+
for _ in 0 ..< 10:
13+
let res = await rl.admit("payload".toBytes())
14+
check res.isOk()
15+
16+
asyncTest "admit is a pass-through in the skeleton even when enabled":
17+
## Documents the current skeleton behaviour: per-epoch enforcement is
18+
## not wired yet, so every call is admitted regardless of the
19+
## configured budget. This test flips to red as soon as real
20+
## enforcement lands, at which point it should be replaced with
21+
## budget-boundary assertions.
22+
let rl = RateLimitManager.new(
23+
RateLimitConfig(enabled: true, epochPeriodSec: 600, messagesPerEpoch: 1)
24+
)
25+
check (await rl.admit("first".toBytes())).isOk()
26+
check (await rl.admit("second".toBytes())).isOk()

0 commit comments

Comments
 (0)