Chore: bump waku-rlnv2-contract-repo commit (#3651)

* Bump commit for vendor wakurlnv2contract

* Update RLN registration proc for contract updates

* add option to runAnvil for state dump or load with optional contract deployment on setup

* Code clean up

* Upodate rln relay tests to use cached anvil state

* Minor updates to utils and new test for anvil state dump

* stopAnvil needs to wait for graceful shutdown

* configure runAnvil to use load state in other tests

* reduce ci timeout

* Allow for RunAnvil load state file to be compressed

* Fix linting

* Change return type of sendMintCall to Futre[void]

* Update naming of ci path for interop tests
This commit is contained in:
Tanya S 2025-12-08 08:29:48 +02:00 committed by GitHub
parent a8590a0a7d
commit 2cf4fe559a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 259 additions and 87 deletions

View File

@ -94,7 +94,7 @@ jobs:
matrix:
os: [ubuntu-22.04, macos-15]
runs-on: ${{ matrix.os }}
timeout-minutes: 90
timeout-minutes: 45
name: test-${{ matrix.os }}
steps:
@ -137,7 +137,7 @@ jobs:
nwaku-nwaku-interop-tests:
needs: build-docker-image
uses: logos-messaging/waku-interop-tests/.github/workflows/nim_waku_PR.yml@SMOKE_TEST_0.0.1
uses: logos-messaging/logos-messaging-interop-tests/.github/workflows/nim_waku_PR.yml@SMOKE_TEST_0.0.1
with:
node_nwaku: ${{ needs.build-docker-image.outputs.image }}

View File

@ -135,8 +135,8 @@ suite "RLN Proofs as a Lightpush Service":
server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0))
client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0))
anvilProc = runAnvil()
manager = waitFor setupOnchainGroupManager()
anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH))
manager = waitFor setupOnchainGroupManager(deployContracts = false)
# mount rln-relay
let wakuRlnConfig = getWakuRlnConfig(manager = manager, index = MembershipIndex(1))

View File

@ -135,8 +135,8 @@ suite "RLN Proofs as a Lightpush Service":
server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0))
client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0))
anvilProc = runAnvil()
manager = waitFor setupOnchainGroupManager()
anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH))
manager = waitFor setupOnchainGroupManager(deployContracts = false)
# mount rln-relay
let wakuRlnConfig = getWakuRlnConfig(manager = manager, index = MembershipIndex(1))

View File

@ -0,0 +1,29 @@
{.used.}
{.push raises: [].}
import std/[options, os], results, testutils/unittests, chronos, web3
import
waku/[
waku_rln_relay,
waku_rln_relay/conversion_utils,
waku_rln_relay/group_manager/on_chain/group_manager,
],
./utils_onchain
suite "Token and RLN Contract Deployment":
test "anvil should dump state to file on exit":
# git will ignore this file, if the contract has been updated and the state file needs to be regenerated then this file can be renamed to replace the one in the repo (tests/waku_rln_relay/anvil_state/tests/waku_rln_relay/anvil_state/state-deployed-contracts-mint-and-approved.json)
let testStateFile = some("tests/waku_rln_relay/anvil_state/anvil_state.ignore.json")
let anvilProc = runAnvil(stateFile = testStateFile, dumpStateOnExit = true)
let manager = waitFor setupOnchainGroupManager(deployContracts = true)
stopAnvil(anvilProc)
check:
fileExists(testStateFile.get())
#The test should still pass even if thie compression fails
compressGzipFile(testStateFile.get(), testStateFile.get() & ".gz").isOkOr:
error "Failed to compress state file", error = error

View File

@ -33,8 +33,8 @@ suite "Onchain group manager":
var manager {.threadVar.}: OnchainGroupManager
setup:
anvilProc = runAnvil()
manager = waitFor setupOnchainGroupManager()
anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH))
manager = waitFor setupOnchainGroupManager(deployContracts = false)
teardown:
stopAnvil(anvilProc)

View File

@ -27,8 +27,8 @@ suite "Waku rln relay":
var manager {.threadVar.}: OnchainGroupManager
setup:
anvilProc = runAnvil()
manager = waitFor setupOnchainGroupManager()
anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH))
manager = waitFor setupOnchainGroupManager(deployContracts = false)
teardown:
stopAnvil(anvilProc)

View File

@ -30,8 +30,8 @@ procSuite "WakuNode - RLN relay":
var manager {.threadVar.}: OnchainGroupManager
setup:
anvilProc = runAnvil()
manager = waitFor setupOnchainGroupManager()
anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH))
manager = waitFor setupOnchainGroupManager(deployContracts = false)
teardown:
stopAnvil(anvilProc)

View File

@ -3,7 +3,7 @@
{.push raises: [].}
import
std/[options, os, osproc, deques, streams, strutils, tempfiles, strformat],
std/[options, os, osproc, streams, strutils, strformat],
results,
stew/byteutils,
testutils/unittests,
@ -14,7 +14,6 @@ import
web3/conversions,
web3/eth_api_types,
json_rpc/rpcclient,
json,
libp2p/crypto/crypto,
eth/keys,
results
@ -24,25 +23,19 @@ import
waku_rln_relay,
waku_rln_relay/protocol_types,
waku_rln_relay/constants,
waku_rln_relay/contract,
waku_rln_relay/rln,
],
../testlib/common,
./utils
../testlib/common
const CHAIN_ID* = 1234'u256
template skip0xPrefix(hexStr: string): int =
## Returns the index of the first meaningful char in `hexStr` by skipping
## "0x" prefix
if hexStr.len > 1 and hexStr[0] == '0' and hexStr[1] in {'x', 'X'}: 2 else: 0
func strip0xPrefix(s: string): string =
let prefixLen = skip0xPrefix(s)
if prefixLen != 0:
s[prefixLen .. ^1]
else:
s
# Path to the file which Anvil loads at startup to initialize the chain with pre-deployed contracts, an account funded with tokens and approved for spending
const DEFAULT_ANVIL_STATE_PATH* =
"tests/waku_rln_relay/anvil_state/state-deployed-contracts-mint-and-approved.json.gz"
# The contract address of the TestStableToken used for the RLN Membership registration fee
const TOKEN_ADDRESS* = "0x5FbDB2315678afecb367f032d93F642f64180aa3"
# The contract address used ti interact with the WakuRLNV2 contract via the proxy
const WAKU_RLNV2_PROXY_ADDRESS* = "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707"
proc generateCredentials*(): IdentityCredential =
let credRes = membershipKeyGen()
@ -106,7 +99,7 @@ proc sendMintCall(
recipientAddress: Address,
amountTokens: UInt256,
recipientBalanceBeforeExpectedTokens: Option[UInt256] = none(UInt256),
): Future[TxHash] {.async.} =
): Future[void] {.async.} =
let doBalanceAssert = recipientBalanceBeforeExpectedTokens.isSome()
if doBalanceAssert:
@ -142,7 +135,7 @@ proc sendMintCall(
tx.data = Opt.some(byteutils.hexToSeqByte(mintCallData))
trace "Sending mint call"
let txHash = await web3.send(tx)
discard await web3.send(tx)
let balanceOfSelector = "0x70a08231"
let balanceCallData = balanceOfSelector & paddedAddress
@ -157,8 +150,6 @@ proc sendMintCall(
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
@ -487,20 +478,64 @@ proc getAnvilPath*(): string =
anvilPath = joinPath(anvilPath, ".foundry/bin/anvil")
return $anvilPath
proc decompressGzipFile*(
compressedPath: string, targetPath: string
): Result[void, string] =
## Decompress a gzipped file using the gunzip command-line utility
let cmd = fmt"gunzip -c {compressedPath} > {targetPath}"
try:
let (output, exitCode) = execCmdEx(cmd)
if exitCode != 0:
return err(
"Failed to decompress '" & compressedPath & "' to '" & targetPath & "': " &
output
)
except OSError as e:
return err("Failed to execute gunzip command: " & e.msg)
except IOError as e:
return err("Failed to execute gunzip command: " & e.msg)
ok()
proc compressGzipFile*(sourcePath: string, targetPath: string): Result[void, string] =
## Compress a file with gzip using the gzip command-line utility
let cmd = fmt"gzip -c {sourcePath} > {targetPath}"
try:
let (output, exitCode) = execCmdEx(cmd)
if exitCode != 0:
return err(
"Failed to compress '" & sourcePath & "' to '" & targetPath & "': " & output
)
except OSError as e:
return err("Failed to execute gzip command: " & e.msg)
except IOError as e:
return err("Failed to execute gzip command: " & e.msg)
ok()
# Runs Anvil daemon
proc runAnvil*(port: int = 8540, chainId: string = "1234"): Process =
proc runAnvil*(
port: int = 8540,
chainId: string = "1234",
stateFile: Option[string] = none(string),
dumpStateOnExit: bool = false,
): 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.
# --load-state Initialize the chain from a previously saved state snapshot (read-only)
# --dump-state Dump the state on exit to the given file (write-only)
# See anvil documentation https://book.getfoundry.sh/reference/anvil/ for more details
try:
let anvilPath = getAnvilPath()
info "Anvil path", anvilPath
let runAnvil = startProcess(
anvilPath,
args = [
var args =
@[
"--port",
$port,
"--gas-limit",
@ -509,9 +544,54 @@ proc runAnvil*(port: int = 8540, chainId: string = "1234"): Process =
"1000000000",
"--chain-id",
$chainId,
],
options = {poUsePath, poStdErrToStdOut},
)
]
# Add state file argument if provided
if stateFile.isSome():
var statePath = stateFile.get()
info "State file parameter provided",
statePath = statePath,
dumpStateOnExit = dumpStateOnExit,
absolutePath = absolutePath(statePath)
# Check if the file is gzip compressed and handle decompression
if statePath.endsWith(".gz"):
let decompressedPath = statePath[0 .. ^4] # Remove .gz extension
debug "Gzip compressed state file detected",
compressedPath = statePath, decompressedPath = decompressedPath
if not fileExists(decompressedPath):
decompressGzipFile(statePath, decompressedPath).isOkOr:
error "Failed to decompress state file", error = error
return nil
statePath = decompressedPath
if dumpStateOnExit:
# Ensure the directory exists
let stateDir = parentDir(statePath)
if not dirExists(stateDir):
createDir(stateDir)
# Fresh deployment: start clean and dump state on exit
args.add("--dump-state")
args.add(statePath)
debug "Anvil configured to dump state on exit", path = statePath
else:
# Using cache: only load state, don't overwrite it (preserves clean cached state)
if fileExists(statePath):
args.add("--load-state")
args.add(statePath)
debug "Anvil configured to load state file (read-only)", path = statePath
else:
warn "State file does not exist, anvil will start fresh",
path = statePath, absolutePath = absolutePath(statePath)
else:
info "No state file provided, anvil will start fresh without state persistence"
info "Starting anvil with arguments", args = args.join(" ")
let runAnvil =
startProcess(anvilPath, args = args, options = {poUsePath, poStdErrToStdOut})
let anvilPID = runAnvil.processID
# We read stdout from Anvil to see when daemon is ready
@ -549,6 +629,13 @@ proc stopAnvil*(runAnvil: Process) {.used.} =
# Send termination signals
when not defined(windows):
discard execCmdEx(fmt"kill -TERM {anvilPID}")
# Wait for graceful shutdown to allow state dumping
sleep(200)
# Only force kill if process is still running
let checkResult = execCmdEx(fmt"kill -0 {anvilPID} 2>/dev/null")
if checkResult.exitCode == 0:
info "Anvil process still running after TERM signal, sending KILL",
anvilPID = anvilPID
discard execCmdEx(fmt"kill -9 {anvilPID}")
else:
discard execCmdEx(fmt"taskkill /F /PID {anvilPID}")
@ -560,37 +647,85 @@ proc stopAnvil*(runAnvil: Process) {.used.} =
info "Error stopping Anvil daemon", anvilPID = anvilPID, error = e.msg
proc setupOnchainGroupManager*(
ethClientUrl: string = EthClient, amountEth: UInt256 = 10.u256
ethClientUrl: string = EthClient,
amountEth: UInt256 = 10.u256,
deployContracts: bool = true,
): Future[OnchainGroupManager] {.async.} =
## Setup an onchain group manager for testing
## If deployContracts is false, it will assume that the Anvil testnet already has the required contracts deployed, this significantly speeds up test runs.
## To run Anvil with a cached state file containing pre-deployed contracts, see runAnvil documentation.
##
## To generate/update the cached state file:
## 1. Call runAnvil with stateFile and dumpStateOnExit=true
## 2. Run setupOnchainGroupManager with deployContracts=true to deploy contracts
## 3. The state will be saved to the specified file when anvil exits
## 4. Commit this file to git
##
## To use cached state:
## 1. Call runAnvil with stateFile and dumpStateOnExit=false
## 2. Anvil loads state in read-only mode (won't overwrite the cached file)
## 3. Call setupOnchainGroupManager with deployContracts=false
## 4. Tests run fast using pre-deployed contracts
let rlnInstanceRes = createRlnInstance()
check:
rlnInstanceRes.isOk()
let rlnInstance = rlnInstanceRes.get()
# connect to the eth client
let web3 = await newWeb3(ethClientUrl)
let accounts = await web3.provider.eth_accounts()
web3.defaultAccount = accounts[1]
let (privateKey, acc) = createEthAccount(web3)
var privateKey: keys.PrivateKey
var acc: Address
var testTokenAddress: Address
var contractAddress: Address
# we just need to fund the default account
# the send procedure returns a tx hash that we don't use, hence discard
if not deployContracts:
info "Using contract addresses from constants"
testTokenAddress = Address(hexToByteArray[20](TOKEN_ADDRESS))
contractAddress = Address(hexToByteArray[20](WAKU_RLNV2_PROXY_ADDRESS))
(privateKey, acc) = createEthAccount(web3)
# Fund the test account
discard await sendEthTransfer(web3, web3.defaultAccount, acc, ethToWei(1000.u256))
# Mint tokens to the test account
await sendMintCall(
web3, web3.defaultAccount, testTokenAddress, acc, ethToWei(1000.u256)
)
# Approve the contract to spend tokens
let tokenApprovalResult = await approveTokenAllowanceAndVerify(
web3, acc, privateKey, testTokenAddress, contractAddress, ethToWei(200.u256)
)
assert tokenApprovalResult.isOk(), tokenApprovalResult.error
else:
info "Performing Token and RLN contracts deployment"
(privateKey, acc) = createEthAccount(web3)
# fund the default account
discard await sendEthTransfer(
web3, web3.defaultAccount, acc, ethToWei(1000.u256), some(0.u256)
)
let testTokenAddress = (await deployTestToken(privateKey, acc, web3)).valueOr:
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)
await sendMintCall(
web3,
web3.defaultAccount,
testTokenAddress,
acc,
ethToWei(1000.u256),
some(0.u256),
)
let contractAddress = (await executeForgeContractDeployScripts(privateKey, acc, web3)).valueOr:
contractAddress = (await executeForgeContractDeployScripts(privateKey, acc, web3)).valueOr:
assert false, "Failed to deploy RLN contract: " & $error
return
@ -605,7 +740,7 @@ proc setupOnchainGroupManager*(
some(0.u256),
)
assert tokenApprovalResult.isOk, tokenApprovalResult.error()
assert tokenApprovalResult.isOk(), tokenApprovalResult.error
let manager = OnchainGroupManager(
ethClientUrls: @[ethClientUrl],

View File

@ -41,8 +41,8 @@ suite "Waku v2 REST API - health":
var manager {.threadVar.}: OnchainGroupManager
setup:
anvilProc = runAnvil()
manager = waitFor setupOnchainGroupManager()
anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH))
manager = waitFor setupOnchainGroupManager(deployContracts = false)
teardown:
stopAnvil(anvilProc)

@ -1 +1 @@
Subproject commit 900d4f95e0e618bdeb4c241f7a4b6347df6bb950
Subproject commit 8a338f354481e8a3f3d64a72e38fad4c62e32dcd

View File

@ -242,7 +242,7 @@ method register*(
fetchedGasPrice = fetchedGasPrice, gasPrice = calculatedGasPrice
calculatedGasPrice
let idCommitmentHex = identityCredential.idCommitment.inHex()
info "identityCredential idCommitmentHex", idCommitment = idCommitmentHex
debug "identityCredential idCommitmentHex", idCommitment = idCommitmentHex
let idCommitment = identityCredential.idCommitment.toUInt256()
let idCommitmentsToErase: seq[UInt256] = @[]
info "registering the member",
@ -259,11 +259,10 @@ method register*(
var tsReceipt: ReceiptObject
g.retryWrapper(tsReceipt, "Failed to get the transaction receipt"):
await ethRpc.getMinedTransactionReceipt(txHash)
info "registration transaction mined", txHash = txHash
debug "registration transaction mined", txHash = txHash
g.registrationTxHash = some(txHash)
# the receipt topic holds the hash of signature of the raised events
# TODO: make this robust. search within the event list for the event
info "ts receipt", receipt = tsReceipt[]
debug "ts receipt", receipt = tsReceipt[]
if tsReceipt.status.isNone():
raise newException(ValueError, "Transaction failed: status is None")
@ -272,18 +271,27 @@ method register*(
ValueError, "Transaction failed with status: " & $tsReceipt.status.get()
)
## Extract MembershipRegistered event from transaction logs (third event)
let thirdTopic = tsReceipt.logs[2].topics[0]
info "third topic", thirdTopic = thirdTopic
if thirdTopic !=
cast[FixedBytes[32]](keccak.keccak256.digest(
## Search through all transaction logs to find the MembershipRegistered event
let expectedEventSignature = cast[FixedBytes[32]](keccak.keccak256.digest(
"MembershipRegistered(uint256,uint256,uint32)"
).data):
raise newException(ValueError, "register: unexpected event signature")
).data)
## Parse MembershipRegistered event data: rateCommitment(256) || membershipRateLimit(256) || index(32)
let arguments = tsReceipt.logs[2].data
info "tx log data", arguments = arguments
var membershipRegisteredLog: Option[LogObject]
for log in tsReceipt.logs:
if log.topics.len > 0 and log.topics[0] == expectedEventSignature:
membershipRegisteredLog = some(log)
break
if membershipRegisteredLog.isNone():
raise newException(
ValueError, "register: MembershipRegistered event not found in transaction logs"
)
let registrationLog = membershipRegisteredLog.get()
## Parse MembershipRegistered event data: idCommitment(256) || membershipRateLimit(256) || index(32)
let arguments = registrationLog.data
trace "registration transaction log data", arguments = arguments
let
## Extract membership index from transaction log data (big endian)
membershipIndex = UInt256.fromBytesBE(arguments[64 .. 95])