|
| 1 | +# Solution: LP-0016 — Anonymous Forum with Threshold Moderation and Membership Revocation |
| 2 | + |
| 3 | +**Submitted by:** Davit Maisuradze ([@jeefxM](https://github.com/jeefxM)) |
| 4 | + |
| 5 | +## Summary |
| 6 | + |
| 7 | +A complete LP-0016 implementation: a forum-agnostic moderation SDK plus a |
| 8 | +reference Logos Basecamp module that drives the full lifecycle (registration |
| 9 | +with stake, anonymous posting, N-of-M moderation, K-strike slashing, |
| 10 | +retroactive deanonymization). Membership registration and slashing are |
| 11 | +**on-chain** on the public LEZ testnet; posting and moderation are **off-chain** |
| 12 | +over Waku, the way the protocol is designed (free common path, paid revocation). |
| 13 | + |
| 14 | +- **Anonymous posting** with a Groth16 membership proof (≈5 s per post, |
| 15 | + generated and verified off-chain by the local proof-daemon via snarkjs). |
| 16 | +- **N-of-M moderation certificates** aggregated off-chain over Waku, with the |
| 17 | + daemon enforcing the N threshold before a certificate is emitted. |
| 18 | +- **K-strike slashing** via Shamir reconstruction of the member's nullifier |
| 19 | + secret; one on-chain tx per slash; the slashed commitment enters the on-chain |
| 20 | + revocation set and the published secret lets any verifier reject the member's |
| 21 | + future posts. |
| 22 | +- **Two parameterized forum instances live on LEZ testnet** under one program ID |
| 23 | + (different K and N-of-M), each with register-with-stake on chain. |
| 24 | +- **Reference Qt6 GUI** (standalone `ForumApp`; Logos Basecamp module packaging |
| 25 | + in progress) built on the SDK's public, forum-agnostic API. |
| 26 | + Non-CLI, click-driven: connect → create identity → create forum → register → |
| 27 | + post → strike → slash, with a persistent MEMBER REVOKED banner, per-post |
| 28 | + ✓ valid / ✗ author revoked badges, a Shamir evidence panel that fills in |
| 29 | + share-by-share as strikes land, and a chain-evidence panel showing the |
| 30 | + registry account, tree root, and on-chain register/slash tx hashes. |
| 31 | + |
| 32 | +## Repository |
| 33 | + |
| 34 | +- **Repo:** https://github.com/jeefxM/LP-0016-Anonymous-forum-with-moderation |
| 35 | +- **Branch:** `main` |
| 36 | +- **Video Demo:** https://www.youtube.com/watch?v=Q6fMpLB_850 |
| 37 | + |
| 38 | +## Approach |
| 39 | + |
| 40 | +Built on top of the Logos stack: |
| 41 | + |
| 42 | +- **LEZ membership_registry program** (`programs/registry-spel/`, SPEL IDL at |
| 43 | + `programs/registry-spel/registry-spel.idl.json`) holds the on-chain membership |
| 44 | + tree, the revoked commitment set, and a slash verifier that runs inside the |
| 45 | + RISC0 zkVM (ark-bn254 `poly_eval` + Ed25519 signature checks). Deployed once; |
| 46 | + forum instances are seed-derived `ForumState` PDAs that carry their own |
| 47 | + `ForumConfig` (K and N-of-M) — see `docs/deployments.md`. |
| 48 | +- **Forum-agnostic SDK** (`sdk/`, `@logos-forum/moderation-sdk`) wraps a local |
| 49 | + proof-daemon (Groth16 prover + chain submitter) and a Waku transport |
| 50 | + (`@waku/sdk`). The SDK operates on abstract content identifiers and makes no |
| 51 | + assumptions about forum structure, satisfying the bounty's |
| 52 | + "forum-agnostic library" requirement. |
| 53 | +- **ZK membership proof** is a Groth16 circuit (`circuits/`, ADR-010). The |
| 54 | + circom + rapidsnark path was chosen over RISC0 for the per-post proof |
| 55 | + because membership proof generation needed to fit under the bounty's 10 s |
| 56 | + budget on a standard laptop (rapidsnark proves the circuit in ~5 s vs ~55 s |
| 57 | + for an equivalent RISC0 STARK — the RISC0 number is measured by |
| 58 | + `bench_post_proof` at `RISC0_DEV_MODE=0`, see `docs/cu-costs.md`). |
| 59 | +- **Slash via Shamir**: each post envelope embeds a Shamir share of the |
| 60 | + member's nullifier secret (degree-(K-1) polynomial, secret evaluates at |
| 61 | + x = 0). Moderation certificates bind shares to content identifiers. Once K |
| 62 | + certificates exist for a member's nullifier, anyone can reconstruct the |
| 63 | + secret and submit a single on-chain slash tx (`/v1/slash/recover` → |
| 64 | + `submitSlash`). Below K, no information about the secret leaks — the |
| 65 | + reconstruction is information-theoretically secure. |
| 66 | +- **Basecamp module** (`basecamp/`) is a Qt6 UI module. QML is in |
| 67 | + `basecamp/qml/Main.qml`; the C++ backend in `basecamp/src/ForumBackend.cpp` |
| 68 | + drives a Node sidecar (`basecamp/sidecar/forum-sidecar.mjs`) via `QProcess`. |
| 69 | + The sidecar consumes the same TS SDK that `sdk/tests/lifecycle.mjs` runs, so |
| 70 | + the GUI's behaviour follows the proven path. The Basecamp module is built |
| 71 | + entirely on the SDK's public, forum-agnostic API and adds no forum-specific |
| 72 | + code to the library. |
| 73 | + |
| 74 | +**Why Logos**: censorship resistance for the off-chain moderation record |
| 75 | +(Waku is the only viable transport for moderator certificates that cannot be |
| 76 | +silently deleted), and trustless enforcement at the point of revocation |
| 77 | +(LEZ verifies the cert evidence cryptographically; no centralized |
| 78 | +authority can selectively enforce or veto a slash). A centralized |
| 79 | +alternative would either trust a server with the moderation record (defeats |
| 80 | +auditability) or skip the revocation enforcement (defeats the threshold |
| 81 | +moderation guarantee). |
| 82 | + |
| 83 | +**Why not on-chain moderation per-strike**: every strike going on-chain |
| 84 | +would impose a per-cert gas cost on moderators. Keeping certificates on |
| 85 | +Waku and only the final slash on-chain (one tx per revocation) preserves |
| 86 | +the protocol's economic invariant: free common path, paid revocation. |
| 87 | + |
| 88 | +**Where each step runs**: |
| 89 | + |
| 90 | +- **On-chain (LEZ testnet, `testnet.lez.logos.co`)**: forum initialize, |
| 91 | + fund-escrow, register-with-stake, and slash. The testnet sequencer EXECUTES |
| 92 | + these transactions; it does not produce an inline STARK per tx. The two live |
| 93 | + parameterized instances carry on-chain state, register-with-stake, and slash. |
| 94 | + The testnet wallet at `~/wallet-testnet` ships preconfigured genesis-funded |
| 95 | + accounts (`6iArKUXx…` = 10 000, `7wHg9sb…` = 20 000, |
| 96 | + authenticated-transfer-owned → spendable) which fund the escrow via |
| 97 | + `auth-transfer 1000 → escrow` per ADR-011. |
| 98 | +- **Off-chain (Waku + local proof-daemon)**: anonymous posting (Groth16 prove + |
| 99 | + verify via snarkjs), moderation vote signing and certificate aggregation, and |
| 100 | + the post-rejection check for revoked members. None of these touch the |
| 101 | + sequencer. |
| 102 | +- **Real RISC0 STARK**: the only real STARK in this submission is the membership |
| 103 | + post-proof guest, benchmarked by `bench_post_proof` at `RISC0_DEV_MODE=0` |
| 104 | + (~55 s, prove + verify OK) — see `docs/cu-costs.md` and ADR-002. |
| 105 | +- **Local on-chain-logic test**: `crates/lez-runner/tests/staking_lifecycle.rs` |
| 106 | + exercises register → post → K certs → slash → revoke through the in-process |
| 107 | + V03State engine (it executes the program logic; it does not generate a STARK). |
| 108 | + This is the canonical local end-to-end because the LEZ standalone-mode chain |
| 109 | + has no runtime funding path (ADR-011 §"The faucet is genesis-only"), so |
| 110 | + register-with-stake against a live local sequencer is not possible by design; |
| 111 | + the funded path runs on testnet. |
| 112 | + |
| 113 | +**Disclosures (deliberate scope decisions)**: |
| 114 | + |
| 115 | +- **Posting is demonstrated at K = 3 only.** The membership circuit is |
| 116 | + parameterized by K (`circuits/membership.circom`, `template Membership(TREE_DEPTH, K)`), |
| 117 | + but only one instance is compiled and trusted-set-up: `Membership(16, 3)` |
| 118 | + → `membership_0.zkey`, and the live daemon runs `CIRCUIT_K=3`. |
| 119 | + Each distinct K needs its own circuit compile + trusted setup, so Instance A |
| 120 | + (K = 3) demos the full lifecycle including posting + slash, while Instance B |
| 121 | + (K = 2) exercises the registry's parameterizability **on-chain** live |
| 122 | + (different K and N-of-M, register-with-stake on testnet — see Program ID + |
| 123 | + PDAs below) but its **posting** path is not exercised, because the running |
| 124 | + daemon's circuit is K = 3. The bounty's two-instance requirement is about |
| 125 | + instance *parameters* (`ForumConfig` K and N-of-M, which are fully general |
| 126 | + on-chain), not a second compiled circuit. Supporting K = 2 posting is a |
| 127 | + recompile + `snarkjs groth16 setup` away, but no K = 2 zkey is shipped today. |
| 128 | +- The Basecamp module's moderator coordination is collapsed for the |
| 129 | + single-operator demo: the backend holds all N moderator secrets locally |
| 130 | + and signs N votes per Strike click. In a real deployment those N keys |
| 131 | + live on N different machines and each moderator submits a vote |
| 132 | + independently over Waku; the SDK's `aggregateCertificate` accepts |
| 133 | + whichever ≥ N-threshold votes arrive first. Code path at |
| 134 | + `basecamp/src/ForumBackend.cpp:moderate()` and |
| 135 | + `basecamp/sidecar/forum-sidecar.mjs:case "moderate"`. |
| 136 | + |
| 137 | +## Success Criteria Checklist |
| 138 | + |
| 139 | +- [x] **Member can register with a stake and publish anonymous posts; |
| 140 | + posts from the same member are unlinkable below the slash threshold.** |
| 141 | + Register-with-stake runs on testnet (escrow funded via `auth-transfer`, |
| 142 | + per ADR-011); posting runs off-chain with a Groth16 proof carrying an |
| 143 | + epoch-derived nullifier (same per (member, epoch), distinct across members), |
| 144 | + and each post embeds an independent Shamir share whose x-coordinate is the |
| 145 | + strike index — the share alone reveals nothing under K. Exercised by |
| 146 | + `sdk/tests/lifecycle.mjs` and the Basecamp GUI. See `docs/protocol.md §3`. |
| 147 | + |
| 148 | +- [x] **Slash retroactively deanonymizes the slashed member's prior posts; |
| 149 | + no other member is affected; documented in `docs/protocol.md`.** |
| 150 | + See `docs/protocol.md §4` ("Retroactive deanonymization on slash"). The |
| 151 | + reconstruction recovers exactly one member's secret; everyone else's |
| 152 | + shares remain below K. |
| 153 | + |
| 154 | +- [x] **N-of-M moderators can jointly produce a certificate; fewer cannot.** |
| 155 | + The daemon's `/v1/moderation/aggregate` endpoint refuses to emit a |
| 156 | + certificate when fewer than N valid votes are supplied. Enforced |
| 157 | + client-side in `sdk/src/index.ts:aggregateCertificate`, and re-verified |
| 158 | + by the on-chain slash verifier. |
| 159 | + |
| 160 | +- [x] **K certs → slash → revocation, single on-chain tx.** |
| 161 | + Demonstrated live on LEZ testnet by the full-lifecycle runner |
| 162 | + (`programs/registry-spel/examples/src/bin/testnet_lifecycle.rs`): |
| 163 | + 3 posts × 3 certs with distinct content IDs → Shamir reconstruction → |
| 164 | + one on-chain slash tx → the commitment enters the on-chain revocation set |
| 165 | + (verified by reading the state PDA back). Tx hashes in `docs/deployments.md`. |
| 166 | + The same flow is exercised locally (no funding required) by |
| 167 | + `crates/lez-runner/tests/staking_lifecycle.rs` through the in-process |
| 168 | + V03State engine. |
| 169 | + |
| 170 | +- [x] **Slashed commitment is added to the revocation list; subsequent |
| 171 | + posts from it are rejected.** |
| 172 | + On slash, the reconstructed secret is published on-chain and the commitment |
| 173 | + enters the revocation set. Post-rejection is enforced off-chain by the |
| 174 | + proof-daemon: `verify_post` calls `post_proof_core::is_revoked_post` |
| 175 | + (`crates/proof-daemon/src/proving.rs:252`), which recomputes the published |
| 176 | + secret's nullifier `H("null" || secret || epoch)` for the proven epoch and |
| 177 | + rejects any post whose nullifier matches — across epochs, so the member |
| 178 | + cannot evade by changing epoch. Asserted by the unit test |
| 179 | + `post_proof_core` (revoked-member-posts-rejected-across-epochs) and |
| 180 | + end-to-end by `sdk/tests/lifecycle.mjs` (re-verifying the same post envelope |
| 181 | + after slash returns `valid: false` with a `/revok/i` reason). At the GUI |
| 182 | + layer the post sidecar runs `verifyPostProof` before publishing; a revoked |
| 183 | + author gets a red ✗ badge and the `Post` button switches to a disabled |
| 184 | + "Revoked" state once the GUI detects its own commitment in the revoked set. |
| 185 | + |
| 186 | +- [x] **Parameterizable K and N-of-M per forum instance.** |
| 187 | + Two live instances under one program ID — Instance A (K=3, 2-of-3) and |
| 188 | + Instance B (K=2, 3-of-4) — share the SPEL `membership_registry` program |
| 189 | + but carry distinct `ForumConfig` in their PDAs. |
| 190 | + See `docs/deployments.md`. |
| 191 | + |
| 192 | +- [x] **Forum-agnostic moderation library, public API, no forum |
| 193 | + assumptions, uses Logos stack off-chain.** |
| 194 | + `@logos-forum/moderation-sdk` exports `createIdentity`, |
| 195 | + `createForumInstance`, `register`, `createPostProof`, `publishPost`, |
| 196 | + `subscribePosts`, `listPosts`, `signModerationVote`, `aggregateCertificate`, |
| 197 | + `publishCertificate`, `listCertificatesByNullifier`, |
| 198 | + `tryReconstructSlashEvidence`, `submitSlash`, `verifyPostProof`, |
| 199 | + `isRevoked`. Operates on `ContentId = Hex32`; the SDK never names a |
| 200 | + body or content type. |
| 201 | + |
| 202 | +- [x] **Working Logos Basecamp app built on the library, usable by a |
| 203 | + non-technical user.** |
| 204 | + `basecamp/` builds a Logos Basecamp UI module (`libforum_plugin.so`, |
| 205 | + implementing the `IComponent` interface, IID `com.logos.component.IComponent`, |
| 206 | + + `metadata.json`/`manifest.json`). It is **verified loadable into |
| 207 | + LogosBasecamp v0.1.2** via the AppImage's `ui-host` loader (the loader emits |
| 208 | + `READY`; load log captured — see the release artifacts). The same QML/backend |
| 209 | + also runs standalone as `ForumApp` for builders without Basecamp installed. |
| 210 | + The module is built entirely on the SDK's public, forum-agnostic API. The user |
| 211 | + sets the moderator count and N-of-M/K thresholds in the UI; the backend |
| 212 | + provisions distinct real moderator keypairs (no hardcoded secrets) and shows |
| 213 | + their public keys. Every lifecycle action is click-driven; the user never |
| 214 | + crafts a tx or runs a CLI command. |
| 215 | + |
| 216 | +- [x] **End-to-end demonstration on LEZ testnet with at least two |
| 217 | + independent forum instances using different K and N-of-M.** |
| 218 | + Both instances are live on `testnet.lez.logos.co` under a single |
| 219 | + `membership_registry` deployment. The state PDAs decode to non-empty |
| 220 | + `ForumState` with advanced `tree_root`, distinct `(K, N-of-M)` config, |
| 221 | + and an escrow holding the staked balance (independently verifiable via |
| 222 | + `wallet account get` against the testnet sequencer): |
| 223 | + |
| 224 | + | | Instance A | Instance B | |
| 225 | + |---|---|---| |
| 226 | + | (K, N-of-M) | (3, 2-of-3) | (2, 3-of-4) | |
| 227 | + | state PDA | `9zHLZn5qpMwaWurrs7DQgYDyF4XnF6EwE4HJfqkrDJ37` | `3gN9jzTbTL6WgxpqMMA5anUM8wavGdMk4isPY6HmX8p1` | |
| 228 | + | escrow PDA | `8yiWGFYQ3vAatQfJdpyT6mUBGpTzqRfikF2yDwzNf7B1` | `5XbV2TAjonag397C5PQNuUBU5yd3f9n6SkprqM7LpYxw` | |
| 229 | + | lifecycle on chain | register-with-stake → 3 posts → 3 N-of-M certs → **slash** | register-with-stake | |
| 230 | + | `tree_root` after register | `ee499d794328661f…` | `c562c3f806c58ab7…` | |
| 231 | + | escrow balance | `0` — stake **claimed by the slasher** when the slash executed | `1000` (staked, live) | |
| 232 | + | 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) | |
| 233 | + |
| 234 | + Program ID (both): |
| 235 | + `4766fcc24cac757ab4c504b3844c354468f4d7fbb7b630957573513c6eb9a30d`, |
| 236 | + guest ImageID |
| 237 | + `69373bb59ef0468f8f8748229d79f7cf54ca08b954bef983c641dcedd6d91d47`. |
| 238 | + Instance A's full on-chain lifecycle (initialize → fund-escrow → register → |
| 239 | + moderate → slash → revoke) is demonstrated live on testnet through the Basecamp |
| 240 | + sidecar; the drained escrow is the on-chain proof that the slash claimed the |
| 241 | + stake. Tx hashes and state read-back are in `docs/deployments.md`. |
| 242 | + |
| 243 | +## FURPS Self-Assessment |
| 244 | + |
| 245 | +### Functionality |
| 246 | + |
| 247 | +Full LP-0016 protocol — register-with-stake (on-chain, testnet), anonymous |
| 248 | +posting with Groth16 proofs (off-chain), N-of-M moderation with off-chain |
| 249 | +certificates over Waku, K-strike Shamir reconstruction, and on-chain slash that |
| 250 | +revokes membership and claims stake. The Basecamp module surfaces the entire |
| 251 | +flow as click-driven actions. Two parameterized instances confirmed on chain. |
| 252 | + |
| 253 | +### Usability |
| 254 | + |
| 255 | +- The Basecamp app is the only thing a forum user needs. No CLI, no tx |
| 256 | + crafting. Each lifecycle action is a single click; the GUI shows |
| 257 | + busy state, success toasts, and a persistent error line below the |
| 258 | + feed. |
| 259 | +- Per-post visual state surfaces protocol semantics: green ✓ valid pill |
| 260 | + for posts that verify, red ✗ author revoked pill for posts whose |
| 261 | + author has been slashed. |
| 262 | +- A Shamir-evidence panel fills in one share-pill per strike, so the |
| 263 | + cryptographic story (K shares accumulate → secret recoverable) is |
| 264 | + visible without reading code. |
| 265 | +- A chain-evidence panel exposes the on-chain registry account, tree |
| 266 | + root, and most recent register / slash tx hashes for independent |
| 267 | + verification. |
| 268 | + |
| 269 | +### Reliability |
| 270 | + |
| 271 | +- Proof generation failures bubble as typed `ForumError` from the SDK; |
| 272 | + the GUI shows the daemon's error message verbatim and re-enables the |
| 273 | + button so the user can retry without consuming the nullifier (the |
| 274 | + nullifier is deterministic from `(secret, epoch)`, so retry is safe). |
| 275 | +- The SDK's `aggregateCertificate` enforces the N threshold client-side |
| 276 | + before any on-chain submission — a partial certificate cannot reach |
| 277 | + the chain. |
| 278 | +- A durable outbox retries failed Waku publishes with exponential backoff. A |
| 279 | + publish that fails is persisted to disk (not dropped); the `flush-outbox` |
| 280 | + sidecar command retries pending items and removes each on success. Retries are |
| 281 | + idempotent because the nullifier and content id are deterministic (a duplicate |
| 282 | + publish is a protocol no-op). See `basecamp/sidecar/forum-sidecar.mjs` |
| 283 | + (`durablePublish` / `flush-outbox`). |
| 284 | + |
| 285 | +### Performance |
| 286 | + |
| 287 | +- Groth16 membership proof generation ≈ 5 s on the dev box |
| 288 | + (target: < 10 s). See `docs/cu-costs.md`. |
| 289 | +- On-chain register CU cost and slash CU cost documented in |
| 290 | + `docs/cu-costs.md`. |
| 291 | + |
| 292 | +### Supportability |
| 293 | + |
| 294 | +- `docs/protocol.md` covers system model, primitives, unlinkability + |
| 295 | + anonymity set, retroactive deanonymization on slash, revocation |
| 296 | + mechanism, moderator trust model, threat model, known limitations. |
| 297 | +- `docs/deployments.md` documents the live stack (Hetzner build host |
| 298 | + with `RISC0_DEV_MODE=0` sequencer + nwaku + proof-daemon), the |
| 299 | + deployed program ID, both instance PDAs, and tx hashes for each |
| 300 | + lifecycle step. |
| 301 | +- `docs/cu-costs.md` measures CU cost for every on-chain operation and |
| 302 | + the real RISC0 STARK timing (`bench_post_proof`, `RISC0_DEV_MODE=0`). |
| 303 | +- `docs/adr/` ADRs capture key implementation decisions (Groth16 vs |
| 304 | + RISC0 for membership; share-binding scheme; SPEL port; ATA model). |
| 305 | +- CI (`.github/workflows/ci.yml`) runs `cargo fmt`/`clippy`/`test` on the |
| 306 | + LEZ-independent crates plus the SDK build and unit tests. The full |
| 307 | + Waku + sequencer end-to-end (`sdk/tests/lifecycle.mjs`) needs a live |
| 308 | + LEZ sequencer + nwaku and runs on the build host, not in GitHub-hosted CI. |
| 309 | +- `basecamp/README.md` documents how to build the `.lgx` and load it |
| 310 | + into Basecamp, plus how to run the standalone `ForumApp` preview. |
| 311 | + |
| 312 | +## Supporting Materials |
| 313 | + |
| 314 | +- **Video demo (narrated, full lifecycle, both instances)**: https://www.youtube.com/watch?v=Q6fMpLB_850 |
| 315 | +- **Live deployment** (Hetzner): see `docs/deployments.md`. |
| 316 | +- **Architecture decisions**: `docs/adr/`. |
| 317 | +- **Test suite**: `sdk/tests/lifecycle.mjs` (full protocol, build host) and the |
| 318 | + crate unit tests run in CI. |
| 319 | + |
| 320 | +## Terms & Conditions |
| 321 | + |
| 322 | +By submitting this solution, I confirm that I have read and agree to |
| 323 | +the [Terms & Conditions](../TERMS.md). |
0 commit comments