commit d86a1269fb2db3ee4a6e33cddc050a60cea7c66e Author: Arseniy Klempner Date: Fri Jul 3 20:25:39 2026 -0600 logos-rln-mix-sim: gifted RLN membership allocation over a mixnet Five-node docker simulation, E2E on the hosted LEZ testnet: one funded gifter node allocates on-chain RLN memberships (LIP-158) to four client nodes over an EIP-191-authenticated libp2p protocol, then the nodes exchange request/reply traffic through a 3-hop Sphinx mix (LIP-99) with RLN spam protection verified at every hop (LIP-144). Entry points: docker/testnet/mix_e2e/{bootstrap.sh,orchestrate.sh}; README is the front door. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6b8710a --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.git diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9286dfd --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Built libp2p .lgx (produced by docker/build_lgx_linux.sh) +docker/lp2p-out/ + +# nix build outputs +result +result-* + +# local scratch +*.log +*.exe + +# local working doc (kept untracked) +docker/testnet/mix_e2e/JOURNEY.md diff --git a/LICENSE-APACHE-v2 b/LICENSE-APACHE-v2 new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE-APACHE-v2 @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..e288ae6 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2026 Logos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf36d0a --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# logos-rln-mix-sim + +A reproducible, end-to-end simulation of **gifted RLN membership allocation over a +Sphinx mix network**, running real `logoscore` daemons against the hosted Logos +Execution Zone (LEZ) testnet. + +Five nodes come up as a docker-compose stack. One node — the **gifter** — holds the +only funded wallet; it registers its own RLN membership and then serves a libp2p +membership-allocation protocol (`/logos/rln/membership/1.0.0`, [LIP-158]). The other +four nodes **authenticate with an EIP-191 signature and receive a distinct on-chain +RLN membership without ever funding or signing a transaction themselves.** Those +memberships are then exercised by sending an RLN-protected message through a 3-hop +Sphinx mix with a SURB reply, where spam protection is enforced **per hop on both +legs** ([LIP-144]) — which is exactly why every mix node has to be a member, and why +cheap, authenticated membership allocation matters. + +[LIP-158]: https://lip.logos.co/anoncomms/raw/rln-membership-service.html +[LIP-144]: https://lip.logos.co/anoncomms/raw/mix-spam-protection-rln.html + +## Quick start + +Requires Docker running (~30 GB free in its VM), internet, and stock host tools +(`bash`, `git`, `python3`, `curl`, `rsync` — preinstalled on macOS and most Linux). +No toolchain, no manual keystores — the bootstrap fetches and builds everything. + +```sh +git clone https://github.com/logos-co/logos-rln-mix-sim.git +cd logos-rln-mix-sim +bash docker/testnet/mix_e2e/bootstrap.sh # clone 4 siblings + build .lgx + image (~30-45 min first run) +cd docker/testnet/mix_e2e +bash orchestrate.sh # gifted allocation + 3-hop RLN-over-mix delivery (~15 min) +docker compose down # tear down +``` + +The only runtime knob is the negative tests (both prove RLN gates delivery): + +```sh +NEG=1 bash orchestrate.sh # sender never asks the gifter -> rejected (0 replies) +NEG=2 bash orchestrate.sh # sender's key not allowlisted -> gifter refuses auth +``` + +## What a pass looks like + +- 5 distinct on-chain leaves (1 self-registered + 4 gifted), `leaf_opt == leaf_actual`, + `confirmed=true`, `rlnIsReady=True` on every node. +- `relay1 gifter service mounted (/logos/rln/membership/1.0.0, allowlist=4 clients)`; + relay1's log shows `RLN gifter registration succeeded` ×4; each client logs + `RLN membership granted`. +- `sender->dest: 3/3` and `dest->sender: 3/3` replies received; ~36 per-hop RLN + verifications; `VERDICT: PASS`. + +## Trust model (LIP-158 trade-offs) + +The gifter is a **membership provider / gatekeeper**, with the trade-offs that role +implies: it decides which commitments get registered and can refuse or stall any +request (`NEG=2` demonstrates the refusal path); sybil resistance is exactly the +auth policy and nothing more (here, the spec's demo mode — a static EIP-191 +allowlist, one membership per address); and it fronts the funds and learns a +durable auth-identity↔commitment mapping — but never the RLN secret, so it cannot +forge client proofs. Client-IP↔identity correlation is deferred by the spec to RLN +Stealth Commitments. Spam enforcement itself is not provider-mediated: exceeding +`userMessageLimit` per epoch reuses a nullifier, letting any relay reconstruct the +offender's key and remove it from the group. + +## What it builds on + +`bootstrap.sh` clones four sibling repos next to this one over HTTPS (the mix stack +sits on adklempner forks pending upstreaming) and links them into one loadable +libp2p `.lgx`: + +| repo | branch | role | +|---|---|---| +| [`logos-rln-gifter`](https://github.com/logos-co/logos-rln-gifter) | `master` | RLN membership gifter protocol (LIP-158) | +| [`logos-libp2p-module`](https://github.com/adklempner/logos-libp2p-module) | `rebase/enable-mix` | universal libp2p module (mix + RLN + gifter glue) | +| [`mix-rln-spam-protection-plugin`](https://github.com/adklempner/mix-rln-spam-protection-plugin) | `feat/cbind-rln` | RLN SpamProtection (LIP-144) | +| [`nim-libp2p-mix`](https://github.com/adklempner/nim-libp2p-mix) | `rebase/mix-cbind` | Sphinx mix (LIP-99) | + +The image build additionally clones `logos-co/logos-lez-rln` (`main` pinned @ +`4b403c1`) — which fetches the execution zone (lssa) at `v0.2.0-rc6` via its +flake — and bakes a **deployment profile** (RLN tree + wallet) into `/testnet`. + +## Layout + +``` +docker/ + Dockerfile.testnet-e2e # the runtime image (logoscore + wallet/rln modules + baked deployment) + Dockerfile.lgx-linux # builds the Linux libp2p .lgx from the 4 siblings + build_lgx_linux.sh # driver for the .lgx build + testnet/ + stage.sh # bakes a deployment profile into /testnet at image build time + provision.sh, verify.sh # deployment-profile tooling (thin shims into logos-lez-rln) + deployments/ # deployment profiles (default: shared-5ade) + mix_e2e/ # THE SIM + bootstrap.sh # one-shot: clone siblings + build .lgx + build image + orchestrate.sh # drives the 5 daemons (gifter + clients, mix exchange, verdict) + docker-compose.yml # 5 logoscore services on a shared bridge network + entrypoint.sh, keys.py # per-container setup + host-side key derivation + fixtures/gifter_auth/ # demo EIP-191 keys + allowlist (NOT for production) +``` + +## Docs + +- [`docker/testnet/deployments/README.md`](docker/testnet/deployments/README.md) — deployment + profiles (run-against-existing / redeploy-fresh, `provision.sh`/`verify.sh`). diff --git a/docker/.gitignore b/docker/.gitignore new file mode 100644 index 0000000..25faeb1 --- /dev/null +++ b/docker/.gitignore @@ -0,0 +1 @@ +lp2p-out/ diff --git a/docker/Dockerfile.lgx-linux b/docker/Dockerfile.lgx-linux new file mode 100644 index 0000000..73c6477 --- /dev/null +++ b/docker/Dockerfile.lgx-linux @@ -0,0 +1,37 @@ +# Build the logos-libp2p-module .lgx for Linux (cbind-rln + RLN), COPY-based: +# our three local repos are staged in the context (no .git, darwin librln +# stripped), a Linux librln_mix is built from zerokit, and the flake inputs are +# repointed at the in-container copies via --override-input path:. +FROM nixos/nix:2.34.1 AS builder + +RUN printf 'experimental-features = nix-command flakes\nsandbox = false\n' >> /etc/nix/nix.conf +RUN git config --global url."https://github.com/".insteadOf "git@github.com:" +WORKDIR /build + +# 1. Linux librln_mix from the zerokit mix fork (the vendored one is darwin). +RUN git clone --depth 1 --branch v2.0.0 https://github.com/vacp2p/zerokit.git /build/zerokit +RUN nix shell nixpkgs#cargo nixpkgs#rustc nixpkgs#gcc nixpkgs#pkg-config \ + --command bash -c "cd /build/zerokit && cargo build --release -p rln --manifest-path rln/Cargo.toml" + +# 2. Stage our repos (context already excludes .git/build/junk/darwin librln). +COPY nim-libp2p-mix /src/nim-libp2p-mix +COPY mix-rln-spam-protection-plugin /src/mix-rln-spam-protection-plugin +COPY logos-libp2p-module /src/logos-libp2p-module +COPY logos-rln-gifter /src/logos-rln-gifter + +# 3. Drop the Linux librln where cbind-rln.nix expects it. +RUN mkdir -p /src/mix-rln-spam-protection-plugin/vendor && \ + cp /build/zerokit/target/release/librln.a \ + /src/mix-rln-spam-protection-plugin/vendor/librln_mix_v2.0.0.a + +# 4. Build the Linux .lgx, repointing the local git+file inputs at the copies. +RUN nix build "path:/src/logos-libp2p-module#lgx" \ + --override-input libp2p "path:/src/mix-rln-spam-protection-plugin" \ + --override-input libp2p/libp2p_mix "path:/src/nim-libp2p-mix" \ + --override-input libp2p/logos_rln_gifter "path:/src/logos-rln-gifter" \ + --out-link /build/result-lgx && \ + cp -L /build/result-lgx/*.lgx /build/libp2p_module.lgx && \ + ls -la /build/libp2p_module.lgx + +FROM scratch AS lgx +COPY --from=builder /build/libp2p_module.lgx /libp2p_module.lgx diff --git a/docker/Dockerfile.testnet-e2e b/docker/Dockerfile.testnet-e2e new file mode 100644 index 0000000..9ad63a1 --- /dev/null +++ b/docker/Dockerfile.testnet-e2e @@ -0,0 +1,100 @@ +# testnet E2E image (NO local sequencer / risc0). Builds logos-logoscore-cli + +# the wallet & rln modules (clone of logos-lez-rln, pinned to main), bundles our +# COPY-built Linux libp2p_module .lgx, and bakes the deployment profile. This is +# the base image for the 5-node gifted-RLN-over-mix sim; the compose entrypoint +# overrides the container command and orchestrate.sh drives the run. +# +# Build context = repo root; expects docker/lp2p-out/libp2p_module.lgx (from +# docker/build_lgx_linux.sh) and the deployment profile under docker/testnet/. +# Ubuntu base + apt build tooling — the +# zerokit/lez-rln-ffi cargo build needs build-essential + clang/libclang for +# bindgen, which the bare nixos/nix (alpine) base lacks. +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 clang libclang-dev xxd \ + && rm -rf /var/lib/apt/lists/* +RUN curl -L https://nixos.org/nix/install | bash -s -- --daemon --yes +# accept-flake-config=false: ignore flakes' nix-cache.status.im substituter, +# which hangs for untrusted users (the Phase-0 2h+ silent-stall gotcha). +# connect-timeout + fallback so any unreachable substituter fails fast. +RUN mkdir -p /root/.config/nix && \ + printf 'experimental-features = nix-command flakes\nsandbox = false\naccept-flake-config = false\nconnect-timeout = 5\nfallback = true\n' > /root/.config/nix/nix.conf && \ + mkdir -p /etc/nix && cp /root/.config/nix/nix.conf /etc/nix/nix.conf +RUN rmdir /homeless-shelter 2>/dev/null || true +RUN git config --global url."https://github.com/".insteadOf "git@github.com:" +SHELL ["/bin/bash","-lc"] +ENV LIBCLANG_PATH=/usr/lib/llvm-18/lib + +# logoscore CLI (the -D / load-module / call daemon binary). +RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \ + nix build --no-accept-flake-config --fallback github:logos-co/logos-logoscore-cli -o /logoscore + +# Clone logos-lez-rln pinned to main @ 4b403c1 (PR #16: the rc6 SPEL stack — +# lez-rln-ffi PDA-prefix fix, reviewed rln-module, tri-state subtree fetch). +# Its flake fetches the execution zone (lssa) at v0.2.0-rc6 (e37876a6) via +# fetchFromGitHub — so nothing is vendored/overlaid. nix emits raw lib/include; +# the daemon needs the .lgx layout (manifest + variant), produced by the bundle +# step below. +RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \ + git clone https://github.com/logos-co/logos-lez-rln.git /root/logos-lez-rln && \ + git -C /root/logos-lez-rln checkout 4b403c13988679115f3e4ff8c0ec33b47c6c3b8b + +# Wallet module: UPSTREAM logos-execution-zone-module d70225c (rc6). Upstream +# natively ships program-id-based send_generic_public_transaction (+ rc6 transfer +# sigs + resolve_public_account); its flake pins execution-zone e37876a6 (rc6). +# The rln module calls send_generic_public_transaction directly with typed +# multi-arg invokeRemoteMethod. Built FIRST (stable input) so rln-side iterations +# keep it cached. +RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \ + nix build --no-accept-flake-config --fallback \ + github:logos-blockchain/logos-execution-zone-module/d70225ced646934d2294fd9e8f8b03615c104b80#lgx \ + -o /root/wallet-lgx + +# RLN module .lgx, built from the pinned main checkout above. +RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \ + cd /root/logos-lez-rln && \ + nix bundle --no-accept-flake-config --fallback \ + --bundler github:logos-co/nix-bundle-lgx .#logos-rln-module -o /root/rln-bundle + +# Runtime tools into a dedicated profile so the container runtime (entrypoint + +# in-container exec calls) needs no `nix`. +RUN source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && \ + nix profile install --no-accept-flake-config --fallback --profile /rttools \ + nixpkgs#bash nixpkgs#python3 nixpkgs#coreutils nixpkgs#gnutar nixpkgs#gnugrep nixpkgs#curl nixpkgs#procps +ENV PATH=/rttools/bin:$PATH + +# Our Linux RLN-enabled libp2p module (overlaid fresh by the compose entrypoint). +COPY docker/lp2p-out/libp2p_module.lgx /artifacts/libp2p_module.lgx +# Deployment profile -> /testnet fixtures. A deployment is fully captured by its +# descriptor (tree_id + derived config + payment/supply pointers) plus the wallet +# (storage.json). stage.sh generates the flat files the daemons read and asserts +# the wallet<->deployment binding (schema + payment/supply present). Point the sim +# at a different provisioned deployment with --build-arg DEPLOYMENT=. +ARG DEPLOYMENT=shared-5ade +COPY docker/testnet/deployments/ /deployments/ +COPY docker/testnet/stage.sh /stage.sh +RUN bash /stage.sh "/deployments/$DEPLOYMENT" /testnet + +# Install all three modules the way logos-core expects: // +# with manifest.json + flattened variant libs + a `variant` marker (mirrors the +# sim's install_lgx). rln/wallet .lgx come from the nix bundles; libp2p from our +# COPY'd artifact. Variant key auto-detected (single per-platform variant). +RUN mkdir -p /modules && \ + rlnlgx=$(find -L /root/rln-bundle -maxdepth 2 -name '*.lgx' | head -1) && \ + walletlgx=$(find -L /root/wallet-lgx -maxdepth 2 -name '*.lgx' | head -1) && \ + [ -f "$rlnlgx" ] && [ -f "$walletlgx" ] || { echo "missing .lgx (rln=$rlnlgx wallet=$walletlgx)"; exit 1; } && \ + for lgx in /artifacts/libp2p_module.lgx "$rlnlgx" "$walletlgx"; do \ + name=$(tar xzOf "$lgx" manifest.json | python3 -c 'import json,sys;print(json.load(sys.stdin)["name"])') && \ + tmp=$(mktemp -d) && tar xzf "$lgx" -C "$tmp" && \ + var=$(ls "$tmp/variants" | head -1) && \ + rm -rf "/modules/$name" && mkdir -p "/modules/$name" && \ + cp "$tmp/manifest.json" "/modules/$name/" && \ + cp -L "$tmp/variants/$var/"* "/modules/$name/" && \ + printf '%s' "$var" > "/modules/$name/variant" && \ + echo "installed $name (variant=$var)"; \ + done + +ENV LOGOSCORE=/logoscore/bin/logoscore MODULES_DIR=/modules WALLET_HOME=/testnet +# This image is the base for the 5-node sim; the compose entrypoint overrides +# this. Run the sim via docker/testnet/mix_e2e/orchestrate.sh (see the README). +CMD ["bash","-lc","echo 'lp2p-mix-e2e base image. Run the sim: cd docker/testnet/mix_e2e && bash orchestrate.sh (see README).'"] diff --git a/docker/build_lgx_linux.sh b/docker/build_lgx_linux.sh new file mode 100755 index 0000000..d1d37f4 --- /dev/null +++ b/docker/build_lgx_linux.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Build the logos-libp2p-module .lgx for Linux from the local (unpushed) +# branches, COPY-based — no GitHub push needed. Stages the three repos into a +# clean build context (no .git / build junk / darwin librln), then Docker +# builds a Linux librln_mix + the Linux .lgx (see Dockerfile.lgx-linux). +# +# Output: ./lp2p-out/libp2p_module.lgx (Linux variant). +# +# Repos expected as siblings under ~/Waku/Logos: +# nim-libp2p-mix (rebase/mix-cbind), mix-rln-spam-protection-plugin +# (feat/cbind-rln), logos-libp2p-module (rebase/enable-mix), logos-rln-gifter +# (the standalone RLN membership gifter protocol module). +set -euo pipefail + +LOGOS_ROOT="${LOGOS_ROOT:-$HOME/Waku/Logos}" +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CTX=$(mktemp -d) +OUT="${1:-$HERE/lp2p-out}" + +for r in nim-libp2p-mix mix-rln-spam-protection-plugin logos-libp2p-module logos-rln-gifter; do + rsync -a \ + --exclude='.git' --exclude='build' --exclude='nimcache*' \ + --exclude='result*' --exclude='*.dylib' --exclude='vendor/librln*.a' \ + "$LOGOS_ROOT/$r/" "$CTX/$r/" +done +cp "$HERE/Dockerfile.lgx-linux" "$CTX/Dockerfile" + +echo "context: $(du -sh "$CTX" | cut -f1)" +docker build --target lgx --output "type=local,dest=$OUT" "$CTX" +rm -rf "$CTX" +echo "built: $OUT/libp2p_module.lgx" diff --git a/docker/testnet/deployments/README.md b/docker/testnet/deployments/README.md new file mode 100644 index 0000000..015a212 --- /dev/null +++ b/docker/testnet/deployments/README.md @@ -0,0 +1,54 @@ +# Deployment profiles + +One **deployment** = one on-chain RLN instance, fully captured by two files: + +``` +deployments// + deployment.json # tree_id + sequencer + program_ids + derived config + payment/supply + storage.json # the wallet (holds payment/supply/token/treasury keypairs) +``` + +`tree_id` is the single source of truth: `config`/`tree_main`/`credit_*` are **derived** +from `(registration_program_id, tree_id)`; `payment`/`supply` are **pointers into the +wallet**. See the canonical docs at `logos-lez-rln/tools/deployments/README.md`. + +## Tooling is shared, not duplicated + +The deployment layer lives once, in **`logos-lez-rln/tools/deployments/`** +(`stage.sh` / `provision.sh` / `verify.sh` + the `derive_accounts` bin). This repo +does **not** submodule logos-lez-rln — the image clones it (`main`, pinned by +commit) at build time. The deployment tooling it consumes two ways: + +- `docker/testnet/stage.sh` — a **vendored copy** of the canonical `stage.sh` (bash+jq). + It sits in the build context so `docker build` stays self-contained (the image has + `jq`). Keep it in sync with the canonical copy. +- `docker/testnet/{provision,verify}.sh` — **thin shims** that `exec` the canonical + scripts via `LEZ_RLN_DIR` (host-only; they need the Rust `run_setup`/`derive_accounts`). + +## Run the sim against a deployment + +```bash +docker build -f docker/Dockerfile.testnet-e2e --build-arg DEPLOYMENT= -t lp2p-mix-e2e . +cd docker/testnet/mix_e2e && bash orchestrate.sh +``` + +`--build-arg DEPLOYMENT=` defaults to `shared-5ade`. The build runs `stage.sh`, which +asserts the wallet schema (rc6) and the wallet<->deployment binding (payment/supply +present) — a mismatched wallet fails the build, not a node at runtime. + +## Provision / verify (needs a logos-lez-rln checkout) + +```bash +(cd "$LEZ_RLN_DIR/lez-rln" && PYO3_PYTHON=$(command -v python3) \ + cargo build --release --bin run_setup --bin derive_accounts) + +# fresh tree + fresh wallet, written into this repo's deployments/: +LEZ_RLN_DIR=/path/to/logos-lez-rln bash docker/testnet/provision.sh --name my-run + +# reuse another sim's wallet (shared accounts across sims), specific tree: +LEZ_RLN_DIR=/path/to/logos-lez-rln bash docker/testnet/provision.sh \ + --name shared --tree <64hex> --adopt-wallet /path/to/other/storage.json + +# guest-drift guard (re-derive from the actual guest binaries, diff the descriptor): +LEZ_RLN_DIR=/path/to/logos-lez-rln bash docker/testnet/verify.sh docker/testnet/deployments/ +``` diff --git a/docker/testnet/deployments/shared-5ade/deployment.json b/docker/testnet/deployments/shared-5ade/deployment.json new file mode 100644 index 0000000..883578f --- /dev/null +++ b/docker/testnet/deployments/shared-5ade/deployment.json @@ -0,0 +1,10 @@ +{ + "name": "shared-5ade", + "tree_id": "5ade5ade5ade5ade58be555c91296c73754f7f5d5c63612c958c11771df670bc", + "sequencer": "https://testnet.lez.logos.co/", + "registration_program_id": "df89eefa733d4e4b26ec2094b593c1a719a7ff99885f5a4f69c4a9e89a888d05", + "merkle_program_id": "6884c4803ae91a3ceaf06acd98b7e33932b4694958c7f9e8be42a0e83aa7e8ea", + "config_account": "FUhP8quu5WKEL33oALSgDnXq9JZ8Qx72en7zSrzmPrDC", + "payment_account": "9xhSHTkuFj8m4BbB1QA5W3pQHAeiRMkQdk4TGG8fCZz4", + "supply_holding": "2BVAc1T4WRPLVc3nTcTwM9fUC5MSDBsiCBsXzoF9QZge" +} diff --git a/docker/testnet/deployments/shared-5ade/storage.json b/docker/testnet/deployments/shared-5ade/storage.json new file mode 100644 index 0000000..b9419ef --- /dev/null +++ b/docker/testnet/deployments/shared-5ade/storage.json @@ -0,0 +1,1875 @@ +{ + "key_chain": { + "accounts": [ + { + "Public": { + "account_id": "2BVAc1T4WRPLVc3nTcTwM9fUC5MSDBsiCBsXzoF9QZge", + "chain_index": [ + 0, + 1 + ], + "data": { + "sk": "1b5119b4c7994bc8158f67725707f605e6c59035e0a28dc701787ab53dc0106c", + "ssk": "6c7c49cc64c2890c0c68dd62afdd07a8b61fcb791c273a9aa665789e40f596ce", + "pk": "2782ed224755d578024e6f7d790e77350fa1319d03b4c0ede80e1baad9f27ab8", + "cc": [ + 40, + 5, + 80, + 248, + 140, + 254, + 46, + 145, + 238, + 149, + 157, + 188, + 123, + 10, + 3, + 21, + 119, + 164, + 108, + 165, + 125, + 185, + 157, + 17, + 237, + 131, + 148, + 241, + 96, + 73, + 139, + 45 + ], + "cci": 1 + } + } + }, + { + "Public": { + "account_id": "79xRxVcEHrbgTAoNf18DLqbmtpn5oEtZY4iHZ9W5UU97", + "chain_index": [ + 2 + ], + "data": { + "sk": "326c366e8d1437b1fa247382b564fa3bf698a9947bd3fa8f2a2ae24563ca48f5", + "ssk": "52ab7434dcd2f421631bb8d1fbc5bc19ef6d78e4fef8735bce672c04c854734e", + "pk": "44adbf9b893660a8fa3da2c73d06cfd4f9c4a80be7781e4b8f93fafe04c62696", + "cc": [ + 189, + 91, + 208, + 136, + 93, + 194, + 4, + 38, + 220, + 240, + 240, + 23, + 215, + 173, + 96, + 95, + 231, + 163, + 170, + 111, + 79, + 214, + 175, + 147, + 66, + 167, + 217, + 70, + 133, + 111, + 211, + 201 + ], + "cci": 2 + } + } + }, + { + "Public": { + "account_id": "7P5uhyBNx49RJYnwAxvKSuknZCHaVJKqCesdCpBFPFDR", + "chain_index": [ + 1 + ], + "data": { + "sk": "5344e8624318f50d9a405c8892975abc882c7fce4118b160146cf187fda3782d", + "ssk": "2944835280cc3b526c042f25a88aafcf1554ea5fd2905d30a9862efe4e5ae9d8", + "pk": "da2f8fc733c55fb9bbaff7272715d50e3ed450f9a79d91ff00dc6a0779b1b4b7", + "cc": [ + 93, + 188, + 61, + 175, + 106, + 189, + 86, + 95, + 6, + 222, + 209, + 14, + 80, + 149, + 68, + 250, + 10, + 178, + 198, + 120, + 100, + 141, + 34, + 232, + 171, + 129, + 68, + 154, + 241, + 68, + 25, + 37 + ], + "cci": 1 + } + } + }, + { + "Public": { + "account_id": "983qVHyFmuh5DBp2uVrSR9PiCMxmd9pnhFWNLVbW26rL", + "chain_index": [], + "data": { + "sk": "dca4e9430cf85b6dd7a0d5b63cfad6e415813d84deca7414749d0286806c9fab", + "ssk": "341b2159161955f342ecdebd83f43cd7c50c353c2812ac481eac79125b98d23e", + "pk": "5a928bc18aff4a3937b1be7c823c15ea8f79033e1e86cfbe814d1bbe9cc32783", + "cc": [ + 92, + 119, + 133, + 168, + 168, + 78, + 67, + 73, + 246, + 254, + 58, + 23, + 186, + 12, + 182, + 102, + 80, + 145, + 149, + 255, + 86, + 31, + 54, + 25, + 185, + 64, + 0, + 206, + 242, + 129, + 31, + 73 + ], + "cci": null + } + } + }, + { + "Public": { + "account_id": "9xhSHTkuFj8m4BbB1QA5W3pQHAeiRMkQdk4TGG8fCZz4", + "chain_index": [ + 3 + ], + "data": { + "sk": "c9d070de95cc6d25ebbecbf3316036640d72a9d63138ab00df0f75ffebfecd70", + "ssk": "1b537d3df0143eefe093d16a5b6277554559741684bd3bc9ddd4eadbfcabb89c", + "pk": "a6857028273339f6aa2939c468d5712d77e2e62be9631fc0375a6c08d101b843", + "cc": [ + 9, + 219, + 233, + 118, + 121, + 104, + 155, + 230, + 210, + 95, + 201, + 212, + 6, + 153, + 26, + 24, + 70, + 91, + 209, + 162, + 147, + 234, + 171, + 252, + 42, + 1, + 142, + 240, + 240, + 211, + 155, + 243 + ], + "cci": 3 + } + } + }, + { + "Public": { + "account_id": "AMjdjcJow4mLHPx94wJCzcbdGAtGosqq9vMmZ1LvQLPr", + "chain_index": [ + 0, + 0 + ], + "data": { + "sk": "134b114954fed639b03eb45b0af7b9b7aa993f4086f897660459c305124322a4", + "ssk": "086095a7b7e7315585104c41c6738a080c3f3cdd05dd6c9bc28673099cf02af8", + "pk": "d81dc6c843667e170aa537833d1b4f09d14486d6d5b1357918ffd6f2edb3357d", + "cc": [ + 191, + 198, + 120, + 235, + 63, + 67, + 147, + 151, + 148, + 194, + 124, + 35, + 225, + 89, + 176, + 241, + 211, + 32, + 153, + 95, + 200, + 125, + 27, + 163, + 191, + 230, + 252, + 218, + 98, + 242, + 162, + 235 + ], + "cci": 0 + } + } + }, + { + "Public": { + "account_id": "BioesGt1dxh6vLCGZeS2uGAtiQVAwavB1297DMZtdEv7", + "chain_index": [ + 1, + 0 + ], + "data": { + "sk": "4d84c1e4f8856da5a8442c0457b12f4ecc55f09e83d03a1b8e8d4d78713382fd", + "ssk": "34bb99de77e760af1d98937a70d95ee977e2068e7f43240ba09fa2eff5ae68e9", + "pk": "c8e12bc2e088ababe1b7deccc700c4ae2c946296d8b33ddafb38d782ec740425", + "cc": [ + 18, + 42, + 65, + 143, + 241, + 49, + 164, + 243, + 53, + 250, + 136, + 79, + 48, + 193, + 244, + 59, + 137, + 96, + 11, + 183, + 188, + 104, + 95, + 197, + 253, + 223, + 252, + 70, + 35, + 45, + 2, + 145 + ], + "cci": 0 + } + } + }, + { + "Public": { + "account_id": "D8Wpda7gR7m3XmjKuyMHVavYo4C1iC4FcpnFYyDm8aRV", + "chain_index": [ + 0, + 0, + 0 + ], + "data": { + "sk": "bdf9508f1c3150440fa85a15dbad776882ea2a2b4683f77daefaeb547dfd062e", + "ssk": "b3748c8523f397d810ae0c3b7cd665d4c54e59703faf51b8d29ecd0cbf52d1a6", + "pk": "8daebeffebd8369f8393e9e247f82facc8d3218d62884e2672c83ea0035638f3", + "cc": [ + 2, + 176, + 12, + 128, + 72, + 187, + 198, + 43, + 45, + 18, + 171, + 255, + 17, + 125, + 78, + 229, + 124, + 40, + 85, + 38, + 154, + 196, + 255, + 64, + 151, + 172, + 188, + 136, + 154, + 89, + 39, + 17 + ], + "cci": 0 + } + } + }, + { + "Public": { + "account_id": "DYmVLpMMpEXaaHPCLVz3HNcYbYVErKjQRGjKAKghYLkt", + "chain_index": [ + 0 + ], + "data": { + "sk": "59bdfe33815be30aa982db912e276a9708b8ebae0d637ee3f049cab96da386eb", + "ssk": "28a154874a8e85ccf202137a615d0a294936b7bd8c07b09efedac43448cb3bb9", + "pk": "172eacc295eca56cadac17036c5e74706ec3518d34a327b8239b06d385a11ae1", + "cc": [ + 240, + 96, + 239, + 106, + 96, + 35, + 228, + 4, + 77, + 210, + 90, + 122, + 81, + 190, + 123, + 69, + 92, + 168, + 92, + 38, + 44, + 21, + 20, + 3, + 153, + 136, + 5, + 80, + 40, + 1, + 97, + 202 + ], + "cci": 0 + } + } + }, + { + "Private": { + "account_id": "HFpK548Z6tKX63eJx5rqx1nGQCT26aDDoF1vid3b7maG", + "chain_index": [], + "data": { + "value": [ + { + "secret_spending_key": [ + 155, + 230, + 10, + 193, + 84, + 81, + 44, + 178, + 64, + 253, + 206, + 211, + 59, + 60, + 128, + 114, + 204, + 41, + 222, + 58, + 104, + 160, + 107, + 38, + 65, + 74, + 20, + 153, + 189, + 37, + 233, + 110 + ], + "private_key_holder": { + "nullifier_secret_key": [ + 197, + 210, + 253, + 164, + 193, + 162, + 64, + 241, + 231, + 103, + 199, + 223, + 94, + 103, + 65, + 18, + 49, + 253, + 37, + 225, + 97, + 75, + 71, + 105, + 31, + 180, + 13, + 199, + 43, + 45, + 28, + 136 + ], + "viewing_secret_key": { + "d": [ + 141, + 89, + 20, + 42, + 13, + 222, + 221, + 135, + 91, + 157, + 8, + 53, + 120, + 195, + 77, + 190, + 50, + 217, + 223, + 200, + 14, + 239, + 100, + 98, + 116, + 82, + 105, + 196, + 168, + 124, + 2, + 165 + ], + "z": [ + 149, + 16, + 2, + 175, + 68, + 205, + 22, + 93, + 128, + 168, + 33, + 76, + 228, + 105, + 54, + 9, + 163, + 161, + 144, + 3, + 14, + 129, + 123, + 191, + 68, + 112, + 63, + 200, + 89, + 249, + 99, + 56 + ] + } + }, + "nullifier_public_key": [ + 44, + 3, + 219, + 14, + 230, + 120, + 218, + 184, + 19, + 254, + 10, + 1, + 146, + 169, + 33, + 162, + 62, + 113, + 200, + 252, + 26, + 44, + 200, + 81, + 33, + 11, + 3, + 110, + 14, + 224, + 18, + 83 + ], + "viewing_public_key": [ + 70, + 202, + 36, + 189, + 97, + 65, + 16, + 104, + 120, + 232, + 162, + 84, + 137, + 201, + 74, + 58, + 161, + 21, + 218, + 106, + 143, + 217, + 160, + 27, + 133, + 184, + 146, + 218, + 133, + 124, + 46, + 21, + 93, + 94, + 182, + 118, + 252, + 33, + 41, + 171, + 27, + 164, + 154, + 151, + 24, + 119, + 229, + 158, + 141, + 153, + 15, + 62, + 116, + 164, + 232, + 103, + 183, + 56, + 113, + 133, + 160, + 82, + 54, + 10, + 122, + 91, + 126, + 231, + 59, + 147, + 196, + 112, + 99, + 98, + 91, + 249, + 160, + 103, + 133, + 234, + 38, + 47, + 1, + 72, + 68, + 153, + 28, + 58, + 233, + 23, + 56, + 182, + 140, + 174, + 105, + 53, + 23, + 96, + 17, + 80, + 86, + 160, + 250, + 42, + 13, + 87, + 178, + 200, + 150, + 214, + 116, + 164, + 9, + 179, + 205, + 165, + 77, + 202, + 107, + 20, + 51, + 145, + 109, + 197, + 36, + 1, + 250, + 211, + 114, + 177, + 24, + 161, + 8, + 242, + 185, + 93, + 4, + 61, + 239, + 164, + 89, + 56, + 181, + 103, + 102, + 208, + 50, + 90, + 148, + 74, + 234, + 138, + 167, + 147, + 56, + 168, + 23, + 86, + 139, + 73, + 169, + 148, + 113, + 26, + 92, + 217, + 135, + 72, + 22, + 85, + 46, + 206, + 211, + 54, + 102, + 67, + 87, + 56, + 210, + 142, + 163, + 196, + 110, + 244, + 232, + 46, + 101, + 26, + 184, + 89, + 42, + 187, + 225, + 81, + 78, + 9, + 39, + 198, + 219, + 57, + 158, + 219, + 116, + 35, + 163, + 236, + 163, + 25, + 234, + 93, + 174, + 71, + 79, + 35, + 161, + 90, + 97, + 164, + 196, + 171, + 113, + 6, + 59, + 197, + 192, + 130, + 130, + 17, + 91, + 91, + 44, + 100, + 54, + 48, + 189, + 231, + 16, + 44, + 81, + 5, + 244, + 218, + 94, + 253, + 217, + 205, + 83, + 185, + 122, + 24, + 211, + 169, + 20, + 146, + 75, + 83, + 233, + 100, + 35, + 178, + 164, + 64, + 155, + 45, + 76, + 105, + 86, + 100, + 108, + 53, + 11, + 226, + 54, + 106, + 192, + 59, + 248, + 240, + 62, + 140, + 181, + 84, + 162, + 19, + 2, + 80, + 178, + 101, + 40, + 43, + 195, + 57, + 24, + 75, + 230, + 113, + 187, + 8, + 250, + 56, + 72, + 27, + 137, + 179, + 194, + 22, + 50, + 216, + 125, + 89, + 235, + 91, + 34, + 171, + 183, + 78, + 220, + 108, + 161, + 20, + 67, + 94, + 136, + 6, + 81, + 122, + 19, + 47, + 204, + 205, + 193, + 218, + 174, + 72, + 203, + 61, + 57, + 169, + 38, + 42, + 42, + 61, + 197, + 155, + 25, + 101, + 212, + 109, + 198, + 166, + 19, + 57, + 225, + 64, + 243, + 251, + 1, + 33, + 181, + 145, + 21, + 145, + 185, + 221, + 215, + 146, + 63, + 18, + 196, + 178, + 172, + 98, + 253, + 194, + 22, + 169, + 64, + 147, + 213, + 21, + 199, + 68, + 105, + 42, + 115, + 64, + 92, + 6, + 107, + 178, + 76, + 161, + 96, + 246, + 57, + 140, + 167, + 32, + 196, + 59, + 40, + 57, + 94, + 104, + 18, + 156, + 219, + 121, + 252, + 115, + 188, + 83, + 21, + 127, + 197, + 215, + 93, + 153, + 107, + 139, + 71, + 113, + 130, + 156, + 16, + 196, + 248, + 176, + 197, + 85, + 199, + 107, + 181, + 70, + 71, + 251, + 89, + 49, + 109, + 226, + 134, + 202, + 70, + 205, + 74, + 90, + 92, + 190, + 171, + 140, + 217, + 53, + 148, + 236, + 197, + 63, + 120, + 151, + 67, + 9, + 164, + 13, + 47, + 242, + 1, + 207, + 52, + 142, + 147, + 6, + 70, + 124, + 75, + 73, + 232, + 182, + 83, + 216, + 74, + 46, + 158, + 51, + 35, + 59, + 82, + 85, + 206, + 188, + 147, + 1, + 136, + 168, + 3, + 8, + 170, + 102, + 8, + 132, + 174, + 12, + 41, + 164, + 105, + 108, + 160, + 203, + 160, + 252, + 187, + 207, + 37, + 217, + 75, + 4, + 246, + 13, + 143, + 113, + 21, + 255, + 132, + 49, + 110, + 160, + 86, + 83, + 146, + 202, + 120, + 249, + 142, + 101, + 138, + 2, + 76, + 116, + 24, + 30, + 203, + 165, + 9, + 151, + 39, + 110, + 41, + 121, + 209, + 39, + 162, + 252, + 212, + 40, + 7, + 72, + 186, + 56, + 0, + 64, + 116, + 200, + 40, + 49, + 58, + 123, + 133, + 64, + 163, + 161, + 136, + 164, + 220, + 7, + 137, + 201, + 246, + 133, + 4, + 85, + 164, + 11, + 72, + 55, + 198, + 103, + 31, + 240, + 69, + 137, + 121, + 35, + 181, + 57, + 17, + 177, + 150, + 172, + 197, + 242, + 231, + 172, + 198, + 84, + 153, + 79, + 163, + 108, + 77, + 128, + 8, + 79, + 133, + 155, + 150, + 103, + 58, + 247, + 154, + 171, + 176, + 19, + 27, + 163, + 177, + 201, + 162, + 196, + 142, + 119, + 150, + 28, + 149, + 185, + 178, + 56, + 70, + 18, + 165, + 48, + 50, + 3, + 28, + 189, + 129, + 114, + 40, + 226, + 128, + 31, + 47, + 100, + 35, + 226, + 0, + 178, + 254, + 160, + 56, + 215, + 140, + 70, + 81, + 80, + 111, + 167, + 195, + 135, + 90, + 2, + 112, + 217, + 152, + 87, + 179, + 107, + 74, + 82, + 11, + 95, + 149, + 70, + 102, + 60, + 150, + 1, + 9, + 156, + 126, + 200, + 103, + 187, + 30, + 88, + 1, + 87, + 18, + 20, + 107, + 215, + 142, + 208, + 34, + 70, + 154, + 101, + 102, + 167, + 133, + 22, + 156, + 204, + 33, + 188, + 22, + 123, + 209, + 217, + 145, + 242, + 161, + 138, + 30, + 23, + 169, + 138, + 10, + 137, + 77, + 117, + 201, + 58, + 66, + 60, + 154, + 210, + 171, + 13, + 193, + 118, + 212, + 213, + 98, + 169, + 181, + 48, + 255, + 49, + 182, + 157, + 228, + 115, + 95, + 72, + 34, + 101, + 229, + 36, + 181, + 40, + 197, + 188, + 169, + 70, + 226, + 72, + 161, + 233, + 130, + 187, + 211, + 216, + 131, + 197, + 231, + 44, + 32, + 38, + 54, + 12, + 139, + 83, + 88, + 129, + 198, + 108, + 133, + 197, + 19, + 75, + 162, + 168, + 202, + 178, + 206, + 131, + 116, + 159, + 211, + 196, + 7, + 7, + 76, + 211, + 172, + 164, + 6, + 220, + 114, + 208, + 89, + 69, + 21, + 69, + 176, + 237, + 247, + 172, + 219, + 218, + 1, + 5, + 177, + 53, + 228, + 152, + 109, + 215, + 243, + 192, + 145, + 169, + 68, + 231, + 54, + 162, + 38, + 214, + 116, + 62, + 233, + 1, + 134, + 199, + 196, + 23, + 144, + 194, + 166, + 171, + 140, + 17, + 180, + 8, + 49, + 232, + 83, + 22, + 69, + 44, + 87, + 151, + 173, + 124, + 90, + 191, + 33, + 23, + 167, + 11, + 145, + 144, + 147, + 198, + 93, + 77, + 134, + 61, + 51, + 84, + 2, + 73, + 166, + 73, + 44, + 84, + 191, + 73, + 104, + 58, + 177, + 32, + 40, + 48, + 42, + 192, + 85, + 231, + 118, + 246, + 196, + 11, + 127, + 216, + 189, + 63, + 131, + 90, + 172, + 147, + 173, + 18, + 20, + 184, + 30, + 25, + 68, + 83, + 240, + 151, + 228, + 132, + 79, + 145, + 148, + 74, + 128, + 184, + 24, + 67, + 49, + 43, + 36, + 216, + 125, + 29, + 35, + 49, + 217, + 5, + 183, + 94, + 153, + 14, + 72, + 134, + 33, + 73, + 200, + 141, + 21, + 203, + 5, + 198, + 5, + 3, + 136, + 202, + 50, + 245, + 214, + 95, + 76, + 36, + 29, + 206, + 138, + 158, + 118, + 150, + 52, + 138, + 198, + 37, + 225, + 200, + 85, + 74, + 245, + 21, + 94, + 85, + 134, + 142, + 17, + 78, + 205, + 28, + 207, + 246, + 50, + 156, + 206, + 11, + 109, + 200, + 151, + 161, + 198, + 167, + 96, + 106, + 100, + 74, + 183, + 187, + 192, + 17, + 232, + 81, + 61, + 22, + 64, + 196, + 72, + 116, + 25, + 130, + 189, + 145, + 22, + 98, + 60, + 186, + 144, + 172, + 39, + 206, + 40, + 92, + 193, + 174, + 163, + 124, + 148, + 152, + 29, + 212, + 81, + 73, + 108, + 247, + 207, + 54, + 147, + 160, + 124, + 5, + 177, + 153, + 108, + 200, + 0, + 75, + 49, + 203, + 226, + 133, + 31, + 172, + 82, + 52, + 56, + 20, + 242, + 18, + 172, + 246, + 230, + 80, + 183, + 122, + 44, + 161, + 144, + 1, + 239, + 228, + 58, + 196, + 104, + 45, + 252, + 18, + 8, + 245, + 102, + 134, + 8, + 69, + 196, + 166, + 96, + 101, + 80, + 209, + 81, + 209, + 225, + 129, + 208, + 73, + 203, + 49, + 230, + 12, + 232, + 217, + 146, + 99, + 33, + 122, + 241, + 148, + 158, + 182, + 233, + 199, + 119, + 55, + 190, + 76, + 80, + 107, + 167, + 80, + 118, + 103, + 76, + 127, + 29, + 220, + 61, + 189, + 231, + 101, + 241, + 186, + 87, + 47, + 38, + 132, + 29, + 121, + 148, + 46, + 130, + 77, + 187, + 99, + 14, + 109, + 55, + 169, + 85, + 98, + 235, + 3, + 251, + 107, + 250, + 92, + 181, + 150, + 245, + 90, + 47, + 18, + 84, + 103, + 132, + 236, + 67, + 207, + 173, + 54, + 224, + 203, + 233, + 23, + 151, + 41, + 33, + 82, + 177, + 224 + ] + }, + [ + [ + { + "Regular": 0 + }, + { + "program_owner": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "balance": 0, + "data": [], + "nonce": 0 + } + ] + ] + ], + "ccc": [ + 73, + 214, + 125, + 6, + 7, + 206, + 78, + 245, + 8, + 247, + 96, + 170, + 155, + 95, + 248, + 206, + 192, + 5, + 126, + 53, + 243, + 200, + 249, + 148, + 144, + 248, + 53, + 203, + 91, + 215, + 178, + 224 + ], + "cci": null + } + } + } + ], + "sealing_secret_key": null, + "group_key_holders": {}, + "shared_private_accounts": {} + }, + "last_synced_block": 730, + "labels": {} +} \ No newline at end of file diff --git a/docker/testnet/mix_e2e/bootstrap.sh b/docker/testnet/mix_e2e/bootstrap.sh new file mode 100755 index 0000000..6a3837a --- /dev/null +++ b/docker/testnet/mix_e2e/bootstrap.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# One-shot bootstrap for the RLN-over-mix sim, runnable by any dev with only +# Docker + git installed. Run it from a clone of logos-rln-mix-sim: +# +# git clone https://github.com/logos-co/logos-rln-mix-sim.git +# cd logos-rln-mix-sim +# bash docker/testnet/mix_e2e/bootstrap.sh +# cd docker/testnet/mix_e2e && bash orchestrate.sh +# +# It clones the four sibling repos (next to this one), builds the Linux libp2p +# .lgx, and builds the base image tagged `lp2p-mix-e2e` (logoscore + wallet/rln +# modules + the baked deployment profile — all fetched by the image build). +# +# All repos are public and clone anonymously over HTTPS — no keys or env vars +# needed. Contributors who prefer SSH can override the clone bases: +# REPO_BASE=git@github.com:adklempner LOGOS_REPO_BASE=git@github.com:logos-co ... +set -euo pipefail + +# The mix stack lives on adklempner forks pending upstreaming; the gifter is a +# logos-co repo. +FORK_BASE="${REPO_BASE:-https://github.com/adklempner}" +LOGOS_BASE="${LOGOS_REPO_BASE:-https://github.com/logos-co}" +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$HERE/../../.." && pwd)" # logos-rln-mix-sim +SIBLINGS="$(cd "$REPO_ROOT/.." && pwd)" # parent dir = sibling root + +command -v docker >/dev/null || { echo "docker is required"; exit 1; } +command -v git >/dev/null || { echo "git is required"; exit 1; } + +clone(){ # repo branch base + if [ -d "$SIBLINGS/$1/.git" ]; then + echo " $1 already present" + else + echo " cloning $1 ($2)" + git clone --depth 1 -b "$2" "$3/$1.git" "$SIBLINGS/$1" + fi +} + +echo "=== 1/3 clone sibling repos into $SIBLINGS ===" +clone logos-libp2p-module rebase/enable-mix "$FORK_BASE" +clone mix-rln-spam-protection-plugin feat/cbind-rln "$FORK_BASE" +clone nim-libp2p-mix rebase/mix-cbind "$FORK_BASE" +clone logos-rln-gifter master "$LOGOS_BASE" + +echo "=== 2/3 build the Linux libp2p .lgx (~6-15 min) ===" +LOGOS_ROOT="$SIBLINGS" bash "$REPO_ROOT/docker/build_lgx_linux.sh" + +echo "=== 3/3 build the base image lp2p-mix-e2e (~30 min first time) ===" +docker build -f "$REPO_ROOT/docker/Dockerfile.testnet-e2e" -t lp2p-mix-e2e "$REPO_ROOT" + +cat < gifter: holds the funded wallet, self-allocates its membership, then +# serves /logos/rln/membership/1.0.0 to gift the other nodes. +# relay2/3, dest, sender -> gifter clients: authenticate (EIP-191) and receive a +# gifted on-chain membership; they never fund/sign a tx. All 5 end up +# RLN members (per-hop mix RLN: verify + regenerate at each hop). +# All services share one image (lp2p-mix-e2e) + the freshly built .lgx, mounted +# read-only and installed by entrypoint.sh. +x-node: &node + # lp2p-mix-e2e is built by docker/Dockerfile.testnet-e2e (bootstrap.sh step 3): + # logoscore + flat, loadable wallet+rln .lgx bundles (with their ffi nix-store + # deps) + the baked deployment profile at /testnet. The entrypoint overlays the + # freshly built libp2p_module .lgx (the one with LIBP2P_LISTEN_ADDRS support). + image: lp2p-mix-e2e + environment: + QT_QPA_PLATFORM: offscreen + LIBP2P_LISTEN_ADDRS: /ip4/0.0.0.0/tcp/9000 + NSSA_WALLET_HOME_DIR: /testnet + volumes: + - ../../lp2p-out/libp2p_module.lgx:/artifacts/libp2p_module.lgx:ro + - ./entrypoint.sh:/entrypoint.sh:ro + entrypoint: ["bash", "/entrypoint.sh"] + networks: [mixnet] + +services: + relay1: { <<: *node } + relay2: { <<: *node } + relay3: { <<: *node } + dest: { <<: *node } + sender: { <<: *node } + +networks: + mixnet: + driver: bridge diff --git a/docker/testnet/mix_e2e/entrypoint.sh b/docker/testnet/mix_e2e/entrypoint.sh new file mode 100755 index 0000000..baeec53 --- /dev/null +++ b/docker/testnet/mix_e2e/entrypoint.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Per-container entrypoint: install the freshly-built libp2p_module .lgx (mounted +# at /artifacts) over the image's baked copy, then run the logoscore daemon in +# the foreground so the container stays up. The orchestrator drives roles via +# `docker compose exec` afterward. +# +# Listen on THIS container's IP only (not 0.0.0.0). Binding 0.0.0.0 makes the node +# advertise BOTH 127.0.0.1 and the container IP, and the mix then sometimes routes +# a next-hop / SURB-reply to 127.0.0.1 -> it dials its own loopback -> Noise +# peer-id mismatch -> dropped. Advertising only the container IP avoids that. +set -euo pipefail +export PATH=/rttools/bin:$PATH + +ip=$(python3 -c 'import socket +s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) +try: + s.connect(("10.255.255.255",1)); print(s.getsockname()[0]) +except Exception: print("") +finally: s.close()' 2>/dev/null) +if [ -n "$ip" ]; then + export LIBP2P_LISTEN_ADDRS="/ip4/$ip/tcp/9000" +fi + +if [ -f /artifacts/libp2p_module.lgx ]; then + lgx=/artifacts/libp2p_module.lgx + name=$(tar xzOf "$lgx" manifest.json | python3 -c 'import json,sys;print(json.load(sys.stdin)["name"])') + tmp=$(mktemp -d); tar xzf "$lgx" -C "$tmp" + var=$(ls "$tmp/variants" | head -1) + rm -rf "/modules/$name"; mkdir -p "/modules/$name" + cp "$tmp/manifest.json" "/modules/$name/" + cp -L "$tmp/variants/$var/"* "/modules/$name/" + printf '%s' "$var" > "/modules/$name/variant" +fi +echo "[entrypoint] installed module; listen=${LIBP2P_LISTEN_ADDRS:-default}" + +exec /logoscore/bin/logoscore -m /modules -D diff --git a/docker/testnet/mix_e2e/fixtures/gifter_auth/addresses.env b/docker/testnet/mix_e2e/fixtures/gifter_auth/addresses.env new file mode 100644 index 0000000..2830139 --- /dev/null +++ b/docker/testnet/mix_e2e/fixtures/gifter_auth/addresses.env @@ -0,0 +1,6 @@ +# Ethereum addresses derived from keys.env. Keep in sync if those keys change. +ADDR_MIX2=0x8e3d4d0a713087e2263e2fcdec894c283c777dcc +ADDR_MIX3=0xca282bbf8bf3636e15af3ad8caf11cdd38bf35d8 +ADDR_SENDER=0x0b6872aaae7a2d4f3c701793cde57b93337f4d4a +ADDR_RECEIVER=0xb5dda07309f5ab06e0847f6036c305ea9ae26937 +ADDR_RECEIVER2=0x98d57890bd155475735648235a4f099e63776b85 diff --git a/docker/testnet/mix_e2e/fixtures/gifter_auth/keys.env b/docker/testnet/mix_e2e/fixtures/gifter_auth/keys.env new file mode 100644 index 0000000..ea1d01a --- /dev/null +++ b/docker/testnet/mix_e2e/fixtures/gifter_auth/keys.env @@ -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_MIX2=b880df1f571109e646f641636794dfe7ffefc2aab19290ba0d720c407758304d +KEY_MIX3=0b1b5e18839a3e15b119519092e4a94a71122acf57d8b2e1014df0121cb6f0ea +KEY_SENDER=5284ac01fed5fcb6b26933ac4a901412b66fcd7ee5b945b799f147a3b42f49ef +KEY_RECEIVER=a5619d6bfde09f54165ec9da55a7be7380f1b258c8279177dbda5ac235d0e904 +KEY_RECEIVER2=3d48489ee2bb7e13a13442ec0e17798c77befb7c635b0b53cf91b2c933869916 diff --git a/docker/testnet/mix_e2e/keys.py b/docker/testnet/mix_e2e/keys.py new file mode 100755 index 0000000..d044bb4 --- /dev/null +++ b/docker/testnet/mix_e2e/keys.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# Key helpers for the multi-node mix E2E. Pure-python (no external deps) so it +# runs on the host or in any container: +# mixpub -> curve25519 mix public key hex (RFC7748 X25519, matches +# nim-libp2p public(priv)) +# peerpub -> secp256k1 libp2p public key hex, decoded from the +# peerId (libp2p inlines secp256k1 pubkeys via the +# identity multihash) +import sys + +# ---- base58 (btc alphabet) ---- +_B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + +def b58decode(s: str) -> bytes: + n = 0 + for c in s: + n = n * 58 + _B58.index(c) + full = n.to_bytes((n.bit_length() + 7) // 8, "big") + pad = len(s) - len(s.lstrip("1")) + return b"\x00" * pad + full + +# ---- RFC 7748 X25519 ---- +_P = 2 ** 255 - 19 + +def _clamp(k: bytes) -> int: + k = bytearray(k) + k[0] &= 248 + k[31] &= 127 + k[31] |= 64 + return int.from_bytes(k, "little") + +def _x25519(scalar: int, u: int) -> int: + x1 = u + x2, z2, x3, z3 = 1, 0, u, 1 + swap = 0 + for t in range(254, -1, -1): + kt = (scalar >> t) & 1 + swap ^= kt + if swap: + x2, x3 = x3, x2 + z2, z3 = z3, z2 + swap = kt + A = (x2 + z2) % _P + AA = (A * A) % _P + B = (x2 - z2) % _P + BB = (B * B) % _P + E = (AA - BB) % _P + C = (x3 + z3) % _P + D = (x3 - z3) % _P + DA = (D * A) % _P + CB = (C * B) % _P + x3 = pow((DA + CB) % _P, 2, _P) + z3 = (x1 * pow((DA - CB) % _P, 2, _P)) % _P + x2 = (AA * BB) % _P + z2 = (E * ((AA + (121665 * E) % _P) % _P)) % _P + if swap: + x2, x3 = x3, x2 + z2, z3 = z3, z2 + return (x2 * pow(z2, _P - 2, _P)) % _P + +def mixpub(privhex: str) -> str: + priv = bytes.fromhex(privhex) + pub = _x25519(_clamp(priv), 9) + return pub.to_bytes(32, "little").hex() + +def peerpub(peerid: str) -> str: + raw = b58decode(peerid) + # identity multihash: 0x00 + assert raw[0] == 0x00, "peerId is not an identity multihash (key not inlined)" + ln = raw[1] + pb = raw[2:2 + ln] + # PublicKey protobuf: field1 = KeyType (varint), field2 = Data (bytes) + i = 0 + keytype = None + data = None + while i < len(pb): + tag = pb[i]; i += 1 + field = tag >> 3 + wt = tag & 7 + if wt == 0: # varint + v = 0; shift = 0 + while True: + b = pb[i]; i += 1 + v |= (b & 0x7F) << shift + if not (b & 0x80): + break + shift += 7 + if field == 1: + keytype = v + elif wt == 2: # length-delimited + ln2 = pb[i]; i += 1 + chunk = pb[i:i + ln2]; i += ln2 + if field == 2: + data = chunk + assert keytype == 2, f"expected Secp256k1 key type (2), got {keytype}" + assert data is not None and len(data) == 33, f"bad secp256k1 key len {len(data) if data else None}" + return data.hex() + +if __name__ == "__main__": + cmd = sys.argv[1] + if cmd == "mixpub": + print(mixpub(sys.argv[2])) + elif cmd == "peerpub": + print(peerpub(sys.argv[2])) + else: + sys.exit("usage: keys.py mixpub | peerpub ") diff --git a/docker/testnet/mix_e2e/orchestrate.sh b/docker/testnet/mix_e2e/orchestrate.sh new file mode 100755 index 0000000..327d327 --- /dev/null +++ b/docker/testnet/mix_e2e/orchestrate.sh @@ -0,0 +1,376 @@ +#!/usr/bin/env bash +# Run the 5-node gifted-RLN-over-mix E2E on testnet, via docker-compose (one +# logoscore daemon per container, all on a shared network). +# +# Every node obtains a DISTINCT RLN membership through a GIFTER +# (membership-allocation, LIP-158): relay1 is the gifter, the ONLY node holding +# the funded wallet. It self-allocates its own membership, then serves +# /logos/rln/membership/1.0.0; the other 4 nodes authenticate with an EIP-191- +# signed request and receive a gifted on-chain registration — they never fund or +# sign a tx. Every mix node ends up a member (per-hop RLN: each hop verifies the +# incoming proof AND regenerates one for the next hop). Then src and dest each do +# 3 request/reply round-trips over the 3-hop mix, RLN-enforced on both legs. +# +# Gifted allocation: the client derives its own identity locally (only the +# idCommitment is sent; the RLN secret never leaves the node). The gifter funds +# and signs register_member with its own wallet and returns the leaf. Distinct +# seeds -> distinct leaves. Registrations are serialized (each client's on-chain +# confirmation barrier passes before the next requests) to avoid nonce races on +# the single gifter wallet. +# +# The only knob is NEG (negative enforcement tests): +# NEG=0 (default) : the full happy-path E2E. +# NEG=1 : leave the SENDER UNREGISTERED (never asks the gifter). Its mixDial +# must be rejected (no valid proof) and not reach the dest. +# NEG=2 : the sender asks the gifter with a NON-allowlisted key -> auth refused +# -> sender stays unregistered -> rejected. Exercises the allocation +# authentication gate specifically. +# Both prove RLN gates delivery (vs the happy path where a member's msg lands). +# +# Roles: relay1 (gifter+relay) + relay2/relay3 + dest + sender — ALL RLN members. +# Setup order: rlnEnable MUST precede mixSetNodeInfo (factory read at mix mount). +# Mesh keys host-derived (keys.py). Node addr = /ip4//tcp/9000. +# bash 3.2 (macOS): no assoc arrays; via sv/gv. +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DC="docker compose -f $HERE/docker-compose.yml" +KEYS="python3 $HERE/keys.py" +LOGOSCORE=/logoscore/bin/logoscore +NEG="${NEG:-0}" + +# Fixed sim parameters (RATE must match the baked deployment's rate limit). +MSG_COUNT=3 +PROTO="/ipfs/ping/1.0.0" +READ_SIZE=32 +RATE=100 +RPC_URL="https://testnet.lez.logos.co/" +SYNC_STEP=3000 +REG_RETRY_SLEEP=15 +ALL="relay1 relay2 relay3 dest sender" +WALLET_MOD="logos_execution_zone" +RLN_MOD="liblogos_rln_module" +GIFTER_CODEC="/logos/rln/membership/1.0.0" + +# relay1 is the gifter (membership provider). The other 4 nodes authenticate to +# it with a distinct EIP-191 key from the fixtures to receive a gifted on-chain +# registration. Fixtures are sourced host-side (orchestrate runs on the host); +# these keys never enter the image. relay1 needs no client key. +GIFTER="relay1" +FIX="$HERE/fixtures/gifter_auth" +[ -f "$FIX/keys.env" ] && . "$FIX/keys.env" +[ -f "$FIX/addresses.env" ] && . "$FIX/addresses.env" +gifter_authkey(){ case "$1" in + relay2) printf '%s' "${KEY_MIX2:-}";; + relay3) printf '%s' "${KEY_MIX3:-}";; + dest) printf '%s' "${KEY_RECEIVER:-}";; + sender) printf '%s' "${KEY_SENDER:-}";; + *) printf '';; esac; } +# The gifter's allowlist = the 4 client addresses (JSON array for rlnGifterServe). +GIFTER_ALLOWLIST="${ADDR_MIX2:-},${ADDR_MIX3:-},${ADDR_RECEIVER:-},${ADDR_SENDER:-}" +# A key deliberately NOT on the allowlist, for the NEG=2 refusal test. +NEG2_KEY="${KEY_RECEIVER2:-}" + +sv(){ eval "_${1}_${2}=\"\$3\""; } +gv(){ eval "printf '%s' \"\${_${1}_${2}:-}\""; } +dexec(){ local svc="$1"; shift; $DC exec -T "$svc" "$@" 2>&1; } +jcall(){ local svc="$1" mod="$2" meth="$3" json="$4" + printf '%s' "$json" | $DC exec -T "$svc" sh -c 'cat > /tmp/arg.json' + dexec "$svc" "$LOGOSCORE" --json call "$mod" "$meth" @/tmp/arg.json +} +call(){ local svc="$1" mod="$2" meth="$3"; shift 3; dexec "$svc" "$LOGOSCORE" --json call "$mod" "$meth" "$@"; } +lc(){ local svc="$1"; shift; dexec "$svc" "$LOGOSCORE" "$@"; } +jval(){ python3 -c 'import json,sys +try: + d=json.load(sys.stdin); r=d.get("result"); print(r.get("value") if isinstance(r,dict) else r) +except Exception: print("ERR")'; } +parse_leaf_idc(){ python3 -c 'import json,sys +try: + v=json.load(sys.stdin)["result"]["value"]; print("lopt=%s; idc=%s"%(v["leaf_index"],v["id_commitment"])) +except Exception: print("lopt=ERR; idc=")'; } +svc_ip(){ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$($DC ps -q "$1")"; } +chain_head(){ curl -s -m 15 -X POST "$RPC_URL" -H 'content-type: application/json' --data '{"jsonrpc":"2.0","method":"getLastBlockId","params":[],"id":1}' | python3 -c 'import json,sys;print(json.load(sys.stdin)["result"])'; } +sync_wallet(){ local svc="$1" head cur n + head=$(chain_head); cur=$(call "$svc" "$WALLET_MOD" get_last_synced_block | jval) + while [ "$cur" != "$head" ] 2>/dev/null; do + local tgt=$((cur+SYNC_STEP)); [ $tgt -gt $head ] && tgt=$head + call "$svc" "$WALLET_MOD" sync_to_block $tgt >/dev/null 2>&1 + n=$(call "$svc" "$WALLET_MOD" get_last_synced_block | jval); [ "$n" = "$cur" ] && break; cur=$n + done + echo "$cur" +} + +# Diagnose a failed/again-unconfirmed registration by scanning the node's logs +# for the rln program's assert strings, then print the exact remediation. Set +# LEZ_RLN_DIR to your logos-lez-rln clone so the printed commands show real paths. +LEZ_RLN_DIR="${LEZ_RLN_DIR:-}" +diagnose_reg(){ local svc="$1"; local logs + logs=$($DC logs --since 900s "$svc" 2>&1) + echo " !! RLN registration for '$svc' did not confirm on-chain." >&2 + if echo "$logs" | grep -qiE "Insufficient balance|may be out of funds|range end index 49"; then + cat >&2 < provision a brand-new tree (see "tree full" below). +EOF + elif echo "$logs" | grep -qiE "Would exceed max total rate limit|max_total_rate_limit"; then + cat >&2 < + 3. docker build -f docker/Dockerfile.testnet-e2e \\ + --build-arg DEPLOYMENT= -t lp2p-mix-e2e . + To reuse the same accounts across sims, add --adopt-wallet in (2). + See docker/testnet/deployments/README.md for the full flow. +EOF + else + cat >&2 < re-run run_setup (docker/testnet/deployments/README.md). +EOF + fi +} + +# On-chain confirmation barrier + readiness gate for a membership (used by BOTH +# the gifter's self-allocation and each gifted client). Waits until the rln +# module reports registered:true for our idCommitment on the CANONICAL tree +# BEFORE the caller proceeds, so the next membership lands on a DISTINCT leaf +# (rlnIsReady alone was unreliable: get_merkle_proofs returns a proof for the +# optimistic leaf before the tree advances, so leaves collided). Reads the ACTUAL +# leaf and flags any mismatch with the optimistic one. Args: svc idc lopt pid. +confirm_and_ready(){ local s="$1" idc="$2" lopt="$3" pid="$4" res lact="" conf=false rdy=False flag="" + for w in $(seq 1 80); do + res=$(call "$s" "$RLN_MOD" is_member_registered "$CONFIG_ACCT" "$idc") + eval "$(echo "$res" | python3 -c 'import json,sys +try: + r=json.loads(json.load(sys.stdin)["result"]); print("conf=%s; lact=%s"%(str(r.get("registered",False)).lower(), r.get("leaf_index",""))) +except Exception: print("conf=false; lact=")')" + [ "$conf" = "true" ] && break + sleep 10 + done + if [ "$conf" != "true" ]; then diagnose_reg "$s"; exit 1; fi + for w in $(seq 1 40); do rdy=$(call "$s" libp2p_module rlnIsReady | jval); [ "$rdy" = "True" ] && break; sleep 10; sync_wallet "$s" >/dev/null 2>&1; done + [ "$lopt" != "$lact" ] && flag=" !! LEAF MISMATCH (proof for $lopt, actual $lact)" + echo " $s peerId=${pid:-EMPTY} leaf_opt=$lopt leaf_actual=$lact confirmed=$conf rlnIsReady=$rdy$flag" +} + +# Root-convergence barrier: run AFTER all registrations, BEFORE the exchange. +# confirm_and_ready makes each node ready at the tree state of ITS OWN +# registration, but every later registration advances the Merkle tree to a new +# root. An earlier node's verification-side valid-roots window (the group +# manager's rootTracker, refreshed on the module's ~epoch proof-refresh timer) +# then lags the newest root — so as a mix hop it rejects a proof built with that +# root ("invalid Merkle root") and silently drops the message (seen as a missing +# reply). Re-sync every wallet to head, wait until all nodes read the SAME +# on-chain valid-roots set, then give the proof-refresh timers one epoch to +# propagate that set into every verifier's window. +roots_sig(){ call "$1" "$RLN_MOD" get_valid_roots "$CONFIG_ACCT" | python3 -c 'import json,sys +try: + d=json.load(sys.stdin); r=d.get("result"); a=json.loads(r) if isinstance(r,str) else r + print(",".join(sorted(x.lower() for x in a)) if a else "EMPTY") +except Exception: print("ERR")'; } +converge_roots(){ local s sig first ok + for s in $ALL; do sync_wallet "$s" >/dev/null 2>&1; done + for w in $(seq 1 24); do + first=""; ok=1 + for s in $ALL; do + sig=$(roots_sig "$s") + case "$sig" in ""|ERR|EMPTY) ok=0;; esac + if [ -z "$first" ]; then first="$sig"; elif [ "$sig" != "$first" ]; then ok=0; fi + done + [ "$ok" = "1" ] && break + sleep 5 + done + if [ "$ok" = "1" ]; then echo " valid-roots converged across all 5 nodes; settling one epoch for verifier windows" + else echo " !! valid-roots did not fully converge in time — proceeding (a first-hop proof reject may drop one round-trip)" >&2; fi + sleep 12 +} + +echo "=== up: 5 daemons (force-recreate for FRESH daemons) ===" +# Force-recreate so each run starts from clean daemons. Module state (e.g. the +# RLN SpamProtection factory registered by rlnEnable) is a process-global that +# lives as long as the daemon process; reusing a daemon would leak stale RLN +# state into the next run. +$DC down --remove-orphans >/dev/null 2>&1 +$DC up -d --force-recreate +for s in $ALL; do + for i in $(seq 1 90); do lc "$s" load-module libp2p_module >/dev/null 2>&1 && break; sleep 1; done +done + +# Config + funder accounts come from the baked deployment profile (/testnet). +CONFIG_ACCT=$(dexec sender sh -c 'tr -d "\n\r" < /testnet/config_account.txt') +HOLDING_ACCT=$(dexec sender sh -c 'tr -d "\n\r" < /testnet/payment_account.txt') +echo " config=$CONFIG_ACCT holding(funder)=$HOLDING_ACCT" + +echo "=== per-node setup (load chain -> wallet+rln -> start -> mixSetNodeInfo -> peerInfo -> register) ===" +for s in $ALL; do + lc "$s" load-module "$WALLET_MOD" >/dev/null 2>&1 + lc "$s" load-module "$RLN_MOD" >/dev/null 2>&1 + lc "$s" load-module libp2p_module >/dev/null 2>&1 + priv=$(python3 -c 'import os;print(os.urandom(32).hex())'); sv MIXPRIV "$s" "$priv" + sv MIXPUB "$s" "$($KEYS mixpub "$priv")" + ip=$(svc_ip "$s"); sv MADDR "$s" "/ip4/$ip/tcp/9000" + + dexec "$s" sh -c '[ -f /testnet/storage.json ] || cp /testnet/storage.json.seed /testnet/storage.json' + call "$s" "$WALLET_MOD" open /testnet/wallet_config.json /testnet/storage.json >/dev/null 2>&1 + synced=$(sync_wallet "$s"); echo " $s wallet synced to $synced" + jcall "$s" libp2p_module rlnEnable "{\"useOnchainLEZ\":true,\"configAccount\":\"$CONFIG_ACCT\",\"userMessageLimit\":$RATE,\"epochDurationSeconds\":10.0}" >/dev/null 2>&1 + + call "$s" libp2p_module start >/dev/null 2>&1 + jcall "$s" libp2p_module mixSetNodeInfo "{\"multiaddr\":\"$(gv MADDR "$s")\",\"mixPrivKeyHex\":\"$priv\"}" >/dev/null 2>&1 + pid=$(call "$s" libp2p_module peerInfo | python3 -c 'import json,sys +try: print(json.load(sys.stdin)["result"]["value"]["peerId"]) +except Exception: print("")') + sv PEERID "$s" "$pid" + sv LPPUB "$s" "$($KEYS peerpub "$pid" 2>/dev/null || echo DECODE_FAIL)" + + if [ "$s" = "$GIFTER" ]; then + # relay1 = the membership provider (gifter). It holds the funded wallet, so + # it self-allocates its OWN membership (register_member funded/signed by its + # wallet), confirms on-chain, then mounts the gifter service the other nodes + # authenticate to. Retry transient sequencer failures; register_member is + # idempotent on the same seed. A persistent failure is diagnosed. + seed=$(python3 -c 'import os;print(os.urandom(32).hex())') + idc=""; lopt=""; reg="" + for attempt in 1 2 3 4; do + reg=$(jcall "$s" libp2p_module rlnRegister "{\"config\":\"$CONFIG_ACCT\",\"wallet\":\"$HOLDING_ACCT\",\"seed\":\"$seed\",\"rate\":$RATE}") + eval "$(echo "$reg" | parse_leaf_idc)" + [ -n "$idc" ] && break + echo " $s rlnRegister attempt $attempt failed ($reg) — re-sync + retry in ${REG_RETRY_SLEEP}s" >&2 + sync_wallet "$s" >/dev/null 2>&1; sleep "$REG_RETRY_SLEEP" + done + if [ -z "$idc" ]; then echo " rlnRegister response: $reg" >&2; diagnose_reg "$s"; exit 1; fi + confirm_and_ready "$s" "$idc" "$lopt" "$pid" + # Mount the gifter service (allowlist auth). Clients dial this codec directly + # (by peerId+multiaddr, pre-mesh) to obtain a gifted membership. + al=$(python3 -c 'import json,sys; print(json.dumps([a for a in sys.argv[1].split(",") if a]))' "$GIFTER_ALLOWLIST") + jcall "$s" libp2p_module rlnGifterServe "{\"config\":\"$CONFIG_ACCT\",\"wallet\":\"$HOLDING_ACCT\",\"allowlist\":$al}" >/dev/null 2>&1 + echo " $s gifter service mounted ($GIFTER_CODEC, allowlist=4 clients)" + elif [ "$NEG" = "1" ] && [ "$s" = "sender" ]; then + # NEG=1: leave the sender UNREGISTERED (never asks the gifter). rlnEnable + + # mix are set up above; we just skip the allocation request. + echo " $s peerId=${pid:-EMPTY} UNREGISTERED (negative) rlnIsReady=$(call "$s" libp2p_module rlnIsReady | jval)" + elif [ "$NEG" = "2" ] && [ "$s" = "sender" ]; then + # NEG=2: sender asks the gifter with a NON-allowlisted key -> auth refused -> + # no membership. Exercises the allocation authentication gate specifically. + seed=$(python3 -c 'import os;print(os.urandom(32).hex())') + req=$(jcall "$s" libp2p_module rlnGifterRequest "{\"gifterPeerId\":\"$(gv PEERID "$GIFTER")\",\"gifterMultiaddr\":\"$(gv MADDR "$GIFTER")\",\"config\":\"$CONFIG_ACCT\",\"seed\":\"$seed\",\"authKey\":\"$NEG2_KEY\",\"rate\":$RATE}") + echo " $s peerId=${pid:-EMPTY} REFUSED (negative, non-allowlisted key) rlnIsReady=$(call "$s" libp2p_module rlnIsReady | jval)" + else + # Gifter client: authenticate (EIP-191 over our idCommitment) and request an + # allocation from relay1. We derive our identity locally — only the + # idCommitment is sent; the RLN secret never leaves this node. The gifter + # funds + signs the tx and returns the leaf; then we run the same on-chain + # confirmation barrier. Re-sync the GIFTER's wallet first so its next tx uses + # the freshest nonce (the previous client's registration is already sealed). + sync_wallet "$GIFTER" >/dev/null 2>&1 + ak=$(gifter_authkey "$s") + seed=$(python3 -c 'import os;print(os.urandom(32).hex())') + idc=""; lopt=""; req="" + for attempt in 1 2 3 4; do + req=$(jcall "$s" libp2p_module rlnGifterRequest "{\"gifterPeerId\":\"$(gv PEERID "$GIFTER")\",\"gifterMultiaddr\":\"$(gv MADDR "$GIFTER")\",\"config\":\"$CONFIG_ACCT\",\"seed\":\"$seed\",\"authKey\":\"$ak\",\"rate\":$RATE}") + eval "$(echo "$req" | parse_leaf_idc)" + [ -n "$idc" ] && break + echo " $s rlnGifterRequest attempt $attempt failed ($req) — re-sync gifter + retry in ${REG_RETRY_SLEEP}s" >&2 + sync_wallet "$GIFTER" >/dev/null 2>&1; sleep "$REG_RETRY_SLEEP" + done + if [ -z "$idc" ]; then echo " rlnGifterRequest response: $req" >&2; diagnose_reg "$GIFTER"; exit 1; fi + confirm_and_ready "$s" "$idc" "$lopt" "$pid" + fi +done + +echo "=== mesh: every node adds the other 4 ===" +for a in $ALL; do for b in $ALL; do [ "$a" = "$b" ] && continue + jcall "$a" libp2p_module mixNodepoolAdd \ + "{\"peerId\":\"$(gv PEERID "$b")\",\"multiaddr\":\"$(gv MADDR "$b")\",\"mixPubKey\":\"$(gv MIXPUB "$b")\",\"libp2pPubKey\":\"$(gv LPPUB "$b")\"}" >/dev/null 2>&1 +done; done +echo " meshed." + +echo "=== rlnIsReady status (each node was confirmed ready before the next registered) ===" +line=" "; for s in $ALL; do line="$line $s=$(call "$s" libp2p_module rlnIsReady | jval)"; done; echo "$line" + +echo "=== root convergence: wait until every node's valid-roots window includes the final root ===" +converge_roots + +echo "=== register dest-read-behavior on all nodes (the SURB exit is random) ===" +for s in $ALL; do + jcall "$s" libp2p_module mixRegisterDestReadBehavior "{\"proto\":\"$PROTO\",\"behavior\":0,\"sizeParam\":$READ_SIZE}" >/dev/null 2>&1 +done +echo " registered ($PROTO, READ_EXACTLY, $READ_SIZE bytes)" + +# One request/reply round-trip: dial-with-reply -> write request -> read the SURB +# reply -> close+release. Returns 0 iff a reply came back (read succeeded). N +# round-trips = N dials (the reply future is one-shot). +roundtrip(){ local from="$1" to="$2" idx="$3" dial sid payload rd + dial=$(jcall "$from" libp2p_module mixDialWithReply \ + "{\"peerId\":\"$(gv PEERID "$to")\",\"multiaddr\":\"$(gv MADDR "$to")\",\"proto\":\"$PROTO\",\"expectReply\":1,\"numSurbs\":1}") + sid=$(echo "$dial" | jval) + case "$sid" in ""|ERR|None) return 1;; esac + # ASCII payload exactly READ_SIZE bytes (ping echoes it back). + payload=$(python3 -c "print(('m%d-%s'%(${idx},'$from'))[:${READ_SIZE}].ljust(${READ_SIZE},'.'))") + call "$from" libp2p_module streamWrite "$sid" "$payload" >/dev/null 2>&1 + rd=$(call "$from" libp2p_module streamReadExactly "$sid" "$READ_SIZE") + call "$from" libp2p_module streamClose "$sid" >/dev/null 2>&1 + call "$from" libp2p_module streamRelease "$sid" >/dev/null 2>&1 + echo "$rd" | grep -q '"success":true' +} + +run_dir(){ local from="$1" to="$2" ok=0 i + for i in $(seq 1 "$MSG_COUNT"); do + roundtrip "$from" "$to" "$i" && ok=$((ok+1)) + done + echo " $from->$to: $ok/$MSG_COUNT replies received" + LAST_OK=$ok +} + +echo "=== exchange: $MSG_COUNT request/reply round-trip(s) per initiator ===" +run_dir sender dest; SD=$LAST_OK; DS=0 +# In a negative run only the (rejected) sender->dest direction is the test. +if [ "$NEG" = "0" ]; then run_dir dest sender; DS=$LAST_OK; fi +sleep 4 + +echo "=== observe: RLN proofs (forward request + SURB reply legs) ===" +vtot=0 +for n in $ALL; do + g=$($DC logs --since 240s "$n" 2>&1 | grep -c 'Generated RLN proof successfully') + v=$($DC logs --since 240s "$n" 2>&1 | grep -c 'Proof verified successfully') + echo " $n: generated=$g verified=$v"; vtot=$((vtot+v)) +done +sgen=$($DC logs --since 240s sender 2>&1 | grep -c 'Generated RLN proof successfully') +echo " replies: sender->dest=$SD dest->sender=$DS ; total verifications=$vtot ; sender proofs=$sgen" +# Gifted allocations succeeded at the gifter (relay1): one log line per client. +greg=$($DC logs --since 1800s "$GIFTER" 2>&1 | grep -c 'RLN gifter registration succeeded') +echo " gifter($GIFTER): 'RLN gifter registration succeeded' x$greg (expect 4 in the happy path)" + +echo "=== VERDICT ===" +if [ "$NEG" != "0" ]; then + if [ "$SD" = "0" ] && [ "$sgen" = "0" ]; then + echo " PASS (negative): sender got 0 replies and generated 0 proofs -> rejected (NEG=$NEG)." + else + echo " FAIL (negative): expected 0 replies / 0 sender proofs, got replies=$SD sgen=$sgen" + echo "DONE (NEG=$NEG)"; exit 1 + fi +else + exp="sender->dest=$MSG_COUNT dest->sender=$MSG_COUNT"; ok=1 + [ "$SD" = "$MSG_COUNT" ] || ok=0 + [ "$DS" = "$MSG_COUNT" ] || ok=0 + if [ "$ok" = "1" ]; then echo " PASS: every round-trip got a reply ($exp)." + else echo " FAIL: expected $exp, got sender->dest=$SD dest->sender=$DS"; echo "DONE (NEG=$NEG)"; exit 1; fi +fi +echo "DONE (NEG=$NEG)" diff --git a/docker/testnet/provision.sh b/docker/testnet/provision.sh new file mode 100755 index 0000000..7dae5f6 --- /dev/null +++ b/docker/testnet/provision.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Thin shim -> canonical logos-lez-rln/tools/deployments/provision.sh. +# FEATURE: deployment-profile tooling. +# +# LEZ_RLN_DIR=/path/to/logos-lez-rln bash provision.sh --name \ +# [--tree <64hex>] [--adopt-wallet ] [--sequencer ] +# +# Provisions on the sequencer and writes the new deployment into THIS repo's build +# context (docker/testnet/deployments/) so `docker build --build-arg DEPLOYMENT=` +# stays self-contained. The provisioning logic lives once, in logos-lez-rln. +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LEZ_RLN_DIR="${LEZ_RLN_DIR:?set LEZ_RLN_DIR to your logos-lez-rln checkout}" +exec bash "$LEZ_RLN_DIR/tools/deployments/provision.sh" --outdir "$HERE/deployments" "$@" diff --git a/docker/testnet/stage.sh b/docker/testnet/stage.sh new file mode 100755 index 0000000..f7c57f1 --- /dev/null +++ b/docker/testnet/stage.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# VENDORED from logos-lez-rln/tools/deployments/stage.sh — keep in sync with the +# canonical copy there. The image build must be self-contained (it runs stage.sh +# at build time, before logos-lez-rln is available), so this copy lives in the +# build context. jq is present in the image + on host. +# Stage a deployment descriptor into a flat fixture dir the daemons/binaries consume. +# FEATURE: deployment-profile tooling — one descriptor+wallet is the source of truth. +# +# bash stage.sh +# +# A deployment is fully captured by tree_id + wallet (storage.json). config is a +# derived cache of tree_id; payment/supply are pointers into the wallet. This emits +# the flat files (storage.json, wallet_config.json, {config,payment,supply}.txt, +# env.sh) enforcing the wallet<->deployment binding so a mismatched wallet fails +# here, not at runtime. Bash+jq (no Python) so every sim + the image build share it. +# The guest-drift guard (re-deriving config from the guest binaries) is verify.sh. +set -euo pipefail + +DEP_DIR="${1:?usage: stage.sh }" +OUT="${2:?usage: stage.sh }" +DESC="$DEP_DIR/deployment.json" +WALLET="$DEP_DIR/storage.json" +command -v jq >/dev/null || { echo "stage: FAIL: jq not found (apt install jq)" >&2; exit 1; } +[ -f "$DESC" ] || { echo "stage: FAIL: missing $DESC" >&2; exit 1; } +[ -f "$WALLET" ] || { echo "stage: FAIL: missing $WALLET" >&2; exit 1; } + +fail(){ echo "stage: FAIL: $1" >&2; exit 1; } +field(){ jq -re ".$1 // empty" "$DESC" 2>/dev/null || fail "descriptor missing required field '$1'"; } + +NAME=$(field name); TREE=$(field tree_id); SEQ=$(field sequencer) +CFG=$(field config_account); PAY=$(field payment_account); SUP=$(field supply_holding) +field registration_program_id >/dev/null +[[ "$TREE" =~ ^[0-9a-f]{64}$ ]] || fail "tree_id must be 64 lowercase hex chars, got '$TREE'" + +# rc6 wallet schema — refuse a wallet whose schema doesn't match the guest version. +jq -e '.key_chain.accounts' "$WALLET" >/dev/null 2>&1 \ + || fail "wallet schema is not rc6 (expected top-level 'key_chain.accounts')" +# wallet<->deployment binding: the wallet must actually hold payment + supply. +holds(){ jq -e --arg a "$1" 'any(.key_chain.accounts[]; .Public.account_id == $a)' "$WALLET" >/dev/null 2>&1; } +holds "$PAY" || fail "wallet does not contain payment_account=$PAY — descriptor and storage.json are mismatched (wrong wallet)" +holds "$SUP" || fail "wallet does not contain supply_holding=$SUP — descriptor and storage.json are mismatched (wrong wallet)" + +mkdir -p "$OUT" +printf '%s' "$CFG" > "$OUT/config_account.txt" +printf '%s' "$PAY" > "$OUT/payment_account.txt" +printf '%s' "$SUP" > "$OUT/supply_holding.txt" +jq '.last_synced_block = 0' "$WALLET" > "$OUT/storage.json.seed" +jq -n --arg s "$SEQ" '{sequencer_addr:$s, seq_poll_timeout:"30s", seq_tx_poll_max_blocks:15, seq_poll_max_retries:10, seq_block_poll_max_amount:100}' > "$OUT/wallet_config.json" +cat > "$OUT/env.sh" < $OUT" diff --git a/docker/testnet/verify.sh b/docker/testnet/verify.sh new file mode 100755 index 0000000..832b871 --- /dev/null +++ b/docker/testnet/verify.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Thin shim -> canonical logos-lez-rln/tools/deployments/verify.sh (guest-drift guard). +# FEATURE: deployment-profile tooling. +# +# LEZ_RLN_DIR=/path/to/logos-lez-rln bash verify.sh docker/testnet/deployments/ +set -euo pipefail +LEZ_RLN_DIR="${LEZ_RLN_DIR:?set LEZ_RLN_DIR to your logos-lez-rln checkout}" +exec bash "$LEZ_RLN_DIR/tools/deployments/verify.sh" "$@"