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) <noreply@anthropic.com>
This commit is contained in:
Prem Chaitanya Prathi 2026-04-09 23:09:26 +05:30
parent 7d54103053
commit 2033358df2
No known key found for this signature in database
19 changed files with 125 additions and 25 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"]

View File

@ -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"]

View File

@ -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"]

View File

@ -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"]

View File

@ -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

View File

@ -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)

@ -1 +1 @@
Subproject commit 037f8e100bfedffdbad1c4442e760d10a2437428
Subproject commit bb787a684b1dd335c82790fc505595e5a54cd6dc

2
vendor/nim-libp2p vendored

@ -1 +1 @@
Subproject commit ff8d51857b4b79a68468e7bcc27b2026cca02996
Subproject commit f7ebceb4ea3c9271a1e0eae404dbccd23dd51574

View File

@ -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

View File

@ -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))
)

View File

@ -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

View File

@ -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])]

View File

@ -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

View File

@ -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)

View File

@ -8,7 +8,7 @@ import
libp2p/protocols/rendezvous,
libp2p/crypto/curve25519,
libp2p/switch,
libp2p/utils/semaphore
chronos/asyncsync
import metrics except collect

View File

@ -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,