Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(rln): Implement rln tests #2639

Merged
merged 21 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading