Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ node_modules/

# RLN / keystore
rlnKeystore.json
rln_keystore*.json
*.tar.gz

# sqlite db
Expand Down Expand Up @@ -86,3 +87,6 @@ nimbledeps

**/anvil_state/state-deployed-contracts-mint-and-approved.json
.gitnexus

# sim driver script (local dev tool, not part of build/CI)
simulations/mixnet/roundtrip_check.sh
19 changes: 16 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,15 @@ deps: | nimble
##################
## RLN ##
##################
.PHONY: librln
.PHONY: librln mix-librln

LIBRLN_BUILDDIR := $(CURDIR)/vendor/zerokit
LIBRLN_VERSION := v0.9.0
MIX_LIBRLN_VERSION ?= v2.0.0
MIX_LIBRLN_REPO ?= https://github.com/vacp2p/zerokit.git
MIX_LIBRLN_SRCDIR ?= $(CURDIR)/build/zerokit_$(MIX_LIBRLN_VERSION)
MIX_LIBRLN_FILE ?= $(CURDIR)/build/librln_mix_$(MIX_LIBRLN_VERSION).a
MIX_LIBRLN_NIM_PARAMS := --passL:$(MIX_LIBRLN_FILE) --passL:-lm

ifeq ($(detected_OS),Windows)
LIBRLN_FILE ?= rln.lib
Expand All @@ -189,12 +194,20 @@ $(LIBRLN_FILE):
echo -e $(BUILD_MSG) "$@" && \
bash scripts/build_rln.sh $(LIBRLN_BUILDDIR) $(LIBRLN_VERSION) $(LIBRLN_FILE)

$(MIX_LIBRLN_FILE):
echo -e $(BUILD_MSG) "$@" && \
./scripts/build_rln_mix.sh $(MIX_LIBRLN_SRCDIR) $(MIX_LIBRLN_VERSION) $(MIX_LIBRLN_FILE) $(MIX_LIBRLN_REPO)

librln: | $(LIBRLN_FILE)
$(eval NIM_PARAMS += --passL:$(LIBRLN_FILE) --passL:-lm)

mix-librln: | $(MIX_LIBRLN_FILE)
$(eval NIM_PARAMS += --passL:$(MIX_LIBRLN_FILE) --passL:-lm)

clean-librln:
cargo clean --manifest-path vendor/zerokit/rln/Cargo.toml
rm -f $(LIBRLN_FILE)
rm -f $(MIX_LIBRLN_FILE)

clean: | clean-librln

Expand All @@ -216,7 +229,7 @@ testwaku: | $(NIMBLEDEPS_STAMP) build rln-deps librln
echo -e $(BUILD_MSG) "build/$@" && \
nimble test

wakunode2: | $(NIMBLEDEPS_STAMP) build deps librln
wakunode2: | $(NIMBLEDEPS_STAMP) build deps librln mix-librln
echo -e $(BUILD_MSG) "build/$@" && \
nimble wakunode2

Expand All @@ -236,7 +249,7 @@ chat2: | $(NIMBLEDEPS_STAMP) build deps librln
echo -e $(BUILD_MSG) "build/$@" && \
nimble chat2

chat2mix: | $(NIMBLEDEPS_STAMP) build deps librln
chat2mix: | $(NIMBLEDEPS_STAMP) build deps librln mix-librln
echo -e $(BUILD_MSG) "build/$@" && \
nimble chat2mix

Expand Down
137 changes: 125 additions & 12 deletions apps/chat2mix/chat2mix.nim
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import
import
waku/[
waku_core,
waku_core/topics/sharding,
waku_lightpush/common,
waku_lightpush/rpc,
waku_enr,
Expand All @@ -47,6 +48,8 @@ import
common/utils/nat,
waku_store/common,
waku_filter_v2/client,
waku_filter_v2/common as filter_common,
waku_mix/protocol,
common/logging,
],
./config_chat2mix
Expand All @@ -57,7 +60,108 @@ import ../../waku/waku_rln_relay
logScope:
topics = "chat2 mix"

const Help = """

#########################
## Mix Spam Protection ##
#########################

# Forward declaration
proc maintainSpamProtectionSubscription(
node: WakuNode, contentTopics: seq[ContentTopic]
) {.async.}

proc setupMixSpamProtectionViaFilter(node: WakuNode) {.async.} =
## Setup filter-based spam protection coordination for mix protocol.
## Since chat2mix doesn't use relay, we subscribe via filter to receive
## spam protection coordination messages.

# Register message handler for spam protection coordination
let spamTopics = node.wakuMix.getSpamProtectionContentTopics()

proc handleSpamMessage(
pubsubTopic: PubsubTopic, message: WakuMessage
): Future[void] {.async, gcsafe.} =
await node.wakuMix.handleMessage(pubsubTopic, message)

node.wakuFilterClient.registerPushHandler(handleSpamMessage)

# Wait for filter peer and maintain subscription
asyncSpawn maintainSpamProtectionSubscription(node, spamTopics)

proc maintainSpamProtectionSubscription(
node: WakuNode, contentTopics: seq[ContentTopic]
) {.async.} =
## Maintain filter subscription for spam protection topics.
## Monitors subscription health with periodic pings and re-subscribes on failure.
const RetryInterval = chronos.seconds(5)
const SubscriptionMaintenance = chronos.seconds(30)
const MaxFailedSubscribes = 3
var currentFilterPeer: Option[RemotePeerInfo] = none(RemotePeerInfo)
var noFailedSubscribes = 0

while true:
# Select or reuse filter peer
if currentFilterPeer.isNone():
let filterPeerOpt = node.peerManager.selectPeer(WakuFilterSubscribeCodec)
if filterPeerOpt.isNone():
debug "No filter peer available yet for spam protection, retrying..."
await sleepAsync(RetryInterval)
continue
currentFilterPeer = some(filterPeerOpt.get())
info "Selected filter peer for spam protection",
peer = currentFilterPeer.get().peerId

# Check if subscription is still alive with ping
let pingErr = (await node.wakuFilterClient.ping(currentFilterPeer.get())).errorOr:
# Subscription is alive, wait before next check
await sleepAsync(SubscriptionMaintenance)
if noFailedSubscribes > 0:
noFailedSubscribes = 0
continue

# Subscription lost, need to re-subscribe
warn "Spam protection filter subscription ping failed, re-subscribing",
error = pingErr, peer = currentFilterPeer.get().peerId

# Determine pubsub topic from content topics (using auto-sharding)
if node.wakuAutoSharding.isNone():
error "Auto-sharding not configured, cannot determine pubsub topic for spam protection"
await sleepAsync(RetryInterval)
continue

let shardRes = node.wakuAutoSharding.get().getShard(contentTopics[0])
if shardRes.isErr():
error "Failed to determine shard for spam protection", error = shardRes.error
await sleepAsync(RetryInterval)
continue

let shard = shardRes.get()
let pubsubTopic: PubsubTopic = shard # converter toPubsubTopic

# Subscribe to spam protection topics
let res = await node.wakuFilterClient.subscribe(
currentFilterPeer.get(), pubsubTopic, contentTopics
)
if res.isErr():
noFailedSubscribes += 1
warn "Failed to subscribe to spam protection topics via filter",
error = res.error, topics = contentTopics, failCount = noFailedSubscribes

if noFailedSubscribes >= MaxFailedSubscribes:
# Try with a different peer
warn "Max subscription failures reached, selecting new filter peer"
currentFilterPeer = none(RemotePeerInfo)
noFailedSubscribes = 0

await sleepAsync(RetryInterval)
else:
info "Successfully subscribed to spam protection topics via filter",
topics = contentTopics, peer = currentFilterPeer.get().peerId
noFailedSubscribes = 0
await sleepAsync(SubscriptionMaintenance)

const Help =
"""
Commands: /[?|help|connect|nick|exit]
help: Prints this help
connect: dials a remote peer
Expand Down Expand Up @@ -209,20 +313,21 @@ proc publish(c: Chat, line: string) {.async.} =
try:
if not c.node.wakuLightpushClient.isNil():
# Attempt lightpush with mix

(
waitFor c.node.lightpushPublish(
some(c.conf.getPubsubTopic(c.node, c.contentTopic)),
message,
none(RemotePeerInfo),
true,
)
).isOkOr:
error "failed to publish lightpush message", error = error
let res = await c.node.lightpushPublish(
some(c.conf.getPubsubTopic(c.node, c.contentTopic)),
message,
none(RemotePeerInfo),
true,
)
if res.isErr():
error "failed to publish lightpush message", error = res.error
echo "Error: " & res.error.desc.get("unknown error")
else:
error "failed to publish message as lightpush client is not initialized"
echo "Error: lightpush client is not initialized"
except CatchableError:
error "caught error publishing message: ", error = getCurrentExceptionMsg()
echo "Error: " & getCurrentExceptionMsg()

# TODO This should read or be subscribe handler subscribe
proc readAndPrint(c: Chat) {.async.} =
Expand Down Expand Up @@ -451,7 +556,11 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} =
error "failed to generate mix key pair", error = error
return

(await node.mountMix(conf.clusterId, mixPrivKey, conf.mixnodes)).isOkOr:
(
await node.mountMix(
conf.clusterId, mixPrivKey, conf.mixnodes, some(conf.rlnUserMessageLimit)
)
).isOkOr:
error "failed to mount waku mix protocol: ", error = $error
quit(QuitFailure)

Expand Down Expand Up @@ -486,6 +595,10 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} =

#await node.mountRendezvousClient(conf.clusterId)

# Subscribe to spam protection coordination topics via filter since chat2mix doesn't use relay
if not node.wakuFilterClient.isNil():
asyncSpawn setupMixSpamProtectionViaFilter(node)

await node.start()

node.peerManager.start()
Expand Down
16 changes: 14 additions & 2 deletions apps/chat2mix/config_chat2mix.nim
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ type

metricsServerAddress* {.
desc: "Listening address of the metrics server.",
defaultValue: parseIpAddress("127.0.0.1"),
defaultValue:
IpAddress(family: IpAddressFamily.IPv4, address_v4: [127.byte, 0, 0, 1]),
name: "metrics-server-address"
.}: IpAddress

Expand Down Expand Up @@ -194,7 +195,11 @@ type

dnsDiscoveryNameServers* {.
desc: "DNS name server IPs to query. Argument may be repeated.",
defaultValue: @[parseIpAddress("1.1.1.1"), parseIpAddress("1.0.0.1")],
defaultValue:
@[
IpAddress(family: IpAddressFamily.IPv4, address_v4: [1.byte, 1, 1, 1]),
IpAddress(family: IpAddressFamily.IPv4, address_v4: [1.byte, 0, 0, 1]),
],
name: "dns-discovery-name-server"
.}: seq[IpAddress]

Expand Down Expand Up @@ -236,6 +241,13 @@ type
name: "kad-bootstrap-node"
.}: seq[string]

## RLN spam protection config
rlnUserMessageLimit* {.
desc: "Maximum messages per epoch for RLN spam protection.",
defaultValue: 100,
name: "rln-user-message-limit"
.}: int

proc parseCmdArg*(T: type MixNodePubInfo, p: string): T =
let elements = p.split(":")
if elements.len != 2:
Expand Down
43 changes: 43 additions & 0 deletions scripts/build_rln_mix.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash

# Build a separate, pinned RLN library for mix spam-protection usage.
# This keeps the main nwaku RLN dependency flow unchanged.

set -euo pipefail

source_dir="${1:-}"
version="${2:-}"
output_file="${3:-}"
repo_url="${4:-https://github.com/vacp2p/zerokit.git}"

if [[ -z "${source_dir}" || -z "${version}" || -z "${output_file}" ]]; then
echo "Usage: $0 <source_dir> <version_tag> <output_file> [repo_url]"
exit 1
fi

mkdir -p "$(dirname "${source_dir}")"
mkdir -p "$(dirname "${output_file}")"

if [[ ! -d "${source_dir}/.git" ]]; then
echo "Cloning zerokit ${version} from ${repo_url}..."
if [[ -e "${source_dir}" ]]; then
echo "Path exists but is not a git repository: ${source_dir}"
echo "Please remove it and retry."
exit 1
fi
git clone --depth 1 --branch "${version}" "${repo_url}" "${source_dir}"
else
echo "Using existing zerokit checkout in ${source_dir}"
current_tag="$(git -C "${source_dir}" describe --tags --exact-match 2>/dev/null || true)"
if [[ "${current_tag}" != "${version}" ]]; then
echo "Updating zerokit checkout to ${version}..."
git -C "${source_dir}" fetch --tags origin "${version}"
git -C "${source_dir}" checkout --detach "${version}"
fi
fi

echo "Building mix RLN library from source (version ${version})..."
cargo build --release -p rln --manifest-path "${source_dir}/rln/Cargo.toml"

cp "${source_dir}/target/release/librln.a" "${output_file}"
echo "Successfully built ${output_file}"
Loading
Loading