chore: allow multiple rln eth clients (#3402)

* use of multiple Eth clients instead of just one
* config_chat2 enhance param comment
* group_manager: raise exception if could not connect to any of the eth clients
This commit is contained in:
Ivan FB 2025-05-12 10:57:13 +02:00 committed by GitHub
parent d86babac3a
commit 42ab866f2c
14 changed files with 80 additions and 52 deletions

View File

@ -560,8 +560,7 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} =
dynamic: conf.rlnRelayDynamic, dynamic: conf.rlnRelayDynamic,
credIndex: conf.rlnRelayCredIndex, credIndex: conf.rlnRelayCredIndex,
chainId: conf.rlnRelayChainId, chainId: conf.rlnRelayChainId,
ethContractAddress: conf.rlnRelayEthContractAddress, ethClientUrls: conf.ethClientUrls.mapIt(string(it)),
ethClientAddress: string(conf.rlnRelayethClientAddress),
creds: some( creds: some(
RlnRelayCreds( RlnRelayCreds(
path: conf.rlnRelayCredPath, password: conf.rlnRelayCredPassword path: conf.rlnRelayCredPath, password: conf.rlnRelayCredPassword

View File

@ -18,7 +18,8 @@ type
prod prod
test test
EthRpcUrl = distinct string EthRpcUrl* = distinct string
Chat2Conf* = object ## General node config Chat2Conf* = object ## General node config
logLevel* {. logLevel* {.
desc: "Sets the log level.", defaultValue: LogLevel.INFO, name: "log-level" desc: "Sets the log level.", defaultValue: LogLevel.INFO, name: "log-level"
@ -248,11 +249,12 @@ type
name: "rln-relay-id-commitment-key" name: "rln-relay-id-commitment-key"
.}: string .}: string
rlnRelayEthClientAddress* {. ethClientUrls* {.
desc: "HTTP address of an Ethereum testnet client e.g., http://localhost:8540/", desc:
defaultValue: "http://localhost:8540/", "HTTP address of an Ethereum testnet client e.g., http://localhost:8540/. Argument may be repeated.",
defaultValue: newSeq[EthRpcUrl](0),
name: "rln-relay-eth-client-address" name: "rln-relay-eth-client-address"
.}: EthRpcUrl .}: seq[EthRpcUrl]
rlnRelayEthContractAddress* {. rlnRelayEthContractAddress* {.
desc: "Address of membership contract on an Ethereum testnet", desc: "Address of membership contract on an Ethereum testnet",

View File

@ -638,7 +638,7 @@ when isMainModule:
dynamic: conf.rlnRelayDynamic, dynamic: conf.rlnRelayDynamic,
credIndex: some(uint(0)), credIndex: some(uint(0)),
ethContractAddress: conf.rlnRelayEthContractAddress, ethContractAddress: conf.rlnRelayEthContractAddress,
ethClientAddress: string(conf.rlnRelayethClientAddress), ethClientUrls: conf.ethClientUrls.mapIt(string(it)),
treePath: conf.rlnRelayTreePath, treePath: conf.rlnRelayTreePath,
epochSizeSec: conf.rlnEpochSizeSec, epochSizeSec: conf.rlnEpochSizeSec,
creds: none(RlnRelayCreds), creds: none(RlnRelayCreds),

View File

@ -8,7 +8,7 @@ import
stew/shims/net, stew/shims/net,
regex regex
type EthRpcUrl = distinct string type EthRpcUrl* = distinct string
type NetworkMonitorConf* = object type NetworkMonitorConf* = object
logLevel* {. logLevel* {.
@ -82,11 +82,12 @@ type NetworkMonitorConf* = object
name: "rln-relay-tree-path" name: "rln-relay-tree-path"
.}: string .}: string
rlnRelayEthClientAddress* {. ethClientUrls* {.
desc: "HTTP address of an Ethereum testnet client e.g., http://localhost:8540/", desc:
defaultValue: "http://localhost:8540/", "HTTP address of an Ethereum testnet client e.g., http://localhost:8540/. Argument may be repeated.",
defaultValue: newSeq[EthRpcUrl](0),
name: "rln-relay-eth-client-address" name: "rln-relay-eth-client-address"
.}: EthRpcUrl .}: seq[EthRpcUrl]
rlnRelayEthContractAddress* {. rlnRelayEthContractAddress* {.
desc: "Address of membership contract on an Ethereum testnet", desc: "Address of membership contract on an Ethereum testnet",

View File

@ -26,7 +26,7 @@ suite "Waku config - apply preset":
cmd: noCommand, cmd: noCommand,
preset: "twn", preset: "twn",
relay: true, relay: true,
rlnRelayEthClientAddress: "http://someaddress".EthRpcUrl, ethClientUrls: @["http://someaddress".EthRpcUrl],
rlnRelayTreePath: "/tmp/sometreepath", rlnRelayTreePath: "/tmp/sometreepath",
) )
@ -109,7 +109,7 @@ suite "Waku config - apply preset":
cmd: noCommand, cmd: noCommand,
clusterId: 1.uint16, clusterId: 1.uint16,
relay: true, relay: true,
rlnRelayEthClientAddress: "http://someaddress".EthRpcUrl, ethClientUrls: @["http://someaddress".EthRpcUrl],
rlnRelayTreePath: "/tmp/sometreepath", rlnRelayTreePath: "/tmp/sometreepath",
) )

View File

@ -77,10 +77,10 @@ suite "Onchain group manager":
assert metadata.contractAddress == manager.ethContractAddress, assert metadata.contractAddress == manager.ethContractAddress,
"contractAddress is not equal to " & manager.ethContractAddress "contractAddress is not equal to " & manager.ethContractAddress
let differentContractAddress = await uploadRLNContract(manager.ethClientUrl) let differentContractAddress = await uploadRLNContract(manager.ethClientUrls[0])
# simulating a change in the contractAddress # simulating a change in the contractAddress
let manager2 = OnchainGroupManager( let manager2 = OnchainGroupManager(
ethClientUrl: EthClient, ethClientUrls: @[EthClient],
ethContractAddress: $differentContractAddress, ethContractAddress: $differentContractAddress,
rlnInstance: manager.rlnInstance, rlnInstance: manager.rlnInstance,
onFatalErrorAction: proc(errStr: string) = onFatalErrorAction: proc(errStr: string) =

View File

@ -250,7 +250,7 @@ proc stopAnvil*(runAnvil: Process) {.used.} =
error "Anvil daemon termination failed: ", err = getCurrentExceptionMsg() error "Anvil daemon termination failed: ", err = getCurrentExceptionMsg()
proc setupOnchainGroupManager*( proc setupOnchainGroupManager*(
ethClientAddress: string = EthClient, amountEth: UInt256 = 10.u256 ethClientUrl: string = EthClient, amountEth: UInt256 = 10.u256
): Future[OnchainGroupManager] {.async.} = ): Future[OnchainGroupManager] {.async.} =
let rlnInstanceRes = let rlnInstanceRes =
createRlnInstance(tree_path = genTempPath("rln_tree", "group_manager_onchain")) createRlnInstance(tree_path = genTempPath("rln_tree", "group_manager_onchain"))
@ -259,9 +259,9 @@ proc setupOnchainGroupManager*(
let rlnInstance = rlnInstanceRes.get() let rlnInstance = rlnInstanceRes.get()
let contractAddress = await uploadRLNContract(ethClientAddress) let contractAddress = await uploadRLNContract(ethClientUrl)
# connect to the eth client # connect to the eth client
let web3 = await newWeb3(ethClientAddress) let web3 = await newWeb3(ethClientUrl)
let accounts = await web3.provider.eth_accounts() let accounts = await web3.provider.eth_accounts()
web3.defaultAccount = accounts[0] web3.defaultAccount = accounts[0]
@ -275,7 +275,7 @@ proc setupOnchainGroupManager*(
) )
let manager = OnchainGroupManager( let manager = OnchainGroupManager(
ethClientUrl: ethClientAddress, ethClientUrls: @[ethClientUrl],
ethContractAddress: $contractAddress, ethContractAddress: $contractAddress,
chainId: CHAIN_ID, chainId: CHAIN_ID,
ethPrivateKey: some($privateKey), ethPrivateKey: some($privateKey),

View File

@ -3,7 +3,7 @@ when (NimMajor, NimMinor) < (1, 4):
else: else:
{.push raises: [].} {.push raises: [].}
import chronicles, results, std/tempfiles import chronicles, results, std/[tempfiles, sequtils]
import import
waku/[ waku/[
@ -19,7 +19,7 @@ logScope:
type RlnKeystoreGeneratorConf* = object type RlnKeystoreGeneratorConf* = object
execute*: bool execute*: bool
ethContractAddress*: string ethContractAddress*: string
ethClientAddress*: string ethClientUrls*: seq[string]
chainId*: uint chainId*: uint
credPath*: string credPath*: string
credPassword*: string credPassword*: string
@ -65,7 +65,7 @@ proc doRlnKeystoreGenerator*(conf: RlnKeystoreGeneratorConf) =
# 4. initialize OnchainGroupManager # 4. initialize OnchainGroupManager
let groupManager = OnchainGroupManager( let groupManager = OnchainGroupManager(
ethClientUrl: string(conf.ethClientAddress), ethClientUrls: conf.ethClientUrls,
chainId: conf.chainId, chainId: conf.chainId,
ethContractAddress: conf.ethContractAddress, ethContractAddress: conf.ethContractAddress,
rlnInstance: rlnInstance, rlnInstance: rlnInstance,

View File

@ -9,9 +9,8 @@ logScope:
############################## ##############################
type RlnRelayConfBuilder* = object type RlnRelayConfBuilder* = object
enabled*: Option[bool] enabled*: Option[bool]
chainId*: Option[uint] chainId*: Option[uint]
ethClientAddress*: Option[string] ethClientUrls*: Option[seq[string]]
ethContractAddress*: Option[string] ethContractAddress*: Option[string]
credIndex*: Option[uint] credIndex*: Option[uint]
credPassword*: Option[string] credPassword*: Option[string]
@ -42,8 +41,8 @@ proc withCredPath*(b: var RlnRelayConfBuilder, credPath: string) =
proc withDynamic*(b: var RlnRelayConfBuilder, dynamic: bool) = proc withDynamic*(b: var RlnRelayConfBuilder, dynamic: bool) =
b.dynamic = some(dynamic) b.dynamic = some(dynamic)
proc withEthClientAddress*(b: var RlnRelayConfBuilder, ethClientAddress: string) = proc withEthClientUrls*(b: var RlnRelayConfBuilder, ethClientUrls: seq[string]) =
b.ethClientAddress = some(ethClientAddress) b.ethClientUrls = some(ethClientUrls)
proc withEthContractAddress*(b: var RlnRelayConfBuilder, ethContractAddress: string) = proc withEthContractAddress*(b: var RlnRelayConfBuilder, ethContractAddress: string) =
b.ethContractAddress = some(ethContractAddress) b.ethContractAddress = some(ethContractAddress)
@ -76,8 +75,8 @@ proc build*(b: RlnRelayConfBuilder): Result[Option[RlnRelayConf], string] =
if b.dynamic.isNone(): if b.dynamic.isNone():
return err("rlnRelay.dynamic is not specified") return err("rlnRelay.dynamic is not specified")
if b.ethClientAddress.get("") == "": if b.ethClientUrls.get(newSeq[string](0)).len == 0:
return err("rlnRelay.ethClientAddress is not specified") return err("rlnRelay.ethClientUrls is not specified")
if b.ethContractAddress.get("") == "": if b.ethContractAddress.get("") == "":
return err("rlnRelay.ethContractAddress is not specified") return err("rlnRelay.ethContractAddress is not specified")
if b.epochSizeSec.isNone(): if b.epochSizeSec.isNone():
@ -94,7 +93,7 @@ proc build*(b: RlnRelayConfBuilder): Result[Option[RlnRelayConf], string] =
credIndex: b.credIndex, credIndex: b.credIndex,
creds: creds, creds: creds,
dynamic: b.dynamic.get(), dynamic: b.dynamic.get(),
ethClientAddress: b.ethClientAddress.get(), ethClientUrls: b.ethClientUrls.get(),
ethContractAddress: b.ethContractAddress.get(), ethContractAddress: b.ethContractAddress.get(),
epochSizeSec: b.epochSizeSec.get(), epochSizeSec: b.epochSizeSec.get(),
userMessageLimit: b.userMessageLimit.get(), userMessageLimit: b.userMessageLimit.get(),

View File

@ -1,5 +1,5 @@
import import
std/[strutils, strformat], std/[strutils, strformat, sequtils],
results, results,
chronicles, chronicles,
chronos, chronos,
@ -75,11 +75,12 @@ type WakuNodeConf* = object
name: "rln-relay-cred-path" name: "rln-relay-cred-path"
.}: string .}: string
rlnRelayEthClientAddress* {. ethClientUrls* {.
desc: "HTTP address of an Ethereum testnet client e.g., http://localhost:8540/", desc:
defaultValue: "http://localhost:8540/", "HTTP address of an Ethereum testnet client e.g., http://localhost:8540/. Argument may be repeated.",
defaultValue: @[EthRpcUrl("http://localhost:8540/")],
name: "rln-relay-eth-client-address" name: "rln-relay-eth-client-address"
.}: EthRpcUrl .}: seq[EthRpcUrl]
rlnRelayEthContractAddress* {. rlnRelayEthContractAddress* {.
desc: "Address of membership contract on an Ethereum testnet.", desc: "Address of membership contract on an Ethereum testnet.",
@ -867,7 +868,7 @@ proc toKeystoreGeneratorConf*(n: WakuNodeConf): RlnKeystoreGeneratorConf =
RlnKeystoreGeneratorConf( RlnKeystoreGeneratorConf(
execute: n.execute, execute: n.execute,
chainId: n.rlnRelayChainId, chainId: n.rlnRelayChainId,
ethClientAddress: n.rlnRelayEthClientAddress.string, ethClientUrls: n.ethClientUrls.mapIt(string(it)),
ethContractAddress: n.rlnRelayEthContractAddress, ethContractAddress: n.rlnRelayEthContractAddress,
userMessageLimit: n.rlnRelayUserMessageLimit, userMessageLimit: n.rlnRelayUserMessageLimit,
ethPrivateKey: n.rlnRelayEthPrivateKey, ethPrivateKey: n.rlnRelayEthPrivateKey,
@ -907,8 +908,8 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
b.rlnRelayConf.withCredPath(n.rlnRelayCredPath) b.rlnRelayConf.withCredPath(n.rlnRelayCredPath)
if n.rlnRelayCredPassword != "": if n.rlnRelayCredPassword != "":
b.rlnRelayConf.withCredPassword(n.rlnRelayCredPassword) b.rlnRelayConf.withCredPassword(n.rlnRelayCredPassword)
if n.rlnRelayEthClientAddress.string != "": if n.ethClientUrls.len > 0:
b.rlnRelayConf.withEthClientAddress(n.rlnRelayEthClientAddress.string) b.rlnRelayConf.withEthClientUrls(n.ethClientUrls.mapIt(string(it)))
if n.rlnRelayEthContractAddress != "": if n.rlnRelayEthContractAddress != "":
b.rlnRelayConf.withEthContractAddress(n.rlnRelayEthContractAddress) b.rlnRelayConf.withEthContractAddress(n.rlnRelayEthContractAddress)

View File

@ -354,7 +354,7 @@ proc setupProtocols(
credIndex: rlnRelayConf.credIndex, credIndex: rlnRelayConf.credIndex,
ethContractAddress: rlnRelayConf.ethContractAddress, ethContractAddress: rlnRelayConf.ethContractAddress,
chainId: rlnRelayConf.chainId, chainId: rlnRelayConf.chainId,
ethClientAddress: rlnRelayConf.ethClientAddress, ethClientUrls: rlnRelayConf.ethClientUrls,
creds: rlnRelayConf.creds, creds: rlnRelayConf.creds,
treePath: rlnRelayConf.treePath, treePath: rlnRelayConf.treePath,
userMessageLimit: rlnRelayConf.userMessageLimit, userMessageLimit: rlnRelayConf.userMessageLimit,

View File

@ -160,7 +160,7 @@ proc logConf*(conf: WakuConf) =
maxMessageSize = conf.maxMessageSizeBytes, maxMessageSize = conf.maxMessageSizeBytes,
rlnEpochSizeSec = rlnRelayConf.epochSizeSec, rlnEpochSizeSec = rlnRelayConf.epochSizeSec,
rlnRelayUserMessageLimit = rlnRelayConf.userMessageLimit, rlnRelayUserMessageLimit = rlnRelayConf.userMessageLimit,
rlnRelayEthClientAddress = string(rlnRelayConf.ethClientAddress) ethClientUrls = rlnRelayConf.ethClientUrls
proc validateNodeKey(wakuConf: WakuConf): Result[void, string] = proc validateNodeKey(wakuConf: WakuConf): Result[void, string] =
wakuConf.nodeKey.getPublicKey().isOkOr: wakuConf.nodeKey.getPublicKey().isOkOr:
@ -224,8 +224,8 @@ proc validateNoEmptyStrings(wakuConf: WakuConf): Result[void, string] =
if isEmptyOrWhiteSpace(rlnRelayConf.treePath): if isEmptyOrWhiteSpace(rlnRelayConf.treePath):
return err("rlnRelayConf.treepath is an empty string") return err("rlnRelayConf.treepath is an empty string")
if isEmptyOrWhiteSpace(rlnRelayConf.ethClientAddress): if rlnRelayConf.ethClientUrls.len == 0:
return err("rlnRelayConf.ethClientAddress is an empty string") return err("rlnRelayConf.ethClientUrls is empty")
if isEmptyOrWhiteSpace(rlnRelayConf.ethContractAddress): if isEmptyOrWhiteSpace(rlnRelayConf.ethContractAddress):
return err("rlnRelayConf.ethContractAddress is an empty string") return err("rlnRelayConf.ethContractAddress is an empty string")

View File

@ -51,7 +51,7 @@ contract(WakuRlnContract):
type type
WakuRlnContractWithSender = Sender[WakuRlnContract] WakuRlnContractWithSender = Sender[WakuRlnContract]
OnchainGroupManager* = ref object of GroupManager OnchainGroupManager* = ref object of GroupManager
ethClientUrl*: string ethClientUrls*: seq[string]
ethPrivateKey*: Option[string] ethPrivateKey*: Option[string]
ethContractAddress*: string ethContractAddress*: string
ethRpc*: Option[Web3] ethRpc*: Option[Web3]
@ -458,11 +458,35 @@ method onRegister*(g: OnchainGroupManager, cb: OnRegisterCallback) {.gcsafe.} =
method onWithdraw*(g: OnchainGroupManager, cb: OnWithdrawCallback) {.gcsafe.} = method onWithdraw*(g: OnchainGroupManager, cb: OnWithdrawCallback) {.gcsafe.} =
g.withdrawCb = some(cb) g.withdrawCb = some(cb)
proc establishConnection(
g: OnchainGroupManager
): Future[GroupManagerResult[Web3]] {.async.} =
var ethRpc: Web3
g.retryWrapper(ethRpc, "Failed to connect to the Ethereum client"):
var innerEthRpc: Web3
var connected = false
for clientUrl in g.ethClientUrls:
## We give a chance to the user to provide multiple clients
## and we try to connect to each of them
try:
innerEthRpc = await newWeb3(clientUrl)
connected = true
break
except CatchableError:
error "failed connect Eth client", error = getCurrentExceptionMsg()
if not connected:
raise newException(CatchableError, "all failed")
innerEthRpc
return ok(ethRpc)
method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.} = method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.} =
# check if the Ethereum client is reachable # check if the Ethereum client is reachable
var ethRpc: Web3 let ethRpc: Web3 = (await establishConnection(g)).valueOr:
g.retryWrapper(ethRpc, "Failed to connect to the Ethereum client"): return err("failed to connect to Ethereum clients: " & $error)
await newWeb3(g.ethClientUrl)
var fetchedChainId: uint var fetchedChainId: uint
g.retryWrapper(fetchedChainId, "Failed to get the chain id"): g.retryWrapper(fetchedChainId, "Failed to get the chain id"):
@ -544,9 +568,11 @@ method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.}
proc onDisconnect() {.async.} = proc onDisconnect() {.async.} =
error "Ethereum client disconnected" error "Ethereum client disconnected"
var newEthRpc: Web3
g.retryWrapper(newEthRpc, "Failed to reconnect with the Ethereum client"): var newEthRpc: Web3 = (await g.establishConnection()).valueOr:
await newWeb3(g.ethClientUrl) g.onFatalErrorAction("failed to connect to Ethereum clients onDisconnect")
return
newEthRpc.ondisconnect = ethRpc.ondisconnect newEthRpc.ondisconnect = ethRpc.ondisconnect
g.ethRpc = some(newEthRpc) g.ethRpc = some(newEthRpc)

View File

@ -43,7 +43,7 @@ type RlnRelayConf* = object of RootObj
dynamic*: bool dynamic*: bool
credIndex*: Option[uint] credIndex*: Option[uint]
ethContractAddress*: string ethContractAddress*: string
ethClientAddress*: string ethClientUrls*: seq[string]
chainId*: uint chainId*: uint
creds*: Option[RlnRelayCreds] creds*: Option[RlnRelayCreds]
treePath*: string treePath*: string
@ -455,7 +455,7 @@ proc mount(
(none(string), none(string)) (none(string), none(string))
groupManager = OnchainGroupManager( groupManager = OnchainGroupManager(
ethClientUrl: string(conf.ethClientAddress), ethClientUrls: conf.ethClientUrls,
ethContractAddress: $conf.ethContractAddress, ethContractAddress: $conf.ethContractAddress,
chainId: conf.chainId, chainId: conf.chainId,
rlnInstance: rlnInstance, rlnInstance: rlnInstance,