test(rln): Implement rln tests (#2639)

* Implement tests.
* Clean coding.
This commit is contained in:
Álex 2024-08-02 16:43:22 +02:00 committed by GitHub
parent ebda56dd90
commit a3fa175054
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1066 additions and 309 deletions

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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)]()

View File

@ -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) ==

View File

@ -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.}

View File

@ -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])

View File

@ -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
# <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()
@ -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")

View File

@ -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)

View File

@ -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

View File

@ -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
# <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*(
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.}

View File

@ -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