Skip to content

Commit 30b1a16

Browse files
committed
integrate RLN spam protection into mix protocol with OffChainGroupManager and logos-messaging for broadcasting membership changes and general coordination  with API updates and fixesCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5c814aa commit 30b1a16

19 files changed

Lines changed: 771 additions & 61 deletions

.gitmodules

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,8 @@
195195
url = https://github.com/logos-messaging/nim-ffi/
196196
ignore = untracked
197197
branch = master
198+
[submodule "vendor/mix-rln-spam-protection-plugin"]
199+
path = vendor/mix-rln-spam-protection-plugin
200+
url = https://github.com/logos-co/mix-rln-spam-protection-plugin.git
201+
ignore = untracked
202+
branch = master

apps/chat2mix/chat2mix.nim

Lines changed: 122 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import
3636
import
3737
waku/[
3838
waku_core,
39+
waku_core/topics/sharding,
3940
waku_lightpush/common,
4041
waku_lightpush/rpc,
4142
waku_enr,
@@ -48,6 +49,8 @@ import
4849
common/utils/nat,
4950
waku_store/common,
5051
waku_filter_v2/client,
52+
waku_filter_v2/common as filter_common,
53+
waku_mix/protocol,
5154
common/logging,
5255
],
5356
./config_chat2mix
@@ -58,6 +61,105 @@ import ../../waku/waku_rln_relay
5861
logScope:
5962
topics = "chat2 mix"
6063

64+
#########################
65+
## Mix Spam Protection ##
66+
#########################
67+
68+
# Forward declaration
69+
proc maintainSpamProtectionSubscription(
70+
node: WakuNode, contentTopics: seq[ContentTopic]
71+
) {.async.}
72+
73+
proc setupMixSpamProtectionViaFilter(node: WakuNode) {.async.} =
74+
## Setup filter-based spam protection coordination for mix protocol.
75+
## Since chat2mix doesn't use relay, we subscribe via filter to receive
76+
## spam protection coordination messages.
77+
78+
# Register message handler for spam protection coordination
79+
let spamTopics = node.wakuMix.getSpamProtectionContentTopics()
80+
81+
proc handleSpamMessage(
82+
pubsubTopic: PubsubTopic, message: WakuMessage
83+
): Future[void] {.async, gcsafe.} =
84+
await node.wakuMix.handleMessage(pubsubTopic, message)
85+
86+
node.wakuFilterClient.registerPushHandler(handleSpamMessage)
87+
88+
# Wait for filter peer and maintain subscription
89+
asyncSpawn maintainSpamProtectionSubscription(node, spamTopics)
90+
91+
proc maintainSpamProtectionSubscription(
92+
node: WakuNode, contentTopics: seq[ContentTopic]
93+
) {.async.} =
94+
## Maintain filter subscription for spam protection topics.
95+
## Monitors subscription health with periodic pings and re-subscribes on failure.
96+
const RetryInterval = chronos.seconds(5)
97+
const SubscriptionMaintenance = chronos.seconds(30)
98+
const MaxFailedSubscribes = 3
99+
var currentFilterPeer: Option[RemotePeerInfo] = none(RemotePeerInfo)
100+
var noFailedSubscribes = 0
101+
102+
while true:
103+
# Select or reuse filter peer
104+
if currentFilterPeer.isNone():
105+
let filterPeerOpt = node.peerManager.selectPeer(WakuFilterSubscribeCodec)
106+
if filterPeerOpt.isNone():
107+
debug "No filter peer available yet for spam protection, retrying..."
108+
await sleepAsync(RetryInterval)
109+
continue
110+
currentFilterPeer = some(filterPeerOpt.get())
111+
info "Selected filter peer for spam protection",
112+
peer = currentFilterPeer.get().peerId
113+
114+
# Check if subscription is still alive with ping
115+
let pingErr = (await node.wakuFilterClient.ping(currentFilterPeer.get())).errorOr:
116+
# Subscription is alive, wait before next check
117+
await sleepAsync(SubscriptionMaintenance)
118+
if noFailedSubscribes > 0:
119+
noFailedSubscribes = 0
120+
continue
121+
122+
# Subscription lost, need to re-subscribe
123+
warn "Spam protection filter subscription ping failed, re-subscribing",
124+
error = pingErr, peer = currentFilterPeer.get().peerId
125+
126+
# Determine pubsub topic from content topics (using auto-sharding)
127+
if node.wakuAutoSharding.isNone():
128+
error "Auto-sharding not configured, cannot determine pubsub topic for spam protection"
129+
await sleepAsync(RetryInterval)
130+
continue
131+
132+
let shardRes = node.wakuAutoSharding.get().getShard(contentTopics[0])
133+
if shardRes.isErr():
134+
error "Failed to determine shard for spam protection", error = shardRes.error
135+
await sleepAsync(RetryInterval)
136+
continue
137+
138+
let shard = shardRes.get()
139+
let pubsubTopic: PubsubTopic = shard # converter toPubsubTopic
140+
141+
# Subscribe to spam protection topics
142+
let res = await node.wakuFilterClient.subscribe(
143+
currentFilterPeer.get(), pubsubTopic, contentTopics
144+
)
145+
if res.isErr():
146+
noFailedSubscribes += 1
147+
warn "Failed to subscribe to spam protection topics via filter",
148+
error = res.error, topics = contentTopics, failCount = noFailedSubscribes
149+
150+
if noFailedSubscribes >= MaxFailedSubscribes:
151+
# Try with a different peer
152+
warn "Max subscription failures reached, selecting new filter peer"
153+
currentFilterPeer = none(RemotePeerInfo)
154+
noFailedSubscribes = 0
155+
156+
await sleepAsync(RetryInterval)
157+
else:
158+
info "Successfully subscribed to spam protection topics via filter",
159+
topics = contentTopics, peer = currentFilterPeer.get().peerId
160+
noFailedSubscribes = 0
161+
await sleepAsync(SubscriptionMaintenance)
162+
61163
const Help =
62164
"""
63165
Commands: /[?|help|connect|nick|exit]
@@ -229,20 +331,21 @@ proc publish(c: Chat, line: string) {.async.} =
229331
try:
230332
if not c.node.wakuLightpushClient.isNil():
231333
# Attempt lightpush with mix
232-
233-
(
234-
waitFor c.node.lightpushPublish(
235-
some(c.conf.getPubsubTopic(c.node, c.contentTopic)),
236-
message,
237-
none(RemotePeerInfo),
238-
true,
239-
)
240-
).isOkOr:
241-
error "failed to publish lightpush message", error = error
334+
let res = await c.node.lightpushPublish(
335+
some(c.conf.getPubsubTopic(c.node, c.contentTopic)),
336+
message,
337+
none(RemotePeerInfo),
338+
true,
339+
)
340+
if res.isErr():
341+
error "failed to publish lightpush message", error = res.error
342+
echo "Error: " & res.error.desc.get("unknown error")
242343
else:
243344
error "failed to publish message as lightpush client is not initialized"
345+
echo "Error: lightpush client is not initialized"
244346
except CatchableError:
245347
error "caught error publishing message: ", error = getCurrentExceptionMsg()
348+
echo "Error: " & getCurrentExceptionMsg()
246349

247350
# TODO This should read or be subscribe handler subscribe
248351
proc readAndPrint(c: Chat) {.async.} =
@@ -471,7 +574,11 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} =
471574
error "failed to generate mix key pair", error = error
472575
return
473576

474-
(await node.mountMix(conf.clusterId, mixPrivKey, conf.mixnodes)).isOkOr:
577+
(
578+
await node.mountMix(
579+
conf.clusterId, mixPrivKey, conf.mixnodes, some(conf.rlnUserMessageLimit)
580+
)
581+
).isOkOr:
475582
error "failed to mount waku mix protocol: ", error = $error
476583
quit(QuitFailure)
477584

@@ -500,6 +607,10 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} =
500607

501608
#await node.mountRendezvousClient(conf.clusterId)
502609

610+
# Subscribe to spam protection coordination topics via filter since chat2mix doesn't use relay
611+
if not node.wakuFilterClient.isNil():
612+
asyncSpawn setupMixSpamProtectionViaFilter(node)
613+
503614
await node.start()
504615

505616
node.peerManager.start()

apps/chat2mix/config_chat2mix.nim

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,13 @@ type
237237
name: "kad-bootstrap-node"
238238
.}: seq[string]
239239

240+
## RLN spam protection config
241+
rlnUserMessageLimit* {.
242+
desc: "Maximum messages per epoch for RLN spam protection.",
243+
defaultValue: 100,
244+
name: "rln-user-message-limit"
245+
.}: int
246+
240247
proc parseCmdArg*(T: type MixNodePubInfo, p: string): T =
241248
let elements = p.split(":")
242249
if elements.len != 2:

config.nims

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import os
22

3+
when fileExists("nimbus-build-system.paths"):
4+
include "nimbus-build-system.paths"
5+
36
if defined(release):
47
switch("nimcache", "nimcache/release/$projectName")
58
else:

simulations/mixnet/README.md

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,66 +3,127 @@
33
## Aim
44

55
Simulate a local mixnet along with a chat app to publish using mix.
6-
This is helpful to test any changes while development.
7-
It includes scripts that run a `4 node` mixnet along with a lightpush service node(without mix) that can be used to test quickly.
6+
This is helpful to test any changes during development.
87

98
## Simulation Details
109

11-
Note that before running the simulation both `wakunode2` and `chat2mix` have to be built.
10+
The simulation includes:
11+
12+
1. A 5-node mixnet where `run_mix_node.sh` is the bootstrap node for the other 4 nodes
13+
2. Two chat app instances that publish messages using lightpush protocol over the mixnet
14+
15+
### Available Scripts
16+
17+
| Script | Description |
18+
|--------|-------------|
19+
| `run_mix_node.sh` | Bootstrap mix node (must be started first) |
20+
| `run_mix_node1.sh` | Mix node 1 |
21+
| `run_mix_node2.sh` | Mix node 2 |
22+
| `run_mix_node3.sh` | Mix node 3 |
23+
| `run_mix_node4.sh` | Mix node 4 |
24+
| `run_chat_mix.sh` | Chat app instance 1 |
25+
| `run_chat_mix1.sh` | Chat app instance 2 |
26+
| `build_setup.sh` | Build and generate RLN credentials |
27+
28+
## Prerequisites
29+
30+
Before running the simulation, build `wakunode2` and `chat2mix`:
1231

1332
```bash
1433
cd <repo-root-dir>
15-
make wakunode2
16-
make chat2mix
34+
make wakunode2 chat2mix
35+
```
36+
37+
## RLN Spam Protection Setup
38+
39+
Before running the simulation, generate RLN credentials and the shared Merkle tree for all nodes:
40+
41+
```bash
42+
cd simulations/mixnet
43+
./build_setup.sh
1744
```
1845

19-
Simulation includes scripts for:
46+
This script will:
47+
48+
1. Build and run the `setup_credentials` tool
49+
2. Generate RLN credentials for all nodes (5 mix nodes + 2 chat clients)
50+
3. Create `rln_tree.db` - the shared Merkle tree with all members
51+
4. Create keystore files (`rln_keystore_{peerId}.json`) for each node
2052

21-
1. a 4 waku-node mixnet where `node1` is bootstrap node for the other 3 nodes.
22-
2. scripts to run chat app that publishes using lightpush protocol over the mixnet
53+
**Important:** All scripts must be run from this directory (`simulations/mixnet/`) so they can access their credentials and tree file.
54+
55+
To regenerate credentials (e.g., after adding new nodes), run `./build_setup.sh` again - it will clean up old files first.
2356

2457
## Usage
2558

26-
Start the service node with below command, which acts as bootstrap node for all other mix nodes.
59+
### Step 1: Start the Mix Nodes
60+
61+
Start the bootstrap node first (in a separate terminal):
2762

28-
`./run_lp_service_node.sh`
63+
```bash
64+
./run_mix_node.sh
65+
```
2966

30-
To run the nodes for mixnet run the 4 node scripts in different terminals as below.
67+
Look for the following log lines to ensure the node started successfully:
3168

32-
`./run_mix_node1.sh`
69+
```log
70+
INF mounting mix protocol topics="waku node"
71+
INF Node setup complete topics="wakunode main"
72+
```
3373

34-
Look for following 2 log lines to ensure node ran successfully and has also mounted mix protocol.
74+
Verify RLN spam protection initialized correctly by checking for these logs:
3575

3676
```log
37-
INF 2025-08-01 14:51:05.445+05:30 mounting mix protocol topics="waku node" tid=39996871 file=waku_node.nim:231 nodeId="(listenAddresses: @[\"/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o\"], enrUri: \"enr:-NC4QKYtas8STkenlqBTJ3a1TTLzJA2DsGGbFlnxem9aSM2IXm-CSVZULdk2467bAyFnepnt8KP_QlfDzdaMXd_zqtwBgmlkgnY0gmlwhH8AAAGHbWl4LWtleaCdCc5iT3bo9gYmXtucyit96bQXcqbXhL3a-S_6j7p9LIptdWx0aWFkZHJzgIJyc4UAAgEAAIlzZWNwMjU2azGhA6RFtVJVBh0SYOoP8xrgnXSlpiFARmQkF9d8Rn4fSeiog3RjcILqYYN1ZHCCIymFd2FrdTIt\")"
77+
INF Initializing MixRlnSpamProtection
78+
INF MixRlnSpamProtection initialized, waiting for sync
79+
DBG Tree loaded from file
80+
INF MixRlnSpamProtection started
81+
```
3882

39-
INF 2025-08-01 14:49:23.467+05:30 Node setup complete topics="wakunode main" tid=39994244 file=wakunode2.nim:104
83+
Then start the remaining mix nodes in separate terminals:
84+
85+
```bash
86+
./run_mix_node1.sh
87+
./run_mix_node2.sh
88+
./run_mix_node3.sh
89+
./run_mix_node4.sh
4090
```
4191

42-
Once all the 4 nodes are up without any issues, run the script to start the chat application.
92+
### Step 2: Start the Chat Applications
93+
94+
Once all 5 mix nodes are running, start the first chat app:
4395

44-
`./run_chat_app.sh`
96+
```bash
97+
./run_chat_mix.sh
98+
```
4599

46-
Enter a nickname to be used.
100+
Enter a nickname when prompted:
47101

48102
```bash
49103
pubsub topic is: /waku/2/rs/2/0
50104
Choose a nickname >>
51105
```
52106

53-
Once you see below log, it means the app is ready for publishing messages over the mixnet.
107+
Once you see the following log, the app is ready to publish messages over the mixnet:
54108

55109
```bash
56110
Welcome, test!
57111
Listening on
58-
/ip4/192.168.68.64/tcp/60000/p2p/16Uiu2HAkxDGqix1ifY3wF1ZzojQWRAQEdKP75wn1LJMfoHhfHz57
112+
/ip4/<local-network-ip>/tcp/60000/p2p/16Uiu2HAkxDGqix1ifY3wF1ZzojQWRAQEdKP75wn1LJMfoHhfHz57
59113
ready to publish messages now
60114
```
61115

62-
Follow similar instructions to run second instance of chat app.
63-
Once both the apps run successfully, send a message and check if it is received by the other app.
116+
Start the second chat app in another terminal:
117+
118+
```bash
119+
./run_chat_mix1.sh
120+
```
121+
122+
### Step 3: Test Messaging
123+
124+
Once both chat apps are running, send a message from one and verify it is received by the other.
64125

65-
You can exit the chat apps by entering `/exit` as below
126+
To exit the chat apps, enter `/exit`:
66127

67128
```bash
68129
>> /exit

simulations/mixnet/build_setup.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/bash
2+
cd "$(dirname "$0")"
3+
MIXNET_DIR=$(pwd)
4+
cd ../..
5+
ROOT_DIR=$(pwd)
6+
7+
# Clean up old files first
8+
rm -f "$MIXNET_DIR/rln_tree.db" "$MIXNET_DIR"/rln_keystore_*.json
9+
10+
echo "Building and running credentials setup..."
11+
# Compile to temp location, then run from mixnet directory
12+
nim c -d:release --mm:refc \
13+
--passL:"-L$ROOT_DIR/vendor/zerokit/target/release -lrln" \
14+
-o:/tmp/setup_credentials_$$ \
15+
"$MIXNET_DIR/setup_credentials.nim" 2>&1 | tail -30
16+
17+
# Run from mixnet directory so files are created there
18+
cd "$MIXNET_DIR"
19+
/tmp/setup_credentials_$$
20+
21+
# Clean up temp binary
22+
rm -f /tmp/setup_credentials_$$
23+
24+
# Verify output
25+
if [ -f "rln_tree.db" ]; then
26+
echo ""
27+
echo "Tree file ready at: $(pwd)/rln_tree.db"
28+
ls -la rln_keystore_*.json 2>/dev/null | wc -l | xargs -I {} echo "Generated {} keystore files"
29+
else
30+
echo "Setup failed - rln_tree.db not found"
31+
exit 1
32+
fi

0 commit comments

Comments
 (0)