feat: Update implementation for new contract abi (#3390)

* update RLN contract abi functions and procs

* Clean up debugging lines

* Use more descriptive object field names for MembershipInfo

* Fix formatting

* fix group_manager after rebase to use new contract method sig

* Fix linting for group_manager.nim

* Test idcommitment to BE and debug logs

* Improve IdCommitment logging

* Update all keystore credentials to use BE format

* Add workaround for groupmanager web3 eth_call

* Add await to sendEthCallWithChainID

* Add error handling for failed eth_call

* Improve error handling for eth_call workaround

* Revert keystore credentials back to using LE

* Update toRateCommitment proc to use LE instead of BE

* Add IdCommitment to calldata as BE

* feat: Update rln contract deployment and tests (#3408)

* update RLN contract abi functions and procs

* update waku-rlnv2-contract submodule commit to latest

* Add RlnV2 contract deployment using forge scripts

* Clean up output of forge script command, debug logs to trace, warn to error

* Move TestToken deployment to own proc

* first implementation of token minting and approval

* Update rln tests with usermessagelimit new minimum

* Clean up code and error handling

* Rework RLN tests WIP

* Fix RLN test for new contract

* RLN Tests updated

* Fix formatting

* Improve error logs

* Fix error message formatting

* Fix linting

* Add pnpm dependency installation for rln tests

* Update test dependencies in makefile

* Minor updates, error messages etc

* Code cleanup and change some debug logging to trace

* Improve handling of Result return value

* Use absolute path for waku-rlnv2-contract

* Simplify token approval and balance check

* Remove unused Anvil options

* Add additional checks for stopAnvil process

* Fix anvil process call to null

* Add lock to tests for rln_group_manager_onchain

* Debug for forge command

* Verify paths

* Install pnpm as global

* Cleanup anvil running procs

* Add check before installing anvil

* CLean up onchain group_manager

* Add proc to setup environment for contract deployer

* Refactoring and improved error handling

* Fix anvil install directory string

* Fix linting in test_range_split

* Add const for the contract address length

* Add separate checks for why Approval transaction fails

* Update RLN contract address and chainID for TWN
This commit is contained in:
Tanya S 2025-06-20 11:46:08 +02:00 committed by GitHub
parent 4277a53490
commit ee4058b2d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 687 additions and 234 deletions

View File

@ -112,11 +112,8 @@ ifeq (, $(shell which cargo))
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable
endif
anvil: rustup
ifeq (, $(shell which anvil 2> /dev/null))
# Install Anvil if it's not installed
./scripts/install_anvil.sh
endif
rln-deps: rustup
./scripts/install_rln_tests_dependencies.sh
deps: | deps-common nat-libs waku.nims
@ -205,8 +202,8 @@ testcommon: | build deps
##########
.PHONY: testwaku wakunode2 testwakunode2 example2 chat2 chat2bridge liteprotocoltester
# install anvil only for the testwaku target
testwaku: | build deps anvil librln
# install rln-deps only for the testwaku target
testwaku: | build deps rln-deps librln
echo -e $(BUILD_MSG) "build/$@" && \
$(ENV_SCRIPT) nim test -d:os=$(shell uname) $(NIM_PARAMS) waku.nims

View File

@ -9,7 +9,7 @@ x-logging: &logging
x-rln-relay-eth-client-address: &rln_relay_eth_client_address ${RLN_RELAY_ETH_CLIENT_ADDRESS:-} # Add your RLN_RELAY_ETH_CLIENT_ADDRESS after the "-"
x-rln-environment: &rln_env
RLN_RELAY_CONTRACT_ADDRESS: ${RLN_RELAY_CONTRACT_ADDRESS:-0xfe7a9eabcE779a090FD702346Fd0bFAc02ce6Ac8}
RLN_RELAY_CONTRACT_ADDRESS: ${RLN_RELAY_CONTRACT_ADDRESS:-0xB9cd878C90E49F797B4431fBF4fb333108CB90e6}
RLN_RELAY_CRED_PATH: ${RLN_RELAY_CRED_PATH:-} # Optional: Add your RLN_RELAY_CRED_PATH after the "-"
RLN_RELAY_CRED_PASSWORD: ${RLN_RELAY_CRED_PASSWORD:-} # Optional: Add your RLN_RELAY_CRED_PASSWORD after the "-"

View File

@ -24,7 +24,7 @@ fi
docker run -v $(pwd)/keystore:/keystore/:Z harbor.status.im/wakuorg/nwaku:v0.30.1 generateRlnKeystore \
--rln-relay-eth-client-address=${RLN_RELAY_ETH_CLIENT_ADDRESS} \
--rln-relay-eth-private-key=${ETH_TESTNET_KEY} \
--rln-relay-eth-contract-address=0xfe7a9eabcE779a090FD702346Fd0bFAc02ce6Ac8 \
--rln-relay-eth-contract-address=0xB9cd878C90E49F797B4431fBF4fb333108CB90e6 \
--rln-relay-cred-path=/keystore/keystore.json \
--rln-relay-cred-password="${RLN_RELAY_CRED_PASSWORD}" \
--rln-relay-user-message-limit=20 \

View File

@ -2,13 +2,14 @@
# Install Anvil
if ! command -v anvil &> /dev/null; then
BASE_DIR="${XDG_CONFIG_HOME:-$HOME}"
FOUNDRY_DIR="${FOUNDRY_DIR:-"$BASE_DIR/.foundry"}"
FOUNDRY_BIN_DIR="$FOUNDRY_DIR/bin"
BASE_DIR="${XDG_CONFIG_HOME:-$HOME}"
FOUNDRY_DIR="${FOUNDRY_DIR-"$BASE_DIR/.foundry"}"
FOUNDRY_BIN_DIR="$FOUNDRY_DIR/bin"
curl -L https://foundry.paradigm.xyz | bash
# Extract the source path from the download result
echo "foundryup_path: $FOUNDRY_BIN_DIR"
# run foundryup
$FOUNDRY_BIN_DIR/foundryup
curl -L https://foundry.paradigm.xyz | bash
# Extract the source path from the download result
echo "foundryup_path: $FOUNDRY_BIN_DIR"
# run foundryup
$FOUNDRY_BIN_DIR/foundryup
fi

8
scripts/install_pnpm.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Install pnpm
if ! command -v pnpm &> /dev/null; then
echo "pnpm is not installed, installing it now..."
npm i pnpm --global
fi

View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Install Anvil
./scripts/install_anvil.sh
#Install pnpm
./scripts/install_pnpm.sh

View File

@ -3,7 +3,7 @@
{.push raises: [].}
import
std/[options, sequtils, deques, random],
std/[options, sequtils, deques, random, locks],
results,
stew/byteutils,
testutils/unittests,
@ -28,58 +28,71 @@ import
../testlib/wakucore,
./utils_onchain
var testLock: Lock
initLock(testLock)
suite "Onchain group manager":
# We run Anvil
let runAnvil {.used.} = runAnvil()
setup:
# Acquire lock to ensure tests run sequentially
acquire(testLock)
var manager {.threadvar.}: OnchainGroupManager
let runAnvil {.used.} = runAnvil()
asyncSetup:
manager = await setupOnchainGroupManager()
var manager {.threadvar.}: OnchainGroupManager
manager = waitFor setupOnchainGroupManager()
asyncTeardown:
await manager.stop()
teardown:
waitFor manager.stop()
stopAnvil(runAnvil)
# Release lock after test completes
release(testLock)
asyncTest "should initialize successfully":
(await manager.init()).isOkOr:
test "should initialize successfully":
(waitFor manager.init()).isOkOr:
raiseAssert $error
check:
manager.ethRpc.isSome()
manager.wakuRlnContract.isSome()
manager.initialized
manager.rlnRelayMaxMessageLimit == 100
manager.rlnRelayMaxMessageLimit == 600
asyncTest "should error on initialization when chainId does not match":
test "should error on initialization when chainId does not match":
manager.chainId = utils_onchain.CHAIN_ID + 1
(await manager.init()).isErrOr:
(waitFor manager.init()).isErrOr:
raiseAssert "Expected error when chainId does not match"
asyncTest "should initialize when chainId is set to 0":
test "should initialize when chainId is set to 0":
manager.chainId = 0x0'u256
(await manager.init()).isOkOr:
(waitFor manager.init()).isOkOr:
raiseAssert $error
asyncTest "should error on initialization when loaded metadata does not match":
(await manager.init()).isOkOr:
test "should error on initialization when loaded metadata does not match":
(waitFor manager.init()).isOkOr:
assert false, $error
let metadataSetRes = manager.setMetadata()
assert metadataSetRes.isOk(), metadataSetRes.error
let metadataOpt = manager.rlnInstance.getMetadata().valueOr:
assert false, $error
return
assert metadataOpt.isSome(), "metadata is not set"
let metadata = metadataOpt.get()
assert metadata.chainId == 1337, "chainId is not equal to 1337"
assert metadata.chainId == 1234, "chainId is not equal to 1234"
assert metadata.contractAddress == manager.ethContractAddress,
"contractAddress is not equal to " & manager.ethContractAddress
let differentContractAddress = await uploadRLNContract(manager.ethClientUrls[0])
let web3 = manager.ethRpc.get()
let accounts = waitFor web3.provider.eth_accounts()
web3.defaultAccount = accounts[2]
let (privateKey, acc) = createEthAccount(web3)
let tokenAddress = (waitFor deployTestToken(privateKey, acc, web3)).valueOr:
assert false, "Failed to deploy test token contract: " & $error
return
let differentContractAddress = (
waitFor executeForgeContractDeployScripts(privateKey, acc, web3)
).valueOr:
assert false, "Failed to deploy RLN contract: " & $error
return
# simulating a change in the contractAddress
let manager2 = OnchainGroupManager(
ethClientUrls: @[EthClient],
@ -89,52 +102,47 @@ suite "Onchain group manager":
assert false, errStr
,
)
let e = await manager2.init()
let e = waitFor manager2.init()
(e).isErrOr:
assert false, "Expected error when contract address doesn't match"
asyncTest "should error if contract does not exist":
test "should error if contract does not exist":
manager.ethContractAddress = "0x0000000000000000000000000000000000000000"
var triggeredError = false
try:
discard await manager.init()
except CatchableError:
triggeredError = true
(waitFor manager.init()).isErrOr:
raiseAssert "Expected error when contract address doesn't exist"
check triggeredError
asyncTest "should error when keystore path and password are provided but file doesn't exist":
test "should error when keystore path and password are provided but file doesn't exist":
manager.keystorePath = some("/inexistent/file")
manager.keystorePassword = some("password")
(await manager.init()).isErrOr:
(waitFor manager.init()).isErrOr:
raiseAssert "Expected error when keystore file doesn't exist"
asyncTest "trackRootChanges: start tracking roots":
(await manager.init()).isOkOr:
test "trackRootChanges: start tracking roots":
(waitFor manager.init()).isOkOr:
raiseAssert $error
discard manager.trackRootChanges()
asyncTest "trackRootChanges: should guard against uninitialized state":
test "trackRootChanges: should guard against uninitialized state":
try:
discard manager.trackRootChanges()
except CatchableError:
check getCurrentExceptionMsg().len == 38
asyncTest "trackRootChanges: should sync to the state of the group":
test "trackRootChanges: should sync to the state of the group":
let credentials = generateCredentials(manager.rlnInstance)
(await manager.init()).isOkOr:
(waitFor manager.init()).isOkOr:
raiseAssert $error
let merkleRootBefore = manager.fetchMerkleRoot()
try:
await manager.register(credentials, UserMessageLimit(1))
waitFor manager.register(credentials, UserMessageLimit(20))
except Exception, CatchableError:
assert false, "exception raised: " & getCurrentExceptionMsg()
discard await withTimeout(trackRootChanges(manager), 15.seconds)
discard waitFor withTimeout(trackRootChanges(manager), 15.seconds)
let merkleRootAfter = manager.fetchMerkleRoot()
@ -151,7 +159,7 @@ suite "Onchain group manager":
metadata.validRoots == manager.validRoots.toSeq()
merkleRootBefore != merkleRootAfter
asyncTest "trackRootChanges: should fetch history correctly":
test "trackRootChanges: should fetch history correctly":
# TODO: We can't use `trackRootChanges()` directly in this test because its current implementation
# relies on a busy loop rather than event-based monitoring. As a result, some root changes
# may be missed, leading to inconsistent test results (i.e., it may randomly return true or false).
@ -159,15 +167,16 @@ suite "Onchain group manager":
# after each registration.
const credentialCount = 6
let credentials = generateCredentials(manager.rlnInstance, credentialCount)
(await manager.init()).isOkOr:
(waitFor manager.init()).isOkOr:
raiseAssert $error
let merkleRootBefore = manager.fetchMerkleRoot()
try:
for i in 0 ..< credentials.len():
await manager.register(credentials[i], UserMessageLimit(1))
discard await manager.updateRoots()
debug "Registering credential", index = i, credential = credentials[i]
waitFor manager.register(credentials[i], UserMessageLimit(20))
discard waitFor manager.updateRoots()
except Exception, CatchableError:
assert false, "exception raised: " & getCurrentExceptionMsg()
@ -177,13 +186,13 @@ suite "Onchain group manager":
merkleRootBefore != merkleRootAfter
manager.validRoots.len() == credentialCount
asyncTest "register: should guard against uninitialized state":
test "register: should guard against uninitialized state":
let dummyCommitment = default(IDCommitment)
try:
await manager.register(
waitFor manager.register(
RateCommitment(
idCommitment: dummyCommitment, userMessageLimit: UserMessageLimit(1)
idCommitment: dummyCommitment, userMessageLimit: UserMessageLimit(20)
)
)
except CatchableError:
@ -191,18 +200,18 @@ suite "Onchain group manager":
except Exception:
assert false, "exception raised: " & getCurrentExceptionMsg()
asyncTest "register: should register successfully":
test "register: should register successfully":
# TODO :- similar to ```trackRootChanges: should fetch history correctly```
(await manager.init()).isOkOr:
(waitFor manager.init()).isOkOr:
raiseAssert $error
let idCommitment = generateCredentials(manager.rlnInstance).idCommitment
let merkleRootBefore = manager.fetchMerkleRoot()
try:
await manager.register(
waitFor manager.register(
RateCommitment(
idCommitment: idCommitment, userMessageLimit: UserMessageLimit(1)
idCommitment: idCommitment, userMessageLimit: UserMessageLimit(20)
)
)
except Exception, CatchableError:
@ -215,47 +224,47 @@ suite "Onchain group manager":
merkleRootAfter != merkleRootBefore
manager.latestIndex == 1
asyncTest "register: callback is called":
test "register: callback is called":
let idCredentials = generateCredentials(manager.rlnInstance)
let idCommitment = idCredentials.idCommitment
let fut = newFuture[void]()
proc callback(registrations: seq[Membership]): Future[void] {.async.} =
let rateCommitment = getRateCommitment(idCredentials, UserMessageLimit(1)).get()
let rateCommitment = getRateCommitment(idCredentials, UserMessageLimit(20)).get()
check:
registrations.len == 1
registrations[0].rateCommitment == rateCommitment
registrations[0].index == 0
fut.complete()
(await manager.init()).isOkOr:
(waitFor manager.init()).isOkOr:
raiseAssert $error
manager.onRegister(callback)
try:
await manager.register(
waitFor manager.register(
RateCommitment(
idCommitment: idCommitment, userMessageLimit: UserMessageLimit(1)
idCommitment: idCommitment, userMessageLimit: UserMessageLimit(20)
)
)
except Exception, CatchableError:
assert false, "exception raised: " & getCurrentExceptionMsg()
await fut
waitFor fut
asyncTest "withdraw: should guard against uninitialized state":
test "withdraw: should guard against uninitialized state":
let idSecretHash = generateCredentials(manager.rlnInstance).idSecretHash
try:
await manager.withdraw(idSecretHash)
waitFor manager.withdraw(idSecretHash)
except CatchableError:
assert true
except Exception:
assert false, "exception raised: " & getCurrentExceptionMsg()
asyncTest "validateRoot: should validate good root":
test "validateRoot: should validate good root":
let idCredentials = generateCredentials(manager.rlnInstance)
let idCommitment = idCredentials.idCommitment
@ -264,27 +273,27 @@ suite "Onchain group manager":
proc callback(registrations: seq[Membership]): Future[void] {.async.} =
if registrations.len == 1 and
registrations[0].rateCommitment ==
getRateCommitment(idCredentials, UserMessageLimit(1)).get() and
getRateCommitment(idCredentials, UserMessageLimit(20)).get() and
registrations[0].index == 0:
manager.idCredentials = some(idCredentials)
fut.complete()
manager.onRegister(callback)
(await manager.init()).isOkOr:
(waitFor manager.init()).isOkOr:
raiseAssert $error
try:
await manager.register(idCredentials, UserMessageLimit(1))
waitFor manager.register(idCredentials, UserMessageLimit(20))
except Exception, CatchableError:
assert false, "exception raised: " & getCurrentExceptionMsg()
await fut
waitFor fut
let rootUpdated = await manager.updateRoots()
let rootUpdated = waitFor manager.updateRoots()
if rootUpdated:
let proofResult = await manager.fetchMerkleProofElements()
let proofResult = waitFor manager.fetchMerkleProofElements()
if proofResult.isErr():
error "Failed to fetch Merkle proof", error = proofResult.error
manager.merkleProofCache = proofResult.get()
@ -306,14 +315,14 @@ suite "Onchain group manager":
check:
validated
asyncTest "validateRoot: should reject bad root":
test "validateRoot: should reject bad root":
let idCredentials = generateCredentials(manager.rlnInstance)
let idCommitment = idCredentials.idCommitment
(await manager.init()).isOkOr:
(waitFor manager.init()).isOkOr:
raiseAssert $error
manager.userMessageLimit = some(UserMessageLimit(1))
manager.userMessageLimit = some(UserMessageLimit(20))
manager.membershipIndex = some(MembershipIndex(0))
manager.idCredentials = some(idCredentials)
@ -339,9 +348,9 @@ suite "Onchain group manager":
check:
validated == false
asyncTest "verifyProof: should verify valid proof":
test "verifyProof: should verify valid proof":
let credentials = generateCredentials(manager.rlnInstance)
(await manager.init()).isOkOr:
(waitFor manager.init()).isOkOr:
raiseAssert $error
let fut = newFuture[void]()
@ -349,7 +358,7 @@ suite "Onchain group manager":
proc callback(registrations: seq[Membership]): Future[void] {.async.} =
if registrations.len == 1 and
registrations[0].rateCommitment ==
getRateCommitment(credentials, UserMessageLimit(1)).get() and
getRateCommitment(credentials, UserMessageLimit(20)).get() and
registrations[0].index == 0:
manager.idCredentials = some(credentials)
fut.complete()
@ -357,15 +366,15 @@ suite "Onchain group manager":
manager.onRegister(callback)
try:
await manager.register(credentials, UserMessageLimit(1))
waitFor manager.register(credentials, UserMessageLimit(20))
except Exception, CatchableError:
assert false, "exception raised: " & getCurrentExceptionMsg()
await fut
waitFor fut
let rootUpdated = await manager.updateRoots()
let rootUpdated = waitFor manager.updateRoots()
if rootUpdated:
let proofResult = await manager.fetchMerkleProofElements()
let proofResult = waitFor manager.fetchMerkleProofElements()
if proofResult.isErr():
error "Failed to fetch Merkle proof", error = proofResult.error
manager.merkleProofCache = proofResult.get()
@ -388,21 +397,21 @@ suite "Onchain group manager":
check:
verified
asyncTest "verifyProof: should reject invalid proof":
(await manager.init()).isOkOr:
test "verifyProof: should reject invalid proof":
(waitFor manager.init()).isOkOr:
raiseAssert $error
let idCredential = generateCredentials(manager.rlnInstance)
try:
await manager.register(idCredential, UserMessageLimit(1))
waitFor manager.register(idCredential, UserMessageLimit(20))
except Exception, CatchableError:
assert false,
"exception raised when calling startGroupSync: " & getCurrentExceptionMsg()
let messageBytes = "Hello".toBytes()
let rootUpdated = await manager.updateRoots()
let rootUpdated = waitFor manager.updateRoots()
manager.merkleProofCache = newSeq[byte](640)
for i in 0 ..< 640:
@ -427,10 +436,10 @@ suite "Onchain group manager":
check:
verified == false
asyncTest "root queue should be updated correctly":
test "root queue should be updated correctly":
const credentialCount = 12
let credentials = generateCredentials(manager.rlnInstance, credentialCount)
(await manager.init()).isOkOr:
(waitFor manager.init()).isOkOr:
raiseAssert $error
type TestBackfillFuts = array[0 .. credentialCount - 1, Future[void]]
@ -445,7 +454,7 @@ suite "Onchain group manager":
proc callback(registrations: seq[Membership]): Future[void] {.async.} =
if registrations.len == 1 and
registrations[0].rateCommitment ==
getRateCommitment(credentials[futureIndex], UserMessageLimit(1)).get() and
getRateCommitment(credentials[futureIndex], UserMessageLimit(20)).get() and
registrations[0].index == MembershipIndex(futureIndex):
futs[futureIndex].complete()
futureIndex += 1
@ -456,47 +465,40 @@ suite "Onchain group manager":
manager.onRegister(generateCallback(futures, credentials))
for i in 0 ..< credentials.len():
await manager.register(credentials[i], UserMessageLimit(1))
discard await manager.updateRoots()
waitFor manager.register(credentials[i], UserMessageLimit(20))
discard waitFor manager.updateRoots()
except Exception, CatchableError:
assert false, "exception raised: " & getCurrentExceptionMsg()
await allFutures(futures)
waitFor allFutures(futures)
check:
manager.validRoots.len() == credentialCount
asyncTest "isReady should return false if ethRpc is none":
(await manager.init()).isOkOr:
test "isReady should return false if ethRpc is none":
(waitFor manager.init()).isOkOr:
raiseAssert $error
manager.ethRpc = none(Web3)
var isReady = true
try:
isReady = await manager.isReady()
isReady = waitFor manager.isReady()
except Exception, CatchableError:
assert false, "exception raised: " & getCurrentExceptionMsg()
check:
isReady == false
asyncTest "isReady should return true if ethRpc is ready":
(await manager.init()).isOkOr:
test "isReady should return true if ethRpc is ready":
(waitFor manager.init()).isOkOr:
raiseAssert $error
var isReady = false
try:
isReady = await manager.isReady()
isReady = waitFor manager.isReady()
except Exception, CatchableError:
assert false, "exception raised: " & getCurrentExceptionMsg()
check:
isReady == true
################################
## Terminating/removing Anvil
################################
# We stop Anvil daemon
stopAnvil(runAnvil)

View File

@ -30,7 +30,7 @@ import
../testlib/common,
./utils
const CHAIN_ID* = 1337'u256
const CHAIN_ID* = 1234'u256
template skip0xPrefix(hexStr: string): int =
## Returns the index of the first meaningful char in `hexStr` by skipping
@ -61,64 +61,347 @@ proc generateCredentials*(rlnInstance: ptr RLN, n: int): seq[IdentityCredential]
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
proc getContractAddressFromDeployScriptOutput(output: string): Result[string, string] =
const searchStr = "Return ==\n0: address "
const addressLength = 42 # Length of an Ethereum address in hex format
let idx = output.find(searchStr)
if idx >= 0:
let startPos = idx + searchStr.len
let endPos = output.find('\n', startPos)
if (endPos - startPos) >= addressLength:
let address = output[startPos ..< endPos]
return ok(address)
return err("Unable to find contract address in deploy script output")
# 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
proc getForgePath(): string =
var forgePath = ""
if existsEnv("XDG_CONFIG_HOME"):
forgePath = joinPath(forgePath, os.getEnv("XDG_CONFIG_HOME", ""))
else:
forgePath = joinPath(forgePath, os.getEnv("HOME", ""))
forgePath = joinPath(forgePath, ".foundry/bin/forge")
return $forgePath
let balance =
await web3.provider.eth_getBalance(web3.defaultAccount, blockId("latest"))
debug "Initial account balance: ", balance
contract(ERC20Token):
proc allowance(owner: Address, spender: Address): UInt256 {.view.}
proc balanceOf(account: Address): UInt256 {.view.}
# deploy poseidon hasher bytecode
let poseidonT3Receipt = await web3.deployContract(PoseidonT3)
let poseidonT3Address = poseidonT3Receipt.contractAddress.get()
let poseidonAddressStripped = strip0xPrefix($poseidonT3Address)
proc getTokenBalance(
web3: Web3, tokenAddress: Address, account: Address
): Future[UInt256] {.async.} =
let token = web3.contractSender(ERC20Token, tokenAddress)
return await token.balanceOf(account).call()
# deploy lazy imt bytecode
let lazyImtReceipt = await web3.deployContract(
LazyIMT.replace("__$PoseidonT3$__", poseidonAddressStripped)
)
let lazyImtAddress = lazyImtReceipt.contractAddress.get()
let lazyImtAddressStripped = strip0xPrefix($lazyImtAddress)
proc ethToWei(eth: UInt256): UInt256 =
eth * 1000000000000000000.u256
# deploy waku rlnv2 contract
let wakuRlnContractReceipt = await web3.deployContract(
WakuRlnV2Contract.replace("__$PoseidonT3$__", poseidonAddressStripped).replace(
"__$LazyIMT$__", lazyImtAddressStripped
proc sendMintCall(
web3: Web3,
accountFrom: Address,
tokenAddress: Address,
recipientAddress: Address,
amountTokens: UInt256,
recipientBalanceBeforeExpectedTokens: Option[UInt256] = none(UInt256),
): Future[TxHash] {.async.} =
let doBalanceAssert = recipientBalanceBeforeExpectedTokens.isSome()
if doBalanceAssert:
let balanceBeforeMint = await getTokenBalance(web3, tokenAddress, recipientAddress)
let balanceBeforeExpectedTokens = recipientBalanceBeforeExpectedTokens.get()
assert balanceBeforeMint == balanceBeforeExpectedTokens,
fmt"Balance is {balanceBeforeMint} before minting but expected {balanceBeforeExpectedTokens}"
# Create mint transaction
# Method ID for mint(address,uint256) is 0x40c10f19 which is part of the openzeppelin ERC20 standard
# The method ID for a deployed test token can be viewed here https://sepolia.lineascan.build/address/0x185A0015aC462a0aECb81beCc0497b649a64B9ea#writeContract
let mintSelector = "0x40c10f19"
let addressHex = recipientAddress.toHex()
# Pad the address and amount to 32 bytes each
let paddedAddress = addressHex.align(64, '0')
let amountHex = amountTokens.toHex()
let amountWithout0x =
if amountHex.toLower().startsWith("0x"):
amountHex[2 .. ^1]
else:
amountHex
let paddedAmount = amountWithout0x.align(64, '0')
let mintCallData = mintSelector & paddedAddress & paddedAmount
let gasPrice = int(await web3.provider.eth_gasPrice())
# Create the transaction
var tx: TransactionArgs
tx.`from` = Opt.some(accountFrom)
tx.to = Opt.some(tokenAddress)
tx.value = Opt.some(0.u256) # No ETH is sent for token operations
tx.gasPrice = Opt.some(Quantity(gasPrice))
tx.data = Opt.some(byteutils.hexToSeqByte(mintCallData))
trace "Sending mint call"
let txHash = await web3.send(tx)
let balanceOfSelector = "0x70a08231"
let balanceCallData = balanceOfSelector & paddedAddress
# Wait a bit for transaction to be mined
await sleepAsync(500.milliseconds)
if doBalanceAssert:
let balanceAfterMint = await getTokenBalance(web3, tokenAddress, recipientAddress)
let balanceAfterExpectedTokens =
recipientBalanceBeforeExpectedTokens.get() + amountTokens
assert balanceAfterMint == balanceAfterExpectedTokens,
fmt"Balance is {balanceAfterMint} after transfer but expected {balanceAfterExpectedTokens}"
return txHash
# Check how many tokens a spender (the RLN contract) is allowed to spend on behalf of the owner (account which wishes to register a membership)
proc checkTokenAllowance(
web3: Web3, tokenAddress: Address, owner: Address, spender: Address
): Future[UInt256] {.async.} =
let token = web3.contractSender(ERC20Token, tokenAddress)
let allowance = await token.allowance(owner, spender).call()
trace "Current allowance", owner = owner, spender = spender, allowance = allowance
return allowance
proc setupContractDeployment(
forgePath: string, submodulePath: string
): Result[void, string] =
trace "Contract deployer paths", forgePath = forgePath, submodulePath = submodulePath
# Build the Foundry project
try:
let (forgeCleanOutput, forgeCleanExitCode) =
execCmdEx(fmt"""cd {submodulePath} && {forgePath} clean""")
trace "Executed forge clean command", output = forgeCleanOutput
if forgeCleanExitCode != 0:
return err("forge clean command failed")
let (forgeInstallOutput, forgeInstallExitCode) =
execCmdEx(fmt"""cd {submodulePath} && {forgePath} install""")
trace "Executed forge install command", output = forgeInstallOutput
if forgeInstallExitCode != 0:
return err("forge install command failed")
let (pnpmInstallOutput, pnpmInstallExitCode) =
execCmdEx(fmt"""cd {submodulePath} && pnpm install""")
trace "Executed pnpm install command", output = pnpmInstallOutput
if pnpmInstallExitCode != 0:
return err("pnpm install command failed" & pnpmInstallOutput)
let (forgeBuildOutput, forgeBuildExitCode) =
execCmdEx(fmt"""cd {submodulePath} && {forgePath} build""")
trace "Executed forge build command", output = forgeBuildOutput
if forgeBuildExitCode != 0:
return err("forge build command failed")
# Set the environment variable API keys to anything for local testnet deployment
putEnv("API_KEY_CARDONA", "123")
putEnv("API_KEY_LINEASCAN", "123")
putEnv("API_KEY_ETHERSCAN", "123")
except OSError, IOError:
return err("Command execution failed: " & getCurrentExceptionMsg())
return ok()
proc deployTestToken*(
pk: keys.PrivateKey, acc: Address, web3: Web3
): Future[Result[Address, string]] {.async.} =
## Executes a Foundry forge script that deploys the a token contract (ERC-20) used for testing. This is a prerequisite to enable the contract deployment and this token contract address needs to be minted and approved for the accounts that need to register memberships with the contract
## submodulePath: path to the submodule containing contract deploy scripts
# All RLN related tests should be run from the root directory of the project
let submodulePath = absolutePath("./vendor/waku-rlnv2-contract")
# Verify submodule path exists
if not dirExists(submodulePath):
error "Submodule path does not exist", submodulePath = submodulePath
return err("Submodule path does not exist: " & submodulePath)
let forgePath = getForgePath()
setupContractDeployment(forgePath, submodulePath).isOkOr:
error "Failed to setup contract deployment", error = $error
return err("Failed to setup contract deployment: " & $error)
# Deploy TestToken contract
let forgeCmdTestToken =
fmt"""cd {submodulePath} && {forgePath} script test/TestToken.sol --broadcast -vvv --rpc-url http://localhost:8540 --tc TestTokenFactory --private-key {pk} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json"""
let (outputDeployTestToken, exitCodeDeployTestToken) = execCmdEx(forgeCmdTestToken)
trace "Executed forge command to deploy TestToken contract",
output = outputDeployTestToken
if exitCodeDeployTestToken != 0:
return error("Forge command to deploy TestToken contract failed")
# Parse the command output to find contract address
let testTokenAddress = getContractAddressFromDeployScriptOutput(outputDeployTestToken).valueOr:
error "Failed to get TestToken contract address from deploy script output",
error = $error
return err(
"Failed to get TestToken contract address from deploy script output: " & $error
)
)
let wakuRlnContractAddress = wakuRlnContractReceipt.contractAddress.get()
let wakuRlnAddressStripped = strip0xPrefix($wakuRlnContractAddress)
debug "Address of the TestToken contract", testTokenAddress
debug "Address of the deployed rlnv2 contract: ", wakuRlnContractAddress
let testTokenAddressBytes = hexToByteArray[20](testTokenAddress)
let testTokenAddressAddress = Address(testTokenAddressBytes)
putEnv("TOKEN_ADDRESS", testTokenAddressAddress.toHex())
# need to send concat: impl & init_bytes
let contractInput =
byteutils.toHex(encode(wakuRlnContractAddress)) & Erc1967ProxyContractInput
debug "contractInput", contractInput
let proxyReceipt =
await web3.deployContract(Erc1967Proxy, contractInput = contractInput)
return ok(testTokenAddressAddress)
debug "proxy receipt", contractAddress = proxyReceipt.contractAddress.get()
let proxyAddress = proxyReceipt.contractAddress.get()
# Sends an ERC20 token approval call to allow a spender to spend a certain amount of tokens on behalf of the owner
proc approveTokenAllowanceAndVerify*(
web3: Web3,
accountFrom: Address,
privateKey: keys.PrivateKey,
tokenAddress: Address,
spender: Address,
amountWei: UInt256,
expectedAllowanceBefore: Option[UInt256] = none(UInt256),
): Future[Result[TxHash, string]] {.async.} =
var allowanceBefore: UInt256
if expectedAllowanceBefore.isSome():
allowanceBefore =
await checkTokenAllowance(web3, tokenAddress, accountFrom, spender)
let expected = expectedAllowanceBefore.get()
if allowanceBefore != expected:
return
err(fmt"Allowance is {allowanceBefore} before approval but expected {expected}")
let newBalance = await web3.provider.eth_getBalance(web3.defaultAccount, "latest")
debug "Account balance after the contract deployment: ", newBalance
# Temporarily set the private key
let oldPrivateKey = web3.privateKey
web3.privateKey = Opt.some(privateKey)
web3.lastKnownNonce = Opt.none(Quantity)
try:
# ERC20 approve function signature: approve(address spender, uint256 amount)
# Method ID for approve(address,uint256) is 0x095ea7b3
const APPROVE_SELECTOR = "0x095ea7b3"
let addressHex = spender.toHex().align(64, '0')
let amountHex = amountWei.toHex().align(64, '0')
let approveCallData = APPROVE_SELECTOR & addressHex & amountHex
let gasPrice = await web3.provider.eth_gasPrice()
var tx: TransactionArgs
tx.`from` = Opt.some(accountFrom)
tx.to = Opt.some(tokenAddress)
tx.value = Opt.some(0.u256)
tx.gasPrice = Opt.some(gasPrice)
tx.gas = Opt.some(Quantity(100000))
tx.data = Opt.some(byteutils.hexToSeqByte(approveCallData))
tx.chainId = Opt.some(CHAIN_ID)
trace "Sending approve call", tx = tx
let txHash = await web3.send(tx)
let receipt = await web3.getMinedTransactionReceipt(txHash)
if receipt.status.isNone():
return err("Approval transaction failed receipt is none")
if receipt.status.get() != 1.Quantity:
return err("Approval transaction failed status quantity not 1")
# Single verification check after mining (no extra sleep needed)
let allowanceAfter =
await checkTokenAllowance(web3, tokenAddress, accountFrom, spender)
let expectedAfter =
if expectedAllowanceBefore.isSome():
expectedAllowanceBefore.get() + amountWei
else:
amountWei
if allowanceAfter < expectedAfter:
return err(
fmt"Allowance is {allowanceAfter} after approval but expected at least {expectedAfter}"
)
return ok(txHash)
except CatchableError as e:
return err(fmt"Failed to send approve transaction: {e.msg}")
finally:
# Restore the old private key
web3.privateKey = oldPrivateKey
proc executeForgeContractDeployScripts*(
privateKey: keys.PrivateKey, acc: Address, web3: Web3
): Future[Result[Address, string]] {.async, gcsafe.} =
## Executes a set of foundry forge scripts required to deploy the RLN contract and returns the deployed proxy contract address
## submodulePath: path to the submodule containing contract deploy scripts
# All RLN related tests should be run from the root directory of the project
let submodulePath = "./vendor/waku-rlnv2-contract"
# Verify submodule path exists
if not dirExists(submodulePath):
error "Submodule path does not exist", submodulePath = submodulePath
return err("Submodule path does not exist: " & submodulePath)
let forgePath = getForgePath()
debug "Forge path", forgePath
# Verify forge executable exists
if not fileExists(forgePath):
error "Forge executable not found", forgePath = forgePath
return err("Forge executable not found: " & forgePath)
trace "contract deployer account details", account = acc, privateKey = privateKey
let setupContractEnv = setupContractDeployment(forgePath, submodulePath)
if setupContractEnv.isErr():
error "Failed to setup contract deployment"
return err("Failed to setup contract deployment")
# Deploy LinearPriceCalculator contract
let forgeCmdPriceCalculator =
fmt"""cd {submodulePath} && {forgePath} script script/Deploy.s.sol --broadcast -vvvv --rpc-url http://localhost:8540 --tc DeployPriceCalculator --private-key {privateKey} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json"""
let (outputDeployPriceCalculator, exitCodeDeployPriceCalculator) =
execCmdEx(forgeCmdPriceCalculator)
trace "Executed forge command to deploy LinearPriceCalculator contract",
output = outputDeployPriceCalculator
if exitCodeDeployPriceCalculator != 0:
return error("Forge command to deploy LinearPriceCalculator contract failed")
# Parse the output to find contract address
let priceCalculatorAddressRes =
getContractAddressFromDeployScriptOutput(outputDeployPriceCalculator)
if priceCalculatorAddressRes.isErr():
error "Failed to get LinearPriceCalculator contract address from deploy script output"
let priceCalculatorAddress = priceCalculatorAddressRes.get()
debug "Address of the LinearPriceCalculator contract", priceCalculatorAddress
putEnv("PRICE_CALCULATOR_ADDRESS", priceCalculatorAddress)
let forgeCmdWakuRln =
fmt"""cd {submodulePath} && {forgePath} script script/Deploy.s.sol --broadcast -vvvv --rpc-url http://localhost:8540 --tc DeployWakuRlnV2 --private-key {privateKey} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json"""
let (outputDeployWakuRln, exitCodeDeployWakuRln) = execCmdEx(forgeCmdWakuRln)
trace "Executed forge command to deploy WakuRlnV2 contract",
output = outputDeployWakuRln
if exitCodeDeployWakuRln != 0:
error "Forge command to deploy WakuRlnV2 contract failed",
output = outputDeployWakuRln
# Parse the output to find contract address
let wakuRlnV2AddressRes =
getContractAddressFromDeployScriptOutput(outputDeployWakuRln)
if wakuRlnV2AddressRes.isErr():
error "Failed to get WakuRlnV2 contract address from deploy script output"
##TODO: raise exception here?
let wakuRlnV2Address = wakuRlnV2AddressRes.get()
debug "Address of the WakuRlnV2 contract", wakuRlnV2Address
putEnv("WAKURLNV2_ADDRESS", wakuRlnV2Address)
# Deploy Proxy contract
let forgeCmdProxy =
fmt"""cd {submodulePath} && {forgePath} script script/Deploy.s.sol --broadcast -vvvv --rpc-url http://localhost:8540 --tc DeployProxy --private-key {privateKey} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json"""
let (outputDeployProxy, exitCodeDeployProxy) = execCmdEx(forgeCmdProxy)
trace "Executed forge command to deploy proxy contract", output = outputDeployProxy
if exitCodeDeployProxy != 0:
error "Forge command to deploy Proxy failed", error = outputDeployProxy
return err("Forge command to deploy Proxy failed")
let proxyAddress = getContractAddressFromDeployScriptOutput(outputDeployProxy)
let proxyAddressBytes = hexToByteArray[20](proxyAddress.get())
let proxyAddressAddress = Address(proxyAddressBytes)
info "Address of the Proxy contract", proxyAddressAddress
await web3.close()
debug "disconnected from ", ethClientAddress
return proxyAddress
return ok(proxyAddressAddress)
proc sendEthTransfer*(
web3: Web3,
@ -133,7 +416,7 @@ proc sendEthTransfer*(
let balanceBeforeWei = await web3.provider.eth_getBalance(accountTo, "latest")
let balanceBeforeExpectedWei = accountToBalanceBeforeExpectedWei.get()
assert balanceBeforeWei == balanceBeforeExpectedWei,
fmt"Balance is {balanceBeforeWei} but expected {balanceBeforeExpectedWei}"
fmt"Balance is {balanceBeforeWei} before transfer but expected {balanceBeforeExpectedWei}"
let gasPrice = int(await web3.provider.eth_gasPrice())
@ -146,17 +429,17 @@ proc sendEthTransfer*(
# TODO: handle the error if sending fails
let txHash = await web3.send(tx)
# Wait a bit for transaction to be mined
await sleepAsync(200.milliseconds)
if doBalanceAssert:
let balanceAfterWei = await web3.provider.eth_getBalance(accountTo, "latest")
let balanceAfterExpectedWei = accountToBalanceBeforeExpectedWei.get() + amountWei
assert balanceAfterWei == balanceAfterExpectedWei,
fmt"Balance is {balanceAfterWei} but expected {balanceAfterExpectedWei}"
fmt"Balance is {balanceAfterWei} after transfer but expected {balanceAfterExpectedWei}"
return txHash
proc ethToWei(eth: UInt256): UInt256 =
eth * 1000000000000000000.u256
proc createEthAccount*(
ethAmount: UInt256 = 1000.u256
): Future[(keys.PrivateKey, Address)] {.async.} =
@ -198,7 +481,7 @@ proc getAnvilPath*(): string =
return $anvilPath
# Runs Anvil daemon
proc runAnvil*(port: int = 8540, chainId: string = "1337"): Process =
proc runAnvil*(port: int = 8540, chainId: string = "1234"): Process =
# Passed options are
# --port Port to listen on.
# --gas-limit Sets the block gas limit in WEI.
@ -212,13 +495,13 @@ proc runAnvil*(port: int = 8540, chainId: string = "1337"): Process =
anvilPath,
args = [
"--port",
"8540",
$port,
"--gas-limit",
"300000000000000",
"--balance",
"1000000000",
"--chain-id",
$CHAIN_ID,
$chainId,
],
options = {poUsePath},
)
@ -242,14 +525,26 @@ proc runAnvil*(port: int = 8540, chainId: string = "1337"): Process =
# Stops Anvil daemon
proc stopAnvil*(runAnvil: Process) {.used.} =
if runAnvil.isNil:
debug "stopAnvil called with nil Process"
return
let anvilPID = runAnvil.processID
# We wait the daemon to exit
debug "Stopping Anvil daemon", anvilPID = anvilPID
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()
# Send termination signals
when not defined(windows):
discard execCmdEx(fmt"kill -TERM {anvilPID}")
discard execCmdEx(fmt"kill -9 {anvilPID}")
else:
discard execCmdEx(fmt"taskkill /F /PID {anvilPID}")
# Close Process object to release resources
close(runAnvil)
debug "Anvil daemon stopped", anvilPID = anvilPID
except Exception as e:
debug "Error stopping Anvil daemon", anvilPID = anvilPID, error = e.msg
proc setupOnchainGroupManager*(
ethClientUrl: string = EthClient, amountEth: UInt256 = 10.u256
@ -261,12 +556,10 @@ proc setupOnchainGroupManager*(
let rlnInstance = rlnInstanceRes.get()
let contractAddress = await uploadRLNContract(ethClientUrl)
# connect to the eth client
let web3 = await newWeb3(ethClientUrl)
let accounts = await web3.provider.eth_accounts()
web3.defaultAccount = accounts[0]
web3.defaultAccount = accounts[1]
let (privateKey, acc) = createEthAccount(web3)
@ -276,6 +569,32 @@ proc setupOnchainGroupManager*(
web3, web3.defaultAccount, acc, ethToWei(1000.u256), some(0.u256)
)
let testTokenAddress = (await deployTestToken(privateKey, acc, web3)).valueOr:
assert false, "Failed to deploy test token contract: " & $error
return
# mint the token from the generated account
discard await sendMintCall(
web3, web3.defaultAccount, testTokenAddress, acc, ethToWei(1000.u256), some(0.u256)
)
let contractAddress = (await executeForgeContractDeployScripts(privateKey, acc, web3)).valueOr:
assert false, "Failed to deploy RLN contract: " & $error
return
# If the generated account wishes to register a membership, it needs to approve the contract to spend its tokens
let tokenApprovalResult = await approveTokenAllowanceAndVerify(
web3,
acc, # owner
privateKey,
testTokenAddress, # ERC20 token address
contractAddress, # spender - the proxy contract that will spend the tokens
ethToWei(200.u256),
some(0.u256), # expected allowance before approval
)
assert tokenApprovalResult.isOk, tokenApprovalResult.error()
let manager = OnchainGroupManager(
ethClientUrls: @[ethClientUrl],
ethContractAddress: $contractAddress,

View File

@ -209,7 +209,7 @@ suite "Waku Sync reconciliation":
baseHash = hashLocal
alteredHash = toDigest("msg" & $i & "_x")
hashRemote = alteredHash
remote.insert(SyncID(time: ts, hash: hashRemote)).isOkOr:
assert false, "failed to insert hash: " & $error

@ -1 +1 @@
Subproject commit a576a8949ca20e310f2fbb4ec0bd05a57ac3045f
Subproject commit b7e9a9b1bc69256a2a3076c1f099b50ce84e7eff

View File

@ -30,12 +30,12 @@ type ClusterConf* = object
# Cluster configuration corresponding to The Waku Network. Note that it
# overrides existing cli configuration
proc TheWakuNetworkConf*(T: type ClusterConf): ClusterConf =
const RelayChainId = 11155111'u256
const RelayChainId = 59141'u256
return ClusterConf(
maxMessageSize: "150KiB",
clusterId: 1,
rlnRelay: true,
rlnRelayEthContractAddress: "0xfe7a9eabcE779a090FD702346Fd0bFAc02ce6Ac8",
rlnRelayEthContractAddress: "0xB9cd878C90E49F797B4431fBF4fb333108CB90e6",
rlnRelayDynamic: true,
rlnRelayChainId: RelayChainId,
rlnEpochSizeSec: 600,

View File

@ -30,23 +30,29 @@ logScope:
# using the when predicate does not work within the contract macro, hence need to dupe
contract(WakuRlnContract):
# this serves as an entrypoint into the rln membership set
proc register(idCommitment: UInt256, userMessageLimit: UInt32)
proc register(
idCommitment: UInt256, userMessageLimit: UInt32, idCommitmentsToErase: seq[UInt256]
)
# Initializes the implementation contract (only used in unit tests)
proc initialize(maxMessageLimit: UInt256)
# this event is raised when a new member is registered
proc MemberRegistered(rateCommitment: UInt256, index: UInt32) {.event.}
# this event is emitted when a new member is registered
proc MembershipRegistered(
idCommitment: UInt256, membershipRateLimit: UInt256, index: UInt32
) {.event.}
# this function denotes existence of a given user
proc memberExists(idCommitment: UInt256): UInt256 {.view.}
proc isInMembershipSet(idCommitment: Uint256): bool {.view.}
# this constant describes the next index of a new member
proc commitmentIndex(): UInt256 {.view.}
proc nextFreeIndex(): UInt256 {.view.}
# this constant describes the block number this contract was deployed on
proc deployedBlockNumber(): UInt256 {.view.}
# this constant describes max message limit of rln contract
proc MAX_MESSAGE_LIMIT(): UInt256 {.view.}
# this function returns the merkleProof for a given index
# proc merkleProofElements(index: UInt40): seq[byte] {.view.}
# this function returns the merkle root
proc root(): UInt256 {.view.}
proc maxMembershipRateLimit(): UInt256 {.view.}
# this function returns the merkleProof for a given index
# proc getMerkleProof(index: EthereumUInt40): seq[array[32, byte]] {.view.}
# this function returns the Merkle root
proc root(): Uint256 {.view.}
type
WakuRlnContractWithSender = Sender[WakuRlnContract]
@ -67,11 +73,7 @@ type
proc setMetadata*(
g: OnchainGroupManager, lastProcessedBlock = none(BlockNumber)
): GroupManagerResult[void] =
let normalizedBlock =
if lastProcessedBlock.isSome():
lastProcessedBlock.get()
else:
g.latestProcessedBlock
let normalizedBlock = lastProcessedBlock.get(g.latestProcessedBlock)
try:
let metadataSetRes = g.rlnInstance.setMetadata(
RlnMetadata(
@ -87,14 +89,68 @@ proc setMetadata*(
return err("failed to persist rln metadata: " & getCurrentExceptionMsg())
return ok()
proc sendEthCallWithChainId(
ethRpc: Web3,
functionSignature: string,
fromAddress: Address,
toAddress: Address,
chainId: UInt256,
): Future[Result[UInt256, string]] {.async.} =
## Workaround for web3 chainId=null issue on some networks (e.g., linea-sepolia)
## Makes contract calls with explicit chainId for view functions with no parameters
let functionHash =
keccak256.digest(functionSignature.toOpenArrayByte(0, functionSignature.len - 1))
let functionSelector = functionHash.data[0 .. 3]
let dataSignature = "0x" & functionSelector.mapIt(it.toHex(2)).join("")
var tx: TransactionArgs
tx.`from` = Opt.some(fromAddress)
tx.to = Opt.some(toAddress)
tx.value = Opt.some(0.u256)
tx.data = Opt.some(byteutils.hexToSeqByte(dataSignature))
tx.chainId = Opt.some(chainId)
let resultBytes = await ethRpc.provider.eth_call(tx, "latest")
if resultBytes.len == 0:
return err("No result returned for function call: " & functionSignature)
return ok(UInt256.fromBytesBE(resultBytes))
proc sendEthCallWithParams(
ethRpc: Web3,
functionSignature: string,
params: seq[byte],
fromAddress: Address,
toAddress: Address,
chainId: UInt256,
): Future[Result[seq[byte], string]] {.async.} =
## Workaround for web3 chainId=null issue with parameterized contract calls
let functionHash =
keccak256.digest(functionSignature.toOpenArrayByte(0, functionSignature.len - 1))
let functionSelector = functionHash.data[0 .. 3]
let callData = functionSelector & params
var tx: TransactionArgs
tx.`from` = Opt.some(fromAddress)
tx.to = Opt.some(toAddress)
tx.value = Opt.some(0.u256)
tx.data = Opt.some(callData)
tx.chainId = Opt.some(chainId)
let resultBytes = await ethRpc.provider.eth_call(tx, "latest")
return ok(resultBytes)
proc fetchMerkleProofElements*(
g: OnchainGroupManager
): Future[Result[seq[byte], string]] {.async.} =
try:
# let merkleRootInvocation = g.wakuRlnContract.get().root()
# let merkleRoot = await merkleRootInvocation.call()
# The above code is not working with the latest web3 version due to chainId being null (specifically on linea-sepolia)
# TODO: find better solution than this custom sendEthCallWithChainId call
let membershipIndex = g.membershipIndex.get()
let index40 = stuint(membershipIndex, 40)
let methodSig = "merkleProofElements(uint40)"
let methodSig = "getMerkleProof(uint40)"
let methodIdDigest = keccak.keccak256.digest(methodSig)
let methodId = methodIdDigest.data[0 .. 3]
@ -111,6 +167,7 @@ proc fetchMerkleProofElements*(
var tx: TransactionArgs
tx.to = Opt.some(fromHex(Address, g.ethContractAddress))
tx.data = Opt.some(callData)
tx.chainId = Opt.some(g.chainId) # Explicitly set the chain ID
let responseBytes = await g.ethRpc.get().provider.eth_call(tx, "latest")
@ -123,8 +180,17 @@ proc fetchMerkleRoot*(
g: OnchainGroupManager
): Future[Result[UInt256, string]] {.async.} =
try:
let merkleRootInvocation = g.wakuRlnContract.get().root()
let merkleRoot = await merkleRootInvocation.call()
let merkleRoot = (
await sendEthCallWithChainId(
ethRpc = g.ethRpc.get(),
functionSignature = "root()",
fromAddress = g.ethRpc.get().defaultAccount,
toAddress = fromHex(Address, g.ethContractAddress),
chainId = g.chainId,
)
).valueOr:
error "Failed to fetch Merkle root", error = $error
return err("Failed to fetch merkle root: " & $error)
return ok(merkleRoot)
except CatchableError:
error "Failed to fetch Merkle root", error = getCurrentExceptionMsg()
@ -151,6 +217,7 @@ proc updateRoots*(g: OnchainGroupManager): Future[bool] {.async.} =
return false
let merkleRoot = UInt256ToField(rootRes.get())
if g.validRoots.len == 0:
g.validRoots.addLast(merkleRoot)
return true
@ -183,8 +250,26 @@ proc trackRootChanges*(g: OnchainGroupManager) {.async: (raises: [CatchableError
error "Failed to fetch Merkle proof", error = proofResult.error
g.merkleProofCache = proofResult.get()
# also need update registerd membership
let memberCount = cast[int64](await wakuRlnContract.commitmentIndex().call())
# also need to update registered membership
# g.rlnRelayMaxMessageLimit =
# cast[uint64](await wakuRlnContract.nextFreeIndex().call())
# The above code is not working with the latest web3 version due to chainId being null (specifically on linea-sepolia)
# TODO: find better solution than this custom sendEthCallWithChainId call
let nextFreeIndex = await sendEthCallWithChainId(
ethRpc = ethRpc,
functionSignature = "nextFreeIndex()",
fromAddress = ethRpc.defaultAccount,
toAddress = fromHex(Address, g.ethContractAddress),
chainId = g.chainId,
)
if nextFreeIndex.isErr():
error "Failed to fetch next free index", error = nextFreeIndex.error
raise newException(
CatchableError, "Failed to fetch next free index: " & nextFreeIndex.error
)
let memberCount = cast[int64](nextFreeIndex.get())
waku_rln_number_registered_memberships.set(float64(memberCount))
await sleepAsync(rpcDelay)
@ -219,15 +304,19 @@ method register*(
var gasPrice: int
g.retryWrapper(gasPrice, "Failed to get gas price"):
int(await ethRpc.provider.eth_gasPrice()) * 2
let idCommitmentHex = identityCredential.idCommitment.inHex()
debug "identityCredential idCommitmentHex", idCommitment = idCommitmentHex
let idCommitment = identityCredential.idCommitment.toUInt256()
let idCommitmentsToErase: seq[UInt256] = @[]
debug "registering the member",
idCommitment = idCommitment, userMessageLimit = userMessageLimit
idCommitment = idCommitment,
userMessageLimit = userMessageLimit,
idCommitmentsToErase = idCommitmentsToErase
var txHash: TxHash
g.retryWrapper(txHash, "Failed to register the member"):
await wakuRlnContract.register(idCommitment, userMessageLimit.stuint(32)).send(
gasPrice = gasPrice
)
await wakuRlnContract
.register(idCommitment, userMessageLimit.stuint(32), idCommitmentsToErase)
.send(gasPrice = gasPrice)
# wait for the transaction to be mined
var tsReceipt: ReceiptObject
@ -240,27 +329,29 @@ method register*(
debug "ts receipt", receipt = tsReceipt[]
if tsReceipt.status.isNone():
raise newException(ValueError, "register: transaction failed status is None")
raise newException(ValueError, "Transaction failed: status is None")
if tsReceipt.status.get() != 1.Quantity:
raise newException(
ValueError, "register: transaction failed status is: " & $tsReceipt.status.get()
ValueError, "Transaction failed with status: " & $tsReceipt.status.get()
)
let firstTopic = tsReceipt.logs[0].topics[0]
# the hash of the signature of MemberRegistered(uint256,uint32) event is equal to the following hex value
if firstTopic !=
cast[FixedBytes[32]](keccak.keccak256.digest("MemberRegistered(uint256,uint32)").data):
## Extract MembershipRegistered event from transaction logs (third event)
let thirdTopic = tsReceipt.logs[2].topics[0]
debug "third topic", thirdTopic = thirdTopic
if thirdTopic !=
cast[FixedBytes[32]](keccak.keccak256.digest(
"MembershipRegistered(uint256,uint256,uint32)"
).data):
raise newException(ValueError, "register: unexpected event signature")
# the arguments of the raised event i.e., MemberRegistered are encoded inside the data field
# data = rateCommitment encoded as 256 bits || index encoded as 32 bits
let arguments = tsReceipt.logs[0].data
## Parse MembershipRegistered event data: rateCommitment(256) || membershipRateLimit(256) || index(32)
let arguments = tsReceipt.logs[2].data
debug "tx log data", arguments = arguments
let
# In TX log data, uints are encoded in big endian
membershipIndex = UInt256.fromBytesBE(arguments[32 ..^ 1])
## Extract membership index from transaction log data (big endian)
membershipIndex = UInt256.fromBytesBE(arguments[64 .. 95])
debug "parsed membershipIndex", membershipIndex
trace "parsed membershipIndex", membershipIndex
g.userMessageLimit = some(userMessageLimit)
g.membershipIndex = some(membershipIndex.toMembershipIndex())
g.idCredentials = some(identityCredential)
@ -376,7 +467,7 @@ method generateProof*(
var proofValue = cast[ptr array[320, byte]](output_witness_buffer.`ptr`)
let proofBytes: array[320, byte] = proofValue[]
## parse the proof as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> ]
## Parse the proof as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> ]
let
proofOffset = 128
rootOffset = proofOffset + 32
@ -418,9 +509,7 @@ method generateProof*(
return ok(output)
method verifyProof*(
g: OnchainGroupManager, # verifier context
input: seq[byte], # raw message data (signal)
proof: RateLimitProof, # proof received from the peer
g: OnchainGroupManager, input: seq[byte], proof: RateLimitProof
): GroupManagerResult[bool] {.gcsafe, raises: [].} =
## -- Verifies an RLN rate-limit proof against the set of valid Merkle roots --
@ -543,11 +632,31 @@ method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.}
g.membershipIndex = some(keystoreCred.treeIndex)
g.userMessageLimit = some(keystoreCred.userMessageLimit)
# now we check on the contract if the commitment actually has a membership
let idCommitmentBytes = keystoreCred.identityCredential.idCommitment
let idCommitmentUInt256 = keystoreCred.identityCredential.idCommitment.toUInt256()
let idCommitmentHex = idCommitmentBytes.inHex()
debug "Keystore idCommitment in bytes", idCommitmentBytes = idCommitmentBytes
debug "Keystore idCommitment in UInt256 ", idCommitmentUInt256 = idCommitmentUInt256
debug "Keystore idCommitment in hex ", idCommitmentHex = idCommitmentHex
let idCommitment = idCommitmentUInt256
try:
let membershipExists = await wakuRlnContract
.memberExists(keystoreCred.identityCredential.idCommitment.toUInt256())
.call()
if membershipExists == 0:
let commitmentBytes = keystoreCred.identityCredential.idCommitment
let params = commitmentBytes.reversed()
let resultBytes = await sendEthCallWithParams(
ethRpc = g.ethRpc.get(),
functionSignature = "isInMembershipSet(uint256)",
params = params,
fromAddress = ethRpc.defaultAccount,
toAddress = contractAddress,
chainId = g.chainId,
)
if resultBytes.isErr():
return err("Failed to check membership: " & resultBytes.error)
let responseBytes = resultBytes.get()
let membershipExists = responseBytes.len == 32 and responseBytes[^1] == 1'u8
debug "membershipExists", membershipExists = membershipExists
if membershipExists == false:
return err("the commitment does not have a membership")
except CatchableError:
return err("failed to check if the commitment has a membership")
@ -564,8 +673,18 @@ method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.}
if metadata.contractAddress != g.ethContractAddress.toLower():
return err("persisted data: contract address mismatch")
g.rlnRelayMaxMessageLimit =
cast[uint64](await wakuRlnContract.MAX_MESSAGE_LIMIT().call())
let maxMembershipRateLimit = (
await sendEthCallWithChainId(
ethRpc = ethRpc,
functionSignature = "maxMembershipRateLimit()",
fromAddress = ethRpc.defaultAccount,
toAddress = contractAddress,
chainId = g.chainId,
)
).valueOr:
return err("Failed to fetch max membership rate limit: " & $error)
g.rlnRelayMaxMessageLimit = cast[uint64](maxMembershipRateLimit)
proc onDisconnect() {.async.} =
error "Ethereum client disconnected"