Skip to content

Solution: LP-0003 Private Allowlist / Airdrop Distributor - #126

Open
edenbd1 wants to merge 4 commits into
logos-co:masterfrom
edenbd1:lp-0003-submission
Open

Solution: LP-0003 Private Allowlist / Airdrop Distributor#126
edenbd1 wants to merge 4 commits into
logos-co:masterfrom
edenbd1:lp-0003-submission

Conversation

@edenbd1

@edenbd1 edenbd1 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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.

Updated: redeployed on LEZ v0.2.4 (commit 47eba25). A guest's ImageID
depends on the pinned revision, so both programs 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, and the video was
re-recorded against this deployment. The hashes below are the current ones.

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 derived
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.

Evidence, on the current public testnet

Hash
claim program (deploy) 59c2160b40c5d0f4cce01fd89e7755dbafd9c7d088a071d6f6fd3a10cbbea7c5
claim verifier (deploy) 7b16e471b35ce8c718e066d80a8198f8831ebc7b6704583ddd28bb287092e34c
one claim (privacy tx) 441ccd15e7b5eac388a0849481e95db409f1b6f23a202b6ee1a3ce37ae112c86
curl -s -X POST https://testnet.lez.logos.co -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"getTransaction","params":["<hash>"]}'

Each hash links to its page on the block explorer. Both deploys and all 23 claims render there; a claim shows as Privacy-Preserving Transaction with its proof size, and nothing naming a distribution or a recipient.

The two program hashes are SHA256(borsh(bytecode)) of the binaries committed in
the 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.sh ends on
after five checks.

Beyond the unit tests

docs/onchain-audit.md records three adversarial demonstrations run 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. scripts/adversarial-onchain.sh re-runs them against whatever is
deployed 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 in
docs/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 getTransaction already 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 when scripts/check-explorer.py was written and is
not true now: re-measured on 2026-08-15, the explorer server-side renders, so
one curl separates an indexed transaction from one that cannot exist. Ten
seconds, no browser:

curl -s https://explorer.testnet.lez.logos.co/transaction/441ccd15e7b5eac388a0849481e95db409f1b6f23a202b6ee1a3ce37ae112c86 | wc -c
# 366016 — contains "Privacy-Preserving Transaction" and "Proof Size: 260947 bytes"

curl -s "https://explorer.testnet.lez.logos.co/transaction/$(python3 -c 'print("de"*32)')" | wc -c
# 2416 — "Failed to load transaction: error running server function: Transaction not found"

scripts/check-explorer.py still renders each page headless against the same
impossible-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 per
instruction 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 .lgx with
darwin-arm64 and linux-amd64 variants;
scripts/package-lgx.py --verify app/lp-0003-airdrop.lgx recomputes every
manifest 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=0 visible before any proving starts, and the five checks passing
against 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.

@github-actions

Copy link
Copy Markdown

✅ Validation passed

A reviewer will assess against the prize criteria.
ℹ️ Solution submission for LP-0003.
ℹ️ Checking repo: https://github.com/edenbd1/lp-0003-private-airdrop-distributor


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.
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.

1 participant