Solution: LP-0003 Private Allowlist / Airdrop Distributor - #126
Open
edenbd1 wants to merge 13 commits into
Open
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.
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.
7f0530e.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.solutions/LP-0003.md, the only file this PR addsOne claim, and the same claim refused — 44 seconds. One terminal session at
the commit above,
RISC0_DEV_MODE=0, a real STARK proved on the machine andsubmitted on the privacy path. The session ran 16 min 55 s; one shot is sped
up and it is named: the wait between "prove and submit" and the five on-chain
checks, 12 min 56 s of proving compressed to 4.5 seconds. 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 whose address is
SHA256(prefix ‖ distribution_id ‖ nullifier), where the nullifier is derivedfrom the recipient's secret. An observer who knows every candidate address still
cannot compute the nullifier, so no marker maps back to a recipient.
Evidence, on the current public testnet
59c2160b40c5d0f4cce01fd89e7755dbafd9c7d088a071d6f6fd3a10cbbea7c57b16e471b35ce8c718e066d80a8198f8831ebc7b6704583ddd28bb287092e34c441ccd15e7b5eac388a0849481e95db409f1b6f23a202b6ee1a3ce37ae112c86All three render on the block explorer today. The claim shows as
Privacy-Preserving Transactionwith its proof size, and nothing naming adistribution or a recipient — the privacy property, rendered by a third party.
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:
Five 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; and that marker is owned by the verifier. The last one
is the part no transaction lookup can fake. 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. A run takes 58–80minutes (median ~76), and the run for 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
30 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 a single take on the pinned commit: it opens on
git log -1and aclean
git status, showsRISC0_DEV_MODE=0before any proving starts, proves andsubmits a fresh claim during the recording, passes the five checks against it, and
then resubmits the same claim and is refused. The stretch where the proof runs and
nothing on screen changes is played at eight times speed; nothing is cut.
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.