when (NimMajor, NimMinor) < (1, 4): {.push raises: [Defect].} else: {.push raises: [].} import std/[algorithm, sequtils, strutils, tables, times, os, deques], chronicles, options, chronos, chronos/ratelimit, stint, web3, json, web3/ethtypes, eth/keys, libp2p/protocols/pubsub/rpc/messages, libp2p/protocols/pubsub/pubsub, stew/results, stew/[byteutils, arrayops] import ./group_manager, ./rln, ./conversion_utils, ./constants, ./protocol_types, ./protocol_metrics import ../waku_relay, # for WakuRelayHandler ../waku_core, ../waku_keystore, ../utils/collector logScope: topics = "waku rln_relay" type WakuRlnConfig* = object rlnRelayDynamic*: bool rlnRelayCredIndex*: Option[uint] rlnRelayEthContractAddress*: string rlnRelayEthClientAddress*: string rlnRelayCredPath*: string rlnRelayCredPassword*: string rlnRelayTreePath*: string proc createMembershipList*(rln: ptr RLN, n: int): RlnRelayResult[( seq[RawMembershipCredentials], string )] = ## createMembershipList produces a sequence of identity credentials in the form of (identity trapdoor, identity nullifier, identity secret hash, id commitment) in the hexadecimal format ## this proc also returns the root of a Merkle tree constructed out of the identity commitment keys of the generated list ## the output of this proc is used to initialize a static group keys (to test waku-rln-relay in the off-chain mode) ## Returns an error if it cannot create the membership list var output = newSeq[RawMembershipCredentials]() var idCommitments = newSeq[IDCommitment]() for i in 0..n-1: # generate an identity credential let idCredentialRes = rln.membershipKeyGen() if idCredentialRes.isErr(): return err("could not generate an identity credential: " & idCredentialRes.error()) let idCredential = idCredentialRes.get() let idTuple = (idCredential.idTrapdoor.inHex(), idCredential.idNullifier.inHex(), idCredential.idSecretHash.inHex(), idCredential.idCommitment.inHex()) output.add(idTuple) idCommitments.add(idCredential.idCommitment) # Insert members into tree let membersAdded = rln.insertMembers(0, idCommitments) if not membersAdded: return err("could not insert members into the tree") let root = rln.getMerkleRoot().value().inHex() return ok((output, root)) proc calcEpoch*(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/EpochUnitSeconds) return toEpoch(e) type WakuRLNRelay* = ref object of RootObj # the log of nullifiers and Shamir shares of the past messages grouped per epoch nullifierLog*: OrderedTable[Epoch, seq[ProofMetadata]] lastEpoch*: Epoch # the epoch of the last published rln message groupManager*: GroupManager method stop*(rlnPeer: WakuRLNRelay) {.async.} = ## stops the rln-relay protocol ## Throws an error if it cannot stop the rln-relay protocol # stop the group sync, and flush data to tree db info "stopping rln-relay" await rlnPeer.groupManager.stop() proc hasDuplicate*(rlnPeer: WakuRLNRelay, 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 let externalNullifier = proofMetadata.externalNullifier # check if the epoch exists if not rlnPeer.nullifierLog.hasKey(externalNullifier): return ok(false) try: if rlnPeer.nullifierLog[externalNullifier].contains(proofMetadata): # there is an identical record, mark it as spam return ok(true) # check for a message with the same nullifier but different secret shares let matched = rlnPeer.nullifierLog[externalNullifier].filterIt(( it.nullifier == proofMetadata.nullifier) and ((it.shareX != proofMetadata.shareX) or (it.shareY != proofMetadata.shareY))) if matched.len != 0: # there is a duplicate return ok(true) # there is no duplicate return ok(false) except KeyError as e: return err("the epoch was not found") proc updateLog*(rlnPeer: WakuRLNRelay, proofMetadata: ProofMetadata): RlnRelayResult[void] = ## saves supplied proofMetadata `proofMetadata` ## in the `nullifierLog` of the `rlnPeer` ## Returns an error if it cannot update the log let externalNullifier = proofMetadata.externalNullifier # check if the externalNullifier exists if not rlnPeer.nullifierLog.hasKey(externalNullifier): rlnPeer.nullifierLog[externalNullifier] = @[proofMetadata] return ok() try: # check if an identical record exists if rlnPeer.nullifierLog[externalNullifier].contains(proofMetadata): # TODO: slashing logic return ok() # add proofMetadata to the log rlnPeer.nullifierLog[externalNullifier].add(proofMetadata) return ok() except KeyError as e: return err("the external nullifier was not found") # should never happen proc getCurrentEpoch*(): Epoch = ## gets the current rln Epoch time return 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 validateMessage*(rlnPeer: WakuRLNRelay, msg: WakuMessage, timeOption = none(float64)): MessageValidationResult = ## 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 ## the `msg` does not violate the rate limit ## `timeOption` indicates Unix epoch time (fractional part holds sub-seconds) ## if `timeOption` is supplied, then the current epoch is calculated based on that let decodeRes = RateLimitProof.init(msg.proof) if decodeRes.isErr(): return MessageValidationResult.Invalid let proof = decodeRes.get() # track message count for metrics waku_rln_messages_total.inc() # checks if the `msg`'s epoch is far from the current epoch # it corresponds to the validation of rln external nullifier var epoch: Epoch if timeOption.isSome(): epoch = calcEpoch(timeOption.get()) else: # get current rln epoch epoch = getCurrentEpoch() let msgEpoch = proof.epoch # calculate the gaps gap = absDiff(epoch, msgEpoch) trace "epoch info", currentEpoch = fromEpoch(epoch), msgEpoch = fromEpoch(msgEpoch) # validate the epoch if gap > MaxEpochGap: # message's epoch is too old or too ahead # accept messages whose epoch is within +-MaxEpochGap from the current epoch warn "invalid message: epoch gap exceeds a threshold", gap = gap, payloadLen = msg.payload.len, msgEpoch = fromEpoch(proof.epoch) waku_rln_invalid_messages_total.inc(labelValues=["invalid_epoch"]) return MessageValidationResult.Invalid let rootValidationRes = 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(), validRoots=rlnPeer.groupManager.validRoots.mapIt(it.inHex()) waku_rln_invalid_messages_total.inc(labelValues=["invalid_root"]) return MessageValidationResult.Invalid # verify the proof let contentTopicBytes = msg.contentTopic.toBytes input = concat(msg.payload, contentTopicBytes) waku_rln_proof_verification_total.inc() waku_rln_proof_verification_duration_seconds.nanosecondTime: let proofVerificationRes = rlnPeer.groupManager.verifyProof(input, proof) if proofVerificationRes.isErr(): waku_rln_errors_total.inc(labelValues=["proof_verification"]) warn "invalid message: proof verification failed", payloadLen = msg.payload.len return MessageValidationResult.Invalid if not proofVerificationRes.value(): # invalid proof warn "invalid message: invalid proof", payloadLen = msg.payload.len waku_rln_invalid_messages_total.inc(labelValues=["invalid_proof"]) return MessageValidationResult.Invalid # check if double messaging has happened let proofMetadataRes = proof.extractMetadata() if proofMetadataRes.isErr(): waku_rln_errors_total.inc(labelValues=["proof_metadata_extraction"]) return MessageValidationResult.Invalid let hasDup = rlnPeer.hasDuplicate(proofMetadataRes.get()) if hasDup.isErr(): waku_rln_errors_total.inc(labelValues=["duplicate_check"]) elif hasDup.value == true: trace "invalid message: message is spam", payloadLen = msg.payload.len waku_rln_spam_messages_total.inc() return MessageValidationResult.Spam trace "message is valid", payloadLen = msg.payload.len let rootIndex = rlnPeer.groupManager.indexOfRoot(proof.merkleRoot) waku_rln_valid_messages_total.observe(rootIndex.toFloat()) return MessageValidationResult.Valid proc validateMessageAndUpdateLog*( rlnPeer: WakuRLNRelay, msg: WakuMessage, timeOption = none(float64)): MessageValidationResult = ## validates the message and updates the log to prevent double messaging ## in future messages let result = rlnPeer.validateMessage(msg, timeOption) let decodeRes = RateLimitProof.init(msg.proof) if decodeRes.isErr(): return MessageValidationResult.Invalid let msgProof = decodeRes.get() let proofMetadataRes = msgProof.extractMetadata() if proofMetadataRes.isErr(): return MessageValidationResult.Invalid # insert the message to the log (never errors) discard rlnPeer.updateLog(proofMetadataRes.get()) return result 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` and the `payload` of the supplied `wakumessage` and serializes them into a byte sequence let contentTopicBytes = wakumessage.contentTopic.toBytes() output = concat(wakumessage.payload, contentTopicBytes) return output proc appendRLNProof*(rlnPeer: WakuRLNRelay, msg: var WakuMessage, senderEpochTime: float64): bool = ## returns true if it can create and append a `RateLimitProof` to the supplied `msg` ## returns false otherwise ## `senderEpochTime` indicates the number of seconds passed since Unix epoch. The fractional part holds sub-seconds. ## The `epoch` field of `RateLimitProof` is derived from the provided `senderEpochTime` (using `calcEpoch()`) let input = msg.toRLNSignal() let epoch = calcEpoch(senderEpochTime) let proofGenRes = rlnPeer.groupManager.generateProof(input, epoch) if proofGenRes.isErr(): return false msg.proof = proofGenRes.get().encode().buffer return true proc clearNullifierLog(rlnPeer: WakuRlnRelay) = # clear the first MaxEpochGap epochs of the nullifer log # if more than MaxEpochGap epochs are in the log # note: the epochs are ordered ascendingly if rlnPeer.nullifierLog.len().uint < MaxEpochGap: return trace "clearing epochs from the nullifier log", count = MaxEpochGap let epochsToClear = rlnPeer.nullifierLog.keys().toSeq()[0..