mirror of https://github.com/waku-org/nwaku.git
WIP: Waku-rln-relay: Message validation and double signaling detection (#769)
* 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 trace log for the valid messages * brings the log trace one line up
This commit is contained in:
parent
53b21be2b5
commit
fda5128cc1
|
@ -2,7 +2,7 @@
|
|||
{.used.}
|
||||
|
||||
import
|
||||
std/options, sequtils,
|
||||
std/options, sequtils, times,
|
||||
testutils/unittests, chronos, chronicles, stint, web3,
|
||||
stew/byteutils, stew/shims/net as stewNet,
|
||||
libp2p/crypto/crypto,
|
||||
|
@ -188,7 +188,7 @@ procSuite "Waku rln relay":
|
|||
|
||||
# initialize the WakuRLNRelay
|
||||
var rlnPeer = WakuRLNRelay(membershipKeyPair: membershipKeyPair.get(),
|
||||
membershipIndex: uint(0),
|
||||
membershipIndex: MembershipIndex(0),
|
||||
ethClientAddress: EthClient,
|
||||
ethAccountAddress: ethAccountAddress,
|
||||
membershipContractAddress: contractAddress)
|
||||
|
@ -759,3 +759,144 @@ suite "Waku rln relay":
|
|||
let verified = rln.proofVerify(data = messageBytes,
|
||||
proof = proof)
|
||||
check verified == false
|
||||
test "toEpoch and fromEpoch consistency check":
|
||||
# check edge cases
|
||||
let
|
||||
time = uint64.high
|
||||
epoch = time.toEpoch()
|
||||
decodedTime = epoch.fromEpoch()
|
||||
check time == decodedTime
|
||||
debug "encoded and decode time", time=time, epoch=epoch, decodedTime=decodedTime
|
||||
|
||||
test "Epoch comparison":
|
||||
# check edge cases
|
||||
let
|
||||
time1 = uint64.high
|
||||
time2 = uint64.high - 1
|
||||
epoch1 = time1.toEpoch()
|
||||
epoch2 = time2.toEpoch()
|
||||
check compare(epoch1, epoch2) == int64(1)
|
||||
check compare(epoch2, epoch1) == int64(-1)
|
||||
|
||||
test "updateLog and hasDuplicate tests":
|
||||
let
|
||||
wakurlnrelay = WakuRLNRelay()
|
||||
epoch = getCurrentEpoch()
|
||||
|
||||
# cretae some dummy nullifiers and secret shares
|
||||
var nullifier1: Nullifier
|
||||
for index, x in nullifier1.mpairs: nullifier1[index] = 1
|
||||
var shareX1: MerkleNode
|
||||
for index, x in shareX1.mpairs: shareX1[index] = 1
|
||||
let shareY1 = shareX1
|
||||
|
||||
var nullifier2: Nullifier
|
||||
for index, x in nullifier2.mpairs: nullifier2[index] = 2
|
||||
var shareX2: MerkleNode
|
||||
for index, x in shareX2.mpairs: shareX2[index] = 2
|
||||
let shareY2 = shareX2
|
||||
|
||||
let nullifier3 = nullifier1
|
||||
var shareX3: MerkleNode
|
||||
for index, x in shareX3.mpairs: shareX3[index] = 3
|
||||
let shareY3 = shareX3
|
||||
|
||||
let
|
||||
wm1 = WakuMessage(proof: RateLimitProof(epoch: epoch, nullifier: nullifier1, shareX: shareX1, shareY: shareY1))
|
||||
wm2 = WakuMessage(proof: RateLimitProof(epoch: epoch, nullifier: nullifier2, shareX: shareX2, shareY: shareY2))
|
||||
wm3 = WakuMessage(proof: RateLimitProof(epoch: epoch, nullifier: nullifier3, shareX: shareX3, shareY: shareY3))
|
||||
|
||||
# check whether hasDuplicate correctly finds records with the same nullifiers but different secret shares
|
||||
# no duplicate for wm1 should be found, since the log is empty
|
||||
let result1 = wakurlnrelay.hasDuplicate(wm1)
|
||||
check:
|
||||
result1.isOk
|
||||
# no duplicate is found
|
||||
result1.value == false
|
||||
# add it to the log
|
||||
discard wakurlnrelay.updateLog(wm1)
|
||||
|
||||
# # no duplicate for wm2 should be found, its nullifier differs from wm1
|
||||
let result2 = wakurlnrelay.hasDuplicate(wm2)
|
||||
check:
|
||||
result2.isOk
|
||||
# no duplicate is found
|
||||
result2.value == false
|
||||
# add it to the log
|
||||
discard wakurlnrelay.updateLog(wm2)
|
||||
|
||||
# wm3 has the same nullifier as wm1 but different secret shares, it should be detected as duplicate
|
||||
let result3 = wakurlnrelay.hasDuplicate(wm3)
|
||||
check:
|
||||
result3.isOk
|
||||
# it is a duplicate
|
||||
result3.value == true
|
||||
|
||||
test "validateMessage test":
|
||||
# setup a wakurlnrelay peer with a static group----------
|
||||
|
||||
# create a group of 100 membership keys
|
||||
let
|
||||
(groupKeys, root) = createMembershipList(100)
|
||||
# convert the keys to MembershipKeyPair structs
|
||||
groupKeyPairs = groupKeys.toMembershipKeyPairs()
|
||||
# extract the id commitments
|
||||
groupIDCommitments = groupKeyPairs.mapIt(it.idCommitment)
|
||||
debug "groupKeyPairs", groupKeyPairs
|
||||
debug "groupIDCommitments", groupIDCommitments
|
||||
|
||||
# index indicates the position of a membership key pair in the static list of group keys i.e., groupKeyPairs
|
||||
# the corresponding key pair will be used to mount rlnRelay on the current node
|
||||
# index also represents the index of the leaf in the Merkle tree that contains node's commitment key
|
||||
let index = MembershipIndex(5)
|
||||
|
||||
# create an RLN instance
|
||||
var rlnInstance = createRLNInstance()
|
||||
doAssert(rlnInstance.isOk)
|
||||
var rln = rlnInstance.value
|
||||
|
||||
# add members
|
||||
discard rln.addAll(groupIDCommitments)
|
||||
|
||||
let
|
||||
wakuRlnRelay = WakuRLNRelay(membershipIndex: index, membershipKeyPair: groupKeyPairs[index], rlnInstance: rln)
|
||||
|
||||
# get the current epoch time
|
||||
let time = epochTime()
|
||||
|
||||
# create some messages from the same peer and append rln proof to them, except wm4
|
||||
var
|
||||
wm1 = WakuMessage(payload: "Valid message".toBytes())
|
||||
proofAdded1 = wakuRlnRelay.appendRLNProof(wm1, time)
|
||||
# another message in the same epoch as wm1, it will break the messaging rate limit
|
||||
wm2 = WakuMessage(payload: "Spam".toBytes())
|
||||
proofAdded2 = wakuRlnRelay.appendRLNProof(wm2, time)
|
||||
# wm3 points to the next epoch
|
||||
wm3 = WakuMessage(payload: "Valid message".toBytes())
|
||||
proofAdded3 = wakuRlnRelay.appendRLNProof(wm3, time+EPOCH_UNIT_SECONDS)
|
||||
wm4 = WakuMessage(payload: "Invalid message".toBytes())
|
||||
|
||||
# checks proofs are added
|
||||
check:
|
||||
proofAdded1
|
||||
proofAdded2
|
||||
proofAdded3
|
||||
|
||||
# validate messages
|
||||
# validateMessage proc checks the validity of the message fields and adds it to the log (if valid)
|
||||
let
|
||||
msgValidate1 = wakuRlnRelay.validateMessage(wm1)
|
||||
# wm2 is published within the same Epoch as wm1 and should be found as spam
|
||||
msgValidate2 = wakuRlnRelay.validateMessage(wm2)
|
||||
# a valid message should be validated successfully
|
||||
msgValidate3 = wakuRlnRelay.validateMessage(wm3)
|
||||
# wm4 has no rln proof and should not be validated
|
||||
msgValidate4 = wakuRlnRelay.validateMessage(wm4)
|
||||
|
||||
|
||||
check:
|
||||
msgValidate1 == MessageValidationResult.Valid
|
||||
msgValidate2 == MessageValidationResult.Spam
|
||||
msgValidate3 == MessageValidationResult.Valid
|
||||
msgValidate4 == MessageValidationResult.Invalid
|
||||
|
||||
|
|
|
@ -24,7 +24,9 @@ import
|
|||
../test_helpers
|
||||
|
||||
when defined(rln):
|
||||
import ../../waku/v2/protocol/waku_rln_relay/[waku_rln_relay_utils, waku_rln_relay_types]
|
||||
import
|
||||
../../waku/v2/protocol/waku_rln_relay/[waku_rln_relay_utils, waku_rln_relay_types]
|
||||
from times import epochTime
|
||||
|
||||
const RLNRELAY_PUBSUB_TOPIC = "waku/2/rlnrelay/proto"
|
||||
template sourceDir: string = currentSourcePath.parentDir()
|
||||
|
@ -658,12 +660,10 @@ procSuite "WakuNode":
|
|||
await sleepAsync(2000.millis)
|
||||
|
||||
# prepare the message payload
|
||||
var payload {.noinit.}: array[32, byte]
|
||||
for x in payload.mitems: x = 1
|
||||
let payload = "Hello".toBytes()
|
||||
|
||||
# prepare the epoch
|
||||
var epoch {.noinit.}: Epoch
|
||||
for x in epoch.mitems: x = 2
|
||||
let epoch = getCurrentEpoch()
|
||||
|
||||
# prepare the proof
|
||||
let rateLimitProofRes = node1.wakuRlnRelay.rlnInstance.proofGen(data = payload,
|
||||
|
@ -678,8 +678,8 @@ procSuite "WakuNode":
|
|||
proof: rateLimitProof)
|
||||
|
||||
|
||||
## node1 publishes a message with a non-spam proof, the message is then relayed to node2 which in turn
|
||||
## verifies the non-spam proof of the message and relays the message to node3
|
||||
## node1 publishes a message with a rate limit proof, the message is then relayed to node2 which in turn
|
||||
## verifies the rate limit proof of the message and relays the message to node3
|
||||
## verification at node2 occurs inside a topic validator which is installed as part of the waku-rln-relay mount proc
|
||||
await node1.publish(rlnRelayPubSubTopic, message)
|
||||
await sleepAsync(2000.millis)
|
||||
|
@ -759,12 +759,10 @@ procSuite "WakuNode":
|
|||
await sleepAsync(2000.millis)
|
||||
|
||||
# prepare the message payload
|
||||
var payload {.noinit.}: array[32, byte]
|
||||
for x in payload.mitems: x = 1
|
||||
let payload = "Hello".toBytes()
|
||||
|
||||
# prepare the epoch
|
||||
var epoch {.noinit.}: Epoch
|
||||
for x in epoch.mitems: x = 2
|
||||
let epoch = getCurrentEpoch()
|
||||
|
||||
# prepare the proof
|
||||
let rateLimitProofRes = node1.wakuRlnRelay.rlnInstance.proofGen(data = payload,
|
||||
|
@ -779,8 +777,8 @@ procSuite "WakuNode":
|
|||
proof: rateLimitProof)
|
||||
|
||||
|
||||
## node1 publishes a message with an invalid non-spam proof, the message is then relayed to node2 which in turn
|
||||
## attempts to verify the non-spam proof and fails hence does not relay the message to node3, thus the relayHandler of node3
|
||||
## node1 publishes a message with an invalid rln proof, the message is then relayed to node2 which in turn
|
||||
## attempts to verify the rate limit proof and fails hence does not relay the message to node3, thus the relayHandler of node3
|
||||
## never gets called
|
||||
## verification at node2 occurs inside a topic validator which is installed as part of the waku-rln-relay mount proc
|
||||
await node1.publish(rlnRelayPubSubTopic, message)
|
||||
|
@ -794,6 +792,131 @@ procSuite "WakuNode":
|
|||
await node2.stop()
|
||||
await node3.stop()
|
||||
|
||||
asyncTest "testing rln-relay double-signaling detection":
|
||||
|
||||
let
|
||||
# publisher node
|
||||
nodeKey1 = crypto.PrivateKey.random(Secp256k1, rng[])[]
|
||||
node1 = WakuNode.new(nodeKey1, ValidIpAddress.init("0.0.0.0"), Port(60000))
|
||||
# Relay node
|
||||
nodeKey2 = crypto.PrivateKey.random(Secp256k1, rng[])[]
|
||||
node2 = WakuNode.new(nodeKey2, ValidIpAddress.init("0.0.0.0"), Port(60002))
|
||||
# Subscriber
|
||||
nodeKey3 = crypto.PrivateKey.random(Secp256k1, rng[])[]
|
||||
node3 = WakuNode.new(nodeKey3, ValidIpAddress.init("0.0.0.0"), Port(60003))
|
||||
|
||||
rlnRelayPubSubTopic = RLNRELAY_PUBSUB_TOPIC
|
||||
contentTopic = ContentTopic("/waku/2/default-content/proto")
|
||||
|
||||
# set up three nodes
|
||||
# node1
|
||||
node1.mountRelay(@[rlnRelayPubSubTopic])
|
||||
let (groupOpt1, memKeyPairOpt1, memIndexOpt1) = rlnRelaySetUp(1) # set up rln relay inputs
|
||||
# mount rlnrelay in off-chain mode
|
||||
waitFor node1.mountRlnRelay(groupOpt = groupOpt1,
|
||||
memKeyPairOpt = memKeyPairOpt1,
|
||||
memIndexOpt= memIndexOpt1,
|
||||
onchainMode = false,
|
||||
pubsubTopic = rlnRelayPubSubTopic)
|
||||
await node1.start()
|
||||
|
||||
# node 2
|
||||
node2.mountRelay(@[rlnRelayPubSubTopic])
|
||||
let (groupOpt2, memKeyPairOpt2, memIndexOpt2) = rlnRelaySetUp(2) # set up rln relay inputs
|
||||
# mount rlnrelay in off-chain mode
|
||||
waitFor node2.mountRlnRelay(groupOpt = groupOpt2,
|
||||
memKeyPairOpt = memKeyPairOpt2,
|
||||
memIndexOpt= memIndexOpt2,
|
||||
onchainMode = false,
|
||||
pubsubTopic = rlnRelayPubSubTopic)
|
||||
await node2.start()
|
||||
|
||||
# node 3
|
||||
node3.mountRelay(@[rlnRelayPubSubTopic])
|
||||
let (groupOpt3, memKeyPairOpt3, memIndexOpt3) = rlnRelaySetUp(3) # set up rln relay inputs
|
||||
# mount rlnrelay in off-chain mode
|
||||
waitFor node3.mountRlnRelay(groupOpt = groupOpt3,
|
||||
memKeyPairOpt = memKeyPairOpt3,
|
||||
memIndexOpt= memIndexOpt3,
|
||||
onchainMode = false,
|
||||
pubsubTopic = rlnRelayPubSubTopic)
|
||||
await node3.start()
|
||||
|
||||
# connect the nodes together node1 <-> node2 <-> node3
|
||||
await node1.connectToNodes(@[node2.peerInfo.toRemotePeerInfo()])
|
||||
await node3.connectToNodes(@[node2.peerInfo.toRemotePeerInfo()])
|
||||
|
||||
# get the current epoch time
|
||||
let time = epochTime()
|
||||
# create some messages with rate limit proofs
|
||||
var
|
||||
wm1 = WakuMessage(payload: "message 1".toBytes())
|
||||
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())
|
||||
proofAdded2 = node3.wakuRlnRelay.appendRLNProof(wm2, time)
|
||||
# wm3 points to the next epoch
|
||||
wm3 = WakuMessage(payload: "message 3".toBytes())
|
||||
proofAdded3 = node3.wakuRlnRelay.appendRLNProof(wm3, time+EPOCH_UNIT_SECONDS)
|
||||
wm4 = WakuMessage(payload: "message4".toBytes())
|
||||
|
||||
# check proofs are added correctly
|
||||
check:
|
||||
proofAdded1
|
||||
proofAdded2
|
||||
proofAdded3
|
||||
|
||||
# relay handler for node3
|
||||
var completionFut1 = newFuture[bool]()
|
||||
var completionFut2 = newFuture[bool]()
|
||||
var completionFut3 = newFuture[bool]()
|
||||
var completionFut4 = newFuture[bool]()
|
||||
proc relayHandler(topic: string, data: seq[byte]) {.async, gcsafe.} =
|
||||
let msg = WakuMessage.init(data)
|
||||
if msg.isOk():
|
||||
let wm = msg.value()
|
||||
debug "The received topic:", topic
|
||||
if topic == rlnRelayPubSubTopic:
|
||||
if wm == wm1:
|
||||
completionFut1.complete(true)
|
||||
if wm == wm2:
|
||||
completionFut2.complete(true)
|
||||
if wm == wm3:
|
||||
completionFut3.complete(true)
|
||||
if wm == wm4:
|
||||
completionFut4.complete(true)
|
||||
|
||||
|
||||
# mount the relay handler for node3
|
||||
node3.subscribe(rlnRelayPubSubTopic, relayHandler)
|
||||
await sleepAsync(2000.millis)
|
||||
|
||||
## node1 publishes and relays 4 messages to node2
|
||||
## verification at node2 occurs inside a topic validator which is installed as part of the waku-rln-relay mount proc
|
||||
## node2 relays either of wm1 or wm2 to node3, depending on which message arrives at node2 first
|
||||
## node2 should detect either of wm1 or wm2 as spam and not relay it
|
||||
## node2 should relay wm3 to node3
|
||||
## node2 should not relay wm4 because it has no valid rln proof
|
||||
await node1.publish(rlnRelayPubSubTopic, wm1)
|
||||
await node1.publish(rlnRelayPubSubTopic, wm2)
|
||||
await node1.publish(rlnRelayPubSubTopic, wm3)
|
||||
await node1.publish(rlnRelayPubSubTopic, wm4)
|
||||
await sleepAsync(2000.millis)
|
||||
|
||||
let
|
||||
res1 = await completionFut1.withTimeout(10.seconds)
|
||||
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
|
||||
|
||||
await node1.stop()
|
||||
await node2.stop()
|
||||
await node3.stop()
|
||||
asyncTest "Relay protocol is started correctly":
|
||||
let
|
||||
nodeKey1 = crypto.PrivateKey.random(Secp256k1, rng[])[]
|
||||
|
|
|
@ -440,10 +440,20 @@ when defined(rln):
|
|||
proc validator(topic: string, message: messages.Message): Future[ValidationResult] {.async.} =
|
||||
let msg = WakuMessage.init(message.data)
|
||||
if msg.isOk():
|
||||
# check the proof
|
||||
if node.wakuRlnRelay.rlnInstance.proofVerify(msg.value().payload, msg.value().proof):
|
||||
return ValidationResult.Accept
|
||||
return ValidationResult.Reject
|
||||
let
|
||||
wakumessage = msg.value()
|
||||
# validate the message
|
||||
validationRes = node.wakuRlnRelay.validateMessage(wakumessage)
|
||||
case validationRes:
|
||||
of Valid:
|
||||
info "message validity is verified, relaying:", wakumessage=wakumessage
|
||||
return ValidationResult.Accept
|
||||
of Invalid:
|
||||
info "message validity could not be verified, discarding:", wakumessage=wakumessage
|
||||
return ValidationResult.Reject
|
||||
of Spam:
|
||||
info "A spam message is found! yay! discarding:", wakumessage=wakumessage
|
||||
return ValidationResult.Reject
|
||||
# set a validator for the supplied pubsubTopic
|
||||
let pb = PubSub(node.wakuRelay)
|
||||
pb.addValidator(pubsubTopic, validator)
|
||||
|
|
|
@ -9,8 +9,9 @@
|
|||
{.push raises: [Defect].}
|
||||
|
||||
import
|
||||
libp2p/protobuf/minprotobuf,
|
||||
waku_rln_relay/waku_rln_relay_types
|
||||
libp2p/protobuf/minprotobuf
|
||||
when defined(rln):
|
||||
import waku_rln_relay/waku_rln_relay_types
|
||||
|
||||
type
|
||||
ContentTopic* = string
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{.push raises: [Defect].}
|
||||
|
||||
import
|
||||
std/tables,
|
||||
options, chronos, stint,
|
||||
web3,
|
||||
eth/keys,
|
||||
|
@ -56,6 +57,11 @@ type RateLimitProof* = object
|
|||
|
||||
type MembershipIndex* = uint
|
||||
|
||||
type ProofMetadata* = object
|
||||
nullifier*: Nullifier
|
||||
shareX*: MerkleNode
|
||||
shareY*: MerkleNode
|
||||
|
||||
type WakuRLNRelay* = ref object
|
||||
membershipKeyPair*: MembershipKeyPair
|
||||
# membershipIndex denotes the index of a leaf in the Merkle tree
|
||||
|
@ -71,7 +77,11 @@ type WakuRLNRelay* = ref object
|
|||
ethAccountPrivateKey*: Option[PrivateKey]
|
||||
rlnInstance*: RLN[Bn256]
|
||||
pubsubTopic*: string # the pubsub topic for which rln relay is mounted
|
||||
# the log of nullifiers and Shamir shares of the past messages grouped per epoch
|
||||
nullifierLog*: Table[Epoch, seq[ProofMetadata]]
|
||||
|
||||
type MessageValidationResult* {.pure.} = enum
|
||||
Valid, Invalid, Spam
|
||||
|
||||
# inputs of the membership contract constructor
|
||||
# TODO may be able to make these constants private and put them inside the waku_rln_relay_utils
|
||||
|
@ -103,8 +113,12 @@ 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"
|
||||
|
||||
# Protobufs enc and init
|
||||
const EPOCH_UNIT_SECONDS* = float64(2)
|
||||
const MAX_CLOCK_GAP_SECONDS* = 20.0 # the maximum clock difference between peers
|
||||
# maximum allowed gap between the epochs of messages' RateLimitProofs
|
||||
const MAX_EPOCH_GAP* = int64(MAX_CLOCK_GAP_SECONDS/EPOCH_UNIT_SECONDS)
|
||||
|
||||
# Protobufs enc and init
|
||||
proc init*(T: type RateLimitProof, buffer: seq[byte]): ProtoResult[T] =
|
||||
var nsp: RateLimitProof
|
||||
let pb = initProtoBuffer(buffer)
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
{.push raises: [Defect].}
|
||||
|
||||
import
|
||||
std/sequtils,
|
||||
std/sequtils, tables, times,
|
||||
chronicles, options, chronos, stint,
|
||||
web3,
|
||||
stew/results,
|
||||
stew/[byteutils, arrayops, endians2],
|
||||
rln,
|
||||
waku_rln_relay_types
|
||||
waku_rln_relay_types,
|
||||
../waku_message
|
||||
|
||||
logScope:
|
||||
topics = "wakurlnrelayutils"
|
||||
|
@ -339,3 +340,158 @@ proc rlnRelaySetUp*(rlnRelayMemIndex: MembershipIndex): (Option[seq[IDCommitment
|
|||
memIndexOpt= some(rlnRelayMemIndex)
|
||||
|
||||
return (groupOpt, memKeyPairOpt, memIndexOpt)
|
||||
|
||||
proc hasDuplicate*(rlnPeer: WakuRLNRelay, msg: WakuMessage): Result[bool, string] =
|
||||
## returns true if there is another message in the `nullifierLog` of the `rlnPeer` with the same
|
||||
## epoch and nullifier as `msg`'s epoch and nullifier but different Shamir secret shares
|
||||
## otherwise, returns false
|
||||
## emits an error string if `KeyError` occurs (never happens, it is just to avoid raising unnecessary `KeyError` exception )
|
||||
|
||||
# extract the proof metadata of the supplied `msg`
|
||||
let proofMD = ProofMetadata(nullifier: msg.proof.nullifier, shareX: msg.proof.shareX, shareY: msg.proof.shareY)
|
||||
|
||||
# check if the epoch exists
|
||||
if not rlnPeer.nullifierLog.hasKey(msg.proof.epoch):
|
||||
return ok(false)
|
||||
try:
|
||||
if rlnPeer.nullifierLog[msg.proof.epoch].contains(proofMD):
|
||||
# there is an identical record, ignore rhe mag
|
||||
return ok(false)
|
||||
|
||||
# check for a message with the same nullifier but different secret shares
|
||||
let matched = rlnPeer.nullifierLog[msg.proof.epoch].filterIt((it.nullifier == proofMD.nullifier) and ((it.shareX != proofMD.shareX) or (it.shareY != proofMD.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, msg: WakuMessage): Result[bool, string] =
|
||||
## extracts the `ProofMetadata` of the supplied messages `msg` and
|
||||
## saves it in the `nullifierLog` of the `rlnPeer`
|
||||
|
||||
let proofMD = ProofMetadata(nullifier: msg.proof.nullifier, shareX: msg.proof.shareX, shareY: msg.proof.shareY)
|
||||
debug "proof metadata", proofMD=proofMD
|
||||
|
||||
# check if the epoch exists
|
||||
if not rlnPeer.nullifierLog.hasKey(msg.proof.epoch):
|
||||
rlnPeer.nullifierLog[msg.proof.epoch]= @[proofMD]
|
||||
return ok(true)
|
||||
|
||||
try:
|
||||
# check if an identical record exists
|
||||
if rlnPeer.nullifierLog[msg.proof.epoch].contains(proofMD):
|
||||
return ok(true)
|
||||
# add proofMD to the log
|
||||
rlnPeer.nullifierLog[msg.proof.epoch].add(proofMD)
|
||||
return ok(true)
|
||||
except KeyError as e:
|
||||
return err("the epoch was not found")
|
||||
|
||||
proc toEpoch*(t: uint64): Epoch =
|
||||
## converts `t` to `Epoch` in little-endian order
|
||||
let bytes = toBytes(t, Endianness.littleEndian)
|
||||
debug "bytes", bytes=bytes
|
||||
var epoch: Epoch
|
||||
discard epoch.copyFrom(bytes)
|
||||
return epoch
|
||||
|
||||
proc fromEpoch*(epoch: Epoch): uint64 =
|
||||
## decodes bytes of `epoch` (in little-endian) to uint64
|
||||
let t = fromBytesLE(uint64, array[32,byte](epoch))
|
||||
return t
|
||||
|
||||
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/EPOCH_UNIT_SECONDS)
|
||||
return toEpoch(e)
|
||||
|
||||
proc getCurrentEpoch*(): Epoch =
|
||||
## gets the current rln Epoch time
|
||||
return calcEpoch(epochTime())
|
||||
|
||||
proc compare*(e1, e2: Epoch): int64 =
|
||||
## returns the 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)
|
||||
return int64(epoch1) - int64(epoch2)
|
||||
|
||||
|
||||
proc validateMessage*(rlnPeer: WakuRLNRelay, msg: WakuMessage): MessageValidationResult =
|
||||
## validate the supplied `msg` based on the waku-rln-relay routing protocol i.e.,
|
||||
## the `msg`'s epoch is within MAX_EPOCH_GAP of the current epoch
|
||||
## the `msg` has valid rate limit proof
|
||||
## the `msg` does not violate the rate limit
|
||||
|
||||
|
||||
# checks if the `msg`'s epoch is far from the current epoch
|
||||
# it corresponds to the validation of rln external nullifier
|
||||
let
|
||||
# get current rln epoch
|
||||
epoch = getCurrentEpoch()
|
||||
msgEpoch = msg.proof.epoch
|
||||
# calculate the gaps
|
||||
gap = compare(epoch, 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
|
||||
return MessageValidationResult.Invalid
|
||||
|
||||
# verify the proof
|
||||
if not rlnPeer.rlnInstance.proofVerify(msg.payload, msg.proof):
|
||||
# invalid proof
|
||||
return MessageValidationResult.Invalid
|
||||
|
||||
# check if double messaging has happened
|
||||
let hasDup = rlnPeer.hasDuplicate(msg)
|
||||
if hasDup.isOk and hasDup.value == true:
|
||||
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)
|
||||
return MessageValidationResult.Valid
|
||||
|
||||
|
||||
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)
|
||||
|
||||
var proof: RateLimitProofResult = proofGen(rlnInstance = rlnPeer.rlnInstance, data = input,
|
||||
memKeys = rlnPeer.membershipKeyPair,
|
||||
memIndex = rlnPeer.membershipIndex,
|
||||
epoch = calcEpoch(senderEpochTime))
|
||||
|
||||
if proof.isErr:
|
||||
return false
|
||||
|
||||
msg.proof = proof.value
|
||||
return true
|
||||
|
||||
proc addAll*(rlnInstance: RLN[Bn256], list: seq[IDCommitment]): bool =
|
||||
# add members to the Merkle tree of the `rlnInstance`
|
||||
for i in 0..list.len-1:
|
||||
let member = list[i]
|
||||
let member_is_added = rlnInstance.insertMember(member)
|
||||
if not member_is_added:
|
||||
return false
|
||||
return true
|
Loading…
Reference in New Issue