Solution: LP-0002 Private M-of-N Multisig - #125
Open
edenbd1 wants to merge 14 commits into
Open
Conversation
❌ Validation failedFix before review:
ℹ️ Solution submission for LP-0002. Automated check. See solution template and TERMS. |
The video is recorded and linked, so the two places that still said it was outstanding are gone. The security write-up is presented by what the design defends against rather than by the order in which it was built; every property and every test it refers to is unchanged.
`lee/state_machine/src/program.rs` does not exist: the file is `program/mod.rs`, and the split predates the LEZ release this targets, so the path never resolved. `execution_state.rs:149` sat four lines above the `env::verify` it names; the call is at 153, and 149-155 brackets it with its comment. The executor suite is 25 adversarial rejections plus five honest controls, not 28 rejections.
The file said none of the seven transactions rendered on the block
explorer; that was true when written and is no longer. Re-measured on
2026-08-15 by rendering each /transaction/<hash> page in a headless
browser, with an impossible hash as the control:
- all seven render — the two approvals as Privacy-Preserving
Transaction, the two deploys as Program Deployment Transaction, the
other three as Public Transaction;
- the control still renders Transaction not found, so the positives
are distinguishable from the shell the WASM app serves for every URL;
- getTransaction returns a result for all seven on the RPC.
The explorer's front page was at block 8633 and the sequencer at 8676 on
the same pass — a 43-block lag, against 4351 vs 4496 when this was first
written. The indexing delay is kept in the file as history rather than as
a current claim, because it still applies to anything submitted recently.
… count Three numbers in this file were checked against docs/cu-costs.md in the linked repo on 2026-08-15 and two of them were wrong in the submission's favour, which is the worst direction to be wrong in. Proving time. The file led with 149 s and 154 s. cu-costs.md records those as the LEZ v0.2.0 local-sequencer measurement; the v0.2.4 run that actually produced the deployed lifecycle measured 440 s and 469 s on 2026-08-12. v0.2.4 is roughly three times more expensive than v0.2.0 and a second machine shows the same ratio (CI, 1264 s to 4033 s), so the old figure understated the current chain by about 3x. Now quoted at 440/469 with the version, the date and the contention it was measured under, and with the idle-laptop 437 s and the 935 s contended figure alongside so the number cannot be read as a best case. Cycle count. The file said approve costs 335,564 user cycles; cu-costs.md says 337,105. Re-ran the repo's own measurement, cargo test -p multisig-verifier-tests -- --ignored --nocapture, which reports 337,105 for approve (and 267,055 / 363,510 / 461,604 for execute at M=1/3/5, matching the doc). Corrected in both places. lifecycle.tsv. The file said the timings were written into it as though it were a committed artefact. It is not committed: deploy-and-run.sh writes it into WORK, which defaults to .testnet/, and .gitignore excludes /.testnet*/ deliberately because that directory also holds member keys. The claim now says what is true — the script writes it during the run, it is not committed, and the reviewable record is the table in cu-costs.md. Also dropped the stale note that the old estimate had been four times too pessimistic. That was true of v0.2.0 and is not true of v0.2.4, so it would have been a fourth wrong number.
This was referenced Aug 20, 2026
Closed
This document lives in lambda-prize/solutions/, so a link written as docs/DEPLOYMENT.md resolves there and 404s. Eight of its nine repository links did, including every "here is how to check this" pointer — the worst place to lose a reader. Rewritten to absolute URLs into edenbd1/lp-0002-private-multisig, which is what the merged LP-0005 does (23 absolute links, one relative: ../TERMS.md, the only path that genuinely lives in this repository and the only one left alone here). Each URL was fetched before it was written, blob then tree, and every one of the nine distinct targets answers 200.
…ckable The criterion named three PDAs, said they were owned by the verifier, and told the reader to re-verify with ./scripts/verify-onchain.sh. Read as a first-time reviewer with nothing but a clone, both halves needed work. The explorer has an account view and renders all three. Each shows Program Owner 7AyJ7x4DuAa58ALGqLXYwqdhEvQLCz5A2GFdcDrwyzUZ, which base58-decodes to 5bb40082…a5966f82 — exactly what "spel program-id" prints for the verifier binary committed in the repository. That is the entire claim, checkable in a browser, and this document offered no link to it. It does now. And verify-onchain.sh needs the working directory a lifecycle run leaves behind. From a clean clone it says so and exits 1 — correct behaviour, wrong instruction: the criterion pointed a reviewer at the one command they cannot run. A clone plus ./scripts/demo.sh does the same check from public data, and that is what the criterion names now.
Open
6 tasks
The paragraph on transaction paths ended by gesturing at other people's outcomes, which is not evidence and is not this submission's to cite. The argument stands on its own and is stronger for it: on the public path the sequencer re-executes the program host-side, so the approval logic runs in the clear and there is neither a proof nor any privacy. That is a fact about the runtime, checkable at the file and lines already quoted.
…therwise Three things in here described a deployment that no longer exists. The reference-integration section argued that performing the transfer was deliberately out of scope: the multisig proved a threshold, wrote an execution marker, and left the transfer to a caller. That argument was honest and the primitive is still general, but the criterion names a treasury transfer as its example, and a treasury transfer is a better answer than an essay about one. The multisig now owns a treasury, and `execute` moves value out of it into a recipient held by the native transfer program — so what the gate releases is spendable rather than stranded. The balance falling from 2 to 1 is a fact a reviewer reads off the chain in one call. The account addresses were the previous verifier's. A program's identity on LEZ is its ImageID and every PDA derives from it, so redeploying moved all six. The six current ones are named, with the program owner that base58-decodes to the ImageID `spel program-id` computes from the binary committed here. And the explorer paragraph still claimed the site was a WASM shell that made a size comparison meaningless. That was true when the check script was written and is why it drives a browser; it stopped being true on 2026-08-15. A document that keeps a superseded measurement without dating it is asking to be trusted on the wrong one. Test count corrected from 61 to the 97 the workspace now runs.
…ich video shows what Twenty-three links pointed at `main`, so a reviewer reading this later would have been reading whatever `main` had become rather than the version under review. Every one now names the commit, and every path behind them was checked to exist there. The header also names the CI run that went green on that commit — "CI is green" is a claim about a branch at a moment, a run id is a thing anyone can open — and it separates the two videos. The ten-minute walkthrough was recorded before the 2026-08-24 redeploy, so every identifier it shows on screen is superseded; a short reading of the current deployment is attached to the pull request. Leaving the older one unlabelled invited a reviewer to check a hash from the screen against a chain that no longer has it.
Inside a `- [x] …` item the content column is 2, so a new paragraph indented six spaces sits four beyond it and GitHub renders it as an indented code block. Ten blocks were caught that way — the whole explanation of what the threshold gates, the treasury's balance, the explorer measurement, the Basecamp load test — each displayed as monospace text in a horizontally scrolling box instead of as prose. Re-indented to two spaces. Not a word changed; this is what the document already said, shown as it was meant to be read.
…iewed commit The header named only the host suite. The end-to-end job, which builds LEZ at a pinned revision, starts a standalone sequencer and drives the whole 2-of-3 lifecycle against it with RISC0_DEV_MODE at zero, finished green on the same commit in two hours and forty-eight minutes. A run id is checkable; a claim that the lifecycle works is not.
It was described as a fifty-second silent reading because that is what it was when the line was written. It now carries a voice track, and it runs forty-seven seconds. A duration in a submission is a small claim, but it is a checkable one.
…he runs that go with the commit The 47-second film attached to the pull request is evidence a reviewer has to watch. Its narration is now committed as `recordings/lp-0002-threshold-moves-value.srt`, so it can be read instead, and `scripts/check-transcript.py` ties that text to this film rather than asserting it belongs: cues ordered and landing inside the film, and four anchors requiring the frame to show what the narration is talking about at the second it says it — where the line is "both approvals carry the privacy-preserving variant", the frame must show the variant column. The film shows `ad2267e` on screen and the reviewed commit is later, because a film's transcript can only be committed after the film exists. Everything between the two is documentation: the transcript, the checker, an evidence document, and one correction to the explorer section of `docs/DEPLOYMENT.md`, which had illustrated a `curl` check with a hash from the superseded deployment and a size that holds only for transactions carrying a proof. `docs/recording-evidence.md` says so, and `git diff` confirms it. Every link is re-pinned to the commit that carries all of it, and the two runs named are the ones that went green on that commit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A threshold multisig for LEZ where an approval is unlinkable to the member who
gave it — including to the other members — and the chain records only that the
threshold was met.
ad2267e— every link in the solution file names that commit rather thanmainRISC0_DEV_MODE=0ten-minute narrated walkthrough at https://youtu.be/cPVsWWBVEeE predates
the 2026-08-24 redeploy, so the identifiers on screen in it are the
superseded ones; the tables here are current.
solutions/LP-0002.mdForty-seven seconds, two scripts, no arguments. One terminal session at the commit
above running
verify-onchain-lifecycle.shand thenverify-onchain.sh: theeight transactions and the variant each carries, then the six accounts decoded
field by field, down to the recipient's balance. Nothing is sped up — the two
scripts answer in about eleven seconds between them. Narrated, with the captions
burned in.
lp-0002-threshold-moves-value-sub.mp4
What is verified on chain
Each
approvedeclares aChainedCallto a LEZ-native membership program, sothe privacy circuit composes it with a real
env::verifyand the sequencerchecks the receipt against the node-pinned
PRIVACY_PRESERVING_CIRCUIT_ID. Themember set and the threshold are folded into the PDA address, so neither can be
invented nor lowered — a forged configuration resolves to an address nobody ever
created.
Transactions — a 2-of-3 lifecycle on the current public testnet
membership_lezfb8eb10f7f394286c109cb6502a1c95294180523f30d06f707fc087a589bea98multisig_verifier2d6f720e3c6dd8d876c8617eada5ddcd3c13a978b2edcb1921a3de73231e82e2create_multisiga8d8422ae2c46566b15c31954647974d3e95eadbe0b560eac4ab609c9a25ab55fund_treasury2844eef12695ab0d3c6d55832e94ae316638dd7400735d2f393875a30bb6a5c2create_proposalb194da9ba24e1a17a7bec0d64da0d252a96c6edc96208a778a7e77e71fed9826approve(member A, privacy tx)d13813094f36c1b60c02350adbc272ce5aa88dd7d87ab409a3e36436e70a91c0approve(member B, privacy tx)9f7c541c187c6ed284f67b0f5c6f0942de0ed98ff7e589dc81955ceed7219719execute00ea68384758097dba8b648605b4ecf65d9535ba6b497af335d4fcf2be7f75aeCheck any of them:
The two deployment hashes are
SHA256(borsh(bytecode))of the binaries committedunder
artifacts/programs/, so they can be recomputed from the repositorywithout trusting this table.
The accounts, which are the stronger evidence
All six are owned by the verifier program.
./scripts/verify-onchain.shreadsthem back and decodes every record against the offsets published in
docs/account-layout.md.Hx7Ni2riURJfgng4QAXb3RBq9ZMjrvwj7JREjut9PuBCxtngupTp3tcQU9faCbND73KhCpYqfBqKhmoepQAXoVx9EjLSnKjXB4r8SgBEHcgqf36nkqfyUVXuqSsDzCTRvg7Awd6RNVpMdPkvALi6ubdZyffKb3Bw3HimbUKtUP7YV6F6dmnyiGZu6KtVy6jxyEMvb4HG8N59AuKey7wB9RjbCTVBP6buTDdFThPYWU3NFGSsD8k3ymewStQo4hj86tAwq1aA seventh account is not the program's, and it is the point of the exercise: the
recipient,
8kexXda8j5hPegPeHXzUM9PhvjYNFLpN8wN8PvG5iDhn, went 0 → 1. Itis held by the native transfer program rather than by the verifier, which is what
makes what it received spendable instead of stranded.
The treasury's balance is the one number nothing except a real execution could
produce:
fund_treasuryput 2 in andexecutemoved 1 out, and the two sides ofthat move are visible in two accounts owned by two different programs. Each
approval marker exists only because a membership proof was verified on chain, and
nothing in the pair names a member — read them and you learn the threshold was
met, and nothing about who met it.
On the block explorer, measured rather than asserted
All eight render. Checked at 03:52 UTC on 2026-08-24, once the explorer's
index had passed block 20880.
The index trails the sequencer — on this chain, roughly an hour and three
quarters — so a transaction submitted recently answers "Transaction not found"
there while
getTransactionalready resolves it. That is an indexing delay, nota gap, and it affects anything recent by anyone. These eight were written in
blocks 20856-20880 and were behind the index for about three hours.
And a
curlsize comparison under-reports public transactions, which isworth stating because this description used to recommend one. Three sizes,
measured on these very hashes:
curlType: Public Transaction,Program ID, instruction-data size, signature count, and every account with its nonceA public transaction's page is small because a public transaction carries no
proof, not because anything is missing from it. So the size tells you which kind
of transaction you fetched, and it does not tell you whether it is there —
Type:does.scripts/check-explorer.pyclassifies on that marker rather thanon a length, with an impossible hash as the control, and aborts if the control
ever classifies as found.
Correction. An earlier version of this description said the explorer was a
WASM application returning an identical shell for every
/transaction/<hash>URL, so an indexed transaction and one that cannot exist were byte-identical over
curl. That was true whencheck-explorer.pywas written — it is why the scriptdrives a browser at all. It stopped being true on 2026-08-15, when the
explorer began rendering server-side; and as the table above shows, it stopped
being true only partly, which is the sort of half-change that turns a shortcut
into a wrong answer.
Note on versions
The testnet was reset onto a newer chain in August, so everything here was
rebuilt against LEZ v0.2.4 and redeployed. That is also why the repository
vendors SPEL: no published SPEL release builds against a current LEZ, and
vendor/spel/PATCH.mddocuments every change, how to reproduce the directory,and when it should be deleted.
Against the criteria
97 tests, CI green on Linux and macOS, including a workflow that runs the
whole lifecycle against a real standalone LEZ sequencer with
RISC0_DEV_MODE=0.Fifty-nine of the 97 run the built verifier binary through the sequencer's own
executor rather than the host crate: 30 rejections and honest controls, 22 on the
state it writes and the value it moves, 5 on the IDL and the docs, 2 pinning the
committed membership binary. CU costs are measured rather than estimated, per
instruction and per machine. The Basecamp module ships as a
.lgxwithdarwin-arm64 and linux-amd64 variants and was verified loading in Basecamp 0.2.2,
not merely built.
Happy to address anything in review.