Skip to content
87 changes: 87 additions & 0 deletions solutions/LP-0002.md
Original file line number Diff line number Diff line change
@@ -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.
Loading