Off-chain group construction and management (#718)

* WIP

* WIP: fixes a bug

* adds test for static group formation

* adds static group creation when rln-relay is enabled

* adds createStatic group

* wip: adds group formation to mount rlnrelay

* adds createMembershipList utility function

* adds doc strings and todos

* cleans up the code and add comments

* defaults createRLNInstance depth argument to 32

* renames Depth

* distinguishes between onchain and offchain modes

* updates index boundaries

* updates log levels

* updates docstring

* updates log level of displayed membership keys

* relocates a todo

* activates all the tests

* fixes some comments and todos

* extracts some utils procs for better debugging

* adds todo

* moves calculateMerkleRoot and toMembersipKeyPairs to the rln utils

* makes calls to the utils functions

* adds unit test for createMembershipList

* adds unittest for toMembershipKeyPairs and calcMerkleRoot

* cleans up the code and fixes tree root value

* reverts an unwanted change

* minor

* adds comments and cleans up the code

* updates config message

* adds more comments

* fixes a minor value mismatch

* edits the size of group

* minor rewording

* defines a const var for the group keys

* replaces the sequence literal with the StaticGroupKeys const

* converts var to let when applicable

* replaces hardcoded value with well-defined constants

* moves createMembershipList to the rln relay utils module

* renames HashSize to HashHexSize

* minor updates on the comments

* reorganizes the consts

* indicates that rlnRelayMemIndex is an experimental option

* fixes a type conversion bug

* revises the unittest of "mount waku rln-relay off-chain"

* clarifies the use of index

* updates a docstring

* removes redundant constants and capitalize all of them

* deletes the ETH_CLIENT const from the test file

* renames a few vars for the sake of clarity

* reorganizes unittest into blocks of execution, debug messages, and checks

* adds more comments

* more comments and clarifications

* cleans up the tests

* minor

* adds a minor fix

* replaces a var usage with let

* fixes a bug
This commit is contained in:
Sanaz Taheri Boshrooyeh 2021-09-17 10:31:25 -07:00 committed by GitHub
parent a335a40a21
commit 4895be61ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 271 additions and 76 deletions

View File

@ -2,7 +2,7 @@
{.used.}
import
std/options,
std/options, sequtils,
testutils/unittests, chronos, chronicles, stint, web3,
stew/byteutils, stew/shims/net as stewNet,
libp2p/crypto/crypto,
@ -11,19 +11,14 @@ import
../test_helpers,
./test_utils
# the address of Ethereum client (ganache-cli for now)
# TODO this address in hardcoded in the code, we may need to take it as input from the user
const EthClient = "ws://localhost:8540/"
# poseidonHasherCode holds the bytecode of Poseidon hasher solidity smart contract:
# 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 poseidonHasherCode = readFile("tests/v2/poseidonHasher.txt")
# membershipContractCode contains the bytecode of the membership solidity smart contract:
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 membershipContractCode = readFile("tests/v2/membershipContract.txt")
const MEMBERSHIP_CONTRACT_CODE = readFile("tests/v2/membershipContract.txt")
# the membership contract code in solidity
# uint256 public immutable MEMBERSHIP_DEPOSIT;
@ -105,7 +100,7 @@ proc uploadContract(ethClientAddress: string): Future[Address] {.async.} =
# deploy the poseidon hash first
let
hasherReceipt = await web3.deployContract(poseidonHasherCode)
hasherReceipt = await web3.deployContract(POSEIDON_HASHER_CODE)
hasherAddress = hasherReceipt.contractAddress.get
debug "hasher address: ", hasherAddress
@ -113,7 +108,7 @@ proc uploadContract(ethClientAddress: string): Future[Address] {.async.} =
# encode membership contract inputs to 32 bytes zero-padded
let
membershipFeeEncoded = encode(MembershipFee).data
depthEncoded = encode(Depth).data
depthEncoded = encode(MERKLE_TREE_DEPTH.u256).data
hasherAddressEncoded = encode(hasherAddress).data
# this is the contract constructor input
contractInput = membershipFeeEncoded & depthEncoded & hasherAddressEncoded
@ -125,7 +120,7 @@ proc uploadContract(ethClientAddress: string): Future[Address] {.async.} =
debug "encoded contract input:" , contractInput
# deploy membership contract with its constructor inputs
let receipt = await web3.deployContract(membershipContractCode, contractInput = contractInput)
let receipt = await web3.deployContract(MEMBERSHIP_CONTRACT_CODE, contractInput = contractInput)
var contractAddress = receipt.contractAddress.get
debug "Address of the deployed membership contract: ", contractAddress
@ -180,15 +175,13 @@ procSuite "Waku rln relay":
await web3.close()
# create an RLN instance
var rlnInstance = createRLNInstance(32)
check:
rlnInstance.isOk == true
var rlnInstance = createRLNInstance()
check: rlnInstance.isOk == true
# generate the membership keys
let membershipKeyPair = membershipKeyGen(rlnInstance.value)
check:
membershipKeyPair.isSome
check: membershipKeyPair.isSome
# initialize the WakuRLNRelay
var rlnPeer = WakuRLNRelay(membershipKeyPair: membershipKeyPair.get(),
@ -201,7 +194,7 @@ procSuite "Waku rln relay":
let is_successful = await rlnPeer.register()
check:
is_successful
asyncTest "mounting waku rln relay":
asyncTest "mounting waku rln-relay":
let
nodeKey = crypto.PrivateKey.random(Secp256k1, rng[])[]
node = WakuNode.new(nodeKey, ValidIpAddress.init("0.0.0.0"),
@ -220,7 +213,7 @@ procSuite "Waku rln relay":
await web3.close()
# create current peer's pk
var rlnInstance = createRLNInstance(32)
var rlnInstance = createRLNInstance()
check rlnInstance.isOk == true
var rln = rlnInstance.value
# generate a key pair
@ -259,6 +252,43 @@ procSuite "Waku rln relay":
await node.stop()
asyncTest "mount waku-rln-relay in the off-chain mode":
let
nodeKey = crypto.PrivateKey.random(Secp256k1, rng[])[]
node = WakuNode.new(nodeKey, ValidIpAddress.init("0.0.0.0"),
Port(60000))
await node.start()
# preparing inputs to mount rln-relay
# 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 = MembeshipIndex(5)
# -------- mount rln-relay in the off-chain mode
await node.mountRlnRelay(groupOpt = some(groupIDCommitments), memKeyPairOpt = some(groupKeyPairs[index]), memIndexOpt = some(index), onchainMode = false)
# get the root of Merkle tree which is constructed inside the mountRlnRelay proc
let calculatedRoot = node.wakuRlnRelay.rlnInstance.getMerkleRoot().value().toHex
debug "calculated root by mountRlnRelay", calculatedRoot
# 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
# against the root which is the expected root
check calculatedRoot == root
await node.stop()
suite "Waku rln relay":
test "key_gen Nim Wrappers":
@ -302,7 +332,7 @@ suite "Waku rln relay":
test "membership Key Gen":
# create an RLN instance
var rlnInstance = createRLNInstance(32)
var rlnInstance = createRLNInstance()
check:
rlnInstance.isOk == true
@ -319,7 +349,7 @@ suite "Waku rln relay":
test "get_root Nim binding":
# create an RLN instance which also includes an empty Merkle tree
var rlnInstance = createRLNInstance(32)
var rlnInstance = createRLNInstance()
check:
rlnInstance.isOk == true
@ -349,7 +379,7 @@ suite "Waku rln relay":
doAssert(rootHex1 == rootHex2)
test "getMerkleRoot utils":
# create an RLN instance which also includes an empty Merkle tree
var rlnInstance = createRLNInstance(32)
var rlnInstance = createRLNInstance()
check:
rlnInstance.isOk == true
@ -368,7 +398,7 @@ suite "Waku rln relay":
test "update_next_member Nim Wrapper":
# create an RLN instance which also includes an empty Merkle tree
var rlnInstance = createRLNInstance(32)
var rlnInstance = createRLNInstance()
check:
rlnInstance.isOk == true
@ -385,18 +415,18 @@ suite "Waku rln relay":
test "delete_member Nim wrapper":
# create an RLN instance which also includes an empty Merkle tree
var rlnInstance = createRLNInstance(32)
var rlnInstance = createRLNInstance()
check:
rlnInstance.isOk == true
# delete the first member
var deleted_member_index = uint(0)
var deleted_member_index = MembeshipIndex(0)
let deletion_success = delete_member(rlnInstance.value, deleted_member_index)
doAssert(deletion_success)
test "insertMember rln utils":
# create an RLN instance which also includes an empty Merkle tree
var rlnInstance = createRLNInstance(32)
var rlnInstance = createRLNInstance()
check:
rlnInstance.isOk == true
var rln = rlnInstance.value
@ -408,16 +438,16 @@ suite "Waku rln relay":
test "removeMember rln utils":
# create an RLN instance which also includes an empty Merkle tree
var rlnInstance = createRLNInstance(32)
var rlnInstance = createRLNInstance()
check:
rlnInstance.isOk == true
var rln = rlnInstance.value
check:
rln.removeMember(uint(0))
rln.removeMember(MembeshipIndex(0))
test "Merkle tree consistency check between deletion and insertion":
# create an RLN instance
var rlnInstance = createRLNInstance(32)
var rlnInstance = createRLNInstance()
check:
rlnInstance.isOk == true
@ -448,7 +478,7 @@ suite "Waku rln relay":
doAssert(root2.len == 32)
# delete the first member
var deleted_member_index = uint(0)
var deleted_member_index = MembeshipIndex(0)
let deletion_success = delete_member(rlnInstance.value, deleted_member_index)
doAssert(deletion_success)
@ -480,7 +510,7 @@ suite "Waku rln relay":
doAssert(rootHex1 == rootHex3)
test "Merkle tree consistency check between deletion and insertion using rln utils":
# create an RLN instance
var rlnInstance = createRLNInstance(32)
var rlnInstance = createRLNInstance()
check:
rlnInstance.isOk == true
var rln = rlnInstance.value()
@ -503,7 +533,7 @@ suite "Waku rln relay":
# delete the first member
var deleted_member_index = uint(0)
var deleted_member_index = MembeshipIndex(0)
let deletion_success = rln.removeMember(deleted_member_index)
doAssert(deletion_success)
@ -526,7 +556,7 @@ suite "Waku rln relay":
test "hash Nim Wrappers":
# create an RLN instance
var rlnInstance = createRLNInstance(32)
var rlnInstance = createRLNInstance()
check:
rlnInstance.isOk == true
@ -560,7 +590,7 @@ suite "Waku rln relay":
# create an RLN instance
# check if the rln instance is created successfully
var rlnInstance = createRLNInstance(32)
var rlnInstance = createRLNInstance()
check:
rlnInstance.isOk == true
@ -573,7 +603,7 @@ suite "Waku rln relay":
var index = 5
# prepare the authentication object with peer's index and sk
var authObj: Auth = Auth(secret_buffer: addr skBuffer, index: uint(index))
var authObj: Auth = Auth(secret_buffer: addr skBuffer, index: MembeshipIndex(index))
# Create a Merkle tree with random members
for i in 0..10:
@ -655,7 +685,7 @@ suite "Waku rln relay":
# create and test a bad proof
# prepare a bad authentication object with a wrong peer's index
var badIndex = 8
var badAuthObj: Auth = Auth(secret_buffer: addr skBuffer, index: uint(badIndex))
var badAuthObj: Auth = Auth(secret_buffer: addr skBuffer, index: MembeshipIndex(badIndex))
var badProof: Buffer
let badProofIsSuccessful = generate_proof(rlnInstance.value, addr inputBuffer, addr badAuthObj, addr badProof)
# check whether the generate_proof call is done successfully
@ -666,4 +696,36 @@ suite "Waku rln relay":
doAssert(badVerifyIsSuccessful)
# badF=1 means the proof is not verified
# verification of the bad proof should fail
doAssert(badF == 1)
doAssert(badF == 1)
test "create a list of membership keys and construct a Merkle tree based on the list":
let
groupSize = 100
(list, root) = createMembershipList(groupSize)
debug "created membership key list", list
debug "the Merkle tree root", root
check:
list.len == groupSize # check the number of keys
root.len == HASH_HEX_SIZE # check the size of the calculated tree root
test "check correctness of toMembershipKeyPairs and calcMerkleRoot":
let groupKeys = STATIC_GROUP_KEYS
# create a set of MembershipKeyPair objects from groupKeys
let groupKeyPairs = groupKeys.toMembershipKeyPairs()
# extract the id commitments
let groupIDCommitments = groupKeyPairs.mapIt(it.idCommitment)
# calculate the Merkle tree root out of the extracted id commitments
let root = calcMerkleRoot(groupIDCommitments)
debug "groupKeyPairs", groupKeyPairs
debug "groupIDCommitments", groupIDCommitments
debug "root", root
check:
# check that the correct number of key pairs is created
groupKeyPairs.len == StaticGroupSize
# compare the calculated root against the correct root
root == STATIC_GROUP_MERKLE_ROOT

View File

@ -75,6 +75,11 @@ type
defaultValue: false
name: "rln-relay" }: bool
rlnRelayMemIndex* {.
desc: "(experimental) the index of node in the rln-relay group: a value between 0-49 inclusive",
defaultValue: 0
name: "rln-relay-membership-index" }: uint32
staticnodes* {.
desc: "Peer multiaddr to directly connect with. Argument may be repeated."
name: "staticnode" }: seq[string]

View File

@ -4,6 +4,7 @@ import
std/[options, tables, strutils, sequtils, os],
chronos, chronicles, metrics,
stew/shims/net as stewNet,
stew/byteutils,
eth/keys,
eth/p2p/discoveryv5/enr,
libp2p/crypto/crypto,
@ -16,7 +17,7 @@ import
../protocol/waku_swap/waku_swap,
../protocol/waku_filter/waku_filter,
../protocol/waku_lightpush/waku_lightpush,
../protocol/waku_rln_relay/waku_rln_relay_types,
../protocol/waku_rln_relay/[waku_rln_relay_types],
../utils/peers,
../utils/requests,
./storage/migration/migration_types,
@ -36,7 +37,7 @@ when defined(rln):
import
libp2p/protocols/pubsub/rpc/messages,
web3,
../protocol/waku_rln_relay/[rln, waku_rln_relay_utils]
../protocol/waku_rln_relay/[rln, waku_rln_relay_utils, waku_rln_relay_utils]
declarePublicCounter waku_node_messages, "number of messages received", ["type"]
declarePublicGauge waku_node_filters, "number of content filter subscriptions"
@ -415,48 +416,56 @@ when defined(rln):
memContractAddOpt: Option[Address] = none(Address),
groupOpt: Option[seq[IDCommitment]] = none(seq[IDCommitment]),
memKeyPairOpt: Option[MembershipKeyPair] = none(MembershipKeyPair),
memIndexOpt: Option[uint] = none(uint)) {.async.} =
memIndexOpt: Option[uint] = none(uint),
onchainMode: bool = true) {.async.} =
# TODO return a bool value to indicate the success of the call
# check whether inputs are provided
if ethClientAddrOpt.isNone():
info "failed to mount rln relay: Ethereum client address is not provided"
return
if ethAccAddrOpt.isNone():
info "failed to mount rln relay: Ethereum account address is not provided"
return
if memContractAddOpt.isNone():
info "failed to mount rln relay: membership contract address is not provided"
return
if groupOpt.isNone():
# TODO this check is not necessary for a dynamic group
info "failed to mount rln relay: group information is not provided"
return
if onchainMode:
if memContractAddOpt.isNone():
error "failed to mount rln relay: membership contract address is not provided"
return
if ethClientAddrOpt.isNone():
error "failed to mount rln relay: Ethereum client address is not provided"
return
if ethAccAddrOpt.isNone():
error "failed to mount rln relay: Ethereum account address is not provided"
return
else:
if groupOpt.isNone():
error "failed to mount rln relay: group information is not provided"
return
if memKeyPairOpt.isNone():
info "failed to mount rln relay: membership key of the node is not provided"
error "failed to mount rln relay: membership key of the node is not provided"
return
if memIndexOpt.isNone():
info "failed to mount rln relay: membership index is not provided"
error "failed to mount rln relay: membership index is not provided"
return
let
var
ethClientAddr: string
ethAccAddr: Address
memContractAdd: Address
if onchainMode:
ethClientAddr = ethClientAddrOpt.get()
ethAccAddr = ethAccAddrOpt.get()
memContractAdd = memContractAddOpt.get()
let
group = groupOpt.get()
memKeyPair = memKeyPairOpt.get()
memIndex = memIndexOpt.get()
# check the peer's index and the inclusion of user's identity commitment in the group
doAssert((memKeyPair.idCommitment) == group[int(memIndex)])
# create an RLN instance
var rlnInstance = createRLNInstance(32)
var rlnInstance = createRLNInstance()
doAssert(rlnInstance.isOk)
var rln = rlnInstance.value
# generate the membership keys if none is provided
# this if condition never gets through for a static group of users
# in a happy path, this condition never gets through for a static group of users
# the node should pass its keys i.e., memKeyPairOpt to the function
if not memKeyPairOpt.isSome:
let membershipKeyPair = rln.membershipKeyGen()
@ -479,11 +488,12 @@ when defined(rln):
ethAccountAddress: ethAccAddr,
rlnInstance: rln)
# register the rln-relay peer to the membership contract
let is_successful = await rlnPeer.register()
# check whether registration is done
doAssert(is_successful)
debug "peer is successfully registered into the membership contract"
if onchainMode:
# register the rln-relay peer to the membership contract
let is_successful = await rlnPeer.register()
# check whether registration is done
doAssert(is_successful)
debug "peer is successfully registered into the membership contract"
node.wakuRlnRelay = rlnPeer
@ -539,6 +549,7 @@ proc startRelay*(node: WakuNode) {.async.} =
proc mountRelay*(node: WakuNode,
topics: seq[string] = newSeq[string](),
rlnRelayEnabled = false,
rlnRelayMemIndex = uint(0),
relayMessages = true,
triggerSelf = true)
# @TODO: Better error handling: CatchableError is raised by `waitFor`
@ -573,11 +584,42 @@ proc mountRelay*(node: WakuNode,
when defined(rln):
if rlnRelayEnabled:
# TODO pass rln relay inputs to this proc, right now it uses default values that are set in the mountRlnRelay proc
# TODO get user inputs via cli options
info "WakuRLNRelay is enabled"
waitFor mountRlnRelay(node)
info "WakuRLNRelay is mounted successfully"
# a static list of 50 membership keys in hexadecimal format
let
groupKeys = STATIC_GROUP_KEYS
groupSize = int(groupKeys.len/2)
debug "rln-relay membership index", rlnRelayMemIndex
# validate the user-supplied membership index
if rlnRelayMemIndex < uint(0) or rlnRelayMemIndex >= uint(groupSize):
error "wrong membership index, failed to mount WakuRLNRelay"
else:
# prepare group related inputs from the hardcoded keys
let
groupKeyPairs = groupKeys.toMembershipKeyPairs()
groupIDCommitments = groupKeyPairs.mapIt(it.idCommitment)
# mount rlnrelay in offline mode
waitFor node.mountRlnRelay(groupOpt= some(groupIDCommitments), memKeyPairOpt = some(groupKeyPairs[rlnRelayMemIndex]), memIndexOpt= some(rlnRelayMemIndex), onchainMode = false)
info "membership id key", idkey=groupKeyPairs[rlnRelayMemIndex].idKey.toHex
info "membership id commitment key", idCommitmentkey=groupIDCommitments[rlnRelayMemIndex].toHex
# check the correct construction of the tree by comparing the calculated root against the expected root
# no error should happen as it is already captured in the unit tests
# TODO have added this check to account for unseen corner cases, will remove it later
let
root = node.wakuRlnRelay.rlnInstance.getMerkleRoot.value.toHex()
expectedRoot = STATIC_GROUP_MERKLE_ROOT
if root != expectedRoot:
error "root mismatch: something went wrong not in Merkle tree construction"
debug "the calculated root", root
info "WakuRLNRelay is mounted successfully"
info "relay mounted successfully"
if node.started:
@ -844,7 +886,8 @@ when isMainModule:
mountRelay(node,
conf.topics.split(" "),
rlnRelayEnabled = conf.rlnRelay,
relayMessages = conf.relay) # Indicates if node is capable to relay messages
relayMessages = conf.relay, # Indicates if node is capable to relay messages
rlnRelayMemIndex = conf.rlnRelayMemIndex)
# Keepalive mounted on all nodes
mountLibp2pPing(node)

File diff suppressed because one or more lines are too long

View File

@ -18,7 +18,7 @@ contract(MembershipContract):
# TODO define a return type of bool for register method to signify a successful registration
proc register(pubkey: Uint256) # external payable
proc createRLNInstance*(d: int): RLNResult
proc createRLNInstance*(d: int = MERKLE_TREE_DEPTH): RLNResult
{.raises: [Defect, IOError].} =
## generates an instance of RLN
@ -114,7 +114,7 @@ proc insertMember*(rlnInstance: RLN[Bn256], idComm: IDCommitment): bool =
var member_is_added = update_next_member(rlnInstance, pkBufferPtr)
return member_is_added
proc removeMember*(rlnInstance: RLN[Bn256], index: uint): bool =
proc removeMember*(rlnInstance: RLN[Bn256], index: MembeshipIndex): bool =
let deletion_success = delete_member(rlnInstance, index)
return deletion_success
@ -130,3 +130,63 @@ proc getMerkleRoot*(rlnInstance: RLN[Bn256]): MerkleNodeResult =
var rootValue = cast[ptr array[32,byte]] (root.`ptr`)
let merkleNode = rootValue[]
return ok(merkleNode)
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
## the toMembershipKeyPairs proc populates a sequence of MembershipKeyPairs using the supplied groupKeys
var groupKeyPairs = newSeq[MembershipKeyPair]()
for i in 0..groupKeys.len-1:
let
idKey = groupKeys[i][0].hexToByteArray(32)
idCommitment = groupKeys[i][1].hexToByteArray(32)
groupKeyPairs.add(MembershipKeyPair(idKey: idKey, idCommitment: idCommitment))
return groupKeyPairs
proc calcMerkleRoot*(list: seq[IDCommitment]): string {.raises: [Defect, IOError].} =
## returns the root of the Merkle tree that is computed from the supplied list
## the root is in hexadecimal format
var rlnInstance = createRLNInstance()
doAssert(rlnInstance.isOk)
var rln = rlnInstance.value
# create a Merkle tree
for i in 0..list.len-1:
var member_is_added = false
member_is_added = rln.insertMember(list[i])
doAssert(member_is_added)
let root = rln.getMerkleRoot().value().toHex
return root
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
## 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)
# initialize a Merkle tree
var rlnInstance = createRLNInstance()
if not rlnInstance.isOk:
return (@[], "")
var rln = rlnInstance.value
var output = newSeq[(string,string)]()
for i in 0..n-1:
# generate a key pair
let keypair = rln.membershipKeyGen()
doAssert(keypair.isSome())
let keyTuple = (keypair.get().idKey.toHex, keypair.get().idCommitment.toHex)
output.add(keyTuple)
# insert the key to the Merkle tree
let inserted = rln.insertMember(keypair.get().idCommitment)
if not inserted:
return (@[], "")
let root = rln.getMerkleRoot().value.toHex
return (output, root)