From bdc2810358bd07b344d1310d7470871c0f6679f8 Mon Sep 17 00:00:00 2001 From: retraca Date: Thu, 11 Jun 2026 17:11:43 +0800 Subject: [PATCH 01/12] solution: LP-0003 Private Airdrop --- solutions/LP-0003.md | 82 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 solutions/LP-0003.md diff --git a/solutions/LP-0003.md b/solutions/LP-0003.md new file mode 100644 index 0000000..7e55d2b --- /dev/null +++ b/solutions/LP-0003.md @@ -0,0 +1,82 @@ +# Solution: LP-0003 — Private Airdrop + +**Submitted by:** retraca (Gonçalo Traça) + +## Summary + +A private airdrop protocol where claimants prove Merkle membership without revealing their account identity. Tokens cannot be claimed twice (nullifier), and the receipt cannot be relayed to a different recipient (recipient binding). + +Three components: + +**`circuit/guest`** -- RISC0 zkVM guest that proves: (1) `(account_id, allocation)` is committed in the distribution Merkle tree; (2) the nullifier `SHA256(account_id || distributor_id)` is fresh; (3) the submitted `recipient_note` matches the hash in the journal. Uses domain-tagged Merkle hashes to prevent second-preimage attacks. + +**`programs/airdrop`** -- LEZ on-chain program. Verifies the RISC0 receipt, enforces distributor binding, checks the Merkle root, rejects spent nullifiers, and confirms that `SHA256(recipient_note) == journal.recipient_note_hash`. Stores `DistributionState` (root, total supply, claimed amount, spent nullifiers) on-chain. + +**`circuit/host`** -- Off-chain CLI (`airdrop-claim prove / verify`) that fetches the Merkle inclusion proof from the sequencer, runs the RISC0 prover, and outputs the receipt. + +**`sdk`** -- Reusable Rust crate exposing `submit_claim`, `leaf_hash`, and `node_hash` for integration into other Logos apps. + +## Repository + +- **Repo:** https://github.com/retraca/lp-0003-private-airdrop + +## Approach + +### Merkle tree design + +Leaves: `SHA256(0x00 || account_id || allocation_le)` +Internal nodes: `SHA256(0x01 || left || right)` + +The domain tags prevent second-preimage attacks: no internal node value can be presented as a valid leaf proof. + +### Privacy + +The claimant's `account_id` is a private guest input. The proof only reveals the nullifier (which leaks nothing about the identity -- it's a PRF output binding `account_id` and `distributor_id`) and the allocation amount. + +### Anti-replay (nullifier) + +`nullifier = SHA256(account_id || distributor_id)` + +Binds to both the claimant and the specific distribution. Re-using the same account against the same distribution produces the same nullifier, which the on-chain program rejects. + +### Recipient binding + +`recipient_note_hash = SHA256(recipient_note_preimage)` is committed in the RISC0 journal. The on-chain verifier checks `SHA256(submitted_note) == journal.recipient_note_hash`. A relay that intercepts the receipt and substitutes a different destination note is rejected by the program. + +### Error codes + +| Code | Meaning | +|------|---------| +| 7001 | ERR_PROOF_INVALID | +| 7002 | ERR_DISTRIBUTOR_MISMATCH | +| 7003 | ERR_ROOT_MISMATCH | +| 7004 | ERR_NULLIFIER_SPENT | +| 7005 | ERR_DISTRIBUTION_EXHAUSTED | +| 7006 | ERR_RECIPIENT_MISMATCH | + +## Testing + +```bash +# Start local chain +cd lez-build && ./start-chain.sh + +# Dev mode (mock proofs, instant) +cd lp-0003-private-airdrop && ./demo.sh --dev + +# Full RISC0 proofs +./demo.sh +``` + +## Files + +``` +lp-0003-private-airdrop/ + circuit/ + guest/src/main.rs # RISC0 zkVM guest + host/src/main.rs # CLI: airdrop-claim prove / verify + host/src/prover.rs # Host prover + Merkle proof fetch + programs/ + airdrop/src/lib.rs # LEZ on-chain verifier + sdk/src/lib.rs # Client SDK + demo.sh # End-to-end demo +``` From 6ca80f3889f591daff7086c5df8ce080f789dd70 Mon Sep 17 00:00:00 2001 From: retraca Date: Thu, 11 Jun 2026 17:58:48 +0800 Subject: [PATCH 02/12] fix: LP-0003 solution full template (FURPS, checklist, T&C) --- solutions/LP-0003.md | 122 ++++++++++++++++++++++--------------------- 1 file changed, 63 insertions(+), 59 deletions(-) diff --git a/solutions/LP-0003.md b/solutions/LP-0003.md index 7e55d2b..d6e6d10 100644 --- a/solutions/LP-0003.md +++ b/solutions/LP-0003.md @@ -1,20 +1,10 @@ -# Solution: LP-0003 — Private Airdrop +# Solution: LP-0003 -- Private Airdrop **Submitted by:** retraca (Gonçalo Traça) ## Summary -A private airdrop protocol where claimants prove Merkle membership without revealing their account identity. Tokens cannot be claimed twice (nullifier), and the receipt cannot be relayed to a different recipient (recipient binding). - -Three components: - -**`circuit/guest`** -- RISC0 zkVM guest that proves: (1) `(account_id, allocation)` is committed in the distribution Merkle tree; (2) the nullifier `SHA256(account_id || distributor_id)` is fresh; (3) the submitted `recipient_note` matches the hash in the journal. Uses domain-tagged Merkle hashes to prevent second-preimage attacks. - -**`programs/airdrop`** -- LEZ on-chain program. Verifies the RISC0 receipt, enforces distributor binding, checks the Merkle root, rejects spent nullifiers, and confirms that `SHA256(recipient_note) == journal.recipient_note_hash`. Stores `DistributionState` (root, total supply, claimed amount, spent nullifiers) on-chain. - -**`circuit/host`** -- Off-chain CLI (`airdrop-claim prove / verify`) that fetches the Merkle inclusion proof from the sequencer, runs the RISC0 prover, and outputs the receipt. - -**`sdk`** -- Reusable Rust crate exposing `submit_claim`, `leaf_hash`, and `node_hash` for integration into other Logos apps. +Private airdrop protocol for the Logos Execution Zone. Claimants prove Merkle membership without revealing their account identity. The receipt is bound to a single destination note (prevents relay attacks) and the nullifier prevents double-claiming. ## Repository @@ -24,59 +14,73 @@ Three components: ### Merkle tree design -Leaves: `SHA256(0x00 || account_id || allocation_le)` -Internal nodes: `SHA256(0x01 || left || right)` - -The domain tags prevent second-preimage attacks: no internal node value can be presented as a valid leaf proof. +Leaves: `SHA256(0x00 || account_id || allocation_le)`. Internal nodes: `SHA256(0x01 || left || right)`. Domain tags prevent second-preimage attacks: no internal node value can be presented as a valid leaf proof, and no leaf value can be used as an internal node. ### Privacy -The claimant's `account_id` is a private guest input. The proof only reveals the nullifier (which leaks nothing about the identity -- it's a PRF output binding `account_id` and `distributor_id`) and the allocation amount. +`account_id` is a private RISC0 guest input and never appears in the journal. The only public outputs are the nullifier (a PRF output that reveals nothing about the identity), the allocation, the Merkle root, and the recipient note hash. ### Anti-replay (nullifier) -`nullifier = SHA256(account_id || distributor_id)` - -Binds to both the claimant and the specific distribution. Re-using the same account against the same distribution produces the same nullifier, which the on-chain program rejects. +`nullifier = SHA256(account_id || distributor_id)`. Bound to both the claimant and the specific distribution. Re-using the same account against the same distribution produces the same nullifier, rejected on-chain. ### Recipient binding -`recipient_note_hash = SHA256(recipient_note_preimage)` is committed in the RISC0 journal. The on-chain verifier checks `SHA256(submitted_note) == journal.recipient_note_hash`. A relay that intercepts the receipt and substitutes a different destination note is rejected by the program. - -### Error codes - -| Code | Meaning | -|------|---------| -| 7001 | ERR_PROOF_INVALID | -| 7002 | ERR_DISTRIBUTOR_MISMATCH | -| 7003 | ERR_ROOT_MISMATCH | -| 7004 | ERR_NULLIFIER_SPENT | -| 7005 | ERR_DISTRIBUTION_EXHAUSTED | -| 7006 | ERR_RECIPIENT_MISMATCH | - -## Testing - -```bash -# Start local chain -cd lez-build && ./start-chain.sh - -# Dev mode (mock proofs, instant) -cd lp-0003-private-airdrop && ./demo.sh --dev - -# Full RISC0 proofs -./demo.sh -``` - -## Files - -``` -lp-0003-private-airdrop/ - circuit/ - guest/src/main.rs # RISC0 zkVM guest - host/src/main.rs # CLI: airdrop-claim prove / verify - host/src/prover.rs # Host prover + Merkle proof fetch - programs/ - airdrop/src/lib.rs # LEZ on-chain verifier - sdk/src/lib.rs # Client SDK - demo.sh # End-to-end demo -``` +`recipient_note_hash = SHA256(recipient_note_preimage)` is committed in the RISC0 journal. The on-chain verifier checks `SHA256(submitted_note) == journal.recipient_note_hash`. A relay that intercepts the receipt and substitutes a different destination fails with `ERR_RECIPIENT_MISMATCH`. + +### Why Logos + +LEZ's trustless execution guarantees that once a nullifier is marked spent, no operator can override it. Logos Storage can hold the encrypted Merkle tree data off-chain while only the root lives on-chain, keeping distribution data censorship-resistant. A centralised airdrop coordinator could censor specific claimants or modify allocations; neither is possible here. + +## Success Criteria Checklist + +- [x] Claimants prove Merkle membership without revealing their account identity. +- [x] Tokens cannot be claimed twice (nullifier stored on-chain, `ERR_NULLIFIER_SPENT`). +- [x] The receipt cannot be redirected to a different recipient (recipient binding via `SHA256(note)`). +- [x] Merkle second-preimage attacks are prevented (domain-tagged leaf and internal node hashes). +- [x] Proof generation runs client-side on a standard laptop. +- [x] Full documentation and a clean public repository are delivered. +- [x] Provide a module/SDK (`sdk/src/lib.rs`: `submit_claim`, `leaf_hash`, `node_hash`). +- [ ] Provide a Logos Basecamp app GUI with local build instructions and loadable assets. +- [x] Provide an IDL for the LEZ program (`lp-0003-private-airdrop.idl.json`). +- [x] The system handles proof generation failures gracefully. +- [x] A failed or rejected claim does not mark the claimant as having claimed. +- [x] Deterministic, documented error codes (7001-7006). +- [ ] At least 2 distinct distributions deployed on LEZ testnet with 20 combined claims. +- [ ] Document compute unit (CU) costs on LEZ devnet/testnet. +- [ ] End-to-end integration tests against a LEZ sequencer in CI. +- [x] CI green on the default branch. +- [x] README documents end-to-end usage. +- [x] Reproducible end-to-end demo script (`demo.sh`). +- [ ] Recorded video demo with `RISC0_DEV_MODE=0` terminal output. + +## FURPS Self-Assessment + +### Functionality + +Three on-chain instructions: `initialize` (registers root, total supply, distributor ID), `claim` (verifies RISC0 receipt, checks nullifier uniqueness and recipient binding, transfers allocation), `query_state`. The guest proves: leaf membership in the Merkle tree with domain-tagged hashes, nullifier freshness, and recipient note hash. Allocation is revealed in the journal, inherent to the distribution use case. Error codes 7001-7006 cover all invalid states. + +### Usability + +CLI (`airdrop-claim prove / verify`) handles the full claimant flow. SDK crate provides `submit_claim`, `leaf_hash`, `node_hash` for integration into other Logos apps. `demo.sh --dev` runs end-to-end with mock proofs in seconds. The distributor builds the Merkle tree off-chain and only commits the root to LEZ. + +### Reliability + +Proof verification fails closed. Recipient binding is enforced on-chain with a dedicated error code. Spent nullifiers persist in on-chain state across restarts. A rejected claim does not consume the claimant's nullifier -- they can retry with the correct destination note. + +### Performance + +CU costs: not yet measured (testnet deployment pending). RISC0 proof generation at `RISC0_DEV_MODE=0`: approximately 8-12 minutes on a 2024 MacBook Pro (M3). Merkle path depth scales as `log2(N)`: 10 nodes for 1000 claimants. + +### Supportability + +Integration tests in `programs/airdrop/tests/integration.rs` cover serialization, nullifier rejection, distributor mismatch, root mismatch, and recipient mismatch. CI runs `cargo check`, `cargo clippy -D warnings`, and `cargo test` with `RISC0_DEV_MODE=1` and `RISC0_SKIP_BUILD=1`. + +## Supporting Materials + +- Demo video: _pending testnet deployment_ +- Repository: https://github.com/retraca/lp-0003-private-airdrop + +## Terms & Conditions + +By submitting this solution, I confirm that I have read and agree to the [Terms & Conditions](../TERMS.md). The code is MIT licensed. From 505da867baa01ab89864ac9e958041fd9d0cc84a Mon Sep 17 00:00:00 2001 From: retraca Date: Thu, 11 Jun 2026 17:58:48 +0800 Subject: [PATCH 03/12] fix: LP-0002 solution full template (FURPS, checklist, T&C) --- solutions/LP-0002.md | 88 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 solutions/LP-0002.md diff --git a/solutions/LP-0002.md b/solutions/LP-0002.md new file mode 100644 index 0000000..eaf1846 --- /dev/null +++ b/solutions/LP-0002.md @@ -0,0 +1,88 @@ +# Solution: LP-0002 -- Private M-of-N Multisig + +**Submitted by:** retraca (Gonçalo Traça) + +## Summary + +Private M-of-N multisig for the Logos Execution Zone. Members hold shielded accounts; on-chain observers see only that M valid votes were received, not which members approved. + +Members register a `member_commitment = SHA256("member" || nsk || multisig_id)` at initialization. The nsk stays client-side. To vote, a member runs the RISC0 guest off-chain, which proves nsk knowledge without revealing which commitment was used. + +## Repository + +- **Repo:** https://github.com/retraca/lp-0002-private-multisig + +## Approach + +### LEZ nonce constraint + +LEZ private accounts increment their nonce on every spend. Direct signing for multisig participation would require a spend on every vote, which ties votes to the nonce sequence and breaks privacy. This implementation avoids the constraint: members register separate voting commitments derived from a fresh nsk. Voting proves nsk knowledge via ZK -- no LEZ spend occurs. + +### Threshold proof scheme + +Each vote is a separate RISC0 receipt. The on-chain program counts valid, non-duplicate receipts and gates execution at the threshold. The alternative -- a single proof of M voters (FROST, Semaphore batch) -- requires off-chain coordination between M members before proof generation. Separate receipts let each member vote independently. + +### Privacy design + +Journal: `(multisig_id, proposal_id, nullifier, member_set_root)` + +`member_set_root = SHA256(commitment[0] || ... || commitment[N-1])` proves the voter belongs to the registered set without revealing which specific commitment was used. An earlier design committed the specific `member_commitment` directly -- that would leak the voter identity since all N commitments are public on-chain. The `member_set_root` approach closes that leak. + +### Why Logos + +LEZ's trustless execution guarantees that `execute()` only fires once M valid receipts are on-chain -- no multisig operator or coordinator can override this. Logos Messaging provides the private coordination channel for passing proposals and receipts between members without a central server. A centralised alternative would require trusting the coordinator not to censor votes or reveal member identities. + +## Success Criteria Checklist + +- [x] Any M-of-N member holding a shielded LEZ account can submit an approval without revealing their identity to on-chain observers or other members. +- [x] The on-chain verifier confirms a threshold of M approvals was reached without recording which members approved. +- [x] A member cannot approve the same proposal twice (nullifier = `SHA256("multisig/v1/vote" || nsk || proposal_id || multisig_id)`). +- [x] A completed execution is unlinkable to any individual member's shielded account. +- [x] Proof generation runs client-side on a standard laptop (`RISC0_DEV_MODE=1` for development; full proofs require the RISC0 toolchain). +- [x] All default skills listed above are implemented and documented. +- [ ] A reference integration is delivered on LEZ testnet. +- [ ] At least 1 multisig instance is created on LEZ testnet, with at least one proposal submitted, approved by threshold, and executed. +- [x] Full documentation and a clean public repository are delivered. +- [x] Provide a module/SDK that can be used to build Logos modules for interacting with the program. +- [ ] Provide a Logos Basecamp app GUI with local build instructions, downloadable assets, and loadable in Logos app. +- [x] Provide an IDL for the LEZ program, using the SPEL framework (`lp-0002-private-multisig.idl.json`). +- [x] The system handles proof generation failures gracefully and surfaces a clear error. +- [x] A partial set of approvals (fewer than M) is preserved and resumable across client restarts. +- [x] The verifier program returns deterministic, documented error codes (6001-6010). +- [ ] Document the compute unit (CU) cost of each on-chain operation on LEZ devnet/testnet. +- [ ] End-to-end integration tests run against a LEZ sequencer (standalone mode). +- [x] CI must be green on the default branch. +- [x] A README documents end-to-end usage. +- [x] A reproducible end-to-end demo script is provided (`demo.sh`). +- [ ] A recorded video demo of the end-to-end flow is included. + +## FURPS Self-Assessment + +### Functionality + +Four on-chain instructions: `initialize`, `submit_proposal`, `vote`, `execute`. `initialize` registers up to 20 member commitments and sets the threshold. `vote` accepts a RISC0 receipt and checks: proof validity, multisig/proposal binding, `member_set_root` match against the registered set, nullifier uniqueness. `execute` fires when `vote_count >= threshold`. Proposal content is public (only member identity and votes are private, per spec). Max 20 members, max 100 proposals per multisig -- documented as known limitations. + +### Usability + +CLI (`multisig derive-commitment / vote / verify`) covers the full flow. SDK crate provides `derive_commitment`, `submit_vote`, `execute_proposal` for integration. `demo.sh --dev` runs end-to-end with mock proofs in seconds. Full proofs require the RISC0 toolchain and take several minutes per vote on a standard laptop. + +### Reliability + +Null-checking on all deserialized inputs. Proof verification fails closed -- `ERR_PROOF_INVALID` on any invalid receipt. Spent-nullifier tracking persists in on-chain state; double-vote is rejected even across process restarts. Known limitation: `spent_nullifiers` is a `Vec` and grows linearly with vote count -- acceptable for the max 100 proposals constraint. + +### Performance + +CU costs on LEZ testnet: not yet measured (testnet deployment pending). RISC0 proof generation on a 2024 MacBook Pro (M3): approximately 8-12 minutes per vote at `RISC0_DEV_MODE=0`. With Bonsai proving service: under 1 minute. + +### Supportability + +Unit and integration tests in `programs/multisig/tests/integration.rs` cover serialization round-trips, nullifier rejection, threshold gating, and error code correctness. CI runs `cargo check`, `cargo clippy -D warnings`, and `cargo test` with `RISC0_DEV_MODE=1`. `RISC0_SKIP_BUILD=1` lets CI skip guest compilation -- same pattern used by the LEZ team. + +## Supporting Materials + +- Demo video: _pending testnet deployment_ +- Repository: https://github.com/retraca/lp-0002-private-multisig + +## Terms & Conditions + +By submitting this solution, I confirm that I have read and agree to the [Terms & Conditions](../TERMS.md). The code is MIT licensed. From 461c7ca10950df99e53aa80f6fc61cbcbab2b1c4 Mon Sep 17 00:00:00 2001 From: retraca Date: Thu, 11 Jun 2026 17:59:48 +0800 Subject: [PATCH 04/12] fix: remove accidentally committed LP-0002 file from LP-0003 branch --- solutions/LP-0002.md | 88 -------------------------------------------- 1 file changed, 88 deletions(-) delete mode 100644 solutions/LP-0002.md diff --git a/solutions/LP-0002.md b/solutions/LP-0002.md deleted file mode 100644 index eaf1846..0000000 --- a/solutions/LP-0002.md +++ /dev/null @@ -1,88 +0,0 @@ -# Solution: LP-0002 -- Private M-of-N Multisig - -**Submitted by:** retraca (Gonçalo Traça) - -## Summary - -Private M-of-N multisig for the Logos Execution Zone. Members hold shielded accounts; on-chain observers see only that M valid votes were received, not which members approved. - -Members register a `member_commitment = SHA256("member" || nsk || multisig_id)` at initialization. The nsk stays client-side. To vote, a member runs the RISC0 guest off-chain, which proves nsk knowledge without revealing which commitment was used. - -## Repository - -- **Repo:** https://github.com/retraca/lp-0002-private-multisig - -## Approach - -### LEZ nonce constraint - -LEZ private accounts increment their nonce on every spend. Direct signing for multisig participation would require a spend on every vote, which ties votes to the nonce sequence and breaks privacy. This implementation avoids the constraint: members register separate voting commitments derived from a fresh nsk. Voting proves nsk knowledge via ZK -- no LEZ spend occurs. - -### Threshold proof scheme - -Each vote is a separate RISC0 receipt. The on-chain program counts valid, non-duplicate receipts and gates execution at the threshold. The alternative -- a single proof of M voters (FROST, Semaphore batch) -- requires off-chain coordination between M members before proof generation. Separate receipts let each member vote independently. - -### Privacy design - -Journal: `(multisig_id, proposal_id, nullifier, member_set_root)` - -`member_set_root = SHA256(commitment[0] || ... || commitment[N-1])` proves the voter belongs to the registered set without revealing which specific commitment was used. An earlier design committed the specific `member_commitment` directly -- that would leak the voter identity since all N commitments are public on-chain. The `member_set_root` approach closes that leak. - -### Why Logos - -LEZ's trustless execution guarantees that `execute()` only fires once M valid receipts are on-chain -- no multisig operator or coordinator can override this. Logos Messaging provides the private coordination channel for passing proposals and receipts between members without a central server. A centralised alternative would require trusting the coordinator not to censor votes or reveal member identities. - -## Success Criteria Checklist - -- [x] Any M-of-N member holding a shielded LEZ account can submit an approval without revealing their identity to on-chain observers or other members. -- [x] The on-chain verifier confirms a threshold of M approvals was reached without recording which members approved. -- [x] A member cannot approve the same proposal twice (nullifier = `SHA256("multisig/v1/vote" || nsk || proposal_id || multisig_id)`). -- [x] A completed execution is unlinkable to any individual member's shielded account. -- [x] Proof generation runs client-side on a standard laptop (`RISC0_DEV_MODE=1` for development; full proofs require the RISC0 toolchain). -- [x] All default skills listed above are implemented and documented. -- [ ] A reference integration is delivered on LEZ testnet. -- [ ] At least 1 multisig instance is created on LEZ testnet, with at least one proposal submitted, approved by threshold, and executed. -- [x] Full documentation and a clean public repository are delivered. -- [x] Provide a module/SDK that can be used to build Logos modules for interacting with the program. -- [ ] Provide a Logos Basecamp app GUI with local build instructions, downloadable assets, and loadable in Logos app. -- [x] Provide an IDL for the LEZ program, using the SPEL framework (`lp-0002-private-multisig.idl.json`). -- [x] The system handles proof generation failures gracefully and surfaces a clear error. -- [x] A partial set of approvals (fewer than M) is preserved and resumable across client restarts. -- [x] The verifier program returns deterministic, documented error codes (6001-6010). -- [ ] Document the compute unit (CU) cost of each on-chain operation on LEZ devnet/testnet. -- [ ] End-to-end integration tests run against a LEZ sequencer (standalone mode). -- [x] CI must be green on the default branch. -- [x] A README documents end-to-end usage. -- [x] A reproducible end-to-end demo script is provided (`demo.sh`). -- [ ] A recorded video demo of the end-to-end flow is included. - -## FURPS Self-Assessment - -### Functionality - -Four on-chain instructions: `initialize`, `submit_proposal`, `vote`, `execute`. `initialize` registers up to 20 member commitments and sets the threshold. `vote` accepts a RISC0 receipt and checks: proof validity, multisig/proposal binding, `member_set_root` match against the registered set, nullifier uniqueness. `execute` fires when `vote_count >= threshold`. Proposal content is public (only member identity and votes are private, per spec). Max 20 members, max 100 proposals per multisig -- documented as known limitations. - -### Usability - -CLI (`multisig derive-commitment / vote / verify`) covers the full flow. SDK crate provides `derive_commitment`, `submit_vote`, `execute_proposal` for integration. `demo.sh --dev` runs end-to-end with mock proofs in seconds. Full proofs require the RISC0 toolchain and take several minutes per vote on a standard laptop. - -### Reliability - -Null-checking on all deserialized inputs. Proof verification fails closed -- `ERR_PROOF_INVALID` on any invalid receipt. Spent-nullifier tracking persists in on-chain state; double-vote is rejected even across process restarts. Known limitation: `spent_nullifiers` is a `Vec` and grows linearly with vote count -- acceptable for the max 100 proposals constraint. - -### Performance - -CU costs on LEZ testnet: not yet measured (testnet deployment pending). RISC0 proof generation on a 2024 MacBook Pro (M3): approximately 8-12 minutes per vote at `RISC0_DEV_MODE=0`. With Bonsai proving service: under 1 minute. - -### Supportability - -Unit and integration tests in `programs/multisig/tests/integration.rs` cover serialization round-trips, nullifier rejection, threshold gating, and error code correctness. CI runs `cargo check`, `cargo clippy -D warnings`, and `cargo test` with `RISC0_DEV_MODE=1`. `RISC0_SKIP_BUILD=1` lets CI skip guest compilation -- same pattern used by the LEZ team. - -## Supporting Materials - -- Demo video: _pending testnet deployment_ -- Repository: https://github.com/retraca/lp-0002-private-multisig - -## Terms & Conditions - -By submitting this solution, I confirm that I have read and agree to the [Terms & Conditions](../TERMS.md). The code is MIT licensed. From 516e3520811d16780990312bf0f853d595214b68 Mon Sep 17 00:00:00 2001 From: retraca Date: Thu, 11 Jun 2026 23:22:39 +0800 Subject: [PATCH 05/12] fix: LP-0003 solution -- accurate checklist, behavioral tests, clean prose --- solutions/LP-0003.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/solutions/LP-0003.md b/solutions/LP-0003.md index d6e6d10..2b4692a 100644 --- a/solutions/LP-0003.md +++ b/solutions/LP-0003.md @@ -14,29 +14,29 @@ Private airdrop protocol for the Logos Execution Zone. Claimants prove Merkle me ### Merkle tree design -Leaves: `SHA256(0x00 || account_id || allocation_le)`. Internal nodes: `SHA256(0x01 || left || right)`. Domain tags prevent second-preimage attacks: no internal node value can be presented as a valid leaf proof, and no leaf value can be used as an internal node. +Leaves: `SHA256(0x00 || account_id || allocation_le)`. Internal nodes: `SHA256(0x01 || left || right)`. Domain tags prevent second-preimage attacks: an internal node hash cannot be presented as a valid leaf proof and vice versa. ### Privacy -`account_id` is a private RISC0 guest input and never appears in the journal. The only public outputs are the nullifier (a PRF output that reveals nothing about the identity), the allocation, the Merkle root, and the recipient note hash. +`account_id` is a private RISC0 guest input and never appears in the journal. The public outputs are: the nullifier (a PRF output bound to the claimant and distributor), the allocation, the Merkle root, and the recipient note hash. The allocation is inherently public in an airdrop context -- the token amount must be verifiable by the on-chain program. ### Anti-replay (nullifier) -`nullifier = SHA256(account_id || distributor_id)`. Bound to both the claimant and the specific distribution. Re-using the same account against the same distribution produces the same nullifier, rejected on-chain. +`nullifier = SHA256(account_id || distributor_id)`. A claimant who attempts to claim twice against the same distribution produces the same nullifier, which the on-chain program rejects with `ERR_NULLIFIER_SPENT`. ### Recipient binding -`recipient_note_hash = SHA256(recipient_note_preimage)` is committed in the RISC0 journal. The on-chain verifier checks `SHA256(submitted_note) == journal.recipient_note_hash`. A relay that intercepts the receipt and substitutes a different destination fails with `ERR_RECIPIENT_MISMATCH`. +`recipient_note_hash = SHA256(recipient_note_preimage)` is committed in the RISC0 journal. The on-chain program checks `SHA256(submitted_note) == journal.recipient_note_hash` before any state mutation. A relay that intercepts the receipt and substitutes a different destination address fails with `ERR_RECIPIENT_MISMATCH`. Critically, this check runs before the nullifier is marked spent -- a failed relay attempt leaves the legitimate claimant's nullifier unconsumed. ### Why Logos -LEZ's trustless execution guarantees that once a nullifier is marked spent, no operator can override it. Logos Storage can hold the encrypted Merkle tree data off-chain while only the root lives on-chain, keeping distribution data censorship-resistant. A centralised airdrop coordinator could censor specific claimants or modify allocations; neither is possible here. +LEZ's program model commits state changes atomically: either the full `claim()` execution succeeds and the nullifier is marked spent, or it fails and no state changes. No operator can selectively apply state (e.g., marking a nullifier spent without transferring tokens). Only the Merkle root lives on-chain; the plaintext allocation list stays off-chain, where it can be stored on Logos Storage without being visible to the sequencer. A centralised airdrop coordinator could censor specific claimants or alter allocations post-commitment; neither is possible with an on-chain root. ## Success Criteria Checklist - [x] Claimants prove Merkle membership without revealing their account identity. - [x] Tokens cannot be claimed twice (nullifier stored on-chain, `ERR_NULLIFIER_SPENT`). -- [x] The receipt cannot be redirected to a different recipient (recipient binding via `SHA256(note)`). +- [x] The receipt cannot be redirected to a different recipient (recipient binding via `SHA256(note)`, checked before any state mutation). - [x] Merkle second-preimage attacks are prevented (domain-tagged leaf and internal node hashes). - [x] Proof generation runs client-side on a standard laptop. - [x] Full documentation and a clean public repository are delivered. @@ -44,7 +44,7 @@ LEZ's trustless execution guarantees that once a nullifier is marked spent, no o - [ ] Provide a Logos Basecamp app GUI with local build instructions and loadable assets. - [x] Provide an IDL for the LEZ program (`lp-0003-private-airdrop.idl.json`). - [x] The system handles proof generation failures gracefully. -- [x] A failed or rejected claim does not mark the claimant as having claimed. +- [x] A failed or rejected claim does not mark the claimant as having claimed (recipient binding checked before nullifier write). - [x] Deterministic, documented error codes (7001-7006). - [ ] At least 2 distinct distributions deployed on LEZ testnet with 20 combined claims. - [ ] Document compute unit (CU) costs on LEZ devnet/testnet. @@ -58,23 +58,23 @@ LEZ's trustless execution guarantees that once a nullifier is marked spent, no o ### Functionality -Three on-chain instructions: `initialize` (registers root, total supply, distributor ID), `claim` (verifies RISC0 receipt, checks nullifier uniqueness and recipient binding, transfers allocation), `query_state`. The guest proves: leaf membership in the Merkle tree with domain-tagged hashes, nullifier freshness, and recipient note hash. Allocation is revealed in the journal, inherent to the distribution use case. Error codes 7001-7006 cover all invalid states. +Three on-chain instructions: `initialize` (registers Merkle root, total supply; distributor ID is the account ID of the distribution account), `claim` (verifies RISC0 receipt, enforces recipient binding, checks nullifier uniqueness, transfers allocation), `query_state`. The guest proves: leaf membership in the domain-tagged Merkle tree, nullifier computation from the private account ID, and recipient note hash. Error codes 7001-7006 cover all invalid states. ### Usability -CLI (`airdrop-claim prove / verify`) handles the full claimant flow. SDK crate provides `submit_claim`, `leaf_hash`, `node_hash` for integration into other Logos apps. `demo.sh --dev` runs end-to-end with mock proofs in seconds. The distributor builds the Merkle tree off-chain and only commits the root to LEZ. +CLI (`airdrop-claim prove / verify`) handles the full claimant flow offline. `demo.sh --dev` runs end-to-end without a chain in seconds. SDK crate provides `submit_claim`, `leaf_hash`, `node_hash` for integration into other LEZ programs. The distributor builds the Merkle tree off-chain and commits only the root to LEZ. ### Reliability -Proof verification fails closed. Recipient binding is enforced on-chain with a dedicated error code. Spent nullifiers persist in on-chain state across restarts. A rejected claim does not consume the claimant's nullifier -- they can retry with the correct destination note. +Proof verification fails closed. Recipient binding is the first check in `claim()` -- a relay interception leaves no side effects. Spent nullifiers persist in on-chain state across restarts. A rejected claim (any error code) leaves the nullifier unconsumed; the claimant can retry. ### Performance -CU costs: not yet measured (testnet deployment pending). RISC0 proof generation at `RISC0_DEV_MODE=0`: approximately 8-12 minutes on a 2024 MacBook Pro (M3). Merkle path depth scales as `log2(N)`: 10 nodes for 1000 claimants. +CU costs: not yet measured (testnet deployment pending). RISC0 proof generation at `RISC0_DEV_MODE=0`: approximately 8-12 minutes on a 2024 MacBook Pro (M3). Merkle path depth scales as `log2(N)`: 10 hops for 1000 claimants. ### Supportability -Integration tests in `programs/airdrop/tests/integration.rs` cover serialization, nullifier rejection, distributor mismatch, root mismatch, and recipient mismatch. CI runs `cargo check`, `cargo clippy -D warnings`, and `cargo test` with `RISC0_DEV_MODE=1` and `RISC0_SKIP_BUILD=1`. +Integration tests in `programs/airdrop/tests/integration.rs` exercise `apply_claim` directly (no sequencer or RISC0 receipt needed): successful claim state update, nullifier-spent rejection, distributor mismatch, root mismatch, recipient mismatch, and distribution exhausted. The recipient-mismatch test verifies the nullifier remains unconsumed after a failed relay attempt. CI runs `cargo check`, `cargo clippy -D warnings`, and `cargo test` with `RISC0_DEV_MODE=1`. ## Supporting Materials From 1c5d1552cba757fb8dfcb559ab44cf4edf0a8b19 Mon Sep 17 00:00:00 2001 From: retraca Date: Fri, 12 Jun 2026 00:05:08 +0800 Subject: [PATCH 06/12] fix: mark Basecamp app delivered --- solutions/LP-0003.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solutions/LP-0003.md b/solutions/LP-0003.md index 2b4692a..4a79ba8 100644 --- a/solutions/LP-0003.md +++ b/solutions/LP-0003.md @@ -41,7 +41,7 @@ LEZ's program model commits state changes atomically: either the full `claim()` - [x] Proof generation runs client-side on a standard laptop. - [x] Full documentation and a clean public repository are delivered. - [x] Provide a module/SDK (`sdk/src/lib.rs`: `submit_claim`, `leaf_hash`, `node_hash`). -- [ ] Provide a Logos Basecamp app GUI with local build instructions and loadable assets. +- [x] Provide a Logos Basecamp app GUI with local build instructions and loadable assets (`basecamp-app/`: `module.json`, `index.html`, `README.md` — loads directly in Logos Basecamp, no build step). - [x] Provide an IDL for the LEZ program (`lp-0003-private-airdrop.idl.json`). - [x] The system handles proof generation failures gracefully. - [x] A failed or rejected claim does not mark the claimant as having claimed (recipient binding checked before nullifier write). From 681015336c5d09a1fcf8c1c3e3e703f7abe5b632 Mon Sep 17 00:00:00 2001 From: retraca Date: Fri, 12 Jun 2026 00:25:04 +0800 Subject: [PATCH 07/12] fix: submitted by -> retraca only --- solutions/LP-0003.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solutions/LP-0003.md b/solutions/LP-0003.md index 4a79ba8..85db60b 100644 --- a/solutions/LP-0003.md +++ b/solutions/LP-0003.md @@ -1,6 +1,6 @@ # Solution: LP-0003 -- Private Airdrop -**Submitted by:** retraca (Gonçalo Traça) +**Submitted by:** retraca ## Summary From e5e3a25afbddd9a3822f0376cf2d2f8451c305dd Mon Sep 17 00:00:00 2001 From: retraca Date: Fri, 12 Jun 2026 01:37:19 +0800 Subject: [PATCH 08/12] solution: add program_id for LP-0003 deployment --- solutions/LP-0003.md | 1 + 1 file changed, 1 insertion(+) diff --git a/solutions/LP-0003.md b/solutions/LP-0003.md index 85db60b..c819a6a 100644 --- a/solutions/LP-0003.md +++ b/solutions/LP-0003.md @@ -9,6 +9,7 @@ Private airdrop protocol for the Logos Execution Zone. Claimants prove Merkle me ## Repository - **Repo:** https://github.com/retraca/lp-0003-private-airdrop +- **Program ID:** `d7f401fde733a4ac2b54f4fa909de9e2c86d2f2fd9e256498efea527ade52e85` ## Approach From c14eda1576821490c0a13fb86402b131712d48c8 Mon Sep 17 00:00:00 2001 From: retraca Date: Fri, 12 Jun 2026 15:01:13 +0800 Subject: [PATCH 09/12] fix: add chain subcommand docs and testnet demo.sh flag --- solutions/LP-0003.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solutions/LP-0003.md b/solutions/LP-0003.md index c819a6a..06de66b 100644 --- a/solutions/LP-0003.md +++ b/solutions/LP-0003.md @@ -63,7 +63,7 @@ Three on-chain instructions: `initialize` (registers Merkle root, total supply; ### Usability -CLI (`airdrop-claim prove / verify`) handles the full claimant flow offline. `demo.sh --dev` runs end-to-end without a chain in seconds. SDK crate provides `submit_claim`, `leaf_hash`, `node_hash` for integration into other LEZ programs. The distributor builds the Merkle tree off-chain and commits only the root to LEZ. +CLI covers the full flow: `airdrop-claim prove / verify` for offline use, and `airdrop-claim chain initialize / claim` for on-chain submission (compiled with `--features chain`). `demo.sh --dev` runs end-to-end without a chain in seconds; `demo.sh --dev --chain` adds the on-chain steps (`SEQUENCER=https://testnet.lez.logos.co` for testnet). SDK crate provides `submit_claim`, `leaf_hash`, `node_hash` for integration into other LEZ programs. The distributor builds the Merkle tree off-chain and commits only the root to LEZ. ### Reliability From 7dec60ecbf2996174f1fa67291c18300b7cad09f Mon Sep 17 00:00:00 2001 From: retraca Date: Fri, 12 Jun 2026 18:14:25 +0800 Subject: [PATCH 10/12] solution: LP-0003 hosted-testnet evidence (deploy + 2 distributions), claim-path constraint documented --- solutions/LP-0003.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solutions/LP-0003.md b/solutions/LP-0003.md index 06de66b..2bc18c8 100644 --- a/solutions/LP-0003.md +++ b/solutions/LP-0003.md @@ -47,7 +47,7 @@ LEZ's program model commits state changes atomically: either the full `claim()` - [x] The system handles proof generation failures gracefully. - [x] A failed or rejected claim does not mark the claimant as having claimed (recipient binding checked before nullifier write). - [x] Deterministic, documented error codes (7001-7006). -- [ ] At least 2 distinct distributions deployed on LEZ testnet with 20 combined claims. +- [ ] At least 2 distinct distributions deployed on LEZ testnet with 20 combined claims. **Partial:** program deployed (tx `5e1b1952…c5df`, program ID `d7f401fd…2e85`) and two distinct distributions initialized on the hosted testnet `https://testnet.lez.logos.co` (accounts `213a015c…9878` and `ad4009d1…1215`, txs `ac74f7e1…68df` / `83c4f97d…cd76`, supplies 1,000,000 and 500,000) -- see `docs/TESTNET_EVIDENCE.md` in the repo. On-chain claims are blocked by an LEZ platform constraint (public transactions carry no RISC0 receipts, so the claim-proof assumption cannot be resolved); the fix is submitting claims via the LEZ privacy-preserving transaction path, in progress. - [ ] Document compute unit (CU) costs on LEZ devnet/testnet. - [ ] End-to-end integration tests against a LEZ sequencer in CI. - [x] CI green on the default branch. @@ -71,7 +71,7 @@ Proof verification fails closed. Recipient binding is the first check in `claim( ### Performance -CU costs: not yet measured (testnet deployment pending). RISC0 proof generation at `RISC0_DEV_MODE=0`: approximately 8-12 minutes on a 2024 MacBook Pro (M3). Merkle path depth scales as `log2(N)`: 10 hops for 1000 claimants. +CU costs: `initialize` executes in ~5-9 ms of zkVM executor time on the sequencer (well under the 32M-cycle public execution budget); the receipt-verifying `claim` is pending the privacy-path rework. RISC0 claim proof generation at `RISC0_DEV_MODE=0`: approximately 8-12 minutes on a 2024 MacBook Pro (M3). Merkle path depth scales as `log2(N)`: 10 hops for 1000 claimants. ### Supportability From 46f2c2235d36ff03b4cf1659c06fdce37abcc4d6 Mon Sep 17 00:00:00 2001 From: retraca Date: Fri, 12 Jun 2026 23:56:25 +0800 Subject: [PATCH 11/12] solution: LP-0003 complete -- 2 distributions, 20 real-proof claims confirmed on hosted testnet --- solutions/LP-0003.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solutions/LP-0003.md b/solutions/LP-0003.md index 2bc18c8..c8af444 100644 --- a/solutions/LP-0003.md +++ b/solutions/LP-0003.md @@ -47,7 +47,7 @@ LEZ's program model commits state changes atomically: either the full `claim()` - [x] The system handles proof generation failures gracefully. - [x] A failed or rejected claim does not mark the claimant as having claimed (recipient binding checked before nullifier write). - [x] Deterministic, documented error codes (7001-7006). -- [ ] At least 2 distinct distributions deployed on LEZ testnet with 20 combined claims. **Partial:** program deployed (tx `5e1b1952…c5df`, program ID `d7f401fd…2e85`) and two distinct distributions initialized on the hosted testnet `https://testnet.lez.logos.co` (accounts `213a015c…9878` and `ad4009d1…1215`, txs `ac74f7e1…68df` / `83c4f97d…cd76`, supplies 1,000,000 and 500,000) -- see `docs/TESTNET_EVIDENCE.md` in the repo. On-chain claims are blocked by an LEZ platform constraint (public transactions carry no RISC0 receipts, so the claim-proof assumption cannot be resolved); the fix is submitting claims via the LEZ privacy-preserving transaction path, in progress. +- [x] At least 2 distinct distributions deployed on LEZ testnet with 20 combined claims. **Complete, with REAL proofs (`RISC0_DEV_MODE=0`):** two distributions on the hosted testnet `https://testnet.lez.logos.co` (distributors `4bfe13df…ce4e` and `65fdea01…d857`, 16-leaf eligibility trees, 10 claimants each) with **all 20 claims confirmed on-chain** -- every claim a privacy-preserving transaction proven locally and confirmed before the next began. Final on-chain state per distribution: `claimed = 5500` (sum of all allocations), 10 spent nullifiers. All 24 transaction hashes (2 program deploys, 2 initializes, 20 claims) in `docs/TESTNET_EVIDENCE.md`, reproducible via `scripts/testnet_claims_run.sh`. - [ ] Document compute unit (CU) costs on LEZ devnet/testnet. - [ ] End-to-end integration tests against a LEZ sequencer in CI. - [x] CI green on the default branch. @@ -71,7 +71,7 @@ Proof verification fails closed. Recipient binding is the first check in `claim( ### Performance -CU costs: `initialize` executes in ~5-9 ms of zkVM executor time on the sequencer (well under the 32M-cycle public execution budget); the receipt-verifying `claim` is pending the privacy-path rework. RISC0 claim proof generation at `RISC0_DEV_MODE=0`: approximately 8-12 minutes on a 2024 MacBook Pro (M3). Merkle path depth scales as `log2(N)`: 10 hops for 1000 claimants. +CU costs: `initialize` runs in ~4-10 ms of zkVM executor time on the sequencer (well under the 32M-cycle public execution budget). A claim costs the sequencer one succinct receipt verification (the same as any privacy-preserving transaction). Client-side claim proving at `RISC0_DEV_MODE=0`: ~7-10 minutes on Apple silicon; the 20-claim evidence run completed in ~3.2 hours on one laptop. Merkle path depth scales as `log2(N)`: 10 hops for 1000 claimants. ### Supportability From cdc98bbd7551d6203617d3ad6750ece3c45e52e9 Mon Sep 17 00:00:00 2001 From: retraca Date: Sat, 13 Jun 2026 00:06:41 +0800 Subject: [PATCH 12/12] solution: LP-0003 v2 program IDs (airdrop + claim-circuit) --- solutions/LP-0003.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solutions/LP-0003.md b/solutions/LP-0003.md index c8af444..dcbde49 100644 --- a/solutions/LP-0003.md +++ b/solutions/LP-0003.md @@ -9,7 +9,8 @@ Private airdrop protocol for the Logos Execution Zone. Claimants prove Merkle me ## Repository - **Repo:** https://github.com/retraca/lp-0003-private-airdrop -- **Program ID:** `d7f401fde733a4ac2b54f4fa909de9e2c86d2f2fd9e256498efea527ade52e85` +- **Airdrop program ID:** `641e17aa9ac2c393a01d4cdf3d12621c1816466b685e0b6993a760c16f5d2e8f` +- **Claim-circuit program ID:** `2919d161b729ec935b5ef5cc40b319fda02ad6d81df81f0245f5308b86b7fcd8` ## Approach