Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:01:25 +08:00
commit d733741f8a
37 changed files with 26647 additions and 0 deletions

View File

@@ -0,0 +1,992 @@
# 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.