From 5c7def8213c84e8ae1e7fde7743914f43c876d7a Mon Sep 17 00:00:00 2001 From: ananas Date: Fri, 23 Jan 2026 15:11:34 +0000 Subject: [PATCH] feat: first draft --- programs/system/CLAUDE.md | 233 ++++++++ programs/system/docs/ACCOUNTS.md | 511 ++++++++++++++++++ programs/system/docs/CLAUDE.md | 41 ++ programs/system/docs/CPI_CONTEXT.md | 264 +++++++++ programs/system/docs/INSTRUCTIONS.md | 150 +++++ programs/system/docs/PROCESSING_PIPELINE.md | 459 ++++++++++++++++ .../docs/init/INIT_CPI_CONTEXT_ACCOUNT.md | 108 ++++ .../docs/init/REINIT_CPI_CONTEXT_ACCOUNT.md | 150 +++++ programs/system/docs/invoke/INVOKE.md | 192 +++++++ programs/system/docs/invoke_cpi/INVOKE_CPI.md | 198 +++++++ .../INVOKE_CPI_WITH_ACCOUNT_INFO.md | 251 +++++++++ .../invoke_cpi/INVOKE_CPI_WITH_READ_ONLY.md | 199 +++++++ 12 files changed, 2756 insertions(+) create mode 100644 programs/system/CLAUDE.md create mode 100644 programs/system/docs/ACCOUNTS.md create mode 100644 programs/system/docs/CLAUDE.md create mode 100644 programs/system/docs/CPI_CONTEXT.md create mode 100644 programs/system/docs/INSTRUCTIONS.md create mode 100644 programs/system/docs/PROCESSING_PIPELINE.md create mode 100644 programs/system/docs/init/INIT_CPI_CONTEXT_ACCOUNT.md create mode 100644 programs/system/docs/init/REINIT_CPI_CONTEXT_ACCOUNT.md create mode 100644 programs/system/docs/invoke/INVOKE.md create mode 100644 programs/system/docs/invoke_cpi/INVOKE_CPI.md create mode 100644 programs/system/docs/invoke_cpi/INVOKE_CPI_WITH_ACCOUNT_INFO.md create mode 100644 programs/system/docs/invoke_cpi/INVOKE_CPI_WITH_READ_ONLY.md diff --git a/programs/system/CLAUDE.md b/programs/system/CLAUDE.md new file mode 100644 index 0000000000..ec3552fbd0 --- /dev/null +++ b/programs/system/CLAUDE.md @@ -0,0 +1,233 @@ +# Light System Program + +## Summary + +- Core validation and coordination layer for Light Protocol +- Verifies ZK proofs for compressed account state transitions +- Manages CPI context for multi-program transactions +- Coordinates with account-compression program via CPI +- Handles SOL compression/decompression + +## Used In + +The Light System Program is invoked by: + +- **Compressed Token Program** - All compressed token operations (mint, transfer, burn) invoke this program via CPI +- **Custom Anchor Programs** - Programs using Light SDK invoke this for compressed PDA operations via `InvokeCpi` or `InvokeCpiWithAccountInfo` +- **Direct Clients** - For simple compressed SOL transfers using the `Invoke` instruction +- **Multi-Program Transactions** - Any transaction requiring multiple programs to coordinate via shared CPI context + +**Example transaction flow:** +1. Program A calls `InvokeCpiWithAccountInfo` with `first_set_context=true` to write to CPI context +2. Program B calls `InvokeCpiWithAccountInfo` with `set_context=true` to append additional data +3. Program C calls `InvokeCpiWithAccountInfo` with `execute=true` to execute the combined state transition with a single ZK proof + +## Documentation + +**Navigation Guide:** [docs/CLAUDE.md](docs/CLAUDE.md) + +**Core Concepts:** +- [docs/PROCESSING_PIPELINE.md](docs/PROCESSING_PIPELINE.md) - 19-step processing flow (the heart of the program) +- [docs/CPI_CONTEXT.md](docs/CPI_CONTEXT.md) - Multi-program transaction coordination +- [docs/ACCOUNTS.md](docs/ACCOUNTS.md) - CpiContextAccount layouts and structures +- [docs/INSTRUCTIONS.md](docs/INSTRUCTIONS.md) - Instruction discriminators and error codes + +**Instruction Details:** +- [docs/init/](docs/init/) - CPI context account initialization +- [docs/invoke/](docs/invoke/) - Direct invocation +- [docs/invoke_cpi/](docs/invoke_cpi/) - CPI invocation modes + +## Key Sections + +### Accounts + +**CpiContextAccount (Version 2):** +- Stores instruction data across multiple CPI invocations +- Enables multi-program transactions with single ZK proof +- Default capacity: 14020 bytes (configurable via initialization parameters) +- Associated with a specific state Merkle tree + +**See:** [docs/ACCOUNTS.md](docs/ACCOUNTS.md) + +### Instructions + +**CPI Context Management (2):** +- `InitializeCpiContextAccount` - Create new CPI context account +- `ReInitCpiContextAccount` - Migrate from version 1 to version 2 + +**Direct Invocation (1):** +- `Invoke` - Process compressed accounts for single program (no CPI) + +**CPI Invocation (3):** +- `InvokeCpi` - Standard CPI invocation (Anchor mode) +- `InvokeCpiWithReadOnly` - CPI with read-only account support +- `InvokeCpiWithAccountInfo` - CPI with dynamic account configuration (V2 mode) + +**See:** [docs/INSTRUCTIONS.md](docs/INSTRUCTIONS.md) for complete list with discriminators + +### Source Code Structure + +``` +programs/system/src/ +├── lib.rs # [ENTRY] Instruction dispatch, process_instruction() +├── constants.rs # Discriminators, program ID +├── errors.rs # Error definitions (6000-6066) +├── context.rs # Wrapped instruction data context +├── utils.rs # Helper functions +├── accounts/ +│ ├── mod.rs # Account traits and exports +│ ├── init_context_account.rs # CPI context account initialization +│ ├── account_checks.rs # Account validation helpers +│ ├── account_traits.rs # Traits for account access +│ ├── mode.rs # AccountMode (Anchor/V2) +│ └── remaining_account_checks.rs # Remaining account validation +├── invoke/ +│ ├── mod.rs +│ ├── instruction.rs # InvokeInstruction accounts +│ └── verify_signer.rs # Authority signature check +├── invoke_cpi/ +│ ├── mod.rs +│ ├── instruction.rs # InvokeCpiInstruction (Anchor mode) +│ ├── instruction_v2.rs # InvokeCpiInstructionV2 (V2 mode) +│ ├── processor.rs # CPI invocation processing +│ └── verify_signer.rs # CPI signer check (PDA derivation) +├── processor/ +│ ├── mod.rs +│ ├── process.rs # [CORE] Main processing pipeline (19 steps) +│ ├── cpi.rs # CPI to account-compression program +│ ├── verify_proof.rs # ZK proof verification +│ ├── sum_check.rs # Lamport conservation check +│ ├── sol_compression.rs # SOL compress/decompress +│ ├── read_only_account.rs # Read-only account verification +│ ├── read_only_address.rs # Read-only address verification +│ ├── create_address_cpi_data.rs # Address derivation and CPI data +│ ├── create_inputs_cpi_data.rs # Input account processing +│ └── create_outputs_cpi_data.rs # Output account processing +├── cpi_context/ +│ ├── mod.rs +│ ├── state.rs # CpiContextAccount (V1 and V2) +│ ├── process_cpi_context.rs # CPI context processing logic +│ ├── account.rs # CpiContextInAccount, CpiContextOutAccount +│ ├── address.rs # CpiContextNewAddressParamsAssignedPacked +│ └── instruction_data_trait.rs # Trait for instruction data access +└── account_compression_state/ + ├── mod.rs + ├── state.rs # State Merkle tree wrappers + ├── address.rs # Address Merkle tree wrappers + └── queue.rs # Queue wrappers +``` + +## Key Features + +### 1. ZK Proof Verification +Verifies zero-knowledge proofs that validate: +- Input compressed account inclusion in state Merkle trees +- New address non-inclusion in address Merkle trees +- Read-only account inclusion +- Read-only address inclusion + +### 2. CPI Context Management +Enables multiple programs to share a single ZK proof: +- First program writes instruction data to CPI context account +- Additional programs append their data +- Final program executes with combined data and one proof +- Significant savings in compute units and instruction data size + +### 3. Multi-Mode Support +- **Invoke Mode:** Direct invocation for user-owned accounts +- **InvokeCpi Mode (Anchor):** CPI for program-owned accounts with Anchor-style account layout +- **V2 Mode:** CPI with dynamic account configuration - accounts passed via instruction data instead of fixed layout, reducing transaction size + +### 4. SOL Compression +- Compress: Transfer SOL from user account to Sol Pool PDA, create compressed account +- Decompress: Extract compressed SOL, transfer from Sol Pool PDA to recipient + +### 5. Read-Only Support +- Verify compressed accounts exist without modifying them +- Verify addresses exist without creating new ones +- Useful for authorization and multi-account validations + +## Processing Pipeline + +The 19-step processing flow (`src/processor/process.rs`) is the core of the program: + +1. **Allocate CPI Data** - Pre-allocate memory for account-compression CPI +2. **Deserialize Accounts** - Parse and validate Merkle tree accounts +3. **Process Addresses** - Derive new addresses, verify read-only addresses +4. **Process Outputs** - Hash output accounts, validate indices +5. **Process Inputs** - Hash input accounts, create transaction hash +6. **Sum Check** - Verify lamport conservation (inputs + compress = outputs + decompress) +7. **SOL Compress/Decompress** - Transfer SOL to/from Sol Pool PDA +8. **Verify Read-Only** - Verify read-only accounts by index +9. **Verify ZK Proof** - Validate zero-knowledge proof covering all inputs/outputs/addresses +10. **Transfer Fees** - Pay network, address, and rollover fees +11. **Copy CPI Context** - Copy outputs for indexing (when using CPI context) +12. **CPI Account Compression** - Execute state transition via CPI to account-compression program + +**See:** [docs/PROCESSING_PIPELINE.md](docs/PROCESSING_PIPELINE.md) for detailed step-by-step breakdown + +## Error Codes + +| Range | Category | +|-------|----------| +| 6000-6005 | Sum check and computation errors | +| 6006-6007 | Address errors | +| 6008-6012 | SOL compression/decompression errors | +| 6013-6019 | Validation errors | +| 6020-6028 | CPI context errors | +| 6029-6066 | Additional validation and processing errors | + +**See:** [docs/INSTRUCTIONS.md](docs/INSTRUCTIONS.md) for complete list + +## Testing + +**Integration tests:** `program-tests/system-test/` + +Tests are located in `program-tests/` because they depend on `light-test-utils` for instruction execution assertions and Solana runtime setup. + +```bash +# Run all system program tests +cargo test-sbf -p system-test + +# Run specific test with debugging (use long tail to see all Solana logs) +RUST_BACKTRACE=1 cargo test-sbf -p system-test -- --test test_name --nocapture 2>&1 | tail -500 +``` + +**SDK tests:** `sdk-tests/` + +```bash +# Native SDK tests +cargo test-sbf -p sdk-native-test + +# Anchor SDK tests +cargo test-sbf -p sdk-anchor-test + +# Token SDK tests +cargo test-sbf -p sdk-token-test +``` + +## Dependencies + +**Program Libraries:** +- `light-account-checks` - Account validation utilities +- `light-compressed-account` - Compressed account types and instruction data +- `light-batched-merkle-tree` - Batched Merkle tree operations +- `light-hasher` - Poseidon hashing +- `light-verifier` - ZK proof verification +- `light-zero-copy` - Zero-copy serialization + +**External:** +- `pinocchio` - Efficient Solana program framework +- `borsh` - Binary serialization (legacy CPI context V1) + +## Program ID + +``` +SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7 +``` + +## Related Programs + +- **Account Compression Program** - Owns and manages Merkle tree accounts +- **Compressed Token Program** - Uses this program for all token operations +- **Registry Program** - Forester access control and protocol configuration diff --git a/programs/system/docs/ACCOUNTS.md b/programs/system/docs/ACCOUNTS.md new file mode 100644 index 0000000000..e287e67414 --- /dev/null +++ b/programs/system/docs/ACCOUNTS.md @@ -0,0 +1,511 @@ +# Account Layouts + +## Overview + +The Light System Program owns one primary account type: **CpiContextAccount**. This account is used to collect instruction data across multiple CPI invocations before executing a combined state transition with a single ZK proof. + +## 1. CpiContextAccount (Version 2) + +### Description +The CpiContextAccount collects instruction data without executing a compressed transaction. It enables multi-program transactions by: +1. Caching validated instruction data from multiple CPI invocations +2. Combining all cached data with the final executing CPI +3. Executing the combined state transition with a single ZK proof + +### Discriminator +```rust +CPI_CONTEXT_ACCOUNT_2_DISCRIMINATOR: [u8; 8] = [34, 184, 183, 14, 100, 80, 183, 124] +``` + +### Ownership +- **Owner:** Light System Program (`SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7`) + +### State Layout + +**Source:** `programs/system/src/cpi_context/state.rs` + +```rust +pub struct ZCpiContextAccount2<'a> { + pub fee_payer: Ref<&'a mut [u8], Pubkey>, // 32 bytes - Transaction fee payer + pub associated_merkle_tree: Ref<&'a mut [u8], Pubkey>, // 32 bytes - Associated state Merkle tree + _associated_queue: Ref<&'a mut [u8], Pubkey>, // 32 bytes - Placeholder for future queue + _place_holder_bytes: Ref<&'a mut [u8], [u8; 32]>, // 32 bytes - Reserved + pub new_addresses: ZeroCopyVecU8<'a, CpiContextNewAddressParamsAssignedPacked>, // Variable + pub readonly_addresses: ZeroCopyVecU8<'a, ZPackedReadOnlyAddress>, // Variable + pub readonly_accounts: ZeroCopyVecU8<'a, ZPackedReadOnlyCompressedAccount>, // Variable + pub in_accounts: ZeroCopyVecU8<'a, CpiContextInAccount>, // Variable + pub out_accounts: ZeroCopyVecU8<'a, CpiContextOutAccount>, // Variable + total_output_data_len: Ref<&'a mut [u8], U16>, // 2 bytes - Total serialized output size + output_data_len: Ref<&'a mut [u8], U16>, // 2 bytes - Number of output data entries + pub output_data: Vec>, // Variable - Output account data + remaining_data: &'a mut [u8], // Remaining capacity +} +``` + +**Fixed Header Size:** 8 (discriminator) + 32 + 32 + 32 + 32 = 136 bytes + +**Note:** The `Ref<&'a mut [u8], T>` wrapper is used for zero-copy access to fixed-size fields. The lifetime parameter `'a` ensures the account data remains borrowed for the duration of the operation. + +### Initialization Parameters + +**Source:** `programs/system/src/cpi_context/state.rs` + +```rust +pub struct CpiContextAccountInitParams { + pub associated_merkle_tree: Pubkey, + pub associated_queue: Pubkey, // Currently placeholder (always Pubkey::default()) + pub new_addresses_len: u8, // Default: 10 - Pre-allocated capacity + pub readonly_addresses_len: u8, // Default: 10 - Pre-allocated capacity + pub readonly_accounts_len: u8, // Default: 10 - Pre-allocated capacity + pub in_accounts_len: u8, // Default: 20 - Pre-allocated capacity + pub out_accounts_len: u8, // Default: 30 - Pre-allocated capacity +} +``` + +**Capacity Planning:** +The length parameters specify pre-allocated capacity for each vector collection. This capacity determines the maximum number of items that can be stored without resizing. Choose values based on expected transaction complexity: +- Simple transactions: Use defaults (10/10/10/20/30) +- Complex multi-program transactions: Increase capacity as needed +- Each item has a fixed size (see Supporting Types section for exact sizes) + +**Default Account Size:** `DEFAULT_CPI_CONTEXT_ACCOUNT_SIZE_V2` = 14020 bytes + +**Source:** `program-libs/batched-merkle-tree/src/constants.rs` + +**Size Calculation Example with Defaults:** +``` +Fixed header: 136 bytes +Vector metadata (5 vectors × 2 bytes): 10 bytes +New addresses (10 × 70 bytes): 700 bytes +Readonly addresses (10 × 36 bytes): 360 bytes +Readonly accounts (10 × 48 bytes): 480 bytes +Input accounts (20 × 144 bytes): 2880 bytes +Output accounts (30 × 128 bytes): 3840 bytes +Output data metadata: 4 bytes +Remaining buffer for output data: ~5610 bytes +Total: 14020 bytes +``` + +**Note:** The remaining buffer is used for variable-length output data storage (compressed account data payloads). + +### Serialization + +Uses zero-copy serialization via `light_zero_copy` crate for performance: +- **`Ref<&'a mut [u8], T>`** - Zero-copy wrapper for fixed-size fields (fee_payer, associated_merkle_tree, etc.) +- **`ZeroCopyVecU8<'a, T>`** - Variable-length vectors with u8 length prefix (2 bytes: 1 byte length + 1 byte capacity) +- **`ZeroCopySliceMut<'a, U16, u8>`** - Output data slices with u16 length prefix + +**Memory Layout:** +``` +[8 bytes discriminator] +[32 bytes fee_payer] +[32 bytes associated_merkle_tree] +[32 bytes _associated_queue] +[32 bytes _place_holder_bytes] +[2 bytes new_addresses metadata][variable new_addresses data] +[2 bytes readonly_addresses metadata][variable readonly_addresses data] +[2 bytes readonly_accounts metadata][variable readonly_accounts data] +[2 bytes in_accounts metadata][variable in_accounts data] +[2 bytes out_accounts metadata][variable out_accounts data] +[2 bytes total_output_data_len] +[2 bytes output_data_len] +[variable output_data with u16 length prefixes] +[remaining unallocated space] +``` + +### Associated Instructions + +| Instruction | Purpose | Context Flag | Effect | +|-------------|---------|--------------|--------| +| `InitializeCpiContextAccount` | Create new V2 account | N/A | Initializes discriminator, sets tree, pre-allocates capacity | +| `ReInitCpiContextAccount` | Migrate V1 to V2 | N/A | Validates V1, zeros data, writes V2 discriminator, resizes | +| `InvokeCpi` | Standard CPI (Anchor) | `set_context=true` | Writes/appends instruction data to context | +| `InvokeCpiWithReadOnly` | CPI with read-only | `set_context=true` | Writes/appends with read-only account support | +| `InvokeCpiWithAccountInfo` | CPI V2 mode | `first_set_context=true` | First write to context (validates empty) | +| `InvokeCpiWithAccountInfo` | CPI V2 mode | `set_context=true` | Append to existing context data | +| `InvokeCpi*` (any variant) | Execute transaction | `set_context=false` | Reads all context data, executes, auto-clears | + +**Documentation Links:** +- [INIT_CPI_CONTEXT_ACCOUNT.md](init/INIT_CPI_CONTEXT_ACCOUNT.md) - Create new CPI context account +- [REINIT_CPI_CONTEXT_ACCOUNT.md](init/REINIT_CPI_CONTEXT_ACCOUNT.md) - Migrate from V1 to V2 +- [INVOKE_CPI.md](invoke_cpi/INVOKE_CPI.md) - Standard CPI invocation (Anchor mode) +- [INVOKE_CPI_WITH_READ_ONLY.md](invoke_cpi/INVOKE_CPI_WITH_READ_ONLY.md) - CPI with read-only support +- [INVOKE_CPI_WITH_ACCOUNT_INFO.md](invoke_cpi/INVOKE_CPI_WITH_ACCOUNT_INFO.md) - CPI V2 mode (dynamic accounts) + +**Context Flag Behavior:** +- `first_set_context=true`: Validates account is empty, then writes data (prevents overwriting existing context) +- `set_context=true`: Appends data to existing context (allows multi-program collection) +- Both flags false: Executes transaction using all collected data, then clears account + +--- + +## 2. CpiContextAccount (Version 1 - Legacy) + +### Description +Legacy version of the CPI context account. Should be migrated to version 2 using `ReInitCpiContextAccount`. + +**Key Differences from V2:** +- Uses Borsh serialization instead of zero-copy +- Only stores fee_payer and associated_merkle_tree +- No support for collecting instruction data across multiple CPIs +- Smaller size (minimal state storage) + +### Discriminator +```rust +CPI_CONTEXT_ACCOUNT_1_DISCRIMINATOR: [u8; 8] = [22, 20, 149, 218, 74, 204, 128, 166] +``` + +**Source:** `programs/system/src/constants.rs` + +### State Layout (Borsh Serialization) + +**Source:** `programs/system/src/cpi_context/state.rs` + +```rust +#[derive(BorshDeserialize)] +pub struct CpiContextAccount { + pub fee_payer: Pubkey, // 32 bytes - Transaction fee payer + pub associated_merkle_tree: Pubkey, // 32 bytes - Associated state Merkle tree +} +``` + +**Total Size:** 8 (discriminator) + 32 + 32 = 72 bytes (minimum account size) + +### Migration +Use `ReInitCpiContextAccount` instruction to migrate from version 1 to version 2. The migration process: +1. Validates V1 discriminator +2. Zeros out account data +3. Writes V2 discriminator +4. Initializes V2 structure with default capacity parameters +5. Resizes account to 14020 bytes + +See [REINIT_CPI_CONTEXT_ACCOUNT.md](init/REINIT_CPI_CONTEXT_ACCOUNT.md) for details. + +--- + +## Version Comparison (V1 vs V2) + +| Feature | Version 1 (Legacy) | Version 2 (Current) | +|---------|-------------------|---------------------| +| **Discriminator** | `[22, 20, 149, 218, 74, 204, 128, 166]` | `[34, 184, 183, 14, 100, 80, 183, 124]` | +| **Serialization** | Borsh | Zero-copy (light_zero_copy) | +| **Default Size** | 72 bytes (minimal) | 14020 bytes (configurable) | +| **State Fields** | fee_payer, associated_merkle_tree | fee_payer, associated_merkle_tree, plus 5 vector collections | +| **CPI Support** | Basic single-program | Multi-program with context collection | +| **New Addresses** | Not supported | Up to 10 (default) | +| **Input Accounts** | Not stored | Up to 20 (default) | +| **Output Accounts** | Not stored | Up to 30 (default) | +| **Read-Only Support** | No | Yes (addresses and accounts) | +| **Output Data Storage** | No | Variable-length with u16 prefixes | +| **Performance** | Lower (Borsh deserialization) | Higher (zero-copy access) | +| **Use Case** | Single program invocation | Multi-program transactions with shared ZK proof | + +**Migration Path:** V1 → V2 via `ReInitCpiContextAccount` instruction + +--- + +## 3. Supporting Types + +All supporting types use the zerocopy crate's derive macros for safe zero-copy serialization: +- **`FromBytes`** - Safe deserialization from byte slices +- **`IntoBytes`** - Safe serialization to byte slices +- **`KnownLayout`** - Compile-time memory layout verification +- **`Immutable`** - Guarantees no interior mutability +- **`Unaligned`** - No alignment requirements (packed layout) + +These traits ensure memory safety and prevent undefined behavior when casting between byte slices and struct references. + +### CpiContextInAccount + +Represents an input compressed account stored in CPI context. + +**Source:** `programs/system/src/cpi_context/account.rs` + +```rust +#[repr(C)] +pub struct CpiContextInAccount { + pub owner: Pubkey, // 32 bytes - Account owner + pub has_data: u8, // 1 byte - Has compressed data flag + pub discriminator: [u8; 8], // 8 bytes - Data discriminator + pub data_hash: [u8; 32], // 32 bytes - Hash of account data + pub merkle_context: ZPackedMerkleContext, // 12 bytes - Merkle tree context (with padding) + pub root_index: U16, // 2 bytes - Merkle root index + pub lamports: U64, // 8 bytes - Account lamports + pub with_address: u8, // 1 byte - Has address flag + pub address: [u8; 32], // 32 bytes - Optional address +} +``` + +**Size:** 144 bytes (includes alignment padding) + +**Note:** The actual size is 144 bytes due to Rust struct alignment and padding, not 124 bytes as simple field addition would suggest. + +**Trait Implementations:** +- `InputAccount<'_>` - Provides methods to access input account fields (owner, lamports, address, merkle_context, has_data, data, hash_with_hashed_values, root_index) +- Used during CPI context processing to validate and hash input accounts + +### CpiContextOutAccount + +Represents an output compressed account stored in CPI context. + +**Source:** `programs/system/src/cpi_context/account.rs` + +```rust +#[repr(C)] +pub struct CpiContextOutAccount { + pub owner: Pubkey, // 32 bytes - Account owner + pub has_data: u8, // 1 byte - Has compressed data flag + pub discriminator: [u8; 8], // 8 bytes - Data discriminator + pub data_hash: [u8; 32], // 32 bytes - Hash of account data + pub output_merkle_tree_index: u8, // 1 byte - Output tree index + pub lamports: U64, // 8 bytes - Account lamports + pub with_address: u8, // 1 byte - Has address flag + pub address: [u8; 32], // 32 bytes - Optional address +} +``` + +**Size:** 128 bytes (includes alignment padding) + +**Note:** The actual size is 128 bytes due to Rust struct alignment and padding, not 115 bytes as simple field addition would suggest. + +**Trait Implementations:** +- `OutputAccount<'_>` - Provides methods to access output account fields (owner, lamports, address, has_data, data, merkle_tree_index, hash_with_hashed_values) +- Used during CPI context processing to validate and hash output accounts before inserting into Merkle trees + +### CpiContextNewAddressParamsAssignedPacked + +Represents a new address to be created, stored in CPI context. + +**Source:** `programs/system/src/cpi_context/address.rs` + +```rust +#[repr(C)] +pub struct CpiContextNewAddressParamsAssignedPacked { + pub owner: [u8; 32], // 32 bytes - Address owner (program) + pub seed: [u8; 32], // 32 bytes - Address seed + pub address_queue_account_index: u8, // 1 byte - Queue account index + pub address_merkle_tree_account_index: u8, // 1 byte - Tree account index + pub address_merkle_tree_root_index: U16, // 2 bytes - Root index + pub assigned_to_account: u8, // 1 byte - Is assigned flag + pub assigned_account_index: u8, // 1 byte - Assigned output index +} +``` + +**Size:** 70 bytes + +**Trait Implementations:** +- `NewAddress<'_>` - Provides methods to access new address parameters (seed, address_queue_index, address_merkle_tree_account_index, address_merkle_tree_root_index, assigned_compressed_account_index, owner) +- Used during address derivation and validation in the processing pipeline + +### ZPackedReadOnlyAddress + +Represents a read-only address to be verified (exists in address Merkle tree). + +**Source:** `program-libs/compressed-account/src/instruction_data/zero_copy.rs` + +```rust +#[repr(C)] +pub struct ZPackedReadOnlyAddress { + pub address: [u8; 32], // 32 bytes - Address to verify + pub address_merkle_tree_root_index: U16, // 2 bytes - Root index + pub address_merkle_tree_account_index: u8, // 1 byte - Tree account index +} +``` + +**Size:** 36 bytes (includes 1 byte alignment padding) + +### ZPackedReadOnlyCompressedAccount + +Represents a read-only compressed account to be verified (exists in state Merkle tree). + +**Source:** `program-libs/compressed-account/src/instruction_data/zero_copy.rs` + +```rust +#[repr(C)] +pub struct ZPackedReadOnlyCompressedAccount { + pub account_hash: [u8; 32], // 32 bytes - Account hash + pub merkle_context: ZPackedMerkleContext, // 12 bytes - Merkle tree context + pub root_index: U16, // 2 bytes - Root index +} +``` + +**Size:** 48 bytes (includes 2 bytes alignment padding) + +### ZPackedMerkleContext + +Packed Merkle tree context for input accounts. + +**Source:** `program-libs/compressed-account/src/instruction_data/zero_copy.rs` + +```rust +#[repr(C)] +pub struct ZPackedMerkleContext { + pub merkle_tree_pubkey_index: u8, // 1 byte - Index in remaining accounts + pub queue_pubkey_index: u8, // 1 byte - Queue index in remaining accounts + pub leaf_index: U32, // 4 bytes - Leaf index in tree + pub prove_by_index: u8, // 1 byte - Prove by index flag (0 or 1) +} +``` + +**Size:** 12 bytes (includes 5 bytes of alignment padding after prove_by_index) + +**Note:** Due to `U32` alignment requirements, the compiler adds padding to align the struct to 4-byte boundaries, resulting in 12 bytes total instead of 7 bytes. + +--- + +## 4. Account Lifecycle + +### Creation +1. User creates account via System Program with rent exemption +2. Calls `InitializeCpiContextAccount` instruction +3. Account is resized to `DEFAULT_CPI_CONTEXT_ACCOUNT_SIZE_V2` (14020 bytes) +4. Discriminator is set to `CPI_CONTEXT_ACCOUNT_2_DISCRIMINATOR` +5. Fee payer is initialized to zero (set during first use) +6. Associated Merkle tree is set to specified tree pubkey +7. Vector capacities are pre-allocated based on initialization parameters + +### Usage Pattern + +**Single-Program Transaction (Direct Invoke):** +``` +User → Invoke instruction → Process → Account Compression CPI +``` +- No CPI context account needed +- All data passed in instruction data + +**Multi-Program Transaction (CPI Context):** +``` +Program A → InvokeCpi (set_context=true) → Write to CPI context +Program B → InvokeCpi (set_context=true) → Append to CPI context +Program C → InvokeCpi (set_context=false) → Execute with combined data +``` +1. **First CPI (set_context=true):** Writes instruction data to CPI context account +2. **Additional CPIs (set_context=true):** Append more instruction data +3. **Final CPI (set_context=false):** Combines all data, executes with single ZK proof +4. **Auto-clear:** Account is cleared after successful execution + +### State Transitions + +``` +[Empty] → [Collecting] → [Executing] → [Empty] + ↑ ↓ ↑ ↓ ↑ + | | | | | + | set_context set_context execute + | (first) (additional) (final) + | | + └──────────────── clear ──────────────┘ +``` + +**State Details:** +- **Empty:** `is_empty() == true`, ready for new transaction +- **Collecting:** Contains partial instruction data from one or more CPIs +- **Executing:** Final CPI reads all data, executes transaction, clears account +- **Error State:** If execution fails, account may need manual clearing or reinit + +### Clearing + +The CPI context account is automatically cleared after successful execution: +- All vector lengths set to 0 +- Fee payer reset to zero +- Output data cleared +- Remaining capacity restored + +**Manual Clearing:** If needed, call `deserialize_cpi_context_account_cleared()` which zeros out all data fields. + +--- + +## Account Validation + +### Ownership Check +All CPI context account operations validate ownership: +```rust +check_owner(&ID, account_info).map_err(|_| SystemProgramError::InvalidCpiContextOwner)?; +``` + +### Discriminator Check +Version 2 accounts must have the correct discriminator: +```rust +if discriminator != CPI_CONTEXT_ACCOUNT_2_DISCRIMINATOR { + return Err(SystemProgramError::InvalidCpiContextDiscriminator.into()); +} +``` + +### Association Check +CPI context accounts must be associated with the first Merkle tree used in the transaction: +```rust +if *cpi_context_account.associated_merkle_tree != first_merkle_tree_pubkey { + return Err(SystemProgramError::CpiContextAssociatedMerkleTreeMismatch.into()); +} +``` + +--- + +## Practical Examples + +### Example 1: Single Program Transaction (No CPI Context) + +```rust +// Direct invocation - no CPI context account needed +process_instruction( + &system_program_id, + accounts, + &invoke_instruction_data, +) +``` + +**Use Case:** Simple compressed account transfers within one program. + +### Example 2: Multi-Program Transaction + +```rust +// Step 1: Program A sets initial context +invoke_cpi_instruction_data.cpi_context = Some(CpiContext { + set_context: true, + first_set_context: true, // Validates empty +}); +program_a_invoke_cpi(&cpi_context_account, instruction_data_a)?; + +// Step 2: Program B appends to context +invoke_cpi_instruction_data.cpi_context = Some(CpiContext { + set_context: true, + first_set_context: false, // Appends +}); +program_b_invoke_cpi(&cpi_context_account, instruction_data_b)?; + +// Step 3: Program C executes with combined data +invoke_cpi_instruction_data.cpi_context = Some(CpiContext { + set_context: false, // Execute + first_set_context: false, +}); +program_c_invoke_cpi(&cpi_context_account, instruction_data_c)?; +// Account is automatically cleared after successful execution +``` + +**Use Case:** Complex DeFi operations requiring coordination between multiple programs (e.g., token swap + liquidity provision). + +### Example 3: V1 to V2 Migration + +```rust +// Check if account is V1 +let discriminator = &account_data[0..8]; +if discriminator == CPI_CONTEXT_ACCOUNT_1_DISCRIMINATOR { + // Migrate to V2 + reinit_cpi_context_account(&[ + cpi_context_account, + associated_merkle_tree_account, + payer, + system_program, + ])?; +} +``` + +**Use Case:** Upgrading existing V1 CPI context accounts to take advantage of V2 features. + +--- + +## Related Documentation + +- **[PROCESSING_PIPELINE.md](PROCESSING_PIPELINE.md)** - How CPI context data flows through the 19-step processing pipeline +- **[CPI_CONTEXT.md](CPI_CONTEXT.md)** - Detailed explanation of multi-program transaction coordination +- **[INSTRUCTIONS.md](INSTRUCTIONS.md)** - Full instruction reference and error codes +- **[../CLAUDE.md](../CLAUDE.md)** - System program overview and source code structure diff --git a/programs/system/docs/CLAUDE.md b/programs/system/docs/CLAUDE.md new file mode 100644 index 0000000000..d3fa6378a7 --- /dev/null +++ b/programs/system/docs/CLAUDE.md @@ -0,0 +1,41 @@ +# Documentation Structure + +## Overview +This documentation covers the Light System Program - the core validation and coordination layer for Light Protocol that handles ZK proof verification, CPI context management, and compressed account state transitions. + +## Structure +- **`CLAUDE.md`** (this file) - Documentation structure guide +- **`../CLAUDE.md`** (parent) - Main entry point with summary and source code structure +- **`INSTRUCTIONS.md`** - Full instruction reference and discriminator table +- **`ACCOUNTS.md`** - Account layouts and state structures +- **`PROCESSING_PIPELINE.md`** - 19-step processing pipeline documentation +- **`CPI_CONTEXT.md`** - CPI context state management and multi-program transactions +- **`init/`** - CPI context account initialization + - `INIT_CPI_CONTEXT_ACCOUNT.md` - Initialize CPI context account (version 2) + - `REINIT_CPI_CONTEXT_ACCOUNT.md` - Migrate from version 1 to version 2 +- **`invoke/`** - Direct invocation + - `INVOKE.md` - Direct invocation instruction +- **`invoke_cpi/`** - CPI invocation modes + - `INVOKE_CPI.md` - Standard CPI invocation (Anchor mode) + - `INVOKE_CPI_WITH_READ_ONLY.md` - CPI with read-only account support + - `INVOKE_CPI_WITH_ACCOUNT_INFO.md` - CPI with dynamic account configuration (V2 mode) + +## Navigation Tips +- Start with `../CLAUDE.md` for program overview and source code structure +- Use `INSTRUCTIONS.md` for discriminator reference and instruction index +- Use `ACCOUNTS.md` for CpiContextAccount layout and initialization +- Refer to specific instruction docs for implementation details + +| Task | Start Here | +|------|------------| +| Understand program architecture | `../CLAUDE.md` | +| Find instruction discriminators | `INSTRUCTIONS.md` | +| Understand CpiContextAccount layout | `ACCOUNTS.md` | +| Learn 19-step processing flow | `PROCESSING_PIPELINE.md` | +| Multi-program transactions | `CPI_CONTEXT.md` | +| Direct invocation (single program) | `invoke/INVOKE.md` | +| CPI invocation (Anchor mode) | `invoke_cpi/INVOKE_CPI.md` | +| CPI with read-only accounts | `invoke_cpi/INVOKE_CPI_WITH_READ_ONLY.md` | +| CPI with dynamic accounts (V2) | `invoke_cpi/INVOKE_CPI_WITH_ACCOUNT_INFO.md` | +| Initialize CPI context account | `init/INIT_CPI_CONTEXT_ACCOUNT.md` | +| Migrate to V2 CPI context | `init/REINIT_CPI_CONTEXT_ACCOUNT.md` | diff --git a/programs/system/docs/CPI_CONTEXT.md b/programs/system/docs/CPI_CONTEXT.md new file mode 100644 index 0000000000..05f318bc1f --- /dev/null +++ b/programs/system/docs/CPI_CONTEXT.md @@ -0,0 +1,264 @@ +# CPI Context + +## Quick Reference + +```rust +// 1. Initialize CPI context account (once) +initialize_cpi_context_account(state_tree_pubkey, capacity_params); + +// 2. First program invocation - clears and writes +CompressedCpiContext::first() // { first_set_context: true, set_context: false } + +// 3. Additional program invocations - append (0 or more) +CompressedCpiContext::set() // { first_set_context: false, set_context: true } + +// 4. Final program invocation - execute +CompressedCpiContext::default() // { first_set_context: false, set_context: false } +``` + +**Critical rules:** +- Same fee payer across all invocations +- Same state Merkle tree across all invocations +- One proof in final invocation covers all accounts +- CPI context automatically cleared after execution + +--- + +## Overview + +CPI Context is a coordination mechanism that enables multiple Solana programs to share a single zero-knowledge proof when operating on compressed accounts. Without CPI Context, each program would need its own proof, multiplying compute costs and transaction size. + +**Key Benefits:** +- Share one ZK proof across multiple programs (50-75% cost reduction) +- Enable complex multi-program workflows (DeFi compositions, token + PDA updates) +- Maintain security - each program validates its own accounts before contributing to shared proof + +**How it works:** +1. First program writes its account data to CPI context account +2. Additional programs append their account data +3. Final program reads all data, verifies one proof covering everything, executes transaction + +**Source:** `programs/system/src/cpi_context/` + +--- + +## State Machine + +``` + +-------------------+ + | Empty/Cleared | + | (after init or | + | after execute) | + +-------------------+ + | + | first_set_context = true + | (clear & set fee payer) + v + +-------------------+ + | First Set | + | - fee payer set | + | - data stored | + +-------------------+ + | + | set_context = true (0 or more times) + | (validate fee payer & append data) + v + +-------------------+ + | Accumulating | + | - multiple | + | programs' data | + +-------------------+ + | + | both flags = false + | (read all data & execute) + v + +-------------------+ + | Executed | + | - proof verified | + | - state updated | + | - account cleared| + +-------------------+ + | + v + (back to Empty/Cleared) +``` + +### Modes + +| Mode | Flags | Behavior | +|------|-------|----------| +| **First Set** | `first_set_context = true` | Clear account, set fee payer, store instruction data, return early | +| **Set Context** | `set_context = true` | Validate fee payer, append data, return early | +| **Execute** | Both `false` | Read all data, verify proof, execute state transition, clear account | + +--- + +## CPI Context Flow + +### Step 1: First Invocation + +```rust +cpi_context: Some(CompressedCpiContext { + first_set_context: true, + set_context: false, + cpi_context_account_index: 0, +}) +``` + +**Processing:** +1. Clear entire account (zero out all data) +2. Set `fee_payer` field +3. Store instruction data (inputs, outputs, addresses) +4. Return early (no proof verification) + +### Step 2: Subsequent Invocations (0 or more) + +```rust +cpi_context: Some(CompressedCpiContext { + first_set_context: false, + set_context: true, + cpi_context_account_index: 0, +}) +``` + +**Processing:** +1. Validate fee payer matches first invocation +2. Validate account is not empty +3. Append instruction data to existing vectors +4. Return early (no proof verification) + +### Step 3: Final Invocation + +```rust +cpi_context: Some(CompressedCpiContext { + first_set_context: false, + set_context: false, + cpi_context_account_index: 0, +}) +``` + +**Processing:** +1. Validate fee payer matches +2. Read all accumulated data from CPI context +3. Combine with current instruction data +4. Verify single ZK proof against all accounts +5. Execute complete state transition +6. Clear CPI context account + +--- + +## Data Structures + +### CompressedCpiContext (Instruction Data) + +```rust +pub struct CompressedCpiContext { + pub first_set_context: bool, // Clear and write + pub set_context: bool, // Append + pub cpi_context_account_index: u8, // Index in remaining accounts +} +``` + +**Source:** `program-libs/compressed-account/src/instruction_data/cpi_context.rs` + +### Stored Data in CpiContextAccount + +| Field | Purpose | +|-------|---------| +| `fee_payer` | Transaction fee payer (set on first_set_context) | +| `associated_merkle_tree` | Required Merkle tree association | +| `new_addresses` | Addresses to create | +| `readonly_addresses` | Read-only address references | +| `readonly_accounts` | Read-only account references | +| `in_accounts` | Input compressed accounts | +| `out_accounts` | Output compressed accounts | +| `output_data` | Variable-length output account data | + +--- + +## Validation Rules + +### 1. Fee Payer Consistency +All invocations must use the same fee payer. Set during `first_set_context`, validated on all subsequent operations. + +```rust +if *cpi_context_account.fee_payer != fee_payer { + return Err(SystemProgramError::CpiContextFeePayerMismatch); +} +``` + +### 2. Merkle Tree Association +CPI context must be associated with the first Merkle tree used in the transaction. + +```rust +if *cpi_context_account.associated_merkle_tree != first_merkle_tree_pubkey { + return Err(SystemProgramError::CpiContextAssociatedMerkleTreeMismatch); +} +``` + +### 3. Non-Empty on Execute +Account must contain data when executing. + +```rust +if cpi_context_account.is_empty() { + return Err(SystemProgramError::CpiContextEmpty); +} +``` + +### 4. Account Must Exist for Context Operations +Cannot use `set_context` or `first_set_context` flags without passing the CPI context account. + +--- + +## Limitations + +### 1. Read-Only Accounts/Addresses +Not supported when writing to CPI context account. Include read-only accounts only in the final execute invocation. + +### 2. Fixed Capacity +CPI context accounts have fixed capacity determined at initialization: + +```rust +pub struct CpiContextAccountInitParams { + pub new_addresses_len: u8, // Default: 10 + pub readonly_addresses_len: u8, // Default: 10 + pub readonly_accounts_len: u8, // Default: 10 + pub in_accounts_len: u8, // Default: 20 + pub out_accounts_len: u8, // Default: 30 +} +``` + +**Default account size:** 14,020 bytes + +Overflow returns `ZeroCopyError::InsufficientCapacity` and transaction fails. + +### 3. Single Merkle Tree Association +All operations in a CPI context transaction must use the same state Merkle tree. + +--- + +## Error Codes + +| Code | Error | Cause | +|------|-------|-------| +| 6020 | CpiContextAccountUndefined | CPI context account required but not provided | +| 6021 | CpiContextEmpty | Account empty during execute mode | +| 6022 | CpiContextMissing | CPI context data missing in instruction | +| 6027 | CpiContextFeePayerMismatch | Fee payer doesn't match first invocation | +| 6028 | CpiContextAssociatedMerkleTreeMismatch | Wrong Merkle tree association | +| 6049 | CpiContextAlreadySet | Attempting to set when already set | +| 6054 | CpiContextPassedAsSetContext | Account doesn't exist but passed as set_context | +| 6055 | InvalidCpiContextOwner | Wrong account owner | +| 6056 | InvalidCpiContextDiscriminator | Wrong discriminator | +| 6064 | CpiContextDeactivated | CPI context is deactivated | + +--- + +## Related Files + +| File | Purpose | +|------|---------| +| `src/cpi_context/state.rs` | CpiContextAccount structure and serialization | +| `src/cpi_context/process_cpi_context.rs` | CPI context processing logic | +| `src/cpi_context/account.rs` | CpiContextInAccount, CpiContextOutAccount types | +| `src/cpi_context/address.rs` | CpiContextNewAddressParamsAssignedPacked type | diff --git a/programs/system/docs/INSTRUCTIONS.md b/programs/system/docs/INSTRUCTIONS.md new file mode 100644 index 0000000000..ab7ef1ea6f --- /dev/null +++ b/programs/system/docs/INSTRUCTIONS.md @@ -0,0 +1,150 @@ +# Instruction Reference + +## Program ID +``` +SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7 +``` + +## Discriminator Table + +| Instruction | Discriminator | Enum Variant | Source | +|-------------|---------------|--------------|--------| +| InitializeCpiContextAccount | `[233, 112, 71, 66, 121, 33, 178, 188]` | `InstructionDiscriminator::InitializeCpiContextAccount` | `src/accounts/init_context_account.rs` | +| Invoke | `[26, 16, 169, 7, 21, 202, 242, 25]` | `InstructionDiscriminator::Invoke` | `src/invoke/` | +| InvokeCpi | `[49, 212, 191, 129, 39, 194, 43, 196]` | `InstructionDiscriminator::InvokeCpi` | `src/invoke_cpi/` | +| InvokeCpiWithReadOnly | `[86, 47, 163, 166, 21, 223, 92, 8]` | `InstructionDiscriminator::InvokeCpiWithReadOnly` | `src/lib.rs` | +| InvokeCpiWithAccountInfo | `[228, 34, 128, 84, 47, 139, 86, 240]` | `InstructionDiscriminator::InvokeCpiWithAccountInfo` | `src/lib.rs` | +| ReInitCpiContextAccount | `[187, 147, 22, 142, 104, 180, 136, 190]` | `InstructionDiscriminator::ReInitCpiContextAccount` | `src/accounts/init_context_account.rs` (feature: "reinit") | + +**Source:** `programs/system/src/constants.rs` + +## Instruction Categories + +### CPI Context Account Management +Instructions for initializing and migrating CPI context accounts. + +| Instruction | Description | Documentation | +|-------------|-------------|---------------| +| InitializeCpiContextAccount | Initialize a new CPI context account (version 2) | [INIT_CPI_CONTEXT_ACCOUNT.md](init/INIT_CPI_CONTEXT_ACCOUNT.md) | +| ReInitCpiContextAccount | Migrate CPI context account from version 1 to version 2 | [REINIT_CPI_CONTEXT_ACCOUNT.md](init/REINIT_CPI_CONTEXT_ACCOUNT.md) | + +### Direct Invocation +Direct invocation for single-program compressed account operations. + +| Instruction | Description | Documentation | +|-------------|-------------|---------------| +| Invoke | Process compressed accounts directly (no CPI) | [INVOKE.md](invoke/INVOKE.md) | + +### CPI Invocation +CPI invocation modes for multi-program compressed account operations. + +| Instruction | Description | Documentation | +|-------------|-------------|---------------| +| InvokeCpi | Standard CPI invocation with Anchor-style accounts | [INVOKE_CPI.md](invoke_cpi/INVOKE_CPI.md) | +| InvokeCpiWithReadOnly | CPI with read-only compressed account support | [INVOKE_CPI_WITH_READ_ONLY.md](invoke_cpi/INVOKE_CPI_WITH_READ_ONLY.md) | +| InvokeCpiWithAccountInfo | CPI with dynamic account configuration (V2 mode) | [INVOKE_CPI_WITH_ACCOUNT_INFO.md](invoke_cpi/INVOKE_CPI_WITH_ACCOUNT_INFO.md) | + +## Input Combinations + +| Instruction | Proof Required | CPI Context | Read-Only Accounts | Read-Only Addresses | Data on Inputs | +|-------------|----------------|-------------|-------------------|---------------------|----------------| +| Invoke | When inputs exist | No | No | No | No (error 6001) | +| InvokeCpi | In execute mode | Optional | No | No | Yes (program-owned) | +| InvokeCpiWithReadOnly | In execute mode | Optional | Yes | Execute mode only | Yes | +| InvokeCpiWithAccountInfo | In execute mode | Optional | Yes | Execute mode only | Yes | + +### CPI Context Mode Combinations + +| first_set_context | set_context | Behavior | Proof Required | +|-------------------|-------------|----------|----------------| +| true | false | Clear + write to context | No | +| false | true | Append to context | No | +| false | false | Execute with proof | Yes | + +--- + +## Instruction Schema Reminder + +Every instruction documentation must include: +- **discriminator** - 8-byte value +- **enum** - `InstructionDiscriminator::*` variant +- **path** - Source file path +- **description** - High-level overview, state changes, usage scenarios +- **instruction_data** - Path to structs, field descriptions +- **Accounts** - Ordered list with signer/writable/checks +- **instruction logic and checks** - Step-by-step processing +- **Errors** - Comprehensive list with codes + +## Error Codes Reference (SystemProgramError) + +| Code | Error | Description | +|------|-------|-------------| +| 6000 | SumCheckFailed | Lamport sum of inputs + compression != outputs + decompression | +| 6001 | SignerCheckFailed | Authority is not a signer for input compressed accounts | +| 6002 | CpiSignerCheckFailed | Invoking program doesn't match expected signer PDA | +| 6003 | ComputeInputSumFailed | Failed to compute input lamports sum | +| 6004 | ComputeOutputSumFailed | Failed to compute output lamports sum | +| 6005 | ComputeRpcSumFailed | Failed to compute RPC sum | +| 6006 | InvalidAddress | Address validation failed | +| 6007 | DeriveAddressError | Failed to derive address from seed | +| 6008 | CompressedSolPdaUndefinedForCompressSol | Sol pool PDA required for compression | +| 6009 | DecompressLamportsUndefinedForCompressSol | Decompression lamports required | +| 6010 | CompressedSolPdaUndefinedForDecompressSol | Sol pool PDA required for decompression | +| 6011 | DeCompressLamportsUndefinedForDecompressSol | Decompression lamports required | +| 6012 | DecompressRecipientUndefinedForDecompressSol | Recipient required for decompression | +| 6013 | WriteAccessCheckFailed | Program doesn't have write access to Merkle tree | +| 6014 | InvokingProgramNotProvided | Invoking program account missing | +| 6015 | InvalidCapacity | CPI context account capacity invalid | +| 6016 | InvalidMerkleTreeOwner | Merkle tree owner mismatch | +| 6017 | ProofIsNone | ZK proof required but not provided | +| 6018 | ProofIsSome | ZK proof provided but not needed | +| 6019 | EmptyInputs | No inputs, outputs, or addresses provided | +| 6020 | CpiContextAccountUndefined | CPI context account required but not provided | +| 6021 | CpiContextEmpty | CPI context account is empty during execute | +| 6022 | CpiContextMissing | CPI context data missing in instruction | +| 6023 | DecompressionRecipientDefined | Unexpected decompression recipient | +| 6024 | SolPoolPdaDefined | Unexpected sol pool PDA | +| 6025 | AppendStateFailed | Failed to append state to Merkle tree | +| 6026 | InstructionNotCallable | Instruction disabled | +| 6027 | CpiContextFeePayerMismatch | Fee payer mismatch in CPI context | +| 6028 | CpiContextAssociatedMerkleTreeMismatch | Merkle tree mismatch in CPI context | +| 6029 | NoInputs | No input accounts provided | +| 6030 | InputMerkleTreeIndicesNotInOrder | Input Merkle tree indices must be ascending | +| 6031 | OutputMerkleTreeIndicesNotInOrder | Output Merkle tree indices must be ascending | +| 6032 | OutputMerkleTreeNotUnique | Output Merkle trees must be unique per batch | +| 6033 | DataFieldUndefined | Required data field is undefined | +| 6034 | ReadOnlyAddressAlreadyExists | Read-only address already in use | +| 6035 | ReadOnlyAccountDoesNotExist | Read-only account not found | +| 6036 | HashChainInputsLenghtInconsistent | Hash chain length mismatch | +| 6037 | InvalidAddressTreeHeight | Address tree height invalid for proof | +| 6038 | InvalidStateTreeHeight | State tree height invalid for proof | +| 6039 | InvalidArgument | Generic invalid argument | +| 6040 | InvalidAccount | Generic invalid account | +| 6041 | AddressMerkleTreeAccountDiscriminatorMismatch | Wrong address Merkle tree discriminator | +| 6042 | StateMerkleTreeAccountDiscriminatorMismatch | Wrong state Merkle tree discriminator | +| 6043 | ProofVerificationFailed | ZK proof verification failed | +| 6044 | InvalidAccountMode | Account mode not recognized | +| 6045 | InvalidInstructionDataDiscriminator | Unknown instruction discriminator | +| 6046 | NewAddressAssignedIndexOutOfBounds | Assigned index exceeds output count | +| 6047 | AddressIsNone | Address expected but not provided | +| 6048 | AddressDoesNotMatch | Address doesn't match expected value | +| 6049 | CpiContextAlreadySet | CPI context already initialized | +| 6050 | InvalidTreeHeight | Tree height invalid | +| 6051 | TooManyOutputAccounts | Exceeds MAX_OUTPUT_ACCOUNTS (30) | +| 6052 | BorrowingDataFailed | Account data borrow failed | +| 6053 | DuplicateAccountInInputsAndReadOnly | Same account in both inputs and read-only | +| 6054 | CpiContextPassedAsSetContext | CPI context account doesn't exist but passed as set_context | +| 6055 | InvalidCpiContextOwner | CPI context account wrong owner | +| 6056 | InvalidCpiContextDiscriminator | CPI context account wrong discriminator | +| 6057 | InvalidAccountIndex | Account index out of bounds | +| 6058 | AccountCompressionCpiDataExceedsLimit | CPI data exceeds 10KB limit | +| 6059 | AddressOwnerIndexOutOfBounds | Address owner index invalid | +| 6060 | AddressAssignedAccountIndexOutOfBounds | Address assigned index invalid | +| 6061 | OutputMerkleTreeIndexOutOfBounds | Output tree index invalid | +| 6062 | PackedAccountIndexOutOfBounds | Packed account index invalid | +| 6063 | Unimplemented | Feature not yet implemented | +| 6064 | CpiContextDeactivated | CPI context is deactivated | +| 6065 | InputMerkleTreeIndexOutOfBounds | Input tree index invalid | +| 6066 | MissingLegacyMerkleContext | Legacy Merkle context required | + +**Source:** `programs/system/src/errors.rs` diff --git a/programs/system/docs/PROCESSING_PIPELINE.md b/programs/system/docs/PROCESSING_PIPELINE.md new file mode 100644 index 0000000000..1cf0fedf85 --- /dev/null +++ b/programs/system/docs/PROCESSING_PIPELINE.md @@ -0,0 +1,459 @@ +# Processing Pipeline + +## Overview + +The Light System Program processes compressed account state transitions through a comprehensive pipeline that validates inputs, verifies ZK proofs, and coordinates with the account-compression program via CPI. + +**Source:** `programs/system/src/processor/process.rs` + +## Input Types + +The processing pipeline handles four types of compressed account inputs: + +### 1. Writable Compressed Accounts +Input accounts that will be nullified and potentially create new outputs. + +**Field:** `inputs.input_compressed_accounts_with_merkle_context` + +**Operations:** +- Sum check lamports (input + compression = output + decompression) +- Compress or decompress lamports +- Hash input compressed accounts +- Insert nullifiers into queue +- Verify inclusion (by index or ZKP) + +### 2. Read-Only Compressed Accounts +Accounts that are verified to exist but not modified. + +**Field:** `read_only_accounts` + +**Operations:** +- Verify inclusion (by index or ZKP) +- Check no duplicates with writable inputs + +### 3. New Addresses +New compressed account addresses to be created. + +**Field:** `inputs.new_address_params` + +**Operations:** +- Derive addresses from seed and invoking program +- Insert addresses into address Merkle tree queue +- Verify non-inclusion (address doesn't exist yet) + +### 4. Read-Only Addresses +Addresses verified to exist without modification. + +**Field:** `read_only_addresses` + +**Operations:** +- Verify non-inclusion in bloom filter queue +- Verify inclusion by ZKP + +--- + +## Processing Steps + +### Step 1: Allocate CPI Data and Initialize Context + +```rust +let (mut context, mut cpi_ix_bytes) = create_cpi_data_and_context( + ctx, + num_output_compressed_accounts, + num_input_accounts, + num_new_addresses, + hashed_pubkeys_capacity, + cpi_outputs_data_len, + invoking_program, + remaining_accounts, +)?; +``` + +**Purpose:** Pre-allocate memory for CPI instruction data and create the processing context. + +**Context includes:** +- Hashed pubkey cache for efficiency +- Fee tracking for rollover and protocol fees +- Address collection for validation + +### Step 2: Deserialize and Validate Merkle Tree Accounts + +```rust +let mut accounts = try_from_account_infos(remaining_accounts, &mut context)?; +``` + +**Purpose:** Deserialize all Merkle tree and queue accounts from remaining accounts, performing validation checks. + +**Validates:** +- Account ownership (account-compression program) +- Account discriminators +- Tree heights and parameters + +### Step 3: Deserialize CPI Instruction Data + +```rust +let (mut cpi_ix_data, bytes) = InsertIntoQueuesInstructionDataMut::new_at( + &mut cpi_ix_bytes[12..], + num_output_compressed_accounts, + num_input_accounts, + num_new_addresses, + min(remaining_accounts.len(), num_output_compressed_accounts), + min(remaining_accounts.len(), num_input_accounts), + min(remaining_accounts.len(), num_new_addresses), +)?; +``` + +**Purpose:** Initialize zero-copy instruction data structure for account-compression CPI. + +### Step 4: Read Address Roots + +```rust +let address_tree_height = read_address_roots( + accounts.as_slice(), + inputs.new_addresses(), + read_only_addresses, + &mut new_address_roots, +)?; +``` + +**Purpose:** Read Merkle roots from address trees for proof verification. + +### Step 5: Collect Existing Addresses + +```rust +inputs.input_accounts().for_each(|account| { + context.addresses.push(account.address()); +}); +``` + +**Purpose:** Collect all addresses from input accounts to validate that output accounts only use existing or new addresses. + +### Step 6: Derive New Addresses + +```rust +derive_new_addresses::( + inputs.new_addresses(), + remaining_accounts, + &mut context, + &mut cpi_ix_data, + accounts.as_slice(), +)?; +``` + +**Purpose:** Derive new addresses from seeds and invoking program, validate address assignments to output accounts. + +### Step 7: Verify Read-Only Address Non-Inclusion + +```rust +verify_read_only_address_queue_non_inclusion( + accounts.as_mut_slice(), + inputs.read_only_addresses().unwrap_or_default(), +)?; +``` + +**Purpose:** Verify read-only addresses aren't in pending bloom filter queues (would indicate address not yet in tree). + +### Step 8: Process Outputs + +```rust +let output_compressed_account_hashes = create_outputs_cpi_data::( + &inputs, + remaining_accounts, + &mut context, + &mut cpi_ix_data, + accounts.as_slice(), +)?; +``` + +**Purpose:** Prepare output compressed accounts: +- Compute output account hashes +- Validate output Merkle tree indices are in order +- Check new address assignments are valid +- Collect output queue/tree accounts + +### Step 9: Process Inputs + +```rust +let input_compressed_account_hashes = create_inputs_cpi_data( + remaining_accounts, + &inputs, + &mut context, + &mut cpi_ix_data, + accounts.as_slice(), +)?; +``` + +**Purpose:** Process input compressed accounts: +- Hash input compressed accounts for nullification +- Collect input queue/tree accounts + +### Step 10: Create Transaction Hash + +```rust +if inputs.with_transaction_hash() { + cpi_ix_data.tx_hash = create_tx_hash_from_hash_chains( + &input_compressed_account_hashes, + &output_compressed_account_hashes, + current_slot, + )?; +} +``` + +**Purpose:** Create transaction hash from input and output hash chains for transaction tracking (optional). + +### Step 11: Check No Duplicate Accounts + +```rust +check_no_duplicate_accounts_in_inputs_and_read_only( + &cpi_ix_data.nullifiers, + read_only_accounts, +)?; +``` + +**Purpose:** Validate no account appears in both input (writable) and read-only arrays. + +### Step 12: Sum Check + +```rust +let num_input_accounts_by_index = sum_check(&inputs, &None, &inputs.is_compress())?; +``` + +**Purpose:** Verify lamport conservation: +``` +input_lamports + compress_lamports = output_lamports + decompress_lamports +``` + +### Step 13: SOL Compression/Decompression + +```rust +compress_or_decompress_lamports::(&inputs, ctx)?; +``` + +**Purpose:** Transfer SOL between: +- **Compress:** User account -> Sol Pool PDA (creates compressed SOL) +- **Decompress:** Sol Pool PDA -> Decompression Recipient (extracts compressed SOL) + +### Step 14: Verify Read-Only Account Inclusion by Index + +```rust +let num_read_only_accounts_by_index = + verify_read_only_account_inclusion_by_index(accounts.as_mut_slice(), read_only_accounts)?; +``` + +**Purpose:** Verify read-only accounts exist by checking their inclusion in output queues using indexed lookup. + +### Step 15: Read State Roots + +```rust +let state_tree_height = read_input_state_roots( + accounts.as_slice(), + inputs.input_accounts(), + read_only_accounts, + &mut input_compressed_account_roots, +)?; +``` + +**Purpose:** Read Merkle roots from state trees for accounts that will be proven by ZKP (not by index). + +### Step 16: Verify ZK Proof + +```rust +verify_proof( + &input_compressed_account_roots, + &proof_input_compressed_account_hashes, + &new_address_roots, + &new_addresses, + &compressed_proof, + address_tree_height, + state_tree_height, +)?; +``` + +**Purpose:** Verify the ZK proof covering: +1. Input compressed account inclusion (state tree) +2. Read-only account inclusion (state tree) +3. New address non-inclusion (address tree) +4. Read-only address inclusion (address tree) + +**Proof inputs order:** +1. Input compressed accounts (not proven by index) +2. Read-only compressed accounts (not proven by index) +3. New addresses +4. Read-only addresses + +### Step 17: Transfer Fees + +```rust +context.transfer_fees(remaining_accounts, ctx.get_fee_payer())?; +``` + +**Purpose:** Transfer network, address, and rollover fees from fee payer to appropriate recipients. + +**Note:** Rollover fees are transferred from the system program instead of the account-compression program to reduce CPI depth. + +### Step 18: Copy CPI Context Outputs + +```rust +copy_cpi_context_outputs(inputs.get_cpi_context_account(), bytes)?; +``` + +**Purpose:** If using CPI context, copy output account data to ensure all data is emitted in the transaction for indexing. + +### Step 19: CPI Account Compression Program + +```rust +cpi_account_compression_program(context, cpi_ix_bytes) +``` + +**Purpose:** Execute CPI to account-compression program to: +- Insert nullifiers into nullifier queues +- Append output states to output queues +- Insert new addresses into address queues + +--- + +## Processing Flow Diagram + +``` + +--------------------------+ + | Instruction Entry | + +--------------------------+ + | + v + +--------------------------+ + | 1. Allocate CPI Data | + +--------------------------+ + | + v + +--------------------------+ + | 2. Deserialize Accounts | + +--------------------------+ + | + v + +--------------------------+ + | 3. Deserialize CPI Data | + +--------------------------+ + | + v + +--------------------------+ + | 4. Read Address Roots | + +--------------------------+ + | + v + +--------------------------+ + | 5. Collect Addresses | + +--------------------------+ + | + v + +--------------------------+ + | 6. Derive New Addresses | + +--------------------------+ + | + v + +--------------------------+ + | 7. Verify Read-Only | + | Address Non-Inclusion | + +--------------------------+ + | + v + +--------------------------+ + | 8. Process Outputs | + +--------------------------+ + | + v + +--------------------------+ + | 9. Process Inputs | + +--------------------------+ + | + v + +--------------------------+ + | 10. Create Tx Hash | + +--------------------------+ + | + v + +--------------------------+ + | 11. Check No Duplicates | + +--------------------------+ + | + v + +--------------------------+ + | 12. Sum Check | + +--------------------------+ + | + v + +--------------------------+ + | 13. SOL Compress/ | + | Decompress | + +--------------------------+ + | + v + +--------------------------+ + | 14. Verify Read-Only | + | by Index | + +--------------------------+ + | + v + +--------------------------+ + | 15. Read State Roots | + +--------------------------+ + | + v + +--------------------------+ + | 16. Verify ZK Proof | + +--------------------------+ + | + v + +--------------------------+ + | 17. Transfer Fees | + +--------------------------+ + | + v + +--------------------------+ + | 18. Copy CPI Context | + +--------------------------+ + | + v + +--------------------------+ + | 19. CPI Account | + | Compression | + +--------------------------+ +``` + +--- + +## Proof Verification Modes + +### 1. Proof by Index +For recently inserted accounts, inclusion can be verified by checking the account exists at a specific index in the output queue. + +```rust +if input_account.prove_by_index() { + // Verify by checking output queue at leaf_index +} +``` + +### 2. Proof by ZKP +For accounts in the Merkle tree (not in queue), inclusion is verified via ZK proof. + +```rust +if !input_account.prove_by_index() { + // Include in ZK proof verification +} +``` + +--- + +## Related Files + +| File | Purpose | +|------|---------| +| `src/processor/process.rs` | Main processing pipeline | +| `src/processor/cpi.rs` | CPI to account-compression program | +| `src/processor/create_address_cpi_data.rs` | Address derivation and CPI data | +| `src/processor/create_inputs_cpi_data.rs` | Input account processing | +| `src/processor/create_outputs_cpi_data.rs` | Output account processing | +| `src/processor/read_only_account.rs` | Read-only account verification | +| `src/processor/read_only_address.rs` | Read-only address verification | +| `src/processor/sol_compression.rs` | SOL compression/decompression | +| `src/processor/sum_check.rs` | Lamport conservation check | +| `src/processor/verify_proof.rs` | ZK proof verification | diff --git a/programs/system/docs/init/INIT_CPI_CONTEXT_ACCOUNT.md b/programs/system/docs/init/INIT_CPI_CONTEXT_ACCOUNT.md new file mode 100644 index 0000000000..f8ce429114 --- /dev/null +++ b/programs/system/docs/init/INIT_CPI_CONTEXT_ACCOUNT.md @@ -0,0 +1,108 @@ +# InitializeCpiContextAccount + +## Summary + +| Field | Value | +|-------|-------| +| **Discriminator** | `[233, 112, 71, 66, 121, 33, 178, 188]` | +| **Enum** | `InstructionDiscriminator::InitializeCpiContextAccount` | +| **Path** | `programs/system/src/accounts/init_context_account.rs` | + +## Description + +Initializes a new CPI context account (version 2) for use in multi-program compressed account transactions. The account is associated with a specific state Merkle tree and allocated with default capacity parameters. + +### Use Cases +- Setting up a CPI context account for a new Merkle tree +- Preparing for multi-program transactions that share a single ZK proof +- Creating dedicated context accounts for specific applications + +### State Changes +- Account data is initialized with version 2 discriminator +- Associated Merkle tree is set +- Default capacity parameters are applied + +--- + +## Instruction Data + +This instruction takes no additional data beyond the discriminator. + +```rust +// Instruction data layout: +// [0..8]: Discriminator +``` + +**Total size:** 8 bytes + +--- + +## Accounts + +| Index | Name | Signer | Writable | Description | +|-------|------|--------|----------|-------------| +| 0 | `fee_payer` | Yes | Yes | Pays for account creation | +| 1 | `cpi_context_account` | No | Yes | Account to initialize (must be pre-allocated) | +| 2 | `associated_merkle_tree` | No | No | State Merkle tree to associate with | + +### Account Validations + +**fee_payer (index 0):** +- Must be a signer +- Must be writable + +**cpi_context_account (index 1):** +- Must be pre-allocated with exactly 14020 bytes (`DEFAULT_CPI_CONTEXT_ACCOUNT_SIZE_V2`) +- Must be owned by Light System Program +- Discriminator must be zero (uninitialized) + +**associated_merkle_tree (index 2):** +- Must be owned by Account Compression Program +- Discriminator must match state or batched Merkle tree + +--- + +## Instruction Logic + +### Step 1: Validate Accounts +```rust +let ctx = InitializeCpiContextAccount::from_account_infos(accounts)?; +``` + +### Step 2: Initialize Account +```rust +let params = CpiContextAccountInitParams::new(*ctx.associated_merkle_tree.key()); +cpi_context_account_new::(ctx.cpi_context_account, params)?; +``` + +Initialization: +1. Write version 2 discriminator: `[34, 184, 183, 14, 100, 80, 183, 124]` +2. Set fee_payer to zero (will be set during operation) +3. Set associated_merkle_tree +4. Set up vector capacities with defaults: + - `new_addresses_len`: 10 + - `readonly_addresses_len`: 10 + - `readonly_accounts_len`: 10 + - `in_accounts_len`: 20 + - `out_accounts_len`: 30 + +--- + +## Errors + +| Code | Error | Cause | +|------|-------|-------| +| - | `NotEnoughAccountKeys` | Less than 3 accounts provided | +| - | `MissingRequiredSignature` | fee_payer is not a signer | +| - | `IllegalOwner` | associated_merkle_tree not owned by account-compression program OR cpi_context_account not owned by Light System Program | +| 6042 | `StateMerkleTreeAccountDiscriminatorMismatch` | associated_merkle_tree discriminator doesn't match state or batched Merkle tree | +| 6055 | `InvalidCpiContextOwner` | cpi_context_account not owned by Light System Program | +| 6056 | `InvalidCpiContextDiscriminator` | cpi_context_account discriminator is not zero (already initialized) | + +--- + +## Related Documentation + +- [ACCOUNTS.md](../ACCOUNTS.md) - CpiContextAccount layout details +- [CPI_CONTEXT.md](../CPI_CONTEXT.md) - How CPI context is used +- [REINIT_CPI_CONTEXT_ACCOUNT.md](REINIT_CPI_CONTEXT_ACCOUNT.md) - Migration from version 1 diff --git a/programs/system/docs/init/REINIT_CPI_CONTEXT_ACCOUNT.md b/programs/system/docs/init/REINIT_CPI_CONTEXT_ACCOUNT.md new file mode 100644 index 0000000000..0b789df004 --- /dev/null +++ b/programs/system/docs/init/REINIT_CPI_CONTEXT_ACCOUNT.md @@ -0,0 +1,150 @@ +# ReInitCpiContextAccount + +## Summary + +| Field | Value | +|-------|-------| +| **Discriminator** | `[187, 147, 22, 142, 104, 180, 136, 190]` | +| **Enum** | `InstructionDiscriminator::ReInitCpiContextAccount` | +| **Path** | `programs/system/src/accounts/init_context_account.rs` | +| **Feature Gate** | `reinit` | + +## Description + +Migrates an existing CPI context account from version 1 to version 2. This instruction reads the associated Merkle tree from the existing account, resizes the account to the new size, and reinitializes with version 2 format. + +**Critical:** This is an in-place migration. The account must be owned by Light System Program and have the V1 discriminator. + +### Use Cases +- Upgrading legacy CPI context accounts to the new format +- Migrating accounts after protocol upgrades +- Preparing existing accounts for enhanced CPI context features + +### State Changes +- Account is resized from 20,488 bytes (V1) to 14,020 bytes (V2) +- Discriminator is updated from V1 `[22, 20, 149, 218, 74, 204, 128, 166]` to V2 `[34, 184, 183, 14, 100, 80, 183, 124]` +- All account data is zeroed except the associated Merkle tree (preserved) +- Vector capacities are initialized with default values + +--- + +## Instruction Data + +This instruction takes no additional data beyond the discriminator. + +```rust +// Instruction data layout: +// [0..8]: Discriminator +``` + +**Total size:** 8 bytes + +--- + +## Accounts + +| Index | Name | Signer | Writable | Description | +|-------|------|--------|----------|-------------| +| 0 | `cpi_context_account` | No | Yes | Existing version 1 CPI context account to migrate | + +### Account Validations + +**cpi_context_account (index 0):** +- Must be owned by Light System Program +- Must have discriminator `[22, 20, 149, 218, 74, 204, 128, 166]` (V1) +- Must be writable +- Will be resized from 20,488 bytes to 14,020 bytes + +--- + +## Instruction Logic + +### Step 1: Validate Ownership +```rust +check_owner(&crate::ID, cpi_context_account)?; +``` + +### Step 2: Read Associated Merkle Tree +```rust +let associated_merkle_tree = { + let data = cpi_context_account.try_borrow_data()?; + CpiContextAccount::deserialize(&mut &data[8..])?.associated_merkle_tree +}; +``` + +Must happen before resize to preserve the merkle tree reference. + +### Step 3: Resize Account +```rust +cpi_context_account.resize(DEFAULT_CPI_CONTEXT_ACCOUNT_SIZE_V2 as usize)?; +``` + +V2 is smaller than V1, so no additional rent payment required. + +### Step 4: Reinitialize Account +```rust +let params = CpiContextAccountInitParams::new(associated_merkle_tree); +cpi_context_account_new::(cpi_context_account, params)?; +``` + +Writes V2 discriminator, preserves associated_merkle_tree, initializes vector capacities with defaults. + +--- + +## Errors + +| Code | Error | Cause | +|------|-------|-------| +| - | `NotEnoughAccountKeys` | No accounts provided | +| - | `IllegalOwner` | Account not owned by Light System Program | +| - | `BorshIoError` | Failed to deserialize V1 account data | +| 6025 | `InvalidCpiContextDiscriminator` | Account discriminator is not V1 (may already be migrated) | + +--- + +## Version Differences + +### Version 1 (Legacy) + +```rust +// Discriminator: [22, 20, 149, 218, 74, 204, 128, 166] +pub struct CpiContextAccount { + pub fee_payer: Pubkey, + pub associated_merkle_tree: Pubkey, +} +``` + +**Size:** 20,488 bytes + +### Version 2 (Current) + +```rust +// Discriminator: [34, 184, 183, 14, 100, 80, 183, 124] +pub struct ZCpiContextAccount2 { + pub fee_payer: Pubkey, + pub associated_merkle_tree: Pubkey, + _associated_queue: Pubkey, + _place_holder_bytes: [u8; 32], + pub new_addresses: ZeroCopyVecU8, + pub readonly_addresses: ZeroCopyVecU8, + pub readonly_accounts: ZeroCopyVecU8, + pub in_accounts: ZeroCopyVecU8, + pub out_accounts: ZeroCopyVecU8, + // ... +} +``` + +**Size:** 14,020 bytes (31% reduction) + +**Benefits:** +- Zero-copy deserialization (faster) +- Structured vector management +- Smaller size saves rent + +--- + +## Related Documentation + +- [ACCOUNTS.md](../ACCOUNTS.md) - CpiContextAccount layout details +- [CPI_CONTEXT.md](../CPI_CONTEXT.md) - How CPI context is used +- [INIT_CPI_CONTEXT_ACCOUNT.md](INIT_CPI_CONTEXT_ACCOUNT.md) - Creating new version 2 accounts diff --git a/programs/system/docs/invoke/INVOKE.md b/programs/system/docs/invoke/INVOKE.md new file mode 100644 index 0000000000..6d1b5d22aa --- /dev/null +++ b/programs/system/docs/invoke/INVOKE.md @@ -0,0 +1,192 @@ +# Invoke + +## Summary + +| Field | Value | +|-------|-------| +| **Discriminator** | `[26, 16, 169, 7, 21, 202, 242, 25]` | +| **Enum** | `InstructionDiscriminator::Invoke` | +| **Path** | `programs/system/src/invoke/` | + +## Description + +Processes compressed account state transitions for a single program (no CPI). This is the direct invocation mode where the authority directly signs for all input compressed accounts without delegating to another program. + +### Use Cases +- Transferring compressed SOL between accounts +- Creating/closing compressed accounts owned by a single authority +- Direct state transitions without multi-program coordination + +### Constraints +- All input compressed accounts must be owned by the authority (signer) +- Input accounts CANNOT have data (only programs can own accounts with data) +- Cannot be used for multi-program transactions (use InvokeCpi for that) + +### Key Differences from InvokeCpi + +| Feature | Invoke | InvokeCpi | +|---------|--------|-----------| +| **Caller** | Direct user invocation | CPI from another program | +| **Authority signer** | Must be a signer | Not required to be a signer | +| **Signer check** | Authority must own all inputs | Invoking program PDA must own inputs | +| **Data accounts** | Input accounts CANNOT have data | Program-owned accounts CAN have data | +| **CPI context** | Not supported | Supported for multi-program txs | +| **Use case** | Compressed SOL transfers, user-owned accounts | Token transfers, custom program PDAs | + +### State Changes +- Input compressed accounts are nullified (inserted into nullifier queue) +- Output compressed accounts are created (inserted into output queue) +- New addresses are created (inserted into address queue) +- SOL can be compressed or decompressed + +--- + +## Instruction Data + +**Source:** `program-libs/compressed-account/src/instruction_data/zero_copy.rs` + +```rust +pub struct ZInstructionDataInvoke<'a> { + pub proof: Option>, + pub input_compressed_accounts_with_merkle_context: + Vec>, + pub output_compressed_accounts: Vec>, + pub relay_fee: Option>, + pub new_address_params: ZeroCopySliceBorsh<'a, ZNewAddressParamsPacked>, + pub compress_or_decompress_lamports: Option>, + pub is_compress: bool, +} +``` + +### Data Layout + +``` +[0..8]: Discriminator +[8..12]: Vec length prefix (4 bytes, always skip) +[12..]: Serialized instruction data +``` + +**Key fields:** +- `proof`: Optional ZK proof (compressed, ~128 bytes when present) +- `input_compressed_accounts_with_merkle_context`: Accounts to nullify (with Merkle context for verification) +- `output_compressed_accounts`: Accounts to create (with packed context) +- `relay_fee`: Optional relay fee amount +- `new_address_params`: Parameters for creating new addresses +- `compress_or_decompress_lamports`: Amount of SOL to compress/decompress +- `is_compress`: true = compress SOL, false = decompress SOL + +--- + +## Accounts + +| Index | Name | Signer | Writable | Description | +|-------|------|--------|----------|-------------| +| 0 | `fee_payer` | Yes | Yes | Pays transaction fees, rollover fees, protocol fees | +| 1 | `authority` | Yes | No | Signs for all input compressed accounts | +| 2 | `registered_program_pda` | No | No | Registered program PDA (checked in account-compression) | +| 3 | `_unused` | No | No | Backwards compatibility slot (previously noop program) | +| 4 | `account_compression_authority` | No | No | PDA authority for account-compression CPI | +| 5 | `account_compression_program` | No | No | Account Compression Program | +| 6 | `sol_pool_pda` | No | Yes (conditional) | Sol pool PDA for compress/decompress operations | +| 7 | `decompression_recipient` | No | Yes (conditional) | Recipient account for decompressed SOL | +| 8 | `system_program` | No | No | System Program (for SOL transfers) | +| 9+ | `remaining_accounts` | - | - | Merkle trees, queues, and other dynamic accounts | + +### Account Validations + +**fee_payer (index 0):** +- Must be a signer +- Must be writable +- Pays for all transaction fees + +**authority (index 1):** +- Must be a signer +- Can be the same account as fee_payer +- Must match the owner of all input compressed accounts + +**sol_pool_pda (index 6):** +- Required when `compress_or_decompress_lamports` is set +- Optional (can be system program placeholder) otherwise + +**decompression_recipient (index 7):** +- Required when decompressing SOL (`is_compress = 0`) +- Must be writable + +### Remaining Accounts + +The remaining accounts array contains Merkle trees and queues in a specific order: + +1. **State Merkle trees** (1 per unique tree in inputs) +2. **State output queues** (1 per unique tree in outputs) +3. **Address Merkle trees** (1 per new address) +4. **Address queues** (1 per new address) + +--- + +## Instruction Logic + +### Step 1: Parse Instruction Data +```rust +let instruction_data = &instruction_data[4..]; // Skip vec prefix +let (inputs, _) = ZInstructionDataInvoke::zero_copy_at(instruction_data)?; +``` + +### Step 2: Parse and Validate Accounts +```rust +let (ctx, remaining_accounts) = InvokeInstruction::from_account_infos(accounts)?; +``` + +### Step 3: Verify Authority Signature +```rust +input_compressed_accounts_signer_check( + &inputs.input_compressed_accounts_with_merkle_context, + ctx.authority.key(), +)?; +``` + +**For each input compressed account:** +- Account owner must equal authority pubkey +- Account must NOT have data (`data.is_none()`) + +### Step 4: Process State Transition +```rust +process::( + wrapped_inputs, + None, // No CPI context + &ctx, + 0, // Default relay fee + remaining_accounts, +)?; +``` + +Executes the full 19-step processing pipeline (see [PROCESSING_PIPELINE.md](../PROCESSING_PIPELINE.md)). + +--- + +## Errors + +| Code | Error | Cause | +|------|-------|-------| +| - | `NotEnoughAccountKeys` | Less than 9 accounts provided | +| - | `MissingRequiredSignature` | fee_payer or authority not a signer | +| - | `IncorrectProgramId` | account_compression_program or system_program wrong | +| 6000 | `SumCheckFailed` | Lamport conservation violated | +| 6001 | `SignerCheckFailed` | Authority doesn't own input account OR account has data | +| 6008 | `CompressedSolPdaUndefinedForCompressSol` | sol_pool_pda missing when compressing | +| 6009 | `DecompressLamportsUndefinedForCompressSol` | compress_or_decompress_lamports missing | +| 6010 | `CompressedSolPdaUndefinedForDecompressSol` | sol_pool_pda missing when decompressing | +| 6011 | `DeCompressLamportsUndefinedForDecompressSol` | compress_or_decompress_lamports missing | +| 6012 | `DecompressRecipientUndefinedForDecompressSol` | decompression_recipient missing when decompressing | +| 6017 | `ProofIsNone` | ZK proof required but not provided | +| 6019 | `EmptyInputs` | No inputs, outputs, or addresses provided | +| 6043 | `ProofVerificationFailed` | ZK proof verification failed | + +See [INSTRUCTIONS.md](../INSTRUCTIONS.md) for complete error list. + +--- + +## Related Documentation + +- [PROCESSING_PIPELINE.md](../PROCESSING_PIPELINE.md) - Detailed 19-step processing flow +- [INSTRUCTIONS.md](../INSTRUCTIONS.md) - Complete error reference +- [InvokeCpi](../invoke_cpi/INVOKE_CPI.md) - CPI invocation mode (Anchor layout) diff --git a/programs/system/docs/invoke_cpi/INVOKE_CPI.md b/programs/system/docs/invoke_cpi/INVOKE_CPI.md new file mode 100644 index 0000000000..aff71a3814 --- /dev/null +++ b/programs/system/docs/invoke_cpi/INVOKE_CPI.md @@ -0,0 +1,198 @@ +# InvokeCpi + +## Summary + +| Field | Value | +|-------|-------| +| **Discriminator** | `[49, 212, 191, 129, 39, 194, 43, 196]` | +| **Enum** | `InstructionDiscriminator::InvokeCpi` | +| **Path** | `programs/system/src/invoke_cpi/` | + +## Description + +Processes compressed account state transitions via CPI (Cross-Program Invocation). This instruction is invoked by other programs (e.g., compressed-token program, custom Anchor programs) to execute compressed account operations on behalf of their users. + +### Use Cases +- Compressed token transfers (invoked by compressed-token program) +- Custom program operations on compressed PDAs +- Multi-program transactions using CPI context +- Any program-owned compressed account state transitions + +### Key Differences from Invoke +| Feature | Invoke | InvokeCpi | +|---------|--------|-----------| +| **Caller** | Direct user invocation | CPI from another program | +| **Signer check** | Authority owns inputs | Invoking program PDA owns inputs | +| **Data accounts** | Not allowed | Required for program-owned accounts | +| **CPI context** | Not supported | Supported for multi-program txs | + +### State Changes +- Input compressed accounts are nullified +- Output compressed accounts are created +- CPI context can be written (first_set_context, set_context) or executed +- SOL can be compressed or decompressed + +--- + +## Instruction Data + +**Source:** `program-libs/compressed-account/src/instruction_data/zero_copy.rs` + +```rust +pub struct ZInstructionDataInvokeCpi<'a> { + pub proof: Option>, + pub input_root_indices: ZeroCopySlice, + pub new_address_params: ZeroCopySlice, + pub input_compressed_accounts_with_merkle_context: + ZeroCopySlice, + pub output_compressed_accounts: ZeroCopySlice, + pub relay_fee: Option>, + pub compress_or_decompress_lamports: Option>, + pub is_compress: u8, + pub cpi_context: Option>, +} +``` + +### Data Layout + +``` +[0..8]: Discriminator (8 bytes) +[8..12]: Vec length prefix (4 bytes, always skip in zero-copy parsing) +[12..]: Zero-copy serialized instruction data (parsed by ZInstructionDataInvokeCpi) +``` + +**CPI-specific field:** +- `cpi_context`: Controls CPI context mode + - `None`: Execute immediately without CPI context + - `Some(CompressedCpiContext { first_set_context: true, set_context: false })`: First write + - `Some(CompressedCpiContext { first_set_context: false, set_context: true })`: Subsequent write + - `Some(CompressedCpiContext { first_set_context: false, set_context: false })`: Execute from context + +--- + +## Accounts + +| Index | Name | Signer | Writable | Description | +|-------|------|--------|----------|-------------| +| 0 | `fee_payer` | Yes | Yes | Pays transaction fees | +| 1 | `authority` | No | No | Authority (not necessarily signer in CPI) | +| 2 | `registered_program_pda` | No | No | Registered program PDA | +| 3 | `_unused` | No | No | Reserved slot (skipped during validation) | +| 4 | `account_compression_authority` | No | No | PDA for account-compression CPI | +| 5 | `account_compression_program` | No | No | Account Compression Program | +| 6 | `invoking_program` | No | No | Program making the CPI (verified as signer) | +| 7 | `sol_pool_pda` | No | Yes (conditional) | Sol pool PDA for compress/decompress | +| 8 | `decompression_recipient` | No | Yes (conditional) | Recipient for decompressed SOL | +| 9 | `system_program` | No | No | System Program | +| 10 | `cpi_context_account` | No | Yes (conditional) | CPI context account | +| 11+ | `remaining_accounts` | - | - | Merkle trees, queues | + +### Account Validations + +**authority (index 1):** +- Not required to be a signer (CPI mode) +- Must be a PDA derived from invoking_program with seeds `[CPI_AUTHORITY_PDA_SEED]` +- Proves the invoking program controls the authority + +**invoking_program (index 6):** +- Must be the program making the CPI call +- Must match the authority PDA derivation + +**cpi_context_account (index 10):** +- Required when `cpi_context` instruction data is present +- Must be owned by Light System Program +- Must have version 2 discriminator +- Must be associated with first Merkle tree + +--- + +## Instruction Logic + +### Step 1: Parse Instruction Data +```rust +let instruction_data = &instruction_data[4..]; // Skip 4-byte vec length prefix +let (inputs, _) = ZInstructionDataInvokeCpi::zero_copy_at(instruction_data)?; +``` + +### Step 2: Parse and Validate Accounts +```rust +let (ctx, remaining_accounts) = InvokeCpiInstruction::from_account_infos(accounts)?; +``` + +### Step 3: CPI Signer Checks +```rust +cpi_signer_checks::( + &invoking_program, + accounts.get_authority().key(), + &instruction_data, +)?; +``` + +**Three-part validation:** + +1. **Authority PDA Check** - Derive PDA from invoking_program with seeds `[CPI_AUTHORITY_PDA_SEED]`, verify it matches authority +2. **Input Account Ownership Check** - For each input: verify owner == invoking_program +3. **Output Account Write Access Check** - For outputs with data: verify owner == invoking_program + +### Step 4: Process CPI Context or Execute + +**Write Mode (first_set_context or set_context):** +- Write/append instruction data to CPI context account +- Return early without executing + +**Execute Mode (neither flag set):** +- Read data from CPI context account +- Combine with current instruction data +- Execute combined state transition with proof + +**No CPI context:** +```rust +process::( + wrapped_inputs, + None, + &ctx, + 0, + remaining_accounts, +)?; +``` + +--- + +## CPI Context Modes + +| Mode | Flags | Behavior | +|------|-------|----------| +| **First Set** | `first_set_context = true` | Clear account, set fee payer, store data, return early | +| **Set Context** | `set_context = true` | Validate fee payer, append data, return early | +| **Execute** | Both `false` | Read all data, verify proof, execute, clear account | +| **No Context** | `cpi_context = None` | Execute immediately with current instruction data only | + +See [CPI_CONTEXT.md](../CPI_CONTEXT.md) for detailed explanation. + +--- + +## Errors + +| Code | Error | Cause | +|------|-------|-------| +| - | `NotEnoughAccountKeys` | Less than 11 accounts provided | +| 6002 | `CpiSignerCheckFailed` | Invoking program doesn't match PDA signer | +| 6014 | `InvokingProgramNotProvided` | invoking_program account missing | +| 6020 | `CpiContextAccountUndefined` | CPI context data present but account missing | +| 6021 | `CpiContextEmpty` | CPI context account empty during execute | +| 6022 | `CpiContextMissing` | CPI context instruction data missing when expected | +| 6027 | `CpiContextFeePayerMismatch` | Fee payer doesn't match first invocation | +| 6028 | `CpiContextAssociatedMerkleTreeMismatch` | Wrong Merkle tree association | +| 6049 | `CpiContextAlreadySet` | Attempting to set when already set | +| 6054 | `CpiContextPassedAsSetContext` | Account doesn't exist but marked as set_context | + +See [INSTRUCTIONS.md](../INSTRUCTIONS.md) for complete error list. + +--- + +## Related Documentation + +- [CPI_CONTEXT.md](../CPI_CONTEXT.md) - Detailed CPI context explanation +- [PROCESSING_PIPELINE.md](../PROCESSING_PIPELINE.md) - Processing flow +- [INVOKE.md](../invoke/INVOKE.md) - Direct invocation comparison +- [INVOKE_CPI_WITH_READ_ONLY.md](INVOKE_CPI_WITH_READ_ONLY.md) - With read-only support diff --git a/programs/system/docs/invoke_cpi/INVOKE_CPI_WITH_ACCOUNT_INFO.md b/programs/system/docs/invoke_cpi/INVOKE_CPI_WITH_ACCOUNT_INFO.md new file mode 100644 index 0000000000..9fc4592aa9 --- /dev/null +++ b/programs/system/docs/invoke_cpi/INVOKE_CPI_WITH_ACCOUNT_INFO.md @@ -0,0 +1,251 @@ +# InvokeCpiWithAccountInfo + +## Summary + +| Field | Value | +|-------|-------| +| **Discriminator** | `[228, 34, 128, 84, 47, 139, 86, 240]` | +| **Enum** | `InstructionDiscriminator::InvokeCpiWithAccountInfo` | +| **Path** | `programs/system/src/lib.rs` | + +## Description + +Advanced CPI invocation instruction with dynamic account configuration. This instruction supports V2 account mode where the account list is determined by `AccountOptions` configuration flags embedded in the instruction data, enabling more flexible and efficient account passing. + +### Use Cases +- Programs that want to minimize account overhead +- Dynamic account configurations based on operation type +- Advanced CPI scenarios with optional accounts +- Programs using non-Anchor account layouts + +### Key Differences from Other Instructions + +| Feature | InvokeCpi | InvokeCpiWithReadOnly | InvokeCpiWithAccountInfo | +|---------|-----------|------------------------|---------------------------| +| **Account layout** | Fixed (Anchor) | Anchor or V2 | V2 only | +| **Read-only** | No | Yes | Yes | +| **Configuration** | Implicit | Explicit (mode field) | Explicit (AccountOptions) | +| **Overhead** | Highest | Medium | Lowest | + +### State Changes +- Input compressed accounts are nullified +- Output compressed accounts are created +- Read-only accounts are verified +- Read-only addresses are verified +- SOL can be compressed or decompressed + +--- + +## Instruction Data + +**Source:** `program-libs/compressed-account/src/instruction_data/with_account_info.rs` + +```rust +pub struct InstructionDataInvokeCpiWithAccountInfo<'a> { + pub invoking_program_id: Pubkey, + pub mode: u8, // Always 1 (V2 mode) + pub account_option_config: AccountOptions, + pub proof: Option>, + pub new_address_params: ZeroCopySlice, + pub account_infos: Vec, + pub relay_fee: Option, + pub compress_or_decompress_lamports: Option, + pub is_compress: bool, + pub cpi_context: Option, + pub read_only_accounts: ZeroCopySlice, + pub read_only_addresses: ZeroCopySlice, +} +``` + +### AccountOptions Configuration + +```rust +pub struct AccountOptions { + pub sol_pool_pda: bool, + pub decompression_recipient: bool, + pub cpi_context_account: bool, + pub write_to_cpi_context: bool, +} +``` + +| Flag | When true | When false | +|------|-----------|------------| +| `write_to_cpi_context` | Omits all execution accounts (4 saved) | Includes execution accounts | +| `sol_pool_pda` | Includes sol_pool_pda | Omits (1 saved) | +| `decompression_recipient` | Includes recipient | Omits (1 saved) | +| `cpi_context_account` | Includes CPI context account | Omits (1 saved) | + +--- + +## Accounts (V2 Mode) + +### Base Accounts (Always Present) + +| Index | Name | Signer | Writable | Description | +|-------|------|--------|----------|-------------| +| 0 | `fee_payer` | Yes | Yes | Pays transaction fees | +| 1 | `authority` | Yes | No | Authority signer | + +### Conditional Execution Accounts + +Present only when `write_to_cpi_context = false`: + +| Name | Signer | Writable | Description | +|------|--------|----------|-------------| +| `registered_program_pda` | No | No | Registered program PDA | +| `account_compression_authority` | No | No | PDA for account-compression CPI | +| `account_compression_program` | No | No | Account Compression Program | +| `system_program` | No | No | System Program | + +### Optional Accounts + +| Name | Flag | Writable | Description | +|------|------|----------|-------------| +| `sol_pool_pda` | `sol_pool_pda` | Yes | Sol pool PDA | +| `decompression_recipient` | `decompression_recipient` | Yes | Recipient for decompressed SOL | +| `cpi_context_account` | `cpi_context_account` | Yes | CPI context account | + +--- + +## Account Layout Examples + +### Execute Mode (write_to_cpi_context = false) + +**Full configuration (all flags true):** +``` +[0] fee_payer (signer, writable) +[1] authority (signer) +[2] registered_program_pda +[3] account_compression_authority +[4] account_compression_program +[5] system_program +[6] sol_pool_pda +[7] decompression_recipient +[8] cpi_context_account +[9+] remaining_accounts +``` + +**Minimal configuration:** +``` +[0] fee_payer +[1] authority +[2] registered_program_pda +[3] account_compression_authority +[4] account_compression_program +[5] system_program +[6+] remaining_accounts +``` + +### CPI Context Write Mode (write_to_cpi_context = true) + +**Most compact configuration (3 accounts only):** +``` +[0] fee_payer +[1] authority +[2] cpi_context_account +``` + +--- + +## Instruction Logic + +### Step 1: Parse Instruction Data +```rust +let (inputs, _) = InstructionDataInvokeCpiWithAccountInfo::zero_copy_at(instruction_data)?; +``` + +### Step 2: Validate Mode +```rust +let mode = AccountMode::try_from(inputs.mode)?; +assert_eq!(mode, AccountMode::V2); // V2 only +``` + +### Step 3: Parse Accounts with Dynamic Configuration +```rust +let (ctx, remaining_accounts) = InvokeCpiInstructionV2::from_account_infos( + accounts, + inputs.account_option_config, +)?; +``` + +The `AccountIterator` consumes accounts sequentially based on `AccountOptions` flags. + +### Step 4: Process Invocation +```rust +process_invoke_cpi::( + invoking_program, + ctx, + inputs, + remaining_accounts, +)?; +``` + +--- + +## AccountOptions Usage Patterns + +### Pattern 1: Minimal (No Optional Accounts) +```rust +AccountOptions { + write_to_cpi_context: false, + sol_pool_pda: false, + decompression_recipient: false, + cpi_context_account: false, +} +// Accounts: 6 + remaining +``` + +### Pattern 2: SOL Compression +```rust +AccountOptions { + write_to_cpi_context: false, + sol_pool_pda: true, + decompression_recipient: false, + cpi_context_account: false, +} +// Accounts: 7 + remaining +``` + +### Pattern 3: SOL Decompression +```rust +AccountOptions { + write_to_cpi_context: false, + sol_pool_pda: true, + decompression_recipient: true, + cpi_context_account: false, +} +// Accounts: 8 + remaining +``` + +### Pattern 4: CPI Context Write +```rust +AccountOptions { + write_to_cpi_context: true, + sol_pool_pda: false, + decompression_recipient: false, + cpi_context_account: true, +} +// Accounts: 3 only (no remaining) +``` + +--- + +## Errors + +| Code | Error | Cause | +|------|-------|-------| +| - | `NotEnoughAccountKeys` | Fewer accounts than configuration requires | +| 6044 | `InvalidAccountMode` | mode != 1 (V2) | +| 6054 | `CpiContextPassedAsSetContext` | write_to_cpi_context but no CPI context account | +| 6057 | `InvalidAccountIndex` | Account index out of bounds during parsing | + +See [INSTRUCTIONS.md](../INSTRUCTIONS.md) for complete error list. + +--- + +## Related Documentation + +- [INVOKE_CPI.md](INVOKE_CPI.md) - Base CPI invocation (Anchor mode) +- [INVOKE_CPI_WITH_READ_ONLY.md](INVOKE_CPI_WITH_READ_ONLY.md) - Read-only support +- [CPI_CONTEXT.md](../CPI_CONTEXT.md) - CPI context usage +- [PROCESSING_PIPELINE.md](../PROCESSING_PIPELINE.md) - Processing flow diff --git a/programs/system/docs/invoke_cpi/INVOKE_CPI_WITH_READ_ONLY.md b/programs/system/docs/invoke_cpi/INVOKE_CPI_WITH_READ_ONLY.md new file mode 100644 index 0000000000..010cd21412 --- /dev/null +++ b/programs/system/docs/invoke_cpi/INVOKE_CPI_WITH_READ_ONLY.md @@ -0,0 +1,199 @@ +# InvokeCpiWithReadOnly + +## Summary + +| Field | Value | +|-------|-------| +| **Discriminator** | `[86, 47, 163, 166, 21, 223, 92, 8]` | +| **Enum** | `InstructionDiscriminator::InvokeCpiWithReadOnly` | +| **Path** | `programs/system/src/lib.rs` | + +## Description + +Extended CPI invocation instruction that supports read-only compressed accounts and addresses. This enables programs to verify the existence of compressed state without modifying it, useful for authorization checks and multi-account validations. + +### Use Cases +- Verify compressed account exists before performing operations +- Check compressed PDA state for authorization +- Read-only access to compressed token balances +- Proof-of-ownership checks without state modification + +### Key Differences from InvokeCpi +| Feature | InvokeCpi | InvokeCpiWithReadOnly | +|---------|-----------|------------------------| +| **Read-only accounts** | Not supported | Supported | +| **Read-only addresses** | Not supported | Supported (execute mode only) | +| **Account mode** | Fixed (Anchor) | Configurable (Anchor/V2) | +| **Invoking program ID** | Implicit (from accounts) | Explicit (in instruction data) | + +### State Changes +- Input compressed accounts are nullified (writable inputs only) +- Output compressed accounts are created +- Read-only accounts are verified but not modified +- Read-only addresses are verified to exist + +--- + +## Instruction Data + +**Source:** `program-libs/compressed-account/src/instruction_data/with_readonly.rs` + +```rust +pub struct ZInstructionDataInvokeCpiWithReadOnly<'a> { + // Metadata (fixed layout) + pub mode: u8, // AccountMode: 0 = Anchor, 1 = V2 + pub bump: u8, + pub invoking_program_id: Pubkey, + pub compress_or_decompress_lamports: U64, + pub is_compress: bool, + pub with_cpi_context: bool, + pub with_transaction_hash: bool, + pub cpi_context: ZCompressedCpiContext, + + // Variable-length fields (zero-copy slices) + pub proof: Option>, + pub new_address_params: ZeroCopySliceBorsh<'a, ZNewAddressParamsAssignedPacked>, + pub input_compressed_accounts: Vec>, + pub output_compressed_accounts: Vec>, + pub read_only_addresses: ZeroCopySliceBorsh<'a, ZPackedReadOnlyAddress>, + pub read_only_accounts: ZeroCopySliceBorsh<'a, ZPackedReadOnlyCompressedAccount>, +} +``` + +### Key Fields + +**Mode Selection:** +- `mode`: Account mode (0 = Anchor, 1 = V2) +- `invoking_program_id`: Program ID making the CPI (embedded in instruction data) +- `bump`: PDA bump seed for signer verification + +**Read-Only Features:** +- `read_only_accounts`: Compressed accounts to verify existence without modification +- `read_only_addresses`: Addresses to verify existence in address tree + +--- + +## Accounts + +Account layout depends on the `mode` field: + +### Anchor Mode (mode = 0) + +Same 11 accounts as [INVOKE_CPI.md](INVOKE_CPI.md): + +| Index | Name | Signer | Writable | Description | +|-------|------|--------|----------|-------------| +| 0-10 | (same as InvokeCpi) | - | - | See InvokeCpi documentation | +| 11+ | `remaining_accounts` | - | - | Merkle trees, queues | + +### V2 Mode (mode = 1) + +Dynamic account list based on `account_option_config`: + +| Index | Name | Signer | Writable | Description | +|-------|------|--------|----------|-------------| +| 0 | `fee_payer` | Yes | Yes | Pays transaction fees | +| 1 | `authority` | Yes | No | Authority (signer in V2 mode) | +| 2+ | Dynamic accounts | - | - | Determined by account_option_config | + +--- + +## Instruction Logic + +### Processing Flow +1. **CPI signer checks:** Verify invoking program authority +2. **CPI context processing:** Handle first_set_context, set_context, or execute modes +3. **Main processing pipeline:** Execute 19-step pipeline + - Step 8: **Read-only account verification** + - By index: Direct leaf lookup (if prove_by_index = true) + - By ZKP: Inclusion proof verification (if prove_by_index = false) + - Duplicate check: Ensure read-only accounts don't overlap with writable inputs + - Step 9: **ZK proof verification** (includes read-only proofs) +4. **Read-only address verification** (execute mode only): + - Non-inclusion check: Verify address not in bloom filter queue + - Inclusion proof: Verify address exists in address Merkle tree + +--- + +## Read-Only Account Processing + +### Read-Only Compressed Accounts + +```rust +pub struct ZPackedReadOnlyCompressedAccount { + pub account_hash: [u8; 32], + pub merkle_context: ZPackedMerkleContext, + pub root_index: U16, +} +``` + +- `account_hash`: Pre-computed hash of the compressed account +- `merkle_context`: Merkle tree location (tree index, queue index, leaf index) +- `root_index`: Index of the Merkle root to verify against + +**Verification:** Account hash inclusion in Merkle tree (by index or ZKP). Account is NOT nullified. + +### Read-Only Addresses + +```rust +pub struct ZPackedReadOnlyAddress { + pub address: [u8; 32], + pub address_merkle_tree_account_index: u8, + pub address_merkle_tree_root_index: U16, +} +``` + +**Verification:** +1. Bloom filter check: Verify address is NOT in queue (not pending insertion) +2. Inclusion proof: Verify address exists in address Merkle tree + +--- + +## Limitations + +### Read-Only Addresses with CPI Context + +Read-only addresses cannot be used when writing to CPI context account (first_set_context or set_context modes). + +```rust +if let Some(readonly_addresses) = instruction_data.read_only_addresses() { + if !readonly_addresses.is_empty() { + return Err(SystemProgramError::Unimplemented)?; + } +} +``` + +**Workaround:** Perform read-only address verification in the final executing program. + +--- + +## Errors + +| Code | Error | Cause | +|------|-------|-------| +| 6034 | `ReadOnlyAddressAlreadyExists` | Read-only address found in bloom filter queue | +| 6035 | `ReadOnlyAccountDoesNotExist` | Read-only account hash not found in Merkle tree | +| 6053 | `DuplicateAccountInInputsAndReadOnly` | Same account in both writable inputs and read-only | +| 6063 | `Unimplemented` | Read-only addresses with CPI context write mode | + +See [INSTRUCTIONS.md](../INSTRUCTIONS.md) for complete error list. + +--- + +## Account Mode Comparison + +| Feature | Anchor Mode (0) | V2 Mode (1) | +|---------|-----------------|-------------| +| **Account count** | Fixed 11 base accounts | Dynamic (2+ base) | +| **Account order** | Strict, predefined | Flexible, config-driven | +| **Overhead** | Higher (all 11 accounts required) | Lower (only needed accounts) | +| **Best for** | Existing Anchor integrations | New programs, optimized size | + +--- + +## Related Documentation + +- [INVOKE_CPI.md](INVOKE_CPI.md) - Base CPI invocation (no read-only support) +- [INVOKE_CPI_WITH_ACCOUNT_INFO.md](INVOKE_CPI_WITH_ACCOUNT_INFO.md) - V2 mode with AccountOptions +- [PROCESSING_PIPELINE.md](../PROCESSING_PIPELINE.md) - Complete 19-step processing pipeline +- [CPI_CONTEXT.md](../CPI_CONTEXT.md) - Multi-program transaction coordination