diff --git a/solutions/LP-0002.md b/solutions/LP-0002.md new file mode 100644 index 00000000..aec01477 --- /dev/null +++ b/solutions/LP-0002.md @@ -0,0 +1,198 @@ +# Solution: LP-0002 Private M-of-N Multisig + +**Submitted by:** Davit Maisuradze ([@jeefxM](https://github.com/jeefxM)) + +> **Resubmission (third), ported to Logos LEZ v0.2.4.** This addresses each point +> from the review of [PR #97](https://github.com/logos-co/lambda-prize/pull/97): +> +> 1. **"`demo` script is not actually doing `RISC0_DEV_MODE=0` since it is a +> hardcoded value in the internal script."** Correct, and fixed. The inner +> script clobbered the exported value; it now honours an inherited +> `RISC0_DEV_MODE` and **`./demo.sh` is the documented entrypoint**, defaulting +> to `0`. It is verified green end-to-end **from a fresh clone with real +> STARKs** — 2 h 01 m, `DEMO_EXIT=0`, two genuine ~261 KB succinct receipts. +> The verbatim trace, including the boot line reading +> `(RISC0_DEV_MODE=0)`, is committed at `evidence/demo-realproof-v024.txt`. +> 2. **"CI on default branch must be green."** It is: +> [run 33169435466](https://github.com/jeefxM/lp-0002-private-multisig/actions/runs/33169435466) +> — `msig-tests` (13 tests + runner build) and `msig-e2e-devmode` (the full +> 2-of-3 against a standalone sequencer) both green on `main`. The fork now +> ships only the LP-0002 workflow, so upstream suites this work does not touch +> cannot red the branch. +> 3. **"there are not transactions that I could verify."** The testnet had been +> wiped and redeployed (v0.2.0-rc5 → v0.2.4) between submission and review, +> which retroactively orphaned the hashes. The whole solution is now ported to +> **v0.2.4** with a fresh chain-verified ledger, and the evidence is built to +> survive the next wipe: raw timestamped JSON-RPC responses for all 18 ledger +> artifacts are committed under `evidence/`, and the flow regenerates on the +> current chain with one script (`evidence/README.md`). +> +> Also disclosed up front rather than left to be discovered: **`Execute` is not +> authorization-gated on this rev** — see Security assumptions. The anonymity and +> threshold-counting properties this prize targets are unaffected, but the release +> path is not, and the fix changes the guest image id (and therefore invalidates +> the on-chain evidence above), so it is queued as the immediate follow-up rather +> than silently patched into this submission. + +## Summary + +A private M-of-N multisig primitive for the Logos Execution Zone (LEZ). Members +enroll a public leaf into an on-chain registry, a proposal freezes the member +set, and each approval is a privacy-preserving (ZK) transaction that proves +membership in the frozen member set and records a proposal-bound nullifier. The +on-chain ProposalState records only the member root, the proposal id, a public +approval count, and an opaque list of vote nullifiers. It does not record which +member approved. Once the count reaches the threshold, an Execute instruction +fires a treasury release through a chained call to the `authenticated_transfer` +builtin. + +The design gives approver anonymity within the enrolled set of N public members; +the approval count is public, which specific member approved is hidden. + +- **Per-member ZK membership proofs.** Each approval is a RISC0 zkVM + privacy-preserving transaction proving a depth-5 Merkle membership path against + the proposal's frozen `member_root`, with the member secret carried as a + private witness. The threshold is enforced by a public approval count, not by + signature aggregation. This is NOT FROST signature aggregation. +- **Proposal-bound nullifiers** prevent a member from approving the same + proposal twice, while leaving the same member free to approve a different + proposal. +- **Threshold-gated treasury release.** Execute checks `count >= M` and chains a + debit of the treasury PDA to the recipient via `authenticated_transfer`. +- **Live on LEZ testnet** (`testnet.lez.logos.co`). Built on Logos LEZ + **v0.2.4** (the current testnet rev) and deployed under program id base58 + `4tvD5XPFc4ofgN3YV4ymZ1nWqn3iUwB9tucesYzBKJB9` (decimal words + `[1155063609, 1918607948, 1043343914, 2266441241, 1831314946, 53341822, 1565811176, 2148869898]`). + The testnet is periodically **wiped and redeployed** — it has happened twice + during this submission's lifetime (v0.1.2 → v0.2.0-rc5 → v0.2.4), each time + invalidating the previous program id and transaction hashes. The evidence + model is built for that: the run below is chain-verified as of its capture + date, the raw RPC responses are committed under `evidence/`, and the whole + 2-of-3 flow is reproducible on the current chain with one script (see + `evidence/README.md`). Every approval is generated by a local + `RISC0_DEV_MODE=0` run and lands on chain as a real STARK; the full v0.2.4 + 2-of-3 run is **complete and chain-verified** (deploy tx + `047668a5ba871873645c2ff412414dfbe79526e1c205ccf5dd0baa474e865df1`; proposal + `Hf84MVjYamaaCxmBpziYEow6JNuLH7SBNdzLwArf23vu` reached `approval_count == 2` at + threshold 2 and then executed — treasury 100 → 0, recipient 0 → 100). See + Supporting Materials for the full ledger. + +## Repository + +- **Repo:** https://github.com/jeefxM/lp-0002-private-multisig +- **Branch:** `main` · **Rev:** Logos LEZ **v0.2.4** (crate `lee`) +- **Licence: MIT.** This repository is a fork of upstream Logos LEZ v0.2.4, which + is MIT-licensed (`LICENSE`, Copyright Vac); our LP-0002 contribution is offered + under the same MIT terms. `NOTICE` lists exactly which files are ours versus + upstream. +- **Program id (LEZ testnet):** `4tvD5XPFc4ofgN3YV4ymZ1nWqn3iUwB9tucesYzBKJB9` +- **Demo script:** `./demo.sh` (real STARKs by default) · + **SPEL IDL:** `idl/lp0002-msig.idl.json` · + **Basecamp module:** `basecamp/module.json` +- **Green CI:** https://github.com/jeefxM/lp-0002-private-multisig/actions/runs/33169435466 +- **Video demo:** https://www.youtube.com/watch?v=CXzqWLvBY0A + +## Approach + +The design reimplements the Semaphore/MACI structural pattern — a public leaf +commitment in a Merkle set, an in-circuit membership proof, and a +domain-separated nullifier bound to the action — natively for the LEZ +privacy-preserving transaction model and the RISC0 zkVM. Neither codebase was +ported: both target an EVM/circom toolchain with no meaning against a RISC0 +guest and LEZ's account model. + +**Why the Logos stack.** The property this primitive sells is approver anonymity +that an operator cannot revoke. On a centralised alternative the operator sees +every approval as it arrives, so anonymity is a promise rather than a property, +and the operator can censor or fabricate one. LEZ removes both: the membership +witness never leaves the prover, the sequencer sees only an unattributable +commitment/nullifier pair, and the threshold release is program logic rather than +operator discretion. Three LEZ properties carry the design — the +privacy-preserving transaction model (a single tx mutates a PUBLIC counter while +its instruction data stays private, which is what makes a public threshold over +anonymous approvals possible at all), the account/nullifier model (a native +double-vote guard bound to a live shielded account), and RISC0 guest execution +(the membership check is ordinary Rust, not a hand-written circuit). + +**Alternatives considered, and what did not work.** FROST/threshold signature +aggregation was rejected: it makes the signer set the aggregated object, and +anonymity then rests on the aggregation. We use a public count over per-member ZK +membership proofs instead — independently verifiable, trivially auditable, no +inter-member coordination — at the cost of one STARK per approval. A +derivation-only membership binding was tried and proved insufficient (the +first-round review was right to call it out); the approval now rides the member's +live shielded account with the id re-derived in-guest. Encoding chained-call data +as a bare `u128` broke on-chain against the typed `Instruction` enum. Funding a +fresh treasury PDA by plain transfer fails outright — `authenticated_transfer` +would claim it `Authorized` and a PDA cannot sign — so `InitTreasury` claims the +PDA first; that asymmetry is captured as a regression test. + +**Full technical write-up** — threshold scheme, nullifier design, LEZ account +model, security assumptions (including a disclosed authorization gap in +`Execute`), known limitations, benchmarks, reliability and error tables: +https://github.com/jeefxM/lp-0002-private-multisig/blob/main/docs/LP-0002-solution.md + +## Success Criteria Checklist + +- [x] **Anonymous M-of-N approval.** Each approval is a RISC0 privacy-preserving + transaction proving depth-5 Merkle membership against the proposal's frozen + `member_root`; the state records only root, id, count and opaque nullifiers. +- [x] **A member cannot approve twice.** Proposal-bound nullifier + `H(NULL_DOMAIN || secret || proposal_id)` with an in-guest already-recorded check. +- [x] **Threshold-gated release.** `Execute` asserts `count >= M` and chains the + treasury debit through `authenticated_transfer`. +- [x] **Bound to a live shielded account, in-circuit** (review item #6): the guest + asserts the rider's `AccountId` re-derives from the same secret and that the + rider is a live, non-default account. +- [x] **Live on LEZ testnet, chain-verified**, with raw JSON-RPC captures committed + and a one-script regeneration path: https://github.com/jeefxM/lp-0002-private-multisig/tree/main/evidence +- [x] **Reproducible demo at `RISC0_DEV_MODE=0`**: `./demo.sh`, verified green from + a fresh clone (`DEMO_EXIT=0`, 2 h 01 m, two ~261 KB succinct receipts). +- [x] **CI green on the default branch**: https://github.com/jeefxM/lp-0002-private-multisig/actions/runs/33169435466 +- [x] **Compute cost measured** (no native CU/gas field on this rev): inner msig + guest 1,048,576 cycles, outer circuit 4.7–5.2M cycles, ~261 KB receipts — + https://github.com/jeefxM/lp-0002-private-multisig/blob/main/evidence/measure-v024.txt +- [x] **SPEL IDL**: `idl/lp0002-msig.idl.json` · **Basecamp module**: + `basecamp/module.json` (+ signed multi-variant `.lgx` release) +- [x] **Narrated video demo**: https://www.youtube.com/watch?v=CXzqWLvBY0A + +## FURPS Self-Assessment + +Full per-item assessment with evidence links is in the write-up: +https://github.com/jeefxM/lp-0002-private-multisig/blob/main/docs/LP-0002-solution.md + +### Functionality + +Anonymous M-of-N approval, proposal-bound double-vote rejection, threshold-gated +treasury release, and the in-circuit live-account binding all implemented and +covered by 13 tests (8 state/apply-path, 5 circuit) plus a live-testnet 2-of-3. + +### Usability + +One-command demo (`./demo.sh`), a per-step CLI walkthrough, a Basecamp `ui_qml` +module, and a narrated video. Real-proof runs need >6 GB RAM and ~30 min per +approve; that budget is documented up front. + +### Reliability + +Every failure mode is enumerated with its guest source line and member-facing +meaning in `docs/lp0002-reliability.md`. Partial-approval state survives a +sequencer restart (demonstrated by `scripts/lp0002-resume.sh`). + +### Performance + +Measured on v0.2.4: inner msig guest 1,048,576 cycles (~4.6 min), outer privacy +circuit 4,718,592–5,242,880 cycles (~26–28 min), succinct receipt ~261 KB, on an +8 vCPU / 15 GB host. Public operations are sub-second. Full table in +`docs/lp0002-benchmarks.md`. + +### Supportability + +Dual-audience docs (README walkthrough + full solution write-up), a NOTICE +delimiting our work from upstream, committed raw evidence, and CI running both +the unit suite and the end-to-end flow against a standalone sequencer. + +## Terms & Conditions + +By submitting this solution, I confirm that I have read and agree to the +[Terms & Conditions](https://github.com/logos-co/lambda-prize/blob/master/TERMS.md).