-
Notifications
You must be signed in to change notification settings - Fork 3
Add airdrop implementations #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| # Account Comparison | ||
|
|
||
| Side-by-side implementation of Solana accounts vs compressed accounts. Shows equivalent create and update operations for both account types using identical data structures. | ||
|
|
||
| ## Summary | ||
|
|
||
| - Demonstrates equivalent PDA and compressed account patterns with identical seeds `["account", user.key()]` | ||
| - Compressed accounts use `LightAccount::new_init` for creation and `LightAccount::new_mut` for updates | ||
| - Updates require client to pass existing state because on-chain storage is a Poseidon hash | ||
| - All fields marked `#[hash]` are included in Poseidon hash computation | ||
|
|
||
| ## Source Structure | ||
|
|
||
| ``` | ||
| programs/account-comparison/src/ | ||
| lib.rs # Program entrypoint, instructions, account structs | ||
| programs/account-comparison/tests/ | ||
| test_solana_account.rs # LiteSVM tests for standard accounts | ||
| test_compressed_account.rs # Light Protocol tests for compressed accounts | ||
| ``` | ||
|
|
||
| ## Accounts | ||
|
|
||
| ### AccountData (Solana PDA) | ||
|
|
||
| | Field | Type | Size | | ||
| |-------|------|------| | ||
| | discriminator | `[u8; 8]` | 8 bytes | | ||
| | user | `Pubkey` | 32 bytes | | ||
| | name | `String` | 4 + name_len (max 60 chars) | | ||
| | data | `[u8; 128]` | 128 bytes | | ||
|
|
||
| - **Seeds**: `["account", user.key()]` | ||
| - **Discriminator**: 8 bytes, SHA256("account:AccountData")[0..8] | ||
| - **Space**: 232 bytes. String uses Borsh serialization (4-byte length prefix + variable content). | ||
|
|
||
| ### CompressedAccountData (LightAccount) | ||
|
|
||
| ```rust | ||
| #[derive(LightDiscriminator, LightHasher)] | ||
| pub struct CompressedAccountData { | ||
| #[hash] pub user: Pubkey, | ||
| #[hash] pub name: String, | ||
| #[hash] pub data: [u8; 128], | ||
| } | ||
| ``` | ||
|
|
||
| - **Address seeds**: `["account", user.key()]` | ||
| - **Discriminator**: `LightDiscriminator` derive generates from struct name | ||
| - **Hashing**: Poseidon hash of all `#[hash]` fields (user, name, data) | ||
|
|
||
| ### CPI Signer | ||
|
|
||
| ```rust | ||
| const CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("FYX4GmKJYzSiycc7XZKf12NGXNE9siSx1cJubYJniHcv"); | ||
| ``` | ||
|
|
||
| ## Instructions | ||
|
|
||
| | # | Instruction | Accounts | Parameters | Logic | | ||
| |---|-------------|----------|------------|-------| | ||
| | 0 | `create_account` | `user` (signer, mut), `account` (init PDA), `system_program` | `name: String` | Initializes PDA with seeds `["account", user]`, sets `data = [1u8; 128]` | | ||
| | 1 | `update_data` | `user` (signer, mut), `account` (mut, has_one = user) | `data: [u8; 128]` | Overwrites account.data | | ||
| | 2 | `create_compressed_account` | `user` (signer, mut) + remaining_accounts | `name`, `proof`, `address_tree_info`, `output_tree_index` | Validates ADDRESS_TREE_V2, derives address, calls `LightAccount::new_init`, invokes light-system-program | | ||
| | 3 | `update_compressed_account` | `user` (signer, mut) + remaining_accounts | `new_data`, `existing_data`, `name`, `proof`, `account_meta` | Reconstructs state via `LightAccount::new_mut`, verifies user ownership, invokes light-system-program | | ||
|
|
||
| ## Security | ||
|
|
||
| | Check | Location | Description | | ||
| |-------|----------|-------------| | ||
| | Address tree validation | `create_compressed_account` | Verifies `address_tree_pubkey.to_bytes() == ADDRESS_TREE_V2` | | ||
| | Owner verification | `update_compressed_account` | Asserts `compressed_account.user == ctx.accounts.user.key()` | | ||
| | PDA constraint | `update_data` | Anchor `has_one = user` constraint | | ||
| | Signer requirement | All instructions | User must sign transaction | | ||
|
|
||
| ## Errors | ||
|
|
||
| | Error | Message | | ||
| |-------|---------| | ||
| | `CustomError::Unauthorized` | "No authority to perform this action" | | ||
| | `ProgramError::InvalidAccountData` | Returned when address tree validation fails | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # Airdrop Implementations | ||
|
|
||
| Simple Implementation: simple-claim - Distributes compressed tokens that get decompressed on claim | ||
|
|
||
| Advanced Implementation: distributor - Distributes SPL tokens, uses compressed PDAs to track claims | ||
|
|
||
|
|
||
| ## Quick comparison | ||
|
|
||
| | | simple-claim | distributor | | ||
| |--|------------------|-------------| | ||
| | Time-lock | Slot-based (all-or-nothing) | Timestamp-based (linear vesting) | | ||
| | Partial claims | No | Yes | | ||
| | Clawback | No | Yes | | ||
| | Admin controls | No | Yes | | ||
|
|
||
| ## Cost (10,000 recipients) | ||
|
|
||
| | | simple-claim | distributor | Regular | | ||
| |--|----------------:|------------:|----------------:| | ||
| | Setup | ~0.03 SOL | ~0.002 SOL | ~0.002 SOL | | ||
| | Claim tracking | 0 | ~0.03 SOL | ~6 SOL | | ||
| | **Total** | **~0.03 SOL** | **~0.03 SOL** | **~6 SOL** | | ||
|
|
||
| ## Getting started | ||
|
|
||
| - [simple-claim README](./simple-claim/README.md) | ||
| - [distributor README](./distributor/README.md) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| # Basic Operations Examples | ||
|
|
||
| Example programs showing all basic operations for compressed accounts. | ||
|
|
||
| ## Summary | ||
|
|
||
| - Demonstrates create, update, close, reinit, and burn operations | ||
| - Anchor uses Poseidon hashing (`light_sdk::account::LightAccount`); Native uses SHA-256 (`light_sdk::account::sha::LightAccount`) | ||
| - All programs derive addresses from seeds `[b"message", signer.key.as_ref()]` + `ADDRESS_TREE_V2` + program ID | ||
| - Close preserves address for reinitialization; burn permanently deletes address | ||
|
|
||
| ## [Anchor README](anchor/README.md) | [Native README](native/README.md) | ||
|
|
||
| ## Source structure | ||
|
|
||
| ``` | ||
| basic-operations/ | ||
| ├── anchor/ # Anchor framework (Poseidon hashing) | ||
| │ ├── create/programs/create/src/lib.rs | ||
| │ ├── update/programs/update/src/lib.rs | ||
| │ ├── close/programs/close/src/lib.rs | ||
| │ ├── reinit/programs/reinit/src/lib.rs | ||
| │ └── burn/programs/burn/src/lib.rs | ||
| └── native/ # Native Solana (SHA-256 hashing) | ||
| └── programs/ | ||
| ├── create/src/lib.rs | ||
| ├── update/src/lib.rs | ||
| ├── close/src/lib.rs | ||
| ├── reinit/src/lib.rs | ||
| └── burn/src/lib.rs | ||
| ``` | ||
|
|
||
| ## Accounts | ||
|
|
||
| ### MyCompressedAccount | ||
|
|
||
| Shared account structure across all examples. Derives `LightDiscriminator` for compressed account type identification. | ||
|
|
||
| ```rust | ||
| #[derive(LightDiscriminator)] | ||
| pub struct MyCompressedAccount { | ||
| pub owner: Pubkey, // Account owner | ||
| pub message: String, // User-defined message | ||
| } | ||
| ``` | ||
|
|
||
| ## Instructions | ||
|
|
||
| Native programs use `InstructionType` enum discriminators (first byte of instruction data). | ||
|
|
||
| ### create_account (discriminator: 0) | ||
|
|
||
| | Parameter | Type | Description | | ||
| |-----------|------|-------------| | ||
| | `proof` | `ValidityProof` | ZK proof for address non-existence | | ||
| | `address_tree_info` | `PackedAddressTreeInfo` | Address tree metadata | | ||
| | `output_state_tree_index` | `u8` | Target state tree index | | ||
| | `message` | `String` | Initial message content | | ||
|
|
||
| Calls `LightAccount::new_init()` with derived address, invokes `LightSystemProgramCpi` with `with_new_addresses()`. | ||
|
|
||
| ### update_account (discriminator: 1) | ||
|
|
||
| | Parameter | Type | Description | | ||
| |-----------|------|-------------| | ||
| | `proof` | `ValidityProof` | ZK proof for account existence | | ||
| | `account_meta` | `CompressedAccountMeta` | Current account metadata | | ||
| | `current_account` / `current_message` | varies | Current account state for verification | | ||
| | `new_message` | `String` | Updated message content | | ||
|
|
||
| Calls `LightAccount::new_mut()` with current state, modifies message, invokes CPI. | ||
|
|
||
| ### close_account (discriminator: 1) | ||
|
|
||
| | Parameter | Type | Description | | ||
| |-----------|------|-------------| | ||
| | `proof` | `ValidityProof` | ZK proof for account existence | | ||
| | `account_meta` | `CompressedAccountMeta` | Current account metadata | | ||
| | `current_message` | `String` | Current message for verification | | ||
|
|
||
| Calls `LightAccount::new_close()` - clears data but preserves address for reinitialization. | ||
|
|
||
| ### reinit_account (discriminator: 2) | ||
|
|
||
| | Parameter | Type | Description | | ||
| |-----------|------|-------------| | ||
| | `proof` | `ValidityProof` | ZK proof for empty account at address | | ||
| | `account_meta` | `CompressedAccountMeta` | Account metadata | | ||
|
|
||
| Calls `LightAccount::new_empty()` to reinitialize previously closed account. | ||
|
|
||
| ### burn_account (discriminator: 1) | ||
|
|
||
| | Parameter | Type | Description | | ||
| |-----------|------|-------------| | ||
| | `proof` | `ValidityProof` | ZK proof for account existence | | ||
| | `account_meta` | `CompressedAccountMetaBurn` | Account metadata (burn-specific) | | ||
| | `current_message` / `current_account` | varies | Current state for verification | | ||
|
|
||
| Calls `LightAccount::new_burn()` - permanently deletes account. Address cannot be reused. | ||
|
|
||
| ## Security | ||
|
|
||
| - Address tree validation: Checks `address_tree_pubkey.to_bytes() == ADDRESS_TREE_V2` | ||
| - Program ID verification (native only): `program_id != &ID` returns `IncorrectProgramId` | ||
| - Signer required: First account must be mutable signer | ||
| - State verification: Close/update/burn require current state to match on-chain data | ||
|
|
||
| ## Errors | ||
|
|
||
| | Error | Source | Condition | | ||
| |-------|--------|-----------| | ||
| | `ProgramError::IncorrectProgramId` | Native entrypoint | Program ID mismatch | | ||
| | `ProgramError::InvalidInstructionData` | Entrypoint | Empty or malformed instruction data | | ||
| | `ProgramError::NotEnoughAccountKeys` | All | Missing required accounts | | ||
| | `ProgramError::InvalidAccountData` | All | Invalid address tree | | ||
| | `LightSdkError::Borsh` | Native | Instruction data deserialization failure | | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| # Counter Program | ||
|
|
||
| A counter program that stores state in compressed accounts. Three implementations: Anchor, Native (solana_program), and Pinocchio. | ||
|
|
||
| ## Summary | ||
|
|
||
| - Compressed PDA with address derived from `["counter", signer]` seeds | ||
| - `LightAccount` lifecycle: `new_init()` for create, `new_mut()` for update, `new_close()` for delete | ||
| - Owner field hashed with Poseidon via `#[hash]` attribute for account hash verification | ||
| - Address tree validation enforces `ADDRESS_TREE_V2` | ||
| - Closed addresses cannot be reused | ||
|
|
||
| ## [anchor/README.md](anchor/README.md) | ||
|
|
||
| ## Source structure | ||
|
|
||
| ``` | ||
| counter/ | ||
| ├── anchor/programs/counter/src/lib.rs # Anchor implementation | ||
| ├── native/src/lib.rs # Native solana_program implementation | ||
| └── pinocchio/src/lib.rs # Pinocchio implementation | ||
| ``` | ||
|
|
||
| ## Accounts | ||
|
|
||
| ### CounterAccount (compressed PDA) | ||
|
|
||
| Discriminator: `LightDiscriminator` derive macro generates 8-byte discriminator from struct name hash. | ||
|
|
||
| | Field | Type | Hashing | Description | | ||
| |-------|------|---------|-------------| | ||
| | `owner` | `Pubkey` | Poseidon (`#[hash]`) | Counter owner, included in account hash. | | ||
| | `value` | `u64` | None | Counter value, Borsh-serialized only. | | ||
|
|
||
| **Address derivation:** | ||
|
|
||
| ```rust | ||
| derive_address(&[b"counter", signer.key().as_ref()], &address_tree_pubkey, &program_id) | ||
| ``` | ||
|
|
||
| ## Instructions | ||
|
|
||
| | Discriminator | Enum Variant | Accounts | Logic | | ||
| |---------------|--------------|----------|-------| | ||
| | 0 | `CreateCounter` | signer (mut), remaining_accounts | Validates `address_tree_pubkey == ADDRESS_TREE_V2`. Derives address from seeds. Calls `LightAccount::new_init()`, sets owner to signer, value to 0. | | ||
| | 1 | `IncrementCounter` | signer (mut), remaining_accounts | Calls `LightAccount::new_mut()` with current state. Executes `checked_add(1)`. Invokes Light System Program. | | ||
| | 2 | `DecrementCounter` | signer (mut), remaining_accounts | Calls `LightAccount::new_mut()` with current state. Executes `checked_sub(1)`. Invokes Light System Program. | | ||
| | 3 | `ResetCounter` | signer (mut), remaining_accounts | Calls `LightAccount::new_mut()` with current state. Sets value to 0. Invokes Light System Program. | | ||
| | 4 | `CloseCounter` | signer (mut), remaining_accounts | Calls `LightAccount::new_close()` (input state only, no output). Address cannot be reused. | | ||
|
|
||
| ### Instruction data structs | ||
|
|
||
| | Struct | Fields | | ||
| |--------|--------| | ||
| | `CreateCounterInstructionData` | `proof`, `address_tree_info`, `output_state_tree_index` | | ||
| | `IncrementCounterInstructionData` | `proof`, `counter_value`, `account_meta` | | ||
| | `DecrementCounterInstructionData` | `proof`, `counter_value`, `account_meta` | | ||
| | `ResetCounterInstructionData` | `proof`, `counter_value`, `account_meta` | | ||
| | `CloseCounterInstructionData` | `proof`, `counter_value`, `account_meta` | | ||
|
|
||
| ## Security | ||
|
|
||
| | Check | Location | Description | | ||
| |-------|----------|-------------| | ||
| | Address tree validation | `create_counter` | Rejects if `address_tree_pubkey != ADDRESS_TREE_V2`. | | ||
| | Overflow protection | `increment_counter` | Uses `checked_add(1)`. | | ||
| | Underflow protection | `decrement_counter` | Uses `checked_sub(1)`. | | ||
| | Owner binding | All mutations | Owner reconstructed from signer, included in account hash verification. | | ||
| | Program ID check | Native/Pinocchio | Validates `program_id == ID` at entry. | | ||
|
|
||
| ## Errors | ||
|
|
||
| | Code | Name | Description | | ||
| |------|------|-------------| | ||
| | 1 | `Unauthorized` | No authority to perform action. | | ||
| | 2 | `Overflow` | Counter increment would overflow u64. | | ||
| | 3 | `Underflow` | Counter decrement would underflow below 0. | |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,77 @@ | ||||||||||
| # Create and Update | ||||||||||
|
|
||||||||||
| Demonstrates compressed account lifecycle operations: creation, updates, and atomic multi-account operations in single instructions. | ||||||||||
|
|
||||||||||
| ## Summary | ||||||||||
|
|
||||||||||
| - Create compressed accounts with derived addresses using `LightAccount::new_init()` | ||||||||||
| - Update existing accounts via `LightAccount::new_mut()` which validates input state hash | ||||||||||
| - Execute atomic multi-account operations in single CPI calls (create+update, update+update, create+create) | ||||||||||
| - Addresses derived from `[seed, signer.key()]` with program ID and address tree | ||||||||||
|
|
||||||||||
| ## [README](README.md) | ||||||||||
|
|
||||||||||
| ## Source structure | ||||||||||
|
|
||||||||||
| ``` | ||||||||||
| programs/create-and-update/src/ | ||||||||||
| └── lib.rs # Program entry, instructions, accounts, state structs | ||||||||||
| ``` | ||||||||||
|
Comment on lines
+16
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add language identifier to fenced code block. The code block is missing a language identifier, which helps with syntax highlighting and linting compliance. 📝 Proposed fix-```
+```text
programs/create-and-update/src/
└── lib.rs # Program entry, instructions, accounts, state structs
-```
+```📝 Committable suggestion
Suggested change
🧰 Tools🪛 markdownlint-cli2 (0.18.1)16-16: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| ## Accounts | ||||||||||
|
|
||||||||||
| ### Anchor accounts | ||||||||||
|
|
||||||||||
| | Account | Type | Description | | ||||||||||
| |---------|------|-------------| | ||||||||||
| | `signer` | `Signer` | Transaction fee payer and account owner. Marked `mut`. | | ||||||||||
|
|
||||||||||
| ### Compressed account state | ||||||||||
|
|
||||||||||
| | Struct | Fields | Discriminator | | ||||||||||
| |--------|--------|---------------| | ||||||||||
| | `DataAccount` | `owner: Pubkey`, `message: String` | 8-byte hash of "DataAccount" via `LightDiscriminator` | | ||||||||||
| | `ByteDataAccount` | `owner: Pubkey`, `data: [u8; 31]` | 8-byte hash of "ByteDataAccount" via `LightDiscriminator` | | ||||||||||
|
|
||||||||||
| ### PDAs and address derivation | ||||||||||
|
|
||||||||||
| | Address | Seeds | Description | | ||||||||||
| |---------|-------|-------------| | ||||||||||
| | First address | `[FIRST_SEED, signer.key()]` | Derived via `derive_address` with program ID and address tree. | | ||||||||||
| | Second address | `[SECOND_SEED, signer.key()]` | Derived via `derive_address` with program ID and address tree. | | ||||||||||
|
|
||||||||||
| Constants: | ||||||||||
| - `FIRST_SEED`: `b"first"` | ||||||||||
| - `SECOND_SEED`: `b"second"` | ||||||||||
| - `LIGHT_CPI_SIGNER`: Derived via `derive_light_cpi_signer!` macro from program ID | ||||||||||
|
|
||||||||||
| ### Instruction data structs | ||||||||||
|
|
||||||||||
| | Struct | Fields | Used by | | ||||||||||
| |--------|--------|---------| | ||||||||||
| | `ExistingCompressedAccountIxData` | `account_meta: CompressedAccountMeta`, `message: String`, `update_message: String` | `create_and_update`, `update_two_accounts` | | ||||||||||
| | `NewCompressedAccountIxData` | `address_tree_info: PackedAddressTreeInfo`, `message: String` | `create_and_update` | | ||||||||||
|
|
||||||||||
| ## Instructions | ||||||||||
|
|
||||||||||
| | Discriminator | Instruction | Accounts | Parameters | Logic | | ||||||||||
| |---------------|-------------|----------|------------|-------| | ||||||||||
| | sighash("create_compressed_account") | `create_compressed_account` | `GenericAnchorAccounts` + remaining accounts | `proof`, `address_tree_info`, `output_state_tree_index`, `message` | Validates address tree is ADDRESS_TREE_V2. Derives address from FIRST_SEED + signer. Creates `DataAccount` via `LightAccount::new_init()`. Invokes Light System Program CPI. | | ||||||||||
| | sighash("create_and_update") | `create_and_update` | `GenericAnchorAccounts` + remaining accounts | `proof`, `existing_account`, `new_account` | Creates new `DataAccount` at SECOND_SEED. Updates existing account via `LightAccount::new_mut()` (validates current state hash). Single CPI with both operations. | | ||||||||||
| | sighash("update_two_accounts") | `update_two_accounts` | `GenericAnchorAccounts` + remaining accounts | `proof`, `first_account`, `second_account` | Updates two existing `DataAccount` messages atomically via `LightAccount::new_mut()`. Single CPI call. | | ||||||||||
| | sighash("create_two_accounts") | `create_two_accounts` | `GenericAnchorAccounts` + remaining accounts | `proof`, `address_tree_info`, `output_state_tree_index`, `byte_data`, `message` | Creates `ByteDataAccount` at FIRST_SEED and `DataAccount` at SECOND_SEED in single CPI. | | ||||||||||
|
|
||||||||||
| ## Security | ||||||||||
|
|
||||||||||
| | Check | Location | Description | | ||||||||||
| |-------|----------|-------------| | ||||||||||
| | Address tree validation | `lib.rs:49-52`, `lib.rs:97-100`, `lib.rs:218-221` | Verifies `address_tree_pubkey` matches `ADDRESS_TREE_V2`. | | ||||||||||
| | Signer authorization | Anchor `#[account(mut)]` | Signer must sign transaction and pay fees. | | ||||||||||
| | CPI signer derivation | `lib.rs:17-18` | `LIGHT_CPI_SIGNER` derived from program ID via `derive_light_cpi_signer!` macro. | | ||||||||||
|
|
||||||||||
| ## Errors | ||||||||||
|
|
||||||||||
| | Error | Source | Cause | | ||||||||||
| |-------|--------|-------| | ||||||||||
| | `AccountNotEnoughKeys` | `ErrorCode::AccountNotEnoughKeys` | Address tree pubkey cannot be retrieved from remaining accounts. | | ||||||||||
| | `InvalidAccountData` | `ProgramError::InvalidAccountData` | Address tree pubkey does not match `ADDRESS_TREE_V2`. | | ||||||||||
Uh oh!
There was an error while loading. Please reload this page.