Skip to content

Commit fb93fc7

Browse files
chaitanyapremclaude
andcommitted
integrate RLN spam protection into mix protocol with OffChainGroupManager and logos-messaging for broadcasting membership changes and general coordination with API updates and fixes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 45da95f commit fb93fc7

19 files changed

+763
-66
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ node_modules/
4343

4444
# RLN / keystore
4545
rlnKeystore.json
46+
rln_keystore*.json
4647
*.tar.gz
4748

4849
# Nimbus Build System

.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
@@ -35,6 +35,7 @@ import
3535
import
3636
waku/[
3737
waku_core,
38+
waku_core/topics/sharding,
3839
waku_lightpush/common,
3940
waku_lightpush/rpc,
4041
waku_enr,
@@ -47,6 +48,8 @@ import
4748
common/utils/nat,
4849
waku_store/common,
4950
waku_filter_v2/client,
51+
waku_filter_v2/common as filter_common,
52+
waku_mix/protocol,
5053
common/logging,
5154
],
5255
./config_chat2mix
@@ -57,6 +60,105 @@ import ../../waku/waku_rln_relay
5760
logScope:
5861
topics = "chat2 mix"
5962

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

228331
# TODO This should read or be subscribe handler subscribe
229332
proc readAndPrint(c: Chat) {.async.} =
@@ -452,7 +555,11 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} =
452555
error "failed to generate mix key pair", error = error
453556
return
454557

455-
(await node.mountMix(conf.clusterId, mixPrivKey, conf.mixnodes)).isOkOr:
558+
(
559+
await node.mountMix(
560+
conf.clusterId, mixPrivKey, conf.mixnodes, some(conf.rlnUserMessageLimit)
561+
)
562+
).isOkOr:
456563
error "failed to mount waku mix protocol: ", error = $error
457564
quit(QuitFailure)
458565

@@ -487,6 +594,10 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} =
487594

488595
#await node.mountRendezvousClient(conf.clusterId)
489596

597+
# Subscribe to spam protection coordination topics via filter since chat2mix doesn't use relay
598+
if not node.wakuFilterClient.isNil():
599+
asyncSpawn setupMixSpamProtectionViaFilter(node)
600+
490601
await node.start()
491602

492603
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

0 commit comments

Comments
 (0)