Updates rln-chat2 interface (#846)

* adds ProofMetadata

* adds EPOCH_INTERVAL

* adds messageLog field

* adds updateLog, toEpoch, fromEpoch, getEpoch, compareTo

* adds unit test for toEpoch and fromEpoch

* adds unit test for Epoch comparison

* adds result codes for updateLog

* adds unit test for update log

* renames epoch related consts

* modifies updateLog with new return type and new logic of spam detection

* adds unit text for the modified updateLog

* changes max epoch gap type size

* splits updateLog into two procs isSpam and updateLog

* updates unittests

* fixes a bug, returns false when the message is not spam

* renames messageLog to nullifierLog

* renames isSpam to hasDuplicate

* updates the rln validator, adds comments

* adds appendRLNProof proc plus some code beatification

* unit test for validate message

* adds unhappy test to validateMessage unit test

* renames EPOCH_UNIT_SECONDS

* renames MAX_CLOCK_GAP_SECONDS

* WIP: integration test

* fixes compile errors

* sets a real epoch value

* updates on old unittests

* adds comments to the rln relay tests

* adds more comments

* makes rln import conditional

* adds todos

* adds more todos

* adds rln-relay mount process into chat2

* further todos

* logs contentTopic

* introduces rln relay configs

* changes default pubsub topic

* adds contentTopic config

* imports rln relay dependencies

* consolidates imports

* removes module identifier from ContentTopic

* adds contentTopic field

* adds contentTopic argument to mountRlnRelay calls

* appends rln proof to chat2 messages

* changes the default chat2 contentTopic

* adds missing content topic fields

* fixes a bug

* adds a new logic about empty content topics

* appends proof only when rln flag is active

* removes unnecessary todos

* fixes an indentation issue

* adds log messages

* verifies the proof against the concatenation of msg payload and content topic

* a bug fix

* WIP

* removes duplicate epoch time calculation

* converts echo to log

* invokes handler

* bug fix

* prints calculated epoch

* changes the format of printed epoch

* updates log levels

* logs the input buffer supplied to the generate_proof

* replaces echos with logs

* changes log level to trace

* resets the log level of chat2 to INFO

* upgrades log level to debug

* exports toRLNSignal and adds a doc string

* updates log level

* enables all test2

* removes an echo statement

* modifies a comment

* further updates on the log level

* a minor update

* invokes the spam handler when provided

* checks for payload version

* deletes a redundant check

* deletes a rendant check

* updates default rln-relay cht2 content topic

* adds a todo and log

* changes the case of testnet content topic

* removes a flaky check
This commit is contained in:
Sanaz Taheri Boshrooyeh 2022-02-16 14:52:21 -08:00 committed by GitHub
parent e1254c20ae
commit d851d48424
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 82 additions and 28 deletions

View File

@ -129,6 +129,32 @@ proc showChatPrompt(c: Chat) =
except IOError:
discard
proc getChatLine(c: Chat, msg:WakuMessage): Result[string, string]=
when PayloadV1:
# Use Waku v1 payload encoding/encryption
let
keyInfo = KeyInfo(kind: Symmetric, symKey: c.symKey)
decodedPayload = decodePayload(decoded.get(), keyInfo)
if decodedPayload.isOK():
let
pb = Chat2Message.init(decodedPayload.get().payload)
chatLine = if pb.isOk: pb[].toString()
else: string.fromBytes(decodedPayload.get().payload)
return ok(chatLine)
else:
debug "Invalid encoded WakuMessage payload",
error = decodedPayload.error
return err("Invalid encoded WakuMessage payload")
else:
# No payload encoding/encryption from Waku
let
pb = Chat2Message.init(msg.payload)
chatLine = if pb.isOk: pb[].toString()
else: string.fromBytes(msg.payload)
return ok(chatline)
proc printReceivedMessage(c: Chat, msg: WakuMessage) =
when PayloadV1:
# Use Waku v1 payload encoding/encryption
@ -225,6 +251,8 @@ proc publish(c: Chat, line: string) =
debug "could not append rate limit proof to the message", success=success
else:
debug "rate limit proof is appended to the message", success=success
# TODO move it to log after doogfooding
echo "--rln epoch: ", fromEpoch(message.proof.epoch)
if not c.node.wakuLightPush.isNil():
# Attempt lightpush
asyncSpawn c.node.lightpush(DefaultTopic, message, handler)
@ -245,6 +273,7 @@ proc publish(c: Chat, line: string) =
debug "could not append rate limit proof to the message", success=success
else:
debug "rate limit proof is appended to the message", success=success
echo "--rln epoch: ", fromEpoch(message.proof.epoch)
if not c.node.wakuLightPush.isNil():
# Attempt lightpush
@ -372,7 +401,6 @@ proc processInput(rfd: AsyncFD, rng: ref BrHmacDrbgContext) {.async.} =
prompt: false,
contentTopic: conf.contentTopic,
symKey: generateSymKey(conf.contentTopic))
if conf.staticnodes.len > 0:
await connectToNodes(chat, conf.staticnodes)
elif conf.dnsDiscovery and conf.dnsDiscoveryUrl != "":
@ -495,16 +523,24 @@ proc processInput(rfd: AsyncFD, rng: ref BrHmacDrbgContext) {.async.} =
if conf.rlnRelay:
info "WakuRLNRelay is enabled"
proc spamHandler(wakuMessage: WakuMessage) {.gcsafe, closure.} =
debug "spam handler is called"
let chatLineResult = chat.getChatLine(wakuMessage)
if chatLineResult.isOk():
echo "A spam message is found and discarded : ", chatLineResult.value
else:
echo "A spam message is found and discarded"
# set up rln relay inputs
let (groupOpt, memKeyPairOpt, memIndexOpt) = rlnRelaySetUp(conf.rlnRelayMemIndex)
if memIndexOpt.isNone:
error "failed to mount WakuRLNRelay"
else:
# mount rlnrelay in offline mode (for now)
waitFor node.mountRlnRelay(groupOpt = groupOpt, memKeyPairOpt = memKeyPairOpt, memIndexOpt= memIndexOpt, onchainMode = false, pubsubTopic = conf.rlnRelayPubsubTopic, contentTopic = conf.rlnRelayContentTopic)
waitFor node.mountRlnRelay(groupOpt = groupOpt, memKeyPairOpt = memKeyPairOpt, memIndexOpt= memIndexOpt, onchainMode = false, pubsubTopic = conf.rlnRelayPubsubTopic, contentTopic = conf.rlnRelayContentTopic, spamHandler = some(spamHandler))
trace "membership id key", idkey=memKeyPairOpt.get().idKey.toHex
trace "membership id commitment key", idCommitmentkey=memKeyPairOpt.get().idCommitment.toHex
debug "membership id key", idkey=memKeyPairOpt.get().idKey.toHex
debug "membership id commitment key", idCommitmentkey=memKeyPairOpt.get().idCommitment.toHex
# check the correct construction of the tree by comparing the calculated root against the expected root
# no error should happen as it is already captured in the unit tests
@ -514,8 +550,8 @@ proc processInput(rfd: AsyncFD, rng: ref BrHmacDrbgContext) {.async.} =
expectedRoot = STATIC_GROUP_MERKLE_ROOT
if root != expectedRoot:
error "root mismatch: something went wrong not in Merkle tree construction"
trace "the calculated root", root
trace "WakuRLNRelay is mounted successfully", pubsubtopic=conf.rlnRelayPubsubTopic, contentTopic=conf.rlnRelayContentTopic
debug "the calculated root", root
debug "WakuRLNRelay is mounted successfully", pubsubtopic=conf.rlnRelayPubsubTopic, contentTopic=conf.rlnRelayContentTopic
await chat.readWriteLoop()

View File

@ -241,7 +241,7 @@ type
rlnRelayContentTopic* {.
desc: "the pubsub topic for which rln-relay gets enabled",
defaultValue: "waku/2/rln-relay/proto"
defaultValue: "/toy-chat/2/luzhou/proto"
name: "rln-relay-content-topic" }: ContentTopic
rlnRelayPubsubTopic* {.

View File

@ -911,12 +911,12 @@ procSuite "WakuNode":
wm1 = WakuMessage(payload: "message 1".toBytes(), contentTopic: contentTopic)
proofAdded1 = node3.wakuRlnRelay.appendRLNProof(wm1, time)
# another message in the same epoch as wm1, it will break the messaging rate limit
wm2 = WakuMessage(payload: "message2".toBytes(), contentTopic: contentTopic)
wm2 = WakuMessage(payload: "message 2".toBytes(), contentTopic: contentTopic)
proofAdded2 = node3.wakuRlnRelay.appendRLNProof(wm2, time)
# wm3 points to the next epoch
wm3 = WakuMessage(payload: "message 3".toBytes(), contentTopic: contentTopic)
proofAdded3 = node3.wakuRlnRelay.appendRLNProof(wm3, time+EPOCH_UNIT_SECONDS)
wm4 = WakuMessage(payload: "message4".toBytes(), contentTopic: contentTopic)
wm4 = WakuMessage(payload: "message 4".toBytes(), contentTopic: contentTopic)
# check proofs are added correctly
check:
@ -966,9 +966,7 @@ procSuite "WakuNode":
res2 = await completionFut2.withTimeout(10.seconds)
check:
res1 or res2 == true # either of the wm1 and wm2 is relayed
(res1 and res2) == false # either of the wm1 and wm2 is found as spam hence not relayed
(await completionFut2.withTimeout(10.seconds)) == true
(await completionFut3.withTimeout(10.seconds)) == true
(await completionFut4.withTimeout(10.seconds)) == false

View File

@ -470,30 +470,34 @@ proc mountStore*(node: WakuNode, store: MessageStore = nil, persistMessages: boo
node.switch.mount(node.wakuStore, protocolMatcher(WakuStoreCodec))
when defined(rln):
proc addRLNRelayValidator*(node: WakuNode, pubsubTopic: string, contentTopic: ContentTopic) =
proc addRLNRelayValidator*(node: WakuNode, pubsubTopic: string, contentTopic: ContentTopic, spamHandler: Option[SpamHandler] = none(SpamHandler)) =
## this procedure is a thin wrapper for the pubsub addValidator method
## it sets a validator for the waku messages published on the supplied pubsubTopic and contentTopic
## if contentTopic is empty, then validation takes place for All the messages published on the given pubsubTopic
## the message validation logic is according to https://rfc.vac.dev/spec/17/
proc validator(topic: string, message: messages.Message): Future[pubsub.ValidationResult] {.async.} =
debug "rln-relay topic validator is called"
let msg = WakuMessage.init(message.data)
if msg.isOk():
let wakumessage = msg.value()
# check the contentTopic
if (wakumessage.contentTopic != "") and (contentTopic != "") and (wakumessage.contentTopic != contentTopic):
trace "content topic did not match:", contentTopic=wakumessage.contentTopic, payload=string.fromBytes(wakumessage.payload)
debug "content topic did not match:", contentTopic=wakumessage.contentTopic, payload=string.fromBytes(wakumessage.payload)
return pubsub.ValidationResult.Accept
# validate the message
let validationRes = node.wakuRlnRelay.validateMessage(wakumessage)
case validationRes:
of Valid:
trace "message validity is verified, relaying:", wakumessage=wakumessage, payload=string.fromBytes(wakumessage.payload)
debug "message validity is verified, relaying:", wakumessage=wakumessage, payload=string.fromBytes(wakumessage.payload)
return pubsub.ValidationResult.Accept
of Invalid:
trace "message validity could not be verified, discarding:", wakumessage=wakumessage, payload=string.fromBytes(wakumessage.payload)
debug "message validity could not be verified, discarding:", wakumessage=wakumessage, payload=string.fromBytes(wakumessage.payload)
return pubsub.ValidationResult.Reject
of Spam:
trace "A spam message is found! yay! discarding:", wakumessage=wakumessage, payload=string.fromBytes(wakumessage.payload)
debug "A spam message is found! yay! discarding:", wakumessage=wakumessage, payload=string.fromBytes(wakumessage.payload)
if spamHandler.isSome:
let handler = spamHandler.get
handler(wakumessage)
return pubsub.ValidationResult.Reject
# set a validator for the supplied pubsubTopic
let pb = PubSub(node.wakuRelay)
@ -508,7 +512,8 @@ when defined(rln):
memIndexOpt: Option[MembershipIndex] = none(MembershipIndex),
onchainMode: bool = true,
pubsubTopic: string,
contentTopic: ContentTopic) {.async.} =
contentTopic: ContentTopic,
spamHandler: Option[SpamHandler] = none(SpamHandler)) {.async.} =
# TODO return a bool value to indicate the success of the call
# check whether inputs are provided
@ -601,8 +606,8 @@ when defined(rln):
# adds a topic validator for the supplied pubsub topic at the relay protocol
# messages published on this pubsub topic will be relayed upon a successful validation, otherwise they will be dropped
# the topic validator checks for the correct non-spamming proof of the message
addRLNRelayValidator(node, pubsubTopic, contentTopic)
debug "rln relay topic validator is mounted successfully", pubsubTopic=pubsubTopic
addRLNRelayValidator(node, pubsubTopic, contentTopic, spamHandler)
debug "rln relay topic validator is mounted successfully", pubsubTopic=pubsubTopic, contentTopic=contentTopic
node.wakuRlnRelay = rlnPeer

View File

@ -116,8 +116,8 @@ const
# the root is created locally, using createMembershipList proc from waku_rln_relay_utils module, and the result is hardcoded in here
STATIC_GROUP_MERKLE_ROOT* = "a1877a553eff12e1b21632a0545a916a5c5b8060ad7cc6c69956741134397b2d"
const EPOCH_UNIT_SECONDS* = float64(2)
const MAX_CLOCK_GAP_SECONDS* = 20.0 # the maximum clock difference between peers
const EPOCH_UNIT_SECONDS* = float64(10) # the rln-relay epoch length in seconds
const MAX_CLOCK_GAP_SECONDS* = 20.0 # the maximum clock difference between peers in seconds
# maximum allowed gap between the epochs of messages' RateLimitProofs
const MAX_EPOCH_GAP* = int64(MAX_CLOCK_GAP_SECONDS/EPOCH_UNIT_SECONDS)

View File

@ -16,6 +16,8 @@ logScope:
type RLNResult* = Result[RLN[Bn256], string]
type MerkleNodeResult* = Result[MerkleNode, string]
type RateLimitProofResult* = Result[RateLimitProof, string]
type SpamHandler* = proc(wakuMessage: WakuMessage): void {.gcsafe, closure, raises: [Defect].}
# membership contract interface
contract(MembershipContract):
# TODO define a return type of bool for register method to signify a successful registration
@ -157,6 +159,8 @@ proc proofGen*(rlnInstance: RLN[Bn256], data: openArray[byte], memKeys: Membersh
msg = data)
var inputBuffer = toBuffer(serializedInputs)
debug "input buffer ", inputBuffer
# generate the proof
var proof: Buffer
let proofIsSuccessful = generate_proof(rlnInstance, addr inputBuffer, addr proof)
@ -445,16 +449,19 @@ proc validateMessage*(rlnPeer: WakuRLNRelay, msg: WakuMessage, timeOption: Optio
# get current rln epoch
epoch = getCurrentEpoch()
debug "current epoch", currentEpoch=fromEpoch(epoch)
let
msgEpoch = msg.proof.epoch
# calculate the gaps
gap = compare(epoch, msgEpoch)
debug "message epoch", msgEpoch=fromEpoch(msgEpoch)
# validate the epoch
if abs(gap) >= MAX_EPOCH_GAP:
# message's epoch is too old or too ahead
# accept messages whose epoch is within +-MAX_EPOCH_GAP from the current epoch
debug "invalid message: epoch gap exceeds a threshold",gap=gap
debug "invalid message: epoch gap exceeds a threshold",gap=gap, payload=string.fromBytes(msg.payload)
return MessageValidationResult.Invalid
# verify the proof
@ -463,31 +470,39 @@ proc validateMessage*(rlnPeer: WakuRLNRelay, msg: WakuMessage, timeOption: Optio
input = concat(msg.payload, contentTopicBytes)
if not rlnPeer.rlnInstance.proofVerify(input, msg.proof):
# invalid proof
debug "invalid message: invalid proof"
debug "invalid message: invalid proof", payload=string.fromBytes(msg.payload)
return MessageValidationResult.Invalid
# check if double messaging has happened
let hasDup = rlnPeer.hasDuplicate(msg)
if hasDup.isOk and hasDup.value == true:
debug "invalid message: message is a spam"
debug "invalid message: message is a spam", payload=string.fromBytes(msg.payload)
return MessageValidationResult.Spam
# insert the message to the log
# the result of `updateLog` is discarded because message insertion is guaranteed by the implementation i.e.,
# it will never error out
discard rlnPeer.updateLog(msg)
debug "message is valid", payload=string.fromBytes(msg.payload)
return MessageValidationResult.Valid
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
contentTopicBytes = msg.contentTopic.toBytes
input = concat(msg.payload, contentTopicBytes)
let input = msg.toRLNSignal()
var proof: RateLimitProofResult = proofGen(rlnInstance = rlnPeer.rlnInstance, data = input,
memKeys = rlnPeer.membershipKeyPair,