{.used.} import std/options, testutils/unittests, chronos, chronicles, stint, web3, stew/byteutils, stew/shims/net as stewNet, libp2p/crypto/crypto, ../../waku/v2/protocol/waku_rln_relay/[rln, waku_rln_relay_utils], ../../waku/v2/node/wakunode2, ../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: # 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: # 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") # 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(poseidonHasherCode) hasherAddress = hasherReceipt.contractAddress.get debug "hasher address: ", hasherAddress # encode membership contract inputs to 32 bytes zero-padded let membershipFeeEncoded = encode(MembershipFee).data depthEncoded = encode(Depth).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(membershipContractCode, 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 "contract membership": let contractAddress = await uploadContract(EthClient) # connect to the eth client let web3 = await newWeb3(EthClient) debug "web3 connected to", EthClient # 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", EthClient asyncTest "registration procedure": # deploy the contract let contractAddress = await uploadContract(EthClient) # prepare rln-relay peer inputs let web3 = await newWeb3(EthClient) 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 ctx = RLN[Bn256]() ctxPtr = addr(ctx) ctxPtrPtr = addr(ctxPtr) doAssert(createRLNInstance(32, ctxPtrPtr)) # generate the membership keys let membershipKeyPair = membershipKeyGen(ctxPtrPtr[]) check: membershipKeyPair.isSome # initialize the WakuRLNRelay var rlnPeer = WakuRLNRelay(membershipKeyPair: membershipKeyPair.get(), ethClientAddress: EthClient, 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.init(nodeKey, ValidIpAddress.init("0.0.0.0"), Port(60000)) await node.start() # deploy the contract let membershipContractAddress = await uploadContract(EthClient) # prepare rln-relay inputs let web3 = await newWeb3(EthClient) accounts = await web3.provider.eth_accounts() # choose one of the existing account for the rln-relay peer ethAccountAddress = accounts[9] await web3.close() # start rln-relay await node.mountRlnRelay(ethClientAddress = some(EthClient), ethAccountAddress = some(ethAccountAddress), membershipContractAddress = some(membershipContractAddress)) await node.stop() suite "Waku rln relay": test "key_gen Nim Wrappers": var merkleDepth: csize_t = 32 # parameters.key contains the parameters related to the Poseidon hasher # 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 # cargo run --example export_test_keys # the file is generated separately and copied here parameters = readFile("waku/v2/protocol/waku_rln_relay/parameters.key") pbytes = parameters.toBytes() len : csize_t = uint(pbytes.len) parametersBuffer = Buffer(`ptr`: addr(pbytes[0]), len: len) check: # check the parameters.key is not empty pbytes.len != 0 # ctx holds the information that is going to be used for the key generation var obj = RLN[Bn256]() objPtr = addr(obj) objptrptr = addr(objPtr) ctx = objptrptr let res = new_circuit_from_params(merkleDepth, addr parametersBuffer, ctx) check: # check whether the circuit parameters are generated successfully res == true # keysBufferPtr will hold the generated key pairs i.e., secret and public keys var keysBuffer : Buffer keysBufferPtr = addr(keysBuffer) done = key_gen(ctx[], keysBufferPtr) check: # check whether the keys are generated successfully done == true if done: var generatedKeys = cast[ptr array[64, byte]](keysBufferPtr.`ptr`)[] check: # the public and secret keys together are 64 bytes generatedKeys.len == 64 debug "generated keys: ", generatedKeys test "membership Key Gen": # create an RLN instance var ctx = RLN[Bn256]() ctxPtr = addr(ctx) ctxPtrPtr = addr(ctxPtr) doAssert(createRLNInstance(32, ctxPtrPtr)) var key = membershipKeyGen(ctxPtrPtr[]) var empty : array[32,byte] check: key.isSome key.get().secretKey.len == 32 key.get().publicKey.len == 32 key.get().secretKey != empty key.get().publicKey != empty debug "the generated membership key pair: ", key test "get_root Nim binding": # create an RLN instance which also includes an empty Merkle tree var ctx = RLN[Bn256]() ctxPtr = addr(ctx) ctxPtrPtr = addr(ctxPtr) doAssert(createRLNInstance(32, ctxPtrPtr)) # read the Merkle Tree root var root1 {.noinit.} : Buffer = Buffer() rootPtr1 = addr(root1) get_root_successful1 = get_root(ctxPtrPtr[], rootPtr1) doAssert(get_root_successful1) doAssert(root1.len == 32) # read the Merkle Tree root var root2 {.noinit.} : Buffer = Buffer() rootPtr2 = addr(root2) get_root_successful2 = get_root(ctxPtrPtr[], rootPtr2) doAssert(get_root_successful2) doAssert(root2.len == 32) var rootValue1 = cast[ptr array[32,byte]] (root1.`ptr`) let rootHex1 = rootValue1[].toHex var rootValue2 = cast[ptr array[32,byte]] (root2.`ptr`) let rootHex2 = rootValue2[].toHex # the two roots must be identical doAssert(rootHex1 == rootHex2) test "update_next_member Nim Wrapper": # create an RLN instance which also includes an empty Merkle tree var ctx = RLN[Bn256]() ctxPtr = addr(ctx) ctxPtrPtr = addr(ctxPtr) doAssert(createRLNInstance(32, ctxPtrPtr)) # generate a key pair var keypair = membershipKeyGen(ctxPtrPtr[]) doAssert(keypair.isSome()) var pkBuffer = Buffer(`ptr`: addr(keypair.get().publicKey[0]), len: 32) let pkBufferPtr = addr pkBuffer # add the member to the tree var member_is_added = update_next_member(ctxPtrPtr[], pkBufferPtr) check: member_is_added == true test "delete_member Nim wrapper": # create an RLN instance which also includes an empty Merkle tree var ctx = RLN[Bn256]() ctxPtr = addr(ctx) ctxPtrPtr = addr(ctxPtr) doAssert(createRLNInstance(32, ctxPtrPtr)) # delete the first member var deleted_member_index = uint(0) let deletion_success = delete_member(ctxPtrPtr[], deleted_member_index) doAssert(deletion_success) test "Merkle tree consistency check between deletion and insertion": # create an RLN instance var ctx = RLN[Bn256]() ctxPtr = addr(ctx) ctxPtrPtr = addr(ctxPtr) doAssert(createRLNInstance(32, ctxPtrPtr)) # read the Merkle Tree root var root1 {.noinit.} : Buffer = Buffer() rootPtr1 = addr(root1) get_root_successful1 = get_root(ctxPtrPtr[], rootPtr1) doAssert(get_root_successful1) doAssert(root1.len == 32) # generate a key pair var keypair = membershipKeyGen(ctxPtrPtr[]) doAssert(keypair.isSome()) var pkBuffer = Buffer(`ptr`: addr(keypair.get().publicKey[0]), len: 32) let pkBufferPtr = addr pkBuffer # add the member to the tree var member_is_added = update_next_member(ctxPtrPtr[], pkBufferPtr) doAssert(member_is_added) # read the Merkle Tree root after insertion var root2 {.noinit.} : Buffer = Buffer() rootPtr2 = addr(root2) get_root_successful2 = get_root(ctxPtrPtr[], rootPtr2) doAssert(get_root_successful2) doAssert(root2.len == 32) # delete the first member var deleted_member_index = uint(0) let deletion_success = delete_member(ctxPtrPtr[], deleted_member_index) doAssert(deletion_success) # read the Merkle Tree root after the deletion var root3 {.noinit.} : Buffer = Buffer() rootPtr3 = addr(root3) get_root_successful3 = get_root(ctxPtrPtr[], rootPtr3) doAssert(get_root_successful3) doAssert(root3.len == 32) var rootValue1 = cast[ptr array[32,byte]] (root1.`ptr`) let rootHex1 = rootValue1[].toHex debug "The initial root", rootHex1 var rootValue2 = cast[ptr array[32,byte]] (root2.`ptr`) let rootHex2 = rootValue2[].toHex debug "The root after insertion", rootHex2 var rootValue3 = cast[ptr array[32,byte]] (root3.`ptr`) let rootHex3 = rootValue3[].toHex debug "The root after deletion", rootHex3 # the root must change after the insertion doAssert(not(rootHex1 == rootHex2)) ## The initial root of the tree (empty tree) must be identical to ## the root of the tree after one insertion followed by a deletion doAssert(rootHex1 == rootHex3)