mirror of
https://github.com/logos-messaging/nim-chat-poc.git
synced 2026-07-03 07:00:16 +00:00
feat: mix+LEZ+RLN chat over the testnet via 2-phase gifter
Chat-side integration of the LEZ-backed RLN mix protocol:
- src/chat/delivery/waku_client.nim: mount waku_mix with onchain
RLN spam protection wired to logos_core_client fetchers; gate
the first publish on (a) gifter status confirmation, (b)
cushion of 2 poll intervals after confirmation, and (c) proof
root stability in the local valid_roots window; wrap mix
lightpush in withTimeout so vanished SURB replies surface as
Err instead of pinning the send coroutine.
- src/chat/client.nim: surface sendBytes errors via asyncSpawn
wrapped try/except instead of discarding the future (was
hiding every mix-publish failure).
- chat-side gifter client invocation (RLN membership service
wire format, EIP-191 ethereum-allowlist auth).
- Background membership status watcher that reconciles the
optimistic leaf returned by the gifter against the chain's
authoritative leaf via the status RPC.
Simulation harness (simulations/mix_lez_chat/):
- Spin up sequencer + run_setup + 4 mix nodes (one of which
runs the gifter service) + chat sender + chat receiver.
- SIM_NETWORK={local,testnet}, SIM_SLIM for testnet (reuses
shipped config_account + cached payment_account), Docker
image + GHCR for cross-platform testing.
- Strict mix-pool readiness gate, kademlia + RLN root activity
checks, gifter EIP-191 auth fixture, slim-mode submodule
minimization.
- TREE_ID_HEX pinned to the canonical testnet deployment.
Submodule bumps:
- vendor/nwaku to 8e6ba04 (LEZ-backed RLN mix + 2-phase gifter).
- vendor/logos-lez-rln to 950f287 (SPEL RLN program + mix sim
infrastructure + canonical testnet deploy).
Docs:
- RUN_SLIM_TESTNET.md: slim sim recipe.
- cleanup/MODE_A_GIFTER_SLOT_BUG.md: per-signer nonce collision
postmortem driving the queue+worker fix.
This commit is contained in:
parent
15f68f2ec2
commit
29c64b340d
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
# Everything — the Dockerfile doesn't COPY from the build context.
|
||||
# Guest binaries are staged via docker cp separately.
|
||||
*
|
||||
170
.github/Dockerfile.sim
vendored
Normal file
170
.github/Dockerfile.sim
vendored
Normal file
@ -0,0 +1,170 @@
|
||||
# ===========================================================================
|
||||
# Stage 1: Builder — build everything, package runtime closure
|
||||
# ===========================================================================
|
||||
FROM catthehacker/ubuntu:act-latest AS builder
|
||||
|
||||
RUN apt-get update -qq && apt-get install -y -qq \
|
||||
build-essential pkg-config libssl-dev git curl docker.io xxd \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Nix
|
||||
RUN curl -L https://nixos.org/nix/install | bash -s -- --daemon --yes
|
||||
RUN mkdir -p /root/.config/nix && \
|
||||
printf 'experimental-features = nix-command flakes\nsandbox = false\n' > /root/.config/nix/nix.conf
|
||||
RUN rmdir /homeless-shelter 2>/dev/null || true
|
||||
|
||||
# Rust + cargo-risczero
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
RUN cargo install cargo-risczero
|
||||
|
||||
RUN git config --global url."https://github.com/".insteadOf "git@github.com:"
|
||||
|
||||
SHELL ["/bin/bash", "-lc"]
|
||||
|
||||
# logos-blockchain-circuits (architecture-aware)
|
||||
RUN ARCH=$(uname -m | sed 's/arm64/aarch64/') && \
|
||||
CIRCUITS_URL="https://github.com/logos-blockchain/logos-blockchain-circuits/releases/download/v0.4.2/logos-blockchain-circuits-v0.4.2-linux-${ARCH}.tar.gz" && \
|
||||
echo "Downloading circuits: $CIRCUITS_URL" && \
|
||||
mkdir -p /root/.logos-blockchain-circuits && \
|
||||
curl -sL "$CIRCUITS_URL" | tar xz -C /root/.logos-blockchain-circuits && \
|
||||
mv /root/.logos-blockchain-circuits/logos-blockchain-circuits-*/* /root/.logos-blockchain-circuits/ 2>/dev/null || true && \
|
||||
rmdir /root/.logos-blockchain-circuits/logos-blockchain-circuits-* 2>/dev/null || true
|
||||
|
||||
# --- LEZ modules ---
|
||||
|
||||
RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \
|
||||
cd /root && \
|
||||
git clone -b feat/mix-rln-gifter-sim https://github.com/logos-co/logos-lez-rln.git && \
|
||||
cd logos-lez-rln && \
|
||||
git submodule update --init && \
|
||||
(cd logos-delivery-module && git submodule update --init --recursive)
|
||||
|
||||
RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \
|
||||
cd /root/logos-lez-rln && \
|
||||
nix build .#logos-rln-module -o logos-rln-module/result-rln
|
||||
|
||||
RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \
|
||||
cd /root/logos-lez-rln && \
|
||||
(cd logos-execution-zone-module && \
|
||||
RISC0_SKIP_BUILD_KERNELS=1 nix build --impure \
|
||||
--override-input logos-execution-zone "git+file://$(pwd)/../lssa" \
|
||||
-o ../logos-rln-module/result-wallet)
|
||||
|
||||
RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \
|
||||
nix build github:logos-co/logos-liblogos/7df6195 \
|
||||
--override-input logos-cpp-sdk github:logos-co/logos-cpp-sdk/a4bd66c \
|
||||
-o /root/logoscore-result
|
||||
|
||||
RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \
|
||||
cd /root/logos-lez-rln/logos-delivery-module/vendor/logos-delivery && \
|
||||
make -j1 liblogosdelivery 2>&1 | tail -5
|
||||
|
||||
RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \
|
||||
cd /root/logos-lez-rln && \
|
||||
SDK_PATH=$(nix build github:logos-co/logos-cpp-sdk/a4bd66c --no-link --print-out-paths 2>/dev/null) && \
|
||||
LIBLOGOS_PATH=$(nix build github:logos-co/logos-liblogos/7df6195 \
|
||||
--override-input logos-cpp-sdk github:logos-co/logos-cpp-sdk/a4bd66c \
|
||||
--no-link --print-out-paths 2>/dev/null) && \
|
||||
QT_BASE=$(find /nix/store -maxdepth 1 -name "*qtbase-6*" -type d 2>/dev/null | head -1) && \
|
||||
QT_RO=$(find /nix/store -maxdepth 1 -name "*qtremoteobjects-6*" -type d 2>/dev/null | head -1) && \
|
||||
DELIVERY_MOD="$PWD/logos-delivery-module" && \
|
||||
cd logos-delivery-module && rm -rf build_plugin && mkdir build_plugin && cd build_plugin && \
|
||||
mkdir -p delivery_root/bin delivery_root/liblogosdelivery && \
|
||||
cp "$DELIVERY_MOD/vendor/logos-delivery/build/liblogosdelivery."* delivery_root/bin/ 2>/dev/null || true && \
|
||||
cp "$DELIVERY_MOD/vendor/logos-delivery/liblogosdelivery/liblogosdelivery.h" delivery_root/liblogosdelivery/ && \
|
||||
nix-shell -p cmake ninja pkg-config postgresql --run " \
|
||||
cmake .. -GNinja \
|
||||
-DLOGOS_CPP_SDK_ROOT=$SDK_PATH \
|
||||
-DLOGOS_LIBLOGOS_ROOT=$LIBLOGOS_PATH \
|
||||
-DLOGOS_DELIVERY_ROOT=\$PWD/delivery_root \
|
||||
-DLOGOS_MESSAGING_MODULE_USE_VENDOR=OFF \
|
||||
-DCMAKE_PREFIX_PATH=\"$QT_BASE;$QT_RO\" \
|
||||
-DQT_ADDITIONAL_PACKAGES_PREFIX_PATH=$QT_RO && \
|
||||
ninja" 2>&1 | tail -5
|
||||
|
||||
RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \
|
||||
CLANG_SO=$(find /nix/store -maxdepth 3 -name 'libclang.so' 2>/dev/null | head -1) && \
|
||||
export LIBCLANG_PATH=$(dirname "$CLANG_SO") && \
|
||||
STDBOOL=$(find "$LIBCLANG_PATH" -maxdepth 5 -name 'stdbool.h' 2>/dev/null | head -1) && \
|
||||
[ -n "$STDBOOL" ] && export BINDGEN_EXTRA_CLANG_ARGS="-I$(dirname $STDBOOL)" ; \
|
||||
cd /root/logos-lez-rln/lssa && \
|
||||
cargo build --features standalone -p sequencer_service 2>&1 | tail -5
|
||||
|
||||
# Build run_setup binary (deploys LEZ programs to sequencer at sim runtime)
|
||||
RUN cd /root/logos-lez-rln/lez-rln && \
|
||||
cargo build --bin run_setup 2>&1 | tail -5
|
||||
|
||||
# --- logos-chat (liblogoschat) ---
|
||||
|
||||
RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \
|
||||
cd /root && \
|
||||
git clone -b feat/logos-delivery https://github.com/adklempner/logos-chat.git && \
|
||||
cd logos-chat && \
|
||||
git submodule update --init --depth 1 && \
|
||||
(cd vendor/nwaku && git submodule update --init --recursive --depth 1) && \
|
||||
(cd vendor/nimbus-build-system && git submodule update --init --recursive --depth 1) && \
|
||||
make update && make liblogoschat 2>&1 | tail -5
|
||||
|
||||
# --- logos-chat-module (chat_module_plugin) ---
|
||||
|
||||
RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \
|
||||
cd /root && \
|
||||
git clone -b feat/logos-delivery https://github.com/adklempner/logos-chat-module.git && \
|
||||
cd logos-chat-module && nix build 2>&1 | tail -5
|
||||
|
||||
# --- Package runtime closure ---
|
||||
|
||||
RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \
|
||||
{ \
|
||||
nix-store -qR /root/logos-lez-rln/logos-rln-module/result-rln; \
|
||||
nix-store -qR /root/logos-lez-rln/logos-rln-module/result-wallet; \
|
||||
nix-store -qR /root/logoscore-result; \
|
||||
nix-store -qR /root/logos-chat-module/result; \
|
||||
} | sort -u > /tmp/nix-closure-paths.txt && \
|
||||
tar cf /tmp/nix-closure.tar -T /tmp/nix-closure-paths.txt && \
|
||||
echo "Closure: $(wc -l < /tmp/nix-closure-paths.txt) paths, $(du -sh /tmp/nix-closure.tar | cut -f1)"
|
||||
|
||||
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Stage 2: Runtime — minimal image, no nix/Rust/cargo needed
|
||||
# ===========================================================================
|
||||
FROM catthehacker/ubuntu:act-latest
|
||||
|
||||
RUN apt-get update -qq && apt-get install -y -qq \
|
||||
build-essential pkg-config libssl-dev git curl xxd netcat-openbsd lsof \
|
||||
clang libclang-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Rust + r0vm (needed for: sequencer build + risc0 dev prover)
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
RUN cargo install cargo-risczero
|
||||
|
||||
RUN git config --global url."https://github.com/".insteadOf "git@github.com:"
|
||||
|
||||
# Import runtime nix closure from builder
|
||||
COPY --from=builder /tmp/nix-closure.tar /tmp/
|
||||
# Import nix store runtime closure from builder. These are loaded by
|
||||
# direct path (not via nix), so no nix DB registration needed.
|
||||
# The runtime nix has its own clean DB for nix-shell package downloads.
|
||||
RUN mkdir -p /nix/store && tar xf /tmp/nix-closure.tar -C / && rm /tmp/nix-closure.tar
|
||||
|
||||
# Pre-built LEZ module outputs
|
||||
COPY --from=builder /root/logos-lez-rln/logos-rln-module/result-rln /root/lez-modules/result-rln
|
||||
COPY --from=builder /root/logos-lez-rln/logos-rln-module/result-wallet /root/lez-modules/result-wallet
|
||||
COPY --from=builder /root/logos-lez-rln/logos-delivery-module/build_plugin/modules/ /root/lez-modules/delivery-plugin/
|
||||
COPY --from=builder /root/logos-lez-rln/logos-delivery-module/vendor/logos-delivery/build/ /root/lez-modules/delivery-build/
|
||||
# NOTE: sequencer + run_setup are NOT pre-built. Binaries compiled in the
|
||||
# builder stage crash at runtime due to library mismatch between Docker stages.
|
||||
# They're built from source at runtime with system clang (~1.5 min).
|
||||
# The lssa source is cloned at runtime via setup_and_run.sh submodule init.
|
||||
COPY --from=builder /root/logoscore-result /root/lez-modules/logoscore-result
|
||||
COPY --from=builder /root/.logos-blockchain-circuits /root/.logos-blockchain-circuits
|
||||
COPY --from=builder /root/logos-lez-rln/dev /root/lez-modules/dev
|
||||
|
||||
# Pre-built logos-chat outputs
|
||||
COPY --from=builder /root/logos-chat/build/liblogoschat.so /root/lez-modules/liblogoschat.so
|
||||
COPY --from=builder /root/logos-chat-module/result /root/lez-modules/chat-module-result
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -37,6 +37,9 @@ nimble.paths
|
||||
/metrics/prometheus
|
||||
/metrics/waku-sim-all-nodes-grafana-dashboard.json
|
||||
|
||||
# Simulation runtime state (logs, keystores, configs generated per run)
|
||||
/simulations/**/.sim_state/
|
||||
|
||||
*.log
|
||||
/package-lock.json
|
||||
/package.json
|
||||
|
||||
8
.gitmodules
vendored
8
.gitmodules
vendored
@ -3,9 +3,9 @@
|
||||
url = https://github.com/status-im/nimbus-build-system.git
|
||||
[submodule "vendor/nwaku"]
|
||||
path = vendor/nwaku
|
||||
url = https://github.com/waku-org/nwaku.git
|
||||
url = https://github.com/adklempner/logos-delivery.git
|
||||
ignore = untracked
|
||||
branch = master
|
||||
branch = feat/rln-gifter-sim
|
||||
[submodule "vendor/nim-protobuf-serialization"]
|
||||
path = vendor/nim-protobuf-serialization
|
||||
url = https://github.com/status-im/nim-protobuf-serialization.git
|
||||
@ -28,3 +28,7 @@
|
||||
path = vendor/nim-ffi
|
||||
url = https://github.com/logos-messaging/nim-ffi.git
|
||||
branch = master
|
||||
[submodule "vendor/logos-lez-rln"]
|
||||
path = vendor/logos-lez-rln
|
||||
url = https://github.com/logos-co/logos-lez-rln.git
|
||||
branch = feat/mix-rln-gifter-sim
|
||||
|
||||
33
Makefile
33
Makefile
@ -1,7 +1,9 @@
|
||||
export BUILD_SYSTEM_DIR := vendor/nimbus-build-system
|
||||
export EXCLUDED_NIM_PACKAGES := vendor/nwaku/vendor/nim-dnsdisc/vendor \
|
||||
vendor/nwaku/vendor/nimbus-build-system \
|
||||
vendor/nim-sds/vendor
|
||||
vendor/nwaku/vendor/nim-ffi \
|
||||
vendor/nim-sds/vendor \
|
||||
vendor/logos-lez-rln
|
||||
LINK_PCRE := 0
|
||||
FORMAT_MSG := "\\x1B[95mFormatting:\\x1B[39m"
|
||||
# we don't want an error here, so we can handle things later, in the ".DEFAULT" target
|
||||
@ -69,7 +71,7 @@ TARGET ?= prod
|
||||
## Git version
|
||||
GIT_VERSION ?= $(shell git describe --abbrev=6 --always --tags)
|
||||
## Compilation parameters. If defined in the CLI the assignments won't be executed
|
||||
NIM_PARAMS := $(NIM_PARAMS) -d:git_version=\"$(GIT_VERSION)\"
|
||||
NIM_PARAMS := $(NIM_PARAMS) -d:git_version=\"$(GIT_VERSION)\" -d:libp2p_mix_experimental_exit_is_dest
|
||||
|
||||
##################
|
||||
## Dependencies ##
|
||||
@ -78,6 +80,25 @@ NIM_PARAMS := $(NIM_PARAMS) -d:git_version=\"$(GIT_VERSION)\"
|
||||
CARGO_TARGET_DIR ?= rust-bundle/target
|
||||
RUST_BUNDLE_LIB := $(CARGO_TARGET_DIR)/release/liblogoschat_rust_bundle.a
|
||||
|
||||
# Mix RLN spam protection library (separate zerokit build for mix-rln-spam-protection-plugin)
|
||||
MIX_LIBRLN_VERSION ?= v2.0.0
|
||||
MIX_LIBRLN_FILE ?= $(CURDIR)/build/librln_mix_$(MIX_LIBRLN_VERSION).a
|
||||
MIX_LIBRLN_NIM_PARAMS := --passL:$(MIX_LIBRLN_FILE) --passL:-lm
|
||||
ifneq ($(detected_OS),Darwin)
|
||||
MIX_LIBRLN_NIM_PARAMS += --passL:"-Wl,--allow-multiple-definition"
|
||||
endif
|
||||
|
||||
.PHONY: mix-librln
|
||||
mix-librln: | $(MIX_LIBRLN_FILE)
|
||||
|
||||
$(MIX_LIBRLN_FILE):
|
||||
echo -e $(BUILD_MSG) "$@" && \
|
||||
$(CURDIR)/vendor/nwaku/scripts/build_rln_mix.sh \
|
||||
$(CURDIR)/build/zerokit_$(MIX_LIBRLN_VERSION) \
|
||||
$(MIX_LIBRLN_VERSION) \
|
||||
$(MIX_LIBRLN_FILE) && \
|
||||
$(CURDIR)/scripts/fix_mix_librln_dupes.sh $(MIX_LIBRLN_FILE) $(RUST_BUNDLE_LIB)
|
||||
|
||||
# libchat and rln each embed Rust std when built as staticlibs; linking both
|
||||
# causes duplicate-symbol errors. rust-bundle/ links them as rlibs so std
|
||||
# is emitted once. [1]
|
||||
@ -107,9 +128,9 @@ tests: | build-rust-bundle build-waku-nat logos_chat.nims
|
||||
##########
|
||||
|
||||
# Ensure there is a nimble task with a name that matches the target
|
||||
tui bot_echo pingpong: | build-rust-bundle build-waku-nat logos_chat.nims
|
||||
tui bot_echo pingpong: | build-rust-bundle build-waku-nat mix-librln logos_chat.nims
|
||||
echo -e $(BUILD_MSG) "build/$@" && \
|
||||
$(ENV_SCRIPT) nim $@ $(NIM_PARAMS) \
|
||||
$(ENV_SCRIPT) nim $@ $(NIM_PARAMS) $(MIX_LIBRLN_NIM_PARAMS) \
|
||||
--passL:$(RUST_BUNDLE_LIB) --passL:-lm \
|
||||
--path:src logos_chat.nims
|
||||
|
||||
@ -129,9 +150,9 @@ endif
|
||||
LIBLOGOSCHAT := build/liblogoschat.$(LIBLOGOSCHAT_EXT)
|
||||
|
||||
.PHONY: liblogoschat
|
||||
liblogoschat: | build-rust-bundle build-waku-nat logos_chat.nims
|
||||
liblogoschat: | build-rust-bundle build-waku-nat mix-librln logos_chat.nims
|
||||
echo -e $(BUILD_MSG) "$(LIBLOGOSCHAT)" && \
|
||||
$(ENV_SCRIPT) nim liblogoschat $(NIM_PARAMS) \
|
||||
$(ENV_SCRIPT) nim liblogoschat $(NIM_PARAMS) $(MIX_LIBRLN_NIM_PARAMS) \
|
||||
--passL:$(RUST_BUNDLE_LIB) --passL:-lm \
|
||||
--path:src logos_chat.nims && \
|
||||
echo -e "\n\x1B[92mLibrary built successfully:\x1B[39m" && \
|
||||
|
||||
66
RESTORE.md
Normal file
66
RESTORE.md
Normal file
@ -0,0 +1,66 @@
|
||||
# Restore point: working testnet sim — 2026-05-17
|
||||
|
||||
Snapshot taken before consolidation / refactoring. Reproduces a passing
|
||||
testnet sim run in two modes:
|
||||
|
||||
- **Default**: `SIM_NETWORK=testnet ./simulations/mix_lez_chat/run_simulation.sh --fresh`
|
||||
— runs `run_setup` to create a fresh per-dev payment account, registers
|
||||
all 5 sim participants on the deployed RLN tree, delivers test messages.
|
||||
- **Slim**: `SIM_NETWORK=testnet SIM_SLIM=1 ./simulations/mix_lez_chat/run_simulation.sh --fresh`
|
||||
— skips `run_setup`; uses the shared payment account shipped in the
|
||||
submodule. Useful for fresh-clone devs who don't want to build the
|
||||
`lez-rln/run_setup` Rust binary.
|
||||
|
||||
Both modes pass at ~2/3 reliability (intermittent mix-routing / QtRO flake;
|
||||
re-run with `--fresh` if a single attempt stalls).
|
||||
|
||||
## Pinned commits (per repo)
|
||||
|
||||
Each repo carries both an annotated tag and a branch named
|
||||
`restore/working-2026-05-17` at the listed commit. Either can be used to
|
||||
recover the state if HEAD moves during consolidation.
|
||||
|
||||
| Repo | Branch when tagged | Commit |
|
||||
|---|---|---|
|
||||
| `.` (logos-chat) | `feat/sim-rln-gifter-auth-v2` | `b6f094e` |
|
||||
| `vendor/nwaku` | `feat/sim-rln-gifter-auth` | `60e875f3` |
|
||||
| `vendor/logos-lez-rln` | `feat/eip191-gifter-auth` | `ed88c8f8` |
|
||||
| `vendor/logos-lez-rln/logos-delivery-module` | `feat/eip191-gifter-auth` | `94173a3d` |
|
||||
| `vendor/logos-lez-rln/logos-delivery-module/vendor/logos-delivery` | `feat/eip191-gifter-auth` | `e2799efe` |
|
||||
|
||||
## Restore
|
||||
|
||||
```bash
|
||||
# From the outer repo root:
|
||||
for r in . vendor/nwaku vendor/logos-lez-rln \
|
||||
vendor/logos-lez-rln/logos-delivery-module \
|
||||
vendor/logos-lez-rln/logos-delivery-module/vendor/logos-delivery; do
|
||||
git -C "$r" checkout restore/working-2026-05-17
|
||||
done
|
||||
```
|
||||
|
||||
After checkout, rebuild affected nim/Rust artifacts:
|
||||
|
||||
```bash
|
||||
# Rebuild liblogosdelivery (used by sim's delivery_module_plugin)
|
||||
( cd vendor/logos-lez-rln/logos-delivery-module/vendor/logos-delivery && make liblogosdelivery )
|
||||
|
||||
# Rebuild run_setup (only needed for default mode, not slim)
|
||||
( cd vendor/logos-lez-rln/lez-rln && cargo build --bin run_setup )
|
||||
```
|
||||
|
||||
## What's in this state
|
||||
|
||||
- TREE_ID `…1a05100200000000` (2026-05-16 deploy #2) — programs deployed on
|
||||
testnet, `is_initialized=true`.
|
||||
- `vendor/logos-lez-rln/testnet/storage.json.seed` ships full post-deploy
|
||||
wallet state (10 accounts: 2 roots + 3 deploy publics + intermediate +
|
||||
shared payment + 4 preconfigured).
|
||||
- `vendor/logos-lez-rln/testnet/{supply_holding,payment_account,config_account}.txt`
|
||||
for the slim-mode bootstrap.
|
||||
- Gifter (mix node 0) self-registers as a mix relay at startup
|
||||
(eliminates the "Plugin not ready" forwarding drop when sender's
|
||||
circuit routes through it).
|
||||
- FFI aligned with SPEL `ConfigState` (240-byte borsh) + 4-field Register
|
||||
instruction.
|
||||
- Cross-thread Nim heap-string race fix in `mix_lez_client`.
|
||||
29
RUN_SLIM_TESTNET.md
Normal file
29
RUN_SLIM_TESTNET.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Slim sim against latest testnet deployment
|
||||
|
||||
```bash
|
||||
git clone -b feat/sim-rln-gifter-auth-v2 git@github.com:logos-messaging/logos-chat.git
|
||||
cd logos-chat && git submodule update --init --recursive
|
||||
SIM_NETWORK=testnet SIM_SLIM=1 SIM_DELIVERY_TIMEOUT=1800 \
|
||||
bash simulations/mix_lez_chat/run_simulation.sh --fresh
|
||||
```
|
||||
|
||||
~15-25 min depending on testnet block timing. Pass: **ALL 15 CHECKS PASSED**.
|
||||
|
||||
Slim mode reuses the on-chain RLN programs at the shipped `TREE_ID` (committed in `vendor/logos-lez-rln/testnet/`) — no fresh deploy. The shipped payment account holds 1B RLNTOK (~1000 registrations) drawn from a 10B supply, refilled by re-running `vendor/logos-lez-rln/lez-rln/target/debug/run_setup` against testnet when needed.
|
||||
|
||||
`SIM_DELIVERY_TIMEOUT=1800` (30 min) is required: the gifter now serializes registrations through a single-writer worker that awaits chain confirmation between submissions to avoid per-signer nonce collisions. With testnet's ~60-90 s block cadence and up to ~6 jobs ahead in the queue, the chat sender (last in line) needs the longer delivery window. The default 300 s value would expire before its registration confirms.
|
||||
|
||||
If a run still fails after a clean retry: the most likely cause is a stale `~/.logos-lez-rln/payment_account_<TREE_ID>.txt` cache from a previous shipped TREE_ID. Wipe with:
|
||||
|
||||
```bash
|
||||
rm -f ~/.logos-lez-rln/payment_account_*.txt \
|
||||
~/.logos-lez-rln/supply_holding_*.txt \
|
||||
vendor/logos-lez-rln/testnet/storage.json \
|
||||
vendor/logos-lez-rln/testnet/wallet_config.json
|
||||
```
|
||||
|
||||
and re-run — `seed_copy` will then re-seed from the shipped sidecars.
|
||||
|
||||
If you haven't built the modules on this machine before, run `bash simulations/mix_lez_chat/setup_and_run.sh` once first to build the dylibs.
|
||||
|
||||
Background on the bug class this used to hit: `cleanup/MODE_A_GIFTER_SLOT_BUG.md`.
|
||||
190
cleanup/MODE_A_GIFTER_SLOT_BUG.md
Normal file
190
cleanup/MODE_A_GIFTER_SLOT_BUG.md
Normal file
@ -0,0 +1,190 @@
|
||||
# Mode A — per-signer nonce collision in the gifter's wallet submission path
|
||||
|
||||
**Status:** Open. Root cause identified and reproduced locally on 2026-05-27. Fix sits in the LEZ wallet (`lssa/wallet/`), not in the gifter, not in the chat sender, not in the on-chain `Register` handler.
|
||||
|
||||
**Captured evidence:**
|
||||
- Local reproduction (this session): `/tmp/sim_state_local_NONCE_REPRO/` — full end-to-end repro with `sequencer.log` containing 4 `"Nonce mismatch"` rejections.
|
||||
- Testnet failures: `/tmp/sim_state_testnet_postfix/`, `/tmp/sim_state_cleanwallet/`.
|
||||
|
||||
## TL;DR
|
||||
|
||||
When the gifter fires several `register_member` calls within a single sequencer block window, **all of them fetch the same chain-side nonce N**, sign with N, and submit. The first commits and the signer's nonce advances to N+1; the remaining 2–4 fail `validate_on_state` with `"Nonce mismatch"` and are silently dropped at the sequencer (logged but not returned to the caller). The wallet has no per-signer nonce serialization, the mempool has no dedup, and `get_transaction(tx_hash)` cannot distinguish "rejected" from "still pending."
|
||||
|
||||
Consequence: `tree_main.next_index` advances by ~1 per block window instead of by the number of submissions. Every requester's `register_member` keeps reading the same stale `next_index` (because the chain genuinely hasn't moved past it) and keeps returning the same optimistic `leaf_index`. Each client's gifter-status watcher polls `is_member_registered`, which keeps returning `false` (the chain never wrote their PDA). The chat-sender's 180 s confirmation deadline expires, it publishes against the optimistic-but-incorrect leaf, the rln crate computes a proof root from `(pathElements_for_someone_else, our_creds)`, and self-verify rejects with `rootInOurWindow=false`.
|
||||
|
||||
The duplicate `leaf=6` / `leaf=178` readings in earlier captures are **symptoms** of zero registrations committing, not evidence of a gifter slot-allocator defect. The on-chain Register handler reads `tree_main.next_index` from live state and serializes correctly when txns commit — confirmed by sequencer-core re-execution model (`sequencer/core/src/lib.rs:243-254`).
|
||||
|
||||
## Reproduction (local, deterministic)
|
||||
|
||||
The local sim's default config (`vendor/logos-lez-rln/lssa/sequencer/service/configs/debug/sequencer_config.json`: `max_num_tx_in_block=20`, `block_create_timeout="15s"`) **masks** the bug because all concurrent registrations pack into a single block — they get distinct nonces from `get_accounts_nonces` between blocks and each commits at the right slot.
|
||||
|
||||
To expose the race, widen the block window past the natural registration cadence:
|
||||
|
||||
```jsonc
|
||||
"max_num_tx_in_block": 1, // force one tx per block
|
||||
"block_create_timeout": "90s" // longer than the ~25-30s gap between mix-node registrations
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
SIM_NETWORK=local ./simulations/mix_lez_chat/run_simulation.sh --fresh
|
||||
grep -E "Nonce mismatch" simulations/mix_lez_chat/.sim_state/sequencer.log
|
||||
```
|
||||
|
||||
This is a diagnostic-only change. **Do not commit it.**
|
||||
|
||||
## Evidence
|
||||
|
||||
### 1. Local reproduction (this session, 2026-05-27 17:48 UTC)
|
||||
|
||||
`sequencer.log`:
|
||||
```
|
||||
[17:51:00 ERROR sequencer_core] Transaction with hash 6b69eb67… failed execution check with error: InvalidInput("Nonce mismatch"), skipping it
|
||||
[17:51:00 ERROR sequencer_core] Transaction with hash 17d209c5… failed execution check with error: InvalidInput("Nonce mismatch"), skipping it
|
||||
[17:51:00 ERROR sequencer_core] Transaction with hash c33c7543… failed execution check with error: InvalidInput("Nonce mismatch"), skipping it
|
||||
[17:52:30 ERROR sequencer_core] Transaction with hash 12e9bcc7… failed execution check with error: InvalidInput("Nonce mismatch"), skipping it
|
||||
```
|
||||
|
||||
`node0.log` (gifter) — leaf returned per request:
|
||||
```
|
||||
11:48:11 Gifter self-registered leafIndex=0
|
||||
11:48:33 RLN gifter registration succeeded leafIndex=0 requestId=cd6dd33…
|
||||
11:48:57 RLN gifter registration succeeded leafIndex=0 requestId=40b442b…
|
||||
11:49:22 RLN gifter registration succeeded leafIndex=0 requestId=0740625…
|
||||
11:50:20 RLN gifter registration succeeded leafIndex=1 requestId=ea051b2… ← block window rolled
|
||||
11:50:54 RLN gifter registration succeeded leafIndex=1 requestId=cf37418…
|
||||
```
|
||||
|
||||
Four requesters got `leaf=0`, then the next block let exactly one tx commit (advancing to leaf=1), and the next two requesters again collided on leaf=1. End-to-end the chat sender's failure was the canonical Mode A:
|
||||
|
||||
`chat_sender.log`:
|
||||
```
|
||||
11:50:54 RLN membership granted leafIndex=1
|
||||
11:54:05 WRN Membership confirmation did not arrive within deadline
|
||||
11:54:25 ERR Self-verify of generated proof errored
|
||||
err="Verification error: Expected one of the provided roots"
|
||||
proofRoot=28c9607887077a3c… rootInOurWindow=false
|
||||
11:54:40 ERR Failed to publish via mix
|
||||
err="…mix send failed: Failed to generate spam protection proof…"
|
||||
```
|
||||
|
||||
Tally: 1 FAILED, 14 passed. Identical signature to the testnet captures.
|
||||
|
||||
### 2. Pre-clean-wallet testnet failure (`/tmp/sim_state_testnet_postfix/`)
|
||||
|
||||
Five requesters all got `leafIndex=178`; 6 unrelated `KeyNotFoundError` lines in node0 from a stale `~/.logos-lez-rln/payment_account_*.txt` (separate environmental bug — sidecar staleness, see "Environmental footguns" below). Even when that was fixed, the slot-collision pattern persisted.
|
||||
|
||||
### 3. Post-clean-wallet testnet failure (`/tmp/sim_state_cleanwallet/`)
|
||||
|
||||
Five requesters all got `leafIndex=6`; zero `KeyNotFoundError`; same `Self-verify ... rootInOurWindow=false` self-verify rejection on the chat sender; same 180 s confirmation timeout.
|
||||
|
||||
## Code path
|
||||
|
||||
### Wallet — refetches nonce every call, no cache
|
||||
|
||||
`vendor/logos-lez-rln/lssa/wallet/src/lib.rs:294-326` — `send_public_transaction`:
|
||||
|
||||
```rust
|
||||
// line 301-304
|
||||
let nonces = self.sequencer_client.get_accounts_nonces(vec![signer]).await?;
|
||||
let signer_nonce = nonces.get(&signer).copied().unwrap_or(0);
|
||||
```
|
||||
|
||||
Nonce is fetched fresh from the sequencer on every call. No local cache, no auto-increment, no awareness of in-flight submissions.
|
||||
|
||||
### Mempool — no per-signer dedup
|
||||
|
||||
`vendor/logos-lez-rln/lssa/mempool/src/lib.rs:1-61` — plain async queue. `send_transaction` does a stateless signature check (line 67), then pushes into the FIFO buffer. Two txns from the same signer with the same nonce both accepted into the mempool.
|
||||
|
||||
### Sequencer — silent drop with logged-only feedback
|
||||
|
||||
`vendor/logos-lez-rln/lssa/nssa/src/validated_state_diff.rs:73-78` (public tx) and `:340-344` (privacy-preserving) — `validate_on_state` enforces `current_nonce == *nonce`; mismatch returns `Err(InvalidInput("Nonce mismatch"))`.
|
||||
|
||||
`vendor/logos-lez-rln/lssa/sequencer/core/src/lib.rs:243-254` — on validation error during block building, sequencer logs `"Transaction with hash {tx_hash} failed execution check with error: ..., skipping it"` and silently continues to the next mempool entry. The rejected tx is consumed from the mempool; **no notification flows back to the submitter.**
|
||||
|
||||
### Status polling — cannot distinguish dropped from pending
|
||||
|
||||
`vendor/logos-lez-rln/lssa/wallet/src/poller.rs:33-64` — `get_transaction(tx_hash)` returns `Ok(tx)` only if the tx is found in a committed block. Otherwise after polling timeout: `bail!("Transaction not found")`. A nonce-rejected tx and a still-pending tx are indistinguishable from the client's perspective.
|
||||
|
||||
### Gifter — submit-and-return-optimistic
|
||||
|
||||
`vendor/logos-lez-rln/logos-rln-module/src/logos_rln_module.cpp:316-486` — `register_member`:
|
||||
|
||||
1. Read `tree_main` (line 367-371).
|
||||
2. `rln_ffi_register_plan` → `plan.next_leaf_index` (line 376-388). This is `tree_main.next_index` at read time.
|
||||
3. Build instruction (line 425-437). The instruction itself carries only `tree_id, id_commitment, rate_limit, subtree_id` — **no `leaf_index`**; the on-chain handler derives it from live state.
|
||||
4. Submit via wallet — fire-and-forget (line 462-469).
|
||||
5. Return `plan.next_leaf_index` to caller, with `pending: true`.
|
||||
|
||||
The in-line comment at line 471-484 is candid that `plan.next_leaf_index` is "a pre-submit snapshot — it can be wrong if our tx loses a race." What that comment did not anticipate is that the more common failure mode is the tx not committing at all (silent nonce drop), not the tx committing at a different slot.
|
||||
|
||||
### Note on the on-chain side (where there is **not** a bug)
|
||||
|
||||
The Register handler reads `tree_main.next_index` from live state and assigns a leaf at execution time. The sequencer re-executes each public tx serially against the current state (`sequencer/core/src/lib.rs:243-254`). When two registrations commit sequentially, they get distinct leaves automatically — no program-level CAS is needed.
|
||||
|
||||
There is a narrow latent correctness hole at subtree boundaries: `plan.subtree_account_id` is part of the tx's account list and is derived from the planned leaf, so if a registration is retried after the chain has crossed a subtree boundary, the account list points at the wrong subtree account and the tx will fail. This is a separate, lower-priority concern from the nonce bug — flagged here for follow-up but **not** the cause of the current Mode A failures.
|
||||
|
||||
## Why the existing mitigations don't close it
|
||||
|
||||
| Layer | Mitigation | Why it's insufficient |
|
||||
|---|---|---|
|
||||
| Chat sender — Phase 1 cushion | Wait `2 × pollInterval` after `markMembershipConfirmed()` | If `is_member_registered` never returns true (because the tx was nonce-dropped, never committed), the 180 s deadline expires and sender publishes anyway. |
|
||||
| Chat sender — Phase 2 root-stability gate | Wait until `proofRoot()` is in rootTracker for `stableMs` | Tracks `cachedProof.root` for our optimistic `membershipIndex`. If that index belongs to someone else's commitment (or to no one — slot empty), `cachedProof` is still set to *some* root and Phase 2 passes. |
|
||||
| Watcher background poll | Poll `is_member_registered` every 30 s, fire `onConfirmed` | Useless when the chain never wrote our PDA because our submitting tx was nonce-dropped. |
|
||||
| `register_member` idempotency precheck | Skip resubmit if PDA already populated | Only handles **re-registration**, not first registration. |
|
||||
| `Self-verify` in `spam_protection.generateProof` | Reject the proof locally when `rootInOurWindow=false` | Catches the symptom (we shipped this earlier in the session and it correctly fails fast). Doesn't recover the send. |
|
||||
| Visibility fixes shipped this session | Surface `mix send failed` in chat sender logs | Turns a silent 14/15 into a visible 14/15. Doesn't change the failure rate. |
|
||||
|
||||
All these mitigations assumed a benign "leaf was reassigned" race that the watcher would clean up. The actual mechanism is "the tx never committed in the first place," which renders each mitigation a no-op.
|
||||
|
||||
## Recommended fixes — in priority order, in the right layer
|
||||
|
||||
### A. Wallet-side per-signer nonce serialization (smallest correct fix)
|
||||
|
||||
`vendor/logos-lez-rln/lssa/wallet/src/lib.rs:294-326` — replace the bare `get_accounts_nonces` refetch with:
|
||||
|
||||
1. Maintain a per-signer `nextNonce: Map<Signer, u64>` in wallet state.
|
||||
2. On `send_public_transaction`: `let nonce = max(chain_nonce_for_signer, nextNonce[signer]); nextNonce[signer] = nonce + 1`.
|
||||
3. After tx confirmation (success or failure): reconcile `nextNonce[signer]` against the chain's authoritative nonce — on rejection, decrement and let the caller retry; on commit, advance only if needed.
|
||||
|
||||
Trade-off: wallet becomes stateful. On restart it can rebuild `nextNonce` from chain by re-fetching once per signer. Lost in-flight txns become rejections that the caller has to retry — which requires (B).
|
||||
|
||||
### B. Surface tx-status distinguishably
|
||||
|
||||
`vendor/logos-lez-rln/lssa/wallet/src/poller.rs` + an additive sequencer RPC: have `get_transaction(tx_hash)` return one of `{committed(block), pending, rejected(reason)}`. The sequencer already logs the rejection reason at `sequencer/core/src/lib.rs:243-254` — that information just needs to flow back instead of being log-only. Without this, even a wallet that knows it should retry has no signal to act on.
|
||||
|
||||
### C. Gifter retry on nonce rejection (after A+B land)
|
||||
|
||||
Once the wallet can detect a rejected submission, the gifter's `register_member` can re-submit transparently: refetch the chain nonce, rebuild the instruction (the `plan.next_leaf_index` will have advanced), and retry. Wraps the existing fire-and-forget into a confirm-or-retry loop. Keeps the client-side optimistic flow simple.
|
||||
|
||||
### Strike-through: gifter-side optimistic counter
|
||||
|
||||
An earlier draft of this doc proposed a gifter in-process `Map<id_commitment, leaf_index>` counter to hand out distinct optimistic leaves. **This was wrong.** It would print nicer-looking leaf numbers while the underlying tx submissions continue to silently drop. The chain wouldn't advance any faster, `is_member_registered` would still return `false`, and Mode A would persist. The fix has to address the actual submission failure, not the cosmetic returned value.
|
||||
|
||||
## Environmental footguns hit during this investigation
|
||||
|
||||
Documented for the next session — not related to the nonce bug but cost a couple of failed runs to diagnose:
|
||||
|
||||
1. **Stale `~/.logos-lez-rln/payment_account_<TREE_ID>.txt`** caused `KeyNotFoundError` on every gifter `send_public_transaction` against testnet. The sim's `seed_copy` (`simulations/mix_lez_chat/run_simulation.sh:226-247`) has a `[ -f "$dst" ] && return 0` guard, so once a stale sidecar is cached it never gets refreshed. Workaround: `rm ~/.logos-lez-rln/payment_account_*.txt ~/.logos-lez-rln/supply_holding_*.txt vendor/logos-lez-rln/testnet/storage.json vendor/logos-lez-rln/testnet/wallet_config.json` before re-running. Cleaner fix: change the guard to refresh when the shipped source is newer than the cached destination.
|
||||
|
||||
2. **Stale dylib path mismatch** between loose `vendor/logos-lez-rln/logos-delivery/build/` and canonical submodule path `vendor/logos-lez-rln/logos-delivery-module/vendor/logos-delivery/build/`. Documented in `cleanup/FRESH_CLONE_RESULTS.md`'s caveat section.
|
||||
|
||||
## Open questions / follow-ups
|
||||
|
||||
1. **Confirm against hosted testnet.** The local repro mechanism is identical to what we'd see on testnet (same wallet + sequencer code path). But because we cannot reach the hosted testnet's sequencer logs, we can't directly observe the `"Nonce mismatch"` lines there. One way to close the loop: add a temporary log line in the gifter's `register_member` that records the tx_hash returned by `send_public_transaction`, then add a follow-up `get_transaction(tx_hash)` poll right after submission to detect commit-vs-not. If testnet runs show that none of the gifter's tx_hashes ever appear committed, the nonce hypothesis is corroborated.
|
||||
|
||||
2. **Subtree-boundary retry-time hole.** Mentioned above — the planned `subtree_account_id` is leaf-dependent. A retry after the tree has grown past a subtree boundary will submit a tx whose account list points at the wrong subtree. Independent of the nonce bug, but worth catching before the LEZ user count grows past the first few subtree boundaries.
|
||||
|
||||
3. **Per-signer mempool ordering.** Even after wallet-side nonce serialization, two `register_member` calls submitting from the same signer back-to-back may land in the mempool in arbitrary order if the wallet doesn't preserve submission order. The sequencer drains mempool FIFO, so out-of-order arrivals with sequentially-advanced nonces would all fail validation. A wallet fix needs to either (a) preserve order on submission or (b) batch into a single transaction.
|
||||
|
||||
## What this session shipped
|
||||
|
||||
The following commits are independent of the nonce bug — they make Mode A *visible* instead of silent, which is what enabled this investigation. Worth keeping regardless of how/when the wallet fix lands:
|
||||
|
||||
- `692a467` `fix(chat): surface sendBytes errors instead of swallowing via discard`
|
||||
- `d36cee09` `fix(lightpush): surface mix-dialer write failures as Result error` (in `vendor/nwaku`)
|
||||
- `a555e5f` `chore: bump nwaku to surface mix-dialer write failures`
|
||||
- `49dbb22` `fix(chat): timeout mix lightpush so vanished-reply hangs surface as Err`
|
||||
|
||||
Without these, the local repro would have hung or silently passed 14/15 with no explanatory error line, and the testnet captures would not have surfaced the `Self-verify ... rootInOurWindow=false` pattern that pointed us at the right layer.
|
||||
@ -50,7 +50,22 @@ proc createChatClient(
|
||||
wakuCfg.staticPeers = @[]
|
||||
for peer in config["staticPeers"]:
|
||||
wakuCfg.staticPeers.add(peer.getStr())
|
||||
|
||||
|
||||
if config.hasKey("mixEnabled"):
|
||||
wakuCfg.mixEnabled = config["mixEnabled"].getBool(false)
|
||||
if config.hasKey("mixNodes"):
|
||||
wakuCfg.mixNodes = @[]
|
||||
for node in config["mixNodes"]:
|
||||
wakuCfg.mixNodes.add(node.getStr())
|
||||
if config.hasKey("destPeerAddr"):
|
||||
wakuCfg.destPeerAddr = config["destPeerAddr"].getStr("")
|
||||
if config.hasKey("minMixPoolSize"):
|
||||
wakuCfg.minMixPoolSize = config["minMixPoolSize"].getInt(4)
|
||||
if config.hasKey("gifterNodeAddr"):
|
||||
wakuCfg.gifterNodeAddr = config["gifterNodeAddr"].getStr("")
|
||||
if config.hasKey("gifterAuthKey"):
|
||||
wakuCfg.gifterAuthKey = config["gifterAuthKey"].getStr("")
|
||||
|
||||
# Create Waku client
|
||||
let wakuClient = initWakuClient(wakuCfg)
|
||||
|
||||
@ -117,6 +132,28 @@ proc chat_get_id(
|
||||
let clientId = ctx.myLib[].getId()
|
||||
return ok(clientId)
|
||||
|
||||
#################################################
|
||||
# Mix Protocol Status
|
||||
#################################################
|
||||
|
||||
proc chat_get_mix_status(
|
||||
ctx: ptr FFIContext[ChatClient],
|
||||
callback: FFICallBack,
|
||||
userData: pointer
|
||||
) {.ffi.} =
|
||||
let client = ctx.myLib[]
|
||||
let mixEnabled = client.ds.cfg.mixEnabled
|
||||
var poolSize = 0
|
||||
if mixEnabled:
|
||||
poolSize = client.ds.getMixPoolSize()
|
||||
let status = %*{
|
||||
"mixEnabled": mixEnabled,
|
||||
"mixReady": client.ds.mixReady,
|
||||
"mixPoolSize": poolSize,
|
||||
"minPoolSize": client.ds.cfg.minMixPoolSize
|
||||
}
|
||||
return ok($status)
|
||||
|
||||
#################################################
|
||||
# Conversation List Operations
|
||||
#################################################
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import ffi
|
||||
import src/chat/client
|
||||
import waku/waku_mix/logos_core_client as mix_rln_client
|
||||
|
||||
declareLibrary("logoschat")
|
||||
|
||||
@ -11,3 +12,40 @@ proc set_event_callback(
|
||||
ctx[].eventCallback = cast[pointer](callback)
|
||||
ctx[].eventUserData = userData
|
||||
|
||||
proc chat_set_rln_fetcher(
|
||||
ctx: ptr FFIContext[ChatClient], fetcher: mix_rln_client.RlnFetcherFunc, fetcherData: pointer
|
||||
) {.dynlib, exportc, cdecl.} =
|
||||
if fetcher.isNil:
|
||||
echo "error: nil fetcher in chat_set_rln_fetcher"
|
||||
return
|
||||
mix_rln_client.setRlnFetcher(fetcher, fetcherData)
|
||||
|
||||
proc chat_set_rln_config(
|
||||
ctx: ptr FFIContext[ChatClient], configAccountId: cstring, leafIndex: cint
|
||||
): cint {.dynlib, exportc, cdecl.} =
|
||||
if configAccountId.isNil:
|
||||
return RET_ERR
|
||||
mix_rln_client.setRlnConfig($configAccountId, leafIndex.int)
|
||||
return RET_OK
|
||||
|
||||
proc chat_set_rln_identity(
|
||||
ctx: ptr FFIContext[ChatClient], idSecretHashHex: cstring
|
||||
) {.dynlib, exportc, cdecl.} =
|
||||
if idSecretHashHex.isNil:
|
||||
return
|
||||
mix_rln_client.setRlnIdentity($idSecretHashHex)
|
||||
|
||||
proc chat_push_roots(
|
||||
ctx: ptr FFIContext[ChatClient], rootsJson: cstring
|
||||
) {.dynlib, exportc, cdecl.} =
|
||||
if rootsJson.isNil:
|
||||
return
|
||||
mix_rln_client.pushRoots($rootsJson)
|
||||
|
||||
proc chat_push_proof(
|
||||
ctx: ptr FFIContext[ChatClient], proofJson: cstring
|
||||
) {.dynlib, exportc, cdecl.} =
|
||||
if proofJson.isNil:
|
||||
return
|
||||
mix_rln_client.pushProof($proofJson)
|
||||
|
||||
|
||||
@ -30,6 +30,10 @@ typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len,
|
||||
// - "clusterId": int - Waku cluster ID (optional)
|
||||
// - "shardId": int - Waku shard ID (optional)
|
||||
// - "staticPeers": array of strings - static peer multiaddrs (optional)
|
||||
// - "mixEnabled": bool - enable mix protocol for sender anonymity (default: false)
|
||||
// - "mixNodes": array of strings - mix bootstrap nodes as "multiaddr:mixPubKeyHex"
|
||||
// - "destPeerAddr": string - lightpush destination peer "multiaddr/p2p/peerId"
|
||||
// - "minMixPoolSize": int - minimum mix pool size before sending via mix (default: 4)
|
||||
void *chat_new(const char *configJson, FFICallBack callback, void *userData);
|
||||
|
||||
// Start the chat client and begin listening for messages
|
||||
@ -97,6 +101,38 @@ int chat_get_identity(void *ctx, FFICallBack callback, void *userData);
|
||||
// Returns the intro bundle as an ASCII string (format: logos_chatintro_<version>_<base64url payload>)
|
||||
int chat_create_intro_bundle(void *ctx, FFICallBack callback, void *userData);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Mix Protocol Status
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Get mix protocol status
|
||||
// Returns JSON: {"mixEnabled":bool,"mixReady":bool,"mixPoolSize":int,"minPoolSize":int}
|
||||
int chat_get_mix_status(void *ctx, FFICallBack callback, void *userData);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// RLN Integration (for logos-core module wiring)
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// RLN fetcher callback type: C++ implements this, Nim calls it to get
|
||||
// roots/proofs from the RLN module via LogosAPI IPC.
|
||||
typedef int (*RlnFetcherFunc)(const char *method, const char *params,
|
||||
FFICallBack callback, void *callbackData, void *fetcherData);
|
||||
|
||||
// Register the RLN fetcher callback (called by C++ plugin during init)
|
||||
void chat_set_rln_fetcher(void *ctx, RlnFetcherFunc fetcher, void *fetcherData);
|
||||
|
||||
// Set RLN configuration (account ID + leaf index in the on-chain tree)
|
||||
int chat_set_rln_config(void *ctx, const char *configAccountId, int leafIndex);
|
||||
|
||||
// Set RLN identity credential (seed hex for proof generation)
|
||||
void chat_set_rln_identity(void *ctx, const char *idSecretHashHex);
|
||||
|
||||
// Push valid merkle roots from RLN module events
|
||||
void chat_push_roots(void *ctx, const char *rootsJson);
|
||||
|
||||
// Push merkle proof from RLN module events
|
||||
void chat_push_proof(void *ctx, const char *proofJson);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{ lib, stdenv, nim, which, pkg-config, writeScriptBin,
|
||||
openssl, miniupnpc, libnatpmp,
|
||||
openssl, miniupnpc, libnatpmp, rustPlatform, fetchFromGitHub, darwin ? {},
|
||||
src, # logos-chat source (self from flake, with submodules=1)
|
||||
rustBundleDrv }: # result of rust_bundle.nix
|
||||
|
||||
@ -13,20 +13,50 @@ assert lib.assertMsg ((src.submodules or false) == true)
|
||||
|
||||
let
|
||||
revision = lib.substring 0 8 (src.rev or "dirty");
|
||||
logosChatSrc = src;
|
||||
|
||||
zerokitMixSrc = fetchFromGitHub {
|
||||
owner = "vacp2p";
|
||||
repo = "zerokit";
|
||||
rev = "v2.0.0";
|
||||
hash = "sha256-5a2cL26uw7NdLCD0gCA3tS7uX8W9yxRGqcPhWNevstM=";
|
||||
};
|
||||
|
||||
mixRlnLib = rustPlatform.buildRustPackage {
|
||||
pname = "librln-mix";
|
||||
version = "2.0.0";
|
||||
src = zerokitMixSrc;
|
||||
cargoHash = "sha256-SoMl0QBBgTG1b4UhOlErlzWmg3J6G0xOC0tNDddOptA=";
|
||||
buildAndTestSubdir = "rln";
|
||||
buildType = "release";
|
||||
nativeBuildInputs = lib.optionals stdenv.isDarwin [ darwin.cctools ];
|
||||
doCheck = false;
|
||||
installPhase = ''
|
||||
mkdir -p $out/lib
|
||||
find target -name "librln.a" -exec cp {} $out/lib/librln_mix_v2.0.0.a \;
|
||||
ls $out/lib/librln_mix_v2.0.0.a
|
||||
'' + lib.optionalString stdenv.isDarwin ''
|
||||
bash ${logosChatSrc}/scripts/fix_mix_librln_dupes.sh $out/lib/librln_mix_v2.0.0.a ${rustBundleDrv}/lib/liblogoschat_rust_bundle.a
|
||||
'';
|
||||
};
|
||||
in stdenv.mkDerivation {
|
||||
pname = "liblogoschat";
|
||||
version = "0.1.0";
|
||||
inherit src;
|
||||
|
||||
NIMFLAGS = lib.concatStringsSep " " [
|
||||
NIMFLAGS = lib.concatStringsSep " " ([
|
||||
"--passL:${rustBundleDrv}/lib/liblogoschat_rust_bundle.a"
|
||||
"--passL:${mixRlnLib}/lib/librln_mix_v2.0.0.a"
|
||||
"--passL:-lm"
|
||||
"-d:miniupnpcUseSystemLibs"
|
||||
"-d:libnatpmpUseSystemLibs"
|
||||
"--passL:-lminiupnpc"
|
||||
"--passL:-lnatpmp"
|
||||
"-d:git_version=${revision}"
|
||||
];
|
||||
"-d:libp2p_mix_experimental_exit_is_dest"
|
||||
] ++ lib.optionals stdenv.isLinux [
|
||||
"--passL:-Wl,--allow-multiple-definition"
|
||||
]);
|
||||
|
||||
nativeBuildInputs = let
|
||||
fakeGit = writeScriptBin "git" ''
|
||||
|
||||
4052
rust-bundle/Cargo.lock
generated
4052
rust-bundle/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -9,4 +9,4 @@ crate-type = ["staticlib"]
|
||||
|
||||
[dependencies]
|
||||
libchat = { path = "../vendor/libchat/conversations" }
|
||||
rln = { path = "../vendor/nwaku/vendor/zerokit/rln", features = ["arkzkey"] }
|
||||
rln = { path = "../vendor/nwaku/vendor/zerokit/rln" }
|
||||
|
||||
41
scripts/fix_mix_librln_dupes.sh
Executable file
41
scripts/fix_mix_librln_dupes.sh
Executable file
@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fix duplicate symbols between librln_mix and rust-bundle on macOS.
|
||||
set -uo pipefail
|
||||
|
||||
LIB="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
|
||||
RUST_BUNDLE="${2:-}"
|
||||
[ -n "$RUST_BUNDLE" ] && RUST_BUNDLE="$(cd "$(dirname "$RUST_BUNDLE")" && pwd)/$(basename "$RUST_BUNDLE")"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
[ -z "$RUST_BUNDLE" ] && echo "Usage: $0 <mix-lib> <rust-bundle-lib>" && exit 0
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# Match all global symbols (T=text, D=data, S=common, B=BSS, etc — uppercase = global)
|
||||
(nm "$RUST_BUNDLE" 2>/dev/null || true) | grep " [TDSBCR] " | awk '{print $3}' | sort -u > "$WORK/b.txt"
|
||||
(nm "$LIB" 2>/dev/null || true) | grep " [TDSBCR] " | awk '{print $3}' | sort -u > "$WORK/m.txt"
|
||||
comm -12 "$WORK/b.txt" "$WORK/m.txt" > "$WORK/d.txt"
|
||||
DCOUNT=$(wc -l < "$WORK/d.txt" | tr -d ' ')
|
||||
[ "$DCOUNT" -eq 0 ] && echo "No duplicates." && exit 0
|
||||
echo "Localizing $DCOUNT duplicate symbols in $(basename "$LIB")..."
|
||||
|
||||
mkdir "$WORK/o" && cd "$WORK/o"
|
||||
ar x "$LIB"
|
||||
FIXED=0
|
||||
for f in *.o; do
|
||||
# Get this object's global text symbols, intersect with dupes
|
||||
(nm "$f" 2>/dev/null || true) | grep " [TDSBCR] " | awk '{print $3}' | sort -u > "$WORK/obj.txt"
|
||||
comm -12 "$WORK/d.txt" "$WORK/obj.txt" > "$WORK/obj_dupes.txt"
|
||||
if [ -s "$WORK/obj_dupes.txt" ]; then
|
||||
nmedit -R "$WORK/obj_dupes.txt" "$f"
|
||||
FIXED=$((FIXED + 1))
|
||||
fi
|
||||
done
|
||||
rm "$LIB"
|
||||
ar rcs "$LIB" *.o
|
||||
echo "Fixed $FIXED objects."
|
||||
;;
|
||||
*) echo "No fix needed on $(uname -s)" ;;
|
||||
esac
|
||||
127
scripts/run_in_docker.sh
Executable file
127
scripts/run_in_docker.sh
Executable file
@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the mix+LEZ chat simulation inside a Docker container (Linux).
|
||||
#
|
||||
# The Docker image pre-builds all heavy nix modules. Each sim run clones
|
||||
# the repo, builds sequencer + run_setup from source (nix-shell), symlinks
|
||||
# pre-built artifacts, and runs the simulation.
|
||||
#
|
||||
# First run: ~60 min (one-time docker build)
|
||||
# Subsequent runs: ~10 min (clone + sequencer build + sim)
|
||||
#
|
||||
# Prerequisites: Docker Desktop running.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/run_in_docker.sh
|
||||
# BRANCH=my-branch bash scripts/run_in_docker.sh
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DOCKERFILE="$ROOT/.github/Dockerfile.sim"
|
||||
IMAGE_NAME="${DOCKER_IMAGE:-ghcr.io/adklempner/logos-chat-sim:latest}"
|
||||
CONTAINER_NAME="logos-chat-sim-run"
|
||||
BRANCH="${BRANCH:-feat/logos-delivery}"
|
||||
REPO_URL="${REPO_URL:-https://github.com/adklempner/logos-chat.git}"
|
||||
|
||||
# Build image if it doesn't exist or REBUILD_IMAGE=1
|
||||
if [ "${REBUILD_IMAGE:-0}" = "1" ]; then
|
||||
echo "=== Building Docker image locally (~60 min) ==="
|
||||
docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" "$ROOT"
|
||||
elif ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then
|
||||
echo "=== Pulling Docker image from GHCR ==="
|
||||
docker pull "$IMAGE_NAME" || {
|
||||
echo "Pull failed, building locally..."
|
||||
docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" "$ROOT"
|
||||
}
|
||||
else
|
||||
echo "=== Docker image cached ==="
|
||||
fi
|
||||
|
||||
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
||||
trap 'echo "=== Rescuing logs ==="; docker cp "$CONTAINER_NAME:/root/logos-chat/simulations/mix_lez_chat/.sim_state" ./docker-sim-logs 2>/dev/null || true; docker rm -f "$CONTAINER_NAME" 2>/dev/null || true' EXIT
|
||||
|
||||
echo "=== Starting container ==="
|
||||
docker run -d --name "$CONTAINER_NAME" "$IMAGE_NAME" tail -f /dev/null
|
||||
|
||||
# Stage guest binaries
|
||||
GUEST_REL="vendor/logos-lez-rln/lez-rln/methods/guest/target/riscv32im-risc0-zkvm-elf/docker"
|
||||
GUEST_SRC=""
|
||||
for candidate in \
|
||||
"${GUEST_BINARIES_DIR:-}" \
|
||||
"$ROOT/$GUEST_REL" \
|
||||
"$HOME/Waku/Logos/logos-chat/$GUEST_REL" \
|
||||
"../logos-chat/$GUEST_REL"; do
|
||||
[ -f "$candidate/rln_registration.bin" ] 2>/dev/null && GUEST_SRC="$candidate" && break
|
||||
done
|
||||
if [ -n "$GUEST_SRC" ]; then
|
||||
echo "=== Staging guest binaries ==="
|
||||
docker exec "$CONTAINER_NAME" mkdir -p "/tmp/guest-bins"
|
||||
docker cp "$GUEST_SRC/rln_registration.bin" "$CONTAINER_NAME:/tmp/guest-bins/"
|
||||
docker cp "$GUEST_SRC/incremental_merkle_tree.bin" "$CONTAINER_NAME:/tmp/guest-bins/"
|
||||
fi
|
||||
|
||||
# Collect SIM_* env vars
|
||||
SIM_ENVS=""
|
||||
for var in $(env | grep '^SIM_' | cut -d= -f1); do
|
||||
SIM_ENVS="${SIM_ENVS}export $var='${!var}'; "
|
||||
done
|
||||
|
||||
# Write the run script to the container (avoids heredoc escaping issues)
|
||||
docker exec -i "$CONTAINER_NAME" bash -c "cat > /root/run-sim.sh && chmod +x /root/run-sim.sh" << 'SIMSCRIPT'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
export RISC0_DEV_MODE=1
|
||||
|
||||
# Clone repo
|
||||
cd /root
|
||||
rm -rf logos-chat
|
||||
git clone --depth 1 -b "$BRANCH" "$REPO_URL"
|
||||
cd logos-chat
|
||||
|
||||
# Init submodules (lssa needs full history for auto-sync)
|
||||
git submodule update --init --depth 1
|
||||
(cd vendor/logos-lez-rln && git submodule update --init lssa && \
|
||||
git submodule update --init --depth 1 logos-delivery logos-delivery-module logos-execution-zone-module && \
|
||||
git checkout -- . && \
|
||||
for d in lssa logos-delivery logos-delivery-module logos-execution-zone-module; do \
|
||||
(cd "$d" && git checkout -- .); \
|
||||
done)
|
||||
(cd vendor/logos-lez-rln/logos-delivery-module && git submodule update --init --depth 1 vendor/logos-delivery)
|
||||
|
||||
# Symlink pre-built nix modules from image
|
||||
LEZ_DIR=vendor/logos-lez-rln
|
||||
ln -sf /root/lez-modules/result-rln $LEZ_DIR/logos-rln-module/result-rln
|
||||
ln -sf /root/lez-modules/result-wallet $LEZ_DIR/logos-rln-module/result-wallet
|
||||
mkdir -p $LEZ_DIR/logos-delivery-module/build_plugin
|
||||
cp -r /root/lez-modules/delivery-plugin $LEZ_DIR/logos-delivery-module/build_plugin/modules
|
||||
mkdir -p $LEZ_DIR/logos-delivery-module/vendor/logos-delivery/build
|
||||
cp /root/lez-modules/delivery-build/* $LEZ_DIR/logos-delivery-module/vendor/logos-delivery/build/ 2>/dev/null || true
|
||||
mkdir -p build
|
||||
cp /root/lez-modules/liblogoschat.so build/
|
||||
|
||||
# Restore guest binaries
|
||||
GUEST_DIR="$LEZ_DIR/lez-rln/methods/guest/target/riscv32im-risc0-zkvm-elf/docker"
|
||||
if [ -f /tmp/guest-bins/rln_registration.bin ]; then
|
||||
mkdir -p "$GUEST_DIR"
|
||||
cp /tmp/guest-bins/*.bin "$GUEST_DIR/"
|
||||
fi
|
||||
|
||||
# Chat module
|
||||
mkdir -p /root/logos-chat-module
|
||||
ln -sf /root/lez-modules/chat-module-result /root/logos-chat-module/result
|
||||
|
||||
# Build sequencer + run_setup from source (system clang, r0vm in PATH)
|
||||
echo "Building sequencer + run_setup..."
|
||||
export LIBCLANG_PATH=/usr/lib/llvm-18/lib
|
||||
(cd $LEZ_DIR/lssa && cargo build --features standalone -p sequencer_service 2>&1 | tail -3)
|
||||
(cd $LEZ_DIR/lez-rln && cargo build --bin run_setup 2>&1 | tail -3)
|
||||
|
||||
export LOGOSCORE="/root/lez-modules/logoscore-result/bin/logoscore"
|
||||
bash simulations/mix_lez_chat/run_simulation.sh --fresh
|
||||
SIMSCRIPT
|
||||
|
||||
echo "=== Running simulation ==="
|
||||
docker exec -e BRANCH="$BRANCH" -e REPO_URL="$REPO_URL" $SIM_ENVS "$CONTAINER_NAME" bash /root/run-sim.sh
|
||||
|
||||
echo "=== Done ==="
|
||||
77
simulations/mix_lez_chat/INSTRUCTIONS.md
Normal file
77
simulations/mix_lez_chat/INSTRUCTIONS.md
Normal file
@ -0,0 +1,77 @@
|
||||
# Running the Mix + LEZ RLN Chat Simulation
|
||||
|
||||
End-to-end private chat between two logos-chat-module clients over a 4-node mix network with LEZ-backed RLN spam protection.
|
||||
|
||||
Two logoscore instances (sender + receiver) establish an X3DH key agreement via an out-of-band intro bundle, then exchange double-ratchet-encrypted messages routed through 3-hop Sphinx onion routes with per-hop RLN proof generation and verification. Node 0 mounts the rln_gifter service; nodes 1-3 and both chat clients register RLN memberships on-chain via the gifter protocol. The sender publishes via `lightpushPublish(mixify=true)`, the mix exit node verifies the RLN proof before fanning out via gossipsub relay, and the receiver consumes the message via a Waku filter subscription.
|
||||
|
||||
## macOS
|
||||
|
||||
**Prereqs:** nix (with flakes), Docker, cargo-risczero, SSH access to GitHub.
|
||||
|
||||
```bash
|
||||
git clone -b feat/logos-delivery git@github.com:adklempner/logos-chat.git
|
||||
cd logos-chat && bash simulations/mix_lez_chat/setup_and_run.sh
|
||||
```
|
||||
|
||||
First run: ~15-25 min. Re-runs: `bash simulations/mix_lez_chat/run_simulation.sh --fresh` (~5 min).
|
||||
|
||||
## Linux (native)
|
||||
|
||||
**Prereqs:** nix (with flakes), Docker, cargo-risczero, SSH access to GitHub.
|
||||
|
||||
```bash
|
||||
git clone -b feat/logos-delivery git@github.com:adklempner/logos-chat.git
|
||||
cd logos-chat && bash simulations/mix_lez_chat/setup_and_run.sh
|
||||
```
|
||||
|
||||
Same as macOS. On x86_64 Linux this should work out of the box. On aarch64 Linux, guest zkVM binaries must be pre-built on another platform (rzup doesn't support aarch64-linux) and the wallet module nix build needs `RISC0_SKIP_BUILD_KERNELS=1`.
|
||||
|
||||
## Linux (via Docker)
|
||||
|
||||
**Prereqs:** Docker.
|
||||
|
||||
```bash
|
||||
git clone -b feat/logos-delivery git@github.com:adklempner/logos-chat.git
|
||||
cd logos-chat && bash scripts/run_in_docker.sh
|
||||
```
|
||||
|
||||
The pre-built image (`ghcr.io/adklempner/logos-chat-sim`) is pulled automatically (~8.5GB download). Guest zkVM binaries must exist on the host from a previous macOS/x86_64 build, or set `GUEST_BINARIES_DIR`.
|
||||
|
||||
Each sim run: ~10 min (clone + sequencer build + sim). To force a local image rebuild: `REBUILD_IMAGE=1 bash scripts/run_in_docker.sh`.
|
||||
|
||||
## Pass criteria
|
||||
|
||||
**ALL 15 CHECKS PASSED** — 4 mix nodes mounted, gifter service, LEZ RLN active, sender+receiver initialized/started/mix-mounted, intro bundle created, messages sent and received.
|
||||
|
||||
## LEZ backend
|
||||
|
||||
The simulation runs against the SPEL framework (logos-lez-rln `sim-on-spel` branch, based on `feat/spel`). The on-chain RLN programs use SPEL's `#[lez_program]` macro with 32-byte tree IDs, Borsh-encoded state, and PDA-based account derivation via `combine_seeds`.
|
||||
|
||||
## Configuration
|
||||
|
||||
```bash
|
||||
SIM_LOG_LEVEL=TRACE SIM_KADEMLIA_MIN_WAIT=10 bash simulations/mix_lez_chat/run_simulation.sh --fresh
|
||||
```
|
||||
|
||||
Full list of `SIM_*` variables in `simulations/mix_lez_chat/README.md`.
|
||||
|
||||
## If it fails
|
||||
|
||||
Re-run with fresh state:
|
||||
```bash
|
||||
bash simulations/mix_lez_chat/run_simulation.sh --fresh
|
||||
```
|
||||
|
||||
Wallet/sequencer errors:
|
||||
```bash
|
||||
rm -f vendor/logos-lez-rln/dev/wallet_config.json vendor/logos-lez-rln/dev/storage.json
|
||||
rm -f ~/.logos-lez-rln/payment_account_*.txt
|
||||
```
|
||||
|
||||
Guest binary errors after updating submodules:
|
||||
```bash
|
||||
rm -rf vendor/logos-lez-rln/lez-rln/methods/guest/target
|
||||
bash simulations/mix_lez_chat/setup_and_run.sh
|
||||
```
|
||||
|
||||
Docker logs are rescued to `./docker-sim-logs/` on failure.
|
||||
229
simulations/mix_lez_chat/README.md
Normal file
229
simulations/mix_lez_chat/README.md
Normal file
@ -0,0 +1,229 @@
|
||||
# Mix + LEZ RLN Chat Simulation
|
||||
|
||||
End-to-end private chat between two logos-chat-module clients over a 4-node mix network with LEZ-backed RLN spam protection.
|
||||
|
||||
Two logoscore instances (sender + receiver) establish an X3DH key agreement via an out-of-band intro bundle, then exchange double-ratchet-encrypted messages routed through 3-hop Sphinx onion routes with per-hop RLN proof generation and verification. Node 0 mounts the rln_gifter service; nodes 1-3 and both chat clients register RLN memberships on-chain via the gifter protocol. The sender publishes via `lightpushPublish(mixify=true)`, the mix exit node verifies the RLN proof before fanning out via gossipsub relay, and the receiver consumes the message via a Waku filter subscription.
|
||||
|
||||
## macOS
|
||||
|
||||
**Prereqs:** nix (with flakes), Docker, cargo-risczero, SSH access to GitHub.
|
||||
|
||||
```bash
|
||||
git clone -b feat/logos-delivery git@github.com:adklempner/logos-chat.git
|
||||
cd logos-chat && bash simulations/mix_lez_chat/setup_and_run.sh
|
||||
```
|
||||
|
||||
First run: ~15-25 min. Re-runs: `bash simulations/mix_lez_chat/run_simulation.sh --fresh` (~5 min).
|
||||
|
||||
## Linux (native)
|
||||
|
||||
**Prereqs:** nix (with flakes), Docker, cargo-risczero, SSH access to GitHub.
|
||||
|
||||
```bash
|
||||
git clone -b feat/logos-delivery git@github.com:adklempner/logos-chat.git
|
||||
cd logos-chat && bash simulations/mix_lez_chat/setup_and_run.sh
|
||||
```
|
||||
|
||||
Same as macOS. On x86_64 Linux this should work out of the box. On aarch64 Linux, guest zkVM binaries must be pre-built on another platform (rzup doesn't support aarch64-linux) and the wallet module nix build needs `RISC0_SKIP_BUILD_KERNELS=1`.
|
||||
|
||||
## Linux (via Docker)
|
||||
|
||||
**Prereqs:** Docker with 24GB RAM allocated.
|
||||
|
||||
```bash
|
||||
git clone -b feat/logos-delivery git@github.com:adklempner/logos-chat.git
|
||||
cd logos-chat && bash scripts/run_in_docker.sh
|
||||
```
|
||||
|
||||
The pre-built image (`ghcr.io/adklempner/logos-chat-sim`) is pulled automatically (~8.5GB download). Guest zkVM binaries must exist on the host from a previous macOS/x86_64 build, or set `GUEST_BINARIES_DIR`.
|
||||
|
||||
Each sim run: ~10 min (clone + sequencer build + sim). To force a local image rebuild: `REBUILD_IMAGE=1 bash scripts/run_in_docker.sh`.
|
||||
|
||||
## Pass criteria
|
||||
|
||||
**ALL 15 CHECKS PASSED** — 4 mix nodes mounted, gifter service, LEZ RLN active, sender+receiver initialized/started/mix-mounted, intro bundle created, messages sent and received.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
logoscore (per mix node) logoscore (per chat client)
|
||||
├── wallet_module (LEZ wallet) ├── wallet_module (LEZ wallet)
|
||||
├── liblogos_rln_module (RLN proofs) ├── liblogos_rln_module (RLN proofs)
|
||||
└── delivery_module (Waku mix relay) └── chat_module (logos-chat-module)
|
||||
├── liblogosdelivery.so ├── chat_module_plugin.so
|
||||
└── mix + relay + filter + gifter └── liblogoschat.so
|
||||
└── mix client + filter + gifter client
|
||||
```
|
||||
|
||||
Node 0 runs the RLN gifter service. Nodes 1-3 register via gifter on startup. Chat clients also register via gifter when `startChat()` runs.
|
||||
|
||||
## Configuration
|
||||
|
||||
Override defaults via environment:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `SIM_NUM_NODES` | `4` | Number of mix relay nodes |
|
||||
| `SIM_BASE_TCP_PORT` | `60001` | First node's TCP port (increments per node) |
|
||||
| `SIM_BASE_DISC_PORT` | `9001` | First node's discv5 UDP port (increments per node) |
|
||||
| `SIM_CLUSTER_ID` | `99` | Waku cluster ID |
|
||||
| `SIM_LOG_LEVEL` | `INFO` | Node log level (TRACE, DEBUG, INFO, WARN, ERROR) |
|
||||
| `SIM_CHAT_RECV_PORT` | `60010` | Chat receiver TCP port |
|
||||
| `SIM_CHAT_SEND_PORT` | `60011` | Chat sender TCP port |
|
||||
| `SIM_KADEMLIA_MIN_WAIT` | `30` (local) / `120` (testnet) | Minimum seconds to wait for kademlia propagation |
|
||||
| `SIM_RECEIVER_MIN_WAIT` | `15` (local) / `60` (testnet) | Minimum seconds to wait for receiver to join mix |
|
||||
| `SIM_DELIVERY_TIMEOUT` | `120` (local) / `300` (testnet) | Max seconds to wait for message delivery |
|
||||
| `SIM_NODE_STARTUP_SLEEP` | `10` (local) / `30` (testnet) | Seconds between launching each mix node |
|
||||
| `SIM_NETWORK` | `local` | `local` runs against a sequencer on `127.0.0.1:3040`; `testnet` runs against `https://testnet.lez.logos.co/` |
|
||||
|
||||
Example — fast iteration with verbose logging:
|
||||
|
||||
```bash
|
||||
SIM_LOG_LEVEL=TRACE SIM_KADEMLIA_MIN_WAIT=10 SIM_RECEIVER_MIN_WAIT=5 \
|
||||
bash simulations/mix_lez_chat/run_simulation.sh --fresh
|
||||
```
|
||||
|
||||
## Running against the public testnet
|
||||
|
||||
```bash
|
||||
SIM_NETWORK=testnet bash simulations/mix_lez_chat/run_simulation.sh --fresh
|
||||
```
|
||||
|
||||
Effect of `SIM_NETWORK=testnet`:
|
||||
- Phase 1 (local sequencer launch) is skipped; the script does a one-shot reachability check against `https://testnet.lez.logos.co/` and dies up front if unreachable.
|
||||
- Wallet config is picked from `vendor/logos-lez-rln/testnet/` instead of `dev/`. The wallet's `storage.json` and the on-chain registration accounts persist across runs.
|
||||
- `run_setup` deploys + initializes on first run, then short-circuits via `is_initialized` on every run after — see "Config account: …" in the setup output either way.
|
||||
- Timing floors (`SIM_KADEMLIA_MIN_WAIT`, `SIM_RECEIVER_MIN_WAIT`, `SIM_DELIVERY_TIMEOUT`, `SIM_NODE_STARTUP_SLEEP`) and `LEZ_RLN_BLOCK_SEAL_SECS` default higher to match ~60s testnet block times.
|
||||
|
||||
Prerequisites:
|
||||
- The gifter mix node's payment account must be funded on testnet. The first successful `SIM_NETWORK=testnet … --fresh` run populates `~/.logos-lez-rln/payment_account_<tree_id>.txt` automatically; subsequent runs reuse it.
|
||||
- Expected wall-clock runtime: ~20–25 minutes (first run) / ~15 minutes (subsequent runs), vs. ~3 minutes locally.
|
||||
- Only one developer at a time — concurrent testnet sim runs share the gifter wallet and will collide.
|
||||
|
||||
### Reproducibility on a fresh clone
|
||||
|
||||
The canonical testnet deployment (RLN tree + minted supply) is shared across developers. On first run, the script seeds two artifacts from the submodule so `run_setup` can short-circuit to `create_funded_user`:
|
||||
|
||||
- `vendor/logos-lez-rln/testnet/storage.json.seed` → copied to `vendor/logos-lez-rln/testnet/storage.json` if absent. Contains only the supply holding account + its signing key.
|
||||
- `vendor/logos-lez-rln/testnet/supply_holding.txt` → copied to `~/.logos-lez-rln/supply_holding_<tree_id>.txt` if absent. Contains the supply `AccountId`.
|
||||
|
||||
What stays shared vs. fresh:
|
||||
|
||||
| Artifact | Shared | Per-dev fresh |
|
||||
|---|---|---|
|
||||
| `TREE_ID`, sequencer URL, deployed program IDs (on-chain), gifter EIP-191 auth keys, mix node identity keys | ✓ | |
|
||||
| Supply holding account + signing key (seeded from submodule) | ✓ | |
|
||||
| Per-run payment account (`~/.logos-lez-rln/payment_account_<tree>.txt`) | | ✓ |
|
||||
| Working-copy `testnet/storage.json` (gitignored; accumulates payment accounts) | | ✓ |
|
||||
| Mix + chat RLN credentials (in `.sim_state/rln_keystore_*.json`) | | ✓ |
|
||||
|
||||
**Security:** the supply signing key being in the repo is acceptable only because testnet does not charge gas and the tokens are test tokens with no real value.
|
||||
|
||||
### Slim mode (`SIM_SLIM=1`, testnet only)
|
||||
|
||||
`SIM_SLIM=1 SIM_NETWORK=testnet ./run_simulation.sh --fresh` skips `run_setup` entirely and reuses the shipped `config_account` + cached `payment_account` from `vendor/logos-lez-rln/testnet/`. Two consequences:
|
||||
|
||||
- No `lez-rln/run_setup` binary build is required. Combined with the submodule split below, a fresh clone can run the sim without ever invoking `cargo` from `lez-rln/`.
|
||||
- All slim-mode runs share one on-chain payment account — concurrent runs across devs will race on its nonce. Use serially.
|
||||
|
||||
**Minimum submodule set for slim mode:**
|
||||
|
||||
```bash
|
||||
git clone --branch feat/logos-delivery <repo> logos-chat
|
||||
cd logos-chat
|
||||
git submodule update --init vendor/logos-lez-rln vendor/nwaku vendor/nimbus-build-system vendor/nim-protobuf-serialization vendor/npeg vendor/blake2 vendor/libchat vendor/nim-ffi
|
||||
(cd vendor/logos-lez-rln && git submodule update --init logos-delivery-module)
|
||||
(cd vendor/logos-lez-rln/logos-delivery-module && git submodule update --init --recursive vendor/logos-delivery)
|
||||
```
|
||||
|
||||
The previously-required `lssa` (~11 GB) and `logos-execution-zone-module` clones are unnecessary for slim mode — both are fetched via nix flake from GitHub when building the wallet/RLN modules. The top-level `vendor/logos-lez-rln/logos-delivery` submodule was removed entirely (the active copy is the nested `logos-delivery-module/vendor/logos-delivery`).
|
||||
|
||||
For the local-sequencer flow (`SIM_NETWORK=local`) or to hack on the wallet/sequencer source, init the extras: `(cd vendor/logos-lez-rln && git submodule update --init lssa logos-execution-zone-module)`. The Docker bootstrap (`setup_and_run.sh`) gates these on `SIM_NETWORK=local` automatically; pass `SIM_FULL_SUBMODS=1` to force the wide init.
|
||||
|
||||
## `--fresh` behavior
|
||||
|
||||
When `--fresh` is passed:
|
||||
- Kills all existing `logos_host` processes
|
||||
- Cleans `/tmp/logos_*` Qt RemoteObjects sockets
|
||||
- Removes `.sim_state/` directory
|
||||
- On `SIM_NETWORK=local` (default): removes sequencer state (`rocksdb/`, `bedrock_signing_key`), rebuilds and restarts the sequencer, redeploys RLN programs via `run_setup`
|
||||
- On `SIM_NETWORK=testnet`: leaves on-chain state and the persistent wallet under `vendor/logos-lez-rln/testnet/` intact; `run_setup` short-circuits to `create_funded_user`
|
||||
|
||||
Without `--fresh`, on `SIM_NETWORK=local` it reuses an existing sequencer if port 3040 is already bound.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Re-run with fresh state:**
|
||||
```bash
|
||||
bash simulations/mix_lez_chat/run_simulation.sh --fresh
|
||||
```
|
||||
|
||||
**"Sequencer failed to start"** — port 3040 already in use:
|
||||
```bash
|
||||
kill $(lsof -ti tcp:3040) && bash simulations/mix_lez_chat/run_simulation.sh --fresh
|
||||
```
|
||||
|
||||
**"run_setup failed" / "Timeout waiting for account"** — stale guest binaries or wallet state:
|
||||
```bash
|
||||
rm -rf vendor/logos-lez-rln/lez-rln/methods/guest/target
|
||||
rm -f vendor/logos-lez-rln/dev/wallet_config.json vendor/logos-lez-rln/dev/storage.json
|
||||
bash simulations/mix_lez_chat/setup_and_run.sh
|
||||
```
|
||||
|
||||
**"Sender started FAIL"** — stale Qt RemoteObjects sockets:
|
||||
```bash
|
||||
rm -f /tmp/logos_*
|
||||
bash simulations/mix_lez_chat/run_simulation.sh --fresh
|
||||
```
|
||||
|
||||
Docker logs are rescued to `./docker-sim-logs/` on failure.
|
||||
|
||||
## Adapting for other LEZ programs
|
||||
|
||||
This simulation provides a complete mix network infrastructure that other logos modules can reuse for testing. To test your own module:
|
||||
|
||||
### What the sim provides
|
||||
- 4 logoscore mix nodes with `delivery_module` (Waku relay + mix + RLN)
|
||||
- LEZ sequencer with deployed RLN programs
|
||||
- RLN gifter service on node 0
|
||||
- Wallet modules for on-chain transactions
|
||||
|
||||
### What you replace
|
||||
The chat_module sender/receiver instances (phase 5 of run_simulation.sh). Your module needs:
|
||||
|
||||
1. **A C++ Qt plugin** implementing `PluginInterface` (see `chat_module_plugin.cpp`)
|
||||
- `initLogos(LogosAPI*)` — receive the LogosAPI instance
|
||||
- `eventResponse(QString, QVariantList)` signal — mandatory per logos-liblogos contract
|
||||
- Methods exposed via `LOGOS_METHOD` for logoscore `-c` invocation
|
||||
2. **A shared library** with your program logic (like `liblogoschat.so`)
|
||||
3. **RLN integration** — wire `setRlnConfig` to pass RLN credentials from the C++ plugin to your library
|
||||
4. **EVENT: stderr fallback** — on Linux, Qt signal forwarding from plugin to logoscore doesn't work across the FFI thread boundary. Write event data to stderr in `EVENT:name:data` format (gated by `LOGOS_EVENT_STDERR` env var) for cross-platform reliability.
|
||||
|
||||
### How to stage your module
|
||||
|
||||
```bash
|
||||
MDIR=$(mktemp -d)
|
||||
mkdir -p "$MDIR/your_module"
|
||||
cp your_module_plugin.so "$MDIR/your_module/"
|
||||
cp libyour_library.so "$MDIR/your_module/"
|
||||
echo '{"name":"your_module","version":"1.0.0","type":"core",...}' > "$MDIR/your_module/manifest.json"
|
||||
|
||||
logoscore -m "$MDIR" \
|
||||
-l "liblogos_execution_zone_wallet_module,liblogos_rln_module,your_module" \
|
||||
-c "liblogos_execution_zone_wallet_module.open($WALLET_CONFIG,$WALLET_STORAGE)" \
|
||||
-c "your_module.init(@config.json)" \
|
||||
-c "your_module.start()"
|
||||
```
|
||||
|
||||
### Reference
|
||||
- `chat_module_plugin.cpp` — complete working example with RLN, gifter, mix, and event emission
|
||||
- `delivery_module_plugin.cpp` — more complex example with full RLN fetcher integration
|
||||
- `run_simulation.sh` — orchestration, module staging, and verification patterns
|
||||
|
||||
## Logs
|
||||
|
||||
All logs in `simulations/mix_lez_chat/.sim_state/`:
|
||||
- `node0.log` – `node3.log` — mix relay nodes
|
||||
- `chat_receiver.log` — receiver chat module
|
||||
- `chat_sender.log` — sender chat module
|
||||
- `sequencer.log` — LEZ sequencer
|
||||
@ -0,0 +1,6 @@
|
||||
# Ethereum addresses derived from keys.env. Keep in sync if those keys change.
|
||||
ADDR_MIX1=0x8ba6d3237e6f2c84b0e3d71aa57bc5869d3b5218
|
||||
ADDR_MIX2=0x8e3d4d0a713087e2263e2fcdec894c283c777dcc
|
||||
ADDR_MIX3=0xca282bbf8bf3636e15af3ad8caf11cdd38bf35d8
|
||||
ADDR_SENDER=0x0b6872aaae7a2d4f3c701793cde57b93337f4d4a
|
||||
ADDR_RECEIVER=0xb5dda07309f5ab06e0847f6036c305ea9ae26937
|
||||
9
simulations/mix_lez_chat/fixtures/gifter_auth/keys.env
Normal file
9
simulations/mix_lez_chat/fixtures/gifter_auth/keys.env
Normal file
@ -0,0 +1,9 @@
|
||||
# Test fixtures for the RLN gifter EIP-191 auth path. NOT FOR PRODUCTION.
|
||||
# Each value is a 64-hex-char secp256k1 private key. addresses.env holds
|
||||
# the corresponding Ethereum addresses; if these keys change, regenerate
|
||||
# the addresses (any keccak256(secp256k1 pubkey)[12:] tool will do).
|
||||
KEY_MIX1=2c974e0a453f65dd2230d403f6981fc18f9a3ad7675afb647910e0798a3eaa4f
|
||||
KEY_MIX2=b880df1f571109e646f641636794dfe7ffefc2aab19290ba0d720c407758304d
|
||||
KEY_MIX3=0b1b5e18839a3e15b119519092e4a94a71122acf57d8b2e1014df0121cb6f0ea
|
||||
KEY_SENDER=5284ac01fed5fcb6b26933ac4a901412b66fcd7ee5b945b799f147a3b42f49ef
|
||||
KEY_RECEIVER=a5619d6bfde09f54165ec9da55a7be7380f1b258c8279177dbda5ac235d0e904
|
||||
685
simulations/mix_lez_chat/run_simulation.sh
Executable file
685
simulations/mix_lez_chat/run_simulation.sh
Executable file
@ -0,0 +1,685 @@
|
||||
#!/usr/bin/env bash
|
||||
# Mix + LEZ RLN simulation using logos-chat-module as sender/receiver.
|
||||
# Reuses the logoscore mix node infrastructure from logos-lez-rln and replaces
|
||||
# chat2mix with logoscore instances running the chat_module plugin.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - logos-lez-rln repo as sibling or set LEZ_RLN_DIR
|
||||
# - logos-chat-module built (nix build in ../logos-chat-module)
|
||||
# - logos-chat built (make liblogoschat in this repo)
|
||||
#
|
||||
# Usage: ./run_simulation.sh [--fresh]
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOGOS_CHAT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
CHAT_MODULE_DIR="${CHAT_MODULE_DIR:-$(cd "$LOGOS_CHAT_DIR/../logos-chat-module" && pwd)}"
|
||||
|
||||
# Use vendored logos-lez-rln submodule, or auto-detect as sibling
|
||||
LEZ_RLN_DIR="${LEZ_RLN_DIR:-}"
|
||||
if [ -z "$LEZ_RLN_DIR" ] && [ -d "$LOGOS_CHAT_DIR/vendor/logos-lez-rln/lez-rln" ]; then
|
||||
LEZ_RLN_DIR="$LOGOS_CHAT_DIR/vendor/logos-lez-rln"
|
||||
fi
|
||||
for candidate in "$LOGOS_CHAT_DIR/.." "$LOGOS_CHAT_DIR/../logos-lez-rln"; do
|
||||
[ -n "$LEZ_RLN_DIR" ] && break
|
||||
[ -d "$candidate/lez-rln" ] && LEZ_RLN_DIR="$(cd "$candidate" && pwd)" && break
|
||||
done
|
||||
[ -z "$LEZ_RLN_DIR" ] && { echo "FATAL: Cannot find logos-lez-rln repo. Set LEZ_RLN_DIR or run: git submodule update --init --recursive"; exit 1; }
|
||||
|
||||
DELIVERY_MODULE_DIR="${DELIVERY_MODULE_DIR:-$LEZ_RLN_DIR/logos-delivery-module}"
|
||||
DELIVERY_DIR="$DELIVERY_MODULE_DIR/vendor/logos-delivery"
|
||||
|
||||
export RISC0_DEV_MODE=1
|
||||
export TMPDIR=/tmp
|
||||
export LOGOS_EVENT_STDERR=1 # Mirror EVENT: lines to stderr so the sim can grep them.
|
||||
|
||||
die() { echo " FATAL: $*" >&2; exit 1; }
|
||||
log() { echo "[$(date '+%H:%M:%S')] $*"; }
|
||||
|
||||
# Poll a logoscore log until it shows >= $expected "Method call successful"
|
||||
# lines, or until $timeout iterations (sleeping $sleep_sec each) have elapsed.
|
||||
# Sets global $N to the last observed count so callers can branch on it.
|
||||
wait_method_calls() {
|
||||
local logfile="$1" expected="$2" timeout="$3" sleep_sec="${4:-1}"
|
||||
local t
|
||||
for t in $(seq 1 "$timeout"); do
|
||||
N=$(grep -c '^Method call successful' "$logfile" 2>/dev/null || true); N=${N:-0}
|
||||
[ "$N" -ge "$expected" ] && return 0
|
||||
sleep "$sleep_sec"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- Node identity constants (4 mix nodes) ---
|
||||
NODEKEYS=(
|
||||
"f98e3fba96c32e8d1967d460f1b79457380e1a895f7971cecc8528abe733781a"
|
||||
"09e9d134331953357bd38bbfce8edb377f4b6308b4f3bfbe85c610497053d684"
|
||||
"ed54db994682e857d77cd6fb81be697382dc43aa5cd78e16b0ec8098549f860e"
|
||||
"42f96f29f2d6670938b0864aced65a332dcf5774103b4c44ec4d0ea4ef3c47d6"
|
||||
)
|
||||
PEER_IDS=(
|
||||
"16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o"
|
||||
"16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF"
|
||||
"16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA"
|
||||
"16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f"
|
||||
)
|
||||
MIXKEYS=(
|
||||
"c86029e02c05a7e25182974b519d0d52fcbafeca6fe191fbb64857fb05be1a53"
|
||||
"b858ac16bbb551c4b2973313b1c8c8f7ea469fca03f1608d200bbf58d388ec7f"
|
||||
"d8bd379bb394b0f22dd236d63af9f1a9bc45266beffc3fbbe19e8b6575f2535b"
|
||||
"780fff09e51e98df574e266bf3266ec6a3a1ddfcf7da826a349a29c137009d49"
|
||||
)
|
||||
MIX_PUBKEYS=(
|
||||
"9231e86da6432502900a84f867004ce78632ab52cd8e30b1ec322cd795710c2a"
|
||||
"275cd6889e1f29ca48e5b9edb800d1a94f49f13d393a0ecf1a07af753506de6c"
|
||||
"e0ed594a8d506681be075e8e23723478388fb182477f7a469309a25e7076fc18"
|
||||
"8fd7a1a7c19b403d231452a9b1ea40eb1cc76f455d918ef8980e7685f9eeeb1f"
|
||||
)
|
||||
# --- Configurable parameters (override via environment) ---
|
||||
NUM_NODES=${SIM_NUM_NODES:-4}
|
||||
BASE_TCP_PORT=${SIM_BASE_TCP_PORT:-60001}
|
||||
BASE_DISC_PORT=${SIM_BASE_DISC_PORT:-9001}
|
||||
CLUSTER_ID=${SIM_CLUSTER_ID:-99}
|
||||
NUM_SHARDS=1
|
||||
CONTENT_TOPIC="/logos-chat/1/mix-test/proto"
|
||||
TEST_MESSAGE_PREFIX="chatmixtest"
|
||||
LOG_LEVEL=${SIM_LOG_LEVEL:-INFO}
|
||||
CHAT_RECV_PORT=${SIM_CHAT_RECV_PORT:-60010}
|
||||
CHAT_SEND_PORT=${SIM_CHAT_SEND_PORT:-60011}
|
||||
SIM_NETWORK=${SIM_NETWORK:-local}
|
||||
case "$SIM_NETWORK" in
|
||||
local|testnet) ;;
|
||||
*) die "SIM_NETWORK must be 'local' or 'testnet', got: $SIM_NETWORK";;
|
||||
esac
|
||||
|
||||
# Timing floors: local sequencer ~15s blocks vs testnet ~60s + more variance.
|
||||
if [ "$SIM_NETWORK" = testnet ]; then
|
||||
KADEMLIA_MIN_WAIT=${SIM_KADEMLIA_MIN_WAIT:-120}
|
||||
# Testnet block times + finality lag can stretch RLN tx confirmations to
|
||||
# multiple minutes per mix node. 4 nodes × ~5 min each + slack ≈ 30 min.
|
||||
KADEMLIA_HARD_CAP=${SIM_KADEMLIA_HARD_CAP:-1800}
|
||||
RECEIVER_MIN_WAIT=${SIM_RECEIVER_MIN_WAIT:-60}
|
||||
DELIVERY_TIMEOUT=${SIM_DELIVERY_TIMEOUT:-300}
|
||||
NODE_STARTUP_SLEEP=${SIM_NODE_STARTUP_SLEEP:-30}
|
||||
export LEZ_RLN_BLOCK_SEAL_SECS="${LEZ_RLN_BLOCK_SEAL_SECS:-90}"
|
||||
else
|
||||
KADEMLIA_MIN_WAIT=${SIM_KADEMLIA_MIN_WAIT:-30}
|
||||
KADEMLIA_HARD_CAP=${SIM_KADEMLIA_HARD_CAP:-180}
|
||||
RECEIVER_MIN_WAIT=${SIM_RECEIVER_MIN_WAIT:-15}
|
||||
DELIVERY_TIMEOUT=${SIM_DELIVERY_TIMEOUT:-120}
|
||||
NODE_STARTUP_SLEEP=${SIM_NODE_STARTUP_SLEEP:-10}
|
||||
fi
|
||||
TESTNET_RPC_URL="https://testnet.lez.logos.co/"
|
||||
|
||||
case "$(uname -s)-$(uname -m)" in
|
||||
Darwin-arm64) PLATFORM="darwin-arm64-dev"; EXT="dylib";;
|
||||
Linux-x86_64) PLATFORM="linux-x86_64-dev"; EXT="so";;
|
||||
Linux-aarch64) PLATFORM="linux-aarch64-dev"; EXT="so";;
|
||||
*) die "Unsupported platform";;
|
||||
esac
|
||||
|
||||
STATE_DIR="$SCRIPT_DIR/.sim_state"
|
||||
FRESH=0
|
||||
for arg in "$@"; do [ "$arg" = "--fresh" ] && FRESH=1; done
|
||||
[ "$FRESH" -eq 1 ] && rm -rf "$STATE_DIR"
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
SEQUENCER_PID=""
|
||||
OWN_SEQUENCER=0
|
||||
INSTANCE_PIDS=()
|
||||
MODULES_DIRS=()
|
||||
SENDER_PID=""
|
||||
RECEIVER_PID=""
|
||||
EXIT_CODE=1
|
||||
|
||||
cleanup() {
|
||||
set +u
|
||||
echo ""; echo "=== Shutting down ==="
|
||||
[ -n "$SENDER_PID" ] && kill "$SENDER_PID" 2>/dev/null || true
|
||||
[ -n "$RECEIVER_PID" ] && kill "$RECEIVER_PID" 2>/dev/null || true
|
||||
for pid in "${INSTANCE_PIDS[@]+"${INSTANCE_PIDS[@]}"}"; do [ -n "$pid" ] && kill "$pid" 2>/dev/null || true; done
|
||||
pkill -f 'logos_host' 2>/dev/null || true
|
||||
[ "$OWN_SEQUENCER" -eq 1 ] && [ -n "$SEQUENCER_PID" ] && kill "$SEQUENCER_PID" 2>/dev/null || true
|
||||
for mdir in "${MODULES_DIRS[@]+"${MODULES_DIRS[@]}"}"; do [ -n "$mdir" ] && rm -rf "$mdir"; done
|
||||
echo " Logs: $STATE_DIR"; echo "Done."; exit "$EXIT_CODE"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "=== Mix + LEZ RLN Chat Simulation ($NUM_NODES nodes) ==="
|
||||
echo " Network: $SIM_NETWORK"
|
||||
[ "$SIM_NETWORK" = testnet ] && echo " Testnet RPC: $TESTNET_RPC_URL"
|
||||
echo " LEZ repo: $LEZ_RLN_DIR"
|
||||
echo " Chat module: $CHAT_MODULE_DIR"
|
||||
echo " Logos-chat: $LOGOS_CHAT_DIR"
|
||||
echo ""
|
||||
|
||||
pkill -f 'logos_host' 2>/dev/null || true; sleep 1
|
||||
# Stale QtRO LocalServer sockets confuse capability_module lookups.
|
||||
rm -f /tmp/logos_* 2>/dev/null || true
|
||||
|
||||
# ---------- Phase 1: Sequencer ----------
|
||||
echo "[1/6] Sequencer..."
|
||||
if [ "$SIM_NETWORK" = testnet ]; then
|
||||
# Fail fast if testnet RPC is unreachable before 10+ min of setup work.
|
||||
BLOCK_RESP=$(curl -sS -m 10 -X POST -H 'Content-Type: application/json' \
|
||||
--data '{"jsonrpc":"2.0","method":"getLastBlockId","params":[],"id":1}' \
|
||||
"$TESTNET_RPC_URL" 2>&1)
|
||||
case "$BLOCK_RESP" in
|
||||
*'"result"'*) log " Testnet reachable: $(echo "$BLOCK_RESP" | grep -oE '"result":[0-9]+' | head -1)";;
|
||||
*) die "Testnet unreachable at $TESTNET_RPC_URL: $BLOCK_RESP";;
|
||||
esac
|
||||
elif nc -z 127.0.0.1 3040 2>/dev/null && [ "$FRESH" -eq 0 ]; then
|
||||
SEQUENCER_PID=$(lsof -ti tcp:3040 2>/dev/null || true)
|
||||
echo " Already running (PID $SEQUENCER_PID)"
|
||||
else
|
||||
[ "$(nc -z 127.0.0.1 3040 2>/dev/null; echo $?)" = "0" ] && kill "$(lsof -ti tcp:3040 2>/dev/null)" 2>/dev/null || true; sleep 1
|
||||
rm -rf "$LEZ_RLN_DIR/lssa/rocksdb" "$LEZ_RLN_DIR/lssa/sequencer/service/bedrock_signing_key"
|
||||
|
||||
# Pre-built binary path skips both the cargo build and the lssa auto-sync
|
||||
# (auto-sync needs full git history; shallow Docker clones break it).
|
||||
if [ -x "$LEZ_RLN_DIR/lssa/target/debug/sequencer_service" ]; then
|
||||
log " Using pre-built sequencer"
|
||||
SEQ_BIN="./target/debug/sequencer_service"; SEQ_CFG="sequencer/service/configs/debug/sequencer_config.json"
|
||||
else
|
||||
# lssa rev must match what lez-rln's host-side client pins, else
|
||||
# DeserializeUnexpectedEnd from wire-format divergence.
|
||||
LSSA_REV=$(grep -oE '(rev|tag)\s*=\s*"[^"]+"' "$LEZ_RLN_DIR/lez-rln/Cargo.toml" | head -1 | sed 's/.*"\([^"]*\)"/\1/')
|
||||
[ -z "$LSSA_REV" ] && die "Could not extract lssa rev from lez-rln/Cargo.toml"
|
||||
if ! git -C "$LEZ_RLN_DIR/lssa" merge-base --is-ancestor "$LSSA_REV" HEAD 2>/dev/null; then
|
||||
log " Pinning lssa to $LSSA_REV..."
|
||||
(cd "$LEZ_RLN_DIR/lssa" && git fetch --quiet --tags origin && git checkout --quiet "$LSSA_REV") \
|
||||
|| die "lssa checkout $LSSA_REV failed"
|
||||
fi
|
||||
log " Building sequencer..."
|
||||
if (cd "$LEZ_RLN_DIR/lssa" && cargo build --features standalone -p sequencer_service 2>&1 | tail -3); then
|
||||
SEQ_BIN="./target/debug/sequencer_service"; SEQ_CFG="sequencer/service/configs/debug/sequencer_config.json"
|
||||
elif (cd "$LEZ_RLN_DIR/lssa" && cargo build --features standalone -p sequencer_runner 2>&1 | tail -3); then
|
||||
SEQ_BIN="./target/debug/sequencer_runner"; SEQ_CFG="sequencer_runner/configs/debug"
|
||||
else die "sequencer build failed"; fi
|
||||
fi
|
||||
(cd "$LEZ_RLN_DIR/lssa" && env RUST_LOG=info "$SEQ_BIN" "$SEQ_CFG") >"$STATE_DIR/sequencer.log" 2>&1 &
|
||||
SEQUENCER_PID=$!; OWN_SEQUENCER=1; echo " PID: $SEQUENCER_PID"
|
||||
for _ in $(seq 1 60); do nc -z 127.0.0.1 3040 2>/dev/null && break; sleep 1; done
|
||||
nc -z 127.0.0.1 3040 2>/dev/null || die "Sequencer failed to start"
|
||||
log " Ready."
|
||||
fi
|
||||
|
||||
# ---------- Phase 2: Deploy programs ----------
|
||||
echo "[2/6] Deploying programs..."
|
||||
# `local` -> dev/ (re-init each run); `testnet` -> testnet/ (persistent
|
||||
# wallet + on-chain state). wallet_config.json under each picks the sequencer.
|
||||
WALLET_HOME_SUBDIR=$([ "$SIM_NETWORK" = testnet ] && echo testnet || echo dev)
|
||||
export NSSA_WALLET_HOME_DIR="$LEZ_RLN_DIR/$WALLET_HOME_SUBDIR"
|
||||
export WALLET_CONFIG="$NSSA_WALLET_HOME_DIR/wallet_config.json"
|
||||
export WALLET_STORAGE="$NSSA_WALLET_HOME_DIR/storage.json"
|
||||
TREE_ID_HEX="000102030405060708090a0b0c0d0e0f10111213141516171a05100200000001"
|
||||
GIFTER_ACCOUNT_FILE="$HOME/.logos-lez-rln/payment_account_${TREE_ID_HEX}.txt"
|
||||
|
||||
# Local: clean wallet each run so run_setup re-deploys.
|
||||
# Testnet: persist wallet + on-chain state; run_setup short-circuits via
|
||||
# is_initialized() into create_funded_user.
|
||||
if [ "$SIM_NETWORK" = local ] && [ "${SIM_PERSIST_LOCAL:-0}" != "1" ]; then
|
||||
rm -f "$WALLET_CONFIG" "$WALLET_STORAGE"
|
||||
fi
|
||||
|
||||
# Testnet bootstrap: seed wallet + supply-holding sidecar from the
|
||||
# submodule-shipped artifacts on first run. After that, create_funded_user
|
||||
# draws fresh per-dev payment accounts from the shared supply.
|
||||
if [ "$SIM_NETWORK" = testnet ]; then
|
||||
# Copy shipped seed -> runtime location iff runtime is missing.
|
||||
seed_copy() {
|
||||
local label="$1" src="$2" dst="$3"
|
||||
[ -f "$dst" ] && return 0
|
||||
[ -f "$src" ] || return 0
|
||||
log " Seeding $label -> $dst"
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
cp "$src" "$dst"
|
||||
}
|
||||
seed_copy "testnet wallet" \
|
||||
"$LEZ_RLN_DIR/testnet/storage.json.seed" "$WALLET_STORAGE"
|
||||
seed_copy "supply holding sidecar" \
|
||||
"$LEZ_RLN_DIR/testnet/supply_holding.txt" \
|
||||
"$HOME/.logos-lez-rln/supply_holding_${TREE_ID_HEX}.txt"
|
||||
seed_copy "payment account sidecar" \
|
||||
"$LEZ_RLN_DIR/testnet/payment_account.txt" \
|
||||
"$HOME/.logos-lez-rln/payment_account_${TREE_ID_HEX}.txt"
|
||||
fi
|
||||
|
||||
# Slim mode (SIM_SLIM=1, testnet only): skip run_setup when the shipped
|
||||
# config_account + cached payment_account are both present — lets fresh
|
||||
# clones avoid building lez-rln/run_setup. The shared payment_account is
|
||||
# signed in storage.json.seed and has enough RLNTOK for ~1M Register txs.
|
||||
# Experimental; the default run_setup path is better-tested.
|
||||
CONFIG_ACCOUNT_SEED="$LEZ_RLN_DIR/testnet/config_account.txt"
|
||||
SLIM=0
|
||||
if [ "$SIM_NETWORK" = testnet ] && [ "${SIM_SLIM:-0}" = "1" ] \
|
||||
&& [ -f "$CONFIG_ACCOUNT_SEED" ] && [ -f "$GIFTER_ACCOUNT_FILE" ]; then
|
||||
SLIM=1
|
||||
fi
|
||||
if [ "$SLIM" = "1" ]; then
|
||||
log " Slim mode: skipping run_setup (using shipped config_account + cached payment_account)"
|
||||
CONFIG_ACCOUNT=$(tr -d '\n\r' < "$CONFIG_ACCOUNT_SEED")
|
||||
GIFTER_ACCOUNT=$(cat "$GIFTER_ACCOUNT_FILE")
|
||||
else
|
||||
if [ -x "$LEZ_RLN_DIR/lez-rln/target/debug/run_setup" ]; then
|
||||
SETUP_OUTPUT=$(cd "$LEZ_RLN_DIR/lez-rln" && ./target/debug/run_setup 2>&1) || die "run_setup failed"
|
||||
else
|
||||
SETUP_OUTPUT=$(cd "$LEZ_RLN_DIR/lez-rln" && cargo run --bin run_setup 2>&1) || die "run_setup failed"
|
||||
fi
|
||||
echo "$SETUP_OUTPUT" | tail -4
|
||||
# Both deploy + already-initialized branches print "Config account:".
|
||||
CONFIG_ACCOUNT=$(echo "$SETUP_OUTPUT" | grep -oE 'Config account:\s+\S+' | awk '{print $NF}' || true)
|
||||
[ -z "$CONFIG_ACCOUNT" ] && die "Failed to parse config account"
|
||||
GIFTER_ACCOUNT=$(cat "$GIFTER_ACCOUNT_FILE" 2>/dev/null || true)
|
||||
[ -z "$GIFTER_ACCOUNT" ] && die "Gifter account not found at $GIFTER_ACCOUNT_FILE"
|
||||
fi
|
||||
echo " CONFIG_ACCOUNT=$CONFIG_ACCOUNT"
|
||||
echo " GIFTER_ACCOUNT=$GIFTER_ACCOUNT"
|
||||
|
||||
# EIP-191 auth fixtures: secp256k1 keys + gifter (mix node 0) allowlist.
|
||||
# Committed test fixtures only — do NOT reuse in prod.
|
||||
GIFTER_AUTH_DIR="$SCRIPT_DIR/fixtures/gifter_auth"
|
||||
# shellcheck disable=SC1091
|
||||
source "$GIFTER_AUTH_DIR/keys.env"
|
||||
# shellcheck disable=SC1091
|
||||
source "$GIFTER_AUTH_DIR/addresses.env"
|
||||
GIFTER_ALLOWLIST="$ADDR_MIX1,$ADDR_MIX2,$ADDR_MIX3,$ADDR_SENDER,$ADDR_RECEIVER"
|
||||
echo " Gifter allowlist: $GIFTER_ALLOWLIST"
|
||||
|
||||
# ---------- Phase 3: Prerequisites ----------
|
||||
echo "[3/6] Verifying prerequisites..."
|
||||
# Use liblogos's native SDK pin — overriding to 1468180b breaks liblogos
|
||||
# 7df6195's source (uses old requestObject/onEvent shapes). Plugins built
|
||||
# via logos-module-builder use SDK 8bdbd13 transitively; smoke load shows
|
||||
# they're ABI-compatible with the native build.
|
||||
LOGOSCORE="${LOGOSCORE:-$(nix build github:logos-co/logos-liblogos/7df6195 --no-link --print-out-paths)/bin/logoscore}"
|
||||
RLN_MODULE="$LEZ_RLN_DIR/logos-rln-module/result-rln/lib"
|
||||
WALLET_MODULE="$LEZ_RLN_DIR/logos-rln-module/result-wallet/lib"
|
||||
|
||||
# Delivery module plugin (for mix relay nodes)
|
||||
if [ -f "$DELIVERY_MODULE_DIR/build_plugin/modules/delivery_module_plugin.$EXT" ]; then
|
||||
DELIVERY_PLUGIN="$DELIVERY_MODULE_DIR/build_plugin/modules/delivery_module_plugin.$EXT"
|
||||
else
|
||||
DELIVERY_PLUGIN="$DELIVERY_MODULE_DIR/result/lib/delivery_module_plugin.$EXT"
|
||||
fi
|
||||
|
||||
# Chat module plugin (sender/receiver). Prefer locally-built liblogoschat
|
||||
# (uses vendored Nim toolchain) over the nix result.
|
||||
CHAT_MODULE_RESULT="$CHAT_MODULE_DIR/result"
|
||||
if [ -f "$LOGOS_CHAT_DIR/build/liblogoschat.$EXT" ]; then
|
||||
CHAT_LIB="$LOGOS_CHAT_DIR/build/liblogoschat.$EXT"
|
||||
log " Using locally-built liblogoschat"
|
||||
else
|
||||
CHAT_LIB="$CHAT_MODULE_RESULT/lib/liblogoschat.$EXT"
|
||||
fi
|
||||
CHAT_PLUGIN="$CHAT_MODULE_RESULT/lib/chat_module_plugin.$EXT"
|
||||
|
||||
for check in \
|
||||
"$RLN_MODULE/liblogos_rln_module.$EXT" \
|
||||
"$WALLET_MODULE/liblogos_execution_zone_wallet_module.$EXT" \
|
||||
"$DELIVERY_PLUGIN" \
|
||||
"$CHAT_PLUGIN" \
|
||||
"$CHAT_LIB"; do
|
||||
[ -f "$check" ] || die "Missing: $check"
|
||||
done
|
||||
log " All modules present."
|
||||
|
||||
# ---------- Phase 4: Start mix nodes ----------
|
||||
echo "[4/6] Starting $NUM_NODES mix+LEZ nodes..."
|
||||
LOAD_ORDER="liblogos_execution_zone_wallet_module,liblogos_rln_module,delivery_module"
|
||||
WALLET_CALL="liblogos_execution_zone_wallet_module.open($WALLET_CONFIG,$WALLET_STORAGE)"
|
||||
BOOTSTRAP_PEER="/ip4/127.0.0.1/tcp/$BASE_TCP_PORT/p2p/${PEER_IDS[0]}"
|
||||
|
||||
# All RLN memberships are issued at runtime by the gifter on node 0 — no
|
||||
# off-chain setup_credentials / pre-registration step.
|
||||
|
||||
for i in $(seq 0 $((NUM_NODES - 1))); do
|
||||
TCP_PORT=$((BASE_TCP_PORT + i)); DISC_PORT=$((BASE_DISC_PORT + i))
|
||||
NODE_CONFIG="$STATE_DIR/node${i}_config.json"
|
||||
LOG_FILE="$STATE_DIR/node${i}.log"
|
||||
KAD_BOOTSTRAP="[]"; [ "$i" -gt 0 ] && KAD_BOOTSTRAP="[\"$BOOTSTRAP_PEER\"]"
|
||||
PEER_LIST=""
|
||||
for j in $(seq 0 $((NUM_NODES - 1))); do
|
||||
[ "$j" -eq "$i" ] && continue
|
||||
[ -n "$PEER_LIST" ] && PEER_LIST="$PEER_LIST,"
|
||||
PEER_LIST="$PEER_LIST\"/ip4/127.0.0.1/tcp/$((BASE_TCP_PORT + j))/p2p/${PEER_IDS[$j]}\""
|
||||
done
|
||||
STATIC_PEERS="[$PEER_LIST]"
|
||||
|
||||
GIFTER_FIELDS=""
|
||||
if [ "$i" -eq 0 ]; then
|
||||
GIFTER_FIELDS="\"mixGifterService\": true, \"mixGifterWalletAccount\": \"$GIFTER_ACCOUNT\", \"mixGifterAllowlist\": \"$GIFTER_ALLOWLIST\","
|
||||
else
|
||||
# KEY_MIX{1..3} align with non-gifter nodes 1..3.
|
||||
AUTH_KEY_VAR="KEY_MIX$i"
|
||||
GIFTER_FIELDS="\"mixGifterNode\": \"$BOOTSTRAP_PEER\", \"mixGifterWalletAccount\": \"$GIFTER_ACCOUNT\", \"mixGifterAuthKey\": \"${!AUTH_KEY_VAR}\","
|
||||
fi
|
||||
|
||||
cat > "$NODE_CONFIG" <<EOF
|
||||
{
|
||||
"clusterId": $CLUSTER_ID,
|
||||
"numShardsInNetwork": $NUM_SHARDS,
|
||||
"listenAddress": "127.0.0.1",
|
||||
"tcpPort": $TCP_PORT,
|
||||
"discv5UdpPort": $DISC_PORT,
|
||||
"nat": "extip:127.0.0.1",
|
||||
"extMultiAddrs": ["/ip4/127.0.0.1/tcp/$TCP_PORT"],
|
||||
"extMultiAddrsOnly": true,
|
||||
"nodekey": "${NODEKEYS[$i]}",
|
||||
"staticnodes": $STATIC_PEERS,
|
||||
"relay": true,
|
||||
"lightpush": true,
|
||||
"filter": true,
|
||||
"mix": true,
|
||||
"mixkey": "${MIXKEYS[$i]}",
|
||||
"mixOnchainLEZ": true,
|
||||
$GIFTER_FIELDS
|
||||
"enableKadDiscovery": true,
|
||||
"kadBootstrapNodes": $KAD_BOOTSTRAP,
|
||||
"peerExchange": false,
|
||||
"rendezvous": false,
|
||||
"colocationLimit": 0,
|
||||
"logLevel": "$LOG_LEVEL"
|
||||
}
|
||||
EOF
|
||||
|
||||
MDIR=$(mktemp -d); MODULES_DIRS+=("$MDIR")
|
||||
mkdir -p "$MDIR/liblogos_execution_zone_wallet_module"
|
||||
cp -L "$WALLET_MODULE/liblogos_execution_zone_wallet_module.$EXT" "$MDIR/liblogos_execution_zone_wallet_module/"
|
||||
[ -f "$WALLET_MODULE/libwallet_ffi.$EXT" ] && cp -L "$WALLET_MODULE/libwallet_ffi.$EXT" "$MDIR/liblogos_execution_zone_wallet_module/"
|
||||
echo "{\"name\":\"liblogos_execution_zone_wallet_module\",\"version\":\"1.0.0\",\"type\":\"core\",\"main\":{\"$PLATFORM\":\"liblogos_execution_zone_wallet_module.$EXT\"},\"dependencies\":[],\"capabilities\":[]}" > "$MDIR/liblogos_execution_zone_wallet_module/manifest.json"
|
||||
mkdir -p "$MDIR/liblogos_rln_module"
|
||||
cp -L "$RLN_MODULE/liblogos_rln_module.$EXT" "$MDIR/liblogos_rln_module/"
|
||||
cp -L "$RLN_MODULE/liblez_rln_ffi.$EXT" "$MDIR/liblogos_rln_module/" 2>/dev/null || true
|
||||
echo "{\"name\":\"liblogos_rln_module\",\"version\":\"1.0.0\",\"type\":\"core\",\"main\":{\"$PLATFORM\":\"liblogos_rln_module.$EXT\"},\"dependencies\":[\"liblogos_execution_zone_wallet_module\"],\"capabilities\":[]}" > "$MDIR/liblogos_rln_module/manifest.json"
|
||||
mkdir -p "$MDIR/delivery_module"
|
||||
cp -L "$DELIVERY_PLUGIN" "$MDIR/delivery_module/"
|
||||
if [ -f "$DELIVERY_DIR/build/liblogosdelivery.$EXT" ]; then
|
||||
cp -L "$DELIVERY_DIR/build/liblogosdelivery.$EXT" "$MDIR/delivery_module/"
|
||||
else
|
||||
cp -L "$DELIVERY_MODULE_DIR/result/lib/liblogosdelivery.$EXT" "$MDIR/delivery_module/" 2>/dev/null || true
|
||||
fi
|
||||
for pq in "$DELIVERY_MODULE_DIR"/result/lib/libpq*; do [ -f "$pq" ] && cp -L "$pq" "$MDIR/delivery_module/"; done
|
||||
echo "{\"name\":\"delivery_module\",\"version\":\"1.0.0\",\"type\":\"core\",\"main\":{\"$PLATFORM\":\"delivery_module_plugin.$EXT\"},\"dependencies\":[],\"capabilities\":[]}" > "$MDIR/delivery_module/manifest.json"
|
||||
|
||||
log " Starting node $i (port $TCP_PORT)..."
|
||||
(cd "$STATE_DIR" && TMPDIR=/tmp "$LOGOSCORE" -m "$MDIR" -l "$LOAD_ORDER" \
|
||||
-c "$WALLET_CALL" \
|
||||
-c "delivery_module.createNode(@$NODE_CONFIG)" \
|
||||
-c "delivery_module.start()" \
|
||||
-c "delivery_module.setRlnConfig($CONFIG_ACCOUNT,$i)" \
|
||||
-c "delivery_module.subscribe($CONTENT_TOPIC)" \
|
||||
</dev/null >"$LOG_FILE" 2>&1) &
|
||||
EXPECTED_CALLS=5
|
||||
NODE_PID=$!; INSTANCE_PIDS+=($NODE_PID)
|
||||
wait_method_calls "$LOG_FILE" "$EXPECTED_CALLS" 90 1 || true
|
||||
if [ "${N:-0}" -ge "$EXPECTED_CALLS" ]; then
|
||||
log " Node $i ready ($N/$EXPECTED_CALLS calls) PID: $NODE_PID"
|
||||
else
|
||||
echo " WARNING: Node $i: $N/$EXPECTED_CALLS calls"
|
||||
fi
|
||||
sleep "$NODE_STARTUP_SLEEP"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# ---------- Phase 5: Chat module sender/receiver ----------
|
||||
echo "[5/6] Starting chat module instances..."
|
||||
|
||||
# Wait for all nodes ready (gifter registrations finalize during startup).
|
||||
for i in $(seq 0 $((NUM_NODES - 1))); do
|
||||
wait_method_calls "$STATE_DIR/node${i}.log" 5 120 2 || true
|
||||
done
|
||||
|
||||
# Wait for RLN root convergence. Mix pool is seeded from each node's
|
||||
# mixNodes config (processBootNodes), so kademlia isn't required for routing —
|
||||
# the kad-peer count is logged only for diagnostics.
|
||||
echo " Waiting for kademlia propagation + RLN convergence..."
|
||||
KADEMLIA_T0=$SECONDS
|
||||
# Block chat startup until every mix hop has RLN credentials confirmed
|
||||
# on-chain — unregistered hops drop sphinx packets (Plugin not ready).
|
||||
REQUIRED_CLIENT_REGS=$((NUM_NODES - 1))
|
||||
while true; do
|
||||
ELAPSED=$((SECONDS - KADEMLIA_T0))
|
||||
GR=$(sed 's/\x1b\[[0-9;]*m//g' "$STATE_DIR/node0.log" 2>/dev/null | grep -c "RLN gifter registration succeeded" || true); GR=${GR:-0}
|
||||
SELF=$(sed 's/\x1b\[[0-9;]*m//g' "$STATE_DIR/node0.log" 2>/dev/null | grep -c "Gifter self-registered as mix relay" || true); SELF=${SELF:-0}
|
||||
LR=0
|
||||
MIX_PEERS_PER_NODE=""
|
||||
for i in $(seq 0 $((NUM_NODES - 1))); do
|
||||
LOG="$STATE_DIR/node${i}.log"
|
||||
L=$(sed 's/\x1b\[[0-9;]*m//g' "$LOG" 2>/dev/null | grep -c "Polled valid roots\|Fetched roots from\|valid_roots\|OnchainLEZGroupManager initialized\|Wired LEZ callbacks" || true)
|
||||
LR=$((LR + L))
|
||||
MP=$(sed 's/\x1b\[[0-9;]*m//g' "$LOG" 2>/dev/null | grep -c "mix peer added via kademlia lookup" || true); MP=${MP:-0}
|
||||
MIX_PEERS_PER_NODE="${MIX_PEERS_PER_NODE}n${i}=${MP} "
|
||||
done
|
||||
if [ "$ELAPSED" -ge "$KADEMLIA_MIN_WAIT" ] && [ "$GR" -ge "$REQUIRED_CLIENT_REGS" ] && [ "$SELF" -ge 1 ] && [ "$LR" -ge 40 ]; then break; fi
|
||||
[ "$ELAPSED" -ge "$KADEMLIA_HARD_CAP" ] && break
|
||||
sleep 1
|
||||
done
|
||||
log " Kademlia ready after $((SECONDS - KADEMLIA_T0))s ($GR/$REQUIRED_CLIENT_REGS client regs, self=$SELF, $LR LEZ root events, kad mix peers: $MIX_PEERS_PER_NODE)"
|
||||
|
||||
RECEIVER_LOG="$STATE_DIR/chat_receiver.log"
|
||||
SENDER_LOG="$STATE_DIR/chat_sender.log"
|
||||
|
||||
# Build mix node list (multiaddr:mixPubKey) for chat config.
|
||||
MIXNODE_LIST=""
|
||||
for j in $(seq 0 $((NUM_NODES - 1))); do
|
||||
[ -n "$MIXNODE_LIST" ] && MIXNODE_LIST="$MIXNODE_LIST,"
|
||||
MIXNODE_LIST="$MIXNODE_LIST\"/ip4/127.0.0.1/tcp/$((BASE_TCP_PORT + j))/p2p/${PEER_IDS[$j]}:${MIX_PUBKEYS[$j]}\""
|
||||
done
|
||||
|
||||
# Stage wallet + RLN + chat modules for a logoscore instance.
|
||||
stage_chat_module() {
|
||||
local MDIR=$1
|
||||
mkdir -p "$MDIR/liblogos_execution_zone_wallet_module"
|
||||
cp -L "$WALLET_MODULE/liblogos_execution_zone_wallet_module.$EXT" "$MDIR/liblogos_execution_zone_wallet_module/"
|
||||
[ -f "$WALLET_MODULE/libwallet_ffi.$EXT" ] && cp -L "$WALLET_MODULE/libwallet_ffi.$EXT" "$MDIR/liblogos_execution_zone_wallet_module/"
|
||||
echo "{\"name\":\"liblogos_execution_zone_wallet_module\",\"version\":\"1.0.0\",\"type\":\"core\",\"main\":{\"$PLATFORM\":\"liblogos_execution_zone_wallet_module.$EXT\"},\"dependencies\":[],\"capabilities\":[]}" > "$MDIR/liblogos_execution_zone_wallet_module/manifest.json"
|
||||
|
||||
mkdir -p "$MDIR/liblogos_rln_module"
|
||||
cp -L "$RLN_MODULE/liblogos_rln_module.$EXT" "$MDIR/liblogos_rln_module/"
|
||||
cp -L "$RLN_MODULE/liblez_rln_ffi.$EXT" "$MDIR/liblogos_rln_module/" 2>/dev/null || true
|
||||
echo "{\"name\":\"liblogos_rln_module\",\"version\":\"1.0.0\",\"type\":\"core\",\"main\":{\"$PLATFORM\":\"liblogos_rln_module.$EXT\"},\"dependencies\":[\"liblogos_execution_zone_wallet_module\"],\"capabilities\":[]}" > "$MDIR/liblogos_rln_module/manifest.json"
|
||||
|
||||
mkdir -p "$MDIR/chat_module"
|
||||
cp -L "$CHAT_PLUGIN" "$MDIR/chat_module/"
|
||||
cp -L "$CHAT_LIB" "$MDIR/chat_module/"
|
||||
echo "{\"name\":\"chat_module\",\"version\":\"1.0.0\",\"type\":\"core\",\"main\":{\"$PLATFORM\":\"chat_module_plugin.$EXT\"},\"dependencies\":[],\"capabilities\":[]}" > "$MDIR/chat_module/manifest.json"
|
||||
}
|
||||
|
||||
CHAT_LOAD_ORDER="liblogos_execution_zone_wallet_module,liblogos_rln_module,chat_module"
|
||||
|
||||
# --- Receiver ---
|
||||
RECV_MDIR=$(mktemp -d); MODULES_DIRS+=("$RECV_MDIR")
|
||||
stage_chat_module "$RECV_MDIR"
|
||||
|
||||
RECV_CONFIG="$STATE_DIR/chat_receiver_config.json"
|
||||
# Static peers so chat nodes join the relay mesh.
|
||||
CHAT_STATIC_PEERS=""
|
||||
for j in $(seq 0 $((NUM_NODES - 1))); do
|
||||
[ -n "$CHAT_STATIC_PEERS" ] && CHAT_STATIC_PEERS="$CHAT_STATIC_PEERS,"
|
||||
CHAT_STATIC_PEERS="$CHAT_STATIC_PEERS\"/ip4/127.0.0.1/tcp/$((BASE_TCP_PORT + j))/p2p/${PEER_IDS[$j]}\""
|
||||
done
|
||||
cat > "$RECV_CONFIG" <<EOF
|
||||
{
|
||||
"name": "receiver",
|
||||
"clusterId": $CLUSTER_ID,
|
||||
"shardId": 0,
|
||||
"port": $CHAT_RECV_PORT,
|
||||
"mixEnabled": true,
|
||||
"mixNodes": [$MIXNODE_LIST],
|
||||
"destPeerAddr": "$BOOTSTRAP_PEER",
|
||||
"minMixPoolSize": 4,
|
||||
"gifterNodeAddr": "$BOOTSTRAP_PEER",
|
||||
"gifterAuthKey": "$KEY_RECEIVER",
|
||||
"staticPeers": [$CHAT_STATIC_PEERS]
|
||||
}
|
||||
EOF
|
||||
|
||||
TEST_MSG_HEX=$(printf '%s' "$TEST_MESSAGE_PREFIX" | xxd -p | tr -d '\n')
|
||||
|
||||
log " Starting receiver..."
|
||||
(cd "$STATE_DIR" && TMPDIR=/tmp "$LOGOSCORE" -m "$RECV_MDIR" -l "$CHAT_LOAD_ORDER" \
|
||||
-c "$WALLET_CALL" \
|
||||
-c "chat_module.initChat(@$RECV_CONFIG)" \
|
||||
-c "chat_module.setEventCallback()" \
|
||||
-c "chat_module.startChat()" \
|
||||
-c "chat_module.setRlnConfig($CONFIG_ACCOUNT,5)" \
|
||||
-c "chat_module.createIntroBundle()" \
|
||||
</dev/null >"$RECEIVER_LOG" 2>&1) &
|
||||
RECEIVER_PID=$!; INSTANCE_PIDS+=($RECEIVER_PID)
|
||||
log " Receiver PID: $RECEIVER_PID"
|
||||
|
||||
RECV_EXPECTED=6
|
||||
wait_method_calls "$RECEIVER_LOG" "$RECV_EXPECTED" 180 2 || true
|
||||
log " Receiver method calls: $N/$RECV_EXPECTED"
|
||||
|
||||
# Extract intro bundle (emitted as chatCreateIntroBundleResult).
|
||||
INTRO_BUNDLE=""
|
||||
for t in $(seq 1 30); do
|
||||
INTRO_BUNDLE=$(grep -oE 'logos_chatintro_[A-Za-z0-9_-]+' "$RECEIVER_LOG" 2>/dev/null | head -1 || true)
|
||||
[ -n "$INTRO_BUNDLE" ] && break; sleep 2
|
||||
done
|
||||
if [ -n "$INTRO_BUNDLE" ]; then
|
||||
log " Receiver intro bundle: ${INTRO_BUNDLE:0:40}..."
|
||||
else
|
||||
log " WARNING: Could not extract intro bundle from receiver log"
|
||||
fi
|
||||
|
||||
# Wait for async startChat (Waku client started) + a floor for the filter
|
||||
# subscription to propagate through the relay mesh.
|
||||
echo " Waiting for receiver to join mix network..."
|
||||
JOIN_T0=$SECONDS
|
||||
while true; do
|
||||
ELAPSED=$((SECONDS - JOIN_T0))
|
||||
RS=$(grep -c "Waku client started" "$RECEIVER_LOG" 2>/dev/null || true); RS=${RS:-0}
|
||||
[ "$ELAPSED" -ge "$RECEIVER_MIN_WAIT" ] && [ "$RS" -ge 1 ] && break
|
||||
[ "$ELAPSED" -ge 60 ] && break
|
||||
sleep 1
|
||||
done
|
||||
log " Receiver joined after $((SECONDS - JOIN_T0))s"
|
||||
|
||||
# --- Sender ---
|
||||
SEND_MDIR=$(mktemp -d); MODULES_DIRS+=("$SEND_MDIR")
|
||||
stage_chat_module "$SEND_MDIR"
|
||||
|
||||
SEND_CONFIG="$STATE_DIR/chat_sender_config.json"
|
||||
cat > "$SEND_CONFIG" <<EOF
|
||||
{
|
||||
"name": "sender",
|
||||
"clusterId": $CLUSTER_ID,
|
||||
"shardId": 0,
|
||||
"port": $CHAT_SEND_PORT,
|
||||
"mixEnabled": true,
|
||||
"mixNodes": [$MIXNODE_LIST],
|
||||
"destPeerAddr": "$BOOTSTRAP_PEER",
|
||||
"minMixPoolSize": 4,
|
||||
"gifterNodeAddr": "$BOOTSTRAP_PEER",
|
||||
"gifterAuthKey": "$KEY_SENDER",
|
||||
"staticPeers": [$CHAT_STATIC_PEERS]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Sender -c calls: init/start/setRlnConfig, then newPrivateConversation if
|
||||
# we have the receiver's intro bundle.
|
||||
SENDER_CALLS="-c \"$WALLET_CALL\""
|
||||
SENDER_CALLS="$SENDER_CALLS -c \"chat_module.initChat(@$SEND_CONFIG)\""
|
||||
SENDER_CALLS="$SENDER_CALLS -c \"chat_module.setEventCallback()\""
|
||||
SENDER_CALLS="$SENDER_CALLS -c \"chat_module.startChat()\""
|
||||
# RLN leaf layout: 0-3 mix nodes, 4 gifter-reserved, 5 receiver, 6 sender.
|
||||
SENDER_CALLS="$SENDER_CALLS -c \"chat_module.setRlnConfig($CONFIG_ACCOUNT,6)\""
|
||||
if [ -n "$INTRO_BUNDLE" ]; then
|
||||
SENDER_CALLS="$SENDER_CALLS -c \"chat_module.newPrivateConversation($INTRO_BUNDLE,$TEST_MSG_HEX)\""
|
||||
fi
|
||||
|
||||
log " Starting sender..."
|
||||
eval "(cd \"$STATE_DIR\" && TMPDIR=/tmp \"$LOGOSCORE\" -m \"$SEND_MDIR\" -l \"$CHAT_LOAD_ORDER\" \
|
||||
$SENDER_CALLS \
|
||||
</dev/null >\"$SENDER_LOG\" 2>&1) &"
|
||||
SENDER_PID=$!; INSTANCE_PIDS+=($SENDER_PID)
|
||||
log " Sender PID: $SENDER_PID"
|
||||
|
||||
SEND_EXPECTED=6
|
||||
[ -n "$INTRO_BUNDLE" ] && SEND_EXPECTED=7
|
||||
wait_method_calls "$SENDER_LOG" "$SEND_EXPECTED" 180 2 || true
|
||||
|
||||
# On slower systems (Docker/ARM) the sender's async gifter registration may
|
||||
# trail newPrivateConversation. Wait for RLN readiness; the Nim async code
|
||||
# retries newPrivateConversation once credentials land.
|
||||
echo " Waiting for sender RLN readiness..."
|
||||
SENDER_RLN_T0=$SECONDS
|
||||
for t in $(seq 1 60); do
|
||||
SG=$(sed 's/\x1b\[[0-9;]*m//g' "$SENDER_LOG" 2>/dev/null | grep -c "Registered via RLN gifter\|Waku client started" || true)
|
||||
[ "${SG:-0}" -ge 2 ] && break
|
||||
sleep 1
|
||||
done
|
||||
log " Sender RLN ready after $((SECONDS - SENDER_RLN_T0))s"
|
||||
N=$(grep -c '^Method call successful' "$SENDER_LOG" 2>/dev/null || true); N=${N:-0}
|
||||
log " Sender method calls: $N/$SEND_EXPECTED"
|
||||
|
||||
# Poll receiver log for delivery.
|
||||
echo " Waiting for message delivery via mix..."
|
||||
DELIVERY_T0=$SECONDS
|
||||
for t in $(seq 1 $DELIVERY_TIMEOUT); do
|
||||
RM=$(grep -c "chatNewMessage\|chatNewConversation\|New Message\|new_message" "$RECEIVER_LOG" 2>/dev/null || true); RM=${RM:-0}
|
||||
[ "$RM" -ge 1 ] && break
|
||||
sleep 1
|
||||
done
|
||||
log " Delivery check after $((SECONDS - DELIVERY_T0))s (messages: $RM)"
|
||||
|
||||
echo ""
|
||||
|
||||
# ---------- Phase 6: Verify ----------
|
||||
echo "[6/6] Verification"; echo ""
|
||||
PASS=0; FAIL=0
|
||||
check() { local c=$1 d=$2; if eval "$c"; then echo " PASS: $d"; PASS=$((PASS+1)); else echo " FAIL: $d"; FAIL=$((FAIL+1)); fi; }
|
||||
|
||||
echo " --- logos-core mix nodes ---"
|
||||
for i in $(seq 0 $((NUM_NODES - 1))); do
|
||||
M=$(sed 's/\x1b\[[0-9;]*m//g' "$STATE_DIR/node${i}.log" 2>/dev/null | grep -c "mounting mix protocol" || true)
|
||||
check "[ ${M:-0} -ge 1 ]" "Node $i mounted mix ($M)"
|
||||
done
|
||||
echo ""
|
||||
|
||||
echo " --- RLN gifter ---"
|
||||
GIFTER_MOUNTED=$(sed 's/\x1b\[[0-9;]*m//g' "$STATE_DIR/node0.log" 2>/dev/null | grep -c "RLN gifter service mounted" || true)
|
||||
check "[ ${GIFTER_MOUNTED:-0} -ge 1 ]" "Node 0 gifter service mounted ($GIFTER_MOUNTED)"
|
||||
echo ""
|
||||
|
||||
echo " --- LEZ RLN ---"
|
||||
LEZ_ROOTS=0
|
||||
for i in $(seq 0 $((NUM_NODES - 1))); do
|
||||
R=$(sed 's/\x1b\[[0-9;]*m//g' "$STATE_DIR/node${i}.log" 2>/dev/null | grep -c "Polled valid roots\|Fetched roots from\|valid_roots\|OnchainLEZGroupManager initialized\|Wired LEZ callbacks" || true)
|
||||
LEZ_ROOTS=$((LEZ_ROOTS + R))
|
||||
done
|
||||
check "[ $LEZ_ROOTS -ge 1 ]" "LEZ RLN active ($LEZ_ROOTS events across nodes)"
|
||||
echo ""
|
||||
|
||||
echo " --- chat module ---"
|
||||
RECV_INIT=$(grep -c "chatInitResult\|Chat context created" "$RECEIVER_LOG" 2>/dev/null || true)
|
||||
check "[ ${RECV_INIT:-0} -ge 1 ]" "Receiver initialized ($RECV_INIT)"
|
||||
RECV_START=$(grep -c "chatStartResult\|Waku client started" "$RECEIVER_LOG" 2>/dev/null || true)
|
||||
check "[ ${RECV_START:-0} -ge 1 ]" "Receiver started ($RECV_START)"
|
||||
SEND_INIT=$(grep -c "chatInitResult\|Chat context created" "$SENDER_LOG" 2>/dev/null || true)
|
||||
check "[ ${SEND_INIT:-0} -ge 1 ]" "Sender initialized ($SEND_INIT)"
|
||||
SEND_START=$(grep -c "chatStartResult\|Waku client started" "$SENDER_LOG" 2>/dev/null || true)
|
||||
check "[ ${SEND_START:-0} -ge 1 ]" "Sender started ($SEND_START)"
|
||||
|
||||
RECV_MIX=$(grep -c "mounting mix protocol\|Wired LEZ callbacks" "$RECEIVER_LOG" 2>/dev/null || true)
|
||||
check "[ ${RECV_MIX:-0} -ge 1 ]" "Receiver mounted mix+LEZ ($RECV_MIX)"
|
||||
SEND_MIX=$(grep -c "mounting mix protocol\|Wired LEZ callbacks" "$SENDER_LOG" 2>/dev/null || true)
|
||||
check "[ ${SEND_MIX:-0} -ge 1 ]" "Sender mounted mix+LEZ ($SEND_MIX)"
|
||||
|
||||
RECV_BUNDLE=$(grep -c "logos_chatintro_" "$RECEIVER_LOG" 2>/dev/null || true)
|
||||
check "[ ${RECV_BUNDLE:-0} -ge 1 ]" "Receiver created intro bundle ($RECV_BUNDLE)"
|
||||
echo ""
|
||||
|
||||
echo " --- message exchange ---"
|
||||
SEND_MSG=$(grep -c "chatNewPrivateConversationResult\|chatSendMessageResult\|Message sent via mix" "$SENDER_LOG" 2>/dev/null || true)
|
||||
check "[ ${SEND_MSG:-0} -ge 1 ]" "Sender sent message ($SEND_MSG)"
|
||||
RECV_MSG=$(grep -c "chatNewMessage\|chatNewConversation\|New Message\|new_message" "$RECEIVER_LOG" 2>/dev/null || true)
|
||||
check "[ ${RECV_MSG:-0} -ge 1 ]" "Receiver received message ($RECV_MSG)"
|
||||
|
||||
echo ""; echo " =========================================="
|
||||
if [ "$FAIL" -eq 0 ]; then echo " ALL $PASS CHECKS PASSED"; EXIT_CODE=0
|
||||
else echo " $FAIL FAILED, $PASS passed"; EXIT_CODE=1; fi
|
||||
echo " =========================================="
|
||||
108
simulations/mix_lez_chat/setup_and_run.sh
Executable file
108
simulations/mix_lez_chat/setup_and_run.sh
Executable file
@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-shot: init nested submodules, build all modules, run mix+LEZ chat sim.
|
||||
#
|
||||
# Prerequisites: nix (with flakes), Docker, cargo-risczero.
|
||||
#
|
||||
# Environment variables:
|
||||
# CHAT_MODULE_DIR — path to logos-chat-module checkout (default: ../logos-chat-module)
|
||||
# CHAT_MODULE_REPO — git URL to clone if CHAT_MODULE_DIR doesn't exist
|
||||
# CHAT_MODULE_BRANCH — branch to clone (default: feat/logos-delivery)
|
||||
# SIM_* — simulation parameters, see run_simulation.sh / README.md
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
CHAT_MODULE_DIR="${CHAT_MODULE_DIR:-$ROOT/../logos-chat-module}"
|
||||
CHAT_MODULE_REPO="${CHAT_MODULE_REPO:-git@github.com:adklempner/logos-chat-module.git}"
|
||||
CHAT_MODULE_BRANCH="${CHAT_MODULE_BRANCH:-feat/logos-delivery}"
|
||||
|
||||
log() { echo "[$(date '+%H:%M:%S')] $*"; }
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
# Docker/CI: nix can't sandbox inside containers. Remove /homeless-shelter
|
||||
# (nix's sandbox HOME stub) and export NIX_CONFIG to disable sandboxing.
|
||||
if [ -f /.dockerenv ] || grep -q docker /proc/1/cgroup 2>/dev/null; then
|
||||
rmdir /homeless-shelter 2>/dev/null || true
|
||||
export NIX_CONFIG="sandbox = false
|
||||
${NIX_CONFIG:-}"
|
||||
fi
|
||||
|
||||
# On Linux with nix, bindgen (used by rocksdb/sequencer) needs LIBCLANG_PATH
|
||||
# and system headers. This covers both Docker and native nix-on-Linux.
|
||||
if [ "$(uname -s)" = "Linux" ] && [ -d /nix/store ] && [ -z "${LIBCLANG_PATH:-}" ]; then
|
||||
CLANG_SO=$(find /nix/store -maxdepth 3 -name 'libclang.so' 2>/dev/null | head -1 || true)
|
||||
if [ -n "$CLANG_SO" ]; then
|
||||
export LIBCLANG_PATH=$(dirname "$CLANG_SO")
|
||||
STDBOOL=$(find "$LIBCLANG_PATH" -maxdepth 5 -name 'stdbool.h' 2>/dev/null | head -1 || true)
|
||||
[ -n "$STDBOOL" ] && export BINDGEN_EXTRA_CLANG_ARGS="-I$(dirname "$STDBOOL")"
|
||||
fi
|
||||
log "Docker detected — disabled nix sandbox, LIBCLANG_PATH=$LIBCLANG_PATH"
|
||||
fi
|
||||
|
||||
# 1. Init submodules. Top-level first (non-recursive to avoid circular refs
|
||||
# in vendor/logos-lez-rln), then nwaku/nimbus-build-system recursively,
|
||||
# then logos-lez-rln's nested submodules selectively.
|
||||
log "Initializing top-level submodules..."
|
||||
git submodule update --init
|
||||
(cd vendor/nwaku && git submodule update --init --recursive)
|
||||
(cd vendor/nimbus-build-system && git submodule update --init --recursive)
|
||||
|
||||
# Init nested submodules inside vendor/logos-lez-rln non-recursively
|
||||
# (recursive init hits circular submodule references).
|
||||
# Preserve pre-built guest binaries across submodule reset (they're
|
||||
# architecture-independent RISC-V ELFs that take ~10min to rebuild).
|
||||
GUEST_DIR="vendor/logos-lez-rln/lez-rln/methods/guest/target/riscv32im-risc0-zkvm-elf/docker"
|
||||
# Check repo tree first, then /tmp/guest-bins/ (staged by run_in_docker.sh)
|
||||
GUEST_TMP=""
|
||||
if [ -f "$GUEST_DIR/rln_registration.bin" ]; then
|
||||
GUEST_TMP=$(mktemp -d)
|
||||
cp "$GUEST_DIR"/*.bin "$GUEST_TMP/"
|
||||
log "Preserved guest binaries from repo"
|
||||
elif [ -f "/tmp/guest-bins/rln_registration.bin" ]; then
|
||||
GUEST_TMP="/tmp/guest-bins"
|
||||
log "Using guest binaries from /tmp/guest-bins/"
|
||||
fi
|
||||
log "Initializing nested submodules in vendor/logos-lez-rln..."
|
||||
# Slim default: only logos-delivery-module (+ its nested logos-delivery) is
|
||||
# required to build the chat/delivery/mix Nim modules. lssa and
|
||||
# logos-execution-zone-module are fetched via flake.nix from GitHub when nix
|
||||
# builds the wallet/rln modules, so the local clones aren't needed unless the
|
||||
# dev is running a local sequencer (SIM_NETWORK=local) or hacking those repos.
|
||||
SUBMODS=(logos-delivery-module)
|
||||
if [ "${SIM_NETWORK:-testnet}" = "local" ] || [ "${SIM_FULL_SUBMODS:-0}" = "1" ]; then
|
||||
SUBMODS+=(lssa logos-execution-zone-module)
|
||||
fi
|
||||
(cd vendor/logos-lez-rln && \
|
||||
git submodule update --init "${SUBMODS[@]}" && \
|
||||
git checkout -- . && \
|
||||
for d in "${SUBMODS[@]}"; do \
|
||||
(cd "$d" && git checkout -- .); \
|
||||
done && \
|
||||
cd logos-delivery-module && git submodule update --init vendor/logos-delivery)
|
||||
if [ -d "${GUEST_TMP:-}" ]; then
|
||||
mkdir -p "$GUEST_DIR"
|
||||
cp "$GUEST_TMP"/*.bin "$GUEST_DIR/"
|
||||
rm -rf "$GUEST_TMP"
|
||||
log "Restored guest binaries"
|
||||
fi
|
||||
|
||||
# 2. Build LEZ modules (RLN, wallet, delivery plugin, guest zkVM binaries).
|
||||
log "Building LEZ modules via vendor/logos-lez-rln/build_all.sh..."
|
||||
bash vendor/logos-lez-rln/build_all.sh
|
||||
|
||||
# 3. Build liblogoschat (Nim shared library).
|
||||
log "Building liblogoschat..."
|
||||
make update
|
||||
make liblogoschat
|
||||
|
||||
# 4. Clone and build logos-chat-module (C++ Qt plugin).
|
||||
if [ ! -d "$CHAT_MODULE_DIR" ]; then
|
||||
log "Cloning logos-chat-module to $CHAT_MODULE_DIR..."
|
||||
git clone -b "$CHAT_MODULE_BRANCH" "$CHAT_MODULE_REPO" "$CHAT_MODULE_DIR"
|
||||
fi
|
||||
log "Building logos-chat-module..."
|
||||
(cd "$CHAT_MODULE_DIR" && nix build)
|
||||
|
||||
# 5. Run the simulation.
|
||||
log "Starting mix+LEZ chat simulation..."
|
||||
exec bash "$ROOT/simulations/mix_lez_chat/run_simulation.sh" --fresh "$@"
|
||||
@ -145,10 +145,17 @@ proc createIntroBundle*(self: ChatClient): seq[byte] =
|
||||
notice "IntroBundleCreated", client = self.getId(),
|
||||
bundle = result
|
||||
|
||||
proc sendPayloadWatched(
|
||||
ds: WakuClient, contentTopic: string, data: seq[byte]
|
||||
) {.async.} =
|
||||
try:
|
||||
await ds.sendBytes(contentTopic, data)
|
||||
except CatchableError as e:
|
||||
error "sendBytes failed", contentTopic = contentTopic, err = e.msg
|
||||
|
||||
proc sendPayloads(ds: WakuClient, payloads: seq[PayloadResult]) =
|
||||
for payload in payloads:
|
||||
# TODO: (P2) surface errors
|
||||
discard ds.sendBytes(payload.address, payload.data)
|
||||
asyncSpawn sendPayloadWatched(ds, payload.address, payload.data)
|
||||
|
||||
|
||||
#################################################
|
||||
@ -237,7 +244,7 @@ proc messageQueueConsumer(client: ChatClient) {.async.} =
|
||||
proc start*(client: ChatClient) {.async.} =
|
||||
## Start `ChatClient` and listens for incoming messages.
|
||||
client.ds.addDispatchQueue(client.inboundQueue)
|
||||
asyncSpawn client.ds.start()
|
||||
await client.ds.start()
|
||||
|
||||
client.isRunning = true
|
||||
|
||||
|
||||
@ -2,10 +2,18 @@ import
|
||||
chronicles,
|
||||
chronos,
|
||||
confutils,
|
||||
eth/common/addresses as eth_addresses,
|
||||
eth/common/keys as eth_keys,
|
||||
eth/p2p/discoveryv5/enr as eth_enr,
|
||||
libp2p/crypto/crypto,
|
||||
libp2p/crypto/curve25519,
|
||||
libp2p/peerid,
|
||||
std/random,
|
||||
libp2p/protocols/mix,
|
||||
libp2p/protocols/mix/curve25519 as mix_curve25519,
|
||||
libp2p/protocols/mix/entry_connection,
|
||||
libp2p/protocols/mix/mix_protocol as mix_proto,
|
||||
nimcrypto/utils as ncrutils,
|
||||
std/[random, strutils],
|
||||
stew/byteutils,
|
||||
strformat,
|
||||
waku/[
|
||||
@ -13,13 +21,22 @@ import
|
||||
common/enr as common_enr,
|
||||
node/peer_manager,
|
||||
waku_core,
|
||||
waku_core/codecs,
|
||||
waku_node,
|
||||
waku_enr,
|
||||
waku_mix/protocol as waku_mix_protocol,
|
||||
waku_mix/logos_core_client as mix_lez_client,
|
||||
waku_lightpush/client as lightpush_client,
|
||||
discovery/waku_discv5,
|
||||
discovery/waku_dnsdisc,
|
||||
factory/builder,
|
||||
waku_filter_v2/client,
|
||||
]
|
||||
waku_rln_relay/rln_gifter/client as rln_gifter_client,
|
||||
waku_rln_relay/rln_gifter/protocol as rln_gifter_protocol,
|
||||
],
|
||||
mix_rln_spam_protection/onchain_group_manager,
|
||||
mix_rln_spam_protection/rln_interface as mix_rln_interface,
|
||||
mix_rln_spam_protection/spam_protection
|
||||
|
||||
|
||||
logScope:
|
||||
@ -40,6 +57,7 @@ proc toChatPayload*(msg: WakuMessage, pubsubTopic: PubsubTopic): ChatPayload =
|
||||
const
|
||||
# Placeholder
|
||||
FilterContentTopic = ContentTopic("/chatsdk/test/proto")
|
||||
LibchatDeliveryAddress = ContentTopic("delivery_address")
|
||||
|
||||
## Logos.dev Fleet ENRs
|
||||
|
||||
@ -77,12 +95,18 @@ type QueueRef* = ref object
|
||||
|
||||
|
||||
type WakuConfig* = object
|
||||
nodekey*: crypto.PrivateKey # TODO: protect key exposure
|
||||
nodekey*: crypto.PrivateKey # TODO: protect key exposure
|
||||
port*: uint16
|
||||
clusterId*: uint16
|
||||
shardId*: seq[uint16]
|
||||
pubsubTopic*: string
|
||||
staticPeers*: seq[string]
|
||||
mixEnabled*: bool
|
||||
mixNodes*: seq[string]
|
||||
destPeerAddr*: string
|
||||
minMixPoolSize*: int
|
||||
gifterNodeAddr*: string
|
||||
gifterAuthKey*: string
|
||||
|
||||
type
|
||||
WakuClient* = ref object
|
||||
@ -90,6 +114,8 @@ type
|
||||
node*: WakuNode
|
||||
dispatchQueues: seq[QueueRef]
|
||||
staticPeerList: seq[RemotePeerInfo]
|
||||
mixReady*: bool
|
||||
destPeerId: PeerId
|
||||
|
||||
|
||||
proc DefaultConfig*(): WakuConfig =
|
||||
@ -100,17 +126,121 @@ proc DefaultConfig*(): WakuConfig =
|
||||
|
||||
result = WakuConfig(nodeKey: nodeKey, port: port, clusterId: clusterId,
|
||||
shardId: @[shardId], pubsubTopic: &"/waku/2/rs/{clusterId}/{shardId}",
|
||||
staticPeers: LogosDevStaticPeers)
|
||||
staticPeers: LogosDevStaticPeers,
|
||||
mixEnabled: false, mixNodes: @[], destPeerAddr: "", minMixPoolSize: 4,
|
||||
gifterNodeAddr: "",
|
||||
gifterAuthKey: "")
|
||||
|
||||
|
||||
proc sendBytes*(client: WakuClient, contentTopic: string,
|
||||
bytes: seq[byte]) {.async.} =
|
||||
|
||||
let msg = WakuMessage(contentTopic: contentTopic, payload: bytes)
|
||||
let res = await client.node.publish(some(PubsubTopic(client.cfg.pubsubTopic)), msg)
|
||||
if res.isErr:
|
||||
error "Failed to Publish", err = res.error,
|
||||
pubsubTopic = client.cfg.pubsubTopic
|
||||
|
||||
if client.cfg.mixEnabled:
|
||||
# Wait for mix pool to be ready before sending
|
||||
while not client.mixReady:
|
||||
info "Waiting for mix pool before sending..."
|
||||
await sleepAsync(2.seconds)
|
||||
|
||||
# Wait for RLN spam protection to be ready (roots + proofs fetched from LEZ)
|
||||
if not client.node.wakuMix.isNil:
|
||||
var attempts = 0
|
||||
while not client.node.wakuMix.mixRlnSpamProtection.isReady() and attempts < 45:
|
||||
if attempts mod 5 == 0:
|
||||
info "Waiting for RLN spam protection readiness...", attempt = attempts
|
||||
await sleepAsync(2.seconds)
|
||||
attempts += 1
|
||||
if client.node.wakuMix.mixRlnSpamProtection.isReady():
|
||||
let gm = client.node.wakuMix.mixRlnSpamProtection.groupManager
|
||||
if gm of OnchainLEZGroupManager:
|
||||
let lezGm = OnchainLEZGroupManager(gm)
|
||||
let pollMs = lezGm.getPollInterval().milliseconds
|
||||
let stableMs = max(pollMs * 2, 5000)
|
||||
let probeMs = max(pollMs div 4, 1000)
|
||||
# The gifter serializes registrations through a worker; on slow
|
||||
# chains the chat sender may sit at the tail of a queue of up to
|
||||
# ~6 other registrations, each waiting up to confirmDeadlineMs
|
||||
# (300s) for chain commit. Budget enough headroom that we don't
|
||||
# publish against an un-corrected optimistic leaf.
|
||||
let deadlineMs = 1_500_000
|
||||
|
||||
# Phase 1: wait for the watcher to confirm our registration on
|
||||
# chain plus a cushion of 2 poll intervals, so peers have had
|
||||
# time to fetch the post-registration root. Without this, a
|
||||
# first publish issued right after the gifter's optimistic
|
||||
# response can carry a root mix peers haven't seen yet.
|
||||
let cushionMs = max(pollMs * 2, 10_000)
|
||||
let confirmDeadline = Moment.now() + chronos.milliseconds(deadlineMs)
|
||||
info "Waiting for membership confirmation + propagation cushion",
|
||||
cushionMs = cushionMs, pollMs = pollMs
|
||||
var confirmedSeen = false
|
||||
while Moment.now() < confirmDeadline:
|
||||
let confirmedAt = lezGm.membershipConfirmedAt()
|
||||
if confirmedAt.isSome:
|
||||
let elapsedMs = (Moment.now() - confirmedAt.get()).milliseconds
|
||||
if elapsedMs >= cushionMs:
|
||||
confirmedSeen = true
|
||||
break
|
||||
await sleepAsync(chronos.milliseconds(probeMs))
|
||||
if confirmedSeen:
|
||||
info "Membership confirmed + cushion elapsed"
|
||||
else:
|
||||
warn "Membership confirmation did not arrive within deadline, publishing anyway",
|
||||
deadlineMs = deadlineMs
|
||||
|
||||
# Phase 2: defensive — wait until our proof's merkle root has
|
||||
# been in our own validRoots window for 2 full poll cycles.
|
||||
# Catches the rare case where the watcher confirmed but the
|
||||
# poll loop hasn't yet caught up to the matching proof root.
|
||||
info "Waiting for proof root to stabilize in valid_roots",
|
||||
pollMs = pollMs, stableMs = stableMs
|
||||
var lastRoot = lezGm.proofRoot()
|
||||
var stableSince = Moment.now()
|
||||
let deadline = Moment.now() + chronos.milliseconds(deadlineMs)
|
||||
var settled = false
|
||||
while Moment.now() < deadline:
|
||||
await sleepAsync(chronos.milliseconds(probeMs))
|
||||
let cur = lezGm.proofRoot()
|
||||
if cur != lastRoot:
|
||||
lastRoot = cur
|
||||
stableSince = Moment.now()
|
||||
continue
|
||||
if cur.isSome and lezGm.rootTracker.containsRoot(cur.get()):
|
||||
if Moment.now() - stableSince >= chronos.milliseconds(stableMs):
|
||||
settled = true
|
||||
break
|
||||
if settled:
|
||||
info "Proof root stable in valid_roots, proceeding to publish"
|
||||
else:
|
||||
warn "Proof root did not stabilize within deadline, publishing anyway",
|
||||
deadlineMs = deadlineMs
|
||||
else:
|
||||
info "RLN spam protection ready (non-LEZ), waiting 30s for root convergence"
|
||||
await sleepAsync(30.seconds)
|
||||
else:
|
||||
warn "RLN spam protection not ready after timeout, sending anyway"
|
||||
|
||||
if client.cfg.mixEnabled and client.mixReady:
|
||||
info "Sending via mix (lightpushPublish)", contentTopic = contentTopic, mixPoolSize = client.node.getMixNodePoolSize()
|
||||
let publishFut = client.node.lightpushPublish(
|
||||
some(PubsubTopic(client.cfg.pubsubTopic)), msg, none(RemotePeerInfo), mixify = true
|
||||
)
|
||||
if not await publishFut.withTimeout(15.seconds):
|
||||
await publishFut.cancelAndWait()
|
||||
error "Mix lightpush timed out (no SURB reply within deadline)"
|
||||
else:
|
||||
let res = publishFut.read()
|
||||
if res.isErr:
|
||||
error "Failed to publish via mix", err = $res.error
|
||||
else:
|
||||
info "Message sent via mix successfully"
|
||||
else:
|
||||
warn "Sending via relay fallback (mix not ready or not enabled)",
|
||||
mixEnabled = client.cfg.mixEnabled, mixReady = client.mixReady
|
||||
let res = await client.node.publish(some(PubsubTopic(client.cfg.pubsubTopic)), msg)
|
||||
if res.isErr:
|
||||
error "Failed to Publish", err = res.error,
|
||||
pubsubTopic = client.cfg.pubsubTopic
|
||||
|
||||
proc buildWakuNode(cfg: WakuConfig): WakuNode =
|
||||
let
|
||||
@ -145,6 +275,38 @@ proc buildWakuNode(cfg: WakuConfig): WakuNode =
|
||||
result = node
|
||||
|
||||
|
||||
proc splitPeerIdAndAddr(maddr: string): (string, string) =
|
||||
let parts = maddr.split("/p2p/")
|
||||
if parts.len != 2:
|
||||
error "Invalid multiaddress format", maddr = maddr
|
||||
return ("", "")
|
||||
return (parts[0], parts[1])
|
||||
|
||||
proc parseMixNodes(nodeStrs: seq[string]): seq[MixNodePubInfo] =
|
||||
for nodeStr in nodeStrs:
|
||||
let elements = nodeStr.split(":")
|
||||
if elements.len != 2:
|
||||
error "Invalid mixnode format, expected multiaddr:mixPubKeyHex", node = nodeStr
|
||||
continue
|
||||
result.add(MixNodePubInfo(
|
||||
multiAddr: elements[0],
|
||||
pubKey: intoCurve25519Key(ncrutils.fromHex(elements[1]))
|
||||
))
|
||||
|
||||
proc waitForMixPool(client: WakuClient) {.async.} =
|
||||
while client.node.getMixNodePoolSize() < client.cfg.minMixPoolSize:
|
||||
info "Waiting for mix node pool",
|
||||
current = client.node.getMixNodePoolSize(),
|
||||
required = client.cfg.minMixPoolSize
|
||||
await sleepAsync(1000.milliseconds)
|
||||
client.mixReady = true
|
||||
notice "Mix node pool ready", poolSize = client.node.getMixNodePoolSize()
|
||||
|
||||
proc getMixPoolSize*(client: WakuClient): int =
|
||||
if client.cfg.mixEnabled:
|
||||
return client.node.getMixNodePoolSize()
|
||||
return 0
|
||||
|
||||
proc taskKeepAlive(client: WakuClient) {.async.} =
|
||||
while true:
|
||||
for peerInfo in client.staticPeerList:
|
||||
@ -157,7 +319,7 @@ proc taskKeepAlive(client: WakuClient) {.async.} =
|
||||
|
||||
# TODO: Use filter. Removing this stops relay from working so keeping for now
|
||||
let subscribeRes = await client.node.wakuFilterClient.subscribe(
|
||||
peerInfo, client.cfg.pubsubTopic, @[FilterContentTopic]
|
||||
peerInfo, client.cfg.pubsubTopic, @[FilterContentTopic, LibchatDeliveryAddress]
|
||||
)
|
||||
|
||||
if subscribeRes.isErr():
|
||||
@ -170,65 +332,6 @@ proc taskKeepAlive(client: WakuClient) {.async.} =
|
||||
|
||||
await sleepAsync(60.seconds) # Subscription maintenance interval
|
||||
|
||||
proc start*(client: WakuClient) {.async.} =
|
||||
setupLog(logging.LogLevel.NOTICE, logging.LogFormat.TEXT)
|
||||
await client.node.mountFilter()
|
||||
await client.node.mountFilterClient()
|
||||
|
||||
await client.node.start()
|
||||
(await client.node.mountRelay()).isOkOr:
|
||||
error "failed to mount relay", error = error
|
||||
quit(1)
|
||||
|
||||
client.node.peerManager.start()
|
||||
|
||||
# Connect to all configured static peers
|
||||
if client.staticPeerList.len > 0:
|
||||
info "Connecting to static peers", peerCount = client.staticPeerList.len
|
||||
asyncSpawn client.node.connectToNodes(client.staticPeerList)
|
||||
else:
|
||||
warn "No valid static peers configured"
|
||||
|
||||
let subscription: SubscriptionEvent = (kind: PubsubSub, topic:
|
||||
client.cfg.pubsubTopic)
|
||||
|
||||
proc handler(topic: PubsubTopic, msg: WakuMessage): Future[void] {.async, gcsafe.} =
|
||||
let payloadStr = string.fromBytes(msg.payload)
|
||||
debug "message received",
|
||||
pubsubTopic = topic,
|
||||
contentTopic = msg.contentTopic
|
||||
|
||||
let payload = msg.toChatPayload(topic)
|
||||
|
||||
for queueRef in client.dispatchQueues:
|
||||
await queueRef.queue.put(payload)
|
||||
|
||||
let res = subscribe(client.node, subscription, handler)
|
||||
if res.isErr:
|
||||
error "Subscribe failed", err = res.error
|
||||
|
||||
await allFutures(taskKeepAlive(client))
|
||||
|
||||
proc initWakuClient*(cfg: WakuConfig): WakuClient =
|
||||
# Parse ENRs from static peers configuration
|
||||
var peerInfos: seq[RemotePeerInfo] = @[]
|
||||
for enrStr in cfg.staticPeers:
|
||||
let enrRecord = eth_enr.Record.fromURI(enrStr).valueOr:
|
||||
error "Failed to parse ENR in initWakuClient", enr = enrStr, err = error
|
||||
continue
|
||||
|
||||
let peerInfo = enrRecord.toRemotePeerInfo().valueOr:
|
||||
error "Failed to convert ENR to PeerInfo in initWakuClient", enr = enrStr, err = error
|
||||
continue
|
||||
|
||||
peerInfos.add(peerInfo)
|
||||
|
||||
result = WakuClient(cfg: cfg, node: buildWakuNode(cfg), dispatchQueues: @[],
|
||||
staticPeerList: peerInfos)
|
||||
|
||||
proc addDispatchQueue*(client: var WakuClient, queue: QueueRef) =
|
||||
client.dispatchQueues.add(queue)
|
||||
|
||||
proc getConnectedPeerCount*(client: WakuClient): int =
|
||||
var count = 0
|
||||
for peerId, peerInfo in client.node.peerManager.switch.peerStore.peers:
|
||||
@ -236,5 +339,195 @@ proc getConnectedPeerCount*(client: WakuClient): int =
|
||||
inc count
|
||||
return count
|
||||
|
||||
proc start*(client: WakuClient) {.async.} =
|
||||
setupLog(logging.LogLevel.NOTICE, logging.LogFormat.TEXT)
|
||||
await client.node.mountFilter()
|
||||
await client.node.mountFilterClient()
|
||||
|
||||
client.node.mountAutoSharding(client.cfg.clusterId, uint32(client.cfg.shardId.len)).isOkOr:
|
||||
error "failed to mount auto sharding", error = error
|
||||
|
||||
if client.cfg.mixEnabled:
|
||||
let (mixPrivKey, mixPubKey) = mix_curve25519.generateKeyPair().valueOr:
|
||||
error "Failed to generate mix key pair", error = error
|
||||
quit(QuitFailure)
|
||||
let mixNodeInfos = parseMixNodes(client.cfg.mixNodes)
|
||||
client.node.mountLightPushClient()
|
||||
(await client.node.mountMix(client.cfg.clusterId, mixPrivKey, mixNodeInfos,
|
||||
useOnchainLEZ = true)).isOkOr:
|
||||
error "Failed to mount mix protocol", error = $error
|
||||
quit(QuitFailure)
|
||||
|
||||
# Wire LEZ callbacks BEFORE node.start() so spam protection can initialize
|
||||
if client.cfg.mixEnabled and not client.node.wakuMix.isNil:
|
||||
let gm = client.node.wakuMix.mixRlnSpamProtection.groupManager
|
||||
if gm of OnchainLEZGroupManager:
|
||||
let lezGm = OnchainLEZGroupManager(gm)
|
||||
let clientFetchRoots = mix_lez_client.makeFetchLatestRoots()
|
||||
let clientFetchProof = mix_lez_client.makeFetchMerkleProof()
|
||||
let fetchRoots: onchain_group_manager.FetchRootsCallback = clientFetchRoots
|
||||
let fetchProof: onchain_group_manager.FetchProofCallback = clientFetchProof
|
||||
lezGm.setFetchCallbacks(fetchRoots, fetchProof)
|
||||
mix_lez_client.setGroupManagerRef(lezGm)
|
||||
info "Wired LEZ callbacks for mix RLN spam protection"
|
||||
|
||||
let (_, destId) = splitPeerIdAndAddr(client.cfg.destPeerAddr)
|
||||
client.destPeerId = PeerId.init(destId).valueOr:
|
||||
error "Failed to parse destination peer ID", error = error
|
||||
quit(QuitFailure)
|
||||
asyncSpawn client.waitForMixPool()
|
||||
|
||||
await client.node.start()
|
||||
|
||||
client.node.peerManager.start()
|
||||
|
||||
# Register filter push handler for incoming messages (no relay/gossipsub needed)
|
||||
proc filterHandler(pubsubTopic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} =
|
||||
debug "filter message received",
|
||||
pubsubTopic = pubsubTopic,
|
||||
contentTopic = msg.contentTopic
|
||||
|
||||
let payload = msg.toChatPayload(pubsubTopic)
|
||||
for queueRef in client.dispatchQueues:
|
||||
await queueRef.queue.put(payload)
|
||||
|
||||
client.node.wakuFilterClient.registerPushHandler(filterHandler)
|
||||
|
||||
# Connect to all configured static peers
|
||||
if client.staticPeerList.len > 0:
|
||||
info "Connecting to static peers", peerCount = client.staticPeerList.len
|
||||
await client.node.connectToNodes(client.staticPeerList)
|
||||
info "Connected to static peers"
|
||||
else:
|
||||
warn "No valid static peers configured"
|
||||
|
||||
if client.cfg.mixEnabled and client.cfg.gifterNodeAddr.len > 0 and
|
||||
not client.node.wakuMix.isNil:
|
||||
let gm = client.node.wakuMix.mixRlnSpamProtection.groupManager
|
||||
if gm of OnchainLEZGroupManager:
|
||||
let lezGm = OnchainLEZGroupManager(gm)
|
||||
let gifterClient = rln_gifter_client.WakuRlnGifterClient.new(
|
||||
client.node.peerManager, client.node.rng
|
||||
)
|
||||
let gifterPeer = parsePeerInfo(client.cfg.gifterNodeAddr).valueOr:
|
||||
error "Failed to parse gifter peer", error = error
|
||||
quit(QuitFailure)
|
||||
client.node.peerManager.addServicePeer(gifterPeer, WakuRlnGifterCodec)
|
||||
|
||||
let idCred =
|
||||
if lezGm.credentials.isSome:
|
||||
lezGm.credentials.get()
|
||||
else:
|
||||
mix_rln_interface.membershipKeyGen().valueOr:
|
||||
error "Failed to generate RLN identity", error = $error
|
||||
quit(QuitFailure)
|
||||
let idCommitmentBytes = @(idCred.idCommitment)
|
||||
|
||||
info "Registering via RLN gifter",
|
||||
gifterPeer = client.cfg.gifterNodeAddr,
|
||||
identityCommitmentLen = idCommitmentBytes.len
|
||||
|
||||
var authType: seq[byte]
|
||||
var authPayload: seq[byte]
|
||||
if client.cfg.gifterAuthKey.len > 0:
|
||||
let seckey = eth_keys.PrivateKey.fromHex(client.cfg.gifterAuthKey).valueOr:
|
||||
error "Failed to parse gifter auth key", error = $error
|
||||
quit(QuitFailure)
|
||||
let sig = seckey.sign(rln_gifter_protocol.eip191Message(idCommitmentBytes))
|
||||
authPayload = @(sig.toRaw())
|
||||
for c in rln_gifter_protocol.EthAllowlistAuthType:
|
||||
authType.add(byte(c))
|
||||
info "Signing gifter request with EIP-191 auth key",
|
||||
signer = seckey.toPublicKey().to(eth_addresses.Address).to0xHex()
|
||||
|
||||
let regRes = await gifterClient.requestMembership(
|
||||
idCommitmentBytes,
|
||||
some(uint64(lezGm.userMessageLimit)),
|
||||
gifterPeer,
|
||||
authType,
|
||||
authPayload,
|
||||
)
|
||||
if regRes.isErr:
|
||||
error "Failed to register via gifter", error = regRes.error
|
||||
quit(QuitFailure)
|
||||
let success = regRes.get()
|
||||
|
||||
let configAccountId = success.configAccountId.valueOr:
|
||||
error "Gifter response missing configAccountId extension"
|
||||
quit(QuitFailure)
|
||||
|
||||
lezGm.credentials = some(idCred)
|
||||
lezGm.membershipIndex = some(onchain_group_manager.MembershipIndex(success.leafIndex))
|
||||
mix_lez_client.setRlnConfig(configAccountId, success.leafIndex.int)
|
||||
|
||||
info "Registered via RLN gifter",
|
||||
leafIndex = success.leafIndex,
|
||||
configAccount = configAccountId
|
||||
|
||||
# Correct the optimistic leaf via the status codec if a concurrent
|
||||
# registration tx beat ours to the slot. Pre-publish self-verify drops
|
||||
# bad proofs in the meantime.
|
||||
let watcherLezGm = lezGm
|
||||
let watcherConfigAccount = configAccountId
|
||||
asyncSpawn gifterClient.watchMembershipConfirmation(
|
||||
gifterPeer, configAccountId, idCommitmentBytes, success.leafIndex,
|
||||
"Chat-client",
|
||||
proc(authLeaf: uint64) {.gcsafe, raises: [].} =
|
||||
if some(onchain_group_manager.MembershipIndex(authLeaf)) !=
|
||||
watcherLezGm.membershipIndex:
|
||||
watcherLezGm.membershipIndex =
|
||||
some(onchain_group_manager.MembershipIndex(authLeaf))
|
||||
mix_lez_client.setRlnConfig(watcherConfigAccount, authLeaf.int)
|
||||
watcherLezGm.markMembershipConfirmed(),
|
||||
)
|
||||
|
||||
asyncSpawn taskKeepAlive(client)
|
||||
|
||||
if client.cfg.mixEnabled and not client.node.wakuMix.isNil:
|
||||
let gm = client.node.wakuMix.mixRlnSpamProtection.groupManager
|
||||
if gm of OnchainLEZGroupManager:
|
||||
OnchainLEZGroupManager(gm).startPolling()
|
||||
|
||||
info "Waku client started",
|
||||
relayMounted = not client.node.wakuRelay.isNil,
|
||||
mixMounted = not client.node.wakuMix.isNil,
|
||||
connectedPeers = client.getConnectedPeerCount()
|
||||
|
||||
# Debug: periodically log relay/mesh status
|
||||
proc meshStatusTask(client: WakuClient) {.async.} =
|
||||
while true:
|
||||
await sleepAsync(15.seconds)
|
||||
let connPeers = client.getConnectedPeerCount()
|
||||
info "Peer status",
|
||||
pubsubTopic = client.cfg.pubsubTopic,
|
||||
connectedPeers = connPeers,
|
||||
mixReady = client.mixReady,
|
||||
mixPoolSize = (if client.cfg.mixEnabled and not client.node.wakuMix.isNil: client.node.getMixNodePoolSize() else: 0)
|
||||
|
||||
asyncSpawn meshStatusTask(client)
|
||||
|
||||
proc initWakuClient*(cfg: WakuConfig): WakuClient =
|
||||
var peerInfos: seq[RemotePeerInfo] = @[]
|
||||
for peerStr in cfg.staticPeers:
|
||||
if peerStr.startsWith("/"):
|
||||
let peerInfo = parsePeerInfo(peerStr).valueOr:
|
||||
error "Failed to parse multiaddr peer", peer = peerStr, err = error
|
||||
continue
|
||||
peerInfos.add(peerInfo)
|
||||
else:
|
||||
let enrRecord = eth_enr.Record.fromURI(peerStr).valueOr:
|
||||
error "Failed to parse ENR", enr = peerStr, err = error
|
||||
continue
|
||||
let peerInfo = enrRecord.toRemotePeerInfo().valueOr:
|
||||
error "Failed to convert ENR to PeerInfo", enr = peerStr, err = error
|
||||
continue
|
||||
peerInfos.add(peerInfo)
|
||||
|
||||
result = WakuClient(cfg: cfg, node: buildWakuNode(cfg), dispatchQueues: @[],
|
||||
staticPeerList: peerInfos)
|
||||
|
||||
proc addDispatchQueue*(client: var WakuClient, queue: QueueRef) =
|
||||
client.dispatchQueues.add(queue)
|
||||
|
||||
proc stop*(client: WakuClient) {.async.} =
|
||||
await client.node.stop()
|
||||
|
||||
1
vendor/logos-lez-rln
vendored
Submodule
1
vendor/logos-lez-rln
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 950f2877c13a61d771561ce966ca2d4ea5ae28c1
|
||||
2
vendor/nwaku
vendored
2
vendor/nwaku
vendored
@ -1 +1 @@
|
||||
Subproject commit 41146a9193c1e360b9a0049d672260a72c4ca2bf
|
||||
Subproject commit 8e6ba04d5c7bb1829cffbbb6e682461373246987
|
||||
Loading…
x
Reference in New Issue
Block a user