diff --git a/solutions/LP-0016.md b/solutions/LP-0016.md new file mode 100644 index 0000000..348c7c9 --- /dev/null +++ b/solutions/LP-0016.md @@ -0,0 +1,323 @@ +# Solution: LP-0016 — Anonymous Forum with Threshold Moderation and Membership Revocation + +**Submitted by:** Davit Maisuradze ([@jeefxM](https://github.com/jeefxM)) + +## Summary + +A complete LP-0016 implementation: a forum-agnostic moderation SDK plus a +reference Logos Basecamp module that drives the full lifecycle (registration +with stake, anonymous posting, N-of-M moderation, K-strike slashing, +retroactive deanonymization). Membership registration and slashing are +**on-chain** on the public LEZ testnet; posting and moderation are **off-chain** +over Waku, the way the protocol is designed (free common path, paid revocation). + +- **Anonymous posting** with a Groth16 membership proof (≈5 s per post, + generated and verified off-chain by the local proof-daemon via snarkjs). +- **N-of-M moderation certificates** aggregated off-chain over Waku, with the + daemon enforcing the N threshold before a certificate is emitted. +- **K-strike slashing** via Shamir reconstruction of the member's nullifier + secret; one on-chain tx per slash; the slashed commitment enters the on-chain + revocation set and the published secret lets any verifier reject the member's + future posts. +- **Two parameterized forum instances live on LEZ testnet** under one program ID + (different K and N-of-M), each with register-with-stake on chain. +- **Reference Qt6 GUI** (standalone `ForumApp`; Logos Basecamp module packaging + in progress) built on the SDK's public, forum-agnostic API. + Non-CLI, click-driven: connect → create identity → create forum → register → + post → strike → slash, with a persistent MEMBER REVOKED banner, per-post + ✓ valid / ✗ author revoked badges, a Shamir evidence panel that fills in + share-by-share as strikes land, and a chain-evidence panel showing the + registry account, tree root, and on-chain register/slash tx hashes. + +## Repository + +- **Repo:** https://github.com/jeefxM/LP-0016-Anonymous-forum-with-moderation +- **Branch:** `main` +- **Video Demo:** https://www.youtube.com/watch?v=Q6fMpLB_850 + +## Approach + +Built on top of the Logos stack: + +- **LEZ membership_registry program** (`programs/registry-spel/`, SPEL IDL at + `programs/registry-spel/registry-spel.idl.json`) holds the on-chain membership + tree, the revoked commitment set, and a slash verifier that runs inside the + RISC0 zkVM (ark-bn254 `poly_eval` + Ed25519 signature checks). Deployed once; + forum instances are seed-derived `ForumState` PDAs that carry their own + `ForumConfig` (K and N-of-M) — see `docs/deployments.md`. +- **Forum-agnostic SDK** (`sdk/`, `@logos-forum/moderation-sdk`) wraps a local + proof-daemon (Groth16 prover + chain submitter) and a Waku transport + (`@waku/sdk`). The SDK operates on abstract content identifiers and makes no + assumptions about forum structure, satisfying the bounty's + "forum-agnostic library" requirement. +- **ZK membership proof** is a Groth16 circuit (`circuits/`, ADR-010). The + circom + rapidsnark path was chosen over RISC0 for the per-post proof + because membership proof generation needed to fit under the bounty's 10 s + budget on a standard laptop (rapidsnark proves the circuit in ~5 s vs ~55 s + for an equivalent RISC0 STARK — the RISC0 number is measured by + `bench_post_proof` at `RISC0_DEV_MODE=0`, see `docs/cu-costs.md`). +- **Slash via Shamir**: each post envelope embeds a Shamir share of the + member's nullifier secret (degree-(K-1) polynomial, secret evaluates at + x = 0). Moderation certificates bind shares to content identifiers. Once K + certificates exist for a member's nullifier, anyone can reconstruct the + secret and submit a single on-chain slash tx (`/v1/slash/recover` → + `submitSlash`). Below K, no information about the secret leaks — the + reconstruction is information-theoretically secure. +- **Basecamp module** (`basecamp/`) is a Qt6 UI module. QML is in + `basecamp/qml/Main.qml`; the C++ backend in `basecamp/src/ForumBackend.cpp` + drives a Node sidecar (`basecamp/sidecar/forum-sidecar.mjs`) via `QProcess`. + The sidecar consumes the same TS SDK that `sdk/tests/lifecycle.mjs` runs, so + the GUI's behaviour follows the proven path. The Basecamp module is built + entirely on the SDK's public, forum-agnostic API and adds no forum-specific + code to the library. + +**Why Logos**: censorship resistance for the off-chain moderation record +(Waku is the only viable transport for moderator certificates that cannot be +silently deleted), and trustless enforcement at the point of revocation +(LEZ verifies the cert evidence cryptographically; no centralized +authority can selectively enforce or veto a slash). A centralized +alternative would either trust a server with the moderation record (defeats +auditability) or skip the revocation enforcement (defeats the threshold +moderation guarantee). + +**Why not on-chain moderation per-strike**: every strike going on-chain +would impose a per-cert gas cost on moderators. Keeping certificates on +Waku and only the final slash on-chain (one tx per revocation) preserves +the protocol's economic invariant: free common path, paid revocation. + +**Where each step runs**: + +- **On-chain (LEZ testnet, `testnet.lez.logos.co`)**: forum initialize, + fund-escrow, register-with-stake, and slash. The testnet sequencer EXECUTES + these transactions; it does not produce an inline STARK per tx. The two live + parameterized instances carry on-chain state, register-with-stake, and slash. + The testnet wallet at `~/wallet-testnet` ships preconfigured genesis-funded + accounts (`6iArKUXx…` = 10 000, `7wHg9sb…` = 20 000, + authenticated-transfer-owned → spendable) which fund the escrow via + `auth-transfer 1000 → escrow` per ADR-011. +- **Off-chain (Waku + local proof-daemon)**: anonymous posting (Groth16 prove + + verify via snarkjs), moderation vote signing and certificate aggregation, and + the post-rejection check for revoked members. None of these touch the + sequencer. +- **Real RISC0 STARK**: the only real STARK in this submission is the membership + post-proof guest, benchmarked by `bench_post_proof` at `RISC0_DEV_MODE=0` + (~55 s, prove + verify OK) — see `docs/cu-costs.md` and ADR-002. +- **Local on-chain-logic test**: `crates/lez-runner/tests/staking_lifecycle.rs` + exercises register → post → K certs → slash → revoke through the in-process + V03State engine (it executes the program logic; it does not generate a STARK). + This is the canonical local end-to-end because the LEZ standalone-mode chain + has no runtime funding path (ADR-011 §"The faucet is genesis-only"), so + register-with-stake against a live local sequencer is not possible by design; + the funded path runs on testnet. + +**Disclosures (deliberate scope decisions)**: + +- **Posting is demonstrated at K = 3 only.** The membership circuit is + parameterized by K (`circuits/membership.circom`, `template Membership(TREE_DEPTH, K)`), + but only one instance is compiled and trusted-set-up: `Membership(16, 3)` + → `membership_0.zkey`, and the live daemon runs `CIRCUIT_K=3`. + Each distinct K needs its own circuit compile + trusted setup, so Instance A + (K = 3) demos the full lifecycle including posting + slash, while Instance B + (K = 2) exercises the registry's parameterizability **on-chain** live + (different K and N-of-M, register-with-stake on testnet — see Program ID + + PDAs below) but its **posting** path is not exercised, because the running + daemon's circuit is K = 3. The bounty's two-instance requirement is about + instance *parameters* (`ForumConfig` K and N-of-M, which are fully general + on-chain), not a second compiled circuit. Supporting K = 2 posting is a + recompile + `snarkjs groth16 setup` away, but no K = 2 zkey is shipped today. +- The Basecamp module's moderator coordination is collapsed for the + single-operator demo: the backend holds all N moderator secrets locally + and signs N votes per Strike click. In a real deployment those N keys + live on N different machines and each moderator submits a vote + independently over Waku; the SDK's `aggregateCertificate` accepts + whichever ≥ N-threshold votes arrive first. Code path at + `basecamp/src/ForumBackend.cpp:moderate()` and + `basecamp/sidecar/forum-sidecar.mjs:case "moderate"`. + +## Success Criteria Checklist + +- [x] **Member can register with a stake and publish anonymous posts; + posts from the same member are unlinkable below the slash threshold.** + Register-with-stake runs on testnet (escrow funded via `auth-transfer`, + per ADR-011); posting runs off-chain with a Groth16 proof carrying an + epoch-derived nullifier (same per (member, epoch), distinct across members), + and each post embeds an independent Shamir share whose x-coordinate is the + strike index — the share alone reveals nothing under K. Exercised by + `sdk/tests/lifecycle.mjs` and the Basecamp GUI. See `docs/protocol.md §3`. + +- [x] **Slash retroactively deanonymizes the slashed member's prior posts; + no other member is affected; documented in `docs/protocol.md`.** + See `docs/protocol.md §4` ("Retroactive deanonymization on slash"). The + reconstruction recovers exactly one member's secret; everyone else's + shares remain below K. + +- [x] **N-of-M moderators can jointly produce a certificate; fewer cannot.** + The daemon's `/v1/moderation/aggregate` endpoint refuses to emit a + certificate when fewer than N valid votes are supplied. Enforced + client-side in `sdk/src/index.ts:aggregateCertificate`, and re-verified + by the on-chain slash verifier. + +- [x] **K certs → slash → revocation, single on-chain tx.** + Demonstrated live on LEZ testnet by the full-lifecycle runner + (`programs/registry-spel/examples/src/bin/testnet_lifecycle.rs`): + 3 posts × 3 certs with distinct content IDs → Shamir reconstruction → + one on-chain slash tx → the commitment enters the on-chain revocation set + (verified by reading the state PDA back). Tx hashes in `docs/deployments.md`. + The same flow is exercised locally (no funding required) by + `crates/lez-runner/tests/staking_lifecycle.rs` through the in-process + V03State engine. + +- [x] **Slashed commitment is added to the revocation list; subsequent + posts from it are rejected.** + On slash, the reconstructed secret is published on-chain and the commitment + enters the revocation set. Post-rejection is enforced off-chain by the + proof-daemon: `verify_post` calls `post_proof_core::is_revoked_post` + (`crates/proof-daemon/src/proving.rs:252`), which recomputes the published + secret's nullifier `H("null" || secret || epoch)` for the proven epoch and + rejects any post whose nullifier matches — across epochs, so the member + cannot evade by changing epoch. Asserted by the unit test + `post_proof_core` (revoked-member-posts-rejected-across-epochs) and + end-to-end by `sdk/tests/lifecycle.mjs` (re-verifying the same post envelope + after slash returns `valid: false` with a `/revok/i` reason). At the GUI + layer the post sidecar runs `verifyPostProof` before publishing; a revoked + author gets a red ✗ badge and the `Post` button switches to a disabled + "Revoked" state once the GUI detects its own commitment in the revoked set. + +- [x] **Parameterizable K and N-of-M per forum instance.** + Two live instances under one program ID — Instance A (K=3, 2-of-3) and + Instance B (K=2, 3-of-4) — share the SPEL `membership_registry` program + but carry distinct `ForumConfig` in their PDAs. + See `docs/deployments.md`. + +- [x] **Forum-agnostic moderation library, public API, no forum + assumptions, uses Logos stack off-chain.** + `@logos-forum/moderation-sdk` exports `createIdentity`, + `createForumInstance`, `register`, `createPostProof`, `publishPost`, + `subscribePosts`, `listPosts`, `signModerationVote`, `aggregateCertificate`, + `publishCertificate`, `listCertificatesByNullifier`, + `tryReconstructSlashEvidence`, `submitSlash`, `verifyPostProof`, + `isRevoked`. Operates on `ContentId = Hex32`; the SDK never names a + body or content type. + +- [x] **Working Logos Basecamp app built on the library, usable by a + non-technical user.** + `basecamp/` builds a Logos Basecamp UI module (`libforum_plugin.so`, + implementing the `IComponent` interface, IID `com.logos.component.IComponent`, + + `metadata.json`/`manifest.json`). It is **verified loadable into + LogosBasecamp v0.1.2** via the AppImage's `ui-host` loader (the loader emits + `READY`; load log captured — see the release artifacts). The same QML/backend + also runs standalone as `ForumApp` for builders without Basecamp installed. + The module is built entirely on the SDK's public, forum-agnostic API. The user + sets the moderator count and N-of-M/K thresholds in the UI; the backend + provisions distinct real moderator keypairs (no hardcoded secrets) and shows + their public keys. Every lifecycle action is click-driven; the user never + crafts a tx or runs a CLI command. + +- [x] **End-to-end demonstration on LEZ testnet with at least two + independent forum instances using different K and N-of-M.** + Both instances are live on `testnet.lez.logos.co` under a single + `membership_registry` deployment. The state PDAs decode to non-empty + `ForumState` with advanced `tree_root`, distinct `(K, N-of-M)` config, + and an escrow holding the staked balance (independently verifiable via + `wallet account get` against the testnet sequencer): + + | | Instance A | Instance B | + |---|---|---| + | (K, N-of-M) | (3, 2-of-3) | (2, 3-of-4) | + | state PDA | `9zHLZn5qpMwaWurrs7DQgYDyF4XnF6EwE4HJfqkrDJ37` | `3gN9jzTbTL6WgxpqMMA5anUM8wavGdMk4isPY6HmX8p1` | + | escrow PDA | `8yiWGFYQ3vAatQfJdpyT6mUBGpTzqRfikF2yDwzNf7B1` | `5XbV2TAjonag397C5PQNuUBU5yd3f9n6SkprqM7LpYxw` | + | lifecycle on chain | register-with-stake → 3 posts → 3 N-of-M certs → **slash** | register-with-stake | + | `tree_root` after register | `ee499d794328661f…` | `c562c3f806c58ab7…` | + | escrow balance | `0` — stake **claimed by the slasher** when the slash executed | `1000` (staked, live) | + | slash | tx `22d391be447aa12c…`; commitment `0ba57a0c92e84302…` is in the on-chain revocation set | not run (the live daemon's circuit is K=3, so Instance B exercises parameterization on chain but not posting/slash) | + + Program ID (both): + `4766fcc24cac757ab4c504b3844c354468f4d7fbb7b630957573513c6eb9a30d`, + guest ImageID + `69373bb59ef0468f8f8748229d79f7cf54ca08b954bef983c641dcedd6d91d47`. + Instance A's full on-chain lifecycle (initialize → fund-escrow → register → + moderate → slash → revoke) is demonstrated live on testnet through the Basecamp + sidecar; the drained escrow is the on-chain proof that the slash claimed the + stake. Tx hashes and state read-back are in `docs/deployments.md`. + +## FURPS Self-Assessment + +### Functionality + +Full LP-0016 protocol — register-with-stake (on-chain, testnet), anonymous +posting with Groth16 proofs (off-chain), N-of-M moderation with off-chain +certificates over Waku, K-strike Shamir reconstruction, and on-chain slash that +revokes membership and claims stake. The Basecamp module surfaces the entire +flow as click-driven actions. Two parameterized instances confirmed on chain. + +### Usability + +- The Basecamp app is the only thing a forum user needs. No CLI, no tx + crafting. Each lifecycle action is a single click; the GUI shows + busy state, success toasts, and a persistent error line below the + feed. +- Per-post visual state surfaces protocol semantics: green ✓ valid pill + for posts that verify, red ✗ author revoked pill for posts whose + author has been slashed. +- A Shamir-evidence panel fills in one share-pill per strike, so the + cryptographic story (K shares accumulate → secret recoverable) is + visible without reading code. +- A chain-evidence panel exposes the on-chain registry account, tree + root, and most recent register / slash tx hashes for independent + verification. + +### Reliability + +- Proof generation failures bubble as typed `ForumError` from the SDK; + the GUI shows the daemon's error message verbatim and re-enables the + button so the user can retry without consuming the nullifier (the + nullifier is deterministic from `(secret, epoch)`, so retry is safe). +- The SDK's `aggregateCertificate` enforces the N threshold client-side + before any on-chain submission — a partial certificate cannot reach + the chain. +- A durable outbox retries failed Waku publishes with exponential backoff. A + publish that fails is persisted to disk (not dropped); the `flush-outbox` + sidecar command retries pending items and removes each on success. Retries are + idempotent because the nullifier and content id are deterministic (a duplicate + publish is a protocol no-op). See `basecamp/sidecar/forum-sidecar.mjs` + (`durablePublish` / `flush-outbox`). + +### Performance + +- Groth16 membership proof generation ≈ 5 s on the dev box + (target: < 10 s). See `docs/cu-costs.md`. +- On-chain register CU cost and slash CU cost documented in + `docs/cu-costs.md`. + +### Supportability + +- `docs/protocol.md` covers system model, primitives, unlinkability + + anonymity set, retroactive deanonymization on slash, revocation + mechanism, moderator trust model, threat model, known limitations. +- `docs/deployments.md` documents the live stack (Hetzner build host + with `RISC0_DEV_MODE=0` sequencer + nwaku + proof-daemon), the + deployed program ID, both instance PDAs, and tx hashes for each + lifecycle step. +- `docs/cu-costs.md` measures CU cost for every on-chain operation and + the real RISC0 STARK timing (`bench_post_proof`, `RISC0_DEV_MODE=0`). +- `docs/adr/` ADRs capture key implementation decisions (Groth16 vs + RISC0 for membership; share-binding scheme; SPEL port; ATA model). +- CI (`.github/workflows/ci.yml`) runs `cargo fmt`/`clippy`/`test` on the + LEZ-independent crates plus the SDK build and unit tests. The full + Waku + sequencer end-to-end (`sdk/tests/lifecycle.mjs`) needs a live + LEZ sequencer + nwaku and runs on the build host, not in GitHub-hosted CI. +- `basecamp/README.md` documents how to build the `.lgx` and load it + into Basecamp, plus how to run the standalone `ForumApp` preview. + +## Supporting Materials + +- **Video demo (narrated, full lifecycle, both instances)**: https://www.youtube.com/watch?v=Q6fMpLB_850 +- **Live deployment** (Hetzner): see `docs/deployments.md`. +- **Architecture decisions**: `docs/adr/`. +- **Test suite**: `sdk/tests/lifecycle.mjs` (full protocol, build host) and the + crate unit tests run in CI. + +## Terms & Conditions + +By submitting this solution, I confirm that I have read and agree to +the [Terms & Conditions](../TERMS.md).