From 65494a97777de3207d42f9e6b7335016c1fa3ef1 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Mon, 27 Oct 2025 18:01:56 +0200 Subject: [PATCH 01/22] Only add new roots, not all received --- vendor/waku-rlnv2-contract | 2 +- .../group_manager/on_chain/group_manager.nim | 139 ++++++++++++++++-- 2 files changed, 126 insertions(+), 15 deletions(-) diff --git a/vendor/waku-rlnv2-contract b/vendor/waku-rlnv2-contract index 900d4f95e..58c6c9f4a 160000 --- a/vendor/waku-rlnv2-contract +++ b/vendor/waku-rlnv2-contract @@ -1 +1 @@ -Subproject commit 900d4f95e0e618bdeb4c241f7a4b6347df6bb950 +Subproject commit 58c6c9f4a789c2d93eab33ea68461001fe3157df diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 76c00408e..9ae45b2de 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -74,21 +74,39 @@ proc fetchMerkleProofElements*( error "Failed to fetch Merkle proof elements", error = getCurrentExceptionMsg() return err("Failed to fetch merkle proof elements: " & getCurrentExceptionMsg()) +# proc fetchMerkleRoot*( +# g: OnchainGroupManager +# ): Future[Result[UInt256, string]] {.async.} = +# try: +# let merkleRoot = await sendEthCallWithoutParams( +# ethRpc = g.ethRpc.get(), +# functionSignature = "root()", +# fromAddress = g.ethRpc.get().defaultAccount, +# toAddress = fromHex(Address, g.ethContractAddress), +# chainId = g.chainId, +# ) +# return merkleRoot +# except CatchableError: +# error "Failed to fetch Merkle root", error = getCurrentExceptionMsg() +# return err("Failed to fetch merkle root: " & getCurrentExceptionMsg()) + +## params = @[] - added empty params as way to get the raw return bytes (seq[bytes]) proc fetchMerkleRoot*( g: OnchainGroupManager -): Future[Result[UInt256, string]] {.async.} = +): Future[Result[seq[byte], string]] {.async.} = try: - let merkleRoot = await sendEthCallWithoutParams( + let merkleRoots = await sendEthCallWithParams( ethRpc = g.ethRpc.get(), - functionSignature = "root()", + functionSignature = "getRecentRoots()", + params = @[], fromAddress = g.ethRpc.get().defaultAccount, toAddress = fromHex(Address, g.ethContractAddress), chainId = g.chainId, ) - return merkleRoot + return merkleRoots except CatchableError: - error "Failed to fetch Merkle root", error = getCurrentExceptionMsg() - return err("Failed to fetch merkle root: " & getCurrentExceptionMsg()) + error "Failed to fetch Merkle recent roots list", error = getCurrentExceptionMsg() + return err("Failed to fetch merkle recent roots list: " & getCurrentExceptionMsg()) proc fetchNextFreeIndex*( g: OnchainGroupManager @@ -154,24 +172,117 @@ template retryWrapper( retryWrapper(res, RetryStrategy.new(), errStr, g.onFatalErrorAction): body +# proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} = +# let rootRes = await g.fetchMerkleRoot() +# if rootRes.isErr(): +# return false + +# let merkleRoot = UInt256ToField(rootRes.get()) + +# if g.validRoots.len == 0: +# g.validRoots.addLast(merkleRoot) +# return true + +# if g.validRoots[g.validRoots.len - 1] != merkleRoot: +# if g.validRoots.len > AcceptableRootWindowSize: +# discard g.validRoots.popFirst() +# g.validRoots.addLast(merkleRoot) +# return true + +# return false + proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} = let rootRes = await g.fetchMerkleRoot() + # let rootRes = await g.fetchMerkleRecentRoots() if rootRes.isErr(): + error "Failed to fetch recent roots", error = rootRes.error return false - let merkleRoot = UInt256ToField(rootRes.get()) + let bytes = rootRes.get() + debug "recent roots raw bytes received", length = bytes.len + if (bytes.len mod 32) != 0: + error "Invalid recent roots payload length", length = bytes.len + return false + let chunkCount = bytes.len div 32 + debug "recent roots parsed chunk count", chunks = chunkCount + if chunkCount != 5: + warn "Unexpected number of recent roots returned; proceeding anyway", + count = chunkCount + + # Parse 32-byte chunks into MerkleNode values (UInt256 -> Field bytes) + var newRoots: seq[MerkleNode] = @[] # newest-first order from contract + var zeroIndices: seq[int] = @[] + for i in 0 ..< chunkCount: + let startIdx = i * 32 + let endIdx = (i + 1) * 32 - 1 + var allZero = true + for b in bytes[startIdx .. endIdx]: + if b != 0'u8: + allZero = false + break + if allZero: + zeroIndices.add(i) + let u = UInt256.fromBytesBE(bytes[startIdx .. endIdx]) + let node = UInt256ToField(u) + newRoots.add(node) + + if zeroIndices.len > 0: + debug "zero or empty roots detected in recent roots", + zeroRootsCount = zeroIndices.len, indices = zeroIndices + + # Convert to deque order: oldest-first (so newest ends up at tail) + # Skip zero roots so they are not added to the validRoots list + var newRootsDequeOrder: seq[MerkleNode] = @[] + for i in countdown(newRoots.len - 1, 0): + if zeroIndices.contains(i): + continue + newRootsDequeOrder.add(newRoots[i]) + + if newRootsDequeOrder.len == 0: + debug "no non-zero recent roots to add; skipping update" + return false + + # Determine overlap with existing tail so we only append truly new roots + var overlap = min(g.validRoots.len, newRootsDequeOrder.len) + var matchLen = 0 + # Find the largest n (<= overlap) such that last n of validRoots == first n of newRootsDequeOrder + for n in countdown(overlap, 1): + var ok = true + let startIdx = g.validRoots.len - n + for i in 0 ..< n: + if g.validRoots[startIdx + i] != newRootsDequeOrder[i]: + ok = false + break + if ok: + matchLen = n + break + + let toAdd = newRootsDequeOrder[matchLen ..< newRootsDequeOrder.len] + if toAdd.len == 0: + debug "recent roots already present up-to-date; skipping update" + return false + + # If this is the first time, just seed the deque with all provided roots if g.validRoots.len == 0: - g.validRoots.addLast(merkleRoot) + for r in newRootsDequeOrder: + debug "adding new recent root", root = r + g.validRoots.addLast(r) return true - if g.validRoots[g.validRoots.len - 1] != merkleRoot: - if g.validRoots.len > AcceptableRootWindowSize: - discard g.validRoots.popFirst() - g.validRoots.addLast(merkleRoot) - return true + # Add all new roots to the "top" (tail) and drop the bottom 5 + for r in toAdd: + debug "adding new recent root", root = r + g.validRoots.addLast(r) - return false + var removed = 0 + let addCount = toAdd.len + while removed < addCount and g.validRoots.len > 0: + debug "removing old recent root", root = g.validRoots[0] + discard g.validRoots.popFirst() + inc removed + + return true proc trackRootChanges*(g: OnchainGroupManager) {.async: (raises: [CatchableError]).} = try: From 6d76942950a1244f45c222d6500a9da8715a8e67 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Tue, 28 Oct 2025 09:12:03 +0200 Subject: [PATCH 02/22] Fix error in removing recent roots not checking AcceptableWindowSize --- .../waku_rln_relay/group_manager/on_chain/group_manager.nim | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 9ae45b2de..13c1f700d 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -275,12 +275,10 @@ proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} = debug "adding new recent root", root = r g.validRoots.addLast(r) - var removed = 0 - let addCount = toAdd.len - while removed < addCount and g.validRoots.len > 0: + # Only trim the deque if it exceeds the acceptable window size + while g.validRoots.len > AcceptableRootWindowSize: debug "removing old recent root", root = g.validRoots[0] discard g.validRoots.popFirst() - inc removed return true From b3fe624b1875db5ec5b981df0140889b11ae4b0e Mon Sep 17 00:00:00 2001 From: stubbsta Date: Fri, 22 May 2026 14:12:08 +0200 Subject: [PATCH 03/22] fix merging --- .../group_manager/on_chain/group_manager.nim | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 2af2c3971..995fcdcfe 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -68,10 +68,7 @@ proc fetchMerkleProofElements*( chainId = g.chainId, ) - return response - except CatchableError: - error "Failed to fetch Merkle proof elements", error = getCurrentExceptionMsg() - return err("Failed to fetch merkle proof elements: " & getCurrentExceptionMsg()) + return response # proc fetchMerkleRoot*( # g: OnchainGroupManager @@ -90,22 +87,19 @@ proc fetchMerkleProofElements*( # return err("Failed to fetch merkle root: " & getCurrentExceptionMsg()) ## params = @[] - added empty params as way to get the raw return bytes (seq[bytes]) +## proc fetchMerkleRoot*( g: OnchainGroupManager ): Future[Result[seq[byte], string]] {.async.} = - try: - let merkleRoots = await sendEthCallWithParams( - ethRpc = g.ethRpc.get(), - functionSignature = "getRecentRoots()", - params = @[], - fromAddress = g.ethRpc.get().defaultAccount, - toAddress = fromHex(Address, g.ethContractAddress), - chainId = g.chainId, - ) - return merkleRoots - except CatchableError: - error "Failed to fetch Merkle recent roots list", error = getCurrentExceptionMsg() - return err("Failed to fetch merkle recent roots list: " & getCurrentExceptionMsg()) + let merkleRoots = await sendEthCallWithParams( + ethRpc = g.ethRpc.get(), + functionSignature = "getRecentRoots()", + params = @[], + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) + return merkleRoots proc fetchNextFreeIndex*( g: OnchainGroupManager @@ -152,7 +146,8 @@ proc fetchMaxMembershipRateLimit*( proc checkInitialized(g: OnchainGroupManager): Result[void, string] = if not g.initialized: - raise newException(CatchableError, "OnchainGroupManager is not initialized") + return err("OnchainGroupManager is not initialized") + return ok() template retryWrapper( g: OnchainGroupManager, res: auto, errStr: string, body: untyped @@ -275,27 +270,32 @@ proc trackRootChanges*(g: OnchainGroupManager) {.async: (raises: [CatchableError initializedGuard(g) const rpcDelay = 5.seconds - while true: - await sleepAsync(rpcDelay) - let rootUpdated = await g.updateRoots() + while true: + await sleepAsync(rpcDelay) + let rootUpdated = await g.updateRoots() - if rootUpdated: - ## The membership set on-chain has changed (some new members have joined or some members have left) - if g.membershipIndex.isSome(): - ## A membership index exists only if the node has registered with RLN. - ## Non-registered nodes cannot have Merkle proof elements. - let proofResult = await g.fetchMerkleProofElements() - if proofResult.isErr(): - error "Failed to fetch Merkle proof", error = proofResult.error - else: - g.merkleProofCache = proofResult.get() + if rootUpdated: + ## The membership set on-chain has changed (some new members have joined or some members have left) + if g.membershipIndex.isSome(): + ## A membership index exists only if the node has registered with RLN. + ## Non-registered nodes cannot have Merkle proof elements. + let proofResult = await g.fetchMerkleProofElements() + if proofResult.isErr(): + error "Failed to fetch Merkle proof", error = proofResult.error + else: + g.merkleProofCache = proofResult.get() - let nextFreeIndex = (await g.fetchNextFreeIndex()).valueOr: - error "Failed to fetch next free index", error = error - return err("Failed to fetch next free index: " & error) + let nextFreeIndex = await g.fetchNextFreeIndex() + if nextFreeIndex.isErr(): + error "Failed to fetch next free index", error = nextFreeIndex.error + raise newException( + CatchableError, "Failed to fetch next free index: " & nextFreeIndex.error + ) - let memberCount = cast[int64](nextFreeIndex) - waku_rln_number_registered_memberships.set(float64(memberCount)) + let memberCount = cast[int64](nextFreeIndex.get()) + waku_rln_number_registered_memberships.set(float64(memberCount)) + except CatchableError: + error "Fatal error in trackRootChanges", error = getCurrentExceptionMsg() method register*( g: OnchainGroupManager, rateCommitment: RateCommitment From c52ccac37b0e3ccc81cf4b358ddab7dbe8f80dc0 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Fri, 22 May 2026 14:22:09 +0200 Subject: [PATCH 04/22] more merging fixes --- .../waku_rln_relay/group_manager/on_chain/group_manager.nim | 6 ------ 1 file changed, 6 deletions(-) diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 995fcdcfe..70e3c645b 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -149,12 +149,6 @@ proc checkInitialized(g: OnchainGroupManager): Result[void, string] = return err("OnchainGroupManager is not initialized") return ok() -template retryWrapper( - g: OnchainGroupManager, res: auto, errStr: string, body: untyped -): auto = - retryWrapper(res, RetryStrategy.new(), errStr, g.onFatalErrorAction): - body - # proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} = # let rootRes = await g.fetchMerkleRoot() # if rootRes.isErr(): From 692a000983d723e3dc7435e03dee2c85a4b70367 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Fri, 22 May 2026 14:36:16 +0200 Subject: [PATCH 05/22] merge fixes --- .../group_manager/on_chain/group_manager.nim | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 70e3c645b..c98193034 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -259,37 +259,34 @@ proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} = return true -proc trackRootChanges*(g: OnchainGroupManager) {.async: (raises: [CatchableError]).} = - try: - initializedGuard(g) - const rpcDelay = 5.seconds +proc trackRootChanges*(g: OnchainGroupManager): Future[Result[void, string]] {.async.} = + ?checkInitialized(g) + const rpcDelay = 5.seconds - while true: - await sleepAsync(rpcDelay) - let rootUpdated = await g.updateRoots() + while true: + await sleepAsync(rpcDelay) + let rootUpdated = await g.updateRoots() - if rootUpdated: - ## The membership set on-chain has changed (some new members have joined or some members have left) - if g.membershipIndex.isSome(): - ## A membership index exists only if the node has registered with RLN. - ## Non-registered nodes cannot have Merkle proof elements. - let proofResult = await g.fetchMerkleProofElements() - if proofResult.isErr(): - error "Failed to fetch Merkle proof", error = proofResult.error - else: - g.merkleProofCache = proofResult.get() + if rootUpdated: + ## The membership set on-chain has changed (some new members have joined or some members have left) + if g.membershipIndex.isSome(): + ## A membership index exists only if the node has registered with RLN. + ## Non-registered nodes cannot have Merkle proof elements. + let proofResult = await g.fetchMerkleProofElements() + if proofResult.isErr(): + error "Failed to fetch Merkle proof", error = proofResult.error + else: + g.merkleProofCache = proofResult.get() - let nextFreeIndex = await g.fetchNextFreeIndex() - if nextFreeIndex.isErr(): - error "Failed to fetch next free index", error = nextFreeIndex.error - raise newException( - CatchableError, "Failed to fetch next free index: " & nextFreeIndex.error - ) + let nextFreeIndex = await g.fetchNextFreeIndex() + if nextFreeIndex.isErr(): + error "Failed to fetch next free index", error = nextFreeIndex.error + raise newException( + CatchableError, "Failed to fetch next free index: " & nextFreeIndex.error + ) - let memberCount = cast[int64](nextFreeIndex.get()) - waku_rln_number_registered_memberships.set(float64(memberCount)) - except CatchableError: - error "Fatal error in trackRootChanges", error = getCurrentExceptionMsg() + let memberCount = cast[int64](nextFreeIndex.get()) + waku_rln_number_registered_memberships.set(float64(memberCount)) method register*( g: OnchainGroupManager, rateCommitment: RateCommitment From 5c0b1c1adb28e9e4dae50a5859347a835e0f4f24 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Fri, 22 May 2026 15:05:56 +0200 Subject: [PATCH 06/22] add test for updated merkle roots window --- .../test_rln_group_manager_onchain.nim | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim index 6b5b81532..8a7ad02b9 100644 --- a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim +++ b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim @@ -107,7 +107,8 @@ suite "Onchain group manager": (waitFor manager.init()).isOkOr: raiseAssert $error - let merkleRootBefore = waitFor manager.fetchMerkleRoot() + let merkleRootBefore = (waitFor manager.fetchMerkleRoot()).valueOr: + raiseAssert "Failed to fetch merkle root before: " & error for i in 0 ..< credentials.len(): info "Registering credential", index = i, credential = credentials[i] @@ -115,12 +116,47 @@ suite "Onchain group manager": assert false, "Failed to register credential " & $i & ": " & error discard waitFor manager.updateRoots() - let merkleRootAfter = waitFor manager.fetchMerkleRoot() + let merkleRootAfter = (waitFor manager.fetchMerkleRoot()).valueOr: + raiseAssert "Failed to fetch merkle root after: " & error check: merkleRootBefore != merkleRootAfter manager.validRoots.len() == credentialCount + test "trackRootChanges: oldest roots are evicted once the window is exceeded": + const + initialCount = 5 + additionalCount = 46 + let credentials = generateCredentials(initialCount + additionalCount) + (waitFor manager.init()).isOkOr: + raiseAssert $error + + # Register the first 5 credentials and snapshot the 3 oldest roots. + for i in 0 ..< initialCount: + (waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr: + assert false, "Failed to register credential " & $i & ": " & error + discard waitFor manager.updateRoots() + + check manager.validRoots.len() >= 3 + let firstThreeBefore = + @[manager.validRoots[0], manager.validRoots[1], manager.validRoots[2]] + + # Register the remaining credentials, pushing the deque past AcceptableRootWindowSize. + for i in initialCount ..< credentials.len(): + (waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr: + assert false, "Failed to register credential " & $i & ": " & error + discard waitFor manager.updateRoots() + + let rootsAfter = manager.validRoots.items().toSeq() + + # 51 registrations into a window of 50 evicts exactly the single oldest root, + # so only the first of the original three is gone; the other two remain. + check: + manager.validRoots.len() == AcceptableRootWindowSize + firstThreeBefore[0] notin rootsAfter + firstThreeBefore[1] in rootsAfter + firstThreeBefore[2] in rootsAfter + test "register: should guard against uninitialized state": let dummyCommitment = default(IDCommitment) From 28a7e040cbc188948ba700d185fc1e7a3e1a8d18 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Fri, 22 May 2026 15:50:25 +0200 Subject: [PATCH 07/22] add pr re-add gauge for proof-generation-duration-seconds --- waku/waku_rln_relay/group_manager/on_chain/group_manager.nim | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index c98193034..4a0b1dd75 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -506,8 +506,9 @@ method generateProof*( external_nullifier: extNullifier, ) - let output = generateRlnProofWithWitness(g.rlnInstance, witness, epoch, rlnIdentifier).valueOr: - return err("Failed to generate proof: " & error) + waku_rln_proof_generation_duration_seconds.nanosecondTime: + let output = generateRlnProofWithWitness(g.rlnInstance, witness, epoch, rlnIdentifier).valueOr: + return err("Failed to generate proof: " & error) info "Proof generated successfully", proof = output From 1d6b7ea566664565dd7aa36de64524a18e94e8e1 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Mon, 25 May 2026 08:56:02 +0200 Subject: [PATCH 08/22] Decrease AcceptableRootWindowSize for testing --- waku/waku_rln_relay/constants.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waku/waku_rln_relay/constants.nim b/waku/waku_rln_relay/constants.nim index 8532abaaa..24af7a813 100644 --- a/waku/waku_rln_relay/constants.nim +++ b/waku/waku_rln_relay/constants.nim @@ -5,7 +5,7 @@ import ./protocol_types import ../waku_keystore # Acceptable roots for merkle root validation of incoming messages -const AcceptableRootWindowSize* = 50 +const AcceptableRootWindowSize* = 10 # RLN membership key and index files path const RlnCredentialsFilename* = "rlnCredentials.txt" From c907c29cda27481cb69cfa357b45b8cb59107f97 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Mon, 25 May 2026 10:30:59 +0200 Subject: [PATCH 09/22] debug spam log --- waku/waku_rln_relay/rln_relay.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waku/waku_rln_relay/rln_relay.nim b/waku/waku_rln_relay/rln_relay.nim index 7c36300b2..c0bfa5c6c 100644 --- a/waku/waku_rln_relay/rln_relay.nim +++ b/waku/waku_rln_relay/rln_relay.nim @@ -260,7 +260,7 @@ proc validateMessage*( if hasDup.isErr(): waku_rln_errors_total.inc(labelValues = ["duplicate_check"]) elif hasDup.value == true: - trace "invalid message: message is spam", + debug "invalid message: message is spam", payloadLen = msg.payload.len, contentTopic = msg.contentTopic waku_rln_spam_messages_total.inc() return MessageValidationResult.Spam From 82e597d1c227106c4709b3084d60400be7b940d4 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Mon, 25 May 2026 11:04:44 +0200 Subject: [PATCH 10/22] linting --- tests/waku_rln_relay/test_rln_group_manager_onchain.nim | 2 +- waku/waku_rln_relay/group_manager/on_chain/group_manager.nim | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim index 8a7ad02b9..1c2807d31 100644 --- a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim +++ b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim @@ -126,7 +126,7 @@ suite "Onchain group manager": test "trackRootChanges: oldest roots are evicted once the window is exceeded": const initialCount = 5 - additionalCount = 46 + additionalCount = 6 let credentials = generateCredentials(initialCount + additionalCount) (waitFor manager.init()).isOkOr: raiseAssert $error diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 4a0b1dd75..3b7047de9 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -507,7 +507,9 @@ method generateProof*( ) waku_rln_proof_generation_duration_seconds.nanosecondTime: - let output = generateRlnProofWithWitness(g.rlnInstance, witness, epoch, rlnIdentifier).valueOr: + let output = generateRlnProofWithWitness( + g.rlnInstance, witness, epoch, rlnIdentifier + ).valueOr: return err("Failed to generate proof: " & error) info "Proof generated successfully", proof = output From 7238376fcf45a4ef69c7c484506dbc0c119ae8c5 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Wed, 27 May 2026 14:57:24 +0200 Subject: [PATCH 11/22] start trackRootChanges call loop immediately --- waku/waku_rln_relay/group_manager/on_chain/group_manager.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 3b7047de9..f1382ea5c 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -264,7 +264,6 @@ proc trackRootChanges*(g: OnchainGroupManager): Future[Result[void, string]] {.a const rpcDelay = 5.seconds while true: - await sleepAsync(rpcDelay) let rootUpdated = await g.updateRoots() if rootUpdated: @@ -287,6 +286,7 @@ proc trackRootChanges*(g: OnchainGroupManager): Future[Result[void, string]] {.a let memberCount = cast[int64](nextFreeIndex.get()) waku_rln_number_registered_memberships.set(float64(memberCount)) + await sleepAsync(rpcDelay) method register*( g: OnchainGroupManager, rateCommitment: RateCommitment From 9f95a90e4af2d9dfc5193fdc158b53f2f955fd9d Mon Sep 17 00:00:00 2001 From: stubbsta Date: Wed, 27 May 2026 15:33:27 +0200 Subject: [PATCH 12/22] Fix 5s delay trackRootChanges --- waku/waku_rln_relay/group_manager/on_chain/group_manager.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index f1382ea5c..645740cd4 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -286,7 +286,7 @@ proc trackRootChanges*(g: OnchainGroupManager): Future[Result[void, string]] {.a let memberCount = cast[int64](nextFreeIndex.get()) waku_rln_number_registered_memberships.set(float64(memberCount)) - await sleepAsync(rpcDelay) + await sleepAsync(rpcDelay) method register*( g: OnchainGroupManager, rateCommitment: RateCommitment From 1ff2533765c2c12bb2e7355cce4ec587b13f63cd Mon Sep 17 00:00:00 2001 From: stubbsta Date: Thu, 28 May 2026 09:14:06 +0200 Subject: [PATCH 13/22] set rpcDelay for root tracking to 10s --- waku/waku_rln_relay/group_manager/on_chain/group_manager.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 645740cd4..b6c8e4b58 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -261,7 +261,7 @@ proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} = proc trackRootChanges*(g: OnchainGroupManager): Future[Result[void, string]] {.async.} = ?checkInitialized(g) - const rpcDelay = 5.seconds + const rpcDelay = 10.seconds while true: let rootUpdated = await g.updateRoots() From 3ba630daadd1cc7839f95542f4259d5bb8da9580 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Thu, 28 May 2026 10:57:27 +0200 Subject: [PATCH 14/22] add default params to sendEthCallWithParams --- .../group_manager/on_chain/group_manager.nim | 53 +++++++++---------- .../group_manager/on_chain/rpc_wrapper.nim | 2 +- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index b6c8e4b58..363b86b31 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -62,43 +62,42 @@ proc fetchMerkleProofElements*( let response = await sendEthCallWithParams( ethRpc = g.ethRpc.get(), functionSignature = methodSig, - params = paddedParam, fromAddress = g.ethRpc.get().defaultAccount, toAddress = fromHex(Address, g.ethContractAddress), chainId = g.chainId, + params = paddedParam, ) return response -# proc fetchMerkleRoot*( -# g: OnchainGroupManager -# ): Future[Result[UInt256, string]] {.async.} = -# try: -# let merkleRoot = await sendEthCallWithoutParams( -# ethRpc = g.ethRpc.get(), -# functionSignature = "root()", -# fromAddress = g.ethRpc.get().defaultAccount, -# toAddress = fromHex(Address, g.ethContractAddress), -# chainId = g.chainId, -# ) -# return merkleRoot -# except CatchableError: -# error "Failed to fetch Merkle root", error = getCurrentExceptionMsg() -# return err("Failed to fetch merkle root: " & getCurrentExceptionMsg()) +proc fetchMerkleRoot*( + g: OnchainGroupManager +): Future[Result[UInt256, string]] {.async.} = + try: + let merkleRoot = await sendEthCallWithoutParams( + ethRpc = g.ethRpc.get(), + functionSignature = "root()", + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) + return merkleRoot + except CatchableError: + error "Failed to fetch Merkle root", error = getCurrentExceptionMsg() + return err("Failed to fetch merkle root: " & getCurrentExceptionMsg()) -## params = @[] - added empty params as way to get the raw return bytes (seq[bytes]) -## proc fetchMerkleRoot*( g: OnchainGroupManager ): Future[Result[seq[byte], string]] {.async.} = - let merkleRoots = await sendEthCallWithParams( - ethRpc = g.ethRpc.get(), - functionSignature = "getRecentRoots()", - params = @[], - fromAddress = g.ethRpc.get().defaultAccount, - toAddress = fromHex(Address, g.ethContractAddress), - chainId = g.chainId, - ) + let + # using sendEthCallWithParams to get return type of seq[bytes] for getRecentRoots() function which returns an array of bytes32 (5 recent roots) + merkleRoots = await sendEthCallWithParams( + ethRpc = g.ethRpc.get(), + functionSignature = "getRecentRoots()", + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) return merkleRoots proc fetchNextFreeIndex*( @@ -121,10 +120,10 @@ proc fetchMembershipStatus*( await sendEthCallWithParams( ethRpc = g.ethRpc.get(), functionSignature = "isInMembershipSet(uint256)", - params = params, fromAddress = g.ethRpc.get().defaultAccount, toAddress = fromHex(Address, g.ethContractAddress), chainId = g.chainId, + params = params, ) ).valueOr: return err("Failed to check membership: " & error) diff --git a/waku/waku_rln_relay/group_manager/on_chain/rpc_wrapper.nim b/waku/waku_rln_relay/group_manager/on_chain/rpc_wrapper.nim index 2c47b11fa..a82356af8 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/rpc_wrapper.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/rpc_wrapper.nim @@ -76,10 +76,10 @@ proc sendEthCallWithoutParams*( proc sendEthCallWithParams*( ethRpc: Web3, functionSignature: string, - params: seq[byte], fromAddress: Address, toAddress: Address, chainId: UInt256, + params: seq[byte] = @[], ): Future[Result[seq[byte], string]] {.async.} = ## Workaround for web3 chainId=null issue with parameterized contract calls let functionHash = From 02fc4d488943182f07579a661bc23cc133300a53 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Thu, 28 May 2026 13:32:46 +0200 Subject: [PATCH 15/22] improve recents roots retrieval and logs --- .../test_rln_group_manager_onchain.nim | 45 ++++++++-- .../group_manager/on_chain/group_manager.nim | 85 +++++++------------ 2 files changed, 66 insertions(+), 64 deletions(-) diff --git a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim index 1c2807d31..1123ba0a9 100644 --- a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim +++ b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim @@ -97,7 +97,8 @@ suite "Onchain group manager": check: merkleRootBefore != merkleRootAfter - test "trackRootChanges: should fetch history correctly": + test "trackRootChanges: should fetch history correctly: fetch single root()": + # basic check for the soon to be deprecated root contract function, is replaced by getRecentRoots() # TODO: We can't use `trackRootChanges()` directly in this test because its current implementation # relies on a busy loop rather than event-based monitoring. but that busy loop fetch root every 5 seconds # so we can't use it in this test. @@ -123,6 +124,32 @@ suite "Onchain group manager": merkleRootBefore != merkleRootAfter manager.validRoots.len() == credentialCount + test "trackRootChanges: should fetch history correctly: fetch root cache": + # TODO: We can't use `trackRootChanges()` directly in this test because its current implementation + # relies on a busy loop rather than event-based monitoring. but that busy loop fetch root every 5 seconds + # so we can't use it in this test. + + const credentialCount = 6 + let credentials = generateCredentials(credentialCount) + (waitFor manager.init()).isOkOr: + raiseAssert $error + + let merkleRootCacheBefore = (waitFor manager.fetchMerkleRootsCache()).valueOr: + raiseAssert "Failed to fetch merkle root before: " & error + + for i in 0 ..< credentials.len(): + info "Registering credential", index = i, credential = credentials[i] + (waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr: + assert false, "Failed to register credential " & $i & ": " & error + discard waitFor manager.updateRecentRoots() + + let merkleRootCacheAfter = (waitFor manager.fetchMerkleRootsCache()).valueOr: + raiseAssert "Failed to fetch merkle root after: " & error + + check: + merkleRootCacheBefore != merkleRootCacheAfter + manager.validRoots.len() == credentialCount + test "trackRootChanges: oldest roots are evicted once the window is exceeded": const initialCount = 5 @@ -135,7 +162,7 @@ suite "Onchain group manager": for i in 0 ..< initialCount: (waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr: assert false, "Failed to register credential " & $i & ": " & error - discard waitFor manager.updateRoots() + discard waitFor manager.updateRecentRoots() check manager.validRoots.len() >= 3 let firstThreeBefore = @@ -145,7 +172,7 @@ suite "Onchain group manager": for i in initialCount ..< credentials.len(): (waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr: assert false, "Failed to register credential " & $i & ": " & error - discard waitFor manager.updateRoots() + discard waitFor manager.updateRecentRoots() let rootsAfter = manager.validRoots.items().toSeq() @@ -250,7 +277,7 @@ suite "Onchain group manager": waitFor fut - let rootUpdated = waitFor manager.updateRoots() + let rootUpdated = waitFor manager.updateRecentRoots() if rootUpdated: let proofResult = waitFor manager.fetchMerkleProofElements() @@ -332,7 +359,7 @@ suite "Onchain group manager": assert false, "error returned when calling register: " & error waitFor fut - let rootUpdated = waitFor manager.updateRoots() + let rootUpdated = waitFor manager.updateRecentRoots() if rootUpdated: let proofResult = waitFor manager.fetchMerkleProofElements() @@ -369,7 +396,7 @@ suite "Onchain group manager": let messageBytes = "Hello".toBytes() - let rootUpdated = waitFor manager.updateRoots() + let rootUpdated = waitFor manager.updateRecentRoots() manager.merkleProofCache = newSeq[byte](640) for i in 0 ..< 640: @@ -398,7 +425,7 @@ suite "Onchain group manager": verified == false test "root queue should be updated correctly": - const credentialCount = 12 + const credentialCount = 9 let credentials = generateCredentials(credentialCount) (waitFor manager.init()).isOkOr: raiseAssert $error @@ -427,7 +454,7 @@ suite "Onchain group manager": for i in 0 ..< credentials.len(): (waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr: assert false, "Failed to register credential " & $i & ": " & error - discard waitFor manager.updateRoots() + discard waitFor manager.updateRecentRoots() waitFor allFutures(futures) @@ -472,7 +499,7 @@ suite "Onchain group manager": (waitFor manager.register(credentials, UserMessageLimit(20))).isOkOr: assert false, "register failed: " & error - discard waitFor manager.updateRoots() + discard waitFor manager.updateRecentRoots() let roots = manager.validRoots.items().toSeq() require: roots.len > 0 diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 363b86b31..25ae11f96 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -86,7 +86,7 @@ proc fetchMerkleRoot*( error "Failed to fetch Merkle root", error = getCurrentExceptionMsg() return err("Failed to fetch merkle root: " & getCurrentExceptionMsg()) -proc fetchMerkleRoot*( +proc fetchMerkleRootsCache*( g: OnchainGroupManager ): Future[Result[seq[byte], string]] {.async.} = let @@ -148,72 +148,49 @@ proc checkInitialized(g: OnchainGroupManager): Result[void, string] = return err("OnchainGroupManager is not initialized") return ok() -# proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} = -# let rootRes = await g.fetchMerkleRoot() -# if rootRes.isErr(): -# return false - -# let merkleRoot = UInt256ToField(rootRes.get()) - -# if g.validRoots.len == 0: -# g.validRoots.addLast(merkleRoot) -# return true - -# if g.validRoots[g.validRoots.len - 1] != merkleRoot: -# if g.validRoots.len > AcceptableRootWindowSize: -# discard g.validRoots.popFirst() -# g.validRoots.addLast(merkleRoot) -# return true - -# return false - proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} = let rootRes = await g.fetchMerkleRoot() - # let rootRes = await g.fetchMerkleRecentRoots() + if rootRes.isErr(): + return false + + let merkleRoot = UInt256ToField(rootRes.get()) + + if g.validRoots.len == 0: + g.validRoots.addLast(merkleRoot) + return true + + if g.validRoots[g.validRoots.len - 1] != merkleRoot: + if g.validRoots.len > AcceptableRootWindowSize: + discard g.validRoots.popFirst() + g.validRoots.addLast(merkleRoot) + return true + + return false + +proc updateRecentRoots*(g: OnchainGroupManager): Future[bool] {.async.} = + let rootRes = await g.fetchMerkleRootsCache() if rootRes.isErr(): error "Failed to fetch recent roots", error = rootRes.error return false let bytes = rootRes.get() - debug "recent roots raw bytes received", length = bytes.len if (bytes.len mod 32) != 0: error "Invalid recent roots payload length", length = bytes.len return false let chunkCount = bytes.len div 32 - debug "recent roots parsed chunk count", chunks = chunkCount if chunkCount != 5: warn "Unexpected number of recent roots returned; proceeding anyway", count = chunkCount - # Parse 32-byte chunks into MerkleNode values (UInt256 -> Field bytes) - var newRoots: seq[MerkleNode] = @[] # newest-first order from contract - var zeroIndices: seq[int] = @[] - for i in 0 ..< chunkCount: - let startIdx = i * 32 - let endIdx = (i + 1) * 32 - 1 - var allZero = true - for b in bytes[startIdx .. endIdx]: - if b != 0'u8: - allZero = false - break - if allZero: - zeroIndices.add(i) - let u = UInt256.fromBytesBE(bytes[startIdx .. endIdx]) - let node = UInt256ToField(u) - newRoots.add(node) - - if zeroIndices.len > 0: - debug "zero or empty roots detected in recent roots", - zeroRootsCount = zeroIndices.len, indices = zeroIndices - - # Convert to deque order: oldest-first (so newest ends up at tail) - # Skip zero roots so they are not added to the validRoots list + # Parse 32-byte chunks (contract returns newest-first) into MerkleNode values, + # reversing to oldest-first and skipping zero roots. var newRootsDequeOrder: seq[MerkleNode] = @[] - for i in countdown(newRoots.len - 1, 0): - if zeroIndices.contains(i): + for startIdx in countdown(bytes.len - 32, 0, 32): + let u = UInt256.fromBytesBE(bytes.toOpenArray(startIdx, startIdx + 31)) + if u.isZero: continue - newRootsDequeOrder.add(newRoots[i]) + newRootsDequeOrder.add(UInt256ToField(u)) if newRootsDequeOrder.len == 0: debug "no non-zero recent roots to add; skipping update" @@ -236,24 +213,22 @@ proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} = let toAdd = newRootsDequeOrder[matchLen ..< newRootsDequeOrder.len] if toAdd.len == 0: - debug "recent roots already present up-to-date; skipping update" return false # If this is the first time, just seed the deque with all provided roots if g.validRoots.len == 0: for r in newRootsDequeOrder: - debug "adding new recent root", root = r g.validRoots.addLast(r) + debug "seeded recent roots", count = newRootsDequeOrder.len return true - # Add all new roots to the "top" (tail) and drop the bottom 5 + # Add all new roots to the "top" (tail) and trim to AcceptableRootWindowSize for r in toAdd: - debug "adding new recent root", root = r g.validRoots.addLast(r) + debug "appended recent roots", count = toAdd.len, roots = toAdd - # Only trim the deque if it exceeds the acceptable window size while g.validRoots.len > AcceptableRootWindowSize: - debug "removing old recent root", root = g.validRoots[0] + trace "removing old merkle root", root = g.validRoots[0] discard g.validRoots.popFirst() return true From 19751db182dfc31e3051b01e543a93bbd61292ae Mon Sep 17 00:00:00 2001 From: stubbsta Date: Thu, 28 May 2026 15:18:14 +0200 Subject: [PATCH 16/22] Use updateRecentRoots to track root changes --- waku/waku_rln_relay/group_manager/on_chain/group_manager.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 25ae11f96..e8daa8b98 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -238,7 +238,7 @@ proc trackRootChanges*(g: OnchainGroupManager): Future[Result[void, string]] {.a const rpcDelay = 10.seconds while true: - let rootUpdated = await g.updateRoots() + let rootUpdated = await g.updateRecentRoots() if rootUpdated: ## The membership set on-chain has changed (some new members have joined or some members have left) From ed412023f067cf2e03306bcc6cb759171ac51f5a Mon Sep 17 00:00:00 2001 From: stubbsta Date: Thu, 28 May 2026 15:41:42 +0200 Subject: [PATCH 17/22] simplify updateRecentRoots --- .../group_manager/on_chain/group_manager.nim | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index e8daa8b98..02ec9cc6d 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -90,7 +90,7 @@ proc fetchMerkleRootsCache*( g: OnchainGroupManager ): Future[Result[seq[byte], string]] {.async.} = let - # using sendEthCallWithParams to get return type of seq[bytes] for getRecentRoots() function which returns an array of bytes32 (5 recent roots) + # using sendEthCallWithParams to get return type of seq[bytes] for getRecentRoots() function which returns an array of bytes32 merkleRoots = await sendEthCallWithParams( ethRpc = g.ethRpc.get(), functionSignature = "getRecentRoots()", @@ -149,11 +149,10 @@ proc checkInitialized(g: OnchainGroupManager): Result[void, string] = return ok() proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} = - let rootRes = await g.fetchMerkleRoot() - if rootRes.isErr(): + let rootRes = (await g.fetchMerkleRoot()).valueOr: return false - let merkleRoot = UInt256ToField(rootRes.get()) + let merkleRoot = UInt256ToField(rootRes) if g.validRoots.len == 0: g.validRoots.addLast(merkleRoot) @@ -215,14 +214,7 @@ proc updateRecentRoots*(g: OnchainGroupManager): Future[bool] {.async.} = if toAdd.len == 0: return false - # If this is the first time, just seed the deque with all provided roots - if g.validRoots.len == 0: - for r in newRootsDequeOrder: - g.validRoots.addLast(r) - debug "seeded recent roots", count = newRootsDequeOrder.len - return true - - # Add all new roots to the "top" (tail) and trim to AcceptableRootWindowSize + # Append new roots to the tail; trim happens below if we exceed the window. for r in toAdd: g.validRoots.addLast(r) debug "appended recent roots", count = toAdd.len, roots = toAdd @@ -235,7 +227,8 @@ proc updateRecentRoots*(g: OnchainGroupManager): Future[bool] {.async.} = proc trackRootChanges*(g: OnchainGroupManager): Future[Result[void, string]] {.async.} = ?checkInitialized(g) - const rpcDelay = 10.seconds + + const rpcDelay = 5.seconds while true: let rootUpdated = await g.updateRecentRoots() @@ -251,14 +244,11 @@ proc trackRootChanges*(g: OnchainGroupManager): Future[Result[void, string]] {.a else: g.merkleProofCache = proofResult.get() - let nextFreeIndex = await g.fetchNextFreeIndex() - if nextFreeIndex.isErr(): - error "Failed to fetch next free index", error = nextFreeIndex.error - raise newException( - CatchableError, "Failed to fetch next free index: " & nextFreeIndex.error - ) + let nextFreeIndex = (await g.fetchNextFreeIndex()).valueOr: + error "Failed to fetch next free index", error = error + return err("Failed to fetch next free index: " & error) - let memberCount = cast[int64](nextFreeIndex.get()) + let memberCount = cast[int64](nextFreeIndex) waku_rln_number_registered_memberships.set(float64(memberCount)) await sleepAsync(rpcDelay) From ce412274888e1ffe7da6eadc963db08394623e63 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Fri, 29 May 2026 10:55:10 +0200 Subject: [PATCH 18/22] set root polling to 15s --- waku/waku_rln_relay/group_manager/on_chain/group_manager.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 02ec9cc6d..80387c7da 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -228,7 +228,7 @@ proc updateRecentRoots*(g: OnchainGroupManager): Future[bool] {.async.} = proc trackRootChanges*(g: OnchainGroupManager): Future[Result[void, string]] {.async.} = ?checkInitialized(g) - const rpcDelay = 5.seconds + const rpcDelay = 15.seconds while true: let rootUpdated = await g.updateRecentRoots() From 49700a6302253e17bc924478de70896a8fd247d0 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Fri, 29 May 2026 14:49:14 +0200 Subject: [PATCH 19/22] set rpc poll delay to 30s --- waku/waku_rln_relay/group_manager/on_chain/group_manager.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 80387c7da..00e83b2f1 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -228,7 +228,7 @@ proc updateRecentRoots*(g: OnchainGroupManager): Future[bool] {.async.} = proc trackRootChanges*(g: OnchainGroupManager): Future[Result[void, string]] {.async.} = ?checkInitialized(g) - const rpcDelay = 15.seconds + const rpcDelay = 30.seconds while true: let rootUpdated = await g.updateRecentRoots() From 1142bcc1a898ff99168e7db4bb14010519a1467b Mon Sep 17 00:00:00 2001 From: stubbsta Date: Mon, 1 Jun 2026 10:34:40 +0200 Subject: [PATCH 20/22] set acceptablerootwindowsize and root poll delay --- waku/waku_rln_relay/constants.nim | 2 +- waku/waku_rln_relay/group_manager/on_chain/group_manager.nim | 2 +- waku/waku_rln_relay/rln_relay.nim | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/waku/waku_rln_relay/constants.nim b/waku/waku_rln_relay/constants.nim index 24af7a813..8532abaaa 100644 --- a/waku/waku_rln_relay/constants.nim +++ b/waku/waku_rln_relay/constants.nim @@ -5,7 +5,7 @@ import ./protocol_types import ../waku_keystore # Acceptable roots for merkle root validation of incoming messages -const AcceptableRootWindowSize* = 10 +const AcceptableRootWindowSize* = 50 # RLN membership key and index files path const RlnCredentialsFilename* = "rlnCredentials.txt" diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 00e83b2f1..ff2d54ea5 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -228,7 +228,7 @@ proc updateRecentRoots*(g: OnchainGroupManager): Future[bool] {.async.} = proc trackRootChanges*(g: OnchainGroupManager): Future[Result[void, string]] {.async.} = ?checkInitialized(g) - const rpcDelay = 30.seconds + const rpcDelay = 10.seconds while true: let rootUpdated = await g.updateRecentRoots() diff --git a/waku/waku_rln_relay/rln_relay.nim b/waku/waku_rln_relay/rln_relay.nim index c0bfa5c6c..7c36300b2 100644 --- a/waku/waku_rln_relay/rln_relay.nim +++ b/waku/waku_rln_relay/rln_relay.nim @@ -260,7 +260,7 @@ proc validateMessage*( if hasDup.isErr(): waku_rln_errors_total.inc(labelValues = ["duplicate_check"]) elif hasDup.value == true: - debug "invalid message: message is spam", + trace "invalid message: message is spam", payloadLen = msg.payload.len, contentTopic = msg.contentTopic waku_rln_spam_messages_total.inc() return MessageValidationResult.Spam From 11241d4c4d46cbf28b991a6f30a976515bc6feb5 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Wed, 3 Jun 2026 14:18:06 +0200 Subject: [PATCH 21/22] Improve test 'should fetch history correctly' for root cache --- .../test_rln_group_manager_onchain.nim | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim index 1123ba0a9..07fe21e09 100644 --- a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim +++ b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim @@ -125,17 +125,24 @@ suite "Onchain group manager": manager.validRoots.len() == credentialCount test "trackRootChanges: should fetch history correctly: fetch root cache": + # Verify that the group_manager list of valid roots is updated correctly from the recent roots + # cache as new credentials are registered. # TODO: We can't use `trackRootChanges()` directly in this test because its current implementation # relies on a busy loop rather than event-based monitoring. but that busy loop fetch root every 5 seconds - # so we can't use it in this test. + # so we can't use it in this test. - const credentialCount = 6 + const credentialCount = 5 let credentials = generateCredentials(credentialCount) (waitFor manager.init()).isOkOr: raiseAssert $error let merkleRootCacheBefore = (waitFor manager.fetchMerkleRootsCache()).valueOr: - raiseAssert "Failed to fetch merkle root before: " & error + raiseAssert "Failed to fetch merkle root cache before: " & error + + check: + merkleRootCacheBefore.len == 5 * 32 + merkleRootCacheBefore.allIt(it == 0'u8) + manager.validRoots.len() == 0 for i in 0 ..< credentials.len(): info "Registering credential", index = i, credential = credentials[i] @@ -144,11 +151,13 @@ suite "Onchain group manager": discard waitFor manager.updateRecentRoots() let merkleRootCacheAfter = (waitFor manager.fetchMerkleRootsCache()).valueOr: - raiseAssert "Failed to fetch merkle root after: " & error + raiseAssert "Failed to fetch merkle root cache after: " & error check: - merkleRootCacheBefore != merkleRootCacheAfter + merkleRootCacheAfter.len == 5 * 32 + not merkleRootCacheAfter.allIt(it == 0'u8) manager.validRoots.len() == credentialCount + manager.validRoots.items().toSeq().allIt(it != default(MerkleNode)) test "trackRootChanges: oldest roots are evicted once the window is exceeded": const From 16e17b4a9d76ef496ccd57c61d26e603d4c6af78 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Wed, 3 Jun 2026 14:23:17 +0200 Subject: [PATCH 22/22] Make root cache handling more efficient --- .../group_manager/on_chain/group_manager.nim | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index ff2d54ea5..9799710ec 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -167,12 +167,10 @@ proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} = return false proc updateRecentRoots*(g: OnchainGroupManager): Future[bool] {.async.} = - let rootRes = await g.fetchMerkleRootsCache() - if rootRes.isErr(): - error "Failed to fetch recent roots", error = rootRes.error + let bytes = (await g.fetchMerkleRoot()).valueOr: + error "Failed to fetch current Merkle root", error = error return false - let bytes = rootRes.get() if (bytes.len mod 32) != 0: error "Invalid recent roots payload length", length = bytes.len return false @@ -215,8 +213,7 @@ proc updateRecentRoots*(g: OnchainGroupManager): Future[bool] {.async.} = return false # Append new roots to the tail; trim happens below if we exceed the window. - for r in toAdd: - g.validRoots.addLast(r) + toAdd.mapIt(g.validRoots.addLast(it)) debug "appended recent roots", count = toAdd.len, roots = toAdd while g.validRoots.len > AcceptableRootWindowSize: