diff --git a/tests/node/test_wakunode_relay_rln.nim b/tests/node/test_wakunode_relay_rln.nim index ba20b63f0..e1bdb7c6a 100644 --- a/tests/node/test_wakunode_relay_rln.nim +++ b/tests/node/test_wakunode_relay_rln.nim @@ -1,26 +1,107 @@ {.used.} import - std/[sequtils, tempfiles], - stew/byteutils, + std/[tempfiles, strutils, options], stew/shims/net as stewNet, + stew/results, testutils/unittests, chronos, libp2p/switch, - libp2p/protocols/pubsub/pubsub + libp2p/protocols/pubsub/pubsub, + eth/keys from std/times import epochTime import - waku/[node/waku_node, node/peer_manager, waku_core, waku_node, waku_rln_relay], + ../../../waku/[ + node/waku_node, + node/peer_manager, + waku_core, + waku_node, + common/error_handling, + waku_rln_relay, + waku_rln_relay/rln, + waku_rln_relay/protocol_types, + waku_keystore/keystore, + ], ../waku_store/store_utils, ../waku_archive/archive_utils, - ../waku_relay/utils, - ../waku_rln_relay/test_rln_group_manager_onchain, - ../testlib/[wakucore, wakunode, testasync, futures], - ../resources/payloads + ../testlib/[wakucore, wakunode, testasync, futures, common, assertions], + ../resources/payloads, + ../waku_rln_relay/[utils_static, utils_onchain] -suite "Waku RlnRelay - End to End": +from ../../waku/waku_noise/noise_utils import randomSeqByte + +proc buildRandomIdentityCredentials(): IdentityCredential = + # We generate a random identity credential (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen) + let + idTrapdoor = randomSeqByte(rng[], 32) + idNullifier = randomSeqByte(rng[], 32) + idSecretHash = randomSeqByte(rng[], 32) + idCommitment = randomSeqByte(rng[], 32) + + IdentityCredential( + idTrapdoor: idTrapdoor, + idNullifier: idNullifier, + idSecretHash: idSecretHash, + idCommitment: idCommitment, + ) + +proc addMembershipCredentialsToKeystore( + credentials: IdentityCredential, + keystorePath: string, + appInfo: AppInfo, + rlnRelayEthContractAddress: string, + password: string, + membershipIndex: uint, +): KeystoreResult[void] = + let + contract = MembershipContract(chainId: "0x539", address: rlnRelayEthContractAddress) + # contract = MembershipContract(chainId: "1337", address: rlnRelayEthContractAddress) + index = MembershipIndex(membershipIndex) + membershipCredential = KeystoreMembership( + membershipContract: contract, treeIndex: index, identityCredential: credentials + ) + + addMembershipCredentials( + path = keystorePath, + membership = membershipCredential, + password = password, + appInfo = appInfo, + ) + +proc fatalErrorVoidHandler(errMsg: string) {.gcsafe, raises: [].} = + discard + +proc getWakuRlnConfigOnChain*( + keystorePath: string, + appInfo: AppInfo, + rlnRelayEthContractAddress: string, + password: string, + credIndex: uint, + fatalErrorHandler: Option[OnFatalErrorHandler] = none(OnFatalErrorHandler), + ethClientAddress: Option[string] = none(string), +): WakuRlnConfig = + return WakuRlnConfig( + rlnRelayDynamic: true, + rlnRelayCredIndex: some(credIndex), + rlnRelayEthContractAddress: rlnRelayEthContractAddress, + rlnRelayEthClientAddress: ethClientAddress.get(EthClient), + rlnRelayTreePath: genTempPath("rln_tree", "wakunode_" & $credIndex), + rlnEpochSizeSec: 1, + onFatalErrorAction: fatalErrorHandler.get(fatalErrorVoidHandler), + # If these are used, initialisation fails with "failed to mount WakuRlnRelay: could not initialize the group manager: the commitment does not have a membership" + rlnRelayCredPath: keystorePath, + rlnRelayCredPassword: password, + ) + +proc setupRelayWithOnChainRln*( + node: WakuNode, pubsubTopics: seq[string], wakuRlnConfig: WakuRlnConfig +) {.async.} = + await node.mountRelay(pubsubTopics) + await node.mountRlnRelay(wakuRlnConfig) + +suite "Waku RlnRelay - End to End - Static": var pubsubTopic {.threadvar.}: PubsubTopic contentTopic {.threadvar.}: ContentTopic @@ -61,7 +142,7 @@ suite "Waku RlnRelay - End to End": # When RlnRelay is mounted let catchRes = catch: - await server.setupRln(1) + await server.setupStaticRln(1) # Then Relay and RLN are not mounted,and the process fails check: @@ -72,8 +153,8 @@ suite "Waku RlnRelay - End to End": asyncTest "Pubsub topics subscribed before mounting RlnRelay are added to it": # Given the node enables Relay and Rln while subscribing to a pubsub topic - await server.setupRelayWithRln(1.uint, @[pubsubTopic]) - await client.setupRelayWithRln(2.uint, @[pubsubTopic]) + await server.setupRelayWithStaticRln(1.uint, @[pubsubTopic]) + await client.setupRelayWithStaticRln(2.uint, @[pubsubTopic]) check: server.wakuRelay != nil server.wakuRlnRelay != nil @@ -107,8 +188,8 @@ suite "Waku RlnRelay - End to End": asyncTest "Pubsub topics subscribed after mounting RlnRelay are added to it": # Given the node enables Relay and Rln without subscribing to a pubsub topic - await server.setupRelayWithRln(1.uint, @[]) - await client.setupRelayWithRln(2.uint, @[]) + await server.setupRelayWithStaticRln(1.uint, @[]) + await client.setupRelayWithStaticRln(2.uint, @[]) # And the nodes are connected await client.connectToNodes(@[serverRemotePeerInfo]) @@ -167,8 +248,8 @@ suite "Waku RlnRelay - End to End": suite "Analysis of Bandwith Limitations": asyncTest "Valid Payload Sizes": # Given the node enables Relay and Rln while subscribing to a pubsub topic - await server.setupRelayWithRln(1.uint, @[pubsubTopic]) - await client.setupRelayWithRln(2.uint, @[pubsubTopic]) + await server.setupRelayWithStaticRln(1.uint, @[pubsubTopic]) + await client.setupRelayWithStaticRln(2.uint, @[pubsubTopic]) # And the nodes are connected await client.connectToNodes(@[serverRemotePeerInfo]) @@ -261,8 +342,8 @@ suite "Waku RlnRelay - End to End": asyncTest "Invalid Payload Sizes": # Given the node enables Relay and Rln while subscribing to a pubsub topic - await server.setupRelayWithRln(1.uint, @[pubsubTopic]) - await client.setupRelayWithRln(2.uint, @[pubsubTopic]) + await server.setupRelayWithStaticRln(1.uint, @[pubsubTopic]) + await client.setupRelayWithStaticRln(2.uint, @[pubsubTopic]) # And the nodes are connected await client.connectToNodes(@[serverRemotePeerInfo]) @@ -302,3 +383,375 @@ suite "Waku RlnRelay - End to End": # Then the message is not relayed check not await completionFut.withTimeout(FUTURE_TIMEOUT_LONG) + +suite "Waku RlnRelay - End to End - OnChain": + let runAnvil {.used.} = runAnvil() + + var + pubsubTopic {.threadvar.}: PubsubTopic + contentTopic {.threadvar.}: ContentTopic + + var + server {.threadvar.}: WakuNode + client {.threadvar.}: WakuNode + + var + serverRemotePeerInfo {.threadvar.}: RemotePeerInfo + clientPeerId {.threadvar.}: PeerId + + asyncSetup: + pubsubTopic = DefaultPubsubTopic + contentTopic = DefaultContentTopic + + let + serverKey = generateSecp256k1Key() + clientKey = generateSecp256k1Key() + + server = newTestWakuNode(serverKey, ValidIpAddress.init("0.0.0.0"), Port(0)) + client = newTestWakuNode(clientKey, ValidIpAddress.init("0.0.0.0"), Port(0)) + + await allFutures(server.start(), client.start()) + + serverRemotePeerInfo = server.switch.peerInfo.toRemotePeerInfo() + clientPeerId = client.switch.peerInfo.toRemotePeerInfo().peerId + + asyncTeardown: + await allFutures(client.stop(), server.stop()) + + suite "Smart Contract Availability and Interaction": + asyncTest "Invalid format contract": + let + # One character missing + invalidContractAddress = "0x000000000000000000000000000000000000000" + keystorePath = + genTempPath("rln_keystore", "test_wakunode_relay_rln-no_valid_contract") + appInfo = RlnAppInfo + password = "1234" + wakuRlnConfig1 = getWakuRlnConfigOnChain( + keystorePath, appInfo, invalidContractAddress, password, 0 + ) + wakuRlnConfig2 = getWakuRlnConfigOnChain( + keystorePath, appInfo, invalidContractAddress, password, 1 + ) + idCredential = buildRandomIdentityCredentials() + persistRes = addMembershipCredentialsToKeystore( + idCredential, keystorePath, appInfo, invalidContractAddress, password, 1 + ) + assertResultOk(persistRes) + + # Given the node enables Relay and Rln while subscribing to a pubsub topic + try: + await server.setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig1) + assert false, "Relay should fail mounting when using an invalid contract" + except CatchableError: + assert true + + try: + await client.setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig2) + assert false, "Relay should fail mounting when using an invalid contract" + except CatchableError: + assert true + + asyncTest "Unregistered contract": + # This is a very slow test due to the retries RLN does. Might take upwards of 1m-2m to finish. + let + invalidContractAddress = "0x0000000000000000000000000000000000000000" + keystorePath = + genTempPath("rln_keystore", "test_wakunode_relay_rln-no_valid_contract") + appInfo = RlnAppInfo + password = "1234" + + # Connect to the eth client + discard await newWeb3(EthClient) + + var serverErrorFuture = Future[string].new() + proc serverFatalErrorHandler(errMsg: string) {.gcsafe, closure, raises: [].} = + serverErrorFuture.complete(errMsg) + + var clientErrorFuture = Future[string].new() + proc clientFatalErrorHandler(errMsg: string) {.gcsafe, closure, raises: [].} = + clientErrorFuture.complete(errMsg) + + let + wakuRlnConfig1 = getWakuRlnConfigOnChain( + keystorePath, + appInfo, + invalidContractAddress, + password, + 0, + some(serverFatalErrorHandler), + ) + wakuRlnConfig2 = getWakuRlnConfigOnChain( + keystorePath, + appInfo, + invalidContractAddress, + password, + 1, + some(clientFatalErrorHandler), + ) + + # Given the node enable Relay and Rln while subscribing to a pubsub topic. + # The withTimeout call is a workaround for the test not to terminate with an exception. + # However, it doesn't reduce the retries against the blockchain that the mounting rln process attempts (until it accepts failure). + # Note: These retries might be an unintended library issue. + discard await server + .setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig1) + .withTimeout(FUTURE_TIMEOUT) + discard await client + .setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig2) + .withTimeout(FUTURE_TIMEOUT) + + check: + (await serverErrorFuture.waitForResult()).get() == + "Failed to get the storage index: No response from the Web3 provider" + (await clientErrorFuture.waitForResult()).get() == + "Failed to get the storage index: No response from the Web3 provider" + + asyncTest "Valid contract": + #[ + # Notes + ## Issues + ### TreeIndex + For some reason the calls to `getWakuRlnConfigOnChain` need to be made with `treeIndex` = 0 and 1, in that order. + But the registration needs to be made with 1 and 2. + #### Solutions + Requires investigation + ### Monkeypatching + Instead of running the idCredentials monkeypatch, passing the correct membershipIndex and keystorePath and keystorePassword should work. + #### Solutions + A) Using the register callback to fetch the correct membership + B) Using two different keystores, one for each rlnconfig. If there's only one key, it will fetch it regardless of membershipIndex. + ##### A + - Register is not calling callback even though register is happening, this should happen. + - This command should be working, but it doesn't on the current HEAD of the branch, it does work on master, which suggest there's something wrong with the branch. + - nim c -r --out:build/onchain -d:chronicles_log_level=NOTICE --verbosity:0 --hints:off -d:git_version="v0.27.0-rc.0-3-gaa9c30" -d:release --passL:librln_v0.3.7.a --passL:-lm tests/waku_rln_relay/test_rln_group_manager_onchain.nim && onchain_group_test + - All modified files are tests/*, which is a bit weird. Might be interesting re-creating the branch slowly, and checking out why this is happening. + ##### B + Untested + ]# + + let + onChainGroupManager = await setup() + contractAddress = onChainGroupManager.ethContractAddress + keystorePath = + genTempPath("rln_keystore", "test_wakunode_relay_rln-valid_contract") + appInfo = RlnAppInfo + password = "1234" + rlnInstance = onChainGroupManager.rlnInstance + assertResultOk(createAppKeystore(keystorePath, appInfo)) + + # Generate configs before registering the credentials. Otherwise the file gets cleared up. + let + wakuRlnConfig1 = + getWakuRlnConfigOnChain(keystorePath, appInfo, contractAddress, password, 0) + wakuRlnConfig2 = + getWakuRlnConfigOnChain(keystorePath, appInfo, contractAddress, password, 1) + + # Generate credentials + let + idCredential1 = rlnInstance.membershipKeyGen().get() + idCredential2 = rlnInstance.membershipKeyGen().get() + + discard await onChainGroupManager.init() + try: + # Register credentials in the chain + waitFor onChainGroupManager.register(idCredential1) + waitFor onChainGroupManager.register(idCredential2) + except Exception: + assert false, "Failed to register credentials: " & getCurrentExceptionMsg() + + # Add credentials to keystore + let + persistRes1 = addMembershipCredentialsToKeystore( + idCredential1, keystorePath, appInfo, contractAddress, password, 0 + ) + persistRes2 = addMembershipCredentialsToKeystore( + idCredential2, keystorePath, appInfo, contractAddress, password, 1 + ) + + assertResultOk(persistRes1) + assertResultOk(persistRes2) + + await onChainGroupManager.stop() + + # Given the node enables Relay and Rln while subscribing to a pubsub topic + await server.setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig1) + await client.setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig2) + + try: + (await server.wakuRlnRelay.groupManager.startGroupSync()).isOkOr: + raiseAssert $error + (await client.wakuRlnRelay.groupManager.startGroupSync()).isOkOr: + raiseAssert $error + + # Test Hack: Monkeypatch the idCredentials into the groupManager + server.wakuRlnRelay.groupManager.idCredentials = some(idCredential1) + client.wakuRlnRelay.groupManager.idCredentials = some(idCredential2) + except Exception, CatchableError: + assert false, "exception raised: " & getCurrentExceptionMsg() + + # And the nodes are connected + let serverRemotePeerInfo = server.switch.peerInfo.toRemotePeerInfo() + await client.connectToNodes(@[serverRemotePeerInfo]) + + # And the node registers the completion handler + var completionFuture = subscribeCompletionHandler(server, pubsubTopic) + + # When the client sends a valid RLN message + let isCompleted = + await sendRlnMessage(client, pubsubTopic, contentTopic, completionFuture) + + # Then the valid RLN message is relayed + check isCompleted + assertResultOk(await completionFuture.waitForResult()) + + asyncTest "Not enough gas": + let + onChainGroupManager = await setup(ethAmount = 0.u256) + contractAddress = onChainGroupManager.ethContractAddress + keystorePath = + genTempPath("rln_keystore", "test_wakunode_relay_rln-valid_contract") + appInfo = RlnAppInfo + password = "1234" + rlnInstance = onChainGroupManager.rlnInstance + assertResultOk(createAppKeystore(keystorePath, appInfo)) + + # Generate credentials + let idCredential = rlnInstance.membershipKeyGen().get() + + discard await onChainGroupManager.init() + var errorFuture = Future[string].new() + onChainGroupManager.onFatalErrorAction = proc( + errMsg: string + ) {.gcsafe, closure.} = + errorFuture.complete(errMsg) + try: + # Register credentials in the chain + waitFor onChainGroupManager.register(idCredential) + assert false, "Should have failed to register credentials given there is 0 gas" + except Exception: + assert true + + check (await errorFuture.waitForResult()).get() == + "Failed to register the member: {\"code\":-32003,\"message\":\"Insufficient funds for gas * price + value\"}" + await onChainGroupManager.stop() + + suite "RLN Relay Configuration and Parameters": + asyncTest "RLN Relay Credential Path": + let + onChainGroupManager = await setup() + contractAddress = onChainGroupManager.ethContractAddress + keystorePath = + genTempPath("rln_keystore", "test_wakunode_relay_rln-valid_contract") + appInfo = RlnAppInfo + password = "1234" + rlnInstance = onChainGroupManager.rlnInstance + assertResultOk(createAppKeystore(keystorePath, appInfo)) + + # Generate configs before registering the credentials. Otherwise the file gets cleared up. + let + wakuRlnConfig1 = + getWakuRlnConfigOnChain(keystorePath, appInfo, contractAddress, password, 0) + wakuRlnConfig2 = + getWakuRlnConfigOnChain(keystorePath, appInfo, contractAddress, password, 1) + + # Given the node enables Relay and Rln while subscribing to a pubsub topic + await server.setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig1) + await client.setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig2) + + try: + (await server.wakuRlnRelay.groupManager.startGroupSync()).isOkOr: + raiseAssert $error + (await client.wakuRlnRelay.groupManager.startGroupSync()).isOkOr: + raiseAssert $error + + # Test Hack: Monkeypatch the idCredentials into the groupManager + echo server.wakuRlnRelay.groupManager.idCredentials + echo client.wakuRlnRelay.groupManager.idCredentials + except Exception, CatchableError: + assert false, "exception raised: " & getCurrentExceptionMsg() + + # And the nodes are connected + let serverRemotePeerInfo = server.switch.peerInfo.toRemotePeerInfo() + await client.connectToNodes(@[serverRemotePeerInfo]) + + # And the node registers the completion handler + var completionFuture = subscribeCompletionHandler(server, pubsubTopic) + + # When the client attempts to send a message + try: + let isCompleted = + await sendRlnMessage(client, pubsubTopic, contentTopic, completionFuture) + assert false, "Should have failed to send a message" + except AssertionDefect as e: + # Then the message is not relayed + assert e.msg.endsWith("identity credentials are not set") + + suite "RLN Relay Resilience, Security and Compatibility": + asyncTest "Key Management and Integrity": + let + onChainGroupManager = await setup() + contractAddress = onChainGroupManager.ethContractAddress + keystorePath = + genTempPath("rln_keystore", "test_wakunode_relay_rln-valid_contract") + appInfo = RlnAppInfo + password = "1234" + rlnInstance = onChainGroupManager.rlnInstance + assertResultOk(createAppKeystore(keystorePath, appInfo)) + + # Generate configs before registering the credentials. Otherwise the file gets cleared up. + let + wakuRlnConfig1 = + getWakuRlnConfigOnChain(keystorePath, appInfo, contractAddress, password, 0) + wakuRlnConfig2 = + getWakuRlnConfigOnChain(keystorePath, appInfo, contractAddress, password, 1) + + # Generate credentials + let + idCredential1 = rlnInstance.membershipKeyGen().get() + idCredential2 = rlnInstance.membershipKeyGen().get() + + discard await onChainGroupManager.init() + try: + # Register credentials in the chain + waitFor onChainGroupManager.register(idCredential1) + waitFor onChainGroupManager.register(idCredential2) + except Exception: + assert false, "Failed to register credentials: " & getCurrentExceptionMsg() + + # Add credentials to keystore + let + persistRes1 = addMembershipCredentialsToKeystore( + idCredential1, keystorePath, appInfo, contractAddress, password, 0 + ) + persistRes2 = addMembershipCredentialsToKeystore( + idCredential2, keystorePath, appInfo, contractAddress, password, 1 + ) + + assertResultOk(persistRes1) + assertResultOk(persistRes2) + + # await onChainGroupManager.stop() + + let + registryContract = onChainGroupManager.registryContract.get() + storageIndex = (await registryContract.usingStorageIndex().call()) + rlnContractAddress = await registryContract.storages(storageIndex).call() + contract = onChainGroupManager.ethRpc.get().contractSender( + RlnStorage, rlnContractAddress + ) + contract2 = onChainGroupManager.rlnContract.get() + + echo "###" + echo await (contract.memberExists(idCredential1.idCommitment.toUInt256()).call()) + echo await (contract.memberExists(idCredential2.idCommitment.toUInt256()).call()) + echo await (contract2.memberExists(idCredential1.idCommitment.toUInt256()).call()) + echo await (contract2.memberExists(idCredential2.idCommitment.toUInt256()).call()) + echo "###" + + ################################ + ## Terminating/removing Anvil + ################################ + + # We stop Anvil daemon + stopAnvil(runAnvil) diff --git a/tests/node/test_wakunode_sharding.nim b/tests/node/test_wakunode_sharding.nim index d3f4c5e1f..e66e9e7b4 100644 --- a/tests/node/test_wakunode_sharding.nim +++ b/tests/node/test_wakunode_sharding.nim @@ -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, diff --git a/tests/testlib/assertions.nim b/tests/testlib/assertions.nim index 0d32c843a..0347839d8 100644 --- a/tests/testlib/assertions.nim +++ b/tests/testlib/assertions.nim @@ -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 = + # 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) diff --git a/tests/testlib/futures.nim b/tests/testlib/futures.nim index 4bce30128..e9a793388 100644 --- a/tests/testlib/futures.nim +++ b/tests/testlib/futures.nim @@ -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)]() diff --git a/tests/waku_filter_v2/test_waku_filter_dos_protection.nim b/tests/waku_filter_v2/test_waku_filter_dos_protection.nim index ae84d9fa0..c584d2247 100644 --- a/tests/waku_filter_v2/test_waku_filter_dos_protection.nim +++ b/tests/waku_filter_v2/test_waku_filter_dos_protection.nim @@ -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) == diff --git a/tests/waku_keystore/utils.nim b/tests/waku_keystore/utils.nim new file mode 100644 index 000000000..8af2d2a26 --- /dev/null +++ b/tests/waku_keystore/utils.nim @@ -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.} diff --git a/tests/waku_rln_relay/rln/test_wrappers.nim b/tests/waku_rln_relay/rln/test_wrappers.nim index fd8e1a29f..26e18f9da 100644 --- a/tests/waku_rln_relay/rln/test_wrappers.nim +++ b/tests/waku_rln_relay/rln/test_wrappers.nim @@ -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]) @@ -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]) diff --git a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim index 976dad835..5ef6913f7 100644 --- a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim +++ b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim @@ -10,9 +10,9 @@ import chronicles, stint, web3, - json, libp2p/crypto/crypto, eth/keys + import waku/[ waku_node, @@ -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 -# -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() @@ -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") diff --git a/tests/waku_rln_relay/test_rln_serde.nim b/tests/waku_rln_relay/test_rln_serde.nim index a41d348b8..88badce97 100644 --- a/tests/waku_rln_relay/test_rln_serde.nim +++ b/tests/waku_rln_relay/test_rln_serde.nim @@ -2,34 +2,21 @@ {.push raises: [].} +import stew/results, stint + import ./rln/waku_rln_relay_utils, - waku/[waku_keystore/protocol_types, waku_rln_relay, waku_rln_relay/rln] + waku/[ + waku_keystore/protocol_types, + waku_rln_relay, + waku_rln_relay/rln, + waku_rln_relay/protocol_types, + ], + ../waku_keystore/utils, + testutils/unittests -import testutils/unittests -import stew/results, stint from std/times import epochTime -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" - ), - ) - func defaultRateCommitment*(): RateCommitment = let idCredential = defaultIdentityCredential() return RateCommitment(idCommitment: idCredential.idCommitment, userMessageLimit: 100) diff --git a/tests/waku_rln_relay/test_wakunode_rln_relay.nim b/tests/waku_rln_relay/test_wakunode_rln_relay.nim index 75ff65aa4..e227a0bb7 100644 --- a/tests/waku_rln_relay/test_wakunode_rln_relay.nim +++ b/tests/waku_rln_relay/test_wakunode_rln_relay.nim @@ -11,12 +11,27 @@ import libp2p/protocols/pubsub/pubsub import waku/[waku_core, waku_node, waku_rln_relay], - ../testlib/wakucore, - ../testlib/wakunode, + ../testlib/[wakucore, futures, wakunode], ./rln/waku_rln_relay_utils from std/times import epochTime +proc buildWakuRlnConfig( + credIndex: uint, + epochSizeSec: uint64, + treeFilename: string, + userMessageLimit: uint64 = 1, +): WakuRlnConfig = + let treePath = genTempPath("rln_tree", treeFilename) + # Off-chain + return WakuRlnConfig( + rlnRelayDynamic: false, + rlnRelayCredIndex: some(credIndex.uint), + rlnRelayUserMessageLimit: userMessageLimit, + rlnEpochSizeSec: epochSizeSec, + rlnRelayTreePath: treePath, + ) + procSuite "WakuNode - RLN relay": # NOTE: we set the rlnRelayUserMessageLimit to 1 to make the tests easier to reason about asyncTest "testing rln-relay with valid proof": @@ -467,78 +482,47 @@ procSuite "WakuNode - RLN relay": await node3.stop() asyncTest "clearNullifierLog: should clear epochs > MaxEpochGap": + # Given two nodes let - # publisher node + contentTopic = ContentTopic("/waku/2/default-content/proto") + pubsubTopicSeq = @[DefaultPubsubTopic] nodeKey1 = generateSecp256k1Key() node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), Port(0)) - # Relay node nodeKey2 = generateSecp256k1Key() node2 = newTestWakuNode(nodeKey2, parseIpAddress("0.0.0.0"), Port(0)) - # Subscriber - nodeKey3 = generateSecp256k1Key() - node3 = newTestWakuNode(nodeKey3, parseIpAddress("0.0.0.0"), Port(0)) - - contentTopic = ContentTopic("/waku/2/default-content/proto") - - # set up 2 nodes - # node1 - await node1.mountRelay(@[DefaultPubsubTopic]) - - # mount rlnrelay in off-chain mode - let wakuRlnConfig1 = WakuRlnConfig( - rlnRelayDynamic: false, - rlnRelayCredIndex: some(1.uint), - rlnRelayUserMessageLimit: 1, - rlnEpochSizeSec: 1, - rlnRelayTreePath: genTempPath("rln_tree", "wakunode_10"), - ) + epochSizeSec: uint64 = 5 # This means rlnMaxEpochGap = 4 + # Given both nodes mount relay and rlnrelay + await node1.mountRelay(pubsubTopicSeq) + let wakuRlnConfig1 = buildWakuRlnConfig(1, epochSizeSec, "wakunode_10") await node1.mountRlnRelay(wakuRlnConfig1) - await node1.start() - - # node 2 + # Mount rlnrelay in node2 in off-chain mode await node2.mountRelay(@[DefaultPubsubTopic]) - - # mount rlnrelay in off-chain mode - let wakuRlnConfig2 = WakuRlnConfig( - rlnRelayDynamic: false, - rlnRelayCredIndex: some(2.uint), - rlnRelayUserMessageLimit: 1, - rlnEpochSizeSec: 1, - rlnRelayTreePath: genTempPath("rln_tree", "wakunode_11"), - ) - + let wakuRlnConfig2 = buildWakuRlnConfig(2, epochSizeSec, "wakunode_11") await node2.mountRlnRelay(wakuRlnConfig2) - await node2.start() - + # Given the two nodes are started and connected + waitFor allFutures(node1.start(), node2.start()) await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) - # get the current epoch time - let time = epochTime() - # create some messages with rate limit proofs + # Given some messages var wm1 = WakuMessage(payload: "message 1".toBytes(), contentTopic: contentTopic) - # another message in the same epoch as wm1, it will break the messaging rate limit wm2 = WakuMessage(payload: "message 2".toBytes(), contentTopic: contentTopic) - # wm3 points to the next epoch wm3 = WakuMessage(payload: "message 3".toBytes(), contentTopic: contentTopic) + wm4 = WakuMessage(payload: "message 4".toBytes(), contentTopic: contentTopic) + wm5 = WakuMessage(payload: "message 5".toBytes(), contentTopic: contentTopic) + wm6 = WakuMessage(payload: "message 6".toBytes(), contentTopic: contentTopic) - node1.wakuRlnRelay.unsafeAppendRLNProof(wm1, time).isOkOr: - raiseAssert $error - node1.wakuRlnRelay.unsafeAppendRLNProof(wm2, time).isOkOr: - raiseAssert $error - - node1.wakuRlnRelay.unsafeAppendRLNProof( - wm3, time + float64(node1.wakuRlnRelay.rlnEpochSizeSec * 2) - ).isOkOr: - raiseAssert $error - - # relay handler for node2 - var completionFut1 = newFuture[bool]() - var completionFut2 = newFuture[bool]() - var completionFut3 = newFuture[bool]() + # And node2 mounts a relay handler that completes the respective future when a message is received + var + completionFut1 = newFuture[bool]() + completionFut2 = newFuture[bool]() + completionFut3 = newFuture[bool]() + completionFut4 = newFuture[bool]() + completionFut5 = newFuture[bool]() + completionFut6 = newFuture[bool]() proc relayHandler( topic: PubsubTopic, msg: WakuMessage ): Future[void] {.async, gcsafe.} = @@ -550,25 +534,133 @@ procSuite "WakuNode - RLN relay": completionFut2.complete(true) if msg == wm3: completionFut3.complete(true) + if msg == wm4: + completionFut4.complete(true) + if msg == wm5: + completionFut5.complete(true) + if msg == wm6: + completionFut6.complete(true) - # mount the relay handler for node2 node2.subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), some(relayHandler)) - await sleepAsync(2000.millis) + # Given all messages have an rln proof and are published by the node 1 + let publishSleepDuration: Duration = 5000.millis + let time = epochTime() + + # Epoch 1 + node1.wakuRlnRelay.unsafeAppendRLNProof(wm1, time).isOkOr: + raiseAssert $error + # Message wm2 is published in the same epoch as wm1, so it'll be considered spam + node1.wakuRlnRelay.unsafeAppendRLNProof(wm2, time).isOkOr: + raiseAssert $error discard await node1.publish(some(DefaultPubsubTopic), wm1) discard await node1.publish(some(DefaultPubsubTopic), wm2) - discard await node1.publish(some(DefaultPubsubTopic), wm3) - - let - res1 = await completionFut1.withTimeout(10.seconds) - res2 = await completionFut2.withTimeout(10.seconds) - res3 = await completionFut3.withTimeout(10.seconds) - + await sleepAsync(publishSleepDuration) check: - res1 == true - res2 == false - res3 == true + node1.wakuRlnRelay.nullifierLog.len() == 0 + node2.wakuRlnRelay.nullifierLog.len() == 1 + + # Epoch 2 + node1.wakuRlnRelay.unsafeAppendRLNProof(wm3, epochTime()).isOkOr: + raiseAssert $error + discard await node1.publish(some(DefaultPubsubTopic), wm3) + await sleepAsync(publishSleepDuration) + check: + node1.wakuRlnRelay.nullifierLog.len() == 0 node2.wakuRlnRelay.nullifierLog.len() == 2 - await node1.stop() - await node2.stop() + # Epoch 3 + node1.wakuRlnRelay.unsafeAppendRLNProof(wm4, epochTime()).isOkOr: + raiseAssert $error + discard await node1.publish(some(DefaultPubsubTopic), wm4) + await sleepAsync(publishSleepDuration) + check: + node1.wakuRlnRelay.nullifierLog.len() == 0 + node2.wakuRlnRelay.nullifierLog.len() == 3 + + # Epoch 4 + node1.wakuRlnRelay.unsafeAppendRLNProof(wm5, epochTime()).isOkOr: + raiseAssert $error + discard await node1.publish(some(DefaultPubsubTopic), wm5) + await sleepAsync(publishSleepDuration) + check: + node1.wakuRlnRelay.nullifierLog.len() == 0 + node2.wakuRlnRelay.nullifierLog.len() == 4 + + # Epoch 5 + node1.wakuRlnRelay.unsafeAppendRLNProof(wm6, epochTime()).isOkOr: + raiseAssert $error + discard await node1.publish(some(DefaultPubsubTopic), wm6) + await sleepAsync(publishSleepDuration) + check: + node1.wakuRlnRelay.nullifierLog.len() == 0 + node2.wakuRlnRelay.nullifierLog.len() == 4 + + # Then the node 2 should have cleared the nullifier log for epochs > MaxEpochGap + # Therefore, with 4 max epochs, the first 4 messages will be published (except wm2, which shares epoch with wm1) + check: + (await completionFut1.waitForResult()).value() == true + (await completionFut2.waitForResult()).isErr() + (await completionFut3.waitForResult()).value() == true + (await completionFut4.waitForResult()).value() == true + (await completionFut5.waitForResult()).value() == true + (await completionFut6.waitForResult()).value() == true + + # Cleanup + waitFor allFutures(node1.stop(), node2.stop()) + + asyncTest "Spam Detection and Slashing (currently gossipsub score decrease)": + # Given two nodes + let + contentTopic = ContentTopic("/waku/2/default-content/proto") + pubsubTopicSeq = @[DefaultPubsubTopic] + nodeKey1 = generateSecp256k1Key() + node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), Port(0)) + nodeKey2 = generateSecp256k1Key() + node2 = newTestWakuNode(nodeKey2, parseIpAddress("0.0.0.0"), Port(0)) + epochSizeSec: uint64 = 5 # This means rlnMaxEpochGap = 4 + + # Given both nodes mount relay and rlnrelay + # Mount rlnrelay in node1 in off-chain mode + await node1.mountRelay(pubsubTopicSeq) + let wakuRlnConfig1 = buildWakuRlnConfig(1, epochSizeSec, "wakunode_10") + await node1.mountRlnRelay(wakuRlnConfig1) + + # Mount rlnrelay in node2 in off-chain mode + await node2.mountRelay(@[DefaultPubsubTopic]) + let wakuRlnConfig2 = buildWakuRlnConfig(2, epochSizeSec, "wakunode_11") + await node2.mountRlnRelay(wakuRlnConfig2) + + # Given the two nodes are started and connected + waitFor allFutures(node1.start(), node2.start()) + await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) + + # Given some messages with rln proofs + let time = epochTime() + var + msg1 = WakuMessage(payload: "message 1".toBytes(), contentTopic: contentTopic) + msg2 = WakuMessage(payload: "message 2".toBytes(), contentTopic: contentTopic) + + node1.wakuRlnRelay.unsafeAppendRLNProof(msg1, time).isOkOr: + raiseAssert $error + # Message wm2 is published in the same epoch as wm1, so it'll be considered spam + node1.wakuRlnRelay.unsafeAppendRLNProof(msg2, time).isOkOr: + raiseAssert $error + + # When publishing the first message (valid) + discard await node1.publish(some(DefaultPubsubTopic), msg1) + await sleepAsync(FUTURE_TIMEOUT_SCORING) # Wait for scoring + + # Then the score of node2 should increase + check: + node1.wakuRelay.peerStats[node2.switch.peerInfo.peerId].score == 0.1 + node2.wakuRelay.peerStats[node1.switch.peerInfo.peerId].score == 1.1 + + # When publishing the second message (spam) + discard await node1.publish(some(DefaultPubsubTopic), msg2) + await sleepAsync(FUTURE_TIMEOUT_SCORING) + + # Then the score of node2 should decrease + check: + node1.wakuRelay.peerStats[node2.switch.peerInfo.peerId].score == 0.1 + node2.wakuRelay.peerStats[node1.switch.peerInfo.peerId].score == -99.4 diff --git a/tests/waku_rln_relay/utils_onchain.nim b/tests/waku_rln_relay/utils_onchain.nim new file mode 100644 index 000000000..272ddffa6 --- /dev/null +++ b/tests/waku_rln_relay/utils_onchain.nim @@ -0,0 +1,226 @@ +{.used.} + +{.push raises: [].} + +import + std/[options, os, osproc, sequtils, deques, streams, strutils, tempfiles, strformat], + stew/[results, byteutils], + testutils/unittests, + chronos, + chronicles, + stint, + web3, + json, + libp2p/crypto/crypto, + eth/keys + +import + waku/[ + waku_rln_relay, + waku_rln_relay/protocol_types, + waku_rln_relay/constants, + waku_rln_relay/contract, + waku_rln_relay/rln, + ], + ../testlib/common, + ./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 +# +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*( + ethAmount: UInt256 = 1000.u256 +): 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(ethAmount)) + tx.to = some(acc) + tx.gasPrice = some(gasPrice) + + # Send ethAmount to acc + discard await web3.send(tx) + let balance = await web3.provider.eth_getBalance(acc, "latest") + assert balance == ethToWei(ethAmount), + fmt"Balance is {balance} but expected {ethToWei(ethAmount)}" + + 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*(port: int = 8540, chainId: string = "1337"): 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:" & $port): + 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*( + ethClientAddress: string = EthClient, ethAmount: UInt256 = 10.u256 +): 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(ethClientAddress) + # connect to the eth client + let web3 = await newWeb3(ethClientAddress) + + let accounts = await web3.provider.eth_accounts() + web3.defaultAccount = accounts[0] + + var pk = none(string) + let (privateKey, _) = await createEthAccount(ethAmount) + pk = some($privateKey) + + let manager = OnchainGroupManager( + ethClientUrl: ethClientAddress, + ethContractAddress: $contractAddress, + chainId: CHAIN_ID, + ethPrivateKey: pk, + rlnInstance: rlnInstance, + onFatalErrorAction: proc(errStr: string) = + raiseAssert errStr + , + ) + + return manager + +{.pop.} diff --git a/tests/waku_rln_relay/utils_static.nim b/tests/waku_rln_relay/utils_static.nim new file mode 100644 index 000000000..00f29c704 --- /dev/null +++ b/tests/waku_rln_relay/utils_static.nim @@ -0,0 +1,86 @@ +{.used.} + +import + std/[sequtils, tempfiles], + stew/byteutils, + stew/shims/net as stewNet, + chronos, + libp2p/switch, + libp2p/protocols/pubsub/pubsub + +from std/times import epochTime + +import + ../../../waku/ + [node/waku_node, node/peer_manager, waku_core, waku_node, waku_rln_relay], + ../waku_store/store_utils, + ../waku_archive/archive_utils, + ../testlib/[wakucore, futures, assertions] + +proc setupStaticRln*( + node: WakuNode, + identifier: uint, + rlnRelayEthContractAddress: Option[string] = none(string), +) {.async.} = + await node.mountRlnRelay( + WakuRlnConfig( + rlnRelayDynamic: false, + rlnRelayCredIndex: some(identifier), + rlnRelayTreePath: genTempPath("rln_tree", "wakunode_" & $identifier), + rlnEpochSizeSec: 1, + ) + ) + +proc setupRelayWithStaticRln*( + node: WakuNode, identifier: uint, pubsubTopics: seq[string] +) {.async.} = + await node.mountRelay(pubsubTopics) + await setupStaticRln(node, identifier) + +proc subscribeCompletionHandler*(node: WakuNode, pubsubTopic: string): Future[bool] = + var completionFut = newFuture[bool]() + proc relayHandler( + topic: PubsubTopic, msg: WakuMessage + ): Future[void] {.async, gcsafe.} = + if topic == pubsubTopic: + completionFut.complete(true) + + node.subscribe((kind: PubsubSub, topic: pubsubTopic), some(relayHandler)) + return completionFut + +proc sendRlnMessage*( + client: WakuNode, + pubsubTopic: string, + contentTopic: string, + completionFuture: Future[bool], + payload: seq[byte] = "Hello".toBytes(), +): Future[bool] {.async.} = + var message = WakuMessage(payload: payload, contentTopic: contentTopic) + let appendResult = client.wakuRlnRelay.appendRLNProof(message, epochTime()) + # Assignment required or crashess + assertResultOk(appendResult) + discard await client.publish(some(pubsubTopic), message) + let isCompleted = await completionFuture.withTimeout(FUTURE_TIMEOUT) + return isCompleted + +proc sendRlnMessageWithInvalidProof*( + client: WakuNode, + pubsubTopic: string, + contentTopic: string, + completionFuture: Future[bool], + payload: seq[byte] = "Hello".toBytes(), +): Future[bool] {.async.} = + let + extraBytes: seq[byte] = @[byte(1), 2, 3] + rateLimitProofRes = client.wakuRlnRelay.groupManager.generateProof( + concat(payload, extraBytes), + # we add extra bytes to invalidate proof verification against original payload + client.wakuRlnRelay.getCurrentEpoch(), + ) + rateLimitProof = rateLimitProofRes.get().encode().buffer + message = + WakuMessage(payload: @payload, contentTopic: contentTopic, proof: rateLimitProof) + + discard await client.publish(some(pubsubTopic), message) + let isCompleted = await completionFuture.withTimeout(FUTURE_TIMEOUT) + return isCompleted