Skip to content

Commit 86e424c

Browse files
authored
chore: retrieve cache of merkle roots from RLN contract (#3903)
* Only add new roots, not all received * Fix error in removing recent roots not checking AcceptableWindowSize * fix merging * more merging fixes * merge fixes * add test for updated merkle roots window * add pr re-add gauge for proof-generation-duration-seconds * Decrease AcceptableRootWindowSize for testing * debug spam log * linting * start trackRootChanges call loop immediately * Fix 5s delay trackRootChanges * set rpcDelay for root tracking to 10s * add default params to sendEthCallWithParams * improve recents roots retrieval and logs * Use updateRecentRoots to track root changes * simplify updateRecentRoots * set root polling to 15s * set rpc poll delay to 30s * set acceptablerootwindowsize and root poll delay * Improve test 'should fetch history correctly' for root cache * Make root cache handling more efficient * add contract root cache size as constant and function use fix * updateRecentRoots comments update * Update group_manager and tests * fix linting
1 parent deb6929 commit 86e424c

4 files changed

Lines changed: 173 additions & 25 deletions

File tree

tests/waku_rln_relay/test_rln_group_manager_onchain.nim

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ suite "Onchain group manager":
9797
check:
9898
merkleRootBefore != merkleRootAfter
9999

100-
test "trackRootChanges: should fetch history correctly":
100+
test "trackRootChanges: should fetch history correctly: fetch single root()":
101+
# basic check for the soon to be deprecated root contract function, is replaced by getRecentRoots()
101102
# TODO: We can't use `trackRootChanges()` directly in this test because its current implementation
102103
# relies on a busy loop rather than event-based monitoring. but that busy loop fetch root every 5 seconds
103104
# so we can't use it in this test.
@@ -107,20 +108,92 @@ suite "Onchain group manager":
107108
(waitFor manager.init()).isOkOr:
108109
raiseAssert $error
109110

110-
let merkleRootBefore = waitFor manager.fetchMerkleRoot()
111+
let merkleRootBefore = (waitFor manager.fetchMerkleRoot()).valueOr:
112+
raiseAssert "Failed to fetch merkle root before: " & error
111113

112114
for i in 0 ..< credentials.len():
113115
info "Registering credential", index = i, credential = credentials[i]
114116
(waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr:
115117
assert false, "Failed to register credential " & $i & ": " & error
116118
discard waitFor manager.updateRoots()
117119

118-
let merkleRootAfter = waitFor manager.fetchMerkleRoot()
120+
let merkleRootAfter = (waitFor manager.fetchMerkleRoot()).valueOr:
121+
raiseAssert "Failed to fetch merkle root after: " & error
119122

120123
check:
121124
merkleRootBefore != merkleRootAfter
122125
manager.validRoots.len() == credentialCount
123126

127+
test "trackRootChanges: should fetch history correctly: fetch root cache":
128+
# Verify that the group_manager list of valid roots is updated correctly from the recent roots
129+
# cache as new credentials are registered.
130+
# TODO: We can't use `trackRootChanges()` directly in this test because its current implementation
131+
# relies on a busy loop rather than event-based monitoring. but that busy loop fetch root every 5 seconds
132+
# so we can't use it in this test.
133+
134+
const credentialCount = RlnContractRootCacheSize
135+
let credentials = generateCredentials(credentialCount)
136+
(waitFor manager.init()).isOkOr:
137+
raiseAssert $error
138+
139+
let merkleRootCacheBefore = (waitFor manager.fetchMerkleRootsCache()).valueOr:
140+
raiseAssert "Failed to fetch merkle root cache before: " & error
141+
142+
check:
143+
merkleRootCacheBefore.len == RlnContractRootCacheSize * 32
144+
merkleRootCacheBefore.allIt(it == 0'u8)
145+
manager.validRoots.len() == 0
146+
147+
for i in 0 ..< credentials.len():
148+
info "Registering credential", index = i, credential = credentials[i]
149+
(waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr:
150+
assert false, "Failed to register credential " & $i & ": " & error
151+
discard waitFor manager.updateRecentRoots()
152+
153+
let merkleRootCacheAfter = (waitFor manager.fetchMerkleRootsCache()).valueOr:
154+
raiseAssert "Failed to fetch merkle root cache after: " & error
155+
156+
check:
157+
merkleRootCacheAfter.len == RlnContractRootCacheSize * 32
158+
not merkleRootCacheAfter.allIt(it == 0'u8)
159+
manager.validRoots.len() == credentialCount
160+
manager.validRoots.items().toSeq().allIt(it != default(MerkleNode))
161+
162+
test "trackRootChanges: oldest roots are evicted once the window is exceeded":
163+
const
164+
initialCount = AcceptableRootWindowSize - RlnContractRootCacheSize
165+
additionalCount = RlnContractRootCacheSize + 1
166+
# one more than the cache size to ensure eviction occurs
167+
let credentials = generateCredentials(initialCount + additionalCount)
168+
(waitFor manager.init()).isOkOr:
169+
raiseAssert $error
170+
171+
# Register the first credentials and snapshot the 3 oldest roots.
172+
for i in 0 ..< initialCount:
173+
(waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr:
174+
assert false, "Failed to register credential " & $i & ": " & error
175+
discard waitFor manager.updateRecentRoots()
176+
177+
check manager.validRoots.len() >= 3
178+
let firstThreeBefore =
179+
@[manager.validRoots[0], manager.validRoots[1], manager.validRoots[2]]
180+
181+
# Register the remaining credentials, pushing the deque past AcceptableRootWindowSize.
182+
for i in initialCount ..< credentials.len():
183+
(waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr:
184+
assert false, "Failed to register credential " & $i & ": " & error
185+
discard waitFor manager.updateRecentRoots()
186+
187+
let rootsAfter = manager.validRoots.items().toSeq()
188+
189+
# AcceptableRootWindowSize + 1 registrations evicts exactly the single oldest root,
190+
# so only the first of the original three is gone; the other two remain.
191+
check:
192+
manager.validRoots.len() == AcceptableRootWindowSize
193+
firstThreeBefore[0] notin rootsAfter
194+
firstThreeBefore[1] in rootsAfter
195+
firstThreeBefore[2] in rootsAfter
196+
124197
test "register: should guard against uninitialized state":
125198
let dummyCommitment = default(IDCommitment)
126199

@@ -214,7 +287,7 @@ suite "Onchain group manager":
214287

215288
waitFor fut
216289

217-
let rootUpdated = waitFor manager.updateRoots()
290+
let rootUpdated = waitFor manager.updateRecentRoots()
218291

219292
if rootUpdated:
220293
let proofResult = waitFor manager.fetchMerkleProofElements()
@@ -296,7 +369,7 @@ suite "Onchain group manager":
296369
assert false, "error returned when calling register: " & error
297370
waitFor fut
298371

299-
let rootUpdated = waitFor manager.updateRoots()
372+
let rootUpdated = waitFor manager.updateRecentRoots()
300373

301374
if rootUpdated:
302375
let proofResult = waitFor manager.fetchMerkleProofElements()
@@ -333,7 +406,7 @@ suite "Onchain group manager":
333406

334407
let messageBytes = "Hello".toBytes()
335408

336-
let rootUpdated = waitFor manager.updateRoots()
409+
let rootUpdated = waitFor manager.updateRecentRoots()
337410

338411
manager.merkleProofCache = newSeq[byte](640)
339412
for i in 0 ..< 640:
@@ -362,7 +435,7 @@ suite "Onchain group manager":
362435
verified == false
363436

364437
test "root queue should be updated correctly":
365-
const credentialCount = 12
438+
const credentialCount = 9
366439
let credentials = generateCredentials(credentialCount)
367440
(waitFor manager.init()).isOkOr:
368441
raiseAssert $error
@@ -391,7 +464,7 @@ suite "Onchain group manager":
391464
for i in 0 ..< credentials.len():
392465
(waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr:
393466
assert false, "Failed to register credential " & $i & ": " & error
394-
discard waitFor manager.updateRoots()
467+
discard waitFor manager.updateRecentRoots()
395468

396469
waitFor allFutures(futures)
397470

@@ -436,7 +509,7 @@ suite "Onchain group manager":
436509
(waitFor manager.register(credentials, UserMessageLimit(20))).isOkOr:
437510
assert false, "register failed: " & error
438511

439-
discard waitFor manager.updateRoots()
512+
discard waitFor manager.updateRecentRoots()
440513
let roots = manager.validRoots.items().toSeq()
441514
require:
442515
roots.len > 0

waku/waku_rln_relay/constants.nim

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import ../waku_keystore
77
# Acceptable roots for merkle root validation of incoming messages
88
const AcceptableRootWindowSize* = 50
99

10+
#Size if RLN contract root cache
11+
const RlnContractRootCacheSize* = 5
12+
1013
# RLN membership key and index files path
1114
const RlnCredentialsFilename* = "rlnCredentials.txt"
1215

waku/waku_rln_relay/group_manager/on_chain/group_manager.nim

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,25 +62,43 @@ proc fetchMerkleProofElements*(
6262
let response = await sendEthCallWithParams(
6363
ethRpc = g.ethRpc.get(),
6464
functionSignature = methodSig,
65-
params = paddedParam,
6665
fromAddress = g.ethRpc.get().defaultAccount,
6766
toAddress = fromHex(Address, g.ethContractAddress),
6867
chainId = g.chainId,
68+
params = paddedParam,
6969
)
7070

7171
return response
7272

7373
proc fetchMerkleRoot*(
7474
g: OnchainGroupManager
7575
): Future[Result[UInt256, string]] {.async.} =
76-
let merkleRoot = await sendEthCallWithoutParams(
77-
ethRpc = g.ethRpc.get(),
78-
functionSignature = "root()",
79-
fromAddress = g.ethRpc.get().defaultAccount,
80-
toAddress = fromHex(Address, g.ethContractAddress),
81-
chainId = g.chainId,
82-
)
83-
return merkleRoot
76+
try:
77+
let merkleRoot = await sendEthCallWithoutParams(
78+
ethRpc = g.ethRpc.get(),
79+
functionSignature = "root()",
80+
fromAddress = g.ethRpc.get().defaultAccount,
81+
toAddress = fromHex(Address, g.ethContractAddress),
82+
chainId = g.chainId,
83+
)
84+
return merkleRoot
85+
except CatchableError:
86+
error "Failed to fetch Merkle root", error = getCurrentExceptionMsg()
87+
return err("Failed to fetch merkle root: " & getCurrentExceptionMsg())
88+
89+
proc fetchMerkleRootsCache*(
90+
g: OnchainGroupManager
91+
): Future[Result[seq[byte], string]] {.async.} =
92+
let
93+
# using sendEthCallWithParams to get return type of seq[bytes] for getRecentRoots() function which returns an array of bytes32
94+
merkleRoots = await sendEthCallWithParams(
95+
ethRpc = g.ethRpc.get(),
96+
functionSignature = "getRecentRoots()",
97+
fromAddress = g.ethRpc.get().defaultAccount,
98+
toAddress = fromHex(Address, g.ethContractAddress),
99+
chainId = g.chainId,
100+
)
101+
return merkleRoots
84102

85103
proc fetchNextFreeIndex*(
86104
g: OnchainGroupManager
@@ -102,10 +120,10 @@ proc fetchMembershipStatus*(
102120
await sendEthCallWithParams(
103121
ethRpc = g.ethRpc.get(),
104122
functionSignature = "isInMembershipSet(uint256)",
105-
params = params,
106123
fromAddress = g.ethRpc.get().defaultAccount,
107124
toAddress = fromHex(Address, g.ethContractAddress),
108125
chainId = g.chainId,
126+
params = params,
109127
)
110128
).valueOr:
111129
return err("Failed to check membership: " & error)
@@ -148,14 +166,64 @@ proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} =
148166

149167
return false
150168

169+
proc updateRecentRoots*(g: OnchainGroupManager): Future[bool] {.async.} =
170+
## Fetch recent roots from the contract roots cache and update the validRoots deque, ensuring we maintain a window of unique acceptable roots.
171+
## Contract returns array of uint256 roots, newest first, zero-padded to the cache size (e.g. 5).
172+
let bytes = (await g.fetchMerkleRootsCache()).valueOr:
173+
error "Failed to fetch current Merkle root", error = error
174+
return false
175+
176+
if (bytes.len mod 32) != 0:
177+
error "Invalid recent roots payload length", length = bytes.len
178+
return false
179+
180+
let chunkCount = bytes.len div 32
181+
if chunkCount != RlnContractRootCacheSize:
182+
warn "Unexpected number of recent roots returned; proceeding anyway",
183+
count = chunkCount
184+
185+
# Parse 32-byte chunks (contract returns newest-first) into MerkleNode values,
186+
# reversing to oldest-first and skipping zero roots.
187+
var newRootsDequeOrder: seq[MerkleNode] = @[]
188+
for startIdx in countdown(bytes.len - 32, 0, 32):
189+
let u = UInt256.fromBytesBE(bytes.toOpenArray(startIdx, startIdx + 31))
190+
if u.isZero:
191+
continue
192+
newRootsDequeOrder.add(UInt256ToField(u))
193+
194+
if newRootsDequeOrder.len == 0:
195+
debug "no non-zero recent roots to add; skipping update"
196+
return false
197+
198+
# Determine overlap with existing tail so we only append truly new roots
199+
let overlap = min(g.validRoots.len, newRootsDequeOrder.len)
200+
var matchLen = 0
201+
for startIdx in (g.validRoots.len - overlap) ..< g.validRoots.len:
202+
if g.validRoots[startIdx] == newRootsDequeOrder[0]:
203+
matchLen = g.validRoots.len - startIdx
204+
break
205+
206+
let toAdd = newRootsDequeOrder[matchLen ..< newRootsDequeOrder.len]
207+
if toAdd.len == 0:
208+
return false
209+
210+
# Append new roots to the tail; trim happens below if we exceed the window.
211+
for root in toAdd:
212+
g.validRoots.addLast(root)
213+
debug "appended recent roots to list of valid roots", count = toAdd.len, roots = toAdd
214+
215+
while g.validRoots.len > AcceptableRootWindowSize:
216+
discard g.validRoots.popFirst()
217+
218+
return true
219+
151220
proc trackRootChanges*(g: OnchainGroupManager): Future[Result[void, string]] {.async.} =
152221
?checkInitialized(g)
153222

154-
const rpcDelay = 5.seconds
223+
const rpcDelay = 10.seconds
155224

156225
while true:
157-
await sleepAsync(rpcDelay)
158-
let rootUpdated = await g.updateRoots()
226+
let rootUpdated = await g.updateRecentRoots()
159227

160228
if rootUpdated:
161229
## The membership set on-chain has changed (some new members have joined or some members have left)
@@ -174,6 +242,7 @@ proc trackRootChanges*(g: OnchainGroupManager): Future[Result[void, string]] {.a
174242

175243
let memberCount = cast[int64](nextFreeIndex)
176244
waku_rln_number_registered_memberships.set(float64(memberCount))
245+
await sleepAsync(rpcDelay)
177246

178247
method register*(
179248
g: OnchainGroupManager, rateCommitment: RateCommitment
@@ -393,8 +462,11 @@ method generateProof*(
393462
external_nullifier: extNullifier,
394463
)
395464

396-
let output = generateRlnProofWithWitness(g.rlnInstance, witness, epoch, rlnIdentifier).valueOr:
397-
return err("Failed to generate proof: " & error)
465+
waku_rln_proof_generation_duration_seconds.nanosecondTime:
466+
let output = generateRlnProofWithWitness(
467+
g.rlnInstance, witness, epoch, rlnIdentifier
468+
).valueOr:
469+
return err("Failed to generate proof: " & error)
398470

399471
info "Proof generated successfully", proof = output
400472

waku/waku_rln_relay/group_manager/on_chain/rpc_wrapper.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ proc sendEthCallWithoutParams*(
7676
proc sendEthCallWithParams*(
7777
ethRpc: Web3,
7878
functionSignature: string,
79-
params: seq[byte],
8079
fromAddress: Address,
8180
toAddress: Address,
8281
chainId: UInt256,
82+
params: seq[byte] = @[],
8383
): Future[Result[seq[byte], string]] {.async.} =
8484
## Workaround for web3 chainId=null issue with parameterized contract calls
8585
let functionHash =

0 commit comments

Comments
 (0)