Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions account-comparison/CLAUDE.md
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 |
28 changes: 28 additions & 0 deletions airdrop-implementations/README.md
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)
117 changes: 117 additions & 0 deletions basic-operations/CLAUDE.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 |
77 changes: 77 additions & 0 deletions counter/CLAUDE.md
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. |
77 changes: 77 additions & 0 deletions create-and-update/CLAUDE.md
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
programs/create-and-update/src/
└── lib.rs # Program entry, instructions, accounts, state structs
```
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

16-16: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In @create-and-update/CLAUDE.md around lines 16 - 19, The fenced code block in
CLAUDE.md is missing a language identifier; update the opening fence for the
snippet that shows the directory tree (the block starting with
"programs/create-and-update/src/") to include a language tag such as "text"
(i.e., change ``` to ```text) so the code block is properly labeled for syntax
highlighting and linting.


## 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`. |
Loading