diff --git a/CHANGELOG.md b/CHANGELOG.md index cea6a11bb..b5842b63a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,35 @@ -## v0.29.0 (2024-06-11) +## v0.30.0 (2024-07-01) + +### Notes + +* Before upgrading to this version, if you are currently using RLN, make sure to remove your existing `keystore` folder and `rln_tree` +and start your installation from scratch, as +explained in [nwaku-compose](https://github.com/waku-org/nwaku-compose/blob/1b56575df9ddb904af0941a19ea1df3d36bfddfa/README.md). + +### Release highlights + +* RLN_v2 is used. The maximum rate can be set to +`N` messages per epoch, instead of just one message per epoch. See [this](https://github.com/waku-org/nwaku/issues/2345) for more details. + +### Changes + +- rln-relay: add chain-id flag to wakunode and restrict usage if mismatches rpc provider ([#2858](https://github.com/waku-org/nwaku/pull/2858)) +- rln: fix nullifierlog vulnerability ([#2855](https://github.com/waku-org/nwaku/pull/2855)) +- chore: add TWN parameters for RLNv2 ([#2843](https://github.com/waku-org/nwaku/pull/2843)) +- fix(rln-relay): clear nullifier log only if length is over max epoch gap ([#2836](https://github.com/waku-org/nwaku/pull/2836)) +- rlnv2: clean fork of rlnv2 ([#2828](https://github.com/waku-org/nwaku/issues/2828)) ([a02832fe](https://github.com/waku-org/nwaku/commit/a02832fe)) +- zerokit: bump submodule ([#2830](https://github.com/waku-org/nwaku/issues/2830)) ([bd064882](https://github.com/waku-org/nwaku/commit/bd064882)) + +This release supports the following [libp2p protocols](https://docs.libp2p.io/concepts/protocols/): +| Protocol | Spec status | Protocol id | +| ---: | :---: | :--- | +| [`11/WAKU2-RELAY`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/11/relay.md) | `stable` | `/vac/waku/relay/2.0.0` | +| [`12/WAKU2-FILTER`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/12/filter.md) | `draft` | `/vac/waku/filter/2.0.0-beta1`
`/vac/waku/filter-subscribe/2.0.0-beta1`
`/vac/waku/filter-push/2.0.0-beta1` | +| [`13/WAKU2-STORE`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/13/store.md) | `draft` | `/vac/waku/store/2.0.0-beta4` | +| [`19/WAKU2-LIGHTPUSH`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/19/lightpush.md) | `draft` | `/vac/waku/lightpush/2.0.0-beta1` | +| [`66/WAKU2-METADATA`](https://github.com/waku-org/specs/blob/master/standards/core/metadata.md) | `raw` | `/vac/waku/metadata/1.0.0` | + +## v0.29.0 (2024-06-19) ## What's Changed 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 c6e29e5bd..b839a2f7a 100644 --- a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim +++ b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim @@ -23,6 +23,8 @@ import ../testlib/common, ./utils +const CHAIN_ID = 1337 + proc generateCredentials(rlnInstance: ptr RLN): IdentityCredential = let credRes = membershipKeyGen(rlnInstance) return credRes.get() @@ -137,7 +139,7 @@ proc runAnvil(): Process = anvilPath, args = [ "--port", "8540", "--gas-limit", "300000000000000", "--balance", "1000000000", - "--chain-id", "1337", + "--chain-id", $CHAIN_ID, ], options = {poUsePath}, ) @@ -192,6 +194,7 @@ proc setup(): Future[OnchainGroupManager] {.async.} = let manager = OnchainGroupManager( ethClientUrl: EthClient, ethContractAddress: $contractAddress, + chainId: CHAIN_ID, ethPrivateKey: pk, rlnInstance: rlnInstance, ) @@ -215,6 +218,20 @@ suite "Onchain group manager": await manager.stop() + asyncTest "should error on initialization when chainId does not match": + let manager = await setup() + manager.chainId = CHAIN_ID + 1 + + (await manager.init()).isErrOr: + raiseAssert "Expected error when chainId does not match" + + asyncTest "should initialize when chainId is set to 0": + let manager = await setup() + manager.chainId = 0 + + (await manager.init()).isOkOr: + raiseAssert $error + asyncTest "should error on initialization when loaded metadata does not match": let manager = await setup() (await manager.init()).isOkOr: diff --git a/tests/waku_rln_relay/test_waku_rln_relay.nim b/tests/waku_rln_relay/test_waku_rln_relay.nim index c8f655ad7..454ec10a5 100644 --- a/tests/waku_rln_relay/test_waku_rln_relay.nim +++ b/tests/waku_rln_relay/test_waku_rln_relay.nim @@ -886,3 +886,40 @@ suite "Waku rln relay": check: buckets.len == 5 buckets == [2.0, 4.0, 6.0, 8.0, 10.0] + + asyncTest "nullifierLog clearing only after epoch has passed": + let index = MembershipIndex(0) + + proc runTestForEpochSizeSec(rlnEpochSizeSec: uint) {.async.} = + let wakuRlnConfig = WakuRlnConfig( + rlnRelayDynamic: false, + rlnRelayCredIndex: some(index), + rlnRelayUserMessageLimit: 1, + rlnEpochSizeSec: rlnEpochSizeSec, + rlnRelayTreePath: genTempPath("rln_tree", "waku_rln_relay_4"), + ) + + let wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr: + raiseAssert $error + + let rlnMaxEpochGap = wakuRlnRelay.rlnMaxEpochGap + let testProofMetadata = default(ProofMetadata) + let testProofMetadataTable = {testProofMetadata.nullifier: testProofMetadata}.toTable() + + for i in 0..rlnMaxEpochGap: + # we add epochs to the nullifierLog + let testEpoch = wakuRlnRelay.calcEpoch(epochTime() + float(rlnEpochSizeSec * i)) + wakuRlnRelay.nullifierLog[testEpoch] = testProofMetadataTable + check: wakuRlnRelay.nullifierLog.len().uint == i + 1 + + check: wakuRlnRelay.nullifierLog.len().uint == rlnMaxEpochGap + 1 + + # clearing it now will remove 1 epoch + wakuRlnRelay.clearNullifierLog() + + check: wakuRlnRelay.nullifierLog.len().uint == rlnMaxEpochGap + + var testEpochSizes: seq[uint] = @[1,5,10,30,60,600] + for i in testEpochSizes: + await runTestForEpochSizeSec(i) + diff --git a/tools/rln_keystore_generator/rln_keystore_generator.nim b/tools/rln_keystore_generator/rln_keystore_generator.nim index 6c64ed6d2..2157b1c1b 100644 --- a/tools/rln_keystore_generator/rln_keystore_generator.nim +++ b/tools/rln_keystore_generator/rln_keystore_generator.nim @@ -76,7 +76,7 @@ proc doRlnKeystoreGenerator*(conf: WakuNodeConf) = debug "Transaction hash", txHash = groupManager.registrationTxHash.get() info "Your membership has been registered on-chain.", - chainId = $groupManager.chainId.get(), + chainId = $groupManager.chainId, contractAddress = conf.rlnRelayEthContractAddress, membershipIndex = groupManager.membershipIndex.get() info "Your user message limit is", userMessageLimit = conf.rlnRelayUserMessageLimit @@ -84,7 +84,7 @@ proc doRlnKeystoreGenerator*(conf: WakuNodeConf) = # 6. write to keystore let keystoreCred = KeystoreMembership( membershipContract: MembershipContract( - chainId: $groupManager.chainId.get(), address: conf.rlnRelayEthContractAddress + chainId: $groupManager.chainId, address: conf.rlnRelayEthContractAddress ), treeIndex: groupManager.membershipIndex.get(), identityCredential: credential, diff --git a/waku/factory/external_config.nim b/waku/factory/external_config.nim index bc3c08bb1..a4d487e6a 100644 --- a/waku/factory/external_config.nim +++ b/waku/factory/external_config.nim @@ -76,6 +76,12 @@ type WakuNodeConf* = object name: "rln-relay-eth-contract-address" .}: string + rlnRelayChainId* {. + desc: "Chain ID of the provided contract (optional, will fetch from RPC provider if not used)", + defaultValue: 0, + name: "rln-relay-chain-id" + .}: uint + rlnRelayCredPassword* {. desc: "Password for encrypting RLN credentials", defaultValue: "", diff --git a/waku/factory/networks_config.nim b/waku/factory/networks_config.nim index b31aec3c4..49c6dfd9a 100644 --- a/waku/factory/networks_config.nim +++ b/waku/factory/networks_config.nim @@ -5,6 +5,7 @@ type ClusterConf* = object clusterId*: uint16 rlnRelay*: bool rlnRelayEthContractAddress*: string + rlnRelayChainId*: uint rlnRelayDynamic*: bool rlnRelayBandwidthThreshold*: int rlnEpochSizeSec*: uint64 @@ -32,12 +33,12 @@ proc TheWakuNetworkConf*(T: type ClusterConf): ClusterConf = maxMessageSize: "150KiB", clusterId: 1, rlnRelay: true, - rlnRelayEthContractAddress: "0xF471d71E9b1455bBF4b85d475afb9BB0954A29c4", + rlnRelayEthContractAddress: "0x4976Df0f61135EF3E5720D92eadE2e5F47A68Ef9", rlnRelayDynamic: true, + rlnRelayChainId: 2442, # https://chainlist.org/chain/2442 rlnRelayBandwidthThreshold: 0, - rlnEpochSizeSec: 1, - # parameter to be defined with rln_v2 - rlnRelayUserMessageLimit: 1, + rlnEpochSizeSec: 600, + rlnRelayUserMessageLimit: 20, pubsubTopics: @[ "/waku/2/rs/1/0", "/waku/2/rs/1/1", "/waku/2/rs/1/2", "/waku/2/rs/1/3", diff --git a/waku/factory/node_factory.nim b/waku/factory/node_factory.nim index e2202261c..ad9719ec9 100644 --- a/waku/factory/node_factory.nim +++ b/waku/factory/node_factory.nim @@ -203,6 +203,7 @@ proc setupProtocols( rlnRelayDynamic: conf.rlnRelayDynamic, rlnRelayCredIndex: conf.rlnRelayCredIndex, rlnRelayEthContractAddress: conf.rlnRelayEthContractAddress, + rlnRelayChainId: conf.rlnRelayChainId, rlnRelayEthClientAddress: string(conf.rlnRelayethClientAddress), rlnRelayCredPath: conf.rlnRelayCredPath, rlnRelayCredPassword: conf.rlnRelayCredPassword, diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index 3c8862093..709be62c2 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -113,6 +113,7 @@ proc init*(T: type Waku, conf: WakuNodeConf): Result[Waku, string] = confCopy.clusterId = twnClusterConf.clusterId confCopy.rlnRelay = twnClusterConf.rlnRelay confCopy.rlnRelayEthContractAddress = twnClusterConf.rlnRelayEthContractAddress + confCopy.rlnRelayChainId = twnClusterConf.rlnRelayChainId confCopy.rlnRelayDynamic = twnClusterConf.rlnRelayDynamic confCopy.rlnRelayBandwidthThreshold = twnClusterConf.rlnRelayBandwidthThreshold confCopy.discv5Discovery = twnClusterConf.discv5Discovery 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 1d9935c54..5cc89a6f7 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 @@ -58,7 +58,7 @@ type wakuRlnContract*: Option[WakuRlnContractWithSender] latestProcessedBlock*: BlockNumber registrationTxHash*: Option[TxHash] - chainId*: Option[Quantity] + chainId*: uint keystorePath*: Option[string] keystorePassword*: Option[string] registrationHandler*: Option[RegistrationHandler] @@ -103,7 +103,7 @@ proc setMetadata*( let metadataSetRes = g.rlnInstance.setMetadata( RlnMetadata( lastProcessedBlock: normalizedBlock, - chainId: uint64(g.chainId.get()), + chainId: g.chainId, contractAddress: g.ethContractAddress, validRoots: g.validRoots.toSeq(), ) @@ -537,11 +537,18 @@ method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.} g.retryWrapper(ethRpc, "Failed to connect to the Ethereum client"): await newWeb3(g.ethClientUrl) + var fetchedChainId: uint + g.retryWrapper(fetchedChainId, "Failed to get the chain id"): + uint(await ethRpc.provider.eth_chainId()) + # Set the chain id - var chainId: Quantity - g.retryWrapper(chainId, "Failed to get the chain id"): - await ethRpc.provider.eth_chainId() - g.chainId = some(chainId) + if g.chainId == 0: + warn "Chain ID not set in config, using RPC Provider's Chain ID", providerChainId = fetchedChainId + + if g.chainId != 0 and g.chainId != fetchedChainId: + return err("The RPC Provided a Chain ID which is different than the provided Chain ID: provided = " & $g.chainId & ", actual = " & $fetchedChainId) + + g.chainId = fetchedChainId if g.ethPrivateKey.isSome(): let pk = g.ethPrivateKey.get() @@ -564,7 +571,7 @@ method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.} var keystoreQuery = KeystoreMembership( membershipContract: - MembershipContract(chainId: $g.chainId.get(), address: g.ethContractAddress) + MembershipContract(chainId: $g.chainId, address: g.ethContractAddress) ) if g.membershipIndex.isSome(): keystoreQuery.treeIndex = MembershipIndex(g.membershipIndex.get()) @@ -596,7 +603,7 @@ method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.} warn "could not initialize with persisted rln metadata" elif metadataGetOptRes.get().isSome(): let metadata = metadataGetOptRes.get().get() - if metadata.chainId != uint64(g.chainId.get()): + if metadata.chainId != uint(g.chainId): return err("persisted data: chain id mismatch") if metadata.contractAddress != g.ethContractAddress.toLower(): diff --git a/waku/waku_rln_relay/rln_relay.nim b/waku/waku_rln_relay/rln_relay.nim index e4aeb9552..2cc0cc8c9 100644 --- a/waku/waku_rln_relay/rln_relay.nim +++ b/waku/waku_rln_relay/rln_relay.nim @@ -1,3 +1,4 @@ + {.push raises: [].} import @@ -37,6 +38,7 @@ type WakuRlnConfig* = object rlnRelayCredIndex*: Option[uint] rlnRelayEthContractAddress*: string rlnRelayEthClientAddress*: string + rlnRelayChainId*: uint rlnRelayCredPath*: string rlnRelayCredPassword*: string rlnRelayTreePath*: string @@ -141,7 +143,7 @@ proc updateLog*( try: # check if an identical record exists if rlnPeer.nullifierLog[epoch].hasKeyOrPut(proofMetadata.nullifier, proofMetadata): - # the above condition could be `discarded` but it is kept for clarity, that slashing will + # the above condition could be `discarded` but it is kept for clarity, that slashing will # be implemented here # TODO: slashing logic return ok() @@ -276,8 +278,10 @@ proc validateMessageAndUpdateLog*( if proofMetadataRes.isErr(): return MessageValidationResult.Invalid - # insert the message to the log (never errors) - discard rlnPeer.updateLog(msgProof.epoch, proofMetadataRes.get()) + # insert the message to the log (never errors) only if the + # message is valid. + if isValidMessage == MessageValidationResult.Valid: + discard rlnPeer.updateLog(msgProof.epoch, proofMetadataRes.get()) return isValidMessage @@ -305,21 +309,25 @@ proc appendRLNProof*( let proof = rlnPeer.groupManager.generateProof(input, epoch, nonce).valueOr: return err("could not generate rln-v2 proof: " & $error) - msg.proof = proof.encode().buffer return ok() -proc clearNullifierLog(rlnPeer: WakuRlnRelay) = +proc clearNullifierLog*(rlnPeer: WakuRlnRelay) = # clear the first MaxEpochGap epochs of the nullifer log # if more than MaxEpochGap epochs are in the log - # note: the epochs are ordered ascendingly - if rlnPeer.nullifierLog.len().uint < rlnPeer.rlnMaxEpochGap: - return + let currentEpoch = fromEpoch(rlnPeer.getCurrentEpoch()) - trace "clearing epochs from the nullifier log", count = rlnPeer.rlnMaxEpochGap - let epochsToClear = rlnPeer.nullifierLog.keys().toSeq()[0 ..< rlnPeer.rlnMaxEpochGap] - for epoch in epochsToClear: - rlnPeer.nullifierLog.del(epoch) + var epochsToRemove: seq[Epoch] = @[] + for epoch in rlnPeer.nullifierLog.keys(): + let epochInt = fromEpoch(epoch) + + # clean all epochs that are +- rlnMaxEpochGap from the current epoch + if (currentEpoch+rlnPeer.rlnMaxEpochGap) <= epochInt or epochInt <= (currentEpoch-rlnPeer.rlnMaxEpochGap): + epochsToRemove.add(epoch) + + for epochRemove in epochsToRemove: + trace "clearing epochs from the nullifier log", currentEpoch = currentEpoch, cleanedEpoch = fromEpoch(epochRemove) + rlnPeer.nullifierLog.del(epochRemove) proc generateRlnValidator*( wakuRlnRelay: WakuRLNRelay, spamHandler = none(SpamHandler) @@ -420,6 +428,7 @@ proc mount( groupManager = OnchainGroupManager( ethClientUrl: string(conf.rlnRelayethClientAddress), ethContractAddress: $conf.rlnRelayEthContractAddress, + chainId: conf.rlnRelayChainId, rlnInstance: rlnInstance, registrationHandler: registrationHandler, keystorePath: rlnRelayCredPath,