mirror of
https://github.com/logos-co/logos-rln-e2e.git
synced 2026-08-27 13:01:13 +00:00
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.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
.git
|
||||
+13
@@ -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
|
||||
@@ -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.
|
||||
+21
@@ -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.
|
||||
@@ -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`).
|
||||
@@ -0,0 +1 @@
|
||||
lp2p-out/
|
||||
@@ -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
|
||||
@@ -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=<name>.
|
||||
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: <mdir>/<manifest.name>/
|
||||
# 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).'"]
|
||||
Executable
+31
@@ -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"
|
||||
@@ -0,0 +1,54 @@
|
||||
# Deployment profiles
|
||||
|
||||
One **deployment** = one on-chain RLN instance, fully captured by two files:
|
||||
|
||||
```
|
||||
deployments/<name>/
|
||||
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=<name> -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/<name>
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Executable
+59
@@ -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 <<EOF
|
||||
|
||||
DONE. Run the sim:
|
||||
cd $REPO_ROOT/docker/testnet/mix_e2e
|
||||
bash orchestrate.sh # the full gifted-RLN-over-mix E2E
|
||||
NEG=1 bash orchestrate.sh # negative: unregistered sender rejected
|
||||
NEG=2 bash orchestrate.sh # negative: non-allowlisted sender refused by the gifter
|
||||
docker compose down # tear down
|
||||
EOF
|
||||
@@ -0,0 +1,36 @@
|
||||
# 5-node RLN-over-mix E2E on the new universal logos-libp2p-module stack.
|
||||
# One logoscore daemon per container on a shared bridge network (each on
|
||||
# 0.0.0.0:9000, mutually reachable). Roles are driven post-up by orchestrate.sh:
|
||||
# relay1 -> 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
|
||||
Executable
+36
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+106
@@ -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 <privhex> -> curve25519 mix public key hex (RFC7748 X25519, matches
|
||||
# nim-libp2p public(priv))
|
||||
# peerpub <peerIdB58> -> 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 <len> <PublicKey protobuf>
|
||||
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 <privhex> | peerpub <peerIdB58>")
|
||||
Executable
+376
@@ -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/<container-ip>/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:-<your logos-lez-rln clone>}"
|
||||
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 <<EOF
|
||||
CAUSE: the deployment's payment account is OUT OF RLNTOK (each register costs
|
||||
price_per_unit*rate; the funded account holds a finite amount).
|
||||
FIX: provision a fresh funded payment account on the SAME tree (re-uses the
|
||||
wallet, so run_setup mints a new funded holder), then rebuild the image
|
||||
against the new deployment (the gifter signs with the baked wallet, so a
|
||||
new payment account must be baked in):
|
||||
D=docker/testnet/deployments/shared-5ade
|
||||
(cd "\$LEZ_RLN_DIR/lez-rln" && PYO3_PYTHON=\$(command -v python3) \\
|
||||
cargo build --release --bin run_setup --bin derive_accounts)
|
||||
LEZ_RLN_DIR="\$LEZ_RLN_DIR" bash ../provision.sh --name shared-refunded \\
|
||||
--tree \$(jq -r .tree_id "\$D/deployment.json") --adopt-wallet "\$D/storage.json"
|
||||
docker build -f docker/Dockerfile.testnet-e2e \\
|
||||
--build-arg DEPLOYMENT=shared-refunded -t lp2p-mix-e2e .
|
||||
If you instead saw "supply holding may be out of funds", the master supply is
|
||||
exhausted -> 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 <<EOF
|
||||
CAUSE: the RLN rate-limit pool is exhausted (the tree is effectively full).
|
||||
FIX: provision a fresh deployment on a new tree (tree_id is the single knob),
|
||||
then rebuild the image against it:
|
||||
1. (cd "\$LEZ_RLN_DIR/lez-rln" && PYO3_PYTHON=\$(command -v python3) \\
|
||||
cargo build --release --bin run_setup --bin derive_accounts)
|
||||
2. LEZ_RLN_DIR="\$LEZ_RLN_DIR" bash ../provision.sh --name <new-name>
|
||||
3. docker build -f docker/Dockerfile.testnet-e2e \\
|
||||
--build-arg DEPLOYMENT=<new-name> -t lp2p-mix-e2e .
|
||||
To reuse the same accounts across sims, add --adopt-wallet <storage.json> in (2).
|
||||
See docker/testnet/deployments/README.md for the full flow.
|
||||
EOF
|
||||
else
|
||||
cat >&2 <<EOF
|
||||
CAUSE: unknown. Inspect the node log:
|
||||
docker compose -f docker-compose.yml logs $svc | grep -iE 'register|balance|rate limit|payment|tree'
|
||||
Most common is out-of-funds -> 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)"
|
||||
Executable
+14
@@ -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 <name> \
|
||||
# [--tree <64hex>] [--adopt-wallet <storage.json>] [--sequencer <url>]
|
||||
#
|
||||
# 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" "$@"
|
||||
Executable
+57
@@ -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 <deployment_dir> <out_dir>
|
||||
#
|
||||
# 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 <deployment_dir> <out_dir>}"
|
||||
OUT="${2:?usage: stage.sh <deployment_dir> <out_dir>}"
|
||||
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" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
||||
export LEE_WALLET_HOME_DIR="\$SCRIPT_DIR"
|
||||
export NSSA_WALLET_HOME_DIR="\$SCRIPT_DIR"
|
||||
export LEZ_RLN_TREE_ID_HEX=$TREE
|
||||
EOF
|
||||
|
||||
echo "stage: OK $NAME tree=${TREE:0:8}… config=$CFG payment=$PAY supply=$SUP -> $OUT"
|
||||
Executable
+8
@@ -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/<name>
|
||||
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" "$@"
|
||||
Reference in New Issue
Block a user