993 lines
23 KiB
Markdown
993 lines
23 KiB
Markdown
# 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](#what-are-sysvars)
|
||
2. [Clock Sysvar](#clock-sysvar)
|
||
3. [Rent Sysvar](#rent-sysvar)
|
||
4. [EpochSchedule Sysvar](#epochschedule-sysvar)
|
||
5. [SlotHashes Sysvar](#slothashes-sysvar)
|
||
6. [Other Sysvars](#other-sysvars)
|
||
7. [Access Patterns](#access-patterns)
|
||
8. [Performance Implications](#performance-implications)
|
||
9. [Best Practices](#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
|
||
|
||
```rust
|
||
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)**
|
||
|
||
```rust
|
||
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**
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
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):**
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
// ❌ 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
|
||
|
||
```rust
|
||
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)**
|
||
|
||
```rust
|
||
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**
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
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
|
||
|
||
```rust
|
||
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
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
#[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
|
||
|
||
```rust
|
||
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
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
#[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.
|
||
|
||
```rust
|
||
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).
|
||
|
||
```rust
|
||
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.
|
||
|
||
```rust
|
||
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
|
||
|
||
### Pattern 1: get() - Direct Access (Recommended)
|
||
|
||
**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
|
||
|
||
```rust
|
||
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
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
// ✅ 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:**
|
||
|
||
```rust
|
||
// ❌ 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:**
|
||
|
||
```rust
|
||
// ❌ 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:**
|
||
|
||
```rust
|
||
// ✅ 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:**
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
// ✅ 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:**
|
||
|
||
```rust
|
||
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:**
|
||
|
||
```rust
|
||
/// 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:**
|
||
|
||
```rust
|
||
// ❌ 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:**
|
||
|
||
```rust
|
||
// 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.
|