Skip to content

Commit d130c52

Browse files
authored
Merge pull request #80 from jeefxM/lp-0016-solution
Solution: LP-0016 — Anonymous Forum with Threshold Moderation and Membership Revocation
2 parents bebb487 + 6390f1f commit d130c52

1 file changed

Lines changed: 323 additions & 0 deletions

File tree

solutions/LP-0016.md

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
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

Comments
 (0)