Add root poll if root invalid and retry

This commit is contained in:
stubbsta 2026-06-04 14:00:59 +02:00
parent 2f0f346d9e
commit 4fbe2c81c3
No known key found for this signature in database
5 changed files with 128 additions and 18 deletions

View File

@ -307,7 +307,7 @@ suite "Onchain group manager":
validProofRes.isOk()
let validProof = validProofRes.get()
let validated = manager.validateRoot(validProof.merkleRoot)
let validated = waitFor manager.validateRoot(validProof.merkleRoot)
check:
validated
@ -343,11 +343,90 @@ suite "Onchain group manager":
validProofRes.isOk()
let validProof = validProofRes.get()
let validated = manager.validateRoot(validProof.merkleRoot)
let validated = waitFor manager.validateRoot(validProof.merkleRoot)
check:
validated == false
test "validateRoot: refreshes from on-chain when root is unknown locally":
# Without an explicit updateRecentRoots, validateRoot must detect a
# missing root, refresh the local window from the contract cache, and
# find the root there.
const credentialCount = 3
let credentials = generateCredentials(credentialCount)
(waitFor manager.init()).isOkOr:
raiseAssert $error
for i in 0 ..< credentials.len():
(waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr:
assert false, "Failed to register credential " & $i & ": " & error
let currentRoot = (waitFor manager.fetchMerkleRoot()).valueOr:
raiseAssert "Failed to fetch merkle root: " & error
let rootField = UInt256ToField(currentRoot)
check manager.validRoots.len() == 0
let validated = waitFor manager.validateRoot(rootField)
check:
validated
manager.validRoots.len() > 0
test "validateRoot: debounces refresh requests within the interval":
# After one refresh, a second miss within RootRefreshDebounceInterval
# must return false without issuing another RPC — observable via
# lastRefreshAt staying pinned to the first refresh's timestamp.
let credentials = generateCredentials()
(waitFor manager.init()).isOkOr:
raiseAssert $error
(waitFor manager.register(credentials, UserMessageLimit(20))).isOkOr:
assert false, "register failed: " & error
var bogusRoot: MerkleNode
for i in 0 ..< bogusRoot.len:
bogusRoot[i] = 0xAB'u8
let firstResult = waitFor manager.validateRoot(bogusRoot)
let firstRefreshAt = manager.lastRefreshAt
let secondResult = waitFor manager.validateRoot(bogusRoot)
check:
not firstResult
not secondResult
manager.lastRefreshAt == firstRefreshAt
test "validateRoot: concurrent misses share a single in-flight refresh":
# Two validateRoot calls kicked off before the first's refresh resolves
# must both end up `true` for the new root. Without coalescing, the
# second call would fall through to the debounce gate and return false.
const credentialCount = 3
let credentials = generateCredentials(credentialCount)
(waitFor manager.init()).isOkOr:
raiseAssert $error
for i in 0 ..< credentials.len():
(waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr:
assert false, "Failed to register credential " & $i & ": " & error
let currentRoot = (waitFor manager.fetchMerkleRoot()).valueOr:
raiseAssert "Failed to fetch merkle root: " & error
let rootField = UInt256ToField(currentRoot)
check manager.validRoots.len() == 0
let f1 = manager.validateRoot(rootField)
let f2 = manager.validateRoot(rootField)
waitFor allFutures(f1, f2)
check:
f1.read()
f2.read()
manager.validRoots.len() > 0
test "verifyProof: should verify valid proof":
let credentials = generateCredentials()
(waitFor manager.init()).isOkOr:

View File

@ -269,13 +269,13 @@ suite "Waku rln relay":
# Validate messages
let
msgValidate1 = wakuRlnRelay.validateMessageAndUpdateLog(wm1)
msgValidate1 = await wakuRlnRelay.validateMessageAndUpdateLog(wm1)
# wm2 is within the same epoch as wm1 → should be spam
msgValidate2 = wakuRlnRelay.validateMessageAndUpdateLog(wm2)
msgValidate2 = await wakuRlnRelay.validateMessageAndUpdateLog(wm2)
# wm3 is in the next epoch → should be valid
msgValidate3 = wakuRlnRelay.validateMessageAndUpdateLog(wm3)
msgValidate3 = await wakuRlnRelay.validateMessageAndUpdateLog(wm3)
# wm4 has no RLN proof → should be invalid
msgValidate4 = wakuRlnRelay.validateMessageAndUpdateLog(wm4)
msgValidate4 = await wakuRlnRelay.validateMessageAndUpdateLog(wm4)
check:
msgValidate1 == MessageValidationResult.Valid
@ -323,12 +323,12 @@ suite "Waku rln relay":
raiseAssert $error
# validate the first message because it's timestamp is the same as the generated timestamp
let msgValidate1 = wakuRlnRelay.validateMessageAndUpdateLog(wm1)
let msgValidate1 = await wakuRlnRelay.validateMessageAndUpdateLog(wm1)
# wait for 2 seconds to make the timestamp different from generated timestamp
await sleepAsync(2.seconds)
let msgValidate2 = wakuRlnRelay.validateMessageAndUpdateLog(wm2)
let msgValidate2 = await wakuRlnRelay.validateMessageAndUpdateLog(wm2)
check:
msgValidate1 == MessageValidationResult.Valid
@ -378,8 +378,8 @@ suite "Waku rln relay":
raiseAssert $error
let
msgValidate1 = wakuRlnRelay1.validateMessageAndUpdateLog(wm1)
msgValidate2 = wakuRlnRelay1.validateMessageAndUpdateLog(wm2)
msgValidate1 = await wakuRlnRelay1.validateMessageAndUpdateLog(wm1)
msgValidate2 = await wakuRlnRelay1.validateMessageAndUpdateLog(wm2)
check:
msgValidate1 == MessageValidationResult.Valid

View File

@ -121,9 +121,7 @@ method indexOfRoot*(
## returns the index of the root in the merkle tree and returns -1 if the root is not found
return g.validRoots.find(root)
method validateRoot*(
g: GroupManager, root: MerkleNode
): bool {.base, gcsafe, raises: [].} =
method validateRoot*(g: GroupManager, root: MerkleNode): Future[bool] {.base, async.} =
## validates the root against the valid roots queue
return g.indexOfRoot(root) >= 0

View File

@ -28,6 +28,10 @@ export group_manager_base
logScope:
topics = "waku rln_relay onchain_group_manager"
const RootRefreshDebounceInterval* = 1.seconds
## Minimum gap between on-demand recent-roots refreshes triggered by
## `validateRoot` misses, to bound contract-call rate under spam.
type
WakuRlnContractWithSender = Sender[WakuRlnContract]
OnchainGroupManager* = ref object of GroupManager
@ -43,6 +47,10 @@ type
registrationHandler*: Option[RegistrationHandler]
latestProcessedBlock*: BlockNumber
merkleProofCache*: seq[byte]
lastRefreshAt*: Moment
pendingRefresh*: Future[bool]
## Non-nil while an on-demand recent-roots refresh is in flight, so that
## concurrent `validateRoot` misses can ride along on a single RPC.
# The below code is not working with the latest web3 version due to chainId being null (specifically on linea-sepolia)
# TODO: find better solution than this custom sendEthCallWithoutParams call
@ -474,6 +482,31 @@ method generateProof*(
waku_rln_total_generated_proofs.inc()
return ok(output)
method validateRoot*(g: OnchainGroupManager, root: MerkleNode): Future[bool] {.async.} =
## Validates the root against the local valid roots window. If the root is
## not found, refresh the local window from the on-chain recent-roots cache
## and re-check once before giving up. Concurrent misses share a single
## in-flight refresh; new refreshes are debounced by
## `RootRefreshDebounceInterval` to bound contract-call rate.
if g.indexOfRoot(root) >= 0:
return true
# Coalesce: if a refresh is already in flight, ride along instead of starting a new one.
if not g.pendingRefresh.isNil and not g.pendingRefresh.finished:
discard await g.pendingRefresh
return g.indexOfRoot(root) >= 0
# Debounce: don't queue another refresh too soon after the previous one.
let now = Moment.now()
if now - g.lastRefreshAt < RootRefreshDebounceInterval:
return false
g.lastRefreshAt = now
g.pendingRefresh = g.updateRecentRoots()
discard await g.pendingRefresh
return g.indexOfRoot(root) >= 0
method verifyProof*(
g: OnchainGroupManager, input: seq[byte], proof: RateLimitProof
): GroupManagerResult[bool] {.gcsafe.} =

View File

@ -177,7 +177,7 @@ proc toRLNSignal*(wakumessage: WakuMessage): seq[byte] =
proc validateMessage*(
rlnPeer: WakuRLNRelay, msg: WakuMessage
): MessageValidationResult =
): Future[MessageValidationResult] {.async.} =
## validate the supplied `msg` based on the waku-rln-relay routing protocol i.e.,
## the `msg`'s epoch is within MaxEpochGap of the current epoch
## the `msg` has valid rate limit proof
@ -217,7 +217,7 @@ proc validateMessage*(
waku_rln_invalid_messages_total.inc(labelValues = ["timestamp_mismatch"])
return MessageValidationResult.Invalid
let rootValidationRes = rlnPeer.groupManager.validateRoot(proof.merkleRoot)
let rootValidationRes = await rlnPeer.groupManager.validateRoot(proof.merkleRoot)
if not rootValidationRes:
warn "invalid message: provided root does not belong to acceptable window of roots",
provided = proof.merkleRoot.inHex(),
@ -272,11 +272,11 @@ proc validateMessage*(
proc validateMessageAndUpdateLog*(
rlnPeer: WakuRLNRelay, msg: WakuMessage
): MessageValidationResult =
): Future[MessageValidationResult] {.async.} =
## validates the message and updates the log to prevent double messaging
## in future messages
let isValidMessage = rlnPeer.validateMessage(msg)
let isValidMessage = await rlnPeer.validateMessage(msg)
let msgProof = RateLimitProof.init(msg.proof).valueOr:
return MessageValidationResult.Invalid
@ -353,7 +353,7 @@ proc generateRlnValidator*(
return pubsub.ValidationResult.Reject
# validate the message and update log
let validationRes = wakuRlnRelay.validateMessageAndUpdateLog(message)
let validationRes = await wakuRlnRelay.validateMessageAndUpdateLog(message)
let
proof = byteutils.toHex(msgProof.proof)