diff --git a/solutions/LP-0002.md b/solutions/LP-0002.md new file mode 100644 index 0000000..02eef45 --- /dev/null +++ b/solutions/LP-0002.md @@ -0,0 +1,292 @@ +# Solution: LP-0002 - Private M-of-N Multisig + +**Submitted by:** Davit Maisuradze ([@jeefxM](https://github.com/jeefxM)) + +> **Resubmission (v0.2.0-rc5).** This addresses all six items from the first-round +> review: CU/cycle cost is reported, partial-approval resume across restarts is +> demonstrated, the standalone-sequencer e2e is wired into CI, the README's +> CLI/Basecamp walkthrough is completed, the Basecamp module is hosted as a signed +> downloadable, and the anonymous-approval binding is now an **in-circuit +> live-account proof** (not derivation-only). The submission was also ported from +> nssa v0.1.2 to the current testnet rev **Logos LEZ v0.2.0-rc5** (the testnet was +> redeployed and state-wiped), and the full 2-of-3 flow was re-run live with real +> STARKs on the new rev. + +## Summary + +An anonymous M-of-N multisig program for the Logos Execution Zone: a treasury is +controlled by `N` members, and a proposal releases funds once `M` of them +approve, with each individual approval staying **anonymous among the member +set**. Built natively for the LEZ privacy-preserving-transaction model and the +RISC0 zkVM (no Semaphore/MACI code ported, just the structural pattern +reimplemented for LEZ). + +- **Anonymous approval** is a real RISC0 STARK (~180 s at `RISC0_DEV_MODE=0`) + that proves in-guest Merkle membership in the frozen member set without + revealing which member, records a proposal-bound nullifier (no double votes), + and increments the public count. The member's secret travels only as a private + witness and never touches the chain. +- **Membership is bound to a LIVE shielded account, in-circuit.** Each member's + membership secret IS their real shielded-account nullifier secret key (`nsk`), + HD-derived from the LEZ key tree. The approval rides the member's live shielded + voting account, and the guest asserts **in-circuit** that the rider's account id + equals `for_regular_private_account(npk(secret), VOTE_IDENTIFIER)` (the same + `secret` that drives the membership leaf and the vote nullifier) **and** that the + rider is a live, non-default account; the LEZ privacy circuit independently + proves the rider's commitment is in the on-chain commitment set and emits its + spend nullifier. So the anonymous vote is cryptographically bound to a real live + account, not merely derived from a key. +- **Threshold-gated treasury release.** Once `approval_count >= M`, `Execute` + drains the treasury PDA to the recipient via a chained `authenticated_transfer` + call (only the owning program can move an account's balance). +- **Live on LEZ testnet (v0.2.0-rc5)** under deployed program id + `9pwpqhXCZqzBDYctvTvzPeV1qoviSAENw2utmayHgvBF`, with a full 2-of-3 lifecycle + (proposal `Hf84MVjY`) landed on chain and independently verifiable. +- **Reference Logos Basecamp module** (`ui_qml` plugin), hosted as a signed, + multi-variant downloadable `.lgx`, that casts a real anonymous vote through the + same client path, plus a one-command reproducible demo at `RISC0_DEV_MODE=0`. + +## Repository + +- **Repo:** https://github.com/jeefxM/lp-0002-private-multisig-rc5 +- **Branch:** `main` +- **Rev:** Logos LEZ **v0.2.0-rc5** (crate `lee`) +- **Program id (LEZ testnet):** `9pwpqhXCZqzBDYctvTvzPeV1qoviSAENw2utmayHgvBF` + (decimal words `[3100124547, 2797454125, 2467287583, 3014535533, 2620419628, 3253148841, 840948196, 515808628]`) +- **Hosted Basecamp module (downloadable):** https://github.com/jeefxM/logos-lp0002-msig-module/releases/tag/v0.1.0 + (signed `.lgx`, variants `darwin-arm64` + `linux-amd64` + `linux-arm64`) +- **Basecamp module (in-repo descriptor):** `basecamp/` (`module.json`, `metadata.json`, QML view); full GUI source + the signed downloadable are in the module repo above. +- **Video Demo:** https://www.youtube.com/watch?v=CXzqWLvBY0A +- **License:** MIT (see `LICENSE` and `NOTICE` in the repo). + +The full write-up (threshold scheme, nullifier design, LEZ account model with the +`nonce` and `program_owner` handling, security assumptions, known limitations, +integration guide) lives in the repo at +[`docs/LP-0002-solution.md`](https://github.com/jeefxM/lp-0002-private-multisig-rc5/blob/main/docs/LP-0002-solution.md). +Measured cycle/CU cost and proving times are in `docs/lp0002-benchmarks.md`; +failure modes and error codes in `docs/lp0002-reliability.md`; the architecture map +in `docs/ARCHITECTURE.md`. + +## Approach + +Reimplemented the Semaphore/MACI structural pattern (public leaf commitment in a +Merkle set, in-circuit membership proof, domain-separated action-bound nullifier) +natively for LEZ + RISC0. No FROST, no signature aggregation: the threshold is a +public count over per-member ZK membership proofs. + +- **Threshold scheme:** depth-5 Merkle member set (32 slots). `Enroll` publishes + leaf `H(LEAF_DOMAIN || nsk)`; `CreateProposal` freezes `member_root` + + `proposal_id` with count 0; `Approve` proves Merkle membership in-guest (without + revealing which leaf) and increments the count; `Execute` releases the treasury + at threshold `M` (supplied as an argument, so one deployed program serves any + M-of-N up to 32 members). +- **Nullifier design:** `nullifier = H(NULL_DOMAIN || nsk || proposal_id)`, + domain-separated from the leaf hash. Proposal-bound, so a member can vote on + different proposals but not twice on the same one; two distinct members produce + two distinct nullifiers; the proposal state stores only root + id + count + + opaque nullifiers, never any member identity. +- **In-circuit live-account binding:** the approval rides the member's live + shielded voting account at identifier `VOTE_IDENTIFIER`. The guest asserts + `rider.account_id == for_regular_private_account(npk(secret), VOTE_IDENTIFIER)` + and `rider.account != default`, using the SAME `secret` as the membership leaf + and vote nullifier; the LEZ `PrivateAuthorizedUpdate` circuit re-derives the + account id from `npk(nsk)`, asserts it matches the pre-state, and proves the + rider's commitment is in the on-chain commitment set (liveness) while emitting + the spend nullifier. Control of the `nsk` is control of the account, and the + binding is now proven in-circuit against live chain state. +- **LEZ account-model compatibility:** the treasury is `authenticated_transfer`- + owned and `Execute` chains a call into it (`InitTreasury` first claims the PDA + under msig's PDA authorization). Full detail in `docs/LP-0002-solution.md`. + +## Success Criteria Checklist + +Mapped to the prize's criteria. `[x]` met, `[~]` partial (honest scope noted), +`[ ]` open. + +### Functionality + +- [x] **Anonymous M-of-N approval by a shielded-account holder.** The approval is + a privacy-preserving ZK tx; on-chain state records only root + id + count + + opaque nullifiers, no member identity. Each member's secret IS their real + HD-derived shielded-account `nsk`, and the guest binds the vote **in-circuit** to + the member's live shielded account (`rider.account_id == for_regular_private_account(npk(secret), VOTE_IDENTIFIER)` + and rider != default), with the LEZ circuit proving the rider's commitment is in + the live commitment set. +- [x] **Threshold confirmed without recording who approved.** `Execute` asserts + `count >= M`; recorded nullifiers are opaque. +- [x] **No double-voting.** Proposal-bound nullifier with an in-guest + already-recorded check. +- [x] **Execution unlinkable to any individual member.** `Execute` reads only the + public count and debits via `authenticated_transfer`; it references no secret + or leaf. +- [~] **Client-side proving on a standard laptop.** Proving is client-side; a real + `RISC0_DEV_MODE=0` approve was measured at ~180 s on the build host. Standard- + laptop timing is not separately measured; proving is RAM/CPU-bound, so a laptop + will be slower. +- [x] **Reference integration: threshold-gated treasury transfer on testnet.** The + 2-of-3 run releases a treasury to a recipient at threshold 2. +- [x] **At least one instance on testnet, proposal approved by threshold and + executed, reproducible with evidence.** Program `9pwpqhXC...`, proposal + `Hf84MVjY`, tx hashes below. +- [x] **Full documentation and a clean public repository.** Delivered at + `github.com/jeefxM/lp-0002-private-multisig-rc5`. + +### Usability + +- [~] **Module/SDK to build Logos modules.** `msig_core` is a reusable scheme + crate (byte-identical hashing/Merkle math in-guest and client) and the `run_*` + bins are a worked client; a separately packaged SDK crate is not split out. +- [x] **Logos Basecamp app GUI, hosted as a downloadable.** A native `ui_qml` + Basecamp plugin casts a real anonymous vote through the same client path + (sidecar spawns the real `RISC0_DEV_MODE=0` STARK), demonstrated in the video. + It is **hosted as a signed, multi-variant downloadable** `.lgx` + (`darwin-arm64` + `linux-amd64` + `linux-arm64`) at + https://github.com/jeefxM/logos-lp0002-msig-module/releases/tag/v0.1.0 — + install via Basecamp → Package Manager → *Install from file*; verify with + `lgx verify`. +- [~] **SPEL IDL.** `idl/lp0002-msig.idl.json` (declares spec `spel-0.1`) + describes all five instructions. It is hand-authored JSON; full conformance to + the upstream SPEL toolchain is unverified. + +### Reliability + +- [~] **Proof-generation failures surface a clear error.** The runner propagates + proving errors; a polished member-facing error UX is not built. +- [x] **Partial-approval resume across restarts.** Demonstrated: + `scripts/lp0002-resume-rc5.sh` shows `approval_count == 1` surviving a `kill -9` + of the standalone sequencer and a restart on the SAME RocksDB data dir (reopened, + not re-genesis); the run then completes the second approval to threshold and + executes. State durability is verified against the sequencer's atomic + block+state commit. +- [x] **Deterministic, documented error conditions.** Guest asserts carry stable + strings ("approver is not an enrolled member", "approval nullifier already + recorded (double vote)", "approval count below threshold", "proposal id + mismatch", "rider must be a LIVE funded account"); negative tests pin non-member, + double-vote, and default-rider rejection. + +### Performance + +- [x] **CU / cycle cost of the on-chain proving operation.** Measured on the live + `RISC0_DEV_MODE=0` run: the approve inner guest is **262,144 total RISC0 cycles + (197,041–209,217 user), 1 segment**; the outer succinct circuit is **1,048,576 + cycles**; the succinct receipt is **229,379 bytes (~224 KB)**; one full approve + proves in **~180 s** wall (inner ~30 s + outer ~151 s). On a zkVM execution zone + the RISC0 cycle count is the compute-unit metric. See `docs/lp0002-benchmarks.md`. + +### Supportability + +- [x] **Deployed and tested on LEZ testnet.** Program `9pwpqhXC...`, 2-of-3 run. +- [x] **End-to-end integration tests against a standalone sequencer, in CI.** + `.github/workflows/lp0002-ci.yml` has a `msig-e2e-devmode` job that boots a + standalone local sequencer and drives the full 2-of-3 lifecycle via + `scripts/lp0002-demo-rc5.sh` under `RISC0_DEV_MODE=1`, alongside the + core/state/circuit tests. +- [x] **CI green on the default branch.** `.github/workflows/lp0002-ci.yml` runs + the msig core/state/circuit tests + the standalone-sequencer e2e job. +- [x] **README documents end-to-end usage.** The LP-0002 README covers deployment, + the program address, the per-runner CLI walkthrough, and the Basecamp install/run + flow (with the hosted `.lgx`). +- [x] **Reproducible demo script at `RISC0_DEV_MODE=0`.** `scripts/lp0002-demo-rc5.sh` + runs green end-to-end against a local standalone sequencer (two real proofs, + count = 2, treasury drained, recipient credited). +- [x] **Narrated video showing terminal output confirming `RISC0_DEV_MODE=0`.** + https://www.youtube.com/watch?v=CXzqWLvBY0A + +## FURPS Self-Assessment + +### Functionality + +A working private M-of-N multisig: enroll, create-proposal, anonymous threshold +approval (a privacy-preserving ZK tx), and threshold-gated treasury release through +`authenticated_transfer`. The 2-of-3 live run is the M-of-N proof; the threshold is +a public count over per-member ZK membership proofs, not FROST signature +aggregation. The anonymous vote is bound **in-circuit** to the member's live +shielded account: the guest asserts the rider's account id derives from the same +`nsk` as the membership leaf and is a live, non-default account, and the LEZ privacy +circuit proves the rider's commitment is in the on-chain commitment set. Functional +tests run under `RISC0_DEV_MODE=1`; the ZK soundness evidence is the real +`RISC0_DEV_MODE=0` approve STARK landing and applying on the live sequencer. + +### Usability + +`msig_core` gives integrators byte-identical hashing and Merkle math, so a client +cannot diverge the root or nullifier computation from the guest. The `run_*` bins +are copy-able worked examples for each instruction, including the hard privacy +approve. The Basecamp `ui_qml` module drives the same client path from a GUI and is +hosted as a signed, multi-variant downloadable `.lgx`. Limitation: no packaged +stand-alone SDK crate in this rev. + +### Reliability + +The verifier's failure modes are deterministic and carry stable assert strings, +re-checked by the negative circuit tests (non-member, double vote, default rider) +and the apply-rejection state tests. Approvals are durable on-chain; a partial +approval set survives a sequencer kill+restart on the same RocksDB +(`scripts/lp0002-resume-rc5.sh`). Limitation: a polished member-facing proof-failure +UX is not built. + +### Performance + +Measured on the live `RISC0_DEV_MODE=0` run: the approve inner guest is 262,144 +total RISC0 cycles (197,041 user, 1 segment); the outer succinct circuit is +1,048,576 cycles; the succinct receipt is 229,379 bytes; one full approve proves in +~180 s wall. On a zkVM execution zone the RISC0 cycle count is the compute-unit +metric. + +### Supportability + +This solution plus `docs/LP-0002-solution.md` document the cryptographic approach, +the nullifier scheme, the LEZ account model (nonce and `program_owner`), the security +assumptions, the known limitations, and the integration guide. The LP-0002 CI +workflow runs the msig core/state/circuit tests plus a standalone-sequencer +end-to-end job (`msig-e2e-devmode`) under `RISC0_DEV_MODE=1`. + +## Evidence (live on `testnet.lez.logos.co`, v0.2.0-rc5) + +Full 2-of-3 threshold run, HD-nsk-derived members, program +`9pwpqhXCZqzBDYctvTvzPeV1qoviSAENw2utmayHgvBF`, `RISC0_DEV_MODE=0` real STARKs. +Every value below is independently re-queryable from the live chain. + +- Deploy: `2262403372e8681604ce330f0040a1680b89f7db1c622ad6087e2bcf92fe8892` +- Proposal `Hf84MVjYamaaCxmBpziYEow6JNuLH7SBNdzLwArf23vu` (member_root `fe674331`, proposal_id `9f1c47a2`, threshold 2) +- Enroll ×3: `078237c5` / `2796569d` / `d138eb29` +- Approve #0 (member 0): `2614f4a9193080a03190499055b9280ecdd11d3dfaca9ae28ef4d81a14ce0bb7`, real STARK, count 0 → 1, vote nullifier `a139609a27d7195bd7e4b7dec24b4d902f759ec511e5d1ea124af1d465c123d1` +- Approve #1 (member 1): `09f0067273f1cc96a44c283cd2c798beeccb6722ae7d4e5926147fa3e482f686`, real STARK, count 1 → 2, vote nullifier `0e491ba754364e27ee0b9a0b838701c72481f5eb37773e9c1e9494504dd96d97` +- InitTreasury `d397291b` (treasury `78gWzy7g`) / `4f191345` (recipient `FKFUCJEd`); Fund 100 `c851e0e4`; Execute `2354ebbd131ef55b8df77f3323500b3a8f36b3462de4b7dfc98137d617a24908` +- On-chain end-state (re-queried after the run): proposal count = 2, treasury = 0, recipient = 100 + +The two vote nullifiers are **distinct** (two different members); the proposal +state stores only root + id + count + the opaque nullifiers, never any member +identity. The live proposal account's `program_owner` equals the deployed program +id `9pwpqhXC...`. Any hash is verifiable with +`wallet chain-info transaction --hash `. + +## Reproduce + +From a clean clone (real proofs, the default gate; install Rust + the RISC0 +toolchain via `rzup install` first, see the repo README): + +```bash +./demo.sh # real proofs by default; or: RISC0_DEV_MODE=0 scripts/lp0002-demo-rc5.sh +``` + +It boots its own local standalone sequencer and drives deploy → enroll×3 → +create_proposal → approve×2 (two real STARKs) → init treasury → fund → execute → +assert (count = 2, treasury drained, recipient credited). The partial-approval +restart-resume is `scripts/lp0002-resume-rc5.sh`. + +## Known limitations (honest scope) + +- **Member set is public.** Anonymity is over which member approved, within a + public enrolled set (the standard Semaphore model, consistent with the prize's + "only member identity and vote are private" scope). +- **32-member cap** (`TREE_DEPTH = 5`); a deeper tree is a one-line change at + higher proof cost. +- **Standard-laptop proving timing not separately measured** (measured on the + build host; proving is RAM/CPU-bound). +- Demo keys/amounts are throwaway constants. + +## Terms & Conditions + +By submitting this solution, I confirm that I have read and agree to the +[Terms & Conditions](../TERMS.md).