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