Skip to content

Solution: LP-0002 - Private M-of-N Multisig (v0.2.4) - #133

Closed
jeefxM wants to merge 3 commits into
logos-co:masterfrom
jeefxM:lp-0002-v024-solution
Closed

Solution: LP-0002 - Private M-of-N Multisig (v0.2.4)#133
jeefxM wants to merge 3 commits into
logos-co:masterfrom
jeefxM:lp-0002-v024-solution

Conversation

@jeefxM

@jeefxM jeefxM commented Aug 28, 2026

Copy link
Copy Markdown

LP-0002 — Private M-of-N Multisig (third submission, ported to LEZ v0.2.4)

Thanks for the review on #97. Each point is addressed below, with the artifact that backs it.

1. "demo script is not actually doing RISC0_DEV_MODE=0 since it is a hardcoded value in the internal script"

Correct. scripts/lp0002-demo.sh did export RISC0_DEV_MODE=1, which clobbered the value the root demo.sh exported. The inner script now honours an inherited value, ./demo.sh is the documented entrypoint everywhere (the docs previously pointed at the inner script — that was the same defect in prose), and it defaults to 0.

Verified from a fresh clone on a clean host with real STARKs: DEMO_EXIT=0 in 2 h 01 m, two genuine anonymous approvals with ~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 33169435466ci-image, msig-tests (13 tests + runner build) and msig-e2e-devmode (the full 2-of-3 against a standalone sequencer) all green on main. The fork ships only the LP-0002 workflow plus the ci-image machinery it depends on, so upstream suites this work does not touch cannot red the branch.

3. "main part - there are not transactions that I could verify"

The testnet was wiped and redeployed between submission and review (v0.2.0-rc5 → v0.2.4), which retroactively orphaned every hash — the submission was unverifiable through no action of yours. Fixed twice over:

  • The whole solution is ported to v0.2.4 (the current rev) and re-run live: program 4tvD5XPFc4ofgN3YV4ymZ1nWqn3iUwB9tucesYzBKJB9, deploy tx 047668a5…, two anonymous real-STARK approvals f86ffd5d… / 2ae72df7… with distinct proposal-bound nullifiers, execute ded1cec1…, treasury 100 → 0, recipient 0 → 100, approval_count == 2.
  • The evidence is now built to survive the next wipe: raw timestamped JSON-RPC responses for all 18 ledger artifacts are committed under evidence/, and the entire flow regenerates on the current chain with one script (see evidence/README.md).

Also worth your attention

Execute is not authorization-gated on this rev, and I am disclosing it rather than letting you find it. The guest reads threshold from the caller's instruction and only asserts count >= threshold; it does not bind the proposal/treasury/recipient accounts to one another. So Execute { threshold: 0 } can release a msig treasury with no approvals. The properties this prize targets — in-circuit membership, proposal-bound nullifiers, double-vote rejection, the live-account binding from review item #6 — are unaffected; the gap is in the release path. The fix is mechanical but changes the guest image id and would invalidate the on-chain evidence above, so it is the immediate follow-up rather than a silent patch here. Details in Security assumptions.

The round-1 items remain closed and re-verified on v0.2.4: CU cost is measured (evidence/measure-v024.txt), partial-approval restart-resume is demonstrated green, the sequencer e2e is in CI, the README carries the full CLI + Basecamp walkthrough, the Basecamp module is hosted as a signed downloadable (with its provenance stated — it is the first-round artifact and has not been rebuilt for v0.2.4), and the approval binding is an in-circuit live-account proof.

Links

Addresses the review of logos-co#97: the demo entrypoint now really runs at
RISC0_DEV_MODE=0 (verified green from a fresh clone with real STARKs), CI is
green on the default branch, and the whole solution is ported to LEZ v0.2.4 with
a fresh chain-verified ledger plus committed raw JSON-RPC captures so the
evidence survives the next testnet wipe.
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

✅ Validation passed

A reviewer will assess against the prize criteria.
ℹ️ Solution submission for LP-0002.
ℹ️ Checking repo: https://github.com/jeefxM/lp-0002-private-multisig


Automated check. See solution template and TERMS.

jeefxM added 2 commits August 28, 2026 16:35
…ble repo URL, licence

The bot reported 'Checking repo: logos-co/lambda-prize', i.e. it never extracted
our repository link, so every 'missing in linked repo' error cascaded from that:
the Repo line was inline code rather than a URL. Also renamed two headings to the
template's exact wording (## Repository, ## Success Criteria Checklist) and added
the licence (MIT, inherited from the upstream LEZ fork) plus explicit pointers to
demo.sh, the SPEL IDL and basecamp/module.json.
The validator pipes the solution file into grep -qF per required section. grep -q
exits at the first match and closes the pipe, so on a large file the feeding echo
dies with SIGPIPE and the check misreads as 'section missing' -- visible in the
job log as 'line 158: echo: write error: Broken pipe', and it hit exactly the
three earliest sections (Summary/Repository/Approach at lines 37/80/141) while
the later ones passed.

The 50 KB file was also simply too long for a submission entry; passing
submissions are 18-27 KB. This keeps Summary, Repository, a condensed Approach
(with why-Logos and the alternatives//what-failed narrative), the success-criteria
checklist, the FURPS assessment and Terms, and points at
docs/LP-0002-solution.md in the linked repo for the full technical write-up.
Verified against the validator's exact checks locally before pushing.
@weboko

weboko commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Hi @jeefxM !

I really like your demo video and submission overall looks not bad.

However there are some things I believe need to be addressed or clarified:

  • looks like execute() takes threshold from the caller and never binds the supplied recipient to the proposal, while transferring the whole treasury;
  • no test covers it;
  • the solution file's checklist nonetheless claims it's enforced;

Looking forward to your re-submission!

@weboko weboko closed this Sep 7, 2026
@jeefxM

jeefxM commented Sep 8, 2026

Copy link
Copy Markdown
Author

Thanks for the review, and you are right on all three points. Resubmitted with the release path fixed, tested in-process and on-chain, and re-proven on the live testnet.

1. execute() no longer takes anything security-relevant from the caller. CreateProposal now freezes threshold, recipient and amount into the ProposalState header next to member_root and proposal_id (msig_core::ProposalHeader, 124-byte header). Execute { seed } reads all three back and asserts, in order: the proposal is owned by the msig program; it has not executed before; approval_count >= threshold; the supplied recipient equals the frozen recipient; the supplied treasury equals for_public_pda(self_program_id, seed); the treasury holds at least amount. It then chains a transfer of exactly amount (not the whole balance) and marks the proposal executed, so it releases at most once. Guest: lee/state_machine/test_methods/guest/src/bin/msig.rs, execute().

2. Tests. Seven in-process state tests in lee/state_machine/src/state/tests/msig.rs (msig_execute_*), each asserting the exact guest error: below-threshold, wrong recipient, wrong treasury seed, foreign-owned proposal, insufficient treasury, second execution, and the positive case that exactly the frozen amount moves and the proposal is marked executed. The full msig suite is 20 tests green (cargo test -p lee msig_). The end-to-end scripts now also submit three deliberately wrong Execute transactions against a funded treasury and require each to be dropped: at 1-of-2 approvals, with a different recipient account, and again after the release. RISC0_DEV_MODE=1 ./scripts/lp0002-demo.sh checks the sequencer's failed execution check ... Guest panicked: <message> line for each, and the live-testnet run shows the same three absent from every block with the treasury untouched (state snapshots under evidence/).

3. Solution doc. The checklist item now states what is enforced and where; the "known limitation" block is replaced by the fixed-in-this-rev description with the test list; the error-string list includes the new release-gate rejections.

Because the guest changed, the program id changed; the Supporting Materials are a fresh live 2-of-3 run on the new program id (DLn7wx4RCD3ijThGrxp4T7QuWzUrbmUj9XcpJrJgtxUp) with raw JSON-RPC captures under evidence/, regenerable with scripts/lp0002-capture-evidence.sh. Repo: https://github.com/jeefxM/lp-0002-private-multisig @ 37401f18b (jeefxM/lp-0002-private-multisig@37401f18b); CI: https://github.com/jeefxM/lp-0002-private-multisig/actions/runs/34225361232 (green).

@jeefxM

jeefxM commented Sep 8, 2026

Copy link
Copy Markdown
Author

Reopening with the resubmission, per the review above. This supersedes my comment from yesterday: two things happened after it.

1. The testnet was reset (2026-09-08, ~13:32 UTC), which orphaned yesterday's ledger. The whole 2-of-3 flow was re-run on the new chain generation and re-captured; every hash in the updated solutions/LP-0002.md resolves right now, and the raw JSON-RPC responses are committed under evidence/ with the previous generations' captures kept under evidence/prior-*/ for the record.

2. A second audit pass on the release path found one more gap, now closed. CreateProposal is permissionless and takes member_root from the caller, and the treasury was a program-wide PDA — so a proposal frozen on a member set nobody enrolled could be approved by its creator and pointed at the group's treasury. The treasury a proposal may spend is now derived from the member set it was frozen on (treasury_seed(member_root) = SHA256(TREASURY_DOMAIN || member_root)); Execute asserts the supplied seed equals it before the PDA check, so a proposal on any other root can only reach the (unfunded) treasury of that root. That is disclosed in Security assumptions and Known limitations rather than papered over: the registry is informational, the treasury derivation is what binds.

What the reviewed points look like now, on commit da98a17d5 (jeefxM/lp-0002-private-multisig@da98a17d5):

  • execute() takes nothing security-relevant from the caller: threshold, recipient and amount are frozen at CreateProposal, the treasury is derived from the frozen root, and the release moves exactly the frozen amount, once.
  • Ten msig_execute_* state tests pin each gate (including the foreign-member-set attack in three shapes), plus a header-layout test; 24 msig tests green.
  • Both end-to-end scripts submit four deliberately wrong Execute transactions against a funded treasury and fail unless the chain drops each: below threshold, wrong recipient, treasury seed not derived from the member set, replay after release. On testnet three of the four are getTransaction → null in the ledger; the fourth shares its hash with the later release (an Execute has no nonce) and is evidenced by a timestamped state snapshot.
  • ./demo.sh re-verified green at RISC0_DEV_MODE=0 from a fresh checkout of this revision (trace committed).
  • Our contribution is now dual-licensed MIT + Apache-2.0 (LICENSE-APACHE, NOTICE), as the terms require.
  • CI green on main: https://github.com/jeefxM/lp-0002-private-multisig/actions/runs/34274880779.

Program id 3fNuZskqxs6NZUY4YoenCagQspckdsnjLQi1PUmh2nKa; repo https://github.com/jeefxM/lp-0002-private-multisig.

@jeefxM

jeefxM commented Sep 8, 2026

Copy link
Copy Markdown
Author

GitHub would not reopen this PR (HTTP 422 on both the web and REST paths, even with the branch restored to the closed head), so the resubmission described above is open as #143 — same submission continued, this branch left as the reviewed record.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants