Solution: LP-0003 Private Allowlist / Airdrop Distributor - #126
Open
edenbd1 wants to merge 4 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 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.
solutions/LP-0003.mdWhat 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
59c2160b40c5d0f4cce01fd89e7755dbafd9c7d088a071d6f6fd3a10cbbea7c57b16e471b35ce8c718e066d80a8198f8831ebc7b6704583ddd28bb287092e34c441ccd15e7b5eac388a0849481e95db409f1b6f23a202b6ee1a3ce37ae112c86Each hash links to its page on the block explorer. Both deploys and all 23 claims render there; a claim shows as
Privacy-Preserving Transactionwith its proof size, and nothing naming a distribution or a recipient.The two program hashes are
SHA256(borsh(bytecode))of the binaries committed inthe repository, 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.Each of the 23 marker accounts is derived from the verifier's ImageID and the
claim's nullifier, and each is owned by the verifier — which is the part no
transaction lookup can fake, and what
./scripts/verify-onchain-claim.shends onafter five checks.
Beyond the unit tests
docs/onchain-audit.mdrecords three adversarial demonstrations run against thelive 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.
scripts/adversarial-onchain.shre-runs them against whatever isdeployed and fails the run if any produces a transaction instead of a rejection.
It counts what it actually ran: the double-claim needs a claim whose marker is
already on chain, and that input carries a recipient secret so it cannot be
committed — a run without one reports 2 of 3 and exits non-zero rather than
claiming three. That case is covered on every push by
cargo test -p claim-verifier-tests, and by the live transcript indocs/onchain-audit.md§3.On the explorer
The explorer is a separate index and reaches a transaction about an hour and
three quarters after the sequencer does — measured, not assumed, so a hash
submitted recently is not there yet while
getTransactionalready has it.Correction. An earlier version of this description said the explorer was a
WASM app that returned an identical shell for every URL and could not be probed
with
curl. That was true whenscripts/check-explorer.pywas written and isnot true now: re-measured on 2026-08-15, the explorer server-side renders, so
one
curlseparates an indexed transaction from one that cannot exist. Tenseconds, no browser:
scripts/check-explorer.pystill renders each page headless against the sameimpossible-hash control. It is now belt and braces on the rendered DOM rather
than the only way to get an answer.
Once indexed, a claim renders its transaction type, its proof size and the marker
address — and nothing naming a distribution or a recipient. That is the privacy
property, rendered by a third party.
Against the criteria
30 tests, CI green, including a workflow that runs the whole lifecycle against a
real standalone LEZ sequencer with
RISC0_DEV_MODE=0. CU cost is measured perinstruction by replaying the sequencer's own execution of the deployed binary:
318,242 user cycles for a claim, padded to 524,288 proving cycles — 0.95% and
1.56% respectively of the 33,554,432 public budget, and 1.56% is the figure the
budget is charged against. The Basecamp module ships as a
.lgxwithdarwin-arm64 and linux-amd64 variants;
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 shows a fresh claim proved and submitted during the recording, with
RISC0_DEV_MODE=0visible before any proving starts, and the five checks passingagainst it afterwards. It is a single take; the stretch where the proof runs and
nothing on screen changes is played at eight times speed, and nothing is cut.
Happy to address anything in review.