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/7] 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/7] 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/7] 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 → From 2efd7a9a29550f7fff31c9391c94808890b5f861 Mon Sep 17 00:00:00 2001 From: Davit Maisuradze <87044530+jeefxM@users.noreply.github.com> Date: Fri, 26 Jun 2026 23:18:33 +0400 Subject: [PATCH 4/7] LP-0002 resubmission: rc5 port, all 6 review items addressed, in-circuit binding, hosted signed module --- solutions/LP-0002.md | 243 ++++++++++++++++++++----------------------- 1 file changed, 112 insertions(+), 131 deletions(-) diff --git a/solutions/LP-0002.md b/solutions/LP-0002.md index 1b88ba9..000cb53 100644 --- a/solutions/LP-0002.md +++ b/solutions/LP-0002.md @@ -2,6 +2,16 @@ **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 @@ -11,42 +21,49 @@ 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`) +- **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 shielded accounts by derivation.** Each member's +- **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. 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. + 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** under deployed program id - `HjHCub28GrUNgd2QuJ2SPob7YmaUgDRCGXwbt2jt4UWn`, with a full 2-of-3 lifecycle +- **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) 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`. +- **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 +- **Repo:** https://github.com/jeefxM/lp-0002-private-multisig-rc5 - **Branch:** `main` -- **Program id (LEZ testnet):** `HjHCub28GrUNgd2QuJ2SPob7YmaUgDRCGXwbt2jt4UWn` +- **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`) - **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/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 +[`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 @@ -67,15 +84,16 @@ public count over per-member ZK membership proofs. 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`- +- **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`. @@ -86,13 +104,13 @@ Mapped to the prize's criteria. `[x]` met, `[~]` partial (honest scope noted), ### Functionality -- [~] **Anonymous M-of-N approval by a shielded-account holder.** The approval is +- [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`, 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). + 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 @@ -101,133 +119,96 @@ Mapped to the prize's criteria. `[x]` met, `[~]` partial (honest scope noted), 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. + `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 `HjHCub28...`, proposal + 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`. + `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. -- [~] **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. +- [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. -- [ ] **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] **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"); negative tests pin non-member and double-vote rejection. + mismatch", "rider must be a LIVE funded account"); negative tests pin non-member, + double-vote, and default-rider 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`. +- [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 `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] **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 + 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` + 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); re-verified from a fresh - checkout on a reviewer-equivalent toolchain state. + 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. 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`) +## Evidence (live on `testnet.lez.logos.co`, v0.2.0-rc5) -2-of-3 threshold run, HD-nsk-derived members, program `HjHCub28...`, `RISC0_DEV_MODE=0`: +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. -- 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 +- 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 + 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 +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 `. ## Reproduce @@ -236,24 +217,24 @@ 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 # or, explicitly: RISC0_DEV_MODE=0 scripts/lp0002-demo.sh +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). +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). -- **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. +- **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 From e2f6aa5e94d808b3916313cb5c3a8407e5c19553 Mon Sep 17 00:00:00 2001 From: Davit Maisuradze <87044530+jeefxM@users.noreply.github.com> Date: Sat, 27 Jun 2026 00:12:00 +0400 Subject: [PATCH 5/7] LP-0002: restore FURPS Self-Assessment section (validation requirement) --- solutions/LP-0002.md | 49 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/solutions/LP-0002.md b/solutions/LP-0002.md index 000cb53..2bbd119 100644 --- a/solutions/LP-0002.md +++ b/solutions/LP-0002.md @@ -191,6 +191,55 @@ 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. 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 From ed0eda20674e1830e0823788fcf0c884b1e3bebe Mon Sep 17 00:00:00 2001 From: Davit Maisuradze <87044530+jeefxM@users.noreply.github.com> Date: Sat, 27 Jun 2026 00:13:46 +0400 Subject: [PATCH 6/7] LP-0002: Reproduce via ./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 2bbd119..8480191 100644 --- a/solutions/LP-0002.md +++ b/solutions/LP-0002.md @@ -266,7 +266,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-rc5.sh +./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 → From 2e5e052b9793e72423ecaa50dac4f93945fe22c6 Mon Sep 17 00:00:00 2001 From: Davit Maisuradze <87044530+jeefxM@users.noreply.github.com> Date: Sat, 27 Jun 2026 00:25:04 +0400 Subject: [PATCH 7/7] LP-0002: reference in-repo basecamp/ module descriptor --- solutions/LP-0002.md | 1 + 1 file changed, 1 insertion(+) diff --git a/solutions/LP-0002.md b/solutions/LP-0002.md index 8480191..02eef45 100644 --- a/solutions/LP-0002.md +++ b/solutions/LP-0002.md @@ -55,6 +55,7 @@ reimplemented for LEZ). (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).