Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions tests/waku_rln_relay/test_rln_group_manager_onchain.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions tests/waku_rln_relay/test_waku_rln_relay.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions tests/waku_rln_relay/test_wakunode_rln_relay.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
186 changes: 133 additions & 53 deletions tests/waku_rln_relay/utils_onchain.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -14,7 +14,6 @@ import
web3/conversions,
web3/eth_api_types,
json_rpc/rpcclient,
json,
libp2p/crypto/crypto,
eth/keys,
results
Expand All @@ -24,25 +23,15 @@ 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
const DEFAULT_ANVIL_STATE_PATH* =
"tests/waku_rln_relay/anvil_state/state-deployed-contracts-mint-and-approved.json"
const TOKEN_ADDRESS* = "0x5FbDB2315678afecb367f032d93F642f64180aa3"
const WAKU_RLNV2_PROXY_ADDRESS* = "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707"

proc generateCredentials*(): IdentityCredential =
let credRes = membershipKeyGen()
Expand Down Expand Up @@ -488,19 +477,26 @@ proc getAnvilPath*(): string =
return $anvilPath

# 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",
Expand All @@ -509,9 +505,45 @@ 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():
let statePath = stateFile.get()
info "State file parameter provided",
statePath = statePath,
dumpStateOnExit = dumpStateOnExit,
absolutePath = absolutePath(statePath)

# Ensure the directory exists
let stateDir = parentDir(statePath)
if not dirExists(stateDir):
info "Creating state directory", dir = stateDir
createDir(stateDir)

# Use --load-state (read-only) when we want to use cached state without modifying it
# Use --dump-state (write-only) when we want to create a new cache from fresh deployment
if dumpStateOnExit:
# 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
Expand Down Expand Up @@ -560,52 +592,100 @@ 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
discard await sendEthTransfer(
web3, web3.defaultAccount, acc, ethToWei(1000.u256), some(0.u256)
)
if not deployContracts:
info "Using contract addresses from constants"

let testTokenAddress = (await deployTestToken(privateKey, acc, web3)).valueOr:
assert false, "Failed to deploy test token contract: " & $error
return
testTokenAddress = Address(hexToByteArray[20](TOKEN_ADDRESS))
contractAddress = Address(hexToByteArray[20](WAKU_RLNV2_PROXY_ADDRESS))

# mint the token from the generated account
discard await sendMintCall(
web3, web3.defaultAccount, testTokenAddress, acc, ethToWei(1000.u256), some(0.u256)
)
(privateKey, acc) = createEthAccount(web3)

let contractAddress = (await executeForgeContractDeployScripts(privateKey, acc, web3)).valueOr:
assert false, "Failed to deploy RLN contract: " & $error
return
# Fund the test account
discard await sendEthTransfer(web3, web3.defaultAccount, acc, ethToWei(1000.u256))

# 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,
privateKey,
testTokenAddress,
contractAddress,
ethToWei(200.u256),
some(0.u256),
)
# Mint tokens to the test account
discard 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)
)

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

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,
privateKey,
testTokenAddress,
contractAddress,
ethToWei(200.u256),
some(0.u256),
)

assert tokenApprovalResult.isOk, tokenApprovalResult.error()
assert tokenApprovalResult.isOk, tokenApprovalResult.error()

let manager = OnchainGroupManager(
ethClientUrls: @[ethClientUrl],
Expand Down
2 changes: 1 addition & 1 deletion vendor/waku-rlnv2-contract
40 changes: 24 additions & 16 deletions waku/waku_rln_relay/group_manager/on_chain/group_manager.nim
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ method register*(
g.retryWrapper(gasPrice, "Failed to get gas price"):
int(await ethRpc.provider.eth_gasPrice()) * 2
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",
Expand All @@ -248,11 +248,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")
Expand All @@ -261,18 +260,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(
"MembershipRegistered(uint256,uint256,uint32)"
).data):
raise newException(ValueError, "register: unexpected event signature")

## Parse MembershipRegistered event data: rateCommitment(256) || membershipRateLimit(256) || index(32)
let arguments = tsReceipt.logs[2].data
info "tx log data", arguments = arguments
## Search through all transaction logs to find the MembershipRegistered event
let expectedEventSignature = cast[FixedBytes[32]](keccak.keccak256.digest(
"MembershipRegistered(uint256,uint256,uint32)"
).data)

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()
Comment on lines +285 to +290
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, let's better use valueOr or isOkOr :)

Suggested change
if membershipRegisteredLog.isNone():
raise newException(
ValueError, "register: MembershipRegistered event not found in transaction logs"
)
let registrationLog = membershipRegisteredLog.get()
let registrationLog = membershipRegisteredLog.valueOr:
raise newException(
ValueError, "register: MembershipRegistered event not found in transaction logs"
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, let's better use valueOr or isOkOr :)

The membershipRegisteredLog is of type Option which doesn't seem to work with valueOr or isOkOr, they work with Result
It doesn't quite make sense to me to make membershipRegisteredLog of type Result.
I'll try to see if this section of code can be reworked to fit the required coding style better.


## 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])
Expand Down