From c02b483ae9d73274d96c006b72f9f86d1f8c211f Mon Sep 17 00:00:00 2001 From: stubbsta Date: Mon, 22 Jun 2026 15:25:56 +0200 Subject: [PATCH] move rln specific procs and types --- logos_delivery/waku/waku_rln_relay/config.nim | 26 +++ .../waku/waku_rln_relay/nullifier_log.nim | 75 ++++++++ logos_delivery/waku/waku_rln_relay/proof.nim | 65 +++++++ .../waku/waku_rln_relay/rln_relay.nim | 169 ++---------------- logos_delivery/waku/waku_rln_relay/types.nim | 22 +++ 5 files changed, 198 insertions(+), 159 deletions(-) create mode 100644 logos_delivery/waku/waku_rln_relay/config.nim create mode 100644 logos_delivery/waku/waku_rln_relay/nullifier_log.nim create mode 100644 logos_delivery/waku/waku_rln_relay/proof.nim create mode 100644 logos_delivery/waku/waku_rln_relay/types.nim diff --git a/logos_delivery/waku/waku_rln_relay/config.nim b/logos_delivery/waku/waku_rln_relay/config.nim new file mode 100644 index 000000000..a1682cf9e --- /dev/null +++ b/logos_delivery/waku/waku_rln_relay/config.nim @@ -0,0 +1,26 @@ +{.push raises: [].} + +import std/options, stint + +import logos_delivery/waku/common/error_handling + +type RlnRelayCreds* {.requiresInit.} = object + path*: string + password*: string + +type RlnRelayConf* = object of RootObj + # TODO: severals parameters are only needed when it's dynamic + # change the config to either nest or use enum/type variant so it's obvious + # and then it can be set to `requiresInit` + dynamic*: bool + credIndex*: Option[uint] + ethContractAddress*: string + ethClientUrls*: seq[string] + chainId*: UInt256 + creds*: Option[RlnRelayCreds] + epochSizeSec*: uint64 + userMessageLimit*: uint64 + ethPrivateKey*: Option[string] + +type WakuRlnConfig* = object of RlnRelayConf + onFatalErrorAction*: OnFatalErrorHandler diff --git a/logos_delivery/waku/waku_rln_relay/nullifier_log.nim b/logos_delivery/waku/waku_rln_relay/nullifier_log.nim new file mode 100644 index 000000000..7831db7be --- /dev/null +++ b/logos_delivery/waku/waku_rln_relay/nullifier_log.nim @@ -0,0 +1,75 @@ +{.push raises: [].} + +import std/tables +import chronicles, results + +import ./types, ./protocol_types, ./conversion_utils, ./proof + +logScope: + topics = "waku rln_relay nullifier_log" + +proc hasDuplicate*( + rlnPeer: WakuRLNRelay, epoch: Epoch, proofMetadata: ProofMetadata +): RlnRelayResult[bool] = + ## returns true if there is another message in the `nullifierLog` of the `rlnPeer` with the same + ## epoch and nullifier as `proofMetadata`'s epoch and nullifier + ## otherwise, returns false + ## Returns an error if it cannot check for duplicates + + # check if the epoch exists + let nullifier = proofMetadata.nullifier + if not rlnPeer.nullifierLog.hasKey(epoch): + return ok(false) + try: + if rlnPeer.nullifierLog[epoch].hasKey(nullifier): + # there is an identical record, mark it as spam + return ok(true) + + # there is no duplicate + return ok(false) + except KeyError: + return err("the epoch was not found: " & getCurrentExceptionMsg()) + +proc updateLog*( + rlnPeer: WakuRLNRelay, epoch: Epoch, proofMetadata: ProofMetadata +): RlnRelayResult[void] = + ## saves supplied proofMetadata `proofMetadata` + ## in the `nullifierLog` of the `rlnPeer` + ## Returns an error if it cannot update the log + + # check if the epoch exists + if not rlnPeer.nullifierLog.hasKeyOrPut( + epoch, {proofMetadata.nullifier: proofMetadata}.toTable() + ): + return ok() + + try: + # check if an identical record exists + if rlnPeer.nullifierLog[epoch].hasKeyOrPut(proofMetadata.nullifier, proofMetadata): + # the above condition could be `discarded` but it is kept for clarity, that slashing will + # be implemented here + # TODO: slashing logic + return ok() + return ok() + except KeyError: + return + err("the epoch was not found: " & getCurrentExceptionMsg()) # should never happen + +proc clearNullifierLog*(rlnPeer: WakuRlnRelay) = + # clear the first MaxEpochGap epochs of the nullifer log + # if more than MaxEpochGap epochs are in the log + let currentEpoch = fromEpoch(rlnPeer.getCurrentEpoch()) + + var epochsToRemove: seq[Epoch] = @[] + for epoch in rlnPeer.nullifierLog.keys(): + let epochInt = fromEpoch(epoch) + + # clean all epochs that are +- rlnMaxEpochGap from the current epoch + if (currentEpoch + rlnPeer.rlnMaxEpochGap) <= epochInt or + epochInt <= (currentEpoch - rlnPeer.rlnMaxEpochGap): + epochsToRemove.add(epoch) + + for epochRemove in epochsToRemove: + trace "clearing epochs from the nullifier log", + currentEpoch = currentEpoch, cleanedEpoch = fromEpoch(epochRemove) + rlnPeer.nullifierLog.del(epochRemove) diff --git a/logos_delivery/waku/waku_rln_relay/proof.nim b/logos_delivery/waku/waku_rln_relay/proof.nim new file mode 100644 index 000000000..1b10eec5e --- /dev/null +++ b/logos_delivery/waku/waku_rln_relay/proof.nim @@ -0,0 +1,65 @@ +{.push raises: [].} + +import std/[times, sequtils] +import chronos, results, stew/byteutils + +import ./types, ./protocol_types, ./conversion_utils, ./group_manager, ./nonce_manager + +import logos_delivery/waku/waku_core + +proc calcEpoch*(rlnPeer: WakuRLNRelay, t: float64): Epoch = + ## gets time `t` as `flaot64` with subseconds resolution in the fractional part + ## and returns its corresponding rln `Epoch` value + + let e = uint64(t / rlnPeer.rlnEpochSizeSec.float64) + return toEpoch(e) + +proc nextEpoch*(rlnPeer: WakuRLNRelay, time: float64): float64 = + let + currentEpoch = uint64(time / rlnPeer.rlnEpochSizeSec.float64) + nextEpochTime = float64(currentEpoch + 1) * rlnPeer.rlnEpochSizeSec.float64 + currentTime = epochTime() + + # Ensure we always return a future time + if nextEpochTime > currentTime: + return nextEpochTime + else: + return epochTime() + +proc getCurrentEpoch*(rlnPeer: WakuRLNRelay): Epoch = + return rlnPeer.calcEpoch(epochTime()) + +proc absDiff*(e1, e2: Epoch): uint64 = + ## returns the absolute difference between the two rln `Epoch`s `e1` and `e2` + ## i.e., e1 - e2 + + # convert epochs to their corresponding unsigned numerical values + let + epoch1 = fromEpoch(e1) + epoch2 = fromEpoch(e2) + + # Manually perform an `abs` calculation + if epoch1 > epoch2: + return epoch1 - epoch2 + else: + return epoch2 - epoch1 + +proc toRLNSignal*(wakumessage: WakuMessage): seq[byte] = + ## it is a utility proc that prepares the `data` parameter of the proof generation procedure i.e., `proofGen` that resides in the current module + ## it extracts the `contentTopic`, `timestamp` and the `payload` of the supplied `wakumessage` and serializes them into a byte sequence + + let + contentTopicBytes = toBytes(wakumessage.contentTopic) + timestampBytes = toBytes(wakumessage.timestamp.uint64) + output = concat(wakumessage.payload, contentTopicBytes, @(timestampBytes)) + return output + +proc generateRLNProof*( + rlnPeer: WakuRLNRelay, input: seq[byte], senderEpochTime: float64 +): Future[RlnRelayResult[seq[byte]]] {.async.} = + let epoch = rlnPeer.calcEpoch(senderEpochTime) + let nonce = rlnPeer.nonceManager.getNonce().valueOr: + return err("could not get new message id to generate an rln proof: " & $error) + let proof = (await rlnPeer.groupManager.generateProof(input, epoch, nonce)).valueOr: + return err("could not generate rln-v2 proof: " & $error) + return ok(proof.encode().buffer) diff --git a/logos_delivery/waku/waku_rln_relay/rln_relay.nim b/logos_delivery/waku/waku_rln_relay/rln_relay.nim index 5ae7a2cff..036599099 100644 --- a/logos_delivery/waku/waku_rln_relay/rln_relay.nim +++ b/logos_delivery/waku/waku_rln_relay/rln_relay.nim @@ -23,69 +23,24 @@ import ./constants, ./protocol_types, ./protocol_metrics, - ./nonce_manager + ./nonce_manager, + ./types, + ./config, + ./proof, + ./nullifier_log import logos_delivery/waku/ [common/error_handling, waku_core, requests/rln_requests, waku_keystore] +# Re-export the new submodules so existing `import waku_rln_relay` +# (and `import waku_rln_relay/rln_relay`) callers continue to see the +# moved symbols (WakuRLNRelay, WakuRlnConfig, generateRLNProof, etc.). +export types, config, proof, nullifier_log + logScope: topics = "waku rln_relay" -type RlnRelayCreds* {.requiresInit.} = object - path*: string - password*: string - -type RlnRelayConf* = object of RootObj - # TODO: severals parameters are only needed when it's dynamic - # change the config to either nest or use enum/type variant so it's obvious - # and then it can be set to `requiresInit` - dynamic*: bool - credIndex*: Option[uint] - ethContractAddress*: string - ethClientUrls*: seq[string] - chainId*: UInt256 - creds*: Option[RlnRelayCreds] - epochSizeSec*: uint64 - userMessageLimit*: uint64 - ethPrivateKey*: Option[string] - -type WakuRlnConfig* = object of RlnRelayConf - onFatalErrorAction*: OnFatalErrorHandler - -type WakuRLNRelay* = ref object of RootObj - # the log of nullifiers and Shamir shares of the past messages grouped per epoch - nullifierLog*: OrderedTable[Epoch, Table[Nullifier, ProofMetadata]] - lastEpoch*: Epoch # the epoch of the last published rln message - rlnEpochSizeSec*: uint64 - rlnMaxTimestampGap*: uint64 - rlnMaxEpochGap*: uint64 - groupManager*: GroupManager - onFatalErrorAction*: OnFatalErrorHandler - nonceManager*: NonceManager - epochMonitorFuture*: Future[void] - rootChangesFuture*: Future[Result[void, string]] - brokerCtx*: BrokerContext - -proc calcEpoch*(rlnPeer: WakuRLNRelay, t: float64): Epoch = - ## gets time `t` as `flaot64` with subseconds resolution in the fractional part - ## and returns its corresponding rln `Epoch` value - - let e = uint64(t / rlnPeer.rlnEpochSizeSec.float64) - return toEpoch(e) - -proc nextEpoch*(rlnPeer: WakuRLNRelay, time: float64): float64 = - let - currentEpoch = uint64(time / rlnPeer.rlnEpochSizeSec.float64) - nextEpochTime = float64(currentEpoch + 1) * rlnPeer.rlnEpochSizeSec.float64 - currentTime = epochTime() - - # Ensure we always return a future time - if nextEpochTime > currentTime: - return nextEpochTime - else: - return epochTime() - proc stop*(rlnPeer: WakuRLNRelay) {.async: (raises: [Exception]).} = ## stops the rln-relay protocol ## Throws an error if it cannot stop the rln-relay protocol @@ -95,81 +50,6 @@ proc stop*(rlnPeer: WakuRLNRelay) {.async: (raises: [Exception]).} = RequestGenerateRlnProof.clearProvider(rlnPeer.brokerCtx) await rlnPeer.groupManager.stop() -proc hasDuplicate*( - rlnPeer: WakuRLNRelay, epoch: Epoch, proofMetadata: ProofMetadata -): RlnRelayResult[bool] = - ## returns true if there is another message in the `nullifierLog` of the `rlnPeer` with the same - ## epoch and nullifier as `proofMetadata`'s epoch and nullifier - ## otherwise, returns false - ## Returns an error if it cannot check for duplicates - - # check if the epoch exists - let nullifier = proofMetadata.nullifier - if not rlnPeer.nullifierLog.hasKey(epoch): - return ok(false) - try: - if rlnPeer.nullifierLog[epoch].hasKey(nullifier): - # there is an identical record, mark it as spam - return ok(true) - - # there is no duplicate - return ok(false) - except KeyError: - return err("the epoch was not found: " & getCurrentExceptionMsg()) - -proc updateLog*( - rlnPeer: WakuRLNRelay, epoch: Epoch, proofMetadata: ProofMetadata -): RlnRelayResult[void] = - ## saves supplied proofMetadata `proofMetadata` - ## in the `nullifierLog` of the `rlnPeer` - ## Returns an error if it cannot update the log - - # check if the epoch exists - if not rlnPeer.nullifierLog.hasKeyOrPut( - epoch, {proofMetadata.nullifier: proofMetadata}.toTable() - ): - return ok() - - try: - # check if an identical record exists - if rlnPeer.nullifierLog[epoch].hasKeyOrPut(proofMetadata.nullifier, proofMetadata): - # the above condition could be `discarded` but it is kept for clarity, that slashing will - # be implemented here - # TODO: slashing logic - return ok() - return ok() - except KeyError: - return - err("the epoch was not found: " & getCurrentExceptionMsg()) # should never happen - -proc getCurrentEpoch*(rlnPeer: WakuRLNRelay): Epoch = - return rlnPeer.calcEpoch(epochTime()) - -proc absDiff*(e1, e2: Epoch): uint64 = - ## returns the absolute difference between the two rln `Epoch`s `e1` and `e2` - ## i.e., e1 - e2 - - # convert epochs to their corresponding unsigned numerical values - let - epoch1 = fromEpoch(e1) - epoch2 = fromEpoch(e2) - - # Manually perform an `abs` calculation - if epoch1 > epoch2: - return epoch1 - epoch2 - else: - return epoch2 - epoch1 - -proc toRLNSignal*(wakumessage: WakuMessage): seq[byte] = - ## it is a utility proc that prepares the `data` parameter of the proof generation procedure i.e., `proofGen` that resides in the current module - ## it extracts the `contentTopic`, `timestamp` and the `payload` of the supplied `wakumessage` and serializes them into a byte sequence - - let - contentTopicBytes = toBytes(wakumessage.contentTopic) - timestampBytes = toBytes(wakumessage.timestamp.uint64) - output = concat(wakumessage.payload, contentTopicBytes, @(timestampBytes)) - return output - proc validateMessage*( rlnPeer: WakuRLNRelay, msg: WakuMessage ): Future[MessageValidationResult] {.async.} = @@ -286,35 +166,6 @@ proc validateMessageAndUpdateLog*( return isValidMessage -proc generateRLNProof*( - rlnPeer: WakuRLNRelay, input: seq[byte], senderEpochTime: float64 -): Future[RlnRelayResult[seq[byte]]] {.async.} = - let epoch = rlnPeer.calcEpoch(senderEpochTime) - let nonce = rlnPeer.nonceManager.getNonce().valueOr: - return err("could not get new message id to generate an rln proof: " & $error) - let proof = (await rlnPeer.groupManager.generateProof(input, epoch, nonce)).valueOr: - return err("could not generate rln-v2 proof: " & $error) - return ok(proof.encode().buffer) - -proc clearNullifierLog*(rlnPeer: WakuRlnRelay) = - # clear the first MaxEpochGap epochs of the nullifer log - # if more than MaxEpochGap epochs are in the log - let currentEpoch = fromEpoch(rlnPeer.getCurrentEpoch()) - - var epochsToRemove: seq[Epoch] = @[] - for epoch in rlnPeer.nullifierLog.keys(): - let epochInt = fromEpoch(epoch) - - # clean all epochs that are +- rlnMaxEpochGap from the current epoch - if (currentEpoch + rlnPeer.rlnMaxEpochGap) <= epochInt or - epochInt <= (currentEpoch - rlnPeer.rlnMaxEpochGap): - epochsToRemove.add(epoch) - - for epochRemove in epochsToRemove: - trace "clearing epochs from the nullifier log", - currentEpoch = currentEpoch, cleanedEpoch = fromEpoch(epochRemove) - rlnPeer.nullifierLog.del(epochRemove) - proc monitorEpochs(wakuRlnRelay: WakuRLNRelay) {.async.} = while true: try: diff --git a/logos_delivery/waku/waku_rln_relay/types.nim b/logos_delivery/waku/waku_rln_relay/types.nim new file mode 100644 index 000000000..4c10a37f3 --- /dev/null +++ b/logos_delivery/waku/waku_rln_relay/types.nim @@ -0,0 +1,22 @@ +{.push raises: [].} + +import std/tables, chronos, results +import brokers/broker_context + +import ./group_manager, ./nonce_manager, ./protocol_types + +import logos_delivery/waku/common/error_handling + +type WakuRLNRelay* = ref object of RootObj + # the log of nullifiers and Shamir shares of the past messages grouped per epoch + nullifierLog*: OrderedTable[Epoch, Table[Nullifier, ProofMetadata]] + lastEpoch*: Epoch # the epoch of the last published rln message + rlnEpochSizeSec*: uint64 + rlnMaxTimestampGap*: uint64 + rlnMaxEpochGap*: uint64 + groupManager*: GroupManager + onFatalErrorAction*: OnFatalErrorHandler + nonceManager*: NonceManager + epochMonitorFuture*: Future[void] + rootChangesFuture*: Future[Result[void, string]] + brokerCtx*: BrokerContext