diff --git a/solutions/LP-0003.md b/solutions/LP-0003.md new file mode 100644 index 0000000..226c583 --- /dev/null +++ b/solutions/LP-0003.md @@ -0,0 +1,147 @@ +# Solution: LP-0003 — DistributionX + +**Submitted by:** Timidan + +## Summary + +DistributionX is a private allowlist airdrop for the Logos Execution Zone (LEZ). A distributor commits an encrypted eligibility list on-chain through a Merkle root, funds a vault, and lets eligible recipients claim with a real Risc0 proof using `RISC0_DEV_MODE=0`. + +The privacy claim targeted by the bounty is that on-chain observers should not learn the eligible address, row salt, claim signature, or Merkle path from a valid claim transcript. The active path is the witness-private `claim_ppe` instruction, submitted through LEZ privacy-preserving execution (PPE) via `send_privacy_preserving_tx`. The witness verification (Ed25519 signature, Merkle membership, nullifier derivation) runs inside the PPE circuit; the heavy proof is composed client-side and the sequencer verifies a single succinct receipt. The PPE transaction message carries no instruction data and no witness fields — only public account states, the encrypted private recipient post-state, the new commitment, and the nullifier — so the witness is structurally absent from the on-chain transcript. The credit lands on a LEZ-native private recipient account (the destination commitment), and one-claim-per-recipient is enforced by the nullifier set plus the program's `NullifierRecord` PDA. + +**This is now demonstrated on the live LEZ testnet (`https://testnet.lez.logos.co`, LEZ v0.2.0-rc5): 2 distributions and 20 witness-private `claim_ppe` claims, all confirmed on-chain.** See [docs/TESTNET_EVIDENCE.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/TESTNET_EVIDENCE.md). The receipt-based `claim` and the in-program `claim_private` instructions remain in the program as alternate/fallback verifiers; `claim_private` carries the witness in instruction data and is opt-in only (`DISTRIBUTIONX_USE_CLAIM_PRIVATE=1`). + +## Repository + +- Repository: [https://github.com/Timidan/dist-x](https://github.com/Timidan/dist-x) +- Pinned commit: [`822c508`](https://github.com/Timidan/dist-x/commit/822c508940eeb08ad67cbaf4f9553665087a66f2) +- Demo video: https://github.com/logos-co/lambda-prize/pull/44#issue-4408269105 (`RISC0_DEV_MODE=0` end-to-end narration with terminal output) + +## LEZ Testnet Deployment (rc5) + +The program is deployed to the live LEZ testnet and the full claim flow is recorded on-chain. Every transaction below is verifiable with `getTransaction` against `https://testnet.lez.logos.co`. + +| Item | Value | +|---|---| +| Testnet RPC | `https://testnet.lez.logos.co` (LEZ v0.2.0-rc5) | +| Program id | `218a07eb268df922ded961fefd7d035752b44d05f4bb5172305fb0bc54506989` | +| Deploy tx | `b4e31be3c5f9e784295869904e217b52da6bfbe81f2146dd756f9827263537bc` | +| Distributions | 2 (`lp0003-rc5-b1`, `lp0003-rc5-c1`) | +| Witness-private `claim_ppe` claims | 20 (10 per distribution), all confirmed on-chain | +| Token settlements | 20 | +| Per-claim public-execution CU | 504401 (well under the 32M public-execution cap) | + +Full transaction list, per-tx `getTransaction` verification counts, and per-claim receipts: [docs/TESTNET_EVIDENCE.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/TESTNET_EVIDENCE.md). CU details: [docs/bench/REPORT.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/bench/REPORT.md). + +`claim_ppe` is submitted via `send_privacy_preserving_tx`, so the witness never appears in the on-chain transaction — privacy is demonstrated on the testnet itself, not only on the standalone sequencer. + +## Approach + +DistributionX separates the airdrop into three parts: eligibility commitment, private claim proof, and double-claim prevention. + +The distributor CSV is converted into encrypted bundle rows. Each row is encrypted to the intended recipient's claim key, and the chain stores only the Merkle root and bucket table metadata. This avoids publishing the full allowlist while still giving claimants a package they can scan locally. + +The claimant proves that they can decrypt one valid row, sign for the eligible key, match the committed Merkle root, derive the correct nullifier, and bind the claim to a shielded destination commitment. Risc0 is used because the prize calls for a LEZ-compatible zero-knowledge proof path, and the demo uses the real proof mode with `RISC0_DEV_MODE=0`. The reviewer demo submits the `claim` instruction with the receipt; on-chain verification checks the Groth16 receipt and the journal against airdrop state, debits the vault, and credits the nullifier PDA. A separate token-settlement transaction transfers from the nullifier PDA to the shielded destination once the claim is included. + +Double claims are prevented with nullifiers. A successful claim records the nullifier so the same eligibility row cannot claim again, while observers still cannot link the nullifier back to the eligible address. + +Rejected alternatives: + +- Public Merkle airdrop: simpler, but reveals the eligible address at claim time. +- Publishing the allowlist: easy to audit, but defeats the privacy goal. +- Dev-mode or mock proofs: fast, but not valid for the bounty requirement. +- A custom non-Risc0 proof system: possible, but less aligned with the Logos/LEZ stack. + +LEZ is a good fit because the protocol needs trustless execution, local proof generation, shielded destination handling, and private claim submission. A centralized airdrop service would learn the eligibility list and claim mapping directly. + +## Success Criteria Checklist + +- [x] Distributor commits an eligibility set without revealing the full allowlist. + Evidence: [README.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/README.md), [docs/WRITEUP.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/WRITEUP.md), encrypted bundle generation, Merkle root initialization. + +- [x] Eligible recipients can claim without revealing the eligible address in the public transcript. + Demonstrated on the live testnet via the `claim_ppe` instruction, submitted through LEZ privacy-preserving execution (`send_privacy_preserving_tx`). The PPE transaction message carries no instruction data and no witness fields, so the witness (address, salt, signature, Merkle path) is structurally absent from the on-chain transcript; the witness exists only as a local input to the PPE proof. The credit lands on a LEZ-native private recipient account. Evidence: 20 on-chain `claim_ppe` claims in [docs/TESTNET_EVIDENCE.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/TESTNET_EVIDENCE.md). The `claim_private` instruction (witness in instruction data) remains an opt-in fallback gated behind `DISTRIBUTIONX_USE_CLAIM_PRIVATE=1`. + +- [x] LEZ testnet deployment with >=2 distributions and >=20 claims. + Program deployed at `218a07eb...` on `https://testnet.lez.logos.co`; 2 distributions, 20 witness-private `claim_ppe` claims, 20 settlements, all confirmed via `getTransaction`. See [docs/TESTNET_EVIDENCE.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/TESTNET_EVIDENCE.md). + +- [x] Each recipient can claim only once. + On-chain enforcement is per nullifier: the `NullifierRecord` PDA at seed `["nullifier", airdrop_id, nullifier]` rejects a second initialization with `E_ALREADY_CLAIMED`. Address-level uniqueness is enforced at CSV ingest by the parser in [crates/distributionx-tree/src/csv.rs](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/crates/distributionx-tree/src/csv.rs#L25-L33), which rejects duplicate addresses with `CliDuplicateAddr` before the tree is built. See [docs/WRITEUP.md Claim Uniqueness Scope](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/WRITEUP.md#claim-uniqueness-scope). + +- [x] Real Risc0 proof path with `RISC0_DEV_MODE=0`. + Evidence: [scripts/e2e.sh](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/scripts/e2e.sh) `private-localnet`, `distributionx-cli prove`, `PROVE_LOCAL_OK`, `VERIFY_OK`. + +- [x] LEZ local sequencer integration. + Evidence: [scripts/standalone-sequencer.sh](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/scripts/standalone-sequencer.sh), [scripts/deploy.sh](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/scripts/deploy.sh) `--localnet`, [scripts/local-submit.sh](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/scripts/local-submit.sh). + +- [x] Basecamp GUI. + Evidence: [basecamp-app/](https://github.com/Timidan/dist-x/tree/822c508940eeb08ad67cbaf4f9553665087a66f2/basecamp-app), [scripts/start-basecamp.sh](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/scripts/start-basecamp.sh), LGX artifacts from [scripts/package.sh](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/scripts/package.sh). + +- [x] Logos module / SDK. + Evidence: [distributionx_client_module/](https://github.com/Timidan/dist-x/tree/822c508940eeb08ad67cbaf4f9553665087a66f2/distributionx_client_module). + +- [x] SPEL IDL. + Evidence: [crates/distributionx-program/idl/distributionx.json](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/crates/distributionx-program/idl/distributionx.json). + +- [x] CU and benchmark report. + Evidence: [docs/bench/REPORT.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/bench/REPORT.md). + +- [x] GitHub Actions CI status. + The `scripts`, `rust`, `logos`, and `localnet-e2e` jobs run on every push and PR. `scripts`, `rust`, and `logos` pass on the latest commit. `localnet-e2e` runs `scripts/e2e.sh ci-localnet` and exits skipped on push or PR when `DISTRIBUTIONX_LEZ_SEQUENCER_START_COMMAND` is not configured, so it never reports a hard failure on the default branch when the sequencer infra is absent. The live testnet run is recorded as on-chain evidence ([docs/TESTNET_EVIDENCE.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/TESTNET_EVIDENCE.md)) rather than a CI job, since it needs real-proof generation and a funded testnet signer. + +## Privacy Model And Threat Model + +The full threat model lives in [docs/WRITEUP.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/WRITEUP.md). Three points the reviewer asked for explicitly: + +1. **Witness privacy is provided by the `claim_ppe` path through LEZ privacy-preserving execution (PPE).** Earlier write-ups scoped privacy to a receipt-based `claim`; on LEZ v0.2.0-rc5 that path cannot land (verifying a Groth16 receipt in public execution is ~218M cycles, over the 32M public-execution cap). The shipping path is `claim_ppe`, submitted via `send_privacy_preserving_tx`: the witness verification runs in the PPE circuit, the heavy proof is composed client-side, and the sequencer verifies one succinct receipt. The PPE message format (`lee/state_machine/src/privacy_preserving_transaction/message.rs`) has no instruction-data field, so the witness is structurally absent from the on-chain transaction. This is the formerly-"tracked follow-up" (wiring through the PrivacyPreserving transaction variant), now implemented and demonstrated on the live testnet. The `claim_private` instruction (witness in public instruction data) remains an opt-in, witness-leaking fallback (`DISTRIBUTIONX_USE_CLAIM_PRIVATE=1`). + +2. **Bucket anonymity is bounded by per-bucket population.** The `bucket_id` is public (it is in the journal and in the airdrop's `bucket_table`). Observer unlinkability holds with probability at most 1/k per bucket, where k is the number of eligible recipients in that bucket. A singleton bucket reveals the recipient by amount; small buckets shrink the anonymity set. The CLI's `inspect-csv` command warns when the smallest bucket has fewer than 8 recipients (`crates/distributionx-cli/src/commands.rs:1213-1216`) and the `pad-csv --min-per-bucket N` command lets a distributor top up small buckets. The on-chain program does not enforce a minimum k; the distributor chooses the bucket schedule that fits their privacy budget. + +3. **Salt secrecy depends on the encrypted bundle and the recipient's local keystore.** Salts are 32 bytes from `OsRng` per row (`crates/distributionx-tree/src/bundle.rs:44-48`). Each row is sealed for its intended recipient with X25519 ECDH and ChaCha20-Poly1305 (`crates/distributionx-tree/src/bundle.rs:68-105`). The recipient's seed lives in a `wallet.seed` file under `target/distributionx-testnet/` by default, with a keychain-backed option in `crates/distributionx-wallet-ref/src/storage.rs`. The distributor knows every salt and can precompute a nullifier-to-row mapping; DistributionX protects observers from the eligibility set, not from the distributor. Under the default `claim` path the salt stays inside the encrypted bundle and the zkVM, and `claim.tx` strips the witness; the relayer and the chain transcript do not carry the salt. + +**Acknowledged naming mismatch.** The `claim_private` identifier predates this audit and does not match the instruction's actual privacy properties (it runs the opt-in in-program verifier and does not provide observer privacy for the witness on its current submission path). A rename to a clearer name such as `claim_inline` is deferred because it touches the LEZ program, the three IDL mirrors, the generated client and FFI, scripts, tests, and several doc sections (about 17 files in total). The doc points above describe what the instruction actually does; see the "A note on naming" paragraph in [docs/WRITEUP.md Privacy Model](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/WRITEUP.md#privacy-model). + +## Requirements — Status + +The two requirements previously flagged as pending: + +- **LEZ devnet/testnet evidence — NOW MET.** The program is deployed to the live LEZ testnet (`https://testnet.lez.logos.co`, LEZ v0.2.0-rc5) and the full flow — 2 distributions, 20 witness-private `claim_ppe` claims, and 20 token settlements — is recorded on-chain and verified with `getTransaction`. See the [LEZ Testnet Deployment](#lez-testnet-deployment-rc5) table above and [docs/TESTNET_EVIDENCE.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/TESTNET_EVIDENCE.md). Reaching the testnet required migrating the program off the retired `nssa_core` SDK onto LEZ v0.2.0-rc5 (the version the testnet runs) and moving the claim to the privacy-preserving `claim_ppe` path, which keeps the witness off-chain while fitting the 32M public-execution cap (the receipt-verifying `claim` path is ~218M cycles and cannot land in public execution). + +- **3 distributions from outside the team — dropped by the L-Prize team.** Per the Discord message (08 May 2026, [message link](https://discord.com/channels/973324189794697286/1501897314233618553/1502098264068194314)): "I will drop the '3 distributions from people outside the team' from the requirements. We did a lot of iteration on role of L-Prize and I now agree this is not really appropriate/useful for testnet L-Prize. Adoption criterias make more sense closer and post mainnet." + +## FURPS Self-Assessment + +### Functionality + +DistributionX supports distributor initialization, encrypted bundle creation, vault funding, Risc0 proof generation, proof verification, witness-private claim submission through the `claim_ppe` (PPE) instruction with per-claimant private destinations, duplicate-claim rejection, token settlement, close flow, Basecamp operation, and CLI operation. + +### Usability + +The README gives scratch-clone instructions for building binaries and running create/claim locally. The reviewer fixture seeds make the flow reproducible without regenerating every key. Basecamp provides the visual create/fund/claim flow, while the CLI provides deterministic evidence commands. + +### Reliability + +The CLI fails closed on invalid proofs, mismatched journals, missing bundles, missing destination packets, and duplicate claims. Local submit receipts are written under `target/distributionx-testnet/receipts/` during reproduction. + +### Performance + +Real Risc0 proving (the PPE composite proof) is the bottleneck on the claimant side. On-chain, the `claim_ppe` instruction's public-execution cost is 504401 CU per claim (deterministic across all 20 testnet claims) — far under the 32M public-execution cap, because the heavy verification runs in the PPE proof off the public budget and the sequencer only checks one succinct receipt. CU and wall-clock measurements are documented in [docs/bench/REPORT.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/bench/REPORT.md), with the live testnet run as the primary evidence. + +### Supportability + +The repo includes focused Rust crates, a Logos client module, a Basecamp app, local sequencer scripts, packaging scripts, benchmark docs, reviewer fixtures, and a system architecture diagram. Package verification is covered by [scripts/package.sh](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/scripts/package.sh). + +## Supporting Materials + +- **Testnet evidence (rc5 PPE): [docs/TESTNET_EVIDENCE.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/TESTNET_EVIDENCE.md)** — program id, 2 distributions, 20 witness-private `claim_ppe` claim tx hashes + 20 settlements, `getTransaction` verification. +- **Raw evidence artifacts: [docs/testnet-evidence/](https://github.com/Timidan/dist-x/tree/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/testnet-evidence)** — committed `getTransaction` verification jsonl, per-claim receipts (CU 504401), claim summaries, and run logs for both distributions. Witness-free: no eligible address, salt, signature, or Merkle path appears in them; the seed-bearing working state is intentionally not committed. +- README: [README.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/README.md) +- Technical write-up: [docs/WRITEUP.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/WRITEUP.md) +- Benchmark and CU report: [docs/bench/REPORT.md](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/docs/bench/REPORT.md) +- System architecture diagram: [DistributionX.system-architecture.excalidraw](https://github.com/Timidan/dist-x/blob/822c508940eeb08ad67cbaf4f9553665087a66f2/DistributionX.system-architecture.excalidraw) +- Basecamp app: [basecamp-app/](https://github.com/Timidan/dist-x/tree/822c508940eeb08ad67cbaf4f9553665087a66f2/basecamp-app) +- Logos client module: [distributionx_client_module/](https://github.com/Timidan/dist-x/tree/822c508940eeb08ad67cbaf4f9553665087a66f2/distributionx_client_module) +- Reviewer fixture: [fixtures/reviewer-fast-path/](https://github.com/Timidan/dist-x/tree/822c508940eeb08ad67cbaf4f9553665087a66f2/fixtures/reviewer-fast-path) + +## Terms & Conditions + +By submitting this solution, I confirm that I have read and agree to the [Terms & Conditions](https://github.com/logos-co/lambda-prize/blob/master/TERMS.md).