2023-02-28 13:38:30 +00:00
when ( NimMajor , NimMinor ) < ( 1 , 4 ) :
{. push raises : [ Defect ] . }
else :
{. push raises : [ ] . }
import
std / [ algorithm , sequtils , strutils , tables , times , os , deques ] ,
2023-07-07 11:58:37 +00:00
chronicles , options , chronos , chronos / ratelimit , stint ,
2023-02-28 13:38:30 +00:00
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
2023-09-05 09:05:07 +00:00
.. / waku_relay , # for WakuRelayHandler
2023-04-19 14:39:52 +00:00
.. / waku_core ,
2023-07-07 11:58:37 +00:00
.. / waku_keystore ,
.. / utils / collector
2023-02-28 13:38:30 +00:00
logScope :
topics = " waku rln_relay "
type WakuRlnConfig * = object
rlnRelayDynamic * : bool
2023-09-04 10:16:44 +00:00
rlnRelayCredIndex * : Option [ uint ]
2023-02-28 13:38:30 +00:00
rlnRelayEthContractAddress * : string
rlnRelayEthClientAddress * : string
rlnRelayCredPath * : string
2023-08-29 12:16:21 +00:00
rlnRelayCredPassword * : string
2023-06-08 11:52:21 +00:00
rlnRelayTreePath * : string
2023-02-28 13:38:30 +00:00
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
2023-09-06 08:18:02 +00:00
nullifierLog * : OrderedTable [ Epoch , seq [ ProofMetadata ] ]
2023-02-28 13:38:30 +00:00
lastEpoch * : Epoch # the epoch of the last published rln message
groupManager * : GroupManager
2023-07-27 11:51:21 +00:00
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
2023-08-18 11:08:24 +00:00
info " stopping rln-relay "
2023-07-27 11:51:21 +00:00
await rlnPeer . groupManager . stop ( )
2023-03-13 14:39:33 +00:00
proc hasDuplicate * ( rlnPeer : WakuRLNRelay ,
proofMetadata : ProofMetadata ) : RlnRelayResult [ bool ] =
2023-02-28 13:38:30 +00:00
## returns true if there is another message in the `nullifierLog` of the `rlnPeer` with the same
2023-08-02 05:10:18 +00:00
## epoch and nullifier as `proofMetadata`'s epoch and nullifier
2023-02-28 13:38:30 +00:00
## otherwise, returns false
## Returns an error if it cannot check for duplicates
2023-03-13 14:39:33 +00:00
let externalNullifier = proofMetadata . externalNullifier
2023-02-28 13:38:30 +00:00
# check if the epoch exists
2023-03-13 14:39:33 +00:00
if not rlnPeer . nullifierLog . hasKey ( externalNullifier ) :
2023-02-28 13:38:30 +00:00
return ok ( false )
try :
2023-03-13 14:39:33 +00:00
if rlnPeer . nullifierLog [ externalNullifier ] . contains ( proofMetadata ) :
2023-08-02 05:10:18 +00:00
# there is an identical record, mark it as spam
return ok ( true )
2023-02-28 13:38:30 +00:00
# check for a message with the same nullifier but different secret shares
2023-03-13 14:39:33 +00:00
let matched = rlnPeer . nullifierLog [ externalNullifier ] . filterIt ( (
it . nullifier = = proofMetadata . nullifier ) and ( ( it . shareX ! = proofMetadata . shareX ) or
( it . shareY ! = proofMetadata . shareY ) ) )
2023-02-28 13:38:30 +00:00
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 " )
2023-03-13 14:39:33 +00:00
proc updateLog * ( rlnPeer : WakuRLNRelay ,
proofMetadata : ProofMetadata ) : RlnRelayResult [ void ] =
## saves supplied proofMetadata `proofMetadata`
## in the `nullifierLog` of the `rlnPeer`
2023-02-28 13:38:30 +00:00
## Returns an error if it cannot update the log
2023-03-13 14:39:33 +00:00
let externalNullifier = proofMetadata . externalNullifier
# check if the externalNullifier exists
if not rlnPeer . nullifierLog . hasKey ( externalNullifier ) :
rlnPeer . nullifierLog [ externalNullifier ] = @ [ proofMetadata ]
return ok ( )
2023-02-28 13:38:30 +00:00
try :
# check if an identical record exists
2023-03-13 14:39:33 +00:00
if rlnPeer . nullifierLog [ externalNullifier ] . contains ( proofMetadata ) :
# TODO: slashing logic
return ok ( )
# add proofMetadata to the log
rlnPeer . nullifierLog [ externalNullifier ] . add ( proofMetadata )
return ok ( )
2023-02-28 13:38:30 +00:00
except KeyError as e :
2023-03-13 14:39:33 +00:00
return err ( " the external nullifier was not found " ) # should never happen
2023-02-28 13:38:30 +00:00
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
2023-08-21 06:55:34 +00:00
proc validateMessage * ( rlnPeer : WakuRLNRelay ,
2023-07-07 11:58:37 +00:00
msg : WakuMessage ,
timeOption = none ( float64 ) ) : MessageValidationResult =
2023-02-28 13:38:30 +00:00
## 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
2023-07-07 11:58:37 +00:00
2023-02-28 13:38:30 +00:00
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 ( )
2023-09-01 13:03:59 +00:00
# checks if the `msg`'s epoch is far from the current epoch
2023-02-28 13:38:30 +00:00
# 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 )
2023-09-04 13:13:59 +00:00
trace " epoch info " , currentEpoch = fromEpoch ( epoch ) , msgEpoch = fromEpoch ( msgEpoch )
2023-02-28 13:38:30 +00:00
# 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 ,
2023-09-01 13:03:59 +00:00
payloadLen = msg . payload . len , msgEpoch = fromEpoch ( proof . epoch )
2023-02-28 13:38:30 +00:00
waku_rln_invalid_messages_total . inc ( labelValues = [ " invalid_epoch " ] )
return MessageValidationResult . Invalid
let rootValidationRes = rlnPeer . groupManager . validateRoot ( proof . merkleRoot )
if not rootValidationRes :
2023-09-04 13:13:59 +00:00
warn " invalid message: provided root does not belong to acceptable window of roots " , provided = proof . merkleRoot . inHex ( ) , validRoots = rlnPeer . groupManager . validRoots . mapIt ( it . inHex ( ) )
2023-02-28 13:38:30 +00:00
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 " ] )
2023-09-01 13:03:59 +00:00
warn " invalid message: proof verification failed " , payloadLen = msg . payload . len
2023-02-28 13:38:30 +00:00
return MessageValidationResult . Invalid
if not proofVerificationRes . value ( ) :
# invalid proof
2023-09-04 13:13:59 +00:00
warn " invalid message: invalid proof " , payloadLen = msg . payload . len
2023-02-28 13:38:30 +00:00
waku_rln_invalid_messages_total . inc ( labelValues = [ " invalid_proof " ] )
return MessageValidationResult . Invalid
# check if double messaging has happened
2023-03-13 14:39:33 +00:00
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 ( ) )
2023-02-28 13:38:30 +00:00
if hasDup . isErr ( ) :
waku_rln_errors_total . inc ( labelValues = [ " duplicate_check " ] )
elif hasDup . value = = true :
2023-09-04 13:13:59 +00:00
trace " invalid message: message is spam " , payloadLen = msg . payload . len
2023-02-28 13:38:30 +00:00
waku_rln_spam_messages_total . inc ( )
return MessageValidationResult . Spam
2023-09-04 13:13:59 +00:00
trace " message is valid " , payloadLen = msg . payload . len
2023-02-28 13:38:30 +00:00
let rootIndex = rlnPeer . groupManager . indexOfRoot ( proof . merkleRoot )
waku_rln_valid_messages_total . observe ( rootIndex . toFloat ( ) )
return MessageValidationResult . Valid
2023-09-01 13:03:59 +00:00
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
2023-02-28 13:38:30 +00:00
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
2023-09-06 08:18:02 +00:00
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 .. < MaxEpochGap ]
for epoch in epochsToClear :
rlnPeer . nullifierLog . del ( epoch )
2023-02-28 13:38:30 +00:00
proc generateRlnValidator * ( wakuRlnRelay : WakuRLNRelay ,
2023-09-06 08:18:02 +00:00
spamHandler = none ( SpamHandler ) ) : WakuValidatorHandler =
2023-02-28 13:38:30 +00:00
## this procedure is a thin wrapper for the pubsub addValidator method
2023-08-21 06:55:34 +00:00
## it sets a validator for waku messages, acting in the registered pubsub topic
2023-02-28 13:38:30 +00:00
## the message validation logic is according to https://rfc.vac.dev/spec/17/
2023-09-05 09:05:07 +00:00
proc validator ( topic : string , message : WakuMessage ) : Future [ pubsub . ValidationResult ] {. async . } =
2023-02-28 13:38:30 +00:00
trace " rln-relay topic validator is called "
2023-09-06 08:18:02 +00:00
wakuRlnRelay . clearNullifierLog ( )
2023-07-07 11:58:37 +00:00
2023-09-05 09:05:07 +00:00
let decodeRes = RateLimitProof . init ( message . proof )
2023-07-07 11:58:37 +00:00
2023-09-05 09:05:07 +00:00
if decodeRes . isErr ( ) :
return pubsub . ValidationResult . Reject
2023-08-21 06:55:34 +00:00
2023-09-05 09:05:07 +00:00
let msgProof = decodeRes . get ( )
# validate the message and update log
let validationRes = wakuRlnRelay . validateMessageAndUpdateLog ( message )
2023-02-28 13:38:30 +00:00
2023-09-05 09:05:07 +00:00
let
proof = toHex ( msgProof . proof )
epoch = fromEpoch ( msgProof . epoch )
root = inHex ( msgProof . merkleRoot )
shareX = inHex ( msgProof . shareX )
shareY = inHex ( msgProof . shareY )
nullifier = inHex ( msgProof . nullifier )
payload = string . fromBytes ( message . payload )
case validationRes :
of Valid :
trace " message validity is verified, relaying: " , proof = proof , root = root , shareX = shareX , shareY = shareY , nullifier = nullifier
return pubsub . ValidationResult . Accept
of Invalid :
trace " message validity could not be verified, discarding: " , proof = proof , root = root , shareX = shareX , shareY = shareY , nullifier = nullifier
return pubsub . ValidationResult . Reject
of Spam :
trace " A spam message is found! yay! discarding: " , proof = proof , root = root , shareX = shareX , shareY = shareY , nullifier = nullifier
if spamHandler . isSome ( ) :
let handler = spamHandler . get ( )
handler ( message )
return pubsub . ValidationResult . Reject
2023-02-28 13:38:30 +00:00
return validator
proc mount ( conf : WakuRlnConfig ,
2023-09-06 08:18:02 +00:00
registrationHandler = none ( RegistrationHandler )
2023-02-28 13:38:30 +00:00
) : Future [ WakuRlnRelay ] {. async . } =
var
groupManager : GroupManager
# create an RLN instance
2023-06-08 11:52:21 +00:00
let rlnInstanceRes = createRLNInstance ( tree_path = conf . rlnRelayTreePath )
2023-02-28 13:38:30 +00:00
if rlnInstanceRes . isErr ( ) :
raise newException ( CatchableError , " RLN instance creation failed " )
let rlnInstance = rlnInstanceRes . get ( )
if not conf . rlnRelayDynamic :
# static setup
let parsedGroupKeysRes = StaticGroupKeys . toIdentityCredentials ( )
if parsedGroupKeysRes . isErr ( ) :
raise newException ( ValueError , " Static group keys are not valid " )
groupManager = StaticGroupManager ( groupSize : StaticGroupSize ,
groupKeys : parsedGroupKeysRes . get ( ) ,
2023-09-04 10:16:44 +00:00
membershipIndex : conf . rlnRelayCredIndex ,
2023-02-28 13:38:30 +00:00
rlnInstance : rlnInstance )
# we don't persist credentials in static mode since they exist in ./constants.nim
else :
# dynamic setup
proc useValueOrNone ( s : string ) : Option [ string ] =
if s = = " " : none ( string ) else : some ( s )
let
rlnRelayCredPath = useValueOrNone ( conf . rlnRelayCredPath )
2023-08-29 12:16:21 +00:00
rlnRelayCredPassword = useValueOrNone ( conf . rlnRelayCredPassword )
2023-02-28 13:38:30 +00:00
groupManager = OnchainGroupManager ( ethClientUrl : conf . rlnRelayEthClientAddress ,
ethContractAddress : $ conf . rlnRelayEthContractAddress ,
rlnInstance : rlnInstance ,
registrationHandler : registrationHandler ,
keystorePath : rlnRelayCredPath ,
2023-08-29 12:16:21 +00:00
keystorePassword : rlnRelayCredPassword ,
2023-09-04 10:16:44 +00:00
membershipIndex : conf . rlnRelayCredIndex )
2023-02-28 13:38:30 +00:00
# Initialize the groupManager
await groupManager . init ( )
# Start the group sync
await groupManager . startGroupSync ( )
2023-09-05 09:05:07 +00:00
return WakuRLNRelay ( groupManager : groupManager )
2023-02-28 13:38:30 +00:00
2023-09-06 08:46:19 +00:00
proc isReady * ( rlnPeer : WakuRLNRelay ) : Future [ bool ] {. async . } =
## returns true if the rln-relay protocol is ready to relay messages
## returns false otherwise
# could be nil during startup
if rlnPeer . groupManager = = nil :
return false
try :
return await rlnPeer . groupManager . isReady ( )
except CatchableError :
error " could not check if the rln-relay protocol is ready " , err = getCurrentExceptionMsg ( )
return false
2023-02-28 13:38:30 +00:00
proc new * ( T : type WakuRlnRelay ,
conf : WakuRlnConfig ,
2023-09-25 07:56:16 +00:00
registrationHandler = none ( RegistrationHandler )
2023-02-28 13:38:30 +00:00
) : Future [ RlnRelayResult [ WakuRlnRelay ] ] {. async . } =
## Mounts the rln-relay protocol on the node.
## The rln-relay protocol can be mounted in two modes: on-chain and off-chain.
## Returns an error if the rln-relay protocol could not be mounted.
try :
2023-08-22 14:30:33 +00:00
let rlnRelay = await mount ( conf , registrationHandler )
2023-02-28 13:38:30 +00:00
return ok ( rlnRelay )
except CatchableError as e :
return err ( e . msg )