diff --git a/program-libs/account-checks/CLAUDE.md b/program-libs/account-checks/CLAUDE.md new file mode 100644 index 0000000000..41ee74adf8 --- /dev/null +++ b/program-libs/account-checks/CLAUDE.md @@ -0,0 +1,81 @@ +# Summary +- Unified account validation for both solana-program and pinocchio SDKs +- AccountInfoTrait abstraction enabling single codebase across SDK implementations +- Validation checks with 8-byte discriminators for account type safety +- AccountIterator providing detailed error locations (file:line:column) +- Error codes 12006-12021 with automatic ProgramError conversion + +# Used in +- `light-compressed-token` - Validates all account inputs in compressed token instructions +- `light-system` - Core validation for compressed account operations +- `light-compressible` - Validates CompressibleConfig accounts and PDAs +- `light-ctoken-types` - Uses AccountInfoTrait for runtime-agnostic account handling +- `light-account-compression` - Merkle tree account validation +- `light-batched-merkle-tree` - Batch operation account checks +- `compressed-token-sdk` - Uses validation helpers in instruction builders +- `light-sdk` - Core SDK account validation utilities +- `light-sdk-pinocchio` - Pinocchio-specific SDK validation +- `light-sdk-types` - Uses AccountInfoTrait for CPI context and tree info +- `light-compressed-token-types` - Uses AccountInfoTrait for instruction account structures + +# Navigation +- This file: Overview and module organization +- For detailed documentation on specific components, see the `docs/` directory: + - `docs/CLAUDE.md` - Navigation guide for detailed documentation + - `docs/ACCOUNT_INFO_TRAIT.md` - AccountInfoTrait abstraction and implementations + - `docs/ACCOUNT_CHECKS.md` - Account validation functions and patterns + - `docs/ACCOUNT_ITERATOR.md` - Enhanced iterator with error reporting + - `docs/ERRORS.md` - Error codes (12006-12021), causes, and resolutions + - `docs/DISCRIMINATOR.md` - Discriminator trait for account type identification + - `docs/PACKED_ACCOUNTS.md` - Index-based account access utility + +# Source Code Structure + +## Core Types (`src/`) + +### Account Abstraction (`account_info/`) +- `account_info_trait.rs` - AccountInfoTrait definition abstracting over SDK differences + - Unified data access interface (`try_borrow_data`, `try_borrow_mut_data`) + - PDA derivation functions (`find_program_address`, `create_program_address`) + - Ownership and permission checks +- `pinocchio.rs` - Pinocchio AccountInfo implementation (feature: `pinocchio`) +- `solana.rs` - Solana AccountInfo implementation (feature: `solana`) +- `test_account_info.rs` - Mock implementation for unit testing (feature: `test-only`) + +### Validation Functions (`checks.rs`) +- Account initialization (`account_info_init` - sets discriminator) +- Ownership validation (`check_owner`, `check_program`) +- Permission checks (`check_mut`, `check_non_mut`, `check_signer`) +- Discriminator validation (`check_discriminator`, `set_discriminator`) +- PDA validation (`check_pda_seeds`, `check_pda_seeds_with_bump`) +- Rent exemption checks (`check_account_balance_is_rent_exempt`) +- Combined validators (`check_account_info_mut`, `check_account_info_non_mut`) + +### Account Processing (`account_iterator.rs`) +- Sequential account processing with enhanced error messages +- Named account retrieval with automatic validation +- Location tracking for debugging (file:line:column in errors) +- Convenience methods: `next_signer`, `next_mut`, `next_non_mut` +- Optional account handling (`next_option`, `next_option_mut`) + +### Account Type Identification (`discriminator.rs`) +- Discriminator trait for 8-byte account type prefixes +- Constant discriminator arrays for compile-time verification +- Integration with zero-copy deserialization + +### Dynamic Access (`packed_accounts.rs`) +- Index-based account access for dynamic account sets +- Bounds-checked retrieval with descriptive error messages +- Used for accessing mint, owner, delegate accounts by index + +### Error Handling (`error.rs`) +- AccountError enum with 16 variants (codes 12006-12021) +- Automatic conversions to ProgramError for both SDKs +- Pinocchio ProgramError mapping (standard codes 1-11) +- BorrowError conversions for safe data access + +## Feature Flags +- `solana` - Enables solana-program AccountInfo implementation +- `pinocchio` - Enables pinocchio AccountInfo implementation +- `test-only` - Enables test utilities and mock implementations +- Default: No features (trait definitions only) diff --git a/program-libs/account-checks/docs/ACCOUNT_CHECKS.md b/program-libs/account-checks/docs/ACCOUNT_CHECKS.md new file mode 100644 index 0000000000..2a8e4a66f3 --- /dev/null +++ b/program-libs/account-checks/docs/ACCOUNT_CHECKS.md @@ -0,0 +1,325 @@ +# Account Validation Functions + +**Path:** `program-libs/account-checks/src/checks.rs` + +## Description + +Comprehensive validation functions for Solana account verification. All functions are generic over `AccountInfoTrait`, enabling use with both Solana and Pinocchio runtimes. + +## Core Validation Functions + +### Ownership Validation + +#### `check_owner` +```rust +fn check_owner( + owner: &[u8; 32], + account_info: &A +) -> Result<(), AccountError> +``` +- Verifies account is owned by specified program +- **Error:** `AccountOwnedByWrongProgram` (20001) + +#### `check_program` +```rust +fn check_program( + program_id: &[u8; 32], + account_info: &A +) -> Result<(), AccountError> +``` +- Verifies account key matches program_id AND is executable +- **Errors:** + - `InvalidProgramId` (20011) - Key mismatch + - `ProgramNotExecutable` (20012) - Not marked executable + +### Permission Validation + +#### `check_signer` +```rust +fn check_signer( + account_info: &A +) -> Result<(), AccountError> +``` +- Verifies account is transaction signer +- **Error:** `InvalidSigner` (20009) + +#### `check_mut` +```rust +fn check_mut( + account_info: &A +) -> Result<(), AccountError> +``` +- Verifies account is writable +- **Error:** `AccountNotMutable` (20002) + +#### `check_non_mut` +```rust +fn check_non_mut( + account_info: &A +) -> Result<(), AccountError> +``` +- Verifies account is NOT writable +- **Error:** `AccountMutable` (20005) + +### Discriminator Functions + +#### `check_discriminator` +```rust +fn check_discriminator( + bytes: &[u8] +) -> Result<(), AccountError> +``` +- Verifies first 8 bytes match expected discriminator +- **Errors:** + - `InvalidAccountSize` (20004) - Less than 8 bytes + - `InvalidDiscriminator` (20000) - Mismatch + +#### `set_discriminator` +```rust +fn set_discriminator( + bytes: &mut [u8] +) -> Result<(), AccountError> +``` +- Sets 8-byte discriminator on uninitialized account +- **Error:** `AlreadyInitialized` (20006) - Non-zero discriminator + +#### `account_info_init` +```rust +fn account_info_init( + account_info: &A +) -> Result<(), AccountError> +``` +- Initializes account with discriminator +- **Errors:** + - `BorrowAccountDataFailed` (20003) + - `AlreadyInitialized` (20006) + +### Combined Validators + +#### `check_account_info` +```rust +fn check_account_info( + program_id: &[u8; 32], + account_info: &A +) -> Result<(), AccountError> +``` +Validates: +1. Ownership by program_id +2. Discriminator matches type T + +#### `check_account_info_mut` +```rust +fn check_account_info_mut( + program_id: &[u8; 32], + account_info: &A +) -> Result<(), AccountError> +``` +Validates: +1. Account is writable +2. Ownership by program_id +3. Discriminator matches type T + +#### `check_account_info_non_mut` +```rust +fn check_account_info_non_mut( + program_id: &[u8; 32], + account_info: &A +) -> Result<(), AccountError> +``` +Validates: +1. Account is NOT writable +2. Ownership by program_id +3. Discriminator matches type T + +### PDA Validation + +#### `check_pda_seeds` +```rust +fn check_pda_seeds( + seeds: &[&[u8]], + program_id: &[u8; 32], + account_info: &A +) -> Result<(), AccountError> +``` +- Derives PDA and verifies it matches account key +- Uses `find_program_address` (finds bump) +- **Error:** `InvalidSeeds` (20010) + +#### `check_pda_seeds_with_bump` +```rust +fn check_pda_seeds_with_bump( + seeds: &[&[u8]], // Must include bump + program_id: &[u8; 32], + account_info: &A +) -> Result<(), AccountError> +``` +- Verifies PDA with known bump seed +- Uses `create_program_address` (requires bump) +- **Error:** `InvalidSeeds` (20010) + +### Rent Validation + +#### `check_account_balance_is_rent_exempt` +```rust +fn check_account_balance_is_rent_exempt( + account_info: &A, + expected_size: usize +) -> Result +``` +- Verifies account size and rent exemption +- Returns rent exemption amount +- **Errors:** + - `InvalidAccountSize` (20004) - Size mismatch + - `InvalidAccountBalance` (20007) - Below rent exemption + - `FailedBorrowRentSysvar` (20008) - Can't access rent + +### Initialization Check + +#### `check_data_is_zeroed` +```rust +fn check_data_is_zeroed( + data: &[u8] +) -> Result<(), AccountError> +``` +- Verifies first N bytes are zero (uninitialized) +- **Error:** `AccountNotZeroed` (20013) + +## Usage Examples + +### Initialize New Account +```rust +use light_account_checks::checks::{account_info_init, check_account_balance_is_rent_exempt}; + +struct MyAccount; +impl Discriminator for MyAccount { + const LIGHT_DISCRIMINATOR: [u8; 8] = [180, 4, 231, 26, 220, 144, 55, 168]; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; +} + +fn initialize_account( + account: &A, + expected_size: usize, +) -> Result<(), AccountError> { + // Check rent exemption + check_account_balance_is_rent_exempt(account, expected_size)?; + + // Set discriminator + account_info_init::(account)?; + + Ok(()) +} +``` + +### Validate Mutable Account +```rust +use light_account_checks::checks::check_account_info_mut; + +fn process_update( + program_id: &[u8; 32], + account: &A, +) -> Result<(), AccountError> { + // Validates: writable + owned by program + correct discriminator + check_account_info_mut::(program_id, account)?; + + // Safe to modify account data + let mut data = account.try_borrow_mut_data()?; + // ... modifications + Ok(()) +} +``` + +### Validate PDA +```rust +use light_account_checks::checks::{check_pda_seeds, check_owner}; + +fn validate_config_pda( + program_id: &[u8; 32], + config_account: &A, + authority: &[u8; 32], +) -> Result<(), AccountError> { + // Build seeds + let seeds = &[b"config", authority.as_ref()]; + + // Verify PDA derivation + check_pda_seeds(seeds, program_id, config_account)?; + + // Verify ownership + check_owner(program_id, config_account)?; + + Ok(()) +} +``` + +### Combined Validation Pattern +```rust +fn process_instruction( + program_id: &[u8; 32], + accounts: &[A], +) -> Result<(), AccountError> { + let authority = &accounts[0]; + let target = &accounts[1]; + let config = &accounts[2]; + + // Authority must sign + check_signer(authority)?; + + // Target must be mutable and owned by program + check_account_info_mut::(program_id, target)?; + + // Config must be read-only + check_account_info_non_mut::(program_id, config)?; + + Ok(()) +} +``` + +## Validation Patterns + +### Account State Machine +```rust +// 1. Uninitialized -> check zeroed discriminator +check_data_is_zeroed::<8>(&account_data)?; + +// 2. Initialize -> set discriminator +set_discriminator::(&mut account_data)?; + +// 3. Initialized -> validate discriminator +check_discriminator::(&account_data)?; +``` + +### PDA with Stored vs Derived Bump +```rust +// If bump is stored in account data +let seeds_with_bump = &[b"config", authority.as_ref(), &[stored_bump]]; +check_pda_seeds_with_bump(seeds_with_bump, program_id, account)?; + +// If bump needs to be found +let seeds = &[b"config", authority.as_ref()]; +check_pda_seeds(seeds, program_id, account)?; +``` + +## Error Reference + +| Function | Error Code | Error Name | Condition | +|----------|------------|------------|-----------| +| `check_owner` | 20001 | AccountOwnedByWrongProgram | Owner mismatch | +| `check_mut` | 20002 | AccountNotMutable | Not writable | +| `check_discriminator` | 20000 | InvalidDiscriminator | Wrong type | +| `check_discriminator` | 20004 | InvalidAccountSize | < 8 bytes | +| `set_discriminator` | 20006 | AlreadyInitialized | Non-zero disc | +| `check_non_mut` | 20005 | AccountMutable | Is writable | +| `check_signer` | 20009 | InvalidSigner | Not signer | +| `check_pda_seeds*` | 20010 | InvalidSeeds | PDA mismatch | +| `check_program` | 20011 | InvalidProgramId | Key mismatch | +| `check_program` | 20012 | ProgramNotExecutable | Not executable | +| `check_data_is_zeroed` | 20013 | AccountNotZeroed | Has data | +| `check_account_balance_*` | 20004 | InvalidAccountSize | Size mismatch | +| `check_account_balance_*` | 20007 | InvalidAccountBalance | Low balance | +| `check_account_balance_*` | 20008 | FailedBorrowRentSysvar | Can't get rent | +| `account_info_init` | 20003 | BorrowAccountDataFailed | Can't borrow | + +## See Also +- [ACCOUNT_INFO_TRAIT.md](ACCOUNT_INFO_TRAIT.md) - AccountInfoTrait abstraction +- [DISCRIMINATOR.md](DISCRIMINATOR.md) - Discriminator trait details +- [ACCOUNT_ITERATOR.md](ACCOUNT_ITERATOR.md) - Sequential validation +- [ERRORS.md](ERRORS.md) - Complete error documentation diff --git a/program-libs/account-checks/docs/ACCOUNT_INFO_TRAIT.md b/program-libs/account-checks/docs/ACCOUNT_INFO_TRAIT.md new file mode 100644 index 0000000000..345bb5d108 --- /dev/null +++ b/program-libs/account-checks/docs/ACCOUNT_INFO_TRAIT.md @@ -0,0 +1,183 @@ +# AccountInfoTrait + +**Path:** `program-libs/account-checks/src/account_info/account_info_trait.rs` + +## Description + +AccountInfoTrait provides a unified abstraction layer over different Solana SDK AccountInfo implementations (solana-program and pinocchio). This trait enables writing SDK-agnostic validation code that works seamlessly with both SDKs without conditional compilation in business logic. + +## Trait Definition + +```rust +pub trait AccountInfoTrait { + type Pubkey: Copy + Clone + Debug + PartialEq; + type DataRef<'a>: Deref where Self: 'a; + type DataRefMut<'a>: DerefMut where Self: 'a; + + // Core account access + fn key(&self) -> [u8; 32]; + fn pubkey(&self) -> Self::Pubkey; + fn is_writable(&self) -> bool; + fn is_signer(&self) -> bool; + fn executable(&self) -> bool; + fn lamports(&self) -> u64; + fn data_len(&self) -> usize; + + // Data borrowing + fn try_borrow_data(&self) -> Result, AccountError>; + fn try_borrow_mut_data(&self) -> Result, AccountError>; + + // Ownership and PDAs + fn is_owned_by(&self, program: &[u8; 32]) -> bool; + fn find_program_address(seeds: &[&[u8]], program_id: &[u8; 32]) -> ([u8; 32], u8); + fn create_program_address(seeds: &[&[u8]], program_id: &[u8; 32]) -> Result<[u8; 32], AccountError>; + + // Rent + fn get_min_rent_balance(size: usize) -> Result; + + // Utilities + fn pubkey_from_bytes(bytes: [u8; 32]) -> Self::Pubkey; + fn data_is_empty(&self) -> bool; +} +``` + +## Implementations + +### Solana AccountInfo +**Path:** `program-libs/account-checks/src/account_info/solana.rs` +**Feature:** `solana` + +- **Pubkey type:** `solana_pubkey::Pubkey` +- **Data references:** `std::cell::Ref` / `std::cell::RefMut` +- **PDA functions:** Uses `solana_pubkey` for derivation +- **Rent:** Accesses `solana_sysvar::rent::Rent` sysvar + +### Pinocchio AccountInfo +**Path:** `program-libs/account-checks/src/account_info/pinocchio.rs` +**Feature:** `pinocchio` + +- **Pubkey type:** `[u8; 32]` (raw bytes for efficiency) +- **Data references:** `pinocchio::account_info::Ref` / `RefMut` +- **PDA functions:** Native pinocchio implementations on-chain, falls back to solana_pubkey off-chain +- **Rent:** Uses pinocchio sysvar access + +### Test AccountInfo +**Path:** `program-libs/account-checks/src/account_info/test_account_info.rs` +**Feature:** `test-only` + +Mock implementation for unit testing with configurable behavior and no external dependencies. + +## Usage Examples + +### Generic Function with AccountInfoTrait +```rust +use light_account_checks::{AccountInfoTrait, AccountError}; + +fn validate_owner( + account: &A, + expected_owner: &[u8; 32], +) -> Result<(), AccountError> { + if !account.is_owned_by(expected_owner) { + return Err(AccountError::AccountOwnedByWrongProgram); + } + Ok(()) +} +``` + +### Working with Either SDK +```rust +// With solana-program +#[cfg(feature = "solana")] +fn process_instruction_solana( + accounts: &[solana_account_info::AccountInfo], +) -> Result<(), ProgramError> { + let owner_account = &accounts[0]; + validate_owner(owner_account, &program_id)?; + // ... +} + +// With pinocchio +#[cfg(feature = "pinocchio")] +fn process_instruction_pinocchio( + accounts: &[pinocchio::account_info::AccountInfo], +) -> Result<(), ProgramError> { + let owner_account = &accounts[0]; + validate_owner(owner_account, &program_id)?; + // Same validation code works +} +``` + +### PDA Derivation +```rust +fn derive_config_pda( + seeds: &[&[u8]], + program_id: &[u8; 32], +) -> ([u8; 32], u8) { + A::find_program_address(seeds, program_id) +} +``` + +### Data Access Pattern +```rust +fn read_discriminator( + account: &A, +) -> Result<[u8; 8], AccountError> { + let data = account.try_borrow_data()?; + if data.len() < 8 { + return Err(AccountError::InvalidAccountSize); + } + let mut discriminator = [0u8; 8]; + discriminator.copy_from_slice(&data[..8]); + Ok(discriminator) +} +``` + +## Key Differences Between SDK Implementations + +| Aspect | solana-program | pinocchio | +|--------|---------|-----------| +| **Pubkey Type** | `solana_pubkey::Pubkey` struct | `[u8; 32]` raw bytes | +| **Performance** | Standard | Optimized for on-chain execution | +| **Data Borrowing** | RefCell-based | Direct memory access | +| **Off-chain Support** | Full | Limited (requires fallback) | +| **Memory Overhead** | Higher | Minimal | + +## Associated Types + +### Pubkey +SDK-specific public key representation. Use `key()` for raw bytes when you need compatibility across SDKs. + +### DataRef / DataRefMut +Smart pointers providing safe access to account data. Both dereference to `[u8]` slices but use SDK-specific memory management. + +## Error Handling + +All methods that can fail return `Result`: +- `try_borrow_data` / `try_borrow_mut_data` - `BorrowAccountDataFailed` (12009) +- `create_program_address` - `InvalidSeeds` (12016) +- `get_min_rent_balance` - `FailedBorrowRentSysvar` (12014) + +## Best Practices + +1. **Use raw bytes for keys in generic code:** + ```rust + fn compare_keys(a1: &A, a2: &A) -> bool { + a1.key() == a2.key() // Works across SDKs + } + ``` + +2. **Handle borrow failures gracefully:** + ```rust + let data = account.try_borrow_data() + .map_err(|_| AccountError::BorrowAccountDataFailed)?; + ``` + +3. **Prefer trait bounds over concrete types:** + ```rust + fn process(accounts: &[A]) -> Result<(), AccountError> + ``` + +## See Also +- [ACCOUNT_CHECKS.md](ACCOUNT_CHECKS.md) - Validation functions using AccountInfoTrait +- [ACCOUNT_ITERATOR.md](ACCOUNT_ITERATOR.md) - Iterator pattern for account processing +- [ERRORS.md](ERRORS.md) - Error types and codes \ No newline at end of file diff --git a/program-libs/account-checks/docs/ACCOUNT_ITERATOR.md b/program-libs/account-checks/docs/ACCOUNT_ITERATOR.md new file mode 100644 index 0000000000..ee6adac3a5 --- /dev/null +++ b/program-libs/account-checks/docs/ACCOUNT_ITERATOR.md @@ -0,0 +1,290 @@ +# AccountIterator + +**Path:** `program-libs/account-checks/src/account_iterator.rs` + +## Description + +AccountIterator provides sequential account processing with enhanced error reporting. When accounts are missing or validation fails, it reports the exact location (file:line:column) where the error occurred, making debugging significantly easier in complex instruction processing. + +All methods are marked with `#[inline(always)]` for performance optimization and `#[track_caller]` for accurate error location reporting. + +## Core Structure + +```rust +pub struct AccountIterator<'info, T: AccountInfoTrait> { + accounts: &'info [T], + position: usize, + owner: [u8; 32], // Reserved for future use +} +``` + +## Constructor Methods + +### `new` +```rust +fn new(accounts: &'info [T]) -> Self +``` +Basic constructor for general use. + +### `new_with_owner` +```rust +fn new_with_owner(accounts: &'info [T], owner: [u8; 32]) -> Self +``` +Constructor that stores owner for future validation extensions (currently unused). + +## Account Retrieval Methods + +### Basic Retrieval + +#### `next_account` +```rust +fn next_account(&mut self, account_name: &str) -> Result<&'info T, AccountError> +``` +- Gets next account with descriptive name for error messages +- **Error:** `NotEnoughAccountKeys` (20014) with detailed location + +### Validated Retrieval + +#### `next_signer` +```rust +fn next_signer(&mut self, account_name: &str) -> Result<&'info T, AccountError> +``` +- Gets next account and validates it's a signer +- **Errors:** + - `NotEnoughAccountKeys` (20014) + - `InvalidSigner` (20009) + +#### `next_signer_mut` +```rust +fn next_signer_mut(&mut self, account_name: &str) -> Result<&'info T, AccountError> +``` +- Gets next account validating signer AND writable +- **Errors:** + - `NotEnoughAccountKeys` (20014) + - `InvalidSigner` (20009) + - `AccountNotMutable` (20002) + +#### `next_mut` +```rust +fn next_mut(&mut self, account_name: &str) -> Result<&'info T, AccountError> +``` +- Gets next account validating it's writable +- **Errors:** + - `NotEnoughAccountKeys` (20014) + - `AccountNotMutable` (20002) + +#### `next_non_mut` +```rust +fn next_non_mut(&mut self, account_name: &str) -> Result<&'info T, AccountError> +``` +- Gets next account validating it's NOT writable +- **Errors:** + - `NotEnoughAccountKeys` (20014) + - `AccountMutable` (20005) + +### Special Retrieval + +#### `next_checked_pubkey` +```rust +fn next_checked_pubkey( + &mut self, + account_name: &str, + pubkey: [u8; 32] +) -> Result<&'info T, AccountError> +``` +- Gets next account and validates its public key matches +- **Errors:** + - `NotEnoughAccountKeys` (20014) + - `InvalidAccount` (20015) with expected/actual keys + +#### `next_option` +```rust +fn next_option( + &mut self, + account_name: &str, + is_some: bool +) -> Result, AccountError> +``` +- Conditionally gets next account based on `is_some` flag +- Returns `None` if `is_some` is false (doesn't advance iterator) + +#### `next_option_mut` +```rust +fn next_option_mut( + &mut self, + account_name: &str, + is_some: bool +) -> Result, AccountError> +``` +- Like `next_option` but validates writable if present + +## Bulk Access Methods + +### `remaining` +```rust +fn remaining(&self) -> Result<&'info [T], AccountError> +``` +- Returns all unprocessed accounts +- **Error:** `NotEnoughAccountKeys` (20014) if iterator exhausted + +### `remaining_unchecked` +```rust +fn remaining_unchecked(&self) -> Result<&'info [T], AccountError> +``` +- Returns remaining accounts or empty slice if exhausted +- Never errors + +## Status Methods + +- `position()` - Current index in account array +- `len()` - Total number of accounts +- `is_empty()` - Whether account array is empty +- `iterator_is_empty()` - Whether all accounts have been processed + +## Usage Examples + +### Basic Instruction Processing +```rust +use light_account_checks::{AccountIterator, AccountInfoTrait, AccountError}; + +fn process_transfer( + accounts: &[A], +) -> Result<(), AccountError> { + let mut iter = AccountIterator::new(accounts); + + let authority = iter.next_signer("authority")?; + let source = iter.next_mut("source_account")?; + let destination = iter.next_mut("destination_account")?; + let mint = iter.next_non_mut("mint")?; + + // Process transfer... + Ok(()) +} +``` + +### Optional Accounts +```rust +fn process_transfer_with_fee( + accounts: &[A], + collect_fee: bool, +) -> Result<(), AccountError> { + let mut iter = AccountIterator::new(accounts); + + let authority = iter.next_signer("authority")?; + let source = iter.next_mut("source")?; + let destination = iter.next_mut("destination")?; + + // Fee account only if collect_fee is true + let fee_account = iter.next_option_mut("fee_account", collect_fee)?; + + if let Some(fee_acc) = fee_account { + // Process fee collection + } + + Ok(()) +} +``` + +### System Program Validation +```rust +fn process_with_system_program( + accounts: &[A], +) -> Result<(), AccountError> { + let mut iter = AccountIterator::new(accounts); + + let payer = iter.next_signer_mut("payer")?; + let new_account = iter.next_mut("new_account")?; + + // Validate system program + let system_program = iter.next_checked_pubkey( + "system_program", + solana_program::system_program::ID.to_bytes() + )?; + + Ok(()) +} +``` + +### Processing Variable Account Lists +```rust +fn process_multiple_transfers( + accounts: &[A], +) -> Result<(), AccountError> { + let mut iter = AccountIterator::new(accounts); + + let authority = iter.next_signer("authority")?; + let source = iter.next_mut("source")?; + + // Get all remaining destination accounts + let destinations = iter.remaining()?; + + for (i, dest) in destinations.iter().enumerate() { + // Validate each destination + check_mut(dest).map_err(|_| { + solana_msg::msg!("Destination {} not writable", i); + AccountError::AccountNotMutable + })?; + } + + Ok(()) +} +``` + +## Error Messages + +AccountIterator provides detailed error messages with location tracking: + +``` +ERROR: Not enough accounts. Requested 'mint' at index 3 but only 2 accounts available. src/processor.rs:45:12 + +ERROR: Invalid Signer. for account 'authority' at index 0 src/processor.rs:42:8 + +ERROR: Invalid Account. for account 'system_program' address: 11111111111111111111111111111112, expected: 11111111111111111111111111111111, at index 4 src/processor.rs:48:15 +``` + +Note: The `#[track_caller]` attribute on methods enables accurate file:line:column reporting. + +## Best Practices + +1. **Use descriptive account names:** + ```rust + iter.next_account("token_mint") // Good + iter.next_account("account_3") // Less helpful + ``` + +2. **Validate permissions early:** + ```rust + // Check signers and mutability at the start + let authority = iter.next_signer("authority")?; + let target = iter.next_mut("target")?; + ``` + +3. **Use specialized methods over manual validation:** + ```rust + // Preferred + let signer = iter.next_signer_mut("payer")?; + + // Avoid + let payer = iter.next_account("payer")?; + check_signer(payer)?; + check_mut(payer)?; + ``` + +4. **Handle optional accounts explicitly:** + ```rust + let optional = iter.next_option("optional_account", has_optional)?; + ``` + +## Integration with Validation Functions + +AccountIterator methods internally use validation functions from the `checks` module: +- `next_signer` uses `check_signer` +- `next_mut` uses `check_mut` +- `next_non_mut` uses `check_non_mut` + +This ensures consistent validation across the codebase while providing enhanced error reporting. + +## See Also +- [ACCOUNT_CHECKS.md](ACCOUNT_CHECKS.md) - Underlying validation functions +- [ACCOUNT_INFO_TRAIT.md](ACCOUNT_INFO_TRAIT.md) - AccountInfoTrait abstraction +- [ERRORS.md](ERRORS.md) - Complete error documentation \ No newline at end of file diff --git a/program-libs/account-checks/docs/CLAUDE.md b/program-libs/account-checks/docs/CLAUDE.md new file mode 100644 index 0000000000..097f0dbcc1 --- /dev/null +++ b/program-libs/account-checks/docs/CLAUDE.md @@ -0,0 +1,34 @@ +# Account Checks Documentation + +This directory contains detailed documentation for the `light-account-checks` crate components. + +## Core Components + +### [ACCOUNT_INFO_TRAIT.md](ACCOUNT_INFO_TRAIT.md) +AccountInfoTrait abstraction layer that unifies account handling across Solana and Pinocchio runtimes. Covers the trait definition, implementations, and usage patterns for runtime-agnostic account processing. + +### [ACCOUNT_CHECKS.md](ACCOUNT_CHECKS.md) +Comprehensive validation functions from the `checks` module. Documents all check functions including ownership validation, permission checks, discriminator handling, and PDA verification with code examples. + +### [ACCOUNT_ITERATOR.md](ACCOUNT_ITERATOR.md) +Enhanced account iterator with detailed error reporting. Shows how to sequentially process accounts with automatic validation and location-based error messages for debugging. + +## Type System + +### [DISCRIMINATOR.md](DISCRIMINATOR.md) +Account type identification using 8-byte discriminators. Explains the Discriminator trait, constant arrays for compile-time verification, and integration with account initialization. + +### [ERRORS.md](ERRORS.md) +Complete error type documentation with numeric codes (12006-12021 range), common causes, and resolution strategies. Includes conversion mappings for both Solana and Pinocchio runtimes. + +## Utilities + +### [PACKED_ACCOUNTS.md](PACKED_ACCOUNTS.md) +Index-based dynamic account access for handling variable account sets. Used for accessing mint, owner, and delegate accounts by index with bounds checking. + +## Navigation Tips + +- Each document focuses on a single module or concept +- Code examples demonstrate both Solana and Pinocchio usage where applicable +- Error codes reference actual values that appear in transaction logs +- Cross-references link related concepts across documents \ No newline at end of file diff --git a/program-libs/account-checks/docs/DISCRIMINATOR.md b/program-libs/account-checks/docs/DISCRIMINATOR.md new file mode 100644 index 0000000000..4e51f5de44 --- /dev/null +++ b/program-libs/account-checks/docs/DISCRIMINATOR.md @@ -0,0 +1,221 @@ +# Discriminator Trait + +**Path:** `program-libs/account-checks/src/discriminator.rs` + +## Description + +The Discriminator trait provides a type-safe system for account identification using 8-byte prefixes. This enables compile-time verification of account types and prevents account type confusion attacks. + +## Trait Definition + +```rust +pub const DISCRIMINATOR_LEN: usize = 8; + +pub trait Discriminator { + const LIGHT_DISCRIMINATOR: [u8; 8]; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8]; + + fn discriminator() -> [u8; 8] { + Self::LIGHT_DISCRIMINATOR + } +} +``` + +## Purpose + +Discriminators serve as type identifiers for Solana accounts: +- **First 8 bytes** of account data identify the account type +- **Compile-time constants** ensure type safety +- **Prevents account confusion** by validating expected types +- **Compatible with Anchor** discriminator pattern + +## Implementation Pattern + +### Basic Implementation +```rust +use light_account_checks::discriminator::Discriminator; + +pub struct TokenAccount; + +impl Discriminator for TokenAccount { + const LIGHT_DISCRIMINATOR: [u8; 8] = [180, 4, 231, 26, 220, 144, 55, 168]; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; +} +``` + +### With Account Data Structure +```rust +#[derive(Debug, Clone, Copy)] +pub struct ConfigAccount { + pub discriminator: [u8; 8], // Must match LIGHT_DISCRIMINATOR + pub authority: [u8; 32], + pub settings: u64, +} + +impl Discriminator for ConfigAccount { + const LIGHT_DISCRIMINATOR: [u8; 8] = [99, 111, 110, 102, 105, 103, 0, 0]; // "config\0\0" + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; +} +``` + +## Usage with Validation Functions + +### Account Initialization +```rust +use light_account_checks::checks::{account_info_init, set_discriminator}; + +// Initialize account with discriminator +fn initialize_config( + account: &A, +) -> Result<(), AccountError> { + // Sets the discriminator in the account data + account_info_init::(account)?; + + // Or manually with mutable data + let mut data = account.try_borrow_mut_data()?; + set_discriminator::(&mut data)?; + + Ok(()) +} +``` + +### Account Validation +```rust +use light_account_checks::checks::{check_discriminator, check_account_info}; + +// Validate discriminator only +fn validate_discriminator(data: &[u8]) -> Result<(), AccountError> { + check_discriminator::(data)?; + Ok(()) +} + +// Full account validation with discriminator +fn validate_config( + program_id: &[u8; 32], + account: &A, +) -> Result<(), AccountError> { + // Checks ownership AND discriminator + check_account_info::(program_id, account)?; + Ok(()) +} +``` + +## Discriminator Values + +### Standard Patterns + +1. **Sequential bytes**: Simple incrementing values + ```rust + const LIGHT_DISCRIMINATOR: [u8; 8] = [180, 4, 231, 26, 220, 144, 55, 168]; + ``` + +2. **ASCII strings**: Human-readable identifiers + ```rust + const LIGHT_DISCRIMINATOR: [u8; 8] = *b"tokenacc"; // "tokenacc" + const LIGHT_DISCRIMINATOR: [u8; 8] = *b"config\0\0"; // "config" with padding + ``` + +3. **Hash-derived**: From account type name + ```rust + // First 8 bytes of sha256("ConfigAccount") + const LIGHT_DISCRIMINATOR: [u8; 8] = [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0]; + ``` + +4. **Anchor-compatible**: Matching Anchor's discriminator + ```rust + // Anchor uses sha256("account:")[..8] + const LIGHT_DISCRIMINATOR: [u8; 8] = anchor_discriminator(); + ``` + +## Integration with Checks Module + +The checks module provides functions that work with Discriminator types: + +| Function | Purpose | Discriminator Usage | +|----------|---------|-------------------| +| `set_discriminator` | Initialize account | Writes `T::LIGHT_DISCRIMINATOR` | +| `check_discriminator` | Validate type | Compares against `T::LIGHT_DISCRIMINATOR` | +| `account_info_init` | Initialize with type | Sets discriminator for type T | +| `check_account_info` | Full validation | Checks discriminator matches T | +| `check_account_info_mut` | Validate mutable | Includes discriminator check | +| `check_account_info_non_mut` | Validate readonly | Includes discriminator check | + +## Error Handling + +Discriminator validation can return these errors: + +- **InvalidDiscriminator (12006)**: Mismatch between expected and actual +- **InvalidAccountSize (12010)**: Account smaller than 8 bytes +- **AlreadyInitialized (12012)**: Non-zero discriminator when initializing +- **BorrowAccountDataFailed (12009)**: Can't access account data + +## Best Practices + +1. **Use unique discriminators**: Avoid collisions between account types + ```rust + // BAD: Same discriminator for different types + impl Discriminator for AccountA { + const LIGHT_DISCRIMINATOR: [u8; 8] = [1, 0, 0, 0, 0, 0, 0, 0]; + } + impl Discriminator for AccountB { + const LIGHT_DISCRIMINATOR: [u8; 8] = [1, 0, 0, 0, 0, 0, 0, 0]; // Collision! + } + ``` + +2. **Document discriminator values**: Make values discoverable + ```rust + /// Discriminator: [99, 111, 110, 102, 105, 103, 0, 0] ("config\0\0") + impl Discriminator for ConfigAccount { + const LIGHT_DISCRIMINATOR: [u8; 8] = *b"config\0\0"; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; + } + ``` + +3. **Validate before deserialization**: Check discriminator first + ```rust + fn deserialize_config(data: &[u8]) -> Result { + // Check discriminator BEFORE deserializing + check_discriminator::(data)?; + + // Safe to deserialize after validation + let config = ConfigAccount::deserialize(data)?; + Ok(config) + } + ``` + +## Example: Multi-Account System + +```rust +// Define discriminators for different account types +pub struct UserAccount; +impl Discriminator for UserAccount { + const LIGHT_DISCRIMINATOR: [u8; 8] = *b"user\0\0\0\0"; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; +} + +pub struct AdminAccount; +impl Discriminator for AdminAccount { + const LIGHT_DISCRIMINATOR: [u8; 8] = *b"admin\0\0\0"; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; +} + +// Process instruction with type validation +fn process_instruction( + program_id: &[u8; 32], + accounts: &[A], +) -> Result<(), AccountError> { + let user = &accounts[0]; + let admin = &accounts[1]; + + // Validate each account has correct type + check_account_info::(program_id, user)?; + check_account_info::(program_id, admin)?; + + // Safe to proceed - types are verified + Ok(()) +} +``` + +## See Also +- [ACCOUNT_CHECKS.md](ACCOUNT_CHECKS.md) - Validation functions using discriminators +- [ERRORS.md](ERRORS.md) - Error codes for discriminator failures diff --git a/program-libs/account-checks/docs/ERRORS.md b/program-libs/account-checks/docs/ERRORS.md new file mode 100644 index 0000000000..1fd663888c --- /dev/null +++ b/program-libs/account-checks/docs/ERRORS.md @@ -0,0 +1,153 @@ +# Error Types + +**Path:** `program-libs/account-checks/src/error.rs` + +## Description + +AccountError enum provides comprehensive error types for account validation failures. All errors automatically convert to `ProgramError::Custom(u32)` for both solana-program and pinocchio SDKs. + +## Error Code Ranges + +- **20000-20015**: AccountError variants +- **1-11**: Standard Pinocchio ProgramError codes (when using pinocchio feature) +- **Variable**: Pass-through codes from PinocchioProgramError + +## Complete Error Reference + +| Error Variant | Code | Description | Common Causes | Resolution | +|---------------|------|-------------|---------------|------------| +| `InvalidDiscriminator` | 20000 | Account discriminator mismatch | Wrong account type passed, uninitialized account | Verify correct account type, ensure account initialized | +| `AccountOwnedByWrongProgram` | 20001 | Account owner doesn't match expected | Passing system account to program expecting owned account | Check account owner before passing | +| `AccountNotMutable` | 20002 | Account not marked writable | Missing `mut` in instruction accounts | Add writable flag to account | +| `BorrowAccountDataFailed` | 20003 | Can't borrow account data | Account already borrowed, concurrent access | Ensure no overlapping borrows | +| `InvalidAccountSize` | 20004 | Account size doesn't match expected | Wrong account type, partial initialization | Verify account size matches struct | +| `AccountMutable` | 20005 | Account is writable but shouldn't be | Incorrect mutability specification | Remove writable flag from account | +| `AlreadyInitialized` | 20006 | Account discriminator already set | Attempting to reinitialize account | Check if account exists before init | +| `InvalidAccountBalance` | 20007 | Account balance below rent exemption | Insufficient lamports | Fund account to rent-exempt amount | +| `FailedBorrowRentSysvar` | 20008 | Can't access rent sysvar | Sysvar not available in context | Ensure rent sysvar accessible | +| `InvalidSigner` | 20009 | Account not a signer | Missing signature | Add account as signer in transaction | +| `InvalidSeeds` | 20010 | PDA derivation failed | Wrong seeds or bump | Verify PDA seeds and bump | +| `InvalidProgramId` | 20011 | Program account key mismatch | Wrong program passed | Pass correct program account | +| `ProgramNotExecutable` | 20012 | Program account not executable | Non-program account passed as program | Ensure account is deployed program | +| `AccountNotZeroed` | 20013 | Account data not zeroed | Account has existing data | Clear account data or use existing | +| `NotEnoughAccountKeys` | 20014 | Insufficient accounts provided | Missing required accounts | Provide all required accounts | +| `InvalidAccount` | 20015 | Account validation failed | Wrong account passed | Verify account key matches expected | +| `PinocchioProgramError` | (varies) | Pinocchio-specific error | Various SDK-level errors | Check embedded error code | + +## Error Conversions + +### To ProgramError + +Both SDKs automatically convert AccountError to their respective ProgramError types: + +```rust +// Solana +#[cfg(feature = "solana")] +impl From for solana_program_error::ProgramError { + fn from(e: AccountError) -> Self { + ProgramError::Custom(e.into()) // Converts to u32 + } +} + +// Pinocchio +#[cfg(feature = "pinocchio")] +impl From for pinocchio::program_error::ProgramError { + fn from(e: AccountError) -> Self { + ProgramError::Custom(e.into()) // Converts to u32 + } +} +``` + +### From Pinocchio ProgramError + +When using pinocchio, standard ProgramError variants map to specific codes: + +| Pinocchio ProgramError | Mapped Code | AccountError Variant | +|------------------------|-------------|---------------------| +| `InvalidArgument` | 1 | `PinocchioProgramError(1)` | +| `InvalidInstructionData` | 2 | `PinocchioProgramError(2)` | +| `InvalidAccountData` | 3 | `PinocchioProgramError(3)` | +| `AccountDataTooSmall` | 4 | `PinocchioProgramError(4)` | +| `InsufficientFunds` | 5 | `PinocchioProgramError(5)` | +| `IncorrectProgramId` | 6 | `PinocchioProgramError(6)` | +| `MissingRequiredSignature` | 7 | `PinocchioProgramError(7)` | +| `AccountAlreadyInitialized` | 8 | `PinocchioProgramError(8)` | +| `UninitializedAccount` | 9 | `PinocchioProgramError(9)` | +| `NotEnoughAccountKeys` | 10 | `PinocchioProgramError(10)` | +| `AccountBorrowFailed` | 11 | `PinocchioProgramError(11)` | + +### BorrowError Conversions + +For solana-program SDK, RefCell borrow errors automatically convert: + +```rust +#[cfg(feature = "solana")] +impl From for AccountError { + fn from(_: std::cell::BorrowError) -> Self { + AccountError::BorrowAccountDataFailed + } +} + +impl From for AccountError { + fn from(_: std::cell::BorrowMutError) -> Self { + AccountError::BorrowAccountDataFailed + } +} +``` + +## Usage in Validation Functions + +Each validation function returns specific errors: + +```rust +// check_owner returns AccountOwnedByWrongProgram (20001) +check_owner(owner, account)?; + +// check_signer returns InvalidSigner (20009) +check_signer(account)?; + +// check_discriminator returns InvalidDiscriminator (20000) or InvalidAccountSize (20004) +check_discriminator::(data)?; + +// check_pda_seeds returns InvalidSeeds (20010) +check_pda_seeds(seeds, program_id, account)?; +``` + +## Error Messages in Logs + +Errors appear in transaction logs with their numeric codes: + +```text +Program log: ERROR: Invalid Discriminator. +Program log: Custom program error: 0x4e20 // 20000 in hex + +Program log: ERROR: Not enough accounts. Requested 'mint' at index 3 but only 2 accounts available. +Program log: Custom program error: 0x4e2e // 20014 in hex +``` + +## Debugging Tips + +1. **Check error codes in hex**: Solana logs show custom errors in hexadecimal + - 20000 = 0x4E20 + - 20014 = 0x4E2E + - 20015 = 0x4E2F + +2. **Use AccountIterator for detailed errors**: Provides file:line:column for debugging + +3. **Common error patterns**: + - 20000 + 20004: Usually uninitialized account + - 20001: Wrong program ownership + - 20009: Missing signer + - 20014: Not enough accounts in instruction + +## Integration with Other Crates + +AccountError is used throughout Light Protocol: +- Validation functions in `checks` module return AccountError +- AccountIterator uses AccountError for all failures +- AccountInfoTrait methods return AccountError for borrow/PDA failures + +## See Also +- [ACCOUNT_CHECKS.md](ACCOUNT_CHECKS.md) - Functions that return these errors +- [ACCOUNT_ITERATOR.md](ACCOUNT_ITERATOR.md) - Enhanced error reporting +- [ACCOUNT_INFO_TRAIT.md](ACCOUNT_INFO_TRAIT.md) - Trait methods returning AccountError diff --git a/program-libs/account-checks/docs/PACKED_ACCOUNTS.md b/program-libs/account-checks/docs/PACKED_ACCOUNTS.md new file mode 100644 index 0000000000..06b47c6658 --- /dev/null +++ b/program-libs/account-checks/docs/PACKED_ACCOUNTS.md @@ -0,0 +1,224 @@ +# ProgramPackedAccounts + +**Path:** `program-libs/account-checks/src/packed_accounts.rs` + +## Description + +ProgramPackedAccounts provides index-based access to dynamically sized account arrays. This utility is designed for instructions that work with variable numbers of accounts (mint, owner, delegate, merkle tree, queue accounts) where accounts are referenced by index rather than position. + +## Structure + +```rust +pub struct ProgramPackedAccounts<'info, A: AccountInfoTrait> { + pub accounts: &'info [A], +} +``` + +## Methods + +### `get` +```rust +fn get(&self, index: usize, name: &str) -> Result<&A, AccountError> +``` +- Retrieves account at specified index with bounds checking +- **Parameters:** + - `index`: Zero-based index into account array + - `name`: Descriptive name for error messages +- **Error:** `NotEnoughAccountKeys` (12020) with location tracking + +### `get_u8` +```rust +fn get_u8(&self, index: u8, name: &str) -> Result<&A, AccountError> +``` +- Convenience method for u8 indices (common in instruction data) +- Internally calls `get(index as usize, name)` +- **Error:** `NotEnoughAccountKeys` (12020) + +## Usage Patterns + +### Dynamic Account Access +```rust +use light_account_checks::{AccountInfoTrait, ProgramPackedAccounts}; + +fn process_with_dynamic_accounts( + accounts: &[A], + mint_index: u8, + owner_index: u8, +) -> Result<(), AccountError> { + let packed = ProgramPackedAccounts { accounts }; + + // Access accounts by index from instruction data + let mint = packed.get_u8(mint_index, "mint")?; + let owner = packed.get_u8(owner_index, "owner")?; + + // Validate retrieved accounts + check_owner(&token_program_id, mint)?; + check_signer(owner)?; + + Ok(()) +} +``` + +### Multiple Optional Accounts +```rust +struct TransferInstruction { + mint_indices: Vec, + amounts: Vec, +} + +fn process_multi_transfer( + accounts: &[A], + instruction: TransferInstruction, +) -> Result<(), AccountError> { + let packed = ProgramPackedAccounts { accounts }; + + for (i, &mint_index) in instruction.mint_indices.iter().enumerate() { + let mint = packed.get_u8(mint_index, &format!("mint_{}", i))?; + + // Process transfer for this mint + process_single_transfer(mint, instruction.amounts[i])?; + } + + Ok(()) +} +``` + +### Merkle Tree and Queue Access +```rust +fn access_merkle_accounts( + accounts: &[A], + tree_index: u8, + queue_index: u8, +) -> Result<(), AccountError> { + let packed = ProgramPackedAccounts { accounts }; + + let merkle_tree = packed.get_u8(tree_index, "merkle_tree")?; + let queue = packed.get_u8(queue_index, "queue")?; + + // Validate merkle tree account + check_account_info::(&program_id, merkle_tree)?; + + // Validate queue account + check_account_info::(&program_id, queue)?; + + Ok(()) +} +``` + +## Error Messages + +ProgramPackedAccounts provides detailed error messages with `#[track_caller]`: + +``` +ERROR: Not enough accounts. Requested 'mint' at index 5 but only 3 accounts available. src/processor.rs:42:18 + +ERROR: Not enough accounts. Requested 'merkle_tree' at index 10 but only 8 accounts available. src/processor.rs:55:23 +``` + +## Comparison with AccountIterator + +| Feature | ProgramPackedAccounts | AccountIterator | +|---------|----------------------|-----------------| +| **Access Pattern** | Random by index | Sequential | +| **Use Case** | Dynamic account sets | Fixed account order | +| **Index Source** | Instruction data | Implicit position | +| **Validation** | Manual after retrieval | Built-in methods | +| **State** | Stateless | Tracks position | + +### When to Use Each + +**Use ProgramPackedAccounts when:** +- Account indices come from instruction data +- Accounts can be accessed in any order +- Number of accounts varies per instruction +- Indices might skip positions + +**Use AccountIterator when:** +- Accounts have fixed order +- Sequential processing is natural +- Built-in validation is helpful +- Error context from position is sufficient + +## Integration Example + +Combining with AccountIterator for hybrid access: + +```rust +fn process_complex_instruction( + accounts: &[A], + dynamic_indices: Vec, +) -> Result<(), AccountError> { + let mut iter = AccountIterator::new(accounts); + + // Fixed accounts at start + let authority = iter.next_signer("authority")?; + let payer = iter.next_signer_mut("payer")?; + let config = iter.next_non_mut("config")?; + + // Remaining accounts accessed dynamically + let remaining = iter.remaining()?; + let packed = ProgramPackedAccounts { accounts: remaining }; + + // Access by indices from instruction data + for (i, &index) in dynamic_indices.iter().enumerate() { + let account = packed.get(index as usize, &format!("dynamic_{}", i))?; + // Process dynamic account + } + + Ok(()) +} +``` + +## Future Enhancements + +The TODO comment suggests adding validation methods: +```rust +// TODO: add get_checked_account from PackedAccounts. +``` + +This would enable: +```rust +fn get_checked( + &self, + index: usize, + name: &str, + program_id: &[u8; 32], +) -> Result<&A, AccountError> { + let account = self.get(index, name)?; + check_account_info::(program_id, account)?; + Ok(account) +} +``` + +## Best Practices + +1. **Use descriptive names in errors:** + ```rust + packed.get_u8(index, "token_mint") // Good + packed.get_u8(index, "account") // Less helpful + ``` + +2. **Validate after retrieval:** + ```rust + let account = packed.get(index, "mint")?; + check_owner(&spl_token_id, account)?; // Always validate + ``` + +3. **Handle index bounds explicitly:** + ```rust + if index >= accounts.len() { + return Err(CustomError::InvalidIndex); + } + let account = packed.get(index, "account")?; + ``` + +4. **Consider caching for repeated access:** + ```rust + let mint = packed.get_u8(mint_index, "mint")?; + // Store reference if accessed multiple times + ``` + +## See Also +- [ACCOUNT_ITERATOR.md](ACCOUNT_ITERATOR.md) - Sequential account processing +- [ERRORS.md](ERRORS.md) - NotEnoughAccountKeys error details +- [ACCOUNT_INFO_TRAIT.md](ACCOUNT_INFO_TRAIT.md) - AccountInfoTrait abstraction \ No newline at end of file diff --git a/program-libs/account-checks/src/account_info/account_info_trait.rs b/program-libs/account-checks/src/account_info/account_info_trait.rs index 3a8e81929f..14eddd2b26 100644 --- a/program-libs/account-checks/src/account_info/account_info_trait.rs +++ b/program-libs/account-checks/src/account_info/account_info_trait.rs @@ -7,7 +7,7 @@ use crate::error::AccountError; /// Trait to abstract over different AccountInfo implementations (pinocchio vs solana) pub trait AccountInfoTrait { - type Pubkey: Copy + Clone + Debug; + type Pubkey: Copy + Clone + Debug + PartialEq; type DataRef<'a>: Deref where Self: 'a; diff --git a/program-libs/account-checks/src/account_iterator.rs b/program-libs/account-checks/src/account_iterator.rs index a49b2b5c87..e2be3658d9 100644 --- a/program-libs/account-checks/src/account_iterator.rs +++ b/program-libs/account-checks/src/account_iterator.rs @@ -62,6 +62,29 @@ impl<'info, T: AccountInfoTrait> AccountIterator<'info, T> { Ok(account) } + #[inline(always)] + #[track_caller] + pub fn next_checked_pubkey( + &mut self, + account_name: &str, + pubkey: [u8; 32], + ) -> Result<&'info T, AccountError> { + let account_info = self.next_account(account_name)?; + if account_info.key() != pubkey { + Err(AccountError::InvalidAccount).inspect_err(|e| { + self.print_on_error_pubkey( + e, + account_info.key(), + pubkey, + account_name, + Location::caller(), + ) + }) + } else { + Ok(account_info) + } + } + #[inline(always)] #[track_caller] pub fn next_option( @@ -188,7 +211,6 @@ impl<'info, T: AccountInfoTrait> AccountIterator<'info, T> { } #[cold] - #[inline(never)] fn print_on_error(&self, error: &AccountError, account_name: &str, location: &Location) { solana_msg::msg!( "ERROR: {}. for account '{}' at index {} {}:{}:{}", @@ -200,4 +222,38 @@ impl<'info, T: AccountInfoTrait> AccountIterator<'info, T> { location.column() ); } + #[cold] + fn print_on_error_pubkey( + &self, + error: &AccountError, + pubkey1: [u8; 32], + pubkey2: [u8; 32], + account_name: &str, + location: &Location, + ) { + #[cfg(feature = "solana")] + solana_msg::msg!( + "ERROR: {}. for account '{}' address: {:?}, expected: {:?}, at index {} {}:{}:{}", + error, + account_name, + solana_pubkey::Pubkey::new_from_array(pubkey1), + solana_pubkey::Pubkey::new_from_array(pubkey2), + self.position.saturating_sub(1), + location.file(), + location.line(), + location.column() + ); + #[cfg(not(feature = "solana"))] + solana_msg::msg!( + "ERROR: {}. for account '{}' address: {:?}, expected: {:?}, at index {} {}:{}:{}", + error, + account_name, + pubkey1, + pubkey2, + self.position.saturating_sub(1), + location.file(), + location.line(), + location.column() + ); + } } diff --git a/program-libs/account-checks/src/checks.rs b/program-libs/account-checks/src/checks.rs index fdbc043afa..29d7591dca 100644 --- a/program-libs/account-checks/src/checks.rs +++ b/program-libs/account-checks/src/checks.rs @@ -81,6 +81,11 @@ pub fn check_discriminator(bytes: &[u8]) -> Result<(), Account } if T::LIGHT_DISCRIMINATOR != bytes[0..DISCRIMINATOR_LEN] { + solana_msg::msg!( + "expected discriminator {:?} != {:?} actual", + T::LIGHT_DISCRIMINATOR, + &bytes[0..DISCRIMINATOR_LEN] + ); return Err(AccountError::InvalidDiscriminator); } Ok(()) diff --git a/program-libs/account-checks/src/error.rs b/program-libs/account-checks/src/error.rs index ccc477c6d9..f1d801ce06 100644 --- a/program-libs/account-checks/src/error.rs +++ b/program-libs/account-checks/src/error.rs @@ -32,29 +32,31 @@ pub enum AccountError { AccountNotZeroed, #[error("Not enough account keys provided.")] NotEnoughAccountKeys, + #[error("Invalid Account.")] + InvalidAccount, #[error("Pinocchio program error with code: {0}")] PinocchioProgramError(u32), } -// TODO: reconfigure error codes impl From for u32 { fn from(e: AccountError) -> u32 { match e { - AccountError::AccountOwnedByWrongProgram => 12007, - AccountError::AccountNotMutable => 12008, - AccountError::InvalidDiscriminator => 12006, - AccountError::BorrowAccountDataFailed => 12009, - AccountError::InvalidAccountSize => 12010, - AccountError::AccountMutable => 12011, - AccountError::AlreadyInitialized => 12012, - AccountError::InvalidAccountBalance => 12013, - AccountError::FailedBorrowRentSysvar => 12014, - AccountError::InvalidSigner => 12015, - AccountError::InvalidSeeds => 12016, - AccountError::InvalidProgramId => 12017, - AccountError::ProgramNotExecutable => 12018, - AccountError::AccountNotZeroed => 12019, - AccountError::NotEnoughAccountKeys => 12020, + AccountError::InvalidDiscriminator => 20000, + AccountError::AccountOwnedByWrongProgram => 20001, + AccountError::AccountNotMutable => 20002, + AccountError::BorrowAccountDataFailed => 20003, + AccountError::InvalidAccountSize => 20004, + AccountError::AccountMutable => 20005, + AccountError::AlreadyInitialized => 20006, + AccountError::InvalidAccountBalance => 20007, + AccountError::FailedBorrowRentSysvar => 20008, + AccountError::InvalidSigner => 20009, + AccountError::InvalidSeeds => 20010, + AccountError::InvalidProgramId => 20011, + AccountError::ProgramNotExecutable => 20012, + AccountError::AccountNotZeroed => 20013, + AccountError::NotEnoughAccountKeys => 20014, + AccountError::InvalidAccount => 20015, AccountError::PinocchioProgramError(code) => code, } } diff --git a/program-libs/account-checks/src/packed_accounts.rs b/program-libs/account-checks/src/packed_accounts.rs index 28fd81ea8f..4aad1a8aee 100644 --- a/program-libs/account-checks/src/packed_accounts.rs +++ b/program-libs/account-checks/src/packed_accounts.rs @@ -2,6 +2,8 @@ use std::panic::Location; use crate::{AccountError, AccountInfoTrait}; +/// Dynamic accounts slice for index-based access +/// Contains mint, owner, delegate, merkle tree, and queue accounts pub struct ProgramPackedAccounts<'info, A: AccountInfoTrait> { pub accounts: &'info [A], } @@ -11,8 +13,8 @@ impl ProgramPackedAccounts<'_, A> { #[track_caller] #[inline(always)] pub fn get(&self, index: usize, name: &str) -> Result<&A, AccountError> { + let location = Location::caller(); if index >= self.accounts.len() { - let location = Location::caller(); solana_msg::msg!( "ERROR: Not enough accounts. Requested '{}' at index {} but only {} accounts available. {}:{}:{}", name, index, self.accounts.len(), location.file(), location.line(), location.column() @@ -22,6 +24,7 @@ impl ProgramPackedAccounts<'_, A> { Ok(&self.accounts[index]) } + // TODO: add get_checked_account from PackedAccounts. /// Get account by u8 index with bounds checking #[track_caller] #[inline(always)] diff --git a/program-libs/account-checks/tests/tests.rs b/program-libs/account-checks/tests/tests.rs index d7b45bc366..bf53cc52c2 100644 --- a/program-libs/account-checks/tests/tests.rs +++ b/program-libs/account-checks/tests/tests.rs @@ -52,7 +52,7 @@ pub struct TestStruct { } impl Discriminator for TestStruct { - const LIGHT_DISCRIMINATOR: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; + const LIGHT_DISCRIMINATOR: [u8; 8] = [180, 4, 231, 26, 220, 144, 55, 168]; const LIGHT_DISCRIMINATOR_SLICE: &[u8] = &Self::LIGHT_DISCRIMINATOR; }