chore|feat (waku-rln-relay): modules reorganization|Initial test for capturing events using nim-web3 (#941)

* first edition

* adds the full test scenario

* fixes typos

* fixes a bug in the supplied command

* further edits the description

* displays the chat prompt after spam detection

* updates changelog

* minor wording fix

* adds a new test file for onchain rln relay

* adds the Event proc

* adds one working example of event subscription

* defines a new unitt test for event subscription

* adds the new test file

* cleans up the code

* adds a working event subscription for faucet contract

* wip

* makes faucet test conditional

* updates contract byte codes

* adds a working test for event subscription and cleans up the tests

* fixes case

* adss toUInt256 unit function

* enables the tests

* fixes a bug

* undo commented tests

* cleans up the test

* logs the pk

* removes excess entry in the changelog

* fixes spacing

* comments

* removes unused test codes

* adds the conditional compilation for onchain tests

* uncomments offchain tests

* removes onchain tests

* reorganizes the code and moves the rln contract data into a separate module

* deletes txt files

* beautifies the code

* beautifies the code

* removes an excess line

* more formatting fixes

* minor fix

* updates the case of membership fee const

* renames compare to diff

* renames time to e

* edits the number of arguments of the send proc

* fixes a comment alignment

* fixes indentation

* fixed id style

* splits check from condition

* fixes a naming mismatch
This commit is contained in:
Sanaz Taheri Boshrooyeh 2022-05-10 14:09:18 -07:00 committed by GitHub
parent e635fda9ae
commit 63a9d9e63b
9 changed files with 962 additions and 569 deletions

View File

@ -27,6 +27,8 @@ import
when defined(rln): when defined(rln):
import ./v2/test_waku_rln_relay import ./v2/test_waku_rln_relay
when defined(onchain_rln):
import ./v2/test_waku_rln_relay_onchain
# TODO Only enable this once swap module is integrated more nicely as a dependency, i.e. as submodule with CI etc # TODO Only enable this once swap module is integrated more nicely as a dependency, i.e. as submodule with CI etc

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,8 @@ import
testutils/unittests, chronos, chronicles, stint, web3, testutils/unittests, chronos, chronicles, stint, web3,
stew/byteutils, stew/shims/net as stewNet, stew/byteutils, stew/shims/net as stewNet,
libp2p/crypto/crypto, libp2p/crypto/crypto,
../../waku/v2/protocol/waku_rln_relay/[rln, waku_rln_relay_utils, waku_rln_relay_types], ../../waku/v2/protocol/waku_rln_relay/[rln, waku_rln_relay_utils,
waku_rln_relay_types],
../../waku/v2/node/wakunode2, ../../waku/v2/node/wakunode2,
../test_helpers, ../test_helpers,
./test_utils ./test_utils
@ -14,258 +15,7 @@ import
const RLNRELAY_PUBSUB_TOPIC = "waku/2/rlnrelay/proto" const RLNRELAY_PUBSUB_TOPIC = "waku/2/rlnrelay/proto"
const RLNRELAY_CONTENT_TOPIC = "waku/2/rlnrelay/proto" const RLNRELAY_CONTENT_TOPIC = "waku/2/rlnrelay/proto"
# POSEIDON_HASHER_CODE holds the bytecode of Poseidon hasher solidity smart contract:
# https://github.com/kilic/rlnapp/blob/master/packages/contracts/contracts/crypto/PoseidonHasher.sol
# the solidity contract is compiled separately and the resultant bytecode is copied here
const POSEIDON_HASHER_CODE = readFile("tests/v2/poseidonHasher.txt")
# MEMBERSHIP_CONTRACT_CODE contains the bytecode of the membership solidity smart contract:
# https://github.com/kilic/rlnapp/blob/master/packages/contracts/contracts/RLN.sol
# the solidity contract is compiled separately and the resultant bytecode is copied here
const MEMBERSHIP_CONTRACT_CODE = readFile("tests/v2/membershipContract.txt")
# the membership contract code in solidity
# uint256 public immutable MEMBERSHIP_DEPOSIT;
# uint256 public immutable DEPTH;
# uint256 public immutable SET_SIZE;
# uint256 public pubkeyIndex = 0;
# mapping(uint256 => uint256) public members;
# IPoseidonHasher public poseidonHasher;
# event MemberRegistered(uint256 indexed pubkey, uint256 indexed index);
# event MemberWithdrawn(uint256 indexed pubkey, uint256 indexed index);
# constructor(
# uint256 membershipDeposit,
# uint256 depth,
# address _poseidonHasher
# ) public {
# MEMBERSHIP_DEPOSIT = membershipDeposit;
# DEPTH = depth;
# SET_SIZE = 1 << depth;
# poseidonHasher = IPoseidonHasher(_poseidonHasher);
# }
# function register(uint256 pubkey) external payable {
# require(pubkeyIndex < SET_SIZE, "RLN, register: set is full");
# require(msg.value == MEMBERSHIP_DEPOSIT, "RLN, register: membership deposit is not satisfied");
# _register(pubkey);
# }
# function registerBatch(uint256[] calldata pubkeys) external payable {
# require(pubkeyIndex + pubkeys.length <= SET_SIZE, "RLN, registerBatch: set is full");
# require(msg.value == MEMBERSHIP_DEPOSIT * pubkeys.length, "RLN, registerBatch: membership deposit is not satisfied");
# for (uint256 i = 0; i < pubkeys.length; i++) {
# _register(pubkeys[i]);
# }
# }
# function withdrawBatch(
# uint256[] calldata secrets,
# uint256[] calldata pubkeyIndexes,
# address payable[] calldata receivers
# ) external {
# uint256 batchSize = secrets.length;
# require(batchSize != 0, "RLN, withdrawBatch: batch size zero");
# require(batchSize == pubkeyIndexes.length, "RLN, withdrawBatch: batch size mismatch pubkey indexes");
# require(batchSize == receivers.length, "RLN, withdrawBatch: batch size mismatch receivers");
# for (uint256 i = 0; i < batchSize; i++) {
# _withdraw(secrets[i], pubkeyIndexes[i], receivers[i]);
# }
# }
# function withdraw(
# uint256 secret,
# uint256 _pubkeyIndex,
# address payable receiver
# ) external {
# _withdraw(secret, _pubkeyIndex, receiver);
# }
contract(MembershipContract):
proc register(pubkey: Uint256) # external payable
# proc registerBatch(pubkeys: seq[Uint256]) # external payable
# TODO will add withdraw function after integrating the keyGeneration function (required to compute public keys from secret keys)
# proc withdraw(secret: Uint256, pubkeyIndex: Uint256, receiver: Address)
# proc withdrawBatch( secrets: seq[Uint256], pubkeyIndex: seq[Uint256], receiver: seq[Address])
proc uploadContract(ethClientAddress: string): Future[Address] {.async.} =
let web3 = await newWeb3(ethClientAddress)
debug "web3 connected to", ethClientAddress
# fetch the list of registered accounts
let accounts = await web3.provider.eth_accounts()
web3.defaultAccount = accounts[1]
let add =web3.defaultAccount
debug "contract deployer account address ", add
var balance = await web3.provider.eth_getBalance(web3.defaultAccount , "latest")
debug "Initial account balance: ", balance
# deploy the poseidon hash first
let
hasherReceipt = await web3.deployContract(POSEIDON_HASHER_CODE)
hasherAddress = hasherReceipt.contractAddress.get
debug "hasher address: ", hasherAddress
# encode membership contract inputs to 32 bytes zero-padded
let
membershipFeeEncoded = encode(MembershipFee).data
depthEncoded = encode(MERKLE_TREE_DEPTH.u256).data
hasherAddressEncoded = encode(hasherAddress).data
# this is the contract constructor input
contractInput = membershipFeeEncoded & depthEncoded & hasherAddressEncoded
debug "encoded membership fee: ", membershipFeeEncoded
debug "encoded depth: ", depthEncoded
debug "encoded hasher address: ", hasherAddressEncoded
debug "encoded contract input:" , contractInput
# deploy membership contract with its constructor inputs
let receipt = await web3.deployContract(MEMBERSHIP_CONTRACT_CODE, contractInput = contractInput)
var contractAddress = receipt.contractAddress.get
debug "Address of the deployed membership contract: ", contractAddress
# balance = await web3.provider.eth_getBalance(web3.defaultAccount , "latest")
# debug "Account balance after the contract deployment: ", balance
await web3.close()
debug "disconnected from ", ethClientAddress
return contractAddress
procSuite "Waku rln relay": procSuite "Waku rln relay":
when defined(onchain_rln):
asyncTest "contract membership":
debug "ethereum client address", ETH_CLIENT
let contractAddress = await uploadContract(ETH_CLIENT)
# connect to the eth client
let web3 = await newWeb3(ETH_CLIENT)
debug "web3 connected to", ETH_CLIENT
# fetch the list of registered accounts
let accounts = await web3.provider.eth_accounts()
web3.defaultAccount = accounts[1]
let add = web3.defaultAccount
debug "contract deployer account address ", add
# prepare a contract sender to interact with it
var sender = web3.contractSender(MembershipContract, contractAddress) # creates a Sender object with a web3 field and contract address of type Address
# send takes three parameters, c: ContractCallBase, value = 0.u256, gas = 3000000'u64 gasPrice = 0
# should use send proc for the contract functions that update the state of the contract
let tx = await sender.register(20.u256).send(value = MembershipFee)
debug "The hash of registration tx: ", tx # value is the membership fee
# var members: array[2, uint256] = [20.u256, 21.u256]
# debug "This is the batch registration result ", await sender.registerBatch(members).send(value = (members.len * membershipFee)) # value is the membership fee
# balance = await web3.provider.eth_getBalance(web3.defaultAccount , "latest")
# debug "Balance after registration: ", balance
await web3.close()
debug "disconnected from", ETH_CLIENT
asyncTest "registration procedure":
# deploy the contract
let contractAddress = await uploadContract(ETH_CLIENT)
# prepare rln-relay peer inputs
let
web3 = await newWeb3(ETH_CLIENT)
accounts = await web3.provider.eth_accounts()
# choose one of the existing accounts for the rln-relay peer
ethAccountAddress = accounts[9]
await web3.close()
# create an RLN instance
var rlnInstance = createRLNInstance()
check: rlnInstance.isOk == true
# generate the membership keys
let membershipKeyPair = membershipKeyGen(rlnInstance.value)
check: membershipKeyPair.isSome
# initialize the WakuRLNRelay
var rlnPeer = WakuRLNRelay(membershipKeyPair: membershipKeyPair.get(),
membershipIndex: MembershipIndex(0),
ethClientAddress: ETH_CLIENT,
ethAccountAddress: ethAccountAddress,
membershipContractAddress: contractAddress)
# register the rln-relay peer to the membership contract
let is_successful = await rlnPeer.register()
check:
is_successful
asyncTest "mounting waku rln-relay":
let
nodeKey = crypto.PrivateKey.random(Secp256k1, rng[])[]
node = WakuNode.new(nodeKey, ValidIpAddress.init("0.0.0.0"),
Port(60000))
await node.start()
# deploy the contract
let membershipContractAddress = await uploadContract(ETH_CLIENT)
# prepare rln-relay inputs
let
web3 = await newWeb3(ETH_CLIENT)
accounts = await web3.provider.eth_accounts()
# choose one of the existing account for the rln-relay peer
ethAccountAddress = accounts[9]
await web3.close()
# create current peer's pk
var rlnInstance = createRLNInstance()
check rlnInstance.isOk == true
var rln = rlnInstance.value
# generate a key pair
var keypair = rln.membershipKeyGen()
doAssert(keypair.isSome())
# current peer index in the Merkle tree
let index = uint(5)
# Create a group of 10 members
var group = newSeq[IDCommitment]()
for i in 0..10:
var member_is_added: bool = false
if (uint(i) == index):
# insert the current peer's pk
group.add(keypair.get().idCommitment)
member_is_added = rln.insertMember(keypair.get().idCommitment)
doAssert(member_is_added)
debug "member key", key=keypair.get().idCommitment.toHex
else:
var memberKeypair = rln.membershipKeyGen()
doAssert(memberKeypair.isSome())
group.add(memberKeypair.get().idCommitment)
member_is_added = rln.insertMember(memberKeypair.get().idCommitment)
doAssert(member_is_added)
debug "member key", key=memberKeypair.get().idCommitment.toHex
let expectedRoot = rln.getMerkleRoot().value().toHex
debug "expected root ", expectedRoot
# start rln-relay
node.mountRelay(@[RLNRELAY_PUBSUB_TOPIC])
await node.mountRlnRelay(ethClientAddrOpt = some(EthClient),
ethAccAddrOpt = some(ethAccountAddress),
memContractAddOpt = some(membershipContractAddress),
groupOpt = some(group),
memKeyPairOpt = some(keypair.get()),
memIndexOpt = some(index),
pubsubTopic = RLNRELAY_PUBSUB_TOPIC,
contentTopic = RLNRELAY_CONTENT_TOPIC)
let calculatedRoot = node.wakuRlnRelay.rlnInstance.getMerkleRoot().value().toHex
debug "calculated root ", calculatedRoot
check expectedRoot == calculatedRoot
await node.stop()
asyncTest "mount waku-rln-relay in the off-chain mode": asyncTest "mount waku-rln-relay in the off-chain mode":
let let
nodeKey = crypto.PrivateKey.random(Secp256k1, rng[])[] nodeKey = crypto.PrivateKey.random(Secp256k1, rng[])[]
@ -278,69 +28,72 @@ procSuite "Waku rln relay":
# create a group of 100 membership keys # create a group of 100 membership keys
let let
(groupKeys, root) = createMembershipList(100) (groupKeys, root) = createMembershipList(100)
check groupKeys.len == 100 check:
let groupKeys.len == 100
let
# convert the keys to MembershipKeyPair structs # convert the keys to MembershipKeyPair structs
groupKeyPairs = groupKeys.toMembershipKeyPairs() groupKeyPairs = groupKeys.toMembershipKeyPairs()
# extract the id commitments # extract the id commitments
groupIDCommitments = groupKeyPairs.mapIt(it.idCommitment) groupIDCommitments = groupKeyPairs.mapIt(it.idCommitment)
debug "groupKeyPairs", groupKeyPairs debug "groupKeyPairs", groupKeyPairs
debug "groupIDCommitments", groupIDCommitments debug "groupIDCommitments", groupIDCommitments
# index indicates the position of a membership key pair in the static list of group keys i.e., groupKeyPairs # 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 # 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 # index also represents the index of the leaf in the Merkle tree that contains node's commitment key
let index = MembershipIndex(5) let index = MembershipIndex(5)
# -------- mount rln-relay in the off-chain mode # -------- mount rln-relay in the off-chain mode
node.mountRelay(@[RLNRELAY_PUBSUB_TOPIC]) node.mountRelay(@[RLNRELAY_PUBSUB_TOPIC])
await node.mountRlnRelay(groupOpt = some(groupIDCommitments), await node.mountRlnRelay(groupOpt = some(groupIDCommitments),
memKeyPairOpt = some(groupKeyPairs[index]), memKeyPairOpt = some(groupKeyPairs[index]),
memIndexOpt = some(index), memIndexOpt = some(index),
onchainMode = false, onchainMode = false,
pubsubTopic = RLNRELAY_PUBSUB_TOPIC, pubsubTopic = RLNRELAY_PUBSUB_TOPIC,
contentTopic = RLNRELAY_CONTENT_TOPIC) contentTopic = RLNRELAY_CONTENT_TOPIC)
# get the root of Merkle tree which is constructed inside the mountRlnRelay proc # get the root of Merkle tree which is constructed inside the mountRlnRelay proc
let calculatedRoot = node.wakuRlnRelay.rlnInstance.getMerkleRoot().value().toHex let calculatedRoot = node.wakuRlnRelay.rlnInstance.getMerkleRoot().value().toHex
debug "calculated root by mountRlnRelay", calculatedRoot debug "calculated root by mountRlnRelay", calculatedRoot
# this part checks whether the Merkle tree is constructed correctly inside the mountRlnRelay proc # this part checks whether the Merkle tree is constructed correctly inside the mountRlnRelay proc
# this check is done by comparing the tree root resulted from mountRlnRelay i.e., calculatedRoot # this check is done by comparing the tree root resulted from mountRlnRelay i.e., calculatedRoot
# against the root which is the expected root # against the root which is the expected root
check calculatedRoot == root check:
calculatedRoot == root
await node.stop() await node.stop()
suite "Waku rln relay": suite "Waku rln relay":
test "key_gen Nim Wrappers": test "key_gen Nim Wrappers":
var var
merkleDepth: csize_t = 32 merkleDepth: csize_t = 32
# parameters.key contains the parameters related to the Poseidon hasher # parameters.key contains the parameters related to the Poseidon hasher
# to generate this file, clone this repo https://github.com/kilic/rln # to generate this file, clone this repo https://github.com/kilic/rln
# and run the following command in the root directory of the cloned project # and run the following command in the root directory of the cloned project
# cargo run --example export_test_keys # cargo run --example export_test_keys
# the file is generated separately and copied here # the file is generated separately and copied here
parameters = readFile("waku/v2/protocol/waku_rln_relay/parameters.key") parameters = readFile("waku/v2/protocol/waku_rln_relay/parameters.key")
pbytes = parameters.toBytes() pbytes = parameters.toBytes()
len : csize_t = uint(pbytes.len) len: csize_t = uint(pbytes.len)
parametersBuffer = Buffer(`ptr`: addr(pbytes[0]), len: len) parametersBuffer = Buffer(`ptr`: addr(pbytes[0]), len: len)
check: check:
# check the parameters.key is not empty # check the parameters.key is not empty
pbytes.len != 0 pbytes.len != 0
var var
rlnInstance: RLN[Bn256] rlnInstance: RLN[Bn256]
let res = new_circuit_from_params(merkleDepth, addr parametersBuffer, addr rlnInstance) let res = new_circuit_from_params(merkleDepth, addr parametersBuffer,
addr rlnInstance)
check: check:
# check whether the circuit parameters are generated successfully # check whether the circuit parameters are generated successfully
res == true res == true
# keysBufferPtr will hold the generated key pairs i.e., secret and public keys # keysBufferPtr will hold the generated key pairs i.e., secret and public keys
var var
keysBuffer : Buffer keysBuffer: Buffer
keysBufferPtr = addr(keysBuffer) keysBufferPtr = addr(keysBuffer)
done = key_gen(rlnInstance, keysBufferPtr) done = key_gen(rlnInstance, keysBufferPtr)
check: check:
# check whether the keys are generated successfully # check whether the keys are generated successfully
done == true done == true
@ -350,8 +103,8 @@ suite "Waku rln relay":
check: check:
# the public and secret keys together are 64 bytes # the public and secret keys together are 64 bytes
generatedKeys.len == 64 generatedKeys.len == 64
debug "generated keys: ", generatedKeys debug "generated keys: ", generatedKeys
test "membership Key Gen": test "membership Key Gen":
# create an RLN instance # create an RLN instance
var rlnInstance = createRLNInstance() var rlnInstance = createRLNInstance()
@ -359,15 +112,15 @@ suite "Waku rln relay":
rlnInstance.isOk == true rlnInstance.isOk == true
var key = membershipKeyGen(rlnInstance.value) var key = membershipKeyGen(rlnInstance.value)
var empty : array[32,byte] var empty: array[32, byte]
check: check:
key.isSome key.isSome
key.get().idKey.len == 32 key.get().idKey.len == 32
key.get().idCommitment.len == 32 key.get().idCommitment.len == 32
key.get().idKey != empty key.get().idKey != empty
key.get().idCommitment != empty key.get().idCommitment != empty
debug "the generated membership key pair: ", key debug "the generated membership key pair: ", key
test "get_root Nim binding": test "get_root Nim binding":
# create an RLN instance which also includes an empty Merkle tree # create an RLN instance which also includes an empty Merkle tree
@ -376,8 +129,8 @@ suite "Waku rln relay":
rlnInstance.isOk == true rlnInstance.isOk == true
# read the Merkle Tree root # read the Merkle Tree root
var var
root1 {.noinit.} : Buffer = Buffer() root1 {.noinit.}: Buffer = Buffer()
rootPtr1 = addr(root1) rootPtr1 = addr(root1)
get_root_successful1 = get_root(rlnInstance.value, rootPtr1) get_root_successful1 = get_root(rlnInstance.value, rootPtr1)
check: check:
@ -385,22 +138,23 @@ suite "Waku rln relay":
root1.len == 32 root1.len == 32
# read the Merkle Tree root # read the Merkle Tree root
var var
root2 {.noinit.} : Buffer = Buffer() root2 {.noinit.}: Buffer = Buffer()
rootPtr2 = addr(root2) rootPtr2 = addr(root2)
get_root_successful2 = get_root(rlnInstance.value, rootPtr2) get_root_successful2 = get_root(rlnInstance.value, rootPtr2)
check: check:
get_root_successful2 get_root_successful2
root2.len == 32 root2.len == 32
var rootValue1 = cast[ptr array[32,byte]] (root1.`ptr`) var rootValue1 = cast[ptr array[32, byte]] (root1.`ptr`)
let rootHex1 = rootValue1[].toHex let rootHex1 = rootValue1[].toHex
var rootValue2 = cast[ptr array[32,byte]] (root2.`ptr`) var rootValue2 = cast[ptr array[32, byte]] (root2.`ptr`)
let rootHex2 = rootValue2[].toHex let rootHex2 = rootValue2[].toHex
# the two roots must be identical # the two roots must be identical
check rootHex1 == rootHex2 check:
rootHex1 == rootHex2
test "getMerkleRoot utils": test "getMerkleRoot utils":
# create an RLN instance which also includes an empty Merkle tree # create an RLN instance which also includes an empty Merkle tree
var rlnInstance = createRLNInstance() var rlnInstance = createRLNInstance()
@ -409,16 +163,19 @@ suite "Waku rln relay":
# read the Merkle Tree root # read the Merkle Tree root
var root1 = getMerkleRoot(rlnInstance.value()) var root1 = getMerkleRoot(rlnInstance.value())
check root1.isOk check:
root1.isOk
let rootHex1 = root1.value().toHex let rootHex1 = root1.value().toHex
# read the Merkle Tree root # read the Merkle Tree root
var root2 = getMerkleRoot(rlnInstance.value()) var root2 = getMerkleRoot(rlnInstance.value())
check root2.isOk check:
root2.isOk
let rootHex2 = root2.value().toHex let rootHex2 = root2.value().toHex
# the two roots must be identical # the two roots must be identical
check rootHex1 == rootHex2 check:
rootHex1 == rootHex2
test "update_next_member Nim Wrapper": test "update_next_member Nim Wrapper":
# create an RLN instance which also includes an empty Merkle tree # create an RLN instance which also includes an empty Merkle tree
@ -428,7 +185,8 @@ suite "Waku rln relay":
# generate a key pair # generate a key pair
var keypair = membershipKeyGen(rlnInstance.value) var keypair = membershipKeyGen(rlnInstance.value)
check keypair.isSome() check:
keypair.isSome()
var pkBuffer = toBuffer(keypair.get().idCommitment) var pkBuffer = toBuffer(keypair.get().idCommitment)
let pkBufferPtr = addr pkBuffer let pkBufferPtr = addr pkBuffer
@ -436,17 +194,18 @@ suite "Waku rln relay":
var member_is_added = update_next_member(rlnInstance.value, pkBufferPtr) var member_is_added = update_next_member(rlnInstance.value, pkBufferPtr)
check: check:
member_is_added == true member_is_added == true
test "delete_member Nim wrapper": test "delete_member Nim wrapper":
# create an RLN instance which also includes an empty Merkle tree # create an RLN instance which also includes an empty Merkle tree
var rlnInstance = createRLNInstance() var rlnInstance = createRLNInstance()
check: check:
rlnInstance.isOk == true rlnInstance.isOk == true
# delete the first member # delete the first member
var deleted_member_index = MembershipIndex(0) var deleted_member_index = MembershipIndex(0)
let deletion_success = delete_member(rlnInstance.value, deleted_member_index) let deletion_success = delete_member(rlnInstance.value, deleted_member_index)
check deletion_success check:
deletion_success
test "insertMember rln utils": test "insertMember rln utils":
# create an RLN instance which also includes an empty Merkle tree # create an RLN instance which also includes an empty Merkle tree
@ -456,17 +215,18 @@ suite "Waku rln relay":
var rln = rlnInstance.value var rln = rlnInstance.value
# generate a key pair # generate a key pair
var keypair = rln.membershipKeyGen() var keypair = rln.membershipKeyGen()
check keypair.isSome()
check: check:
rln.insertMember(keypair.get().idCommitment) keypair.isSome()
check:
rln.insertMember(keypair.get().idCommitment)
test "removeMember rln utils": test "removeMember rln utils":
# create an RLN instance which also includes an empty Merkle tree # create an RLN instance which also includes an empty Merkle tree
var rlnInstance = createRLNInstance() var rlnInstance = createRLNInstance()
check: check:
rlnInstance.isOk == true rlnInstance.isOk == true
var rln = rlnInstance.value var rln = rlnInstance.value
check: check:
rln.removeMember(MembershipIndex(0)) rln.removeMember(MembershipIndex(0))
test "Merkle tree consistency check between deletion and insertion": test "Merkle tree consistency check between deletion and insertion":
@ -476,14 +236,14 @@ suite "Waku rln relay":
rlnInstance.isOk == true rlnInstance.isOk == true
# read the Merkle Tree root # read the Merkle Tree root
var var
root1 {.noinit.} : Buffer = Buffer() root1 {.noinit.}: Buffer = Buffer()
rootPtr1 = addr(root1) rootPtr1 = addr(root1)
get_root_successful1 = get_root(rlnInstance.value, rootPtr1) get_root_successful1 = get_root(rlnInstance.value, rootPtr1)
check: check:
get_root_successful1 get_root_successful1
root1.len == 32 root1.len == 32
# generate a key pair # generate a key pair
var keypair = membershipKeyGen(rlnInstance.value) var keypair = membershipKeyGen(rlnInstance.value)
check: keypair.isSome() check: keypair.isSome()
@ -492,49 +252,52 @@ suite "Waku rln relay":
# add the member to the tree # add the member to the tree
var member_is_added = update_next_member(rlnInstance.value, pkBufferPtr) var member_is_added = update_next_member(rlnInstance.value, pkBufferPtr)
check member_is_added check:
member_is_added
# read the Merkle Tree root after insertion # read the Merkle Tree root after insertion
var var
root2 {.noinit.} : Buffer = Buffer() root2 {.noinit.}: Buffer = Buffer()
rootPtr2 = addr(root2) rootPtr2 = addr(root2)
get_root_successful2 = get_root(rlnInstance.value, rootPtr2) get_root_successful2 = get_root(rlnInstance.value, rootPtr2)
check: check:
get_root_successful2 get_root_successful2
root2.len == 32 root2.len == 32
# delete the first member # delete the first member
var deleted_member_index = MembershipIndex(0) var deleted_member_index = MembershipIndex(0)
let deletion_success = delete_member(rlnInstance.value, deleted_member_index) let deletion_success = delete_member(rlnInstance.value, deleted_member_index)
check deletion_success check:
deletion_success
# read the Merkle Tree root after the deletion # read the Merkle Tree root after the deletion
var var
root3 {.noinit.} : Buffer = Buffer() root3 {.noinit.}: Buffer = Buffer()
rootPtr3 = addr(root3) rootPtr3 = addr(root3)
get_root_successful3 = get_root(rlnInstance.value, rootPtr3) get_root_successful3 = get_root(rlnInstance.value, rootPtr3)
check: check:
get_root_successful3 get_root_successful3
root3.len == 32 root3.len == 32
var rootValue1 = cast[ptr array[32,byte]] (root1.`ptr`) var rootValue1 = cast[ptr array[32, byte]] (root1.`ptr`)
let rootHex1 = rootValue1[].toHex let rootHex1 = rootValue1[].toHex
debug "The initial root", rootHex1 debug "The initial root", rootHex1
var rootValue2 = cast[ptr array[32,byte]] (root2.`ptr`) var rootValue2 = cast[ptr array[32, byte]] (root2.`ptr`)
let rootHex2 = rootValue2[].toHex let rootHex2 = rootValue2[].toHex
debug "The root after insertion", rootHex2 debug "The root after insertion", rootHex2
var rootValue3 = cast[ptr array[32,byte]] (root3.`ptr`) var rootValue3 = cast[ptr array[32, byte]] (root3.`ptr`)
let rootHex3 = rootValue3[].toHex let rootHex3 = rootValue3[].toHex
debug "The root after deletion", rootHex3 debug "The root after deletion", rootHex3
# the root must change after the insertion # the root must change after the insertion
check: not(rootHex1 == rootHex2) check: not(rootHex1 == rootHex2)
## The initial root of the tree (empty tree) must be identical to ## The initial root of the tree (empty tree) must be identical to
## the root of the tree after one insertion followed by a deletion ## the root of the tree after one insertion followed by a deletion
check rootHex1 == rootHex3 check:
rootHex1 == rootHex3
test "Merkle tree consistency check between deletion and insertion using rln utils": test "Merkle tree consistency check between deletion and insertion using rln utils":
# create an RLN instance # create an RLN instance
var rlnInstance = createRLNInstance() var rlnInstance = createRLNInstance()
@ -544,29 +307,35 @@ suite "Waku rln relay":
# read the Merkle Tree root # read the Merkle Tree root
var root1 = rln.getMerkleRoot() var root1 = rln.getMerkleRoot()
check root1.isOk check:
root1.isOk
let rootHex1 = root1.value().toHex() let rootHex1 = root1.value().toHex()
# generate a key pair # generate a key pair
var keypair = rln.membershipKeyGen() var keypair = rln.membershipKeyGen()
check keypair.isSome() check:
let member_inserted = rln.insertMember(keypair.get().idCommitment) keypair.isSome()
check member_inserted let member_inserted = rln.insertMember(keypair.get().idCommitment)
check:
member_inserted
# read the Merkle Tree root after insertion # read the Merkle Tree root after insertion
var root2 = rln.getMerkleRoot() var root2 = rln.getMerkleRoot()
check root2.isOk check:
root2.isOk
let rootHex2 = root2.value().toHex() let rootHex2 = root2.value().toHex()
# delete the first member # delete the first member
var deleted_member_index = MembershipIndex(0) var deleted_member_index = MembershipIndex(0)
let deletion_success = rln.removeMember(deleted_member_index) let deletion_success = rln.removeMember(deleted_member_index)
check deletion_success check:
deletion_success
# read the Merkle Tree root after the deletion # read the Merkle Tree root after the deletion
var root3 = rln.getMerkleRoot() var root3 = rln.getMerkleRoot()
check root3.isOk check:
root3.isOk
let rootHex3 = root3.value().toHex() let rootHex3 = root3.value().toHex()
@ -575,35 +344,40 @@ suite "Waku rln relay":
debug "The root after deletion", rootHex3 debug "The root after deletion", rootHex3
# the root must change after the insertion # the root must change after the insertion
check not(rootHex1 == rootHex2) check:
not(rootHex1 == rootHex2)
## The initial root of the tree (empty tree) must be identical to ## The initial root of the tree (empty tree) must be identical to
## the root of the tree after one insertion followed by a deletion ## the root of the tree after one insertion followed by a deletion
check rootHex1 == rootHex3 check:
rootHex1 == rootHex3
test "hash Nim Wrappers": test "hash Nim Wrappers":
# create an RLN instance # create an RLN instance
var rlnInstance = createRLNInstance() var rlnInstance = createRLNInstance()
check: check:
rlnInstance.isOk == true rlnInstance.isOk == true
# prepare the input # prepare the input
var var
msg = "Hello".toBytes() msg = "Hello".toBytes()
hashInput = appendLength(msg) hashInput = appendLength(msg)
hashInputBuffer = toBuffer(hashInput) hashInputBuffer = toBuffer(hashInput)
# prepare other inputs to the hash function # prepare other inputs to the hash function
var outputBuffer: Buffer var outputBuffer: Buffer
let hashSuccess = hash(rlnInstance.value, addr hashInputBuffer, addr outputBuffer)
check hashSuccess
let outputArr = cast[ptr array[32,byte]](outputBuffer.`ptr`)[]
check:
"efb8ac39dc22eaf377fe85b405b99ba78dbc2f3f32494add4501741df946bd1d" == outputArr.toHex()
var let hashSuccess = hash(rlnInstance.value, addr hashInputBuffer,
hashOutput = cast[ptr array[32,byte]] (outputBuffer.`ptr`)[] addr outputBuffer)
check:
hashSuccess
let outputArr = cast[ptr array[32, byte]](outputBuffer.`ptr`)[]
check:
"efb8ac39dc22eaf377fe85b405b99ba78dbc2f3f32494add4501741df946bd1d" ==
outputArr.toHex()
var
hashOutput = cast[ptr array[32, byte]] (outputBuffer.`ptr`)[]
hashOutputHex = hashOutput.toHex() hashOutputHex = hashOutput.toHex()
debug "hash output", hashOutputHex debug "hash output", hashOutputHex
@ -614,26 +388,27 @@ suite "Waku rln relay":
check: check:
rlnInstance.isOk == true rlnInstance.isOk == true
let rln = rlnInstance.value let rln = rlnInstance.value
# prepare the input # prepare the input
let msg = "Hello".toBytes() let msg = "Hello".toBytes()
let hash = rln.hash(msg) let hash = rln.hash(msg)
check: check:
"efb8ac39dc22eaf377fe85b405b99ba78dbc2f3f32494add4501741df946bd1d" == hash.toHex() "efb8ac39dc22eaf377fe85b405b99ba78dbc2f3f32494add4501741df946bd1d" ==
hash.toHex()
test "create a list of membership keys and construct a Merkle tree based on the list": test "create a list of membership keys and construct a Merkle tree based on the list":
let let
groupSize = 100 groupSize = 100
(list, root) = createMembershipList(groupSize) (list, root) = createMembershipList(groupSize)
debug "created membership key list", list debug "created membership key list", list
debug "the Merkle tree root", root debug "the Merkle tree root", root
check: check:
list.len == groupSize # check the number of keys list.len == groupSize # check the number of keys
root.len == HASH_HEX_SIZE # check the size of the calculated tree root root.len == HASH_HEX_SIZE # check the size of the calculated tree root
test "check correctness of toMembershipKeyPairs and calcMerkleRoot": test "check correctness of toMembershipKeyPairs and calcMerkleRoot":
let groupKeys = STATIC_GROUP_KEYS let groupKeys = STATIC_GROUP_KEYS
@ -648,14 +423,14 @@ suite "Waku rln relay":
debug "groupIDCommitments", groupIDCommitments debug "groupIDCommitments", groupIDCommitments
debug "root", root debug "root", root
check: check:
# check that the correct number of key pairs is created # check that the correct number of key pairs is created
groupKeyPairs.len == StaticGroupSize groupKeyPairs.len == StaticGroupSize
# compare the calculated root against the correct root # compare the calculated root against the correct root
root == STATIC_GROUP_MERKLE_ROOT root == STATIC_GROUP_MERKLE_ROOT
test "RateLimitProof Protobuf encode/init test": test "RateLimitProof Protobuf encode/init test":
var var
proof: ZKSNARK proof: ZKSNARK
merkleRoot: MerkleNode merkleRoot: MerkleNode
epoch: Epoch epoch: Epoch
@ -663,14 +438,14 @@ suite "Waku rln relay":
shareY: MerkleNode shareY: MerkleNode
nullifier: Nullifier nullifier: Nullifier
# populate fields with dummy values # populate fields with dummy values
for x in proof.mitems : x = 1 for x in proof.mitems: x = 1
for x in merkleRoot.mitems : x = 2 for x in merkleRoot.mitems: x = 2
for x in epoch.mitems : x = 3 for x in epoch.mitems: x = 3
for x in shareX.mitems : x = 4 for x in shareX.mitems: x = 4
for x in shareY.mitems : x = 5 for x in shareY.mitems: x = 5
for x in nullifier.mitems : x = 6 for x in nullifier.mitems: x = 6
let let
rateLimitProof = RateLimitProof(proof: proof, rateLimitProof = RateLimitProof(proof: proof,
merkleRoot: merkleRoot, merkleRoot: merkleRoot,
epoch: epoch, epoch: epoch,
@ -686,16 +461,17 @@ suite "Waku rln relay":
test "test proofVerify and proofGen for a valid proof": test "test proofVerify and proofGen for a valid proof":
var rlnInstance = createRLNInstance() var rlnInstance = createRLNInstance()
check rlnInstance.isOk check:
rlnInstance.isOk
var rln = rlnInstance.value var rln = rlnInstance.value
let let
# create a membership key pair # create a membership key pair
memKeys = membershipKeyGen(rln).get() memKeys = membershipKeyGen(rln).get()
# peer's index in the Merkle Tree # peer's index in the Merkle Tree
index = 5 index = 5
# Create a Merkle tree with random members # Create a Merkle tree with random members
for i in 0..10: for i in 0..10:
var member_is_added: bool = false var member_is_added: bool = false
if (i == index): if (i == index):
@ -706,27 +482,30 @@ suite "Waku rln relay":
let memberKeys = rln.membershipKeyGen() let memberKeys = rln.membershipKeyGen()
member_is_added = rln.insertMember(memberKeys.get().idCommitment) member_is_added = rln.insertMember(memberKeys.get().idCommitment)
# check the member is added # check the member is added
check member_is_added check:
member_is_added
# prepare the message # prepare the message
let messageBytes = "Hello".toBytes() let messageBytes = "Hello".toBytes()
# prepare the epoch # prepare the epoch
var epoch : Epoch var epoch: Epoch
debug "epoch", epochHex=epoch.toHex() debug "epoch", epochHex = epoch.toHex()
# generate proof # generate proof
let proofRes = rln.proofGen(data = messageBytes, let proofRes = rln.proofGen(data = messageBytes,
memKeys = memKeys, memKeys = memKeys,
memIndex = MembershipIndex(index), memIndex = MembershipIndex(index),
epoch = epoch) epoch = epoch)
check proofRes.isOk() check:
proofRes.isOk()
let proof = proofRes.value let proof = proofRes.value
# verify the proof # verify the proof
let verified = rln.proofVerify(data = messageBytes, let verified = rln.proofVerify(data = messageBytes,
proof = proof) proof = proof)
check verified == true check:
verified == true
test "test proofVerify and proofGen for an invalid proof": test "test proofVerify and proofGen for an invalid proof":
var rlnInstance = createRLNInstance() var rlnInstance = createRLNInstance()
@ -734,13 +513,13 @@ suite "Waku rln relay":
rlnInstance.isOk == true rlnInstance.isOk == true
var rln = rlnInstance.value var rln = rlnInstance.value
let let
# create a membership key pair # create a membership key pair
memKeys = membershipKeyGen(rln).get() memKeys = membershipKeyGen(rln).get()
# peer's index in the Merkle Tree # peer's index in the Merkle Tree
index = 5 index = 5
# Create a Merkle tree with random members # Create a Merkle tree with random members
for i in 0..10: for i in 0..10:
var member_is_added: bool = false var member_is_added: bool = false
if (i == index): if (i == index):
@ -751,50 +530,56 @@ suite "Waku rln relay":
let memberKeys = rln.membershipKeyGen() let memberKeys = rln.membershipKeyGen()
member_is_added = rln.insertMember(memberKeys.get().idCommitment) member_is_added = rln.insertMember(memberKeys.get().idCommitment)
# check the member is added # check the member is added
check member_is_added check:
member_is_added
# prepare the message # prepare the message
let messageBytes = "Hello".toBytes() let messageBytes = "Hello".toBytes()
# prepare the epoch # prepare the epoch
var epoch : Epoch var epoch: Epoch
debug "epoch in bytes", epochHex=epoch.toHex() debug "epoch in bytes", epochHex = epoch.toHex()
let badIndex = 4 let badIndex = 4
# generate proof # generate proof
let proofRes = rln.proofGen(data = messageBytes, let proofRes = rln.proofGen(data = messageBytes,
memKeys = memKeys, memKeys = memKeys,
memIndex = MembershipIndex(badIndex), memIndex = MembershipIndex(badIndex),
epoch = epoch) epoch = epoch)
check proofRes.isOk() check:
proofRes.isOk()
let proof = proofRes.value let proof = proofRes.value
# verify the proof (should not be verified) # verify the proof (should not be verified)
let verified = rln.proofVerify(data = messageBytes, let verified = rln.proofVerify(data = messageBytes,
proof = proof) proof = proof)
check verified == false check:
verified == false
test "toEpoch and fromEpoch consistency check": test "toEpoch and fromEpoch consistency check":
# check edge cases # check edge cases
let let
time = uint64.high epoch = uint64.high # rln epoch
epoch = time.toEpoch() epochBytes = epoch.toEpoch()
decodedTime = epoch.fromEpoch() decodedEpoch = epochBytes.fromEpoch()
check time == decodedTime check:
debug "encoded and decode time", time=time, epoch=epoch, decodedTime=decodedTime epoch == decodedEpoch
debug "encoded and decode time", epoch = epoch, epochBytes = epochBytes,
decodedEpoch = decodedEpoch
test "Epoch comparison": test "Epoch comparison":
# check edge cases # check edge cases
let let
time1 = uint64.high time1 = uint64.high
time2 = uint64.high - 1 time2 = uint64.high - 1
epoch1 = time1.toEpoch() epoch1 = time1.toEpoch()
epoch2 = time2.toEpoch() epoch2 = time2.toEpoch()
check compare(epoch1, epoch2) == int64(1) check:
check compare(epoch2, epoch1) == int64(-1) diff(epoch1, epoch2) == int64(1)
diff(epoch2, epoch1) == int64(-1)
test "updateLog and hasDuplicate tests": test "updateLog and hasDuplicate tests":
let let
wakurlnrelay = WakuRLNRelay() wakurlnrelay = WakuRLNRelay()
epoch = getCurrentEpoch() epoch = getCurrentEpoch()
@ -816,10 +601,13 @@ suite "Waku rln relay":
for index, x in shareX3.mpairs: shareX3[index] = 3 for index, x in shareX3.mpairs: shareX3[index] = 3
let shareY3 = shareX3 let shareY3 = shareX3
let let
wm1 = WakuMessage(proof: RateLimitProof(epoch: epoch, nullifier: nullifier1, shareX: shareX1, shareY: shareY1)) wm1 = WakuMessage(proof: RateLimitProof(epoch: epoch,
wm2 = WakuMessage(proof: RateLimitProof(epoch: epoch, nullifier: nullifier2, shareX: shareX2, shareY: shareY2)) nullifier: nullifier1, shareX: shareX1, shareY: shareY1))
wm3 = WakuMessage(proof: RateLimitProof(epoch: epoch, nullifier: nullifier3, shareX: shareX3, shareY: shareY3)) 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 # 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 # no duplicate for wm1 should be found, since the log is empty
@ -843,7 +631,7 @@ suite "Waku rln relay":
# wm3 has the same nullifier as wm1 but different secret shares, it should be detected as duplicate # wm3 has the same nullifier as wm1 but different secret shares, it should be detected as duplicate
let result3 = wakurlnrelay.hasDuplicate(wm3) let result3 = wakurlnrelay.hasDuplicate(wm3)
check: check:
result3.isOk result3.isOk
# it is a duplicate # it is a duplicate
result3.value == true result3.value == true
@ -859,10 +647,10 @@ suite "Waku rln relay":
groupIDCommitments = groupKeyPairs.mapIt(it.idCommitment) groupIDCommitments = groupKeyPairs.mapIt(it.idCommitment)
debug "groupKeyPairs", groupKeyPairs debug "groupKeyPairs", groupKeyPairs
debug "groupIDCommitments", groupIDCommitments debug "groupIDCommitments", groupIDCommitments
# index indicates the position of a membership key pair in the static list of group keys i.e., groupKeyPairs # 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 # 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 # index also represents the index of the leaf in the Merkle tree that contains node's commitment key
let index = MembershipIndex(5) let index = MembershipIndex(5)
# create an RLN instance # create an RLN instance
@ -873,24 +661,25 @@ suite "Waku rln relay":
# add members # add members
discard rln.addAll(groupIDCommitments) discard rln.addAll(groupIDCommitments)
let let
wakuRlnRelay = WakuRLNRelay(membershipIndex: index, membershipKeyPair: groupKeyPairs[index], rlnInstance: rln) wakuRlnRelay = WakuRLNRelay(membershipIndex: index,
membershipKeyPair: groupKeyPairs[index], rlnInstance: rln)
# get the current epoch time # get the current epoch time
let time = epochTime() let time = epochTime()
# create some messages from the same peer and append rln proof to them, except wm4 # create some messages from the same peer and append rln proof to them, except wm4
var var
wm1 = WakuMessage(payload: "Valid message".toBytes()) wm1 = WakuMessage(payload: "Valid message".toBytes())
proofAdded1 = wakuRlnRelay.appendRLNProof(wm1, time) proofAdded1 = wakuRlnRelay.appendRLNProof(wm1, time)
# another message in the same epoch as wm1, it will break the messaging rate limit # another message in the same epoch as wm1, it will break the messaging rate limit
wm2 = WakuMessage(payload: "Spam".toBytes()) wm2 = WakuMessage(payload: "Spam".toBytes())
proofAdded2 = wakuRlnRelay.appendRLNProof(wm2, time) proofAdded2 = wakuRlnRelay.appendRLNProof(wm2, time)
# wm3 points to the next epoch # wm3 points to the next epoch
wm3 = WakuMessage(payload: "Valid message".toBytes()) wm3 = WakuMessage(payload: "Valid message".toBytes())
proofAdded3 = wakuRlnRelay.appendRLNProof(wm3, time+EPOCH_UNIT_SECONDS) proofAdded3 = wakuRlnRelay.appendRLNProof(wm3, time+EPOCH_UNIT_SECONDS)
wm4 = WakuMessage(payload: "Invalid message".toBytes()) wm4 = WakuMessage(payload: "Invalid message".toBytes())
# checks proofs are added # checks proofs are added
check: check:
proofAdded1 proofAdded1
@ -903,15 +692,15 @@ suite "Waku rln relay":
msgValidate1 = wakuRlnRelay.validateMessage(wm1, some(time)) msgValidate1 = wakuRlnRelay.validateMessage(wm1, some(time))
# wm2 is published within the same Epoch as wm1 and should be found as spam # wm2 is published within the same Epoch as wm1 and should be found as spam
msgValidate2 = wakuRlnRelay.validateMessage(wm2, some(time)) msgValidate2 = wakuRlnRelay.validateMessage(wm2, some(time))
# a valid message should be validated successfully # a valid message should be validated successfully
msgValidate3 = wakuRlnRelay.validateMessage(wm3, some(time)) msgValidate3 = wakuRlnRelay.validateMessage(wm3, some(time))
# wm4 has no rln proof and should not be validated # wm4 has no rln proof and should not be validated
msgValidate4 = wakuRlnRelay.validateMessage(wm4, some(time)) msgValidate4 = wakuRlnRelay.validateMessage(wm4, some(time))
check: check:
msgValidate1 == MessageValidationResult.Valid msgValidate1 == MessageValidationResult.Valid
msgValidate2 == MessageValidationResult.Spam msgValidate2 == MessageValidationResult.Spam
msgValidate3 == MessageValidationResult.Valid msgValidate3 == MessageValidationResult.Valid
msgValidate4 == MessageValidationResult.Invalid msgValidate4 == MessageValidationResult.Invalid

View File

@ -0,0 +1,271 @@
# contains rln-relay tests that require interaction with Ganache i.e., onchain tests
{.used.}
import
std/options, sequtils, times,
testutils/unittests, chronos, chronicles, stint, web3, json,
stew/byteutils, stew/shims/net as stewNet,
libp2p/crypto/crypto,
../../waku/v2/protocol/waku_rln_relay/[rln, waku_rln_relay_utils,
waku_rln_relay_types, rln_relay_contract],
../../waku/v2/node/wakunode2,
../test_helpers,
./test_utils
const RLNRELAY_PUBSUB_TOPIC = "waku/2/rlnrelay/proto"
const RLNRELAY_CONTENT_TOPIC = "waku/2/rlnrelay/proto"
# contract ABI
contract(MembershipContract):
proc register(pubkey: Uint256) # external payable
proc MemberRegistered(pubkey: Uint256, index: Uint256) {.event.}
# proc registerBatch(pubkeys: seq[Uint256]) # external payable
# proc withdraw(secret: Uint256, pubkeyIndex: Uint256, receiver: Address)
# proc withdrawBatch( secrets: seq[Uint256], pubkeyIndex: seq[Uint256], receiver: seq[Address])
# a util function used for testing purposes
# it deploys membership contract on Ganache (or any Eth client available on ETH_CLIENT address)
# must be edited if used for a different contract than membership contract
proc uploadRLNContract*(ethClientAddress: string): Future[Address] {.async.} =
let web3 = await newWeb3(ethClientAddress)
debug "web3 connected to", ethClientAddress
# fetch the list of registered accounts
let accounts = await web3.provider.eth_accounts()
web3.defaultAccount = accounts[1]
let add = web3.defaultAccount
debug "contract deployer account address ", add
var balance = await web3.provider.eth_getBalance(web3.defaultAccount, "latest")
debug "Initial account balance: ", balance
# deploy the poseidon hash contract and gets its address
let
hasherReceipt = await web3.deployContract(POSEIDON_HASHER_CODE)
hasherAddress = hasherReceipt.contractAddress.get
debug "hasher address: ", hasherAddress
# encode membership contract inputs to 32 bytes zero-padded
let
membershipFeeEncoded = encode(MEMBERSHIP_FEE).data
depthEncoded = encode(MERKLE_TREE_DEPTH.u256).data
hasherAddressEncoded = encode(hasherAddress).data
# this is the contract constructor input
contractInput = membershipFeeEncoded & depthEncoded & hasherAddressEncoded
debug "encoded membership fee: ", membershipFeeEncoded
debug "encoded depth: ", depthEncoded
debug "encoded hasher address: ", hasherAddressEncoded
debug "encoded contract input:", contractInput
# deploy membership contract with its constructor inputs
let receipt = await web3.deployContract(MEMBERSHIP_CONTRACT_CODE,
contractInput = contractInput)
var contractAddress = receipt.contractAddress.get
debug "Address of the deployed membership contract: ", contractAddress
balance = await web3.provider.eth_getBalance(web3.defaultAccount, "latest")
debug "Account balance after the contract deployment: ", balance
await web3.close()
debug "disconnected from ", ethClientAddress
return contractAddress
procSuite "Waku-rln-relay":
asyncTest "event subscription":
# preparation ------------------------------
debug "ethereum client address", ETH_CLIENT
let contractAddress = await uploadRLNContract(ETH_CLIENT)
# connect to the eth client
let web3 = await newWeb3(ETH_CLIENT)
debug "web3 connected to", ETH_CLIENT
# fetch the list of registered accounts
let accounts = await web3.provider.eth_accounts()
web3.defaultAccount = accounts[1]
debug "contract deployer account address ",
defaultAccount = web3.defaultAccount
# prepare a contract sender to interact with it
var contractObj = web3.contractSender(MembershipContract,
contractAddress) # creates a Sender object with a web3 field and contract address of type Address
# create an RLN instance
var rlnInstance = createRLNInstance()
check:
rlnInstance.isOk == true
# generate the membership keys
let membershipKeyPair = membershipKeyGen(rlnInstance.value)
check:
membershipKeyPair.isSome
let pk = membershipKeyPair.get().idCommitment.toUInt256()
debug "membership commitment key", pk = pk
# test ------------------------------
var fut = newFuture[void]()
let s = await contractObj.subscribe(MemberRegistered, %*{"fromBlock": "0x0",
"address": contractAddress}) do(
pubkey: Uint256, index: Uint256){.raises: [Defect], gcsafe.}:
try:
debug "onRegister", pubkey = pubkey, index = index
check:
pubkey == pk
fut.complete()
except Exception as err:
# chronos still raises exceptions which inherit directly from Exception
doAssert false, err.msg
do (err: CatchableError):
echo "Error from subscription: ", err.msg
# register a member
let tx = await contractObj.register(pk).send(value = MEMBERSHIP_FEE)
debug "a member is registered", tx = tx
# wait for the event to be received
await fut
# release resources -----------------------
await web3.close()
asyncTest "insert a key to the membership contract":
# preparation ------------------------------
debug "ethereum client address", ETH_CLIENT
let contractAddress = await uploadRLNContract(ETH_CLIENT)
# connect to the eth client
let web3 = await newWeb3(ETH_CLIENT)
debug "web3 connected to", ETH_CLIENT
# fetch the list of registered accounts
let accounts = await web3.provider.eth_accounts()
web3.defaultAccount = accounts[1]
let add = web3.defaultAccount
debug "contract deployer account address ", add
# prepare a contract sender to interact with it
var sender = web3.contractSender(MembershipContract,
contractAddress) # creates a Sender object with a web3 field and contract address of type Address
# send takes the following parameters, c: ContractCallBase, value = 0.u256, gas = 3000000'u64 gasPrice = 0
# should use send proc for the contract functions that update the state of the contract
let tx = await sender.register(20.u256).send(value = MEMBERSHIP_FEE) # value is the membership fee
debug "The hash of registration tx: ", tx
# var members: array[2, uint256] = [20.u256, 21.u256]
# debug "This is the batch registration result ", await sender.registerBatch(members).send(value = (members.len * MEMBERSHIP_FEE)) # value is the membership fee
let balance = await web3.provider.eth_getBalance(web3.defaultAccount, "latest")
debug "Balance after registration: ", balance
await web3.close()
debug "disconnected from", ETH_CLIENT
asyncTest "registration procedure":
# preparation ------------------------------
# deploy the contract
let contractAddress = await uploadRLNContract(ETH_CLIENT)
# prepare rln-relay peer inputs
let
web3 = await newWeb3(ETH_CLIENT)
accounts = await web3.provider.eth_accounts()
# choose one of the existing accounts for the rln-relay peer
ethAccountAddress = accounts[0]
await web3.close()
# create an RLN instance
var rlnInstance = createRLNInstance()
check:
rlnInstance.isOk == true
# generate the membership keys
let membershipKeyPair = membershipKeyGen(rlnInstance.value)
check:
membershipKeyPair.isSome
# test ------------------------------
# initialize the WakuRLNRelay
var rlnPeer = WakuRLNRelay(membershipKeyPair: membershipKeyPair.get(),
membershipIndex: MembershipIndex(0),
ethClientAddress: ETH_CLIENT,
ethAccountAddress: ethAccountAddress,
membershipContractAddress: contractAddress)
# register the rln-relay peer to the membership contract
let is_successful = await rlnPeer.register()
check:
is_successful
asyncTest "mounting waku rln-relay":
# preparation ------------------------------
let
nodeKey = crypto.PrivateKey.random(Secp256k1, rng[])[]
node = WakuNode.new(nodeKey, ValidIpAddress.init("0.0.0.0"),
Port(60000))
await node.start()
# deploy the contract
let membershipContractAddress = await uploadRLNContract(ETH_CLIENT)
# prepare rln-relay inputs
let
web3 = await newWeb3(ETH_CLIENT)
accounts = await web3.provider.eth_accounts()
# choose one of the existing account for the rln-relay peer
ethAccountAddress = accounts[9]
await web3.close()
# create current peer's pk
var rlnInstance = createRLNInstance()
check:
rlnInstance.isOk == true
var rln = rlnInstance.value
# generate a key pair
var keypair = rln.membershipKeyGen()
doAssert(keypair.isSome())
# current peer index in the Merkle tree
let index = uint(5)
# Create a group of 10 members
var group = newSeq[IDCommitment]()
for i in 0..10:
var member_is_added: bool = false
if (uint(i) == index):
# insert the current peer's pk
group.add(keypair.get().idCommitment)
member_is_added = rln.insertMember(keypair.get().idCommitment)
doAssert(member_is_added)
debug "member key", key = keypair.get().idCommitment.toHex
else:
var memberKeypair = rln.membershipKeyGen()
doAssert(memberKeypair.isSome())
group.add(memberKeypair.get().idCommitment)
member_is_added = rln.insertMember(memberKeypair.get().idCommitment)
doAssert(member_is_added)
debug "member key", key = memberKeypair.get().idCommitment.toHex
let expectedRoot = rln.getMerkleRoot().value().toHex
debug "expected root ", expectedRoot
# test ------------------------------
# start rln-relay
node.mountRelay(@[RLNRELAY_PUBSUB_TOPIC])
await node.mountRlnRelay(ethClientAddrOpt = some(EthClient),
ethAccAddrOpt = some(ethAccountAddress),
memContractAddOpt = some(membershipContractAddress),
groupOpt = some(group),
memKeyPairOpt = some(keypair.get()),
memIndexOpt = some(index),
pubsubTopic = RLNRELAY_PUBSUB_TOPIC,
contentTopic = RLNRELAY_CONTENT_TOPIC)
let calculatedRoot = node.wakuRlnRelay.rlnInstance.getMerkleRoot().value().toHex
debug "calculated root ", calculatedRoot
check:
expectedRoot == calculatedRoot
await node.stop()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,12 @@
{.push raises: [Defect].} {.push raises: [Defect].}
import import
std/sequtils, tables, times, std/sequtils, tables, times,
chronicles, options, chronos, stint, chronicles, options, chronos, stint,
web3, web3,
stew/results, stew/results,
stew/[byteutils, arrayops, endians2], stew/[byteutils, arrayops, endians2],
rln, rln,
waku_rln_relay_types, waku_rln_relay_types,
../waku_message ../waku_message
@ -16,7 +16,8 @@ logScope:
type RLNResult* = Result[RLN[Bn256], string] type RLNResult* = Result[RLN[Bn256], string]
type MerkleNodeResult* = Result[MerkleNode, string] type MerkleNodeResult* = Result[MerkleNode, string]
type RateLimitProofResult* = Result[RateLimitProof, string] type RateLimitProofResult* = Result[RateLimitProof, string]
type SpamHandler* = proc(wakuMessage: WakuMessage): void {.gcsafe, closure, raises: [Defect].} type SpamHandler* = proc(wakuMessage: WakuMessage): void {.gcsafe, closure,
raises: [Defect].}
# membership contract interface # membership contract interface
contract(MembershipContract): contract(MembershipContract):
@ -26,14 +27,14 @@ contract(MembershipContract):
proc createRLNInstance*(d: int = MERKLE_TREE_DEPTH): RLNResult proc createRLNInstance*(d: int = MERKLE_TREE_DEPTH): RLNResult
{.raises: [Defect, IOError].} = {.raises: [Defect, IOError].} =
## generates an instance of RLN ## generates an instance of RLN
## An RLN instance supports both zkSNARKs logics and Merkle tree data structure and operations ## An RLN instance supports both zkSNARKs logics and Merkle tree data structure and operations
## d indicates the depth of Merkle tree ## d indicates the depth of Merkle tree
var var
rlnInstance: RLN[Bn256] rlnInstance: RLN[Bn256]
merkleDepth: csize_t = uint(d) merkleDepth: csize_t = uint(d)
## parameters.key contains the prover and verifier keys ## parameters.key contains the prover and verifier keys
## to generate this file, clone this repo https://github.com/kilic/rln ## to generate this file, clone this repo https://github.com/kilic/rln
## and run the following command in the root directory of the cloned project ## and run the following command in the root directory of the cloned project
## cargo run --example export_test_keys ## cargo run --example export_test_keys
## the file is generated separately and copied here ## the file is generated separately and copied here
@ -43,76 +44,84 @@ proc createRLNInstance*(d: int = MERKLE_TREE_DEPTH): RLNResult
## and then proceed as explained above ## and then proceed as explained above
parameters = readFile("waku/v2/protocol/waku_rln_relay/parameters.key") parameters = readFile("waku/v2/protocol/waku_rln_relay/parameters.key")
pbytes = parameters.toBytes() pbytes = parameters.toBytes()
len : csize_t = uint(pbytes.len) len: csize_t = uint(pbytes.len)
parametersBuffer = Buffer(`ptr`: addr(pbytes[0]), len: len) parametersBuffer = Buffer(`ptr`: addr(pbytes[0]), len: len)
# check the parameters.key is not empty # check the parameters.key is not empty
if (pbytes.len == 0): if (pbytes.len == 0):
debug "error in parameters.key" debug "error in parameters.key"
return err("error in parameters.key") return err("error in parameters.key")
# create an instance of RLN # create an instance of RLN
let res = new_circuit_from_params(merkleDepth, addr parametersBuffer, addr rlnInstance) let res = new_circuit_from_params(merkleDepth, addr parametersBuffer,
addr rlnInstance)
# check whether the circuit parameters are generated successfully # check whether the circuit parameters are generated successfully
if (res == false): if (res == false):
debug "error in parameters generation" debug "error in parameters generation"
return err("error in parameters generation") return err("error in parameters generation")
return ok(rlnInstance) return ok(rlnInstance)
proc membershipKeyGen*(ctxPtr: RLN[Bn256]): Option[MembershipKeyPair] = proc membershipKeyGen*(ctxPtr: RLN[Bn256]): Option[MembershipKeyPair] =
## generates a MembershipKeyPair that can be used for the registration into the rln membership contract ## generates a MembershipKeyPair that can be used for the registration into the rln membership contract
# keysBufferPtr will hold the generated key pairs i.e., secret and public keys # keysBufferPtr will hold the generated key pairs i.e., secret and public keys
var var
keysBuffer : Buffer keysBuffer: Buffer
keysBufferPtr = addr(keysBuffer) keysBufferPtr = addr(keysBuffer)
done = key_gen(ctxPtr, keysBufferPtr) done = key_gen(ctxPtr, keysBufferPtr)
# check whether the keys are generated successfully # check whether the keys are generated successfully
if(done == false): if(done == false):
debug "error in key generation" debug "error in key generation"
return none(MembershipKeyPair) return none(MembershipKeyPair)
var generatedKeys = cast[ptr array[64, byte]](keysBufferPtr.`ptr`)[] var generatedKeys = cast[ptr array[64, byte]](keysBufferPtr.`ptr`)[]
# the public and secret keys together are 64 bytes # the public and secret keys together are 64 bytes
if (generatedKeys.len != 64): if (generatedKeys.len != 64):
debug "the generated keys are invalid" debug "the generated keys are invalid"
return none(MembershipKeyPair) return none(MembershipKeyPair)
# TODO define a separate proc to decode the generated keys to the secret and public components # TODO define a separate proc to decode the generated keys to the secret and public components
var var
secret: array[32, byte] secret: array[32, byte]
public: array[32, byte] public: array[32, byte]
for (i,x) in secret.mpairs: x = generatedKeys[i] for (i, x) in secret.mpairs: x = generatedKeys[i]
for (i,x) in public.mpairs: x = generatedKeys[i+32] for (i, x) in public.mpairs: x = generatedKeys[i+32]
var var
keypair = MembershipKeyPair(idKey: secret, idCommitment: public) keypair = MembershipKeyPair(idKey: secret, idCommitment: public)
return some(keypair) return some(keypair)
proc toUInt256*(idCommitment: IDCommitment): UInt256 =
let pk = cast[UInt256](idCommitment)
return pk
proc register*(rlnPeer: WakuRLNRelay): Future[bool] {.async.} = proc register*(rlnPeer: WakuRLNRelay): Future[bool] {.async.} =
## registers the public key of the rlnPeer which is rlnPeer.membershipKeyPair.publicKey ## registers the public key of the rlnPeer which is rlnPeer.membershipKeyPair.publicKey
## into the membership contract whose address is in rlnPeer.membershipContractAddress ## into the membership contract whose address is in rlnPeer.membershipContractAddress
let web3 = await newWeb3(rlnPeer.ethClientAddress) let web3 = await newWeb3(rlnPeer.ethClientAddress)
web3.defaultAccount = rlnPeer.ethAccountAddress web3.defaultAccount = rlnPeer.ethAccountAddress
# when the private key is set in a web3 instance, the send proc (sender.register(pk).send(MembershipFee)) # when the private key is set in a web3 instance, the send proc (sender.register(pk).send(MEMBERSHIP_FEE))
# does the signing using the provided key # does the signing using the provided key
web3.privateKey = rlnPeer.ethAccountPrivateKey web3.privateKey = rlnPeer.ethAccountPrivateKey
var sender = web3.contractSender(MembershipContract, rlnPeer.membershipContractAddress) # creates a Sender object with a web3 field and contract address of type Address var sender = web3.contractSender(MembershipContract,
let pk = cast[UInt256](rlnPeer.membershipKeyPair.idCommitment) rlnPeer.membershipContractAddress) # creates a Sender object with a web3 field and contract address of type Address
discard await sender.register(pk).send(MembershipFee) let pk = toUInt256(rlnPeer.membershipKeyPair.idCommitment)
discard await sender.register(pk).send(MEMBERSHIP_FEE)
debug "pk", pk = pk
# TODO check the receipt and then return true/false # TODO check the receipt and then return true/false
await web3.close() await web3.close()
return true return true
proc appendLength*(input: openArray[byte]): seq[byte] = proc appendLength*(input: openArray[byte]): seq[byte] =
## returns length prefixed version of the input ## returns length prefixed version of the input
## with the following format [len<8>|input<var>] ## with the following format [len<8>|input<var>]
## len: 8-byte value that represents the number of bytes in the `input` ## len: 8-byte value that represents the number of bytes in the `input`
## len is serialized in little-endian ## len is serialized in little-endian
## input: the supplied `input` ## input: the supplied `input`
let let
# the length should be serialized in little-endian # the length should be serialized in little-endian
len = toBytes(uint64(input.len), Endianness.littleEndian) len = toBytes(uint64(input.len), Endianness.littleEndian)
output = concat(@len, @input) output = concat(@len, @input)
@ -125,32 +134,35 @@ proc toBuffer*(x: openArray[byte]): Buffer =
let output = Buffer(`ptr`: addr(temp[0]), len: uint(temp.len)) let output = Buffer(`ptr`: addr(temp[0]), len: uint(temp.len))
return output return output
proc hash*(rlnInstance: RLN[Bn256], data: openArray[byte]): MerkleNode = proc hash*(rlnInstance: RLN[Bn256], data: openArray[byte]): MerkleNode =
## a thin layer on top of the Nim wrapper of the Poseidon hasher ## a thin layer on top of the Nim wrapper of the Poseidon hasher
debug "hash input", hashhex=data.toHex() debug "hash input", hashhex = data.toHex()
var lenPrefData = appendLength(data) var lenPrefData = appendLength(data)
var var
hashInputBuffer = lenPrefData.toBuffer() hashInputBuffer = lenPrefData.toBuffer()
outputBuffer: Buffer # will holds the hash output outputBuffer: Buffer # will holds the hash output
debug "hash input buffer length", bufflen=hashInputBuffer.len debug "hash input buffer length", bufflen = hashInputBuffer.len
let let
hashSuccess = hash(rlnInstance, addr hashInputBuffer, addr outputBuffer) hashSuccess = hash(rlnInstance, addr hashInputBuffer, addr outputBuffer)
output = cast[ptr MerkleNode](outputBuffer.`ptr`)[] output = cast[ptr MerkleNode](outputBuffer.`ptr`)[]
return output return output
proc serialize(idKey: IDKey, memIndex: MembershipIndex, epoch: Epoch, msg: openArray[byte]): seq[byte] = proc serialize(idKey: IDKey, memIndex: MembershipIndex, epoch: Epoch,
msg: openArray[byte]): seq[byte] =
## a private proc to convert RateLimitProof and the data to a byte seq ## a private proc to convert RateLimitProof and the data to a byte seq
## this conversion is used in the proofGen proc ## this conversion is used in the proofGen proc
## the serialization is done as instructed in https://github.com/kilic/rln/blob/7ac74183f8b69b399e3bc96c1ae8ab61c026dc43/src/public.rs#L146 ## the serialization is done as instructed in https://github.com/kilic/rln/blob/7ac74183f8b69b399e3bc96c1ae8ab61c026dc43/src/public.rs#L146
## [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal<var> ] ## [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal<var> ]
let memIndexBytes = toBytes(uint64(memIndex), Endianness.littleEndian) let memIndexBytes = toBytes(uint64(memIndex), Endianness.littleEndian)
let lenPrefMsg = appendLength(msg) let lenPrefMsg = appendLength(msg)
let output = concat(@idKey, @memIndexBytes, @epoch, lenPrefMsg) let output = concat(@idKey, @memIndexBytes, @epoch, lenPrefMsg)
return output return output
proc proofGen*(rlnInstance: RLN[Bn256], data: openArray[byte], memKeys: MembershipKeyPair, memIndex: MembershipIndex, epoch: Epoch): RateLimitProofResult = proc proofGen*(rlnInstance: RLN[Bn256], data: openArray[byte],
memKeys: MembershipKeyPair, memIndex: MembershipIndex,
epoch: Epoch): RateLimitProofResult =
# serialize inputs # serialize inputs
let serializedInputs = serialize(idKey = memKeys.idKey, let serializedInputs = serialize(idKey = memKeys.idKey,
@ -168,12 +180,12 @@ proc proofGen*(rlnInstance: RLN[Bn256], data: openArray[byte], memKeys: Membersh
if not proofIsSuccessful: if not proofIsSuccessful:
return err("could not generate the proof") return err("could not generate the proof")
var proofValue = cast[ptr array[416,byte]] (proof.`ptr`) var proofValue = cast[ptr array[416, byte]] (proof.`ptr`)
let proofBytes: array[416,byte] = proofValue[] let proofBytes: array[416, byte] = proofValue[]
debug "proof content", proofHex=proofValue[].toHex debug "proof content", proofHex = proofValue[].toHex
## parse the proof as |zkSNARKs<256>|root<32>|epoch<32>|share_x<32>|share_y<32>|nullifier<32>| ## parse the proof as |zkSNARKs<256>|root<32>|epoch<32>|share_x<32>|share_y<32>|nullifier<32>|
let let
proofOffset = 256 proofOffset = 256
rootOffset = proofOffset + 32 rootOffset = proofOffset + 32
epochOffset = rootOffset + 32 epochOffset = rootOffset + 32
@ -181,8 +193,8 @@ proc proofGen*(rlnInstance: RLN[Bn256], data: openArray[byte], memKeys: Membersh
shareYOffset = shareXOffset + 32 shareYOffset = shareXOffset + 32
nullifierOffset = shareYOffset + 32 nullifierOffset = shareYOffset + 32
var var
zkproof: ZKSNARK zkproof: ZKSNARK
proofRoot, shareX, shareY: MerkleNode proofRoot, shareX, shareY: MerkleNode
epoch: Epoch epoch: Epoch
nullifier: Nullifier nullifier: Nullifier
@ -209,7 +221,7 @@ proc serialize(proof: RateLimitProof, data: openArray[byte]): seq[byte] =
## the order of serialization is based on https://github.com/kilic/rln/blob/7ac74183f8b69b399e3bc96c1ae8ab61c026dc43/src/public.rs#L205 ## the order of serialization is based on https://github.com/kilic/rln/blob/7ac74183f8b69b399e3bc96c1ae8ab61c026dc43/src/public.rs#L205
## [ proof<256>| root<32>| epoch<32>| share_x<32>| share_y<32>| nullifier<32> | signal_len<8> | signal<var> ] ## [ proof<256>| root<32>| epoch<32>| share_x<32>| share_y<32>| nullifier<32> | signal_len<8> | signal<var> ]
let lenPrefMsg = appendLength(@data) let lenPrefMsg = appendLength(@data)
var proofBytes = concat(@(proof.proof), var proofBytes = concat(@(proof.proof),
@(proof.merkleRoot), @(proof.merkleRoot),
@(proof.epoch), @(proof.epoch),
@(proof.shareX), @(proof.shareX),
@ -219,23 +231,24 @@ proc serialize(proof: RateLimitProof, data: openArray[byte]): seq[byte] =
return proofBytes return proofBytes
proc proofVerify*(rlnInstance: RLN[Bn256], data: openArray[byte], proof: RateLimitProof): bool = proc proofVerify*(rlnInstance: RLN[Bn256], data: openArray[byte],
var proof: RateLimitProof): bool =
proofBytes= serialize(proof, data) var
proofBytes = serialize(proof, data)
proofBuffer = proofBytes.toBuffer() proofBuffer = proofBytes.toBuffer()
f = 0.uint32 f = 0.uint32
trace "serialized proof", proof=proofBytes.toHex() trace "serialized proof", proof = proofBytes.toHex()
let verifyIsSuccessful = verify(rlnInstance, addr proofBuffer, addr f) let verifyIsSuccessful = verify(rlnInstance, addr proofBuffer, addr f)
if not verifyIsSuccessful: if not verifyIsSuccessful:
# something went wrong in verification # something went wrong in verification
return false return false
# f = 0 means the proof is verified # f = 0 means the proof is verified
if f == 0: if f == 0:
return true return true
return false return false
proc insertMember*(rlnInstance: RLN[Bn256], idComm: IDCommitment): bool = proc insertMember*(rlnInstance: RLN[Bn256], idComm: IDCommitment): bool =
var pkBuffer = toBuffer(idComm) var pkBuffer = toBuffer(idComm)
let pkBufferPtr = addr pkBuffer let pkBufferPtr = addr pkBuffer
@ -243,14 +256,14 @@ proc insertMember*(rlnInstance: RLN[Bn256], idComm: IDCommitment): bool =
var member_is_added = update_next_member(rlnInstance, pkBufferPtr) var member_is_added = update_next_member(rlnInstance, pkBufferPtr)
return member_is_added return member_is_added
proc removeMember*(rlnInstance: RLN[Bn256], index: MembershipIndex): bool = proc removeMember*(rlnInstance: RLN[Bn256], index: MembershipIndex): bool =
let deletion_success = delete_member(rlnInstance, index) let deletion_success = delete_member(rlnInstance, index)
return deletion_success return deletion_success
proc getMerkleRoot*(rlnInstance: RLN[Bn256]): MerkleNodeResult = proc getMerkleRoot*(rlnInstance: RLN[Bn256]): MerkleNodeResult =
# read the Merkle Tree root after insertion # read the Merkle Tree root after insertion
var var
root {.noinit.} : Buffer = Buffer() root {.noinit.}: Buffer = Buffer()
rootPtr = addr(root) rootPtr = addr(root)
get_root_successful = get_root(rlnInstance, rootPtr) get_root_successful = get_root(rlnInstance, rootPtr)
if (not get_root_successful): return err("could not get the root") if (not get_root_successful): return err("could not get the root")
@ -259,54 +272,57 @@ proc getMerkleRoot*(rlnInstance: RLN[Bn256]): MerkleNodeResult =
var rootValue = cast[ptr MerkleNode] (root.`ptr`)[] var rootValue = cast[ptr MerkleNode] (root.`ptr`)[]
return ok(rootValue) return ok(rootValue)
proc toMembershipKeyPairs*(groupKeys: seq[(string, string)]): seq[MembershipKeyPair] {.raises: [Defect, ValueError]} = proc toMembershipKeyPairs*(groupKeys: seq[(string, string)]): seq[
MembershipKeyPair] {.raises: [Defect, ValueError].} =
## groupKeys is sequence of membership key tuples in the form of (identity key, identity commitment) all in the hexadecimal format ## groupKeys is sequence of membership key tuples in the form of (identity key, identity commitment) all in the hexadecimal format
## the toMembershipKeyPairs proc populates a sequence of MembershipKeyPairs using the supplied groupKeys ## the toMembershipKeyPairs proc populates a sequence of MembershipKeyPairs using the supplied groupKeys
var groupKeyPairs = newSeq[MembershipKeyPair]() var groupKeyPairs = newSeq[MembershipKeyPair]()
for i in 0..groupKeys.len-1: for i in 0..groupKeys.len-1:
let let
idKey = groupKeys[i][0].hexToByteArray(32) idKey = groupKeys[i][0].hexToByteArray(32)
idCommitment = groupKeys[i][1].hexToByteArray(32) idCommitment = groupKeys[i][1].hexToByteArray(32)
groupKeyPairs.add(MembershipKeyPair(idKey: idKey, idCommitment: idCommitment)) groupKeyPairs.add(MembershipKeyPair(idKey: idKey,
idCommitment: idCommitment))
return groupKeyPairs return groupKeyPairs
proc calcMerkleRoot*(list: seq[IDCommitment]): string {.raises: [Defect, IOError].} = proc calcMerkleRoot*(list: seq[IDCommitment]): string {.raises: [Defect, IOError].} =
## returns the root of the Merkle tree that is computed from the supplied list ## returns the root of the Merkle tree that is computed from the supplied list
## the root is in hexadecimal format ## the root is in hexadecimal format
var rlnInstance = createRLNInstance() var rlnInstance = createRLNInstance()
doAssert(rlnInstance.isOk) doAssert(rlnInstance.isOk)
var rln = rlnInstance.value var rln = rlnInstance.value
# create a Merkle tree # create a Merkle tree
for i in 0..list.len-1: for i in 0..list.len-1:
var member_is_added = false var member_is_added = false
member_is_added = rln.insertMember(list[i]) member_is_added = rln.insertMember(list[i])
doAssert(member_is_added) doAssert(member_is_added)
let root = rln.getMerkleRoot().value().toHex let root = rln.getMerkleRoot().value().toHex
return root return root
proc createMembershipList*(n: int): (seq[(string,string)], string) {.raises: [Defect, IOError].} = proc createMembershipList*(n: int): (seq[(string, string)], string) {.raises: [
Defect, IOError].} =
## createMembershipList produces a sequence of membership key pairs in the form of (identity key, id commitment keys) in the hexadecimal format ## createMembershipList produces a sequence of membership key pairs in the form of (identity key, id commitment keys) 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 ## 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) ## the output of this proc is used to initialize a static group keys (to test waku-rln-relay in the off-chain mode)
# initialize a Merkle tree # initialize a Merkle tree
var rlnInstance = createRLNInstance() var rlnInstance = createRLNInstance()
if not rlnInstance.isOk: if not rlnInstance.isOk:
return (@[], "") return (@[], "")
var rln = rlnInstance.value var rln = rlnInstance.value
var output = newSeq[(string,string)]() var output = newSeq[(string, string)]()
for i in 0..n-1: for i in 0..n-1:
# generate a key pair # generate a key pair
let keypair = rln.membershipKeyGen() let keypair = rln.membershipKeyGen()
doAssert(keypair.isSome()) doAssert(keypair.isSome())
let keyTuple = (keypair.get().idKey.toHex, keypair.get().idCommitment.toHex) let keyTuple = (keypair.get().idKey.toHex, keypair.get().idCommitment.toHex)
output.add(keyTuple) output.add(keyTuple)
@ -314,12 +330,14 @@ proc createMembershipList*(n: int): (seq[(string,string)], string) {.raises: [De
let inserted = rln.insertMember(keypair.get().idCommitment) let inserted = rln.insertMember(keypair.get().idCommitment)
if not inserted: if not inserted:
return (@[], "") return (@[], "")
let root = rln.getMerkleRoot().value.toHex let root = rln.getMerkleRoot().value.toHex
return (output, root) return (output, root)
proc rlnRelaySetUp*(rlnRelayMemIndex: MembershipIndex): (Option[seq[IDCommitment]],Option[MembershipKeyPair], Option[MembershipIndex]) {.raises:[Defect, ValueError].} = proc rlnRelaySetUp*(rlnRelayMemIndex: MembershipIndex): (Option[seq[
IDCommitment]], Option[MembershipKeyPair], Option[
MembershipIndex]) {.raises: [Defect, ValueError].} =
let let
# static group # static group
groupKeys = STATIC_GROUP_KEYS groupKeys = STATIC_GROUP_KEYS
@ -328,31 +346,33 @@ proc rlnRelaySetUp*(rlnRelayMemIndex: MembershipIndex): (Option[seq[IDCommitment
debug "rln-relay membership index", rlnRelayMemIndex debug "rln-relay membership index", rlnRelayMemIndex
# validate the user-supplied membership index # validate the user-supplied membership index
if rlnRelayMemIndex < MembershipIndex(0) or rlnRelayMemIndex >= MembershipIndex(groupSize): if rlnRelayMemIndex < MembershipIndex(0) or rlnRelayMemIndex >=
MembershipIndex(groupSize):
error "wrong membership index" error "wrong membership index"
return(none(seq[IDCommitment]), none(MembershipKeyPair), none(MembershipIndex)) return(none(seq[IDCommitment]), none(MembershipKeyPair), none(MembershipIndex))
# prepare the outputs from the static group keys # prepare the outputs from the static group keys
let let
# create a sequence of MembershipKeyPairs from the group keys (group keys are in string format) # create a sequence of MembershipKeyPairs from the group keys (group keys are in string format)
groupKeyPairs = groupKeys.toMembershipKeyPairs() groupKeyPairs = groupKeys.toMembershipKeyPairs()
# extract id commitment keys # extract id commitment keys
groupIDCommitments = groupKeyPairs.mapIt(it.idCommitment) groupIDCommitments = groupKeyPairs.mapIt(it.idCommitment)
groupOpt= some(groupIDCommitments) groupOpt = some(groupIDCommitments)
# user selected membership key pair # user selected membership key pair
memKeyPairOpt = some(groupKeyPairs[rlnRelayMemIndex]) memKeyPairOpt = some(groupKeyPairs[rlnRelayMemIndex])
memIndexOpt= some(rlnRelayMemIndex) memIndexOpt = some(rlnRelayMemIndex)
return (groupOpt, memKeyPairOpt, memIndexOpt) return (groupOpt, memKeyPairOpt, memIndexOpt)
proc hasDuplicate*(rlnPeer: WakuRLNRelay, msg: WakuMessage): Result[bool, string] = 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 ## 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 ## epoch and nullifier as `msg`'s epoch and nullifier but different Shamir secret shares
## otherwise, returns false ## otherwise, returns false
## emits an error string if `KeyError` occurs (never happens, it is just to avoid raising unnecessary `KeyError` exception ) ## 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` # extract the proof metadata of the supplied `msg`
let proofMD = ProofMetadata(nullifier: msg.proof.nullifier, shareX: msg.proof.shareX, shareY: msg.proof.shareY) let proofMD = ProofMetadata(nullifier: msg.proof.nullifier,
shareX: msg.proof.shareX, shareY: msg.proof.shareY)
# check if the epoch exists # check if the epoch exists
if not rlnPeer.nullifierLog.hasKey(msg.proof.epoch): if not rlnPeer.nullifierLog.hasKey(msg.proof.epoch):
@ -363,12 +383,14 @@ proc hasDuplicate*(rlnPeer: WakuRLNRelay, msg: WakuMessage): Result[bool, string
return ok(false) return ok(false)
# check for a message with the same nullifier but different secret shares # 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))) 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: if matched.len != 0:
# there is a duplicate # there is a duplicate
return ok(true) return ok(true)
# there is no duplicate # there is no duplicate
return ok(false) return ok(false)
@ -376,17 +398,18 @@ proc hasDuplicate*(rlnPeer: WakuRLNRelay, msg: WakuMessage): Result[bool, string
return err("the epoch was not found") return err("the epoch was not found")
proc updateLog*(rlnPeer: WakuRLNRelay, msg: WakuMessage): Result[bool, string] = proc updateLog*(rlnPeer: WakuRLNRelay, msg: WakuMessage): Result[bool, string] =
## extracts the `ProofMetadata` of the supplied messages `msg` and ## extracts the `ProofMetadata` of the supplied messages `msg` and
## saves it in the `nullifierLog` of the `rlnPeer` ## saves it in the `nullifierLog` of the `rlnPeer`
let proofMD = ProofMetadata(nullifier: msg.proof.nullifier, shareX: msg.proof.shareX, shareY: msg.proof.shareY) let proofMD = ProofMetadata(nullifier: msg.proof.nullifier,
debug "proof metadata", proofMD=proofMD shareX: msg.proof.shareX, shareY: msg.proof.shareY)
debug "proof metadata", proofMD = proofMD
# check if the epoch exists # check if the epoch exists
if not rlnPeer.nullifierLog.hasKey(msg.proof.epoch): if not rlnPeer.nullifierLog.hasKey(msg.proof.epoch):
rlnPeer.nullifierLog[msg.proof.epoch]= @[proofMD] rlnPeer.nullifierLog[msg.proof.epoch] = @[proofMD]
return ok(true) return ok(true)
try: try:
# check if an identical record exists # check if an identical record exists
if rlnPeer.nullifierLog[msg.proof.epoch].contains(proofMD): if rlnPeer.nullifierLog[msg.proof.epoch].contains(proofMD):
@ -400,17 +423,17 @@ proc updateLog*(rlnPeer: WakuRLNRelay, msg: WakuMessage): Result[bool, string] =
proc toEpoch*(t: uint64): Epoch = proc toEpoch*(t: uint64): Epoch =
## converts `t` to `Epoch` in little-endian order ## converts `t` to `Epoch` in little-endian order
let bytes = toBytes(t, Endianness.littleEndian) let bytes = toBytes(t, Endianness.littleEndian)
debug "bytes", bytes=bytes debug "bytes", bytes = bytes
var epoch: Epoch var epoch: Epoch
discard epoch.copyFrom(bytes) discard epoch.copyFrom(bytes)
return epoch return epoch
proc fromEpoch*(epoch: Epoch): uint64 = proc fromEpoch*(epoch: Epoch): uint64 =
## decodes bytes of `epoch` (in little-endian) to uint64 ## decodes bytes of `epoch` (in little-endian) to uint64
let t = fromBytesLE(uint64, array[32,byte](epoch)) let t = fromBytesLE(uint64, array[32, byte](epoch))
return t return t
proc calcEpoch*(t: float64): Epoch = proc calcEpoch*(t: float64): Epoch =
## gets time `t` as `flaot64` with subseconds resolution in the fractional part ## gets time `t` as `flaot64` with subseconds resolution in the fractional part
## and returns its corresponding rln `Epoch` value ## and returns its corresponding rln `Epoch` value
let e = uint64(t/EPOCH_UNIT_SECONDS) let e = uint64(t/EPOCH_UNIT_SECONDS)
@ -420,25 +443,26 @@ proc getCurrentEpoch*(): Epoch =
## gets the current rln Epoch time ## gets the current rln Epoch time
return calcEpoch(epochTime()) return calcEpoch(epochTime())
proc compare*(e1, e2: Epoch): int64 = proc diff*(e1, e2: Epoch): int64 =
## returns the difference between the two rln `Epoch`s `e1` and `e2` ## returns the difference between the two rln `Epoch`s `e1` and `e2`
## i.e., e1 - e2 ## i.e., e1 - e2
# convert epochs to their corresponding unsigned numerical values # convert epochs to their corresponding unsigned numerical values
let let
epoch1 = fromEpoch(e1) epoch1 = fromEpoch(e1)
epoch2 = fromEpoch(e2) epoch2 = fromEpoch(e2)
return int64(epoch1) - int64(epoch2) return int64(epoch1) - int64(epoch2)
proc validateMessage*(rlnPeer: WakuRLNRelay, msg: WakuMessage, timeOption: Option[float64] = none(float64)): MessageValidationResult = proc validateMessage*(rlnPeer: WakuRLNRelay, msg: WakuMessage,
timeOption: Option[float64] = none(float64)): MessageValidationResult =
## validate the supplied `msg` based on the waku-rln-relay routing protocol i.e., ## 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`'s epoch is within MAX_EPOCH_GAP of the current epoch
## the `msg` has valid rate limit proof ## the `msg` has valid rate limit proof
## the `msg` does not violate the rate limit ## the `msg` does not violate the rate limit
## `timeOption` indicates Unix epoch time (fractional part holds sub-seconds) ## `timeOption` indicates Unix epoch time (fractional part holds sub-seconds)
## if `timeOption` is supplied, then the current epoch is calculated based on that ## if `timeOption` is supplied, then the current epoch is calculated based on that
# checks if the `msg`'s epoch is far from the current epoch # checks if the `msg`'s epoch is far from the current epoch
# it corresponds to the validation of rln external nullifier # it corresponds to the validation of rln external nullifier
var epoch: Epoch var epoch: Epoch
@ -448,77 +472,79 @@ proc validateMessage*(rlnPeer: WakuRLNRelay, msg: WakuMessage, timeOption: Optio
# get current rln epoch # get current rln epoch
epoch = getCurrentEpoch() epoch = getCurrentEpoch()
debug "current epoch", currentEpoch=fromEpoch(epoch) debug "current epoch", currentEpoch = fromEpoch(epoch)
let let
msgEpoch = msg.proof.epoch msgEpoch = msg.proof.epoch
# calculate the gaps # calculate the gaps
gap = compare(epoch, msgEpoch) gap = diff(epoch, msgEpoch)
debug "message epoch", msgEpoch=fromEpoch(msgEpoch) debug "message epoch", msgEpoch = fromEpoch(msgEpoch)
# validate the epoch # validate the epoch
if abs(gap) >= MAX_EPOCH_GAP: if abs(gap) >= MAX_EPOCH_GAP:
# message's epoch is too old or too ahead # message's epoch is too old or too ahead
# accept messages whose epoch is within +-MAX_EPOCH_GAP from the current epoch # accept messages whose epoch is within +-MAX_EPOCH_GAP from the current epoch
debug "invalid message: epoch gap exceeds a threshold",gap=gap, payload=string.fromBytes(msg.payload) debug "invalid message: epoch gap exceeds a threshold", gap = gap,
payload = string.fromBytes(msg.payload)
return MessageValidationResult.Invalid return MessageValidationResult.Invalid
# verify the proof # verify the proof
let let
contentTopicBytes = msg.contentTopic.toBytes contentTopicBytes = msg.contentTopic.toBytes
input = concat(msg.payload, contentTopicBytes) input = concat(msg.payload, contentTopicBytes)
if not rlnPeer.rlnInstance.proofVerify(input, msg.proof): if not rlnPeer.rlnInstance.proofVerify(input, msg.proof):
# invalid proof # invalid proof
debug "invalid message: invalid proof", payload=string.fromBytes(msg.payload) debug "invalid message: invalid proof", payload = string.fromBytes(msg.payload)
return MessageValidationResult.Invalid return MessageValidationResult.Invalid
# check if double messaging has happened # check if double messaging has happened
let hasDup = rlnPeer.hasDuplicate(msg) let hasDup = rlnPeer.hasDuplicate(msg)
if hasDup.isOk and hasDup.value == true: if hasDup.isOk and hasDup.value == true:
debug "invalid message: message is a spam", payload=string.fromBytes(msg.payload) debug "invalid message: message is a spam", payload = string.fromBytes(msg.payload)
return MessageValidationResult.Spam return MessageValidationResult.Spam
# insert the message to the log # insert the message to the log
# the result of `updateLog` is discarded because message insertion is guaranteed by the implementation i.e., # the result of `updateLog` is discarded because message insertion is guaranteed by the implementation i.e.,
# it will never error out # it will never error out
discard rlnPeer.updateLog(msg) discard rlnPeer.updateLog(msg)
debug "message is valid", payload=string.fromBytes(msg.payload) debug "message is valid", payload = string.fromBytes(msg.payload)
return MessageValidationResult.Valid return MessageValidationResult.Valid
proc toRLNSignal*(wakumessage: WakuMessage): seq[byte] = 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 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 ## it extracts the `contentTopic` and the `payload` of the supplied `wakumessage` and serializes them into a byte sequence
let let
contentTopicBytes = wakumessage.contentTopic.toBytes contentTopicBytes = wakumessage.contentTopic.toBytes
output = concat(wakumessage.payload, contentTopicBytes) output = concat(wakumessage.payload, contentTopicBytes)
return output return output
proc appendRLNProof*(rlnPeer: WakuRLNRelay, msg: var WakuMessage, senderEpochTime: float64): bool =
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 true if it can create and append a `RateLimitProof` to the supplied `msg`
## returns false otherwise ## returns false otherwise
## `senderEpochTime` indicates the number of seconds passed since Unix epoch. The fractional part holds sub-seconds. ## `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()`) ## The `epoch` field of `RateLimitProof` is derived from the provided `senderEpochTime` (using `calcEpoch()`)
let input = msg.toRLNSignal() let input = msg.toRLNSignal()
var proof: RateLimitProofResult = proofGen(rlnInstance = rlnPeer.rlnInstance, data = input, var proof: RateLimitProofResult = proofGen(rlnInstance = rlnPeer.rlnInstance, data = input,
memKeys = rlnPeer.membershipKeyPair, memKeys = rlnPeer.membershipKeyPair,
memIndex = rlnPeer.membershipIndex, memIndex = rlnPeer.membershipIndex,
epoch = calcEpoch(senderEpochTime)) epoch = calcEpoch(senderEpochTime))
if proof.isErr: if proof.isErr:
return false return false
msg.proof = proof.value msg.proof = proof.value
return true return true
proc addAll*(rlnInstance: RLN[Bn256], list: seq[IDCommitment]): bool = proc addAll*(rlnInstance: RLN[Bn256], list: seq[IDCommitment]): bool =
# add members to the Merkle tree of the `rlnInstance` # add members to the Merkle tree of the `rlnInstance`
for i in 0..list.len-1: for i in 0..list.len-1:
let member = list[i] let member = list[i]
let member_is_added = rlnInstance.insertMember(member) let member_is_added = rlnInstance.insertMember(member)
if not member_is_added: if not member_is_added:
return false return false
return true return true