diff --git a/apps/chat2/chat2.nim b/apps/chat2/chat2.nim index c25ce86d4..127a761c0 100644 --- a/apps/chat2/chat2.nim +++ b/apps/chat2/chat2.nim @@ -560,8 +560,7 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = dynamic: conf.rlnRelayDynamic, credIndex: conf.rlnRelayCredIndex, chainId: conf.rlnRelayChainId, - ethContractAddress: conf.rlnRelayEthContractAddress, - ethClientAddress: string(conf.rlnRelayethClientAddress), + ethClientUrls: conf.ethClientUrls.mapIt(string(it)), creds: some( RlnRelayCreds( path: conf.rlnRelayCredPath, password: conf.rlnRelayCredPassword diff --git a/apps/chat2/config_chat2.nim b/apps/chat2/config_chat2.nim index 830222cd9..8cc525208 100644 --- a/apps/chat2/config_chat2.nim +++ b/apps/chat2/config_chat2.nim @@ -18,7 +18,8 @@ type prod test - EthRpcUrl = distinct string + EthRpcUrl* = distinct string + Chat2Conf* = object ## General node config logLevel* {. desc: "Sets the log level.", defaultValue: LogLevel.INFO, name: "log-level" @@ -248,11 +249,12 @@ type name: "rln-relay-id-commitment-key" .}: string - rlnRelayEthClientAddress* {. - desc: "HTTP address of an Ethereum testnet client e.g., http://localhost:8540/", - defaultValue: "http://localhost:8540/", + ethClientUrls* {. + desc: + "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" - .}: EthRpcUrl + .}: seq[EthRpcUrl] rlnRelayEthContractAddress* {. desc: "Address of membership contract on an Ethereum testnet", diff --git a/apps/networkmonitor/networkmonitor.nim b/apps/networkmonitor/networkmonitor.nim index c8b8fe092..7b71a630e 100644 --- a/apps/networkmonitor/networkmonitor.nim +++ b/apps/networkmonitor/networkmonitor.nim @@ -638,7 +638,7 @@ when isMainModule: dynamic: conf.rlnRelayDynamic, credIndex: some(uint(0)), ethContractAddress: conf.rlnRelayEthContractAddress, - ethClientAddress: string(conf.rlnRelayethClientAddress), + ethClientUrls: conf.ethClientUrls.mapIt(string(it)), treePath: conf.rlnRelayTreePath, epochSizeSec: conf.rlnEpochSizeSec, creds: none(RlnRelayCreds), diff --git a/apps/networkmonitor/networkmonitor_config.nim b/apps/networkmonitor/networkmonitor_config.nim index bf1662649..04245f9dd 100644 --- a/apps/networkmonitor/networkmonitor_config.nim +++ b/apps/networkmonitor/networkmonitor_config.nim @@ -8,7 +8,7 @@ import stew/shims/net, regex -type EthRpcUrl = distinct string +type EthRpcUrl* = distinct string type NetworkMonitorConf* = object logLevel* {. @@ -82,11 +82,12 @@ type NetworkMonitorConf* = object name: "rln-relay-tree-path" .}: string - rlnRelayEthClientAddress* {. - desc: "HTTP address of an Ethereum testnet client e.g., http://localhost:8540/", - defaultValue: "http://localhost:8540/", + ethClientUrls* {. + desc: + "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" - .}: EthRpcUrl + .}: seq[EthRpcUrl] rlnRelayEthContractAddress* {. desc: "Address of membership contract on an Ethereum testnet", diff --git a/tests/factory/test_external_config.nim b/tests/factory/test_external_config.nim index 1caeb6e7b..5bd4e2c86 100644 --- a/tests/factory/test_external_config.nim +++ b/tests/factory/test_external_config.nim @@ -26,7 +26,7 @@ suite "Waku config - apply preset": cmd: noCommand, preset: "twn", relay: true, - rlnRelayEthClientAddress: "http://someaddress".EthRpcUrl, + ethClientUrls: @["http://someaddress".EthRpcUrl], rlnRelayTreePath: "/tmp/sometreepath", ) @@ -109,7 +109,7 @@ suite "Waku config - apply preset": cmd: noCommand, clusterId: 1.uint16, relay: true, - rlnRelayEthClientAddress: "http://someaddress".EthRpcUrl, + ethClientUrls: @["http://someaddress".EthRpcUrl], rlnRelayTreePath: "/tmp/sometreepath", ) diff --git a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim index 25a3166ce..7ba64e39b 100644 --- a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim +++ b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim @@ -77,10 +77,10 @@ suite "Onchain group manager": assert metadata.contractAddress == 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 let manager2 = OnchainGroupManager( - ethClientUrl: EthClient, + ethClientUrls: @[EthClient], ethContractAddress: $differentContractAddress, rlnInstance: manager.rlnInstance, onFatalErrorAction: proc(errStr: string) = diff --git a/tests/waku_rln_relay/utils_onchain.nim b/tests/waku_rln_relay/utils_onchain.nim index 82eaf085e..433f865c4 100644 --- a/tests/waku_rln_relay/utils_onchain.nim +++ b/tests/waku_rln_relay/utils_onchain.nim @@ -250,7 +250,7 @@ proc stopAnvil*(runAnvil: Process) {.used.} = error "Anvil daemon termination failed: ", err = getCurrentExceptionMsg() proc setupOnchainGroupManager*( - ethClientAddress: string = EthClient, amountEth: UInt256 = 10.u256 + ethClientUrl: string = EthClient, amountEth: UInt256 = 10.u256 ): Future[OnchainGroupManager] {.async.} = let rlnInstanceRes = createRlnInstance(tree_path = genTempPath("rln_tree", "group_manager_onchain")) @@ -259,9 +259,9 @@ proc setupOnchainGroupManager*( let rlnInstance = rlnInstanceRes.get() - let contractAddress = await uploadRLNContract(ethClientAddress) + let contractAddress = await uploadRLNContract(ethClientUrl) # connect to the eth client - let web3 = await newWeb3(ethClientAddress) + let web3 = await newWeb3(ethClientUrl) let accounts = await web3.provider.eth_accounts() web3.defaultAccount = accounts[0] @@ -275,7 +275,7 @@ proc setupOnchainGroupManager*( ) let manager = OnchainGroupManager( - ethClientUrl: ethClientAddress, + ethClientUrls: @[ethClientUrl], ethContractAddress: $contractAddress, chainId: CHAIN_ID, ethPrivateKey: some($privateKey), diff --git a/tools/rln_keystore_generator/rln_keystore_generator.nim b/tools/rln_keystore_generator/rln_keystore_generator.nim index 0ca1c9968..cd501e52d 100644 --- a/tools/rln_keystore_generator/rln_keystore_generator.nim +++ b/tools/rln_keystore_generator/rln_keystore_generator.nim @@ -3,7 +3,7 @@ when (NimMajor, NimMinor) < (1, 4): else: {.push raises: [].} -import chronicles, results, std/tempfiles +import chronicles, results, std/[tempfiles, sequtils] import waku/[ @@ -19,7 +19,7 @@ logScope: type RlnKeystoreGeneratorConf* = object execute*: bool ethContractAddress*: string - ethClientAddress*: string + ethClientUrls*: seq[string] chainId*: uint credPath*: string credPassword*: string @@ -65,7 +65,7 @@ proc doRlnKeystoreGenerator*(conf: RlnKeystoreGeneratorConf) = # 4. initialize OnchainGroupManager let groupManager = OnchainGroupManager( - ethClientUrl: string(conf.ethClientAddress), + ethClientUrls: conf.ethClientUrls, chainId: conf.chainId, ethContractAddress: conf.ethContractAddress, rlnInstance: rlnInstance, diff --git a/waku/factory/conf_builder/rln_relay_conf_builder.nim b/waku/factory/conf_builder/rln_relay_conf_builder.nim index ff126d058..ea87eb278 100644 --- a/waku/factory/conf_builder/rln_relay_conf_builder.nim +++ b/waku/factory/conf_builder/rln_relay_conf_builder.nim @@ -9,9 +9,8 @@ logScope: ############################## type RlnRelayConfBuilder* = object enabled*: Option[bool] - chainId*: Option[uint] - ethClientAddress*: Option[string] + ethClientUrls*: Option[seq[string]] ethContractAddress*: Option[string] credIndex*: Option[uint] credPassword*: Option[string] @@ -42,8 +41,8 @@ proc withCredPath*(b: var RlnRelayConfBuilder, credPath: string) = proc withDynamic*(b: var RlnRelayConfBuilder, dynamic: bool) = b.dynamic = some(dynamic) -proc withEthClientAddress*(b: var RlnRelayConfBuilder, ethClientAddress: string) = - b.ethClientAddress = some(ethClientAddress) +proc withEthClientUrls*(b: var RlnRelayConfBuilder, ethClientUrls: seq[string]) = + b.ethClientUrls = some(ethClientUrls) proc withEthContractAddress*(b: var RlnRelayConfBuilder, ethContractAddress: string) = b.ethContractAddress = some(ethContractAddress) @@ -76,8 +75,8 @@ proc build*(b: RlnRelayConfBuilder): Result[Option[RlnRelayConf], string] = if b.dynamic.isNone(): return err("rlnRelay.dynamic is not specified") - if b.ethClientAddress.get("") == "": - return err("rlnRelay.ethClientAddress is not specified") + if b.ethClientUrls.get(newSeq[string](0)).len == 0: + return err("rlnRelay.ethClientUrls is not specified") if b.ethContractAddress.get("") == "": return err("rlnRelay.ethContractAddress is not specified") if b.epochSizeSec.isNone(): @@ -94,7 +93,7 @@ proc build*(b: RlnRelayConfBuilder): Result[Option[RlnRelayConf], string] = credIndex: b.credIndex, creds: creds, dynamic: b.dynamic.get(), - ethClientAddress: b.ethClientAddress.get(), + ethClientUrls: b.ethClientUrls.get(), ethContractAddress: b.ethContractAddress.get(), epochSizeSec: b.epochSizeSec.get(), userMessageLimit: b.userMessageLimit.get(), diff --git a/waku/factory/external_config.nim b/waku/factory/external_config.nim index 76b52b20b..ba0785f01 100644 --- a/waku/factory/external_config.nim +++ b/waku/factory/external_config.nim @@ -1,5 +1,5 @@ import - std/[strutils, strformat], + std/[strutils, strformat, sequtils], results, chronicles, chronos, @@ -75,11 +75,12 @@ type WakuNodeConf* = object name: "rln-relay-cred-path" .}: string - rlnRelayEthClientAddress* {. - desc: "HTTP address of an Ethereum testnet client e.g., http://localhost:8540/", - defaultValue: "http://localhost:8540/", + ethClientUrls* {. + desc: + "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" - .}: EthRpcUrl + .}: seq[EthRpcUrl] rlnRelayEthContractAddress* {. desc: "Address of membership contract on an Ethereum testnet.", @@ -867,7 +868,7 @@ proc toKeystoreGeneratorConf*(n: WakuNodeConf): RlnKeystoreGeneratorConf = RlnKeystoreGeneratorConf( execute: n.execute, chainId: n.rlnRelayChainId, - ethClientAddress: n.rlnRelayEthClientAddress.string, + ethClientUrls: n.ethClientUrls.mapIt(string(it)), ethContractAddress: n.rlnRelayEthContractAddress, userMessageLimit: n.rlnRelayUserMessageLimit, ethPrivateKey: n.rlnRelayEthPrivateKey, @@ -907,8 +908,8 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.rlnRelayConf.withCredPath(n.rlnRelayCredPath) if n.rlnRelayCredPassword != "": b.rlnRelayConf.withCredPassword(n.rlnRelayCredPassword) - if n.rlnRelayEthClientAddress.string != "": - b.rlnRelayConf.withEthClientAddress(n.rlnRelayEthClientAddress.string) + if n.ethClientUrls.len > 0: + b.rlnRelayConf.withEthClientUrls(n.ethClientUrls.mapIt(string(it))) if n.rlnRelayEthContractAddress != "": b.rlnRelayConf.withEthContractAddress(n.rlnRelayEthContractAddress) diff --git a/waku/factory/node_factory.nim b/waku/factory/node_factory.nim index 1b8f8e59b..7df5c2567 100644 --- a/waku/factory/node_factory.nim +++ b/waku/factory/node_factory.nim @@ -354,7 +354,7 @@ proc setupProtocols( credIndex: rlnRelayConf.credIndex, ethContractAddress: rlnRelayConf.ethContractAddress, chainId: rlnRelayConf.chainId, - ethClientAddress: rlnRelayConf.ethClientAddress, + ethClientUrls: rlnRelayConf.ethClientUrls, creds: rlnRelayConf.creds, treePath: rlnRelayConf.treePath, userMessageLimit: rlnRelayConf.userMessageLimit, diff --git a/waku/factory/waku_conf.nim b/waku/factory/waku_conf.nim index 766a17aa8..9f5160135 100644 --- a/waku/factory/waku_conf.nim +++ b/waku/factory/waku_conf.nim @@ -160,7 +160,7 @@ proc logConf*(conf: WakuConf) = maxMessageSize = conf.maxMessageSizeBytes, rlnEpochSizeSec = rlnRelayConf.epochSizeSec, rlnRelayUserMessageLimit = rlnRelayConf.userMessageLimit, - rlnRelayEthClientAddress = string(rlnRelayConf.ethClientAddress) + ethClientUrls = rlnRelayConf.ethClientUrls proc validateNodeKey(wakuConf: WakuConf): Result[void, string] = wakuConf.nodeKey.getPublicKey().isOkOr: @@ -224,8 +224,8 @@ proc validateNoEmptyStrings(wakuConf: WakuConf): Result[void, string] = if isEmptyOrWhiteSpace(rlnRelayConf.treePath): return err("rlnRelayConf.treepath is an empty string") - if isEmptyOrWhiteSpace(rlnRelayConf.ethClientAddress): - return err("rlnRelayConf.ethClientAddress is an empty string") + if rlnRelayConf.ethClientUrls.len == 0: + return err("rlnRelayConf.ethClientUrls is empty") if isEmptyOrWhiteSpace(rlnRelayConf.ethContractAddress): return err("rlnRelayConf.ethContractAddress is an empty string") diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index fe3db9102..61c9948ee 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -51,7 +51,7 @@ contract(WakuRlnContract): type WakuRlnContractWithSender = Sender[WakuRlnContract] OnchainGroupManager* = ref object of GroupManager - ethClientUrl*: string + ethClientUrls*: seq[string] ethPrivateKey*: Option[string] ethContractAddress*: string ethRpc*: Option[Web3] @@ -458,11 +458,35 @@ method onRegister*(g: OnchainGroupManager, cb: OnRegisterCallback) {.gcsafe.} = method onWithdraw*(g: OnchainGroupManager, cb: OnWithdrawCallback) {.gcsafe.} = 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.} = # check if the Ethereum client is reachable - var ethRpc: Web3 - g.retryWrapper(ethRpc, "Failed to connect to the Ethereum client"): - await newWeb3(g.ethClientUrl) + let ethRpc: Web3 = (await establishConnection(g)).valueOr: + return err("failed to connect to Ethereum clients: " & $error) var fetchedChainId: uint g.retryWrapper(fetchedChainId, "Failed to get the chain id"): @@ -544,9 +568,11 @@ method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.} proc onDisconnect() {.async.} = error "Ethereum client disconnected" - var newEthRpc: Web3 - g.retryWrapper(newEthRpc, "Failed to reconnect with the Ethereum client"): - await newWeb3(g.ethClientUrl) + + var newEthRpc: Web3 = (await g.establishConnection()).valueOr: + g.onFatalErrorAction("failed to connect to Ethereum clients onDisconnect") + return + newEthRpc.ondisconnect = ethRpc.ondisconnect g.ethRpc = some(newEthRpc) diff --git a/waku/waku_rln_relay/rln_relay.nim b/waku/waku_rln_relay/rln_relay.nim index a42b04f8b..20f5b7b24 100644 --- a/waku/waku_rln_relay/rln_relay.nim +++ b/waku/waku_rln_relay/rln_relay.nim @@ -43,7 +43,7 @@ type RlnRelayConf* = object of RootObj dynamic*: bool credIndex*: Option[uint] ethContractAddress*: string - ethClientAddress*: string + ethClientUrls*: seq[string] chainId*: uint creds*: Option[RlnRelayCreds] treePath*: string @@ -455,7 +455,7 @@ proc mount( (none(string), none(string)) groupManager = OnchainGroupManager( - ethClientUrl: string(conf.ethClientAddress), + ethClientUrls: conf.ethClientUrls, ethContractAddress: $conf.ethContractAddress, chainId: conf.chainId, rlnInstance: rlnInstance,