nwaku/tests/v2/test_rln_group_manager_onch...

353 lines
12 KiB
Nim

when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import
std/[options, osproc, streams, strutils, sequtils],
stew/results,
stew/shims/net as stewNet,
testutils/unittests,
chronos,
chronicles,
stint,
web3,
json,
libp2p/crypto/crypto,
eth/keys
import
../../waku/v2/protocol/waku_rln_relay/protocol_types,
../../waku/v2/protocol/waku_rln_relay/constants,
../../waku/v2/protocol/waku_rln_relay/contract,
../../waku/v2/protocol/waku_rln_relay/rln,
../../waku/v2/protocol/waku_rln_relay/conversion_utils,
../../waku/v2/protocol/waku_rln_relay/group_manager/on_chain/group_manager,
./testlib/common,
./test_utils
from posix import kill, SIGINT
proc generateCredentials(rlnInstance: ptr RLN): IdentityCredential =
let credRes = membershipKeyGen(rlnInstance)
return credRes.get()
proc generateCredentials(rlnInstance: ptr RLN, n: int): seq[IdentityCredential] =
var credentials: seq[IdentityCredential]
for i in 0 ..< n:
credentials.add(generateCredentials(rlnInstance))
return credentials
# a util function used for testing purposes
# it deploys membership contract on Ganache (or any Eth client available on EthClient 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
let 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(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(MerkleTreeDepth.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(MembershipContractCode,
contractInput = contractInput)
let contractAddress = receipt.contractAddress.get
debug "Address of the deployed membership contract: ", contractAddress
let newBalance = await web3.provider.eth_getBalance(web3.defaultAccount, "latest")
debug "Account balance after the contract deployment: ", newBalance
await web3.close()
debug "disconnected from ", ethClientAddress
return contractAddress
proc createEthAccount(): Future[(keys.PrivateKey, Address)] {.async.} =
let web3 = await newWeb3(EthClient)
let accounts = await web3.provider.eth_accounts()
let gasPrice = int(await web3.provider.eth_gasPrice())
web3.defaultAccount = accounts[0]
let pk = keys.PrivateKey.random(rng[])
let acc = Address(toCanonicalAddress(pk.toPublicKey()))
var tx:EthSend
tx.source = accounts[0]
tx.value = some(ethToWei(10.u256))
tx.to = some(acc)
tx.gasPrice = some(gasPrice)
# Send 10 eth to acc
discard await web3.send(tx)
let balance = await web3.provider.eth_getBalance(acc, "latest")
assert(balance == ethToWei(10.u256))
return (pk, acc)
# Runs Ganache daemon
proc runGanache(): Process =
# We run directly "node node_modules/ganache/dist/node/cli.js" rather than using "npx ganache", so that the daemon does not spawn in a new child process.
# In this way, we can directly send a SIGINT signal to the corresponding PID to gracefully terminate Ganache without dealing with multiple processes.
# Passed options are
# --port Port to listen on.
# --miner.blockGasLimit Sets the block gas limit in WEI.
# --wallet.defaultBalance The default account balance, specified in ether.
# See ganache documentation https://www.npmjs.com/package/ganache for more details
try:
let runGanache = startProcess("npx", args = ["--yes", "ganache", "--port", "8540", "--miner.blockGasLimit", "300000000000000", "--wallet.defaultBalance", "10000"], options = {poUsePath})
let ganachePID = runGanache.processID
# We read stdout from Ganache to see when daemon is ready
var ganacheStartLog: string
var cmdline: string
while true:
try:
if runGanache.outputstream.readLine(cmdline):
ganacheStartLog.add(cmdline)
if cmdline.contains("Listening on 127.0.0.1:8540"):
break
except:
break
debug "Ganache daemon is running and ready", pid=ganachePID, startLog=ganacheStartLog
return runGanache
except:
error "Ganache daemon run failed"
# Stops Ganache daemon
proc stopGanache(runGanache: Process) {.used.} =
let ganachePID = runGanache.processID
# We gracefully terminate Ganache daemon by sending a SIGINT signal to the runGanache PID to trigger RPC server termination and clean-up
let returnCodeSIGINT = kill(ganachePID.int32, SIGINT)
debug "Sent SIGINT to Ganache", ganachePID=ganachePID, returnCode=returnCodeSIGINT
# We wait the daemon to exit
try:
let returnCodeExit = runGanache.waitForExit()
debug "Ganache daemon terminated", returnCode=returnCodeExit
debug "Ganache daemon run log", log=runGanache.outputstream.readAll()
except:
error "Ganache daemon termination failed"
proc setup(): Future[OnchainGroupManager] {.async.} =
let rlnInstanceRes = createRlnInstance()
require:
rlnInstanceRes.isOk()
let rlnInstance = rlnInstanceRes.get()
let contractAddress = await uploadRLNContract(EthClient)
# connect to the eth client
let web3 = await newWeb3(EthClient)
let accounts = await web3.provider.eth_accounts()
web3.defaultAccount = accounts[1]
let (pk, _) = await createEthAccount()
let onchainConfig = OnchainGroupManagerConfig(ethClientUrl: EthClient,
ethContractAddress: $contractAddress,
ethPrivateKey: some($pk))
let manager {.used.} = OnchainGroupManager(config: onchainConfig,
rlnInstance: rlnInstance)
return manager
suite "Onchain group manager":
# We run Ganache
let runGanache {.used.} = runGanache()
asyncTest "should initialize successfully":
let manager = await setup()
await manager.init()
check:
manager.config.ethRpc.isSome()
manager.config.rlnContract.isSome()
manager.config.membershipFee.isSome()
manager.initialized
asyncTest "startGroupSync: should start group sync":
let manager = await setup()
await manager.init()
await manager.startGroupSync()
asyncTest "startGroupSync: should guard against uninitialized state":
let manager = await setup()
expect(ValueError):
await manager.startGroupSync()
asyncTest "startGroupSync: should sync to the state of the group":
let manager = await setup()
let credentials = generateCredentials(manager.rlnInstance)
manager.idCredentials = some(credentials)
await manager.init()
let merkleRootBeforeRes = manager.rlnInstance.getMerkleRoot()
require:
merkleRootBeforeRes.isOk()
let merkleRootBefore = merkleRootBeforeRes.get()
let future = newFuture[void]("startGroupSync")
proc generateCallback(fut: Future[void], idCommitment: IDCommitment): OnRegisterCallback =
proc callback(registrations: seq[Membership]): Future[void] {.async.} =
require:
registrations.len == 1
registrations[0].idCommitment == idCommitment
registrations[0].index == 0
fut.complete()
return callback
manager.onRegister(generateCallback(future, credentials.idCommitment))
await manager.startGroupSync()
await future
let merkleRootAfterRes = manager.rlnInstance.getMerkleRoot()
require:
merkleRootAfterRes.isOk()
let merkleRootAfter = merkleRootAfterRes.get()
check:
merkleRootBefore != merkleRootAfter
asyncTest "startGroupSync: should fetch history correctly":
let manager = await setup()
let credentials = generateCredentials(manager.rlnInstance, 5)
await manager.init()
let merkleRootBeforeRes = manager.rlnInstance.getMerkleRoot()
require:
merkleRootBeforeRes.isOk()
let merkleRootBefore = merkleRootBeforeRes.get()
var futures = [newFuture[void](), newFuture[void](), newFuture[void](), newFuture[void](), newFuture[void]()]
proc generateCallback(futs: array[0..4, Future[system.void]], credentials: seq[IdentityCredential]): OnRegisterCallback =
var futureIndex = 0
proc callback(registrations: seq[Membership]): Future[void] {.async.} =
require:
registrations.len == 1
registrations[0].idCommitment == credentials[futureIndex].idCommitment
registrations[0].index == MembershipIndex(futureIndex)
futs[futureIndex].complete()
futureIndex += 1
return callback
manager.onRegister(generateCallback(futures, credentials))
await manager.startGroupSync()
for i in 0 ..< credentials.len():
await manager.register(credentials[i])
await allFutures(futures)
let merkleRootAfterRes = manager.rlnInstance.getMerkleRoot()
require:
merkleRootAfterRes.isOk()
let merkleRootAfter = merkleRootAfterRes.get()
check:
merkleRootBefore != merkleRootAfter
asyncTest "register: should guard against uninitialized state":
let manager = await setup()
let dummyCommitment = default(IDCommitment)
expect(ValueError):
await manager.register(dummyCommitment)
asyncTest "register: should register successfully":
let manager = await setup()
await manager.init()
await manager.startGroupSync()
let idCommitment = generateCredentials(manager.rlnInstance).idCommitment
let merkleRootBeforeRes = manager.rlnInstance.getMerkleRoot()
require:
merkleRootBeforeRes.isOk()
let merkleRootBefore = merkleRootBeforeRes.get()
await manager.register(idCommitment)
let merkleRootAfterRes = manager.rlnInstance.getMerkleRoot()
require:
merkleRootAfterRes.isOk()
let merkleRootAfter = merkleRootAfterRes.get()
check:
merkleRootAfter.inHex() != merkleRootBefore.inHex()
manager.latestIndex == 1
asyncTest "register: callback is called":
let manager = await setup()
var callbackCalled = false
let idCommitment = generateCredentials(manager.rlnInstance).idCommitment
let fut = newFuture[void]()
proc callback(registrations: seq[Membership]): Future[void] {.async.} =
require:
registrations.len == 1
registrations[0].idCommitment == idCommitment
registrations[0].index == 0
callbackCalled = true
fut.complete()
manager.onRegister(callback)
await manager.init()
await manager.startGroupSync()
await manager.register(idCommitment)
await fut
check:
callbackCalled
asyncTest "withdraw: should guard against uninitialized state":
let manager = await setup()
let idSecretHash = generateCredentials(manager.rlnInstance).idSecretHash
expect(ValueError):
await manager.withdraw(idSecretHash)
################################
## Terminating/removing Ganache
################################
# We stop Ganache daemon
stopGanache(runGanache)