From 2033358df25b690855d236da0593fbce2b3f15ef Mon Sep 17 00:00:00 2001 From: Prem Chaitanya Prathi Date: Thu, 9 Apr 2026 23:09:26 +0530 Subject: [PATCH] feat(mix): add cover traffic, disable-spam-protection flag, and fix option_shims crash - Add cover traffic support with constant rate as per spec - Add mix-user-message-limit and mix-disable-spam-protection CLI flags - Fix option_shims.nim double-evaluation bug causing UnpackDefect crash in ping (template expanded await expression twice, racing two calls) - Reduce default rate limit to 2 msgs/epoch for simulation testing - Add check_cover_traffic.sh metrics monitoring script Co-Authored-By: Claude Opus 4.6 (1M context) --- config.nims | 3 ++ simulations/mixnet/check_cover_traffic.sh | 18 ++++++++ simulations/mixnet/config.toml | 3 ++ simulations/mixnet/config1.toml | 3 ++ simulations/mixnet/config2.toml | 3 ++ simulations/mixnet/config3.toml | 3 ++ simulations/mixnet/config4.toml | 3 ++ simulations/mixnet/setup_credentials.nim | 2 +- tools/confutils/cli_args.nim | 14 ++++++ vendor/mix-rln-spam-protection-plugin | 2 +- vendor/nim-libp2p | 2 +- waku/common/option_shims.nim | 28 ++++++++++++ .../factory/conf_builder/mix_conf_builder.nim | 12 +++++- waku/factory/node_factory.nim | 2 +- waku/factory/waku_conf.nim | 2 + waku/node/waku_node.nim | 3 +- waku/waku_mix/protocol.nim | 43 ++++++++++++------- waku/waku_rendezvous/client.nim | 2 +- waku/waku_rendezvous/protocol.nim | 2 +- 19 files changed, 125 insertions(+), 25 deletions(-) create mode 100755 simulations/mixnet/check_cover_traffic.sh create mode 100644 waku/common/option_shims.nim diff --git a/config.nims b/config.nims index f74fe183f..08b015088 100644 --- a/config.nims +++ b/config.nims @@ -99,6 +99,9 @@ if not defined(macosx) and not defined(android): nimStackTraceOverride switch("import", "libbacktrace") +# Compatibility shims for std/options after results library update +switch("import", "waku/common/option_shims") + --define: nimOldCaseObjects # https://github.com/status-im/nim-confutils/issues/9 diff --git a/simulations/mixnet/check_cover_traffic.sh b/simulations/mixnet/check_cover_traffic.sh new file mode 100755 index 000000000..f79ab5893 --- /dev/null +++ b/simulations/mixnet/check_cover_traffic.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Check cover traffic metrics from all mix nodes. +# Ports: 8008 + ports-shift (1-5) = 8009-8013 + +echo "=== Cover Traffic Metrics ===" +echo "" + +for i in 1 2 3 4 5; do + port=$((8008 + i)) + echo "--- Node $i (port $port) ---" + metrics=$(curl -s "http://127.0.0.1:$port/metrics" 2>/dev/null) + if [ -z "$metrics" ]; then + echo " (unreachable)" + else + echo "$metrics" | grep -E "mix_cover_|mix_slots_" | grep -v "^#" || echo " (no cover metrics yet)" + fi + echo "" +done diff --git a/simulations/mixnet/config.toml b/simulations/mixnet/config.toml index 5cd1aa936..44ad6addf 100644 --- a/simulations/mixnet/config.toml +++ b/simulations/mixnet/config.toml @@ -7,6 +7,7 @@ lightpush = true max-connections = 150 peer-exchange = false metrics-logging = false +metrics-server = true cluster-id = 2 discv5-discovery = false discv5-udp-port = 9000 @@ -26,3 +27,5 @@ nat = "extip:127.0.0.1" ext-multiaddr = ["/ip4/127.0.0.1/tcp/60001"] ext-multiaddr-only = true ip-colocation-limit=0 +mix-user-message-limit=2 +mix-disable-spam-protection=true diff --git a/simulations/mixnet/config1.toml b/simulations/mixnet/config1.toml index 73cccb8c6..ad5fccea7 100644 --- a/simulations/mixnet/config1.toml +++ b/simulations/mixnet/config1.toml @@ -7,6 +7,7 @@ lightpush = true max-connections = 150 peer-exchange = false metrics-logging = false +metrics-server = true cluster-id = 2 discv5-discovery = false discv5-udp-port = 9001 @@ -27,4 +28,6 @@ nat = "extip:127.0.0.1" ext-multiaddr = ["/ip4/127.0.0.1/tcp/60002"] ext-multiaddr-only = true ip-colocation-limit=0 +mix-user-message-limit=2 +mix-disable-spam-protection=true #staticnode = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o", "/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA","/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f","/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu"] diff --git a/simulations/mixnet/config2.toml b/simulations/mixnet/config2.toml index 3acd2bf8a..dc7ea12c6 100644 --- a/simulations/mixnet/config2.toml +++ b/simulations/mixnet/config2.toml @@ -7,6 +7,7 @@ lightpush = true max-connections = 150 peer-exchange = false metrics-logging = false +metrics-server = true cluster-id = 2 discv5-discovery = false discv5-udp-port = 9002 @@ -27,4 +28,6 @@ nat = "extip:127.0.0.1" ext-multiaddr = ["/ip4/127.0.0.1/tcp/60003"] ext-multiaddr-only = true ip-colocation-limit=0 +mix-user-message-limit=2 +mix-disable-spam-protection=true #staticnode = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o", "/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF","/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f","/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu"] diff --git a/simulations/mixnet/config3.toml b/simulations/mixnet/config3.toml index bd8e7c4e9..bdf2478e1 100644 --- a/simulations/mixnet/config3.toml +++ b/simulations/mixnet/config3.toml @@ -7,6 +7,7 @@ lightpush = true max-connections = 150 peer-exchange = false metrics-logging = false +metrics-server = true cluster-id = 2 discv5-discovery = false discv5-udp-port = 9003 @@ -27,4 +28,6 @@ nat = "extip:127.0.0.1" ext-multiaddr = ["/ip4/127.0.0.1/tcp/60004"] ext-multiaddr-only = true ip-colocation-limit=0 +mix-user-message-limit=2 +mix-disable-spam-protection=true #staticnode = ["/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF", "/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA","/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o","/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu"] diff --git a/simulations/mixnet/config4.toml b/simulations/mixnet/config4.toml index f174250d5..d667cd963 100644 --- a/simulations/mixnet/config4.toml +++ b/simulations/mixnet/config4.toml @@ -7,6 +7,7 @@ lightpush = true max-connections = 150 peer-exchange = false metrics-logging = false +metrics-server = true cluster-id = 2 discv5-discovery = false discv5-udp-port = 9004 @@ -27,4 +28,6 @@ nat = "extip:127.0.0.1" ext-multiaddr = ["/ip4/127.0.0.1/tcp/60005"] ext-multiaddr-only = true ip-colocation-limit=0 +mix-user-message-limit=2 +mix-disable-spam-protection=true #staticnode = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o", "/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA","/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f","/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF"] diff --git a/simulations/mixnet/setup_credentials.nim b/simulations/mixnet/setup_credentials.nim index 77c796354..be86df7c3 100644 --- a/simulations/mixnet/setup_credentials.nim +++ b/simulations/mixnet/setup_credentials.nim @@ -20,7 +20,7 @@ import const KeystorePassword = "mix-rln-password" # Must match protocol.nim - DefaultUserMessageLimit = 100'u64 # Network-wide default rate limit + DefaultUserMessageLimit = 2'u64 # ~12 msgs/min with 10s epochs SpammerUserMessageLimit = 3'u64 # Lower limit for spammer testing # Peer IDs derived from nodekeys in config files diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index 112e3911a..6c99dc500 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -643,6 +643,17 @@ with the drawback of consuming some more bandwidth.""", name: "mixnode" .}: seq[MixNodePubInfo] + mixUserMessageLimit* {. + desc: "Maximum messages per RLN epoch for mix cover traffic. If not set, uses plugin default.", + name: "mix-user-message-limit" + .}: Option[int] + + mixDisableSpamProtection* {. + desc: "Disable RLN spam protection for mix protocol (for testing only).", + defaultValue: false, + name: "mix-disable-spam-protection" + .}: bool + # Kademlia Discovery config enableKadDiscovery* {. desc: @@ -1073,6 +1084,9 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.withMix(n.mix) if n.mixkey.isSome(): b.mixConf.withMixKey(n.mixkey.get()) + if n.mixUserMessageLimit.isSome(): + b.mixConf.withUserMessageLimit(n.mixUserMessageLimit.get()) + b.mixConf.withDisableSpamProtection(n.mixDisableSpamProtection) b.filterServiceConf.withEnabled(n.filter) b.filterServiceConf.withSubscriptionTimeout(n.filterSubscriptionTimeout) diff --git a/vendor/mix-rln-spam-protection-plugin b/vendor/mix-rln-spam-protection-plugin index 037f8e100..bb787a684 160000 --- a/vendor/mix-rln-spam-protection-plugin +++ b/vendor/mix-rln-spam-protection-plugin @@ -1 +1 @@ -Subproject commit 037f8e100bfedffdbad1c4442e760d10a2437428 +Subproject commit bb787a684b1dd335c82790fc505595e5a54cd6dc diff --git a/vendor/nim-libp2p b/vendor/nim-libp2p index ff8d51857..f7ebceb4e 160000 --- a/vendor/nim-libp2p +++ b/vendor/nim-libp2p @@ -1 +1 @@ -Subproject commit ff8d51857b4b79a68468e7bcc27b2026cca02996 +Subproject commit f7ebceb4ea3c9271a1e0eae404dbccd23dd51574 diff --git a/waku/common/option_shims.nim b/waku/common/option_shims.nim new file mode 100644 index 000000000..6ad67635f --- /dev/null +++ b/waku/common/option_shims.nim @@ -0,0 +1,28 @@ +# Compatibility shims for std/options +# The results library removed valueOr/withValue support for Option[T]. +# These templates restore that functionality. + +{.push raises: [].} + +import std/options + +template valueOr*[T](self: Option[T], def: untyped): T = + let tmp = self + if tmp.isSome(): + tmp.get() + else: + def + +template withValue*[T](self: Option[T], value, body: untyped): untyped = + let tmp = self + if tmp.isSome(): + let value {.inject.} = tmp.get() + body + +template withValue*[T](self: Option[T], value, body, elseBody: untyped): untyped = + let tmp = self + if tmp.isSome(): + let value {.inject.} = tmp.get() + body + else: + elseBody diff --git a/waku/factory/conf_builder/mix_conf_builder.nim b/waku/factory/conf_builder/mix_conf_builder.nim index 145ccb76e..7af7a4ad1 100644 --- a/waku/factory/conf_builder/mix_conf_builder.nim +++ b/waku/factory/conf_builder/mix_conf_builder.nim @@ -12,6 +12,8 @@ type MixConfBuilder* = object enabled: Option[bool] mixKey: Option[string] mixNodes: seq[MixNodePubInfo] + userMessageLimit: Option[int] + disableSpamProtection: bool proc init*(T: type MixConfBuilder): MixConfBuilder = MixConfBuilder() @@ -25,6 +27,12 @@ proc withMixKey*(b: var MixConfBuilder, mixKey: string) = proc withMixNodes*(b: var MixConfBuilder, mixNodes: seq[MixNodePubInfo]) = b.mixNodes = mixNodes +proc withUserMessageLimit*(b: var MixConfBuilder, limit: int) = + b.userMessageLimit = some(limit) + +proc withDisableSpamProtection*(b: var MixConfBuilder, disable: bool) = + b.disableSpamProtection = disable + proc build*(b: MixConfBuilder): Result[Option[MixConf], string] = if not b.enabled.get(false): return ok(none[MixConf]()) @@ -33,11 +41,11 @@ proc build*(b: MixConfBuilder): Result[Option[MixConf], string] = let mixPrivKey = intoCurve25519Key(ncrutils.fromHex(b.mixKey.get())) let mixPubKey = public(mixPrivKey) return ok( - some(MixConf(mixKey: mixPrivKey, mixPubKey: mixPubKey, mixNodes: b.mixNodes)) + some(MixConf(mixKey: mixPrivKey, mixPubKey: mixPubKey, mixNodes: b.mixNodes, userMessageLimit: b.userMessageLimit, disableSpamProtection: b.disableSpamProtection)) ) else: let (mixPrivKey, mixPubKey) = generateKeyPair().valueOr: return err("Generate key pair error: " & $error) return ok( - some(MixConf(mixKey: mixPrivKey, mixPubKey: mixPubKey, mixNodes: b.mixNodes)) + some(MixConf(mixKey: mixPrivKey, mixPubKey: mixPubKey, mixNodes: b.mixNodes, userMessageLimit: b.userMessageLimit, disableSpamProtection: b.disableSpamProtection)) ) diff --git a/waku/factory/node_factory.nim b/waku/factory/node_factory.nim index ebf91f415..38b96ab0b 100644 --- a/waku/factory/node_factory.nim +++ b/waku/factory/node_factory.nim @@ -167,7 +167,7 @@ proc setupProtocols( #mount mix if conf.mixConf.isSome(): let mixConf = conf.mixConf.get() - (await node.mountMix(conf.clusterId, mixConf.mixKey, mixConf.mixnodes)).isOkOr: + (await node.mountMix(conf.clusterId, mixConf.mixKey, mixConf.mixnodes, mixConf.userMessageLimit, mixConf.disableSpamProtection)).isOkOr: return err("failed to mount waku mix protocol: " & $error) # Setup extended kademlia discovery diff --git a/waku/factory/waku_conf.nim b/waku/factory/waku_conf.nim index 6ed34e131..0ff16cc85 100644 --- a/waku/factory/waku_conf.nim +++ b/waku/factory/waku_conf.nim @@ -51,6 +51,8 @@ type MixConf* = ref object mixKey*: Curve25519Key mixPubKey*: Curve25519Key mixnodes*: seq[MixNodePubInfo] + userMessageLimit*: Option[int] + disableSpamProtection*: bool type KademliaDiscoveryConf* = object bootstrapNodes*: seq[(PeerId, seq[MultiAddress])] diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index 0dc927f3a..c7d3a93e3 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -329,6 +329,7 @@ proc mountMix*( mixPrivKey: Curve25519Key, mixnodes: seq[MixNodePubInfo], userMessageLimit: Option[int] = none(int), + disableSpamProtection: bool = false, ): Future[Result[void, string]] {.async.} = info "mounting mix protocol", nodeId = node.info #TODO log the config used @@ -361,7 +362,7 @@ proc mountMix*( node.wakuMix = WakuMix.new( localaddrStr, node.peerManager, clusterId, mixPrivKey, mixnodes, publishMessage, - userMessageLimit, + userMessageLimit, disableSpamProtection, ).valueOr: error "Waku Mix protocol initialization failed", err = error return diff --git a/waku/waku_mix/protocol.nim b/waku/waku_mix/protocol.nim index 7400d7011..e3c19ac41 100644 --- a/waku/waku_mix/protocol.nim +++ b/waku/waku_mix/protocol.nim @@ -11,6 +11,7 @@ import libp2p/protocols/mix/mix_metrics, libp2p/protocols/mix/delay_strategy, libp2p/protocols/mix/spam_protection, + libp2p/protocols/mix/cover_traffic, libp2p/[multiaddress, multicodec, peerid, peerinfo], eth/common/keys @@ -87,6 +88,7 @@ proc new*( bootnodes: seq[MixNodePubInfo], publishMessage: PublishMessage, userMessageLimit: Option[int] = none(int), + disableSpamProtection: bool = false, ): WakuMixResult[T] = let mixPubKey = public(mixPrivKey) trace "mixPubKey", mixPubKey = mixPubKey @@ -97,33 +99,42 @@ proc new*( peermgr.switch.peerInfo.publicKey.skkey, peermgr.switch.peerInfo.privateKey.skkey, ) - # Initialize spam protection with persistent credentials - # Use peerID in keystore path so multiple peers can run from same directory - # Tree path is shared across all nodes to maintain the full membership set - let peerId = peermgr.switch.peerInfo.peerId - var spamProtectionConfig = defaultConfig() - spamProtectionConfig.keystorePath = "rln_keystore_" & $peerId & ".json" - spamProtectionConfig.keystorePassword = "mix-rln-password" - if userMessageLimit.isSome(): - spamProtectionConfig.userMessageLimit = userMessageLimit.get() - # rlnResourcesPath left empty to use bundled resources (via "tree_height_/" placeholder) + let totalSlots = userMessageLimit.get(2) + let ct = ConstantRateCoverTraffic.new( + totalSlots = totalSlots, + epochDurationSec = 10.0, + useInternalEpochTimer = disableSpamProtection, + ) - let spamProtection = newMixRlnSpamProtection(spamProtectionConfig).valueOr: - return err("failed to create spam protection: " & error) + var spamProtectionOpt = default(Opt[SpamProtection]) + if not disableSpamProtection: + # Initialize spam protection with persistent credentials + let peerId = peermgr.switch.peerInfo.peerId + var spamProtectionConfig = defaultConfig() + spamProtectionConfig.keystorePath = "rln_keystore_" & $peerId & ".json" + spamProtectionConfig.keystorePassword = "mix-rln-password" + if userMessageLimit.isSome(): + spamProtectionConfig.userMessageLimit = userMessageLimit.get() + + let spamProtection = newMixRlnSpamProtection(spamProtectionConfig).valueOr: + return err("failed to create spam protection: " & error) + spamProtectionOpt = Opt.some(SpamProtection(spamProtection)) + else: + info "mix spam protection disabled" var m = WakuMix( peerManager: peermgr, clusterId: clusterId, pubKey: mixPubKey, - mixRlnSpamProtection: spamProtection, publishMessage: publishMessage, ) procCall MixProtocol(m).init( localMixNodeInfo, peermgr.switch, - spamProtection = Opt.some(SpamProtection(spamProtection)), - delayStrategy = - ExponentialDelayStrategy.new(meanDelayMs = 100, rng = crypto.newRng()), + spamProtection = spamProtectionOpt, + delayStrategy = Opt.some(DelayStrategy( + ExponentialDelayStrategy.new(meanDelay = 100, rng = crypto.newRng()))), + coverTraffic = Opt.some(CoverTraffic(ct)), ) processBootNodes(bootnodes, peermgr, m) diff --git a/waku/waku_rendezvous/client.nim b/waku/waku_rendezvous/client.nim index 09e789774..b548d1298 100644 --- a/waku/waku_rendezvous/client.nim +++ b/waku/waku_rendezvous/client.nim @@ -8,7 +8,7 @@ import libp2p/protocols/rendezvous, libp2p/crypto/curve25519, libp2p/switch, - libp2p/utils/semaphore + chronos/asyncsync import metrics except collect diff --git a/waku/waku_rendezvous/protocol.nim b/waku/waku_rendezvous/protocol.nim index 00b5f1a5c..789496b99 100644 --- a/waku/waku_rendezvous/protocol.nim +++ b/waku/waku_rendezvous/protocol.nim @@ -8,7 +8,7 @@ import stew/byteutils, libp2p/protocols/rendezvous, libp2p/protocols/rendezvous/protobuf, - libp2p/utils/semaphore, + chronos/asyncsync, libp2p/utils/offsettedseq, libp2p/crypto/curve25519, libp2p/switch,