|
39 | 39 | pubKey*: Curve25519Key |
40 | 40 | mixRlnSpamProtection*: MixRlnSpamProtection |
41 | 41 | publishMessage*: PublishMessage |
| 42 | + dosRegistrationTask: Future[void] |
| 43 | + ## Background task that retries DoS-protection self-registration until |
| 44 | + ## it succeeds. nil until kicked off via registerDoSProtectionWithNetwork; |
| 45 | + ## cancelled in stop(). |
42 | 46 |
|
43 | 47 | WakuMixResult*[T] = Result[T, string] |
44 | 48 |
|
@@ -233,54 +237,113 @@ proc loadSpamProtectionTree*(mix: WakuMix): Result[void, string] = |
233 | 237 | ) |
234 | 238 |
|
235 | 239 | method start*(mix: WakuMix) {.async.} = |
| 240 | + ## Local-only mix protocol initialization. Does NOT touch the network. |
| 241 | + ## The network-dependent self-registration broadcast is handled separately |
| 242 | + ## by registerDoSProtectionWithNetwork so that this proc can run before |
| 243 | + ## peers are connected without blocking on relay startup. |
236 | 244 | info "starting waku mix protocol" |
237 | 245 |
|
238 | | - # Set up spam protection callbacks and start |
239 | 246 | if not mix.mixRlnSpamProtection.isNil(): |
240 | 247 | # Initialize spam protection (MixProtocol.init() does NOT call init() on the plugin) |
241 | 248 | let initRes = await mix.mixRlnSpamProtection.init() |
242 | 249 | if initRes.isErr: |
243 | 250 | error "Failed to initialize spam protection", error = initRes.error |
244 | | - else: |
245 | | - # Load existing tree to sync with other members |
246 | | - # This should be done after init() (which loads credentials) |
247 | | - # but before registerSelf() (which adds us to the tree) |
248 | | - let loadRes = mix.mixRlnSpamProtection.loadTree() |
249 | | - if loadRes.isErr: |
250 | | - debug "No existing tree found or failed to load, starting fresh", |
251 | | - error = loadRes.error |
252 | | - else: |
253 | | - debug "Loaded existing spam protection membership tree from disk" |
254 | | - |
255 | | - # Restore our credentials to the tree (after tree load, whether it succeeded or not) |
256 | | - # This ensures our member is in the tree if we have an index from keystore |
257 | | - let restoreRes = mix.mixRlnSpamProtection.restoreCredentialsToTree() |
258 | | - if restoreRes.isErr: |
259 | | - error "Failed to restore credentials to tree", error = restoreRes.error |
260 | | - |
261 | | - # Set up publish callback (must be before start so registerSelf can use it) |
262 | | - mix.setupSpamProtectionCallbacks() |
263 | | - |
264 | | - let startRes = await mix.mixRlnSpamProtection.start() |
265 | | - if startRes.isErr: |
266 | | - error "Failed to start spam protection", error = startRes.error |
267 | | - else: |
268 | | - # Register self to broadcast membership to the network |
269 | | - let registerRes = await mix.mixRlnSpamProtection.registerSelf() |
270 | | - if registerRes.isErr: |
271 | | - error "Failed to register spam protection credentials", |
272 | | - error = registerRes.error |
273 | | - else: |
274 | | - debug "Registered spam protection credentials", index = registerRes.get() |
| 251 | + return |
275 | 252 |
|
276 | | - # Save tree to persist membership state |
| 253 | + # Load existing tree to sync with other members. |
| 254 | + # Should be done after init() (which loads credentials) but before |
| 255 | + # registerSelf() (which adds us to the tree). |
| 256 | + let loadRes = mix.mixRlnSpamProtection.loadTree() |
| 257 | + if loadRes.isErr: |
| 258 | + debug "No existing tree found or failed to load, starting fresh", |
| 259 | + error = loadRes.error |
| 260 | + else: |
| 261 | + debug "Loaded existing spam protection membership tree from disk" |
| 262 | + |
| 263 | + # Restore our credentials to the tree (after tree load, whether it succeeded or not). |
| 264 | + # Ensures our member is in the tree if we have an index from keystore. |
| 265 | + let restoreRes = mix.mixRlnSpamProtection.restoreCredentialsToTree() |
| 266 | + if restoreRes.isErr: |
| 267 | + error "Failed to restore credentials to tree", error = restoreRes.error |
| 268 | + |
| 269 | + # Set up publish callback. Must be before the network-side registration so |
| 270 | + # the plugin's groupManager.register can broadcast the membership update. |
| 271 | + mix.setupSpamProtectionCallbacks() |
| 272 | + |
| 273 | + let startRes = await mix.mixRlnSpamProtection.start() |
| 274 | + if startRes.isErr: |
| 275 | + error "Failed to start spam protection", error = startRes.error |
| 276 | + |
| 277 | + info "waku mix protocol started" |
| 278 | + |
| 279 | +proc dosRegistrationRetryLoop(mix: WakuMix) {.async.} = |
| 280 | + ## Indefinitely retry the DoS-protection self-registration broadcast until |
| 281 | + ## it succeeds (or this task is cancelled by WakuMix.stop()). For nodes that |
| 282 | + ## already have a membership index in their keystore, registerSelf early- |
| 283 | + ## returns and the loop exits on the first attempt. For fresh nodes, the |
| 284 | + ## broadcast needs at least one relay peer subscribed to the membership |
| 285 | + ## topic to land — this loop survives transient "no peers yet" failures. |
| 286 | + ## |
| 287 | + ## TODO: Remove once RLN membership moves on-chain. With on-chain membership |
| 288 | + ## peers discover each other via the contract / a watcher rather than via a |
| 289 | + ## pubsub broadcast, so the retry loop (and the whole publishCallback path |
| 290 | + ## from registerSelf) becomes unnecessary. |
| 291 | + ## |
| 292 | + ## Retry pacing uses exponential backoff (5s, 10s, 20s, ..., capped at 5min) |
| 293 | + ## so persistent misconfiguration — e.g., relay never available — degrades |
| 294 | + ## to one log line every 5 minutes after the initial ramp instead of every |
| 295 | + ## 5 seconds forever. |
| 296 | + const InitialRetryDelay = chronos.seconds(5) |
| 297 | + const MaxRetryDelay = chronos.minutes(5) |
| 298 | + var delay = InitialRetryDelay |
| 299 | + while true: |
| 300 | + try: |
| 301 | + let registerRes = await mix.mixRlnSpamProtection.registerSelf() |
| 302 | + if registerRes.isOk(): |
| 303 | + debug "DoS-protection self-registration succeeded", |
| 304 | + index = registerRes.get() |
| 305 | + # Persist tree only after a successful register — for fresh nodes this |
| 306 | + # captures the new index; for keystore nodes it's a harmless no-op. |
277 | 307 | let saveRes = mix.mixRlnSpamProtection.saveTree() |
278 | 308 | if saveRes.isErr: |
279 | 309 | warn "Failed to save spam protection tree", error = saveRes.error |
280 | 310 | else: |
281 | 311 | trace "Saved spam protection tree to disk" |
| 312 | + return # success — exit the loop |
| 313 | + warn "DoS-protection self-registration failed, retrying", |
| 314 | + error = registerRes.error, nextDelay = delay |
| 315 | + except CancelledError as e: |
| 316 | + debug "DoS-protection registration loop cancelled" |
| 317 | + raise e |
| 318 | + except CatchableError as e: |
| 319 | + warn "DoS-protection registration raised, retrying", |
| 320 | + error = e.msg, nextDelay = delay |
| 321 | + await sleepAsync(delay) |
| 322 | + delay = min(delay * 2, MaxRetryDelay) |
| 323 | + |
| 324 | +proc registerDoSProtectionWithNetwork*(mix: WakuMix) = |
| 325 | + ## Kick off an indefinite background task that broadcasts this node's |
| 326 | + ## DoS-protection (RLN) membership registration to other mix nodes via |
| 327 | + ## relay. Returns immediately so callers don't block on a possibly-slow |
| 328 | + ## broadcast. The task is cancelled when WakuMix.stop() is called. |
| 329 | + if mix.mixRlnSpamProtection.isNil(): |
| 330 | + return |
| 331 | + # Guard against kicking off the retry loop when the plugin isn't actually |
| 332 | + # usable (e.g., mix.start()'s init/start steps failed). Without this check |
| 333 | + # the loop would spin forever logging "Plugin not initialized" warnings. |
| 334 | + if not mix.mixRlnSpamProtection.isReady(): |
| 335 | + warn "Skipping DoS-protection registration: plugin not ready" |
| 336 | + return |
| 337 | + # Re-call safety: don't spawn a second loop if one is still in flight. |
| 338 | + if not mix.dosRegistrationTask.isNil and not mix.dosRegistrationTask.finished: |
| 339 | + debug "DoS-protection registration already in progress, skipping" |
| 340 | + return |
| 341 | + mix.dosRegistrationTask = mix.dosRegistrationRetryLoop() |
282 | 342 |
|
283 | 343 | method stop*(mix: WakuMix) {.async.} = |
| 344 | + # Cancel the in-flight DoS-protection registration retry loop, if any |
| 345 | + if not mix.dosRegistrationTask.isNil and not mix.dosRegistrationTask.finished: |
| 346 | + await mix.dosRegistrationTask.cancelAndWait() |
284 | 347 | # Stop spam protection |
285 | 348 | if not mix.mixRlnSpamProtection.isNil(): |
286 | 349 | await mix.mixRlnSpamProtection.stop() |
|
0 commit comments