diff --git a/solutions/LP-0002.md b/solutions/LP-0002.md new file mode 100644 index 0000000..a369038 --- /dev/null +++ b/solutions/LP-0002.md @@ -0,0 +1,87 @@ +# Solution: LP-0002 -- Private M-of-N Multisig + +**Submitted by:** retraca + +## Summary + +Private M-of-N multisig for the Logos Execution Zone. Members hold shielded accounts; on-chain observers see only that M valid votes were received, not which members approved. + +Members register a `member_commitment = SHA256("member" || nsk || multisig_id)` at initialization. The nsk stays client-side. To vote, a member runs the RISC0 guest off-chain, which proves nsk knowledge without revealing which commitment was used. + +## Repository + +- **Repo:** https://github.com/retraca/lp-0002-private-multisig +- **Program ID:** `9abec04f2a082b6bf70f5a38f2dc967cc7605b3159a6713d93e62f76b0a55725` + +## Approach + +### LEZ nonce constraint + +LEZ private accounts increment their nonce on every spend. Direct signing for multisig participation would require a spend on every vote, tying votes to the nonce sequence and breaking privacy. This implementation avoids the constraint: members register separate voting commitments derived from a fresh nsk. Voting proves nsk knowledge via ZK -- no LEZ spend occurs. + +### Threshold proof scheme + +Each vote is a separate RISC0 receipt. The on-chain program counts valid, non-duplicate receipts and gates execution at the threshold. The alternative -- a single proof of M voters (FROST, Semaphore batch) -- requires off-chain coordination between M members before proof generation. Separate receipts let each member vote independently. + +### Privacy design + +Journal: `(multisig_id, proposal_id, nullifier, member_set_root)` + +`member_set_root = SHA256(commitment[0] || ... || commitment[N-1])` proves the voter belongs to the registered set without revealing which specific commitment was used. An earlier design committed the specific `member_commitment` directly -- that leaks voter identity since all N commitments are public on-chain. The `member_set_root` approach closes that leak. + +### Why Logos + +LEZ's trustless execution guarantees `execute()` only fires once M valid receipts are on-chain. Logos Messaging provides a private coordination channel for passing proposals and receipts between members without a central server. A centralised alternative would require trusting the coordinator not to censor votes or reveal member identities. + +## Success Criteria Checklist + +- [x] Any M-of-N member can submit an approval without revealing their identity to on-chain observers or other members. +- [x] The on-chain verifier confirms a threshold of M approvals was reached without recording which members approved. +- [x] A member cannot approve the same proposal twice (nullifier = `SHA256("multisig/v1/vote" || nsk || proposal_id || multisig_id)`). +- [x] A completed execution is unlinkable to any individual member's shielded account. +- [x] Proof generation runs client-side on a standard laptop. +- [x] Full documentation and a clean public repository are delivered. +- [x] Provide a module/SDK (`sdk/src/lib.rs`: `derive_commitment`, `submit_vote`, `execute_proposal`). +- [x] Provide a Logos Basecamp app GUI with local build instructions and loadable assets (`basecamp-app/`: `module.json`, `index.html`, `README.md` — loads directly in Logos Basecamp, no build step). +- [x] Provide an IDL for the LEZ program (`lp-0002-private-multisig.idl.json`). +- [x] The system handles proof generation failures gracefully and surfaces a clear error. +- [x] A partial set of approvals is preserved and resumable across client restarts (spent nullifiers and vote counts persist in on-chain state; any client can resume by reading current state). +- [x] The verifier program returns deterministic, documented error codes (6001-6011). +- [ ] At least 1 multisig instance created on LEZ testnet with a proposal submitted, approved, and executed. **Partial:** program deployed (tx `82de65cf…235005`), 2-of-3 instance initialized (account `6c0238c2…b424`, tx `419ddedd…452c`), and proposal submitted (tx `38faa9a3…023a`) on the hosted testnet `https://testnet.lez.logos.co` -- see `docs/TESTNET_EVIDENCE.md` in the repo. On-chain vote submission is blocked by an LEZ platform constraint (public transactions carry no RISC0 receipts, so the program's `env::verify` assumption cannot be resolved); the fix is submitting votes via the LEZ privacy-preserving transaction path, in progress. +- [ ] Document compute unit (CU) costs on LEZ devnet/testnet. +- [ ] End-to-end integration tests against a LEZ sequencer in CI. +- [x] CI green on the default branch. +- [x] README documents end-to-end usage. +- [x] Reproducible end-to-end demo script (`demo.sh`). +- [ ] Recorded video demo with `RISC0_DEV_MODE=0` terminal output. + +## FURPS Self-Assessment + +### Functionality + +Four on-chain instructions: `initialize`, `submit_proposal`, `vote`, `execute`. `vote` checks proof validity, multisig/proposal binding, `member_set_root` match against the registered set, and nullifier uniqueness. `execute` fires when `vote_count >= threshold` and marks the proposal executed; the caller reads `proposal.action_bytes` from the returned state to dispatch the encoded instruction. Proposal content is public (only member identity and votes are private, per spec). Max 20 members, max 100 proposals -- documented limitations. Error codes 6001-6011 cover all invalid states. + +### Usability + +CLI covers the full flow: `multisig derive-commitment / vote / verify` for offline use, and `multisig chain initialize / submit-proposal / submit-vote / execute` for on-chain submission (compiled with `--features chain`, which links against `lez-build`). SDK crate provides `derive_commitment`, `submit_vote`, `execute_proposal` for integration. `demo.sh --dev` runs end-to-end with mock proofs in seconds; `demo.sh --dev --chain` adds the on-chain steps against a local or testnet sequencer (`SEQUENCER=https://testnet.lez.logos.co`). + +### Reliability + +Proof verification fails closed. Spent-nullifier tracking persists in on-chain state across restarts. Double-vote is rejected even after process restart. `spent_nullifiers` grows linearly with vote count -- acceptable for the max 100 proposals constraint. + +### Performance + +CU costs: `initialize` and `submit_proposal` execute in ~5-9 ms of zkVM executor time on the sequencer (well under the 32M-cycle public execution budget); receipt-verifying instructions pending the privacy-path rework. RISC0 vote proof generation at `RISC0_DEV_MODE=0`: approximately 8-12 minutes on a 2024 MacBook Pro (M3). + +### Supportability + +Integration tests in `programs/multisig/tests/integration.rs` exercise `apply_vote` and `apply_execute` directly (no sequencer or RISC0 receipt needed): successful vote, threshold-reached execution, threshold-not-met block, double-vote nullifier rejection, multisig mismatch, unregistered member set root, already-executed rejection, and vote against an executed proposal. CI runs `cargo check`, `cargo clippy -D warnings`, and `cargo test` with `RISC0_DEV_MODE=1`. + +## Supporting Materials + +- Demo video: _pending testnet deployment_ +- Repository: https://github.com/retraca/lp-0002-private-multisig + +## Terms & Conditions + +By submitting this solution, I confirm that I have read and agree to the [Terms & Conditions](../TERMS.md). The code is MIT licensed.