Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions solutions/LP-0002.md
Original file line number Diff line number Diff line change
@@ -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).
Loading