Files
gh-tenequm-claude-plugins-s…/skills/solana-development/references/sysvars.md
2025-11-30 09:01:25 +08:00

23 KiB
Raw Blame History

Sysvars (System Variables)

This reference provides comprehensive coverage of Solana System Variables (sysvars) for native Rust program development, including access patterns, use cases, and performance implications.

Table of Contents

  1. What are Sysvars
  2. Clock Sysvar
  3. Rent Sysvar
  4. EpochSchedule Sysvar
  5. SlotHashes Sysvar
  6. Other Sysvars
  7. Access Patterns
  8. Performance Implications
  9. Best Practices

What are Sysvars

System Variables (sysvars) are special accounts that provide programs with access to blockchain state and cluster information.

Key Characteristics

  1. Cluster-wide state: Same values for all programs in the same slot
  2. Updated automatically: Runtime maintains values
  3. Predictable addresses: Well-known pubkeys
  4. Read-only: Programs cannot modify sysvars
  5. Low CU cost: Cheaper than account reads

When to Use Sysvars

Use sysvars when you need:

  • Current timestamp or slot number
  • Rent exemption calculations
  • Epoch and slot timing information
  • Recent block hashes (for verification)
  • Stake history or epoch rewards

Don't use sysvars for:

  • User-specific data (use accounts)
  • Program state (use PDAs)
  • Cross-program communication (use CPIs)

Clock Sysvar

Address: solana_program::sysvar::clock::ID

The Clock sysvar provides timing information about the blockchain.

Clock Structure

use solana_program::clock::Clock;

pub struct Clock {
    pub slot: Slot,                    // Current slot
    pub epoch_start_timestamp: i64,    // Timestamp of epoch start (approximate)
    pub epoch: Epoch,                  // Current epoch
    pub leader_schedule_epoch: Epoch,  // Epoch for which leader schedule is valid
    pub unix_timestamp: UnixTimestamp, // Estimated wall-clock Unix timestamp
}

Accessing Clock

Pattern 1: get() (Recommended)

use solana_program::clock::Clock;
use solana_program::sysvar::Sysvar;

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    // Get Clock directly (no account needed)
    let clock = Clock::get()?;

    msg!("Current slot: {}", clock.slot);
    msg!("Current timestamp: {}", clock.unix_timestamp);
    msg!("Current epoch: {}", clock.epoch);

    Ok(())
}

Pattern 2: From account

use solana_program::sysvar::clock;

pub fn process_with_account(
    accounts: &[AccountInfo],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let clock_account = next_account_info(account_info_iter)?;

    // Verify it's the Clock sysvar
    if clock_account.key != &clock::ID {
        return Err(ProgramError::InvalidArgument);
    }

    let clock = Clock::from_account_info(clock_account)?;
    msg!("Timestamp: {}", clock.unix_timestamp);

    Ok(())
}

⚠️ Recommendation: Use Clock::get() unless you specifically need the account for validation.

Common Clock Use Cases

1. Timestamping events:

use solana_program::clock::Clock;
use solana_program::sysvar::Sysvar;

#[derive(BorshSerialize, BorshDeserialize)]
pub struct Event {
    pub created_at: i64,
    pub data: Vec<u8>,
}

pub fn create_event(
    event_account: &AccountInfo,
    data: Vec<u8>,
) -> ProgramResult {
    let clock = Clock::get()?;

    let event = Event {
        created_at: clock.unix_timestamp,
        data,
    };

    event.serialize(&mut &mut event_account.data.borrow_mut()[..])?;
    Ok(())
}

2. Time-based logic (vesting, expiration):

pub fn check_vesting(
    vesting_account: &AccountInfo,
) -> ProgramResult {
    let clock = Clock::get()?;
    let vesting = VestingSchedule::try_from_slice(&vesting_account.data.borrow())?;

    if clock.unix_timestamp < vesting.unlock_timestamp {
        msg!("Tokens still locked until {}", vesting.unlock_timestamp);
        return Err(ProgramError::Custom(1)); // Locked
    }

    msg!("Vesting unlocked!");
    Ok(())
}

3. Slot-based mechanics:

pub fn process_epoch_transition(
    state_account: &AccountInfo,
) -> ProgramResult {
    let clock = Clock::get()?;
    let mut state = State::try_from_slice(&state_account.data.borrow())?;

    if clock.epoch > state.last_processed_epoch {
        msg!("Processing epoch transition: {} -> {}",
            state.last_processed_epoch, clock.epoch);

        // Process epoch rewards, resets, etc.
        state.last_processed_epoch = clock.epoch;
        state.serialize(&mut &mut state_account.data.borrow_mut()[..])?;
    }

    Ok(())
}

Clock Gotchas

⚠️ unix_timestamp is approximate:

// ❌ Don't use for precise timing
if clock.unix_timestamp == expected_timestamp {  // Risky!
    // Might miss by seconds
}

// ✅ Use ranges for time checks
if clock.unix_timestamp >= unlock_time {
    // Safe
}

⚠️ Timestamps can vary across validators:

The unix_timestamp is based on validator voting and may differ slightly between validators in the same slot. Don't assume exact precision.


Rent Sysvar

Address: solana_program::sysvar::rent::ID

The Rent sysvar provides rent calculation parameters.

Rent Structure

use solana_program::rent::Rent;

pub struct Rent {
    pub lamports_per_byte_year: u64,  // Base rent rate
    pub exemption_threshold: f64,      // Multiplier for exemption (2.0 = 2 years)
    pub burn_percent: u8,              // Percentage of rent burned
}

Accessing Rent

Pattern 1: get() (Recommended)

use solana_program::rent::Rent;
use solana_program::sysvar::Sysvar;

pub fn calculate_rent_exemption(
    data_size: usize,
) -> Result<u64, ProgramError> {
    let rent = Rent::get()?;

    // Calculate minimum balance for rent exemption
    let min_balance = rent.minimum_balance(data_size);

    msg!("Minimum balance for {} bytes: {} lamports", data_size, min_balance);
    Ok(min_balance)
}

Pattern 2: From account

use solana_program::sysvar::rent;

pub fn check_rent_exemption(
    accounts: &[AccountInfo],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let data_account = next_account_info(account_info_iter)?;
    let rent_account = next_account_info(account_info_iter)?;

    if rent_account.key != &rent::ID {
        return Err(ProgramError::InvalidArgument);
    }

    let rent = Rent::from_account_info(rent_account)?;

    if !rent.is_exempt(data_account.lamports(), data_account.data_len()) {
        msg!("Account is not rent-exempt!");
        return Err(ProgramError::AccountNotRentExempt);
    }

    Ok(())
}

Common Rent Use Cases

1. Account creation with rent exemption:

use solana_program::rent::Rent;
use solana_program::system_instruction;
use solana_program::program::invoke_signed;

pub fn create_account_rent_exempt(
    payer: &AccountInfo,
    new_account: &AccountInfo,
    system_program: &AccountInfo,
    program_id: &Pubkey,
    seeds: &[&[u8]],
    space: usize,
) -> ProgramResult {
    let rent = Rent::get()?;
    let min_balance = rent.minimum_balance(space);

    msg!("Creating account with {} lamports for {} bytes", min_balance, space);

    let create_account_ix = system_instruction::create_account(
        payer.key,
        new_account.key,
        min_balance,
        space as u64,
        program_id,
    );

    invoke_signed(
        &create_account_ix,
        &[payer.clone(), new_account.clone(), system_program.clone()],
        &[seeds],
    )?;

    Ok(())
}

2. Validating account has sufficient balance:

pub fn validate_rent_exempt_account(
    account: &AccountInfo,
) -> ProgramResult {
    let rent = Rent::get()?;

    if !rent.is_exempt(account.lamports(), account.data_len()) {
        let required = rent.minimum_balance(account.data_len());
        let current = account.lamports();

        msg!("Account not rent-exempt: has {} lamports, needs {}",
            current, required);

        return Err(ProgramError::AccountNotRentExempt);
    }

    Ok(())
}

3. Calculating required lamports for reallocation:

pub fn reallocate_account(
    account: &AccountInfo,
    new_size: usize,
) -> ProgramResult {
    let rent = Rent::get()?;

    let old_size = account.data_len();
    let current_lamports = account.lamports();

    let new_min_balance = rent.minimum_balance(new_size);

    if new_size > old_size {
        // Growing account - ensure sufficient lamports
        if current_lamports < new_min_balance {
            msg!("Need {} more lamports for reallocation",
                new_min_balance - current_lamports);
            return Err(ProgramError::InsufficientFunds);
        }
    }

    account.realloc(new_size, false)?;
    Ok(())
}

EpochSchedule Sysvar

Address: solana_program::sysvar::epoch_schedule::ID

The EpochSchedule sysvar provides information about epoch timing and slot calculations.

EpochSchedule Structure

use solana_program::epoch_schedule::EpochSchedule;

pub struct EpochSchedule {
    pub slots_per_epoch: u64,              // Slots per epoch after warmup
    pub leader_schedule_slot_offset: u64,  // Offset for leader schedule
    pub warmup: bool,                      // Whether in warmup period
    pub first_normal_epoch: Epoch,         // First non-warmup epoch
    pub first_normal_slot: Slot,           // First slot of first normal epoch
}

Accessing EpochSchedule

use solana_program::sysvar::epoch_schedule::EpochSchedule;
use solana_program::sysvar::Sysvar;

pub fn get_epoch_info() -> ProgramResult {
    let epoch_schedule = EpochSchedule::get()?;

    msg!("Slots per epoch: {}", epoch_schedule.slots_per_epoch);
    msg!("First normal epoch: {}", epoch_schedule.first_normal_epoch);
    msg!("Warmup: {}", epoch_schedule.warmup);

    Ok(())
}

Common EpochSchedule Use Cases

1. Calculating epoch from slot:

use solana_program::clock::Clock;
use solana_program::epoch_schedule::EpochSchedule;

pub fn calculate_epoch_from_slot(
    slot: u64,
) -> Result<u64, ProgramError> {
    let epoch_schedule = EpochSchedule::get()?;

    let epoch = epoch_schedule.get_epoch(slot);
    msg!("Slot {} is in epoch {}", slot, epoch);

    Ok(epoch)
}

2. Determining slots remaining in epoch:

pub fn slots_until_epoch_end() -> Result<u64, ProgramError> {
    let clock = Clock::get()?;
    let epoch_schedule = EpochSchedule::get()?;

    let current_slot = clock.slot;
    let current_epoch = clock.epoch;

    // Get first slot of next epoch
    let next_epoch_start = epoch_schedule.get_first_slot_in_epoch(current_epoch + 1);

    let remaining = next_epoch_start - current_slot;
    msg!("Slots remaining in epoch: {}", remaining);

    Ok(remaining)
}

3. Epoch-based reward distribution:

#[derive(BorshSerialize, BorshDeserialize)]
pub struct RewardState {
    pub last_distribution_epoch: u64,
    pub total_distributed: u64,
}

pub fn distribute_epoch_rewards(
    reward_state_account: &AccountInfo,
) -> ProgramResult {
    let clock = Clock::get()?;
    let mut state = RewardState::try_from_slice(&reward_state_account.data.borrow())?;

    if clock.epoch > state.last_distribution_epoch {
        let epochs_passed = clock.epoch - state.last_distribution_epoch;

        msg!("Distributing rewards for {} epochs", epochs_passed);

        // Distribute rewards
        let reward_amount = epochs_passed * 1000; // Example
        state.total_distributed += reward_amount;
        state.last_distribution_epoch = clock.epoch;

        state.serialize(&mut &mut reward_state_account.data.borrow_mut()[..])?;
    }

    Ok(())
}

SlotHashes Sysvar

Address: solana_program::sysvar::slot_hashes::ID

The SlotHashes sysvar contains recent slot hashes for verification purposes.

SlotHashes Structure

use solana_program::slot_hashes::SlotHashes;

// SlotHashes contains up to 512 recent (slot, hash) pairs
pub struct SlotHashes {
    // Vector of (slot, hash) tuples
    // Most recent first, up to MAX_ENTRIES (512)
}

Accessing SlotHashes

use solana_program::sysvar::slot_hashes::SlotHashes;
use solana_program::sysvar::Sysvar;

pub fn verify_recent_slot(
    claimed_slot: u64,
    claimed_hash: &[u8; 32],
) -> ProgramResult {
    let slot_hashes = SlotHashes::get()?;

    // Check if slot is in recent history
    for (slot, hash) in slot_hashes.iter() {
        if *slot == claimed_slot {
            if hash.as_ref() == claimed_hash {
                msg!("Slot hash verified!");
                return Ok(());
            } else {
                msg!("Slot hash mismatch!");
                return Err(ProgramError::InvalidArgument);
            }
        }
    }

    msg!("Slot not found in recent history");
    Err(ProgramError::InvalidArgument)
}

Common SlotHashes Use Cases

1. Verifying transaction recency:

pub fn verify_transaction_recent(
    slot_hashes_account: &AccountInfo,
    claimed_slot: u64,
) -> ProgramResult {
    let slot_hashes = SlotHashes::from_account_info(slot_hashes_account)?;

    // Check if claimed slot is in recent 512 slots
    let is_recent = slot_hashes.iter().any(|(slot, _)| *slot == claimed_slot);

    if !is_recent {
        msg!("Transaction too old or slot invalid");
        return Err(ProgramError::Custom(1));
    }

    Ok(())
}

2. Preventing replay attacks:

#[derive(BorshSerialize, BorshDeserialize)]
pub struct ProcessedSlot {
    pub slot: u64,
    pub hash: [u8; 32],
}

pub fn process_once_per_slot(
    state_account: &AccountInfo,
) -> ProgramResult {
    let slot_hashes = SlotHashes::get()?;
    let mut state = ProcessedSlot::try_from_slice(&state_account.data.borrow())?;

    // Get current slot and hash
    let (current_slot, current_hash) = slot_hashes.iter().next()
        .ok_or(ProgramError::InvalidArgument)?;

    if state.slot == *current_slot {
        msg!("Already processed in this slot!");
        return Err(ProgramError::Custom(2)); // Already processed
    }

    // Update state
    state.slot = *current_slot;
    state.hash = current_hash.to_bytes();
    state.serialize(&mut &mut state_account.data.borrow_mut()[..])?;

    Ok(())
}

⚠️ Note: SlotHashes only maintains the most recent 512 slots. For older verification, use a different approach.


Other Sysvars

StakeHistory

Address: solana_program::sysvar::stake_history::ID

Provides historical stake activation and deactivation information.

use solana_program::sysvar::stake_history::StakeHistory;

pub fn get_stake_history() -> ProgramResult {
    let stake_history = StakeHistory::get()?;

    // Access historical stake data by epoch
    msg!("Stake history available");
    Ok(())
}

Use cases:

  • Stake pool programs
  • Historical stake analysis
  • Reward calculations

EpochRewards

Address: solana_program::sysvar::epoch_rewards::ID

Provides information about epoch rewards distribution (if active).

use solana_program::sysvar::epoch_rewards::EpochRewards;

pub fn check_epoch_rewards() -> ProgramResult {
    let epoch_rewards = EpochRewards::get()?;

    msg!("Epoch rewards data available");
    Ok(())
}

Use cases:

  • Stake reward programs
  • Validator reward tracking

Instructions

Address: solana_program::sysvar::instructions::ID

Provides access to instructions in the current transaction.

use solana_program::sysvar::instructions;

pub fn validate_transaction_instructions(
    instructions_account: &AccountInfo,
) -> ProgramResult {
    // Check if current instruction is not the first
    let current_index = instructions::load_current_index_checked(instructions_account)?;

    msg!("Current instruction index: {}", current_index);

    // Load a specific instruction
    if current_index > 0 {
        let prev_ix = instructions::load_instruction_at_checked(
            (current_index - 1) as usize,
            instructions_account,
        )?;

        msg!("Previous instruction program: {}", prev_ix.program_id);
    }

    Ok(())
}

Use cases:

  • Cross-instruction validation
  • Ensuring instruction order
  • Detecting sandwich attacks

Access Patterns

Advantages:

  • No account needed in instruction
  • Saves account space
  • Lower CU cost (~100 CU)
  • Cleaner code

Disadvantages:

  • Not supported for all sysvars
  • Can't be passed to CPIs
use solana_program::sysvar::Sysvar;

pub fn use_sysvar_direct() -> ProgramResult {
    let clock = Clock::get()?;
    let rent = Rent::get()?;

    msg!("Clock: {}", clock.unix_timestamp);
    msg!("Rent: {}", rent.lamports_per_byte_year);

    Ok(())
}

Supported sysvars:

  • Clock
  • Rent
  • EpochSchedule
  • EpochRewards
  • Fees (deprecated)

Pattern 2: from_account_info - Account Access

Advantages:

  • Works for all sysvars
  • Can be validated
  • Can be passed to CPIs
  • Required for some sysvars (SlotHashes, Instructions)

Disadvantages:

  • Account must be passed in instruction
  • Slightly higher CU cost (~300 CU)
  • More boilerplate
use solana_program::sysvar::clock;

pub fn use_sysvar_from_account(
    clock_account: &AccountInfo,
) -> ProgramResult {
    // Validate account address
    if clock_account.key != &clock::ID {
        return Err(ProgramError::InvalidArgument);
    }

    let clock = Clock::from_account_info(clock_account)?;
    msg!("Clock: {}", clock.unix_timestamp);

    Ok(())
}

Required for:

  • SlotHashes
  • StakeHistory
  • Instructions
  • Any sysvar passed to CPI

Pattern 3: Hybrid Approach

Use get() when possible, account when needed:

pub fn hybrid_sysvar_access(
    accounts: &[AccountInfo],
    need_cpi: bool,
) -> ProgramResult {
    if need_cpi {
        // Need account for CPI
        let account_info_iter = &mut accounts.iter();
        let clock_account = next_account_info(account_info_iter)?;

        let clock = Clock::from_account_info(clock_account)?;

        // Can pass clock_account to CPI
        msg!("Using account access");
    } else {
        // Direct access is cheaper
        let clock = Clock::get()?;
        msg!("Using direct access");
    }

    Ok(())
}

Performance Implications

Compute Unit Costs

Access Method Approximate CU Cost
Clock::get() ~100 CU
Rent::get() ~100 CU
EpochSchedule::get() ~100 CU
Clock::from_account_info() ~300 CU
SlotHashes::from_account_info() ~500 CU

Optimization Tips

1. Use get() when possible:

// ✅ Efficient - 100 CU
let clock = Clock::get()?;

// ❌ Wasteful - 300 CU (unless needed for CPI)
let clock = Clock::from_account_info(clock_account)?;

2. Cache sysvar values:

// ❌ Wasteful - calls get() multiple times
for i in 0..10 {
    let clock = Clock::get()?;  // 100 CU × 10 = 1000 CU
    process_item(i, clock.unix_timestamp)?;
}

// ✅ Efficient - call once
let clock = Clock::get()?;  // 100 CU
let timestamp = clock.unix_timestamp;
for i in 0..10 {
    process_item(i, timestamp)?;
}

3. Avoid unnecessary sysvar access:

// ❌ Wasteful - reading sysvar in every call
pub fn update_balance(account: &AccountInfo, amount: u64) -> ProgramResult {
    let clock = Clock::get()?;  // Not needed!
    // ... no clock usage
    Ok(())
}

// ✅ Efficient - only access when needed
pub fn update_with_timestamp(account: &AccountInfo, amount: u64) -> ProgramResult {
    let clock = Clock::get()?;  // Used below
    let timestamp = clock.unix_timestamp;
    // ... use timestamp
    Ok(())
}

Best Practices

1. Prefer get() Over from_account_info()

Unless you need the account for CPI or validation:

// ✅ Default choice
let clock = Clock::get()?;

// Only if needed for CPI
let clock = Clock::from_account_info(clock_account)?;
invoke(&ix, &[..., clock_account])?;

2. Validate Sysvar Accounts

When accepting sysvar accounts, always validate:

pub fn validate_clock_account(
    clock_account: &AccountInfo,
) -> ProgramResult {
    // ✅ Always validate sysvar address
    if clock_account.key != &solana_program::sysvar::clock::ID {
        msg!("Invalid Clock account");
        return Err(ProgramError::InvalidArgument);
    }

    Ok(())
}

3. Use Clock for Timestamps, Not Slot Hashes

For simple time-based logic:

// ✅ Simple and efficient
let clock = Clock::get()?;
if clock.unix_timestamp >= unlock_time {
    // unlock
}

// ❌ Overkill - SlotHashes is for verification, not timing
let slot_hashes = SlotHashes::get()?;
// Complex slot-based timing logic

4. Cache Sysvar Values

Read once, use multiple times:

pub fn process_multiple_accounts(
    accounts: &[AccountInfo],
) -> ProgramResult {
    // ✅ Read once
    let clock = Clock::get()?;
    let timestamp = clock.unix_timestamp;

    for account in accounts {
        update_account_timestamp(account, timestamp)?;
    }

    Ok(())
}

5. Document Sysvar Dependencies

Be explicit about which sysvars your program uses:

/// Processes user staking
///
/// # Sysvars
/// - Clock: for stake timestamp
/// - Rent: for account validation
///
/// # Accounts
/// - `[writable]` stake_account
/// - `[signer]` user
pub fn process_stake(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    amount: u64,
) -> ProgramResult {
    let clock = Clock::get()?;
    let rent = Rent::get()?;

    // ...
    Ok(())
}

6. Handle Clock Drift

Don't assume unix_timestamp is perfectly accurate:

// ❌ Risky - exact timestamp match
if clock.unix_timestamp == expected_time {
    // May never trigger
}

// ✅ Safe - use ranges
if clock.unix_timestamp >= expected_time {
    // Reliable
}

// ✅ Best - add tolerance for early/late
const TOLERANCE: i64 = 60; // 60 seconds
if clock.unix_timestamp >= expected_time - TOLERANCE {
    // Handles clock drift
}

Summary

Key Takeaways:

  1. Use get() when possible for lower CU costs and simpler code
  2. Use from_account_info() when passing to CPIs or for sysvars without get()
  3. Always validate sysvar account addresses when accepting them
  4. Cache sysvar values to avoid redundant reads
  5. Understand timing limitations - unix_timestamp is approximate

Most Common Sysvars:

Sysvar Primary Use Access Method
Clock Timestamps, epochs, slots Clock::get()
Rent Rent exemption calculations Rent::get()
EpochSchedule Epoch/slot calculations EpochSchedule::get()
SlotHashes Recent slot verification from_account_info() only
Instructions Transaction introspection from_account_info() only

Common Patterns:

// Timestamp current event
let clock = Clock::get()?;
event.created_at = clock.unix_timestamp;

// Validate rent exemption
let rent = Rent::get()?;
if !rent.is_exempt(account.lamports(), account.data_len()) {
    return Err(ProgramError::AccountNotRentExempt);
}

// Calculate rent for new account
let rent = Rent::get()?;
let min_balance = rent.minimum_balance(space);

Sysvars provide essential cluster state to your programs. Master their access patterns for efficient, production-ready Solana development.