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: matrix:
os: [ubuntu-22.04, macos-15] os: [ubuntu-22.04, macos-15]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 90 timeout-minutes: 45
name: test-${{ matrix.os }} name: test-${{ matrix.os }}
steps: steps:
@ -137,7 +137,7 @@ jobs:
nwaku-nwaku-interop-tests: nwaku-nwaku-interop-tests:
needs: build-docker-image 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: with:
node_nwaku: ${{ needs.build-docker-image.outputs.image }} 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)) server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0))
client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0))
anvilProc = runAnvil() anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH))
manager = waitFor setupOnchainGroupManager() manager = waitFor setupOnchainGroupManager(deployContracts = false)
# mount rln-relay # mount rln-relay
let wakuRlnConfig = getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) 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)) server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0))
client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0))
anvilProc = runAnvil() anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH))
manager = waitFor setupOnchainGroupManager() manager = waitFor setupOnchainGroupManager(deployContracts = false)
# mount rln-relay # mount rln-relay
let wakuRlnConfig = getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) 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 var manager {.threadVar.}: OnchainGroupManager
setup: setup:
anvilProc = runAnvil() anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH))
manager = waitFor setupOnchainGroupManager() manager = waitFor setupOnchainGroupManager(deployContracts = false)
teardown: teardown:
stopAnvil(anvilProc) stopAnvil(anvilProc)

View File

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

View File

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

View File

@ -3,7 +3,7 @@
{.push raises: [].} {.push raises: [].}
import import
std/[options, os, osproc, deques, streams, strutils, tempfiles, strformat], std/[options, os, osproc, streams, strutils, strformat],
results, results,
stew/byteutils, stew/byteutils,
testutils/unittests, testutils/unittests,
@ -14,7 +14,6 @@ import
web3/conversions, web3/conversions,
web3/eth_api_types, web3/eth_api_types,
json_rpc/rpcclient, json_rpc/rpcclient,
json,
libp2p/crypto/crypto, libp2p/crypto/crypto,
eth/keys, eth/keys,
results results
@ -24,25 +23,19 @@ import
waku_rln_relay, waku_rln_relay,
waku_rln_relay/protocol_types, waku_rln_relay/protocol_types,
waku_rln_relay/constants, waku_rln_relay/constants,
waku_rln_relay/contract,
waku_rln_relay/rln, waku_rln_relay/rln,
], ],
../testlib/common, ../testlib/common
./utils
const CHAIN_ID* = 1234'u256 const CHAIN_ID* = 1234'u256
template skip0xPrefix(hexStr: string): int = # 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
## Returns the index of the first meaningful char in `hexStr` by skipping const DEFAULT_ANVIL_STATE_PATH* =
## "0x" prefix "tests/waku_rln_relay/anvil_state/state-deployed-contracts-mint-and-approved.json.gz"
if hexStr.len > 1 and hexStr[0] == '0' and hexStr[1] in {'x', 'X'}: 2 else: 0 # The contract address of the TestStableToken used for the RLN Membership registration fee
const TOKEN_ADDRESS* = "0x5FbDB2315678afecb367f032d93F642f64180aa3"
func strip0xPrefix(s: string): string = # The contract address used ti interact with the WakuRLNV2 contract via the proxy
let prefixLen = skip0xPrefix(s) const WAKU_RLNV2_PROXY_ADDRESS* = "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707"
if prefixLen != 0:
s[prefixLen .. ^1]
else:
s
proc generateCredentials*(): IdentityCredential = proc generateCredentials*(): IdentityCredential =
let credRes = membershipKeyGen() let credRes = membershipKeyGen()
@ -106,7 +99,7 @@ proc sendMintCall(
recipientAddress: Address, recipientAddress: Address,
amountTokens: UInt256, amountTokens: UInt256,
recipientBalanceBeforeExpectedTokens: Option[UInt256] = none(UInt256), recipientBalanceBeforeExpectedTokens: Option[UInt256] = none(UInt256),
): Future[TxHash] {.async.} = ): Future[void] {.async.} =
let doBalanceAssert = recipientBalanceBeforeExpectedTokens.isSome() let doBalanceAssert = recipientBalanceBeforeExpectedTokens.isSome()
if doBalanceAssert: if doBalanceAssert:
@ -142,7 +135,7 @@ proc sendMintCall(
tx.data = Opt.some(byteutils.hexToSeqByte(mintCallData)) tx.data = Opt.some(byteutils.hexToSeqByte(mintCallData))
trace "Sending mint call" trace "Sending mint call"
let txHash = await web3.send(tx) discard await web3.send(tx)
let balanceOfSelector = "0x70a08231" let balanceOfSelector = "0x70a08231"
let balanceCallData = balanceOfSelector & paddedAddress let balanceCallData = balanceOfSelector & paddedAddress
@ -157,8 +150,6 @@ proc sendMintCall(
assert balanceAfterMint == balanceAfterExpectedTokens, assert balanceAfterMint == balanceAfterExpectedTokens,
fmt"Balance is {balanceAfterMint} after transfer but expected {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) # 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( proc checkTokenAllowance(
web3: Web3, tokenAddress: Address, owner: Address, spender: Address web3: Web3, tokenAddress: Address, owner: Address, spender: Address
@ -487,20 +478,64 @@ proc getAnvilPath*(): string =
anvilPath = joinPath(anvilPath, ".foundry/bin/anvil") anvilPath = joinPath(anvilPath, ".foundry/bin/anvil")
return $anvilPath 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 # 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 # Passed options are
# --port Port to listen on. # --port Port to listen on.
# --gas-limit Sets the block gas limit in WEI. # --gas-limit Sets the block gas limit in WEI.
# --balance The default account balance, specified in ether. # --balance The default account balance, specified in ether.
# --chain-id Chain ID of the network. # --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 # See anvil documentation https://book.getfoundry.sh/reference/anvil/ for more details
try: try:
let anvilPath = getAnvilPath() let anvilPath = getAnvilPath()
info "Anvil path", anvilPath info "Anvil path", anvilPath
let runAnvil = startProcess(
anvilPath, var args =
args = [ @[
"--port", "--port",
$port, $port,
"--gas-limit", "--gas-limit",
@ -509,9 +544,54 @@ proc runAnvil*(port: int = 8540, chainId: string = "1234"): Process =
"1000000000", "1000000000",
"--chain-id", "--chain-id",
$chainId, $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 let anvilPID = runAnvil.processID
# We read stdout from Anvil to see when daemon is ready # We read stdout from Anvil to see when daemon is ready
@ -549,6 +629,13 @@ proc stopAnvil*(runAnvil: Process) {.used.} =
# Send termination signals # Send termination signals
when not defined(windows): when not defined(windows):
discard execCmdEx(fmt"kill -TERM {anvilPID}") 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}") discard execCmdEx(fmt"kill -9 {anvilPID}")
else: else:
discard execCmdEx(fmt"taskkill /F /PID {anvilPID}") 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 info "Error stopping Anvil daemon", anvilPID = anvilPID, error = e.msg
proc setupOnchainGroupManager*( proc setupOnchainGroupManager*(
ethClientUrl: string = EthClient, amountEth: UInt256 = 10.u256 ethClientUrl: string = EthClient,
amountEth: UInt256 = 10.u256,
deployContracts: bool = true,
): Future[OnchainGroupManager] {.async.} = ): 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() let rlnInstanceRes = createRlnInstance()
check: check:
rlnInstanceRes.isOk() rlnInstanceRes.isOk()
let rlnInstance = rlnInstanceRes.get() let rlnInstance = rlnInstanceRes.get()
# connect to the eth client
let web3 = await newWeb3(ethClientUrl) let web3 = await newWeb3(ethClientUrl)
let accounts = await web3.provider.eth_accounts() let accounts = await web3.provider.eth_accounts()
web3.defaultAccount = accounts[1] 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 if not deployContracts:
# the send procedure returns a tx hash that we don't use, hence discard 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( discard await sendEthTransfer(
web3, web3.defaultAccount, acc, ethToWei(1000.u256), some(0.u256) 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 assert false, "Failed to deploy test token contract: " & $error
return return
# mint the token from the generated account # mint the token from the generated account
discard await sendMintCall( await sendMintCall(
web3, web3.defaultAccount, testTokenAddress, acc, ethToWei(1000.u256), some(0.u256) 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 assert false, "Failed to deploy RLN contract: " & $error
return return
@ -605,7 +740,7 @@ proc setupOnchainGroupManager*(
some(0.u256), some(0.u256),
) )
assert tokenApprovalResult.isOk, tokenApprovalResult.error() assert tokenApprovalResult.isOk(), tokenApprovalResult.error
let manager = OnchainGroupManager( let manager = OnchainGroupManager(
ethClientUrls: @[ethClientUrl], ethClientUrls: @[ethClientUrl],

View File

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

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

View File

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