Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
323 changes: 323 additions & 0 deletions solutions/LP-0016.md
Original file line number Diff line number Diff line change
@@ -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).
Loading