Skip to content

Commit dec6aff

Browse files
author
tilo-14
committed
feat: add CLAUDE.md documentation for program examples
1 parent 5074f36 commit dec6aff

File tree

7 files changed

+571
-0
lines changed

7 files changed

+571
-0
lines changed

account-comparison/CLAUDE.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Account Comparison
2+
3+
Side-by-side implementation of Solana accounts vs compressed accounts. Shows equivalent create and update operations for both account types using identical data structures.
4+
5+
## Summary
6+
7+
- Demonstrates equivalent PDA and compressed account patterns with identical seeds `["account", user.key()]`
8+
- Compressed accounts use `LightAccount::new_init` for creation and `LightAccount::new_mut` for updates
9+
- Updates require client to pass existing state because on-chain storage is a Poseidon hash
10+
- All fields marked `#[hash]` are included in Poseidon hash computation
11+
12+
## Source Structure
13+
14+
```
15+
programs/account-comparison/src/
16+
lib.rs # Program entrypoint, instructions, account structs
17+
programs/account-comparison/tests/
18+
test_solana_account.rs # LiteSVM tests for standard accounts
19+
test_compressed_account.rs # Light Protocol tests for compressed accounts
20+
```
21+
22+
## Accounts
23+
24+
### AccountData (Solana PDA)
25+
26+
| Field | Type | Size |
27+
|-------|------|------|
28+
| discriminator | `[u8; 8]` | 8 bytes |
29+
| user | `Pubkey` | 32 bytes |
30+
| name | `String` | 4 + name_len (max 60 chars) |
31+
| data | `[u8; 128]` | 128 bytes |
32+
33+
- **Seeds**: `["account", user.key()]`
34+
- **Discriminator**: 8 bytes, SHA256("account:AccountData")[0..8]
35+
- **Space**: 232 bytes. String uses Borsh serialization (4-byte length prefix + variable content).
36+
37+
### CompressedAccountData (LightAccount)
38+
39+
```rust
40+
#[derive(LightDiscriminator, LightHasher)]
41+
pub struct CompressedAccountData {
42+
#[hash] pub user: Pubkey,
43+
#[hash] pub name: String,
44+
#[hash] pub data: [u8; 128],
45+
}
46+
```
47+
48+
- **Address seeds**: `["account", user.key()]`
49+
- **Discriminator**: `LightDiscriminator` derive generates from struct name
50+
- **Hashing**: Poseidon hash of all `#[hash]` fields (user, name, data)
51+
52+
### CPI Signer
53+
54+
```rust
55+
const CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("FYX4GmKJYzSiycc7XZKf12NGXNE9siSx1cJubYJniHcv");
56+
```
57+
58+
## Instructions
59+
60+
| # | Instruction | Accounts | Parameters | Logic |
61+
|---|-------------|----------|------------|-------|
62+
| 0 | `create_account` | `user` (signer, mut), `account` (init PDA), `system_program` | `name: String` | Initializes PDA with seeds `["account", user]`, sets `data = [1u8; 128]` |
63+
| 1 | `update_data` | `user` (signer, mut), `account` (mut, has_one = user) | `data: [u8; 128]` | Overwrites account.data |
64+
| 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 |
65+
| 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 |
66+
67+
## Security
68+
69+
| Check | Location | Description |
70+
|-------|----------|-------------|
71+
| Address tree validation | `create_compressed_account` | Verifies `address_tree_pubkey.to_bytes() == ADDRESS_TREE_V2` |
72+
| Owner verification | `update_compressed_account` | Asserts `compressed_account.user == ctx.accounts.user.key()` |
73+
| PDA constraint | `update_data` | Anchor `has_one = user` constraint |
74+
| Signer requirement | All instructions | User must sign transaction |
75+
76+
## Errors
77+
78+
| Error | Message |
79+
|-------|---------|
80+
| `CustomError::Unauthorized` | "No authority to perform this action" |
81+
| `ProgramError::InvalidAccountData` | Returned when address tree validation fails |

airdrop-implementations/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Airdrop Implementations
2+
3+
Simple Implementation: simple-claim - Distributes compressed tokens that get decompressed on claim
4+
5+
Advanced Implementation: distributor - Distributes SPL tokens, uses compressed PDAs to track claims
6+
7+
8+
## Quick comparison
9+
10+
| | simple-claim | distributor |
11+
|--|------------------|-------------|
12+
| Time-lock | Slot-based (all-or-nothing) | Timestamp-based (linear vesting) |
13+
| Partial claims | No | Yes |
14+
| Clawback | No | Yes |
15+
| Admin controls | No | Yes |
16+
17+
## Cost (10,000 recipients)
18+
19+
| | simple-claim | distributor | Regular |
20+
|--|----------------:|------------:|----------------:|
21+
| Setup | ~0.03 SOL | ~0.002 SOL | ~0.002 SOL |
22+
| Claim tracking | 0 | ~0.03 SOL | ~6 SOL |
23+
| **Total** | **~0.03 SOL** | **~0.03 SOL** | **~6 SOL** |
24+
25+
## Getting started
26+
27+
- [simple-claim README](./simple-claim/README.md)
28+
- [distributor README](./distributor/README.md)

basic-operations/CLAUDE.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Basic Operations Examples
2+
3+
Example programs showing all basic operations for compressed accounts.
4+
5+
## Summary
6+
7+
- Demonstrates create, update, close, reinit, and burn operations
8+
- Anchor uses Poseidon hashing (`light_sdk::account::LightAccount`); Native uses SHA-256 (`light_sdk::account::sha::LightAccount`)
9+
- All programs derive addresses from seeds `[b"message", signer.key.as_ref()]` + `ADDRESS_TREE_V2` + program ID
10+
- Close preserves address for reinitialization; burn permanently deletes address
11+
12+
## [Anchor README](anchor/README.md) | [Native README](native/README.md)
13+
14+
## Source structure
15+
16+
```
17+
basic-operations/
18+
├── anchor/ # Anchor framework (Poseidon hashing)
19+
│ ├── create/programs/create/src/lib.rs
20+
│ ├── update/programs/update/src/lib.rs
21+
│ ├── close/programs/close/src/lib.rs
22+
│ ├── reinit/programs/reinit/src/lib.rs
23+
│ └── burn/programs/burn/src/lib.rs
24+
└── native/ # Native Solana (SHA-256 hashing)
25+
└── programs/
26+
├── create/src/lib.rs
27+
├── update/src/lib.rs
28+
├── close/src/lib.rs
29+
├── reinit/src/lib.rs
30+
└── burn/src/lib.rs
31+
```
32+
33+
## Accounts
34+
35+
### MyCompressedAccount
36+
37+
Shared account structure across all examples. Derives `LightDiscriminator` for compressed account type identification.
38+
39+
```rust
40+
#[derive(LightDiscriminator)]
41+
pub struct MyCompressedAccount {
42+
pub owner: Pubkey, // Account owner
43+
pub message: String, // User-defined message
44+
}
45+
```
46+
47+
## Instructions
48+
49+
Native programs use `InstructionType` enum discriminators (first byte of instruction data).
50+
51+
### create_account (discriminator: 0)
52+
53+
| Parameter | Type | Description |
54+
|-----------|------|-------------|
55+
| `proof` | `ValidityProof` | ZK proof for address non-existence |
56+
| `address_tree_info` | `PackedAddressTreeInfo` | Address tree metadata |
57+
| `output_state_tree_index` | `u8` | Target state tree index |
58+
| `message` | `String` | Initial message content |
59+
60+
Calls `LightAccount::new_init()` with derived address, invokes `LightSystemProgramCpi` with `with_new_addresses()`.
61+
62+
### update_account (discriminator: 1)
63+
64+
| Parameter | Type | Description |
65+
|-----------|------|-------------|
66+
| `proof` | `ValidityProof` | ZK proof for account existence |
67+
| `account_meta` | `CompressedAccountMeta` | Current account metadata |
68+
| `current_account` / `current_message` | varies | Current account state for verification |
69+
| `new_message` | `String` | Updated message content |
70+
71+
Calls `LightAccount::new_mut()` with current state, modifies message, invokes CPI.
72+
73+
### close_account (discriminator: 1)
74+
75+
| Parameter | Type | Description |
76+
|-----------|------|-------------|
77+
| `proof` | `ValidityProof` | ZK proof for account existence |
78+
| `account_meta` | `CompressedAccountMeta` | Current account metadata |
79+
| `current_message` | `String` | Current message for verification |
80+
81+
Calls `LightAccount::new_close()` - clears data but preserves address for reinitialization.
82+
83+
### reinit_account (discriminator: 2)
84+
85+
| Parameter | Type | Description |
86+
|-----------|------|-------------|
87+
| `proof` | `ValidityProof` | ZK proof for empty account at address |
88+
| `account_meta` | `CompressedAccountMeta` | Account metadata |
89+
90+
Calls `LightAccount::new_empty()` to reinitialize previously closed account.
91+
92+
### burn_account (discriminator: 1)
93+
94+
| Parameter | Type | Description |
95+
|-----------|------|-------------|
96+
| `proof` | `ValidityProof` | ZK proof for account existence |
97+
| `account_meta` | `CompressedAccountMetaBurn` | Account metadata (burn-specific) |
98+
| `current_message` / `current_account` | varies | Current state for verification |
99+
100+
Calls `LightAccount::new_burn()` - permanently deletes account. Address cannot be reused.
101+
102+
## Security
103+
104+
- Address tree validation: Checks `address_tree_pubkey.to_bytes() == ADDRESS_TREE_V2`
105+
- Program ID verification (native only): `program_id != &ID` returns `IncorrectProgramId`
106+
- Signer required: First account must be mutable signer
107+
- State verification: Close/update/burn require current state to match on-chain data
108+
109+
## Errors
110+
111+
| Error | Source | Condition |
112+
|-------|--------|-----------|
113+
| `ProgramError::IncorrectProgramId` | Native entrypoint | Program ID mismatch |
114+
| `ProgramError::InvalidInstructionData` | Entrypoint | Empty or malformed instruction data |
115+
| `ProgramError::NotEnoughAccountKeys` | All | Missing required accounts |
116+
| `ProgramError::InvalidAccountData` | All | Invalid address tree |
117+
| `LightSdkError::Borsh` | Native | Instruction data deserialization failure |

counter/CLAUDE.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Counter Program
2+
3+
A counter program that stores state in compressed accounts. Three implementations: Anchor, Native (solana_program), and Pinocchio.
4+
5+
## Summary
6+
7+
- Compressed PDA with address derived from `["counter", signer]` seeds
8+
- `LightAccount` lifecycle: `new_init()` for create, `new_mut()` for update, `new_close()` for delete
9+
- Owner field hashed with Poseidon via `#[hash]` attribute for account hash verification
10+
- Address tree validation enforces `ADDRESS_TREE_V2`
11+
- Closed addresses cannot be reused
12+
13+
## [anchor/README.md](anchor/README.md)
14+
15+
## Source structure
16+
17+
```
18+
counter/
19+
├── anchor/programs/counter/src/lib.rs # Anchor implementation
20+
├── native/src/lib.rs # Native solana_program implementation
21+
└── pinocchio/src/lib.rs # Pinocchio implementation
22+
```
23+
24+
## Accounts
25+
26+
### CounterAccount (compressed PDA)
27+
28+
Discriminator: `LightDiscriminator` derive macro generates 8-byte discriminator from struct name hash.
29+
30+
| Field | Type | Hashing | Description |
31+
|-------|------|---------|-------------|
32+
| `owner` | `Pubkey` | Poseidon (`#[hash]`) | Counter owner, included in account hash. |
33+
| `value` | `u64` | None | Counter value, Borsh-serialized only. |
34+
35+
**Address derivation:**
36+
37+
```rust
38+
derive_address(&[b"counter", signer.key().as_ref()], &address_tree_pubkey, &program_id)
39+
```
40+
41+
## Instructions
42+
43+
| Discriminator | Enum Variant | Accounts | Logic |
44+
|---------------|--------------|----------|-------|
45+
| 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. |
46+
| 1 | `IncrementCounter` | signer (mut), remaining_accounts | Calls `LightAccount::new_mut()` with current state. Executes `checked_add(1)`. Invokes Light System Program. |
47+
| 2 | `DecrementCounter` | signer (mut), remaining_accounts | Calls `LightAccount::new_mut()` with current state. Executes `checked_sub(1)`. Invokes Light System Program. |
48+
| 3 | `ResetCounter` | signer (mut), remaining_accounts | Calls `LightAccount::new_mut()` with current state. Sets value to 0. Invokes Light System Program. |
49+
| 4 | `CloseCounter` | signer (mut), remaining_accounts | Calls `LightAccount::new_close()` (input state only, no output). Address cannot be reused. |
50+
51+
### Instruction data structs
52+
53+
| Struct | Fields |
54+
|--------|--------|
55+
| `CreateCounterInstructionData` | `proof`, `address_tree_info`, `output_state_tree_index` |
56+
| `IncrementCounterInstructionData` | `proof`, `counter_value`, `account_meta` |
57+
| `DecrementCounterInstructionData` | `proof`, `counter_value`, `account_meta` |
58+
| `ResetCounterInstructionData` | `proof`, `counter_value`, `account_meta` |
59+
| `CloseCounterInstructionData` | `proof`, `counter_value`, `account_meta` |
60+
61+
## Security
62+
63+
| Check | Location | Description |
64+
|-------|----------|-------------|
65+
| Address tree validation | `create_counter` | Rejects if `address_tree_pubkey != ADDRESS_TREE_V2`. |
66+
| Overflow protection | `increment_counter` | Uses `checked_add(1)`. |
67+
| Underflow protection | `decrement_counter` | Uses `checked_sub(1)`. |
68+
| Owner binding | All mutations | Owner reconstructed from signer, included in account hash verification. |
69+
| Program ID check | Native/Pinocchio | Validates `program_id == ID` at entry. |
70+
71+
## Errors
72+
73+
| Code | Name | Description |
74+
|------|------|-------------|
75+
| 1 | `Unauthorized` | No authority to perform action. |
76+
| 2 | `Overflow` | Counter increment would overflow u64. |
77+
| 3 | `Underflow` | Counter decrement would underflow below 0. |

create-and-update/CLAUDE.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Create and Update
2+
3+
Demonstrates compressed account lifecycle operations: creation, updates, and atomic multi-account operations in single instructions.
4+
5+
## Summary
6+
7+
- Create compressed accounts with derived addresses using `LightAccount::new_init()`
8+
- Update existing accounts via `LightAccount::new_mut()` which validates input state hash
9+
- Execute atomic multi-account operations in single CPI calls (create+update, update+update, create+create)
10+
- Addresses derived from `[seed, signer.key()]` with program ID and address tree
11+
12+
## [README](README.md)
13+
14+
## Source structure
15+
16+
```
17+
programs/create-and-update/src/
18+
└── lib.rs # Program entry, instructions, accounts, state structs
19+
```
20+
21+
## Accounts
22+
23+
### Anchor accounts
24+
25+
| Account | Type | Description |
26+
|---------|------|-------------|
27+
| `signer` | `Signer` | Transaction fee payer and account owner. Marked `mut`. |
28+
29+
### Compressed account state
30+
31+
| Struct | Fields | Discriminator |
32+
|--------|--------|---------------|
33+
| `DataAccount` | `owner: Pubkey`, `message: String` | 8-byte hash of "DataAccount" via `LightDiscriminator` |
34+
| `ByteDataAccount` | `owner: Pubkey`, `data: [u8; 31]` | 8-byte hash of "ByteDataAccount" via `LightDiscriminator` |
35+
36+
### PDAs and address derivation
37+
38+
| Address | Seeds | Description |
39+
|---------|-------|-------------|
40+
| First address | `[FIRST_SEED, signer.key()]` | Derived via `derive_address` with program ID and address tree. |
41+
| Second address | `[SECOND_SEED, signer.key()]` | Derived via `derive_address` with program ID and address tree. |
42+
43+
Constants:
44+
- `FIRST_SEED`: `b"first"`
45+
- `SECOND_SEED`: `b"second"`
46+
- `LIGHT_CPI_SIGNER`: Derived via `derive_light_cpi_signer!` macro from program ID
47+
48+
### Instruction data structs
49+
50+
| Struct | Fields | Used by |
51+
|--------|--------|---------|
52+
| `ExistingCompressedAccountIxData` | `account_meta: CompressedAccountMeta`, `message: String`, `update_message: String` | `create_and_update`, `update_two_accounts` |
53+
| `NewCompressedAccountIxData` | `address_tree_info: PackedAddressTreeInfo`, `message: String` | `create_and_update` |
54+
55+
## Instructions
56+
57+
| Discriminator | Instruction | Accounts | Parameters | Logic |
58+
|---------------|-------------|----------|------------|-------|
59+
| 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. |
60+
| 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. |
61+
| 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. |
62+
| 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. |
63+
64+
## Security
65+
66+
| Check | Location | Description |
67+
|-------|----------|-------------|
68+
| Address tree validation | `lib.rs:49-52`, `lib.rs:97-100`, `lib.rs:218-221` | Verifies `address_tree_pubkey` matches `ADDRESS_TREE_V2`. |
69+
| Signer authorization | Anchor `#[account(mut)]` | Signer must sign transaction and pay fees. |
70+
| CPI signer derivation | `lib.rs:17-18` | `LIGHT_CPI_SIGNER` derived from program ID via `derive_light_cpi_signer!` macro. |
71+
72+
## Errors
73+
74+
| Error | Source | Cause |
75+
|-------|--------|-------|
76+
| `AccountNotEnoughKeys` | `ErrorCode::AccountNotEnoughKeys` | Address tree pubkey cannot be retrieved from remaining accounts. |
77+
| `InvalidAccountData` | `ProgramError::InvalidAccountData` | Address tree pubkey does not match `ADDRESS_TREE_V2`. |

0 commit comments

Comments
 (0)