Skip to content

Commit c7f767d

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 f433998 commit c7f767d

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
@@ -4,8 +4,9 @@ import results, libp2p/crypto/crypto
44
import logos_delivery/api/conf/kernel_conf
55
import logos_delivery/waku/common/logging
66
import logos_delivery/waku/factory/networks_config
7+
import logos_delivery/messaging/rate_limit_manager/rate_limit_manager
78

8-
export kernel_conf
9+
export kernel_conf, rate_limit_manager
910

1011
type LogosDeliveryMode* {.pure.} = enum
1112
Edge # client-only node
@@ -52,6 +53,9 @@ type MessagingClientConf* = object
5253
## Process log format (TEXT or JSON); applied by the kernel on node creation.
5354
nodeKey* {.name: "nodekey".}: Opt[crypto.PrivateKey]
5455
## P2P node private key (64-char hex): stable identity / peerId across restarts.
56+
rateLimit*: RateLimitConfig = RateLimitConfig(
57+
epochPeriodSec: DefaultEpochPeriodSec, messagesPerEpoch: DefaultMessagesPerEpoch
58+
) ## RLN-epoch transmission budget enforced by the send service.
5559

5660
proc applyMode*(conf: var WakuNodeConf, mode: LogosDeliveryMode): ConfResult[void] =
5761
## 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
@@ -8,7 +8,8 @@ import
88
./[send_processor, relay_processor, lightpush_processor, delivery_task],
99
logos_delivery/waku/[waku_core, waku_store/common],
1010
logos_delivery/waku/waku,
11-
logos_delivery/waku/api/[store, subscriptions, publish]
11+
logos_delivery/waku/api/[store, subscriptions, publish],
12+
logos_delivery/messaging/rate_limit_manager/rate_limit_manager
1213
import logos_delivery/api/events/messaging_client_events
1314

1415
logScope:
@@ -46,6 +47,9 @@ type SendService* = ref object of RootObj
4647

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

5054
waku: Waku
5155
checkStoreForMessages: bool
@@ -77,7 +81,10 @@ proc setupSendProcessorChain(
7781
return ok(processors[0])
7882

7983
proc new*(
80-
T: typedesc[SendService], preferP2PReliability: bool, waku: Waku
84+
T: typedesc[SendService],
85+
preferP2PReliability: bool,
86+
waku: Waku,
87+
rateLimitManager: RateLimitManager,
8188
): Result[T, string] =
8289
if not waku.hasRelay() and not waku.hasLightpush():
8390
return err(
@@ -94,6 +101,7 @@ proc new*(
94101
taskCache: newSeq[DeliveryTask](),
95102
serviceLoopHandle: nil,
96103
sendProcessor: sendProcessorChain,
104+
rateLimitManager: rateLimitManager,
97105
waku: waku,
98106
checkStoreForMessages: checkStoreForMessages,
99107
lastStoreCheckTime: Moment.now(),
@@ -245,6 +253,12 @@ proc trySendMessages(self: SendService) {.async.} =
245253

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

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

291+
(await self.rateLimitManager.admit(task.msg.payload)).isOkOr:
292+
info "SendService.send: over rate-limit budget, parking task",
293+
requestId = task.requestId, msgHash = task.msgHash.to0xHex()
294+
task.state = DeliveryState.NextRoundRetry
295+
self.addTask(task)
296+
return
297+
277298
await self.sendProcessor.process(task)
278299
reportTaskResult(self, task)
279300
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
@@ -7,7 +7,8 @@ import
77
logos_delivery/api/messaging_client_api,
88
logos_delivery/waku/waku,
99
logos_delivery/waku/factory/conf_builder/waku_conf_builder,
10-
logos_delivery/messaging/delivery_service/[recv_service, send_service]
10+
logos_delivery/messaging/delivery_service/[recv_service, send_service],
11+
logos_delivery/messaging/rate_limit_manager/rate_limit_manager
1112

1213
export messaging_client_api, messaging_conf
1314

@@ -24,7 +25,8 @@ proc new*(
2425
## The messaging layer chains onto Waku: it drives the underlying Waku kernel
2526
## for transport while exposing its own send/recv API.
2627
let reliability = conf.reliabilityEnabled.get(DefaultP2pReliability)
27-
let sendService = ?SendService.new(reliability, waku)
28+
let sendService =
29+
?SendService.new(reliability, waku, RateLimitManager.new(conf.rateLimit))
2830
let recvService = RecvService.new(waku)
2931
return ok(
3032
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)