From deda35630c6bea3dd23ecc136ae723561dc5536c Mon Sep 17 00:00:00 2001 From: Davit Maisuradze <87044530+jeefxM@users.noreply.github.com> Date: Sun, 14 Jun 2026 11:01:56 +0400 Subject: [PATCH 1/3] Solution: LP-0002 - Private M-of-N Multisig --- solutions/LP-0002.md | 212 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 solutions/LP-0002.md diff --git a/solutions/LP-0002.md b/solutions/LP-0002.md new file mode 100644 index 0000000..65c6bb6 --- /dev/null +++ b/solutions/LP-0002.md @@ -0,0 +1,212 @@ +# Solution: LP-0002 - Private M-of-N Multisig + +**Submitted by:** Davit Maisuradze ([@jeefxM](https://github.com/jeefxM)) + +## 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 (~174 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 shielded accounts by derivation.** Each member's + membership secret IS their real shielded-account nullifier secret key (`nsk`), + HD-derived from the LEZ key tree. Control of the `nsk` is control of the + account. The enrolled leaf is `H(LEAF_DOMAIN || nsk)`, a one-way hash, so the + public registry never links a leaf (or a later vote) to an on-chain account. + Stated honestly: this is derivation-binding, not an in-circuit proof that the + leaf maps to a live account in the chain's commitment tree. +- **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** under deployed program id + `HjHCub28GrUNgd2QuJ2SPob7YmaUgDRCGXwbt2jt4UWn`, with a full 2-of-3 lifecycle + (proposal `Hf84MVjY`) landed on chain and independently verifiable. +- **Reference Logos Basecamp module** (`ui_qml` plugin) that derives a leaf and + 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 +- **Branch:** `main` +- **Program id (LEZ testnet):** `HjHCub28GrUNgd2QuJ2SPob7YmaUgDRCGXwbt2jt4UWn` +- **Video Demo:** https://www.youtube.com/watch?v=CXzqWLvBY0A + +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/blob/main/docs/LP-0002-solution.md). +Proving-time and cost proxies are in `docs/lp0002-benchmarks.md`; failure modes +and error codes in `docs/lp0002-reliability.md`; the architecture map and diagram +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. +- **Shielded-account binding:** membership secret = the member's real + shielded-account `nsk` (`SeedHolder -> SecretSpendingKey -> + produce_private_key_holder(i).nullifier_secret_key`). Derivation-binding + (control of the key = control of the account), stated honestly as not an + in-circuit live-account proof. +- **LEZ account-model compatibility:** the approve runner reads the proposal's + **live nonce** right before proving (private accounts can't provide the fresh + zero-nonce the public multisig path expects), and because only an account's + owning program can move its balance, 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 + +- [~] **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`, so membership is bound to a shielded account + by derivation. Partial because the binding is by derivation, not an in-circuit + proof that the leaf maps to a live account in the commitment tree (that path + was not a proven primitive for a custom program on this testnet rev). +- [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 ~174 s on the 16-core 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 `HjHCub28...`, proposal + `Hf84MVjY`, tx hashes below. +- [x] **Full documentation and a clean public repository.** Delivered at + `github.com/jeefxM/lp-0002-private-multisig`. + +### 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. +- [~] **Logos Basecamp app GUI.** A native `ui_qml` Basecamp plugin + (`basecamp/`, prebuilt `msig_plugin.so`) loads in Basecamp's host and casts a + real anonymous vote through the same client path (sidecar spawns + `run_approve_secret`, a real `RISC0_DEV_MODE=0` STARK), demonstrated in the + video. Prebuilt downloadable assets are not separately hosted. +- [~] **SPEL IDL.** `idl/lp0002-msig.idl.json` (declares spec `spel-0.1`, + address `HjHCub28...`) 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. +- [ ] **Partial-approval resume across client restarts.** Approvals are durable + on-chain (count + nullifier list persist), so a partial set survives restarts at + the state layer, but a client resume/recovery flow is not implemented. Open. +- [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"); negative tests pin non-member and double-vote rejection. + +### Performance + +- [ ] **CU cost of each on-chain operation.** Open. This nssa v0.1.2 rev exposes + no compute-unit / gas / fee field (verified absent in the wallet CLI, + chain-info tx/block, and `common/transaction.rs`). Documented with defensible + proxies instead of a fabricated number: real proof ~174 s, succinct receipt + ~224 KB, and the approve tx (carrying the receipt) vs a tiny no-proof Execute tx + as a relative block-size proxy. See `docs/lp0002-benchmarks.md`. + +### Supportability + +- [x] **Deployed and tested on LEZ testnet.** Program `HjHCub28...`, 2-of-3 run. +- [~] **End-to-end integration tests against a standalone sequencer, in CI.** The + 8 state tests + 4 circuit tests exercise the full flow in-process and run in CI + under `RISC0_DEV_MODE=1`; a standalone-sequencer end-to-end CI job is not wired + (the reproducible `scripts/lp0002-demo.sh` covers that path locally). +- [x] **CI green on the default branch.** `.github/workflows/lp0002-ci.yml` runs + the msig core/state/circuit tests + runner build and is green on `main`. +- [~] **README documents end-to-end usage.** The LP-0002 README + write-up cover + deployment, the program address, and the run commands; a full CLI/Basecamp + step-by-step walkthrough is partial. +- [x] **Reproducible demo script at `RISC0_DEV_MODE=0`.** `scripts/lp0002-demo.sh` + runs green end-to-end against a local standalone sequencer (two real proofs, + count = 2, treasury drained, recipient credited); re-verified from a fresh + checkout on a reviewer-equivalent toolchain state. +- [x] **Narrated video showing terminal output confirming `RISC0_DEV_MODE=0`.** + https://www.youtube.com/watch?v=CXzqWLvBY0A + +## Evidence (live on `testnet.lez.logos.co`) + +2-of-3 threshold run, HD-nsk-derived members, program `HjHCub28...`, `RISC0_DEV_MODE=0`: + +- Proposal `Hf84MVjY` (member_root `38ea719c`, proposal_id `9f1c47a2`) +- Approve #0 (member 0): `09c9cf27`, real STARK 174.18 s, count 0 → 1, vote nullifier `748015dc` +- Approve #1 (member 1): `83007dcd`, real STARK 173.78 s, count 1 → 2, vote nullifier `7d37760a` +- InitTreasury `9bfb9fde` / `6696b49d`; Fund 20 `7db0d6c7`; Execute `deed4d0c` +- On-chain assert: count = 2, treasury = 0, recipient = 20, all as expected + +The two vote nullifiers are distinct (two different members); the proposal state +stores only root + id + count + opaque nullifiers. Treasury and recipient are +fresh PDAs (uninitialized before the run), so the credit of exactly 20 is +attributable solely to this execute. Any hash is verifiable with +`wallet chain-info transaction --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 +RISC0_DEV_MODE=0 scripts/lp0002-demo.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). + +## 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). +- **Derivation-binding, not in-circuit live-account binding** (see Functionality). +- **32-member cap** (`TREE_DEPTH = 5`); a deeper tree is a one-line change at + higher proof cost. +- **No CU/gas surface** on this rev (documented with proxies). +- **No client-side partial-approval resume UX** (state persists on-chain). +- Demo keys/amounts are throwaway constants; single-signer enrollment in this rev. + +## Terms & Conditions + +By submitting this solution, I confirm that I have read and agree to the +[Terms & Conditions](../TERMS.md). From 7377a914a237348954d76681580c14a25c143b0a Mon Sep 17 00:00:00 2001 From: Davit Maisuradze <87044530+jeefxM@users.noreply.github.com> Date: Sun, 14 Jun 2026 11:11:36 +0400 Subject: [PATCH 2/3] LP-0002 solution: add FURPS Self-Assessment section and license note --- solutions/LP-0002.md | 49 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/solutions/LP-0002.md b/solutions/LP-0002.md index 65c6bb6..8c67b0f 100644 --- a/solutions/LP-0002.md +++ b/solutions/LP-0002.md @@ -39,6 +39,7 @@ reimplemented for LEZ). - **Branch:** `main` - **Program id (LEZ testnet):** `HjHCub28GrUNgd2QuJ2SPob7YmaUgDRCGXwbt2jt4UWn` - **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, @@ -165,6 +166,54 @@ Mapped to the prize's criteria. `[x]` met, `[~]` partial (honest scope noted), - [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. Functional tests run under `RISC0_DEV_MODE=1` for logic +coverage; 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 (live nonce fetch, witness assembly, proof, submit). The Basecamp `ui_qml` +module drives the same client path from a GUI. 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) and the +apply-rejection state tests. Approvals are durable on-chain (the count and the +nullifier list persist), so a partial approval set survives client restarts at +the state layer. Limitation: a client-side resume/recovery UX over that durable +state is not implemented; proof-failure surfacing is functional but not polished. + +### Performance + +This rev exposes no CU / gas / fee surface, so we document defensible proxies +instead of fabricating a number: a real `RISC0_DEV_MODE=0` approve proof takes +about 174 s on the build host; the on-chain receipt is a succinct STARK of about +224 KB; and the approve transaction (carrying the receipt) is substantially +larger on-chain than a tiny no-proof Execute transaction, the closest stand-in +for per-op cost on a chain with no exposed gas meter. + +### 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 under +`RISC0_DEV_MODE=1` and is green on `main`. A standalone-sequencer end-to-end CI +job is the one open item. + ## Evidence (live on `testnet.lez.logos.co`) 2-of-3 threshold run, HD-nsk-derived members, program `HjHCub28...`, `RISC0_DEV_MODE=0`: From 891c56b817c0c86fd497d441f50dea3ec5fb9fa3 Mon Sep 17 00:00:00 2001 From: Davit Maisuradze <87044530+jeefxM@users.noreply.github.com> Date: Sun, 14 Jun 2026 11:14:22 +0400 Subject: [PATCH 3/3] LP-0002 solution: point Reproduce at the ./demo.sh entrypoint --- solutions/LP-0002.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solutions/LP-0002.md b/solutions/LP-0002.md index 8c67b0f..1b88ba9 100644 --- a/solutions/LP-0002.md +++ b/solutions/LP-0002.md @@ -236,7 +236,7 @@ From a clean clone (real proofs, the default gate; install Rust + the RISC0 toolchain via `rzup install` first, see the repo README): ```bash -RISC0_DEV_MODE=0 scripts/lp0002-demo.sh +./demo.sh # or, explicitly: RISC0_DEV_MODE=0 scripts/lp0002-demo.sh ``` It boots its own local standalone sequencer and drives deploy → enroll×3 →