Skip to content

Commit

Permalink
test(rln): Implement rln tests (#2639)
Browse files Browse the repository at this point in the history
* Implement tests.
* Clean coding.
  • Loading branch information
AlejandroCabeza authored Aug 2, 2024
1 parent ebda56d commit a3fa175
Show file tree
Hide file tree
Showing 12 changed files with 1,064 additions and 307 deletions.
491 changes: 472 additions & 19 deletions tests/node/test_wakunode_relay_rln.nim

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions tests/node/test_wakunode_sharding.nim
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import
waku/[
waku_core/topics/pubsub_topic,
waku_core/topics/sharding,
waku_store_legacy/common,
node/waku_node,
common/paging,
waku_core,
Expand Down
10 changes: 10 additions & 0 deletions tests/testlib/assertions.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,13 @@ import chronos

template assertResultOk*[T, E](result: Result[T, E]) =
assert result.isOk(), $result.error()

template assertResultOk*(result: Result[void, string]) =
assert result.isOk(), $result.error()

template typeEq*(t: typedesc, u: typedesc): bool =
# <a is b> is also true if a is subtype of b
t is u and u is t # Only true if actually equal types

template typeEq*(t: auto, u: typedesc): bool =
typeEq(type(t), u)
1 change: 1 addition & 0 deletions tests/testlib/futures.nim
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const
FUTURE_TIMEOUT_MEDIUM* = 5.seconds
FUTURE_TIMEOUT_LONG* = 10.seconds
FUTURE_TIMEOUT_SHORT* = 100.milliseconds
FUTURE_TIMEOUT_SCORING* = 13.seconds # Scoring is 12s, so we need to wait more

proc newPushHandlerFuture*(): Future[(string, WakuMessage)] =
newFuture[(string, WakuMessage)]()
Expand Down
2 changes: 1 addition & 1 deletion tests/waku_filter_v2/test_waku_filter_dos_protection.nim
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ suite "Waku Filter - DOS protection":
some(FilterSubscribeErrorKind.TOO_MANY_REQUESTS)

# ensure period of time has passed and clients can again use the service
await sleepAsync(600.milliseconds)
await sleepAsync(700.milliseconds)
check client1.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) ==
none(FilterSubscribeErrorKind)
check client2.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) ==
Expand Down
29 changes: 29 additions & 0 deletions tests/waku_keystore/utils.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{.used.}
{.push raises: [].}

import stint

import
waku/[waku_keystore/protocol_types, waku_rln_relay, waku_rln_relay/protocol_types]

func fromStrToBytesLe*(v: string): seq[byte] =
try:
return @(hexToUint[256](v).toBytesLE())
except ValueError:
# this should never happen
return @[]

func defaultIdentityCredential*(): IdentityCredential =
# zero out the values we don't need
return IdentityCredential(
idTrapdoor: default(IdentityTrapdoor),
idNullifier: default(IdentityNullifier),
idSecretHash: fromStrToBytesLe(
"7984f7c054ad7793d9f31a1e9f29eaa8d05966511e546bced89961eb8874ab9"
),
idCommitment: fromStrToBytesLe(
"51c31de3bff7e52dc7b2eb34fc96813bacf38bde92d27fe326ce5d8296322a7"
),
)

{.pop.}
44 changes: 43 additions & 1 deletion tests/waku_rln_relay/rln/test_wrappers.nim
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import
waku/waku_rln_relay/rln,
waku/waku_rln_relay/rln/wrappers,
./waku_rln_relay_utils,
../../testlib/[simple_mock]
../../testlib/[simple_mock, assertions],
../../waku_keystore/utils

from std/times import epochTime

const Empty32Array = default(array[32, byte])

Expand Down Expand Up @@ -131,3 +134,42 @@ suite "RlnConfig":
# Cleanup
mock(new_circuit):
backup

suite "proofGen":
test "Valid zk proof":
# this test vector is from zerokit
let rlnInstanceRes = createRLNInstanceWrapper()
assertResultOk(rlnInstanceRes)
let rlnInstance = rlnInstanceRes.value

let identityCredential = defaultIdentityCredential()
assert rlnInstance.insertMember(identityCredential.idCommitment)

let merkleRootRes = rlnInstance.getMerkleRoot()
assertResultOk(merkleRootRes)
let merkleRoot = merkleRootRes.value

let proofGenRes = rlnInstance.proofGen(
data = @[],
memKeys = identityCredential,
memIndex = MembershipIndex(0),
epoch = uint64(epochTime() / 1.float64).toEpoch(),
)
assertResultOk(proofGenRes)

let
rateLimitProof = proofGenRes.value
proofVerifyRes = rlnInstance.proofVerify(
data = @[], proof = rateLimitProof, validRoots = @[merkleRoot]
)

assertResultOk(proofVerifyRes)
assert proofVerifyRes.value, "proof verification failed"

# Assert the proof fields adhere to the specified types and lengths
check:
typeEq(rateLimitProof.proof, array[256, byte])
typeEq(rateLimitProof.merkleRoot, array[32, byte])
typeEq(rateLimitProof.shareX, array[32, byte])
typeEq(rateLimitProof.shareY, array[32, byte])
typeEq(rateLimitProof.nullifier, array[32, byte])
222 changes: 26 additions & 196 deletions tests/waku_rln_relay/test_rln_group_manager_onchain.nim
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import
chronicles,
stint,
web3,
json,
libp2p/crypto/crypto,
eth/keys

import
waku/[
waku_node,
Expand All @@ -26,202 +26,9 @@ import
waku_rln_relay/group_manager/on_chain/group_manager,
],
../testlib/[wakucore, wakunode, common],
./utils_onchain,
./utils

const CHAIN_ID = 1337

proc generateCredentials(rlnInstance: ptr RLN): IdentityCredential =
let credRes = membershipKeyGen(rlnInstance)
return credRes.get()

proc getRateCommitment(
idCredential: IdentityCredential, userMessageLimit: UserMessageLimit
): RlnRelayResult[RawRateCommitment] =
return RateCommitment(
idCommitment: idCredential.idCommitment, userMessageLimit: userMessageLimit
).toLeaf()

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 Anvil (or any Eth client available on EthClient address)
# must be edited if used for a different contract than membership contract
# <the difference between this and rln-v1 is that there is no need to deploy the poseidon hasher 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 poseidon hasher bytecode
let poseidonT3Receipt = await web3.deployContract(PoseidonT3)
let poseidonT3Address = poseidonT3Receipt.contractAddress.get()
let poseidonAddressStripped = strip0xPrefix($poseidonT3Address)

# deploy lazy imt bytecode
let lazyImtReceipt = await web3.deployContract(
LazyIMT.replace("__$PoseidonT3$__", poseidonAddressStripped)
)
let lazyImtAddress = lazyImtReceipt.contractAddress.get()
let lazyImtAddressStripped = strip0xPrefix($lazyImtAddress)

# deploy waku rlnv2 contract
let wakuRlnContractReceipt = await web3.deployContract(
WakuRlnV2Contract.replace("__$PoseidonT3$__", poseidonAddressStripped).replace(
"__$LazyIMT$__", lazyImtAddressStripped
)
)
let wakuRlnContractAddress = wakuRlnContractReceipt.contractAddress.get()
let wakuRlnAddressStripped = strip0xPrefix($wakuRlnContractAddress)

debug "Address of the deployed rlnv2 contract: ", wakuRlnContractAddress

# need to send concat: impl & init_bytes
let contractInput = encode(wakuRlnContractAddress).data & Erc1967ProxyContractInput
debug "contractInput", contractInput
let proxyReceipt =
await web3.deployContract(Erc1967Proxy, contractInput = contractInput)

debug "proxy receipt", proxyReceipt
let proxyAddress = proxyReceipt.contractAddress.get()

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 proxyAddress

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(1000.u256))
tx.to = some(acc)
tx.gasPrice = some(gasPrice)

# Send 1000 eth to acc
discard await web3.send(tx)
let balance = await web3.provider.eth_getBalance(acc, "latest")
assert balance == ethToWei(1000.u256),
fmt"Balance is {balance} but expected {ethToWei(1000.u256)}"

return (pk, acc)

proc getAnvilPath(): string =
var anvilPath = ""
if existsEnv("XDG_CONFIG_HOME"):
anvilPath = joinPath(anvilPath, os.getEnv("XDG_CONFIG_HOME", ""))
else:
anvilPath = joinPath(anvilPath, os.getEnv("HOME", ""))
anvilPath = joinPath(anvilPath, ".foundry/bin/anvil")
return $anvilPath

# Runs Anvil daemon
proc runAnvil(): Process =
# Passed options are
# --port Port to listen on.
# --gas-limit Sets the block gas limit in WEI.
# --balance The default account balance, specified in ether.
# --chain-id Chain ID of the network.
# See anvil documentation https://book.getfoundry.sh/reference/anvil/ for more details
try:
let anvilPath = getAnvilPath()
debug "Anvil path", anvilPath
let runAnvil = startProcess(
anvilPath,
args = [
"--port",
"8540",
"--gas-limit",
"300000000000000",
"--balance",
"1000000000",
"--chain-id",
$CHAIN_ID,
],
options = {poUsePath},
)
let anvilPID = runAnvil.processID

# We read stdout from Anvil to see when daemon is ready
var anvilStartLog: string
var cmdline: string
while true:
try:
if runAnvil.outputstream.readLine(cmdline):
anvilStartLog.add(cmdline)
if cmdline.contains("Listening on 127.0.0.1:8540"):
break
except Exception, CatchableError:
break
debug "Anvil daemon is running and ready", pid = anvilPID, startLog = anvilStartLog
return runAnvil
except: # TODO: Fix "BareExcept" warning
error "Anvil daemon run failed", err = getCurrentExceptionMsg()

# Stops Anvil daemon
proc stopAnvil(runAnvil: Process) {.used.} =
let anvilPID = runAnvil.processID
# We wait the daemon to exit
try:
# We terminate Anvil daemon by sending a SIGTERM signal to the runAnvil PID to trigger RPC server termination and clean-up
kill(runAnvil)
debug "Sent SIGTERM to Anvil", anvilPID = anvilPID
except:
error "Anvil daemon termination failed: ", err = getCurrentExceptionMsg()

proc setup(): Future[OnchainGroupManager] {.async.} =
let rlnInstanceRes =
createRlnInstance(tree_path = genTempPath("rln_tree", "group_manager_onchain"))
check:
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[0]

var pk = none(string)
let (privateKey, _) = await createEthAccount()
pk = some($privateKey)

let manager = OnchainGroupManager(
ethClientUrl: EthClient,
ethContractAddress: $contractAddress,
chainId: CHAIN_ID,
ethPrivateKey: pk,
rlnInstance: rlnInstance,
onFatalErrorAction: proc(errStr: string) =
raiseAssert errStr
,
)

return manager

suite "Onchain group manager":
# We run Anvil
let runAnvil {.used.} = runAnvil()
Expand Down Expand Up @@ -282,9 +89,32 @@ suite "Onchain group manager":
raiseAssert errStr
,
)
(await manager2.init()).isErrOr:
let e = await manager2.init()
(e).isErrOr:
raiseAssert "Expected error when contract address doesn't match"

echo "---"
discard "persisted data: contract address mismatch"
echo e.error
echo "---"

asyncTest "should error if contract does not exist":
var triggeredError = false

let manager = await setup()
manager.ethContractAddress = "0x0000000000000000000000000000000000000000"
manager.onFatalErrorAction = proc(msg: string) {.gcsafe, closure.} =
echo "---"
discard
"Failed to get the deployed block number. Have you set the correct contract address?: No response from the Web3 provider"
echo msg
echo "---"
triggeredError = true

discard await manager.init()

check triggeredError

asyncTest "should error when keystore path and password are provided but file doesn't exist":
let manager = await setup()
manager.keystorePath = some("/inexistent/file")
Expand Down
Loading

0 comments on commit a3fa175

Please sign in to comment.