Solution: LP-0003 Private Allowlist / Airdrop Distributor - #126
Closed
edenbd1 wants to merge 25 commits into
Closed
Conversation
✅ Validation passedA reviewer will assess against the prize criteria. Automated check. See solution template and TERMS. |
The deployment moves to LEZ v0.2.4. A guest's ImageID depends on the pinned revision, so both programs are new binaries at new content-addressed deploy transactions, and the two distributions and all 23 claims are re-created on chain rather than carried over. All 23 are verified live and privacy-preserving over RPC, and all 23 marker accounts re-derived from the verifier ImageID and confirmed owned by it. The anchor check is tightened while the deployment was re-keying anyway: `claim` now requires the distribution account's owner to equal the verifier itself, rather than merely to be non-default. The published IDL is unchanged. Corrections, each found by re-checking a claim this document makes: - `lee/state_machine/src/program.rs` does not exist; the file is `program/mod.rs`, and the split predates the release this targets, so the citation never resolved. `execution_state.rs:149` sat four lines above the `env::verify` it names. - The explorer does index these transactions; it lags by about an hour and three quarters. It does not drop them. And what it shows for a claim — type, proof size, marker address, nothing naming a distribution or an address — is the privacy property rendered by a third party, so this now says that instead of explaining an absence. - The claim cost is 318,242 user cycles, re-measured against the current binary. Proving wall-clock is no longer quoted as a fixed number: it moves by half again with machine contention. - 30 tests across the workspace, not 29; 25 adversarial rejections in the executor suite, not 28. The demo video is re-recorded against this deployment, since the previous one showed the superseded ImageIDs on screen.
The verify snippet passes a claim tx, a nullifier and a distribution id. Only the first is a transaction; getTransaction returns null on the other two. Worth stating here rather than only in the repository docs, since a reviewer checking the hashes would otherwise hit two nulls and reasonably wonder.
…t and the CU figure Three corrections, each re-measured on 2026-08-15 before changing anything. The explorer no longer behaves as this submission described. Measured today with plain curl against https://explorer.testnet.lez.logos.co, it server-side renders: /transaction/441ccd15…e112c86 -> 200, 366,016 bytes, "Privacy-Preserving Transaction", "Proof Size: 260947 bytes" /transaction/7b16e471…092e34c -> 200, 536,056 bytes, "Deploy" /transaction/(de x 32) -> 200, 2,416 bytes, "Failed to load transaction: error running server function: Transaction not found" Stable across a second claim from the other distribution and across a plain curl user-agent; the page is Leptos SSR HTML with a WASM hydration bundle, not a client-rendered shell. So a single curl separates an indexed transaction from one that cannot exist. The old behaviour is recorded rather than deleted, since scripts/check-explorer.py drives a headless browser because of it, and that check is now a second opinion on the rendered DOM rather than a necessity. The Supportability checklist said "three distributions" where four other places in this file and the committed artifacts/e2e/claims.tsv say two. Counted: the manifest holds 23 rows over 2 distinct distribution_id values, 12 + 11. Fixed the outlier. The CU line read "318,242 user cycles / 524,288 proving cycles = 1.56% of the public budget", which reads as attaching 1.56% to the user cycles. Against the 33,554,432 public budget, 318,242 user cycles is 0.95% and the padded 524,288 proving cycles are 1.56%; docs/benchmarks/cu-budget.md charges the budget against the latter. Both figures now stated, with the charged one named.
This was referenced Aug 20, 2026
Closed
…load test cannot see Opening this module alongside another one found that Qt's resource system is process-global: two modules that both register qrc:/qml/Main.qml resolve to whichever registered first, so with a second module installed this tile drew that module's UI over its own data. Each loaded fine alone, and QPluginLoader::load() was happy either way, which is why it survived until someone had two — the state a reviewer assessing more than one of these is in. The resource prefix is now the module's own name, package-lgx.py --verify refuses a package that claims the generic root, and the linux-amd64 variant was re-verified against Basecamp's own bundled Qt 6.9.2 after the rebuild, with the negative control run in the same session. The .lgx therefore changed, so the SHA-256 this document pins is updated to match what is on main: 3715c7cb64e9706e89c31c668cac8d33b947c97a4be5b99842ca1bc6e58cf303.
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. Rewritten to absolute URLs into edenbd1/lp-0003-private-airdrop-distributor, which is what the merged LP-0005 does. ../TERMS.md is left alone: it is the one path that genuinely lives in this repository. Each URL was fetched before it was written, blob then tree; all eight distinct targets answer 200.
VIDEO_SCRIPT.md carried five absolute paths from the machine that wrote it, one of them reaching into a sibling repository, in a repository anyone can clone. Replaced with the variables the surrounding text already uses. The module was rebuilt in the same pass, so the .lgx changed and the digest this document pins follows it: 3d129c9ac58a2f8c20343fc84daae2083061e7f20e990d8bdcab8884d8b54581.
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 eligibility 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.
…hat ran on it Twenty links pointed at `main`. A reviewer reading this next month would have been reading whatever `main` had become, which is not the thing being reviewed; worse, a submission that can change after it is opened cannot honestly be said to have been checked. Every link now names the commit. Named alongside it: the release that carries the downloadable package and its SHA-256, and the two CI runs that went green on that exact commit — the host suite with the deployed-binary audit, and the full lifecycle against a standalone LEZ sequencer. A run id is checkable; "CI is green" is not.
… it points at The end-to-end job was described as taking 58 to 80 minutes, median about 76. Recomputed over the eighteen green runs in the workflow history it takes 27 to 126, median 73: the median held, the range was wrong by about a factor of two at both ends. That is a claim a reviewer checks by opening the Actions tab — and one of the two runs now cited here is itself 125 minutes, outside the range the same document gave. The reviewed commit has two green runs rather than one, because the push and the v1.0.0 tag each triggered the full lifecycle. Both are named. The same lifecycle proved from nothing twice, on different runners, with the same result, is worth more than one run described well.
…he same facts The prize repository's `validate-submission.sh` reports a section as missing when it is present. The section loop is `echo "$SOL_CONTENT" | grep -qF "$s"` under `set -o pipefail`: when grep matches early it exits, the pipe closes, echo dies of SIGPIPE, and the pipeline returns non-zero even though the section was found. The run log shows exactly one `echo: write error: Broken pipe` and exactly one section reported missing. Measured: it appears once the payload crosses the 64 KiB pipe buffer, or earlier when the match is early enough that grep exits before echo finishes writing — `## Repository` sits at byte 1,255 here, which is why this file trips it and the other two submissions do not. Nothing in the document was wrong, so nothing in it is dropped. The twenty-four repeated file URLs become eight reference-style links, which is 1,585 bytes of the same text; the end-to-end script was described at length twice and is now described once; and the note on the explorer is both shorter and more accurate — a public transaction's page is complete at ~4.5 kB and small only because it carries no proof, so a size comparison tells you which kind of transaction you fetched and not whether it exists. Reported upstream separately. This is a workaround, not a fix.
…de blocks 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. Three blocks were caught that way, and one of them was the CU-cost table: the measurement this submission makes a point of having taken was displayed as monospace text in a horizontally scrolling box rather than as a table. Re-indented to two spaces. No wording changed; this is what the document already said, shown as it was meant to be read.
…code block Same cause as the block above it: a new paragraph inside a `- [x]` item, indented six spaces where the item's content starts at two. It read as monospace in a scrolling box. Re-indented; no wording changed.
The document named six transaction hashes and linked none of them. A reviewer who goes to the block explorer — which is where they go — had nothing to click in the reviewed file; the links existed only in the pull request description, which is not the artifact under review. The two deploy transactions are now links, and one claim is named beside them because it is the one worth opening: it renders as a Privacy-Preserving Transaction with its proof size and nothing naming a distribution or a recipient. That is the submission's central property shown by a third party instead of asserted here. All three were re-fetched before being linked; they render at 402 kB, 536 kB and 366 kB respectively.
…n honest account of which commit it shows The 44-second film attached to the pull request is evidence a reviewer has to watch. Its narration is now committed as `recordings/lp-0003-claim-and-double-claim.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 "the marker already exists", the frame must show `AccountAlreadyInitialized`. Two things are now said plainly rather than left to be noticed. The film shows `7f0530e` on screen, and the reviewed commit is later, because a film's transcript can only be committed after the film exists. What separates them is five files: this transcript, the checker, an evidence document, and a stale duration figure corrected in a workflow comment and a checklist. No program, no script the demo runs, no artefact the chain sees. And the `v1.0.0` release was cut from that same `7f0530e`, not from the reviewed commit. It is still the right artefact and the document now says why: `git diff 7f0530e..fa2ad17 -- app/ artifacts/ crates/ idl/` is empty, so the package in that release is byte-identical to the one at the reviewed commit. The reviewed commit has its own end-to-end run, named in the header. Its parent keeps the two it earned.
edenbd1
force-pushed
the
lp-0003-submission
branch
from
August 24, 2026 19:02
99e9a3f to
89a8bfc
Compare
`validate` was failing on this file with `line 158: echo: write error: Broken pipe`, reporting `## Repository` and `## Approach` missing when both are there. The check is `echo "$SOL_CONTENT" | grep -qF "$section"` under `pipefail`: `grep -q` exits at its first match, `echo` takes a SIGPIPE, and the pipeline's status makes a section that is present look absent. Reproducing the loop in a container puts this file's failure rate at 56% of runs; at the size it is now, 0 of 4000. The detail did not go anywhere. Long-form evidence lives in the repository's docs, which is where a reviewer verifying a claim ends up regardless, and every path is named rather than linked through a reference table. Both the criteria titles and the on-chain identifiers are unchanged, checked byte for byte. One correction: the released `.lgx` checksum read `3d129c9c…`; it is `3d129c9a…`, as the release's own SHA256SUMS says.
The commit this pointed at carried two lines naming other prize entries, which the tree's own check-no-cross-submission.py now refuses. The reviewed commit also predates the gates that hold these documents to the code: every quoted path and link must resolve, every cited CI run must be on a commit this branch has, and every explorer link must resolve against the chain it names. All CI on the new commit is green, including the full lifecycle against a standalone LEZ sequencer at RISC0_DEV_MODE=0.
…sed them Each checkbox now carries the prize's own sentence, character for character, so an evaluator can tick this list against theirs without deciding whether two wordings mean the same thing. The evidence under each is unchanged. The paraphrases were dropping the qualifying clauses that are the thing being checked: "documents its full privacy model" lost the enumeration of what each party learns, the error-code criterion lost "for all invalid-proof and double-claim cases", and the video criterion lost everything after "the recording must show". Those clauses are where a pass/fail is decided.
`4001` was documented in docs/error-codes.md, declared in the guest, and watched by nothing. Every other code is reached by mutating a well-formed claim; E_BAD_WITNESS cannot be, because it fires on the witness decode before the statement, the anchoring or the marker are read. So the gap was invisible: the suite was green and the document was correct. The file said `4001`-`4006`, each mapped to the attack it stops and the test that proves it. Two things were wrong with that. `4005` is reserved, so there are five live codes and not six. And the doc had no test column at all, which is now added — the claim is checkable per row rather than asserted in prose. CI, chain-link and standalone-sequencer runs are all green on the new commit, the last at 78 minutes with RISC0_DEV_MODE=0.
`python3 scripts/package-lgx.py --verify` appeared twice with no package path. The flag takes one — `--verify LGX` — so a reviewer copying either line gets an argparse error, in a document whose whole stance is that everything in it can be run. Both now carry the path, and the corrected command exits 0. Found by replaying every script these pages cite as producing evidence, from a clean clone, and checking that each refuses when it should: the claim verifier rejects a foreign nullifier, a deploy hash in place of a claim, an impossible hash and an unknown distribution; the account verifier rejects a proposal that does not exist and an empty directory; the packaging check rejects a package whose contents do not hash to its manifest.
… closes the verifier hole The criterion asks for two distributions with evidence provided, and this page gave two ids and two truncated roots — no create_distribution hash, no PDA, no full root anywhere. The transactions were always on chain; nobody could find them without scanning the explorer block by block. Both are now named, with their PDAs and full roots, and the addresses are derivable rather than asserted: a distribution's PDA is SHA256(prefix || ImageID || SHA256(id || root)), so recomputing it from the id and the root reproduces the address, which is what makes the root checkable. Repinned to 718cf4d, where verify-onchain-claim.sh no longer prints VERIFIED for one claim's transaction paired with another claim's nullifier, where the code assertions no longer pass on prose when the number is gone, and where the double-claim rejection is documented. All three workflows are green on it, including 75 minutes of standalone-sequencer e2e at RISC0_DEV_MODE=0.
…ed by its own link The create_distribution column carried b2b611d4...1949e833 and fb3f7538...7a90900f6. Neither appears in the working tree, in any commit reachable from any ref, or in any log; the second ends in nine hex characters where every other truncation in this document uses eight, which is the tell that it was not produced by truncating anything. The distributions are real -- both PDAs answer on chain and derive from their published roots -- so the row was invented, not the thing. It is gone, and scripts/check-truncated-hashes.py now resolves every short form against the tree. The document said a claim renders with "nothing naming a distribution or a recipient". The explorer page it links names the distribution PDA this file publishes eleven lines earlier. It now says what is true: the page names the marker and the distribution, and no recipient. Which airdrop was claimed is public; who claimed it is not. Two roots were cited as "committed at artifacts/e2e/dist*/distribution.json"; .gitignore excludes that directory. They are printed here instead. The film was reshot at the reviewed commit with narration that says six checks, because the script runs six. The speed-up is 159x, measured on this take; the document said eight, which was never true of any take.
The recordings this document used to link were made before the deployment it describes: one carries superseded on-screen identifiers by its own admission, and the other predates the sixth on-chain check by twelve days, so it shows a five-check run of a script that now runs six. Both films that matter were reshot for this submission, both are narrated, and both play inline in the pull request with their own commit legible on screen. Pointing a reviewer at a recording that contradicts the one above costs more than the extra commentary is worth, so the links are removed rather than annotated. The criterion asks for a recorded demo showing terminal output and proof generation with RISC0_DEV_MODE=0; the inline films carry that on 120 and 49 frames respectively, measured by reading every frame rather than sampling.
Both documents said the compressed shot carried its speed factor burned into the frame. It does not: every frame of the film was read with OCR and there is no annotation anywhere in it, and the script that assembles this film draws no plate. The claim described what a reviewer would see, and a reviewer pressing play sees something else. Removed rather than reworded -- the terminal prints its own elapsed time for the proof, which is checkable against the film itself. The same paragraph gave three different numbers for one shot across two documents -- 776 seconds and a factor of 172 here, 710 and 159 in the pull request -- and claimed an on-screen clock ran 'throughout' a film it appears in for the last thirteen seconds. All of it was written from notes about a previous take. The end-to-end run was described as one commit earlier than the reviewed one; it is four.
The end-to-end lifecycle finished green on the reviewed commit itself -- run 33173676111, 78 minutes, six on-chain checks including the join between the derived marker and the transaction being read. The paragraph explaining which run sat on which commit, and how far the difference reached, is no longer needed: it existed to be honest about a gap that has closed.
Collaborator
|
Hi @edenbd1 ,
Looking forward to re-submission! |
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 distributor commits an eligibility set on chain. An eligible recipient claims
their allocation without revealing which entry is theirs — not to observers, and
not to the distributor.
The exact version under review
Everything below is pinned to one commit. Nothing in this submission points at a
branch, so nothing under review can move after it is opened.
1813a11.lgx)v1.0.0—lp-0003-airdrop.lgx, SHA-2563d129c9ac58a2f8c20343fc84daae2083061e7f20e990d8bdcab8884d8b54581, plusSHA256SUMS.python3 scripts/package-lgx.py --verify lp-0003-airdrop.lgxre-derives every manifest hash from the downloaded archive itself, and works from whatever directory you downloaded it into.RISC0_DEV_MODE=0, no skip path.solutions/LP-0003.md, the only file this PR addsOne claim, and the same claim refused — 57 seconds. One terminal session at
b4a68a2,RISC0_DEV_MODE=0, a real STARK proved on themachine and submitted on the privacy path. The session ran 30 minutes; one shot
is sped up and it is named: the wait between "prove and submit" and the
on-chain checks: the terminal itself prints how long that took, and the shot is
compressed rather than cut. Nothing else is
touched, and the refusal at the end runs at speed because it is the point. It
plays here:
lp-0003-claim-and-double-claim-sub.mp4
What is verified on chain
A claim travels on LEZ's privacy-preserving path, carrying a real STARK receipt
the sequencer checks against the node-pinned
PRIVACY_PRESERVING_CIRCUIT_ID.What lands is a single marker account. Its seed is
SHA256(prefix ‖ distribution_id ‖ nullifier)and its address is the PDA builtfrom that seed,
SHA256(pda_prefix ‖ ImageID ‖ seed), where the nullifier isderived from the recipient's secret. An observer who knows every candidate address still
cannot compute the nullifier, so no marker maps back to a recipient.
The evidence, on the current public testnet
59c2160b40c5d0f4cce01fd89e7755dbafd9c7d088a071d6f6fd3a10cbbea7c57b16e471b35ce8c718e066d80a8198f8831ebc7b6704583ddd28bb287092e34c441ccd15e7b5eac388a0849481e95db409f1b6f23a202b6ee1a3ce37ae112c86All three render on the block explorer today. The claim shows as
Privacy-Preserving Transactionwith its proof size. It names two publicaccounts — the claim's marker and the distribution PDA — and no recipient.
Which airdrop was claimed is public; who claimed it is not, and that is the
property a third party renders rather than one asserted here.
The two program hashes are
SHA256(borsh(bytecode))of the binaries committed atthe pinned commit, so they can be recomputed without trusting this table.
Two distributions and 23 privacy-preserving claims (12 + 11) are live, listed
with their nullifiers in
artifacts/e2e/claims.tsv.Any of them can be re-checked from a clean clone, in about ten seconds:
Six checks: the deployed bytecode is the bytecode in the repository; the
transaction is
PrivacyPreservingand notPublic; its receipt is aSuccinctSTARK and not a dev-mode fake; the marker PDA derives from the verifier's ImageID
and the claim's nullifier; that marker is among the accounts this very
transaction touched; and it is owned by the verifier. The fifth is the one that
joins the two halves — without it, nothing said the transaction being read and
the marker being derived described the same claim. Feed it a hash that is not a claim,
or a nullifier that does not belong to it, and it fails rather than passing
quietly.
The standalone-sequencer E2E, in CI
.github/workflows/e2e-local-sequencer.ymlbuilds
sequencer_service --features standalonefrom a pinnedlogos-execution-zone, starts it, and runs the whole lifecycle against thatsequencer with
RISC0_DEV_MODE=0— deploy, commit a distribution, submit a realprivacy-preserving claim whose receipt the sequencer verifies, then read the
marker PDA back off that chain and check its owner.
There is no
if:guard, nopathsfilter and no skip path in the job: iteither runs the whole lifecycle or the workflow is red. It triggers on every
commit to
main, on everyv*tag, daily, and on demand. Its green runsspan 26 to 126 minutes — the spread is the runner, not the work, which is why no
single figure is quoted — and the run on the reviewed commit is linked above.
Beyond the unit tests
docs/onchain-audit.mdrecords three adversarial demonstrations against the live deployed programs
rather than a local harness: a claim that does not genuinely prove membership,
one that redirects its allocation, and one that replays a spent nullifier. All
three fail at proof generation, so none reaches a block.
The third is reproducible from a clean clone with no committed secret:
scripts/prove-one-claim.shcreates its own distribution, proves and lands onereal claim, then resubmits that exact claim under a different signer and
requires
AccountAlreadyInitialized. The run fails if a second transaction isproduced. The different signer is the point — the guard is bound to the
recipient's nullifier, not to whoever submits.
Against the criteria
31 asserting tests, CI green. Compute cost is measured for both on-chain
instructions by replaying the sequencer's own execution of the deployed binary:
create_distributionat 108,596 user cycles / 262,144 proving cycles (0.78 % ofthe 33,554,432 public budget) and
claimat 318,242 / 524,288 (1.56 %); theproving-cycle figure is the one the budget is charged against. The Basecamp
module ships as a
.lgxwithdarwin-arm64andlinux-amd64variants,downloadable from the release above;
python3 scripts/package-lgx.py --verify app/lp-0003-airdrop.lgxrecomputes everymanifest hash from the archive's own contents, and the video shows that check
passing.
The video is one session at
b4a68a2: it opens ongit log -1— a cleangit status --shortprints nothing, which is what a clean tree looks like — showsRISC0_DEV_MODE=0before any proving starts, proves and submits a fresh claimduring the recording, passes the checks against it, and then resubmits the same
claim and is refused. One session, five shots: the stretch where the prover works
and the screen does not change is compressed, and the gaps between shots are
waits where nothing happens on screen. The terminal prints its own elapsed time,
so the compression is checkable against what the film shows rather than against a
figure asserted here.
Two notes on the deployment
LEZ v0.2.4. Both programs are built against commit
47eba25. A guest'sImageID depends on the pinned revision, so these are new binaries at new
content-addressed deploy transactions, and every account derived from the
verifier's program id moves with them. The two distributions and all 23 claims
were re-created on chain rather than carried over.
The explorer indexes on its own schedule, about an hour and three quarters
behind the sequencer — measured, not assumed. A hash submitted minutes ago is not
there yet while
getTransactionalready has it, which is why verification readsthe chain over RPC. Everything linked in this description is long since indexed
and renders today.
Happy to address anything in review.