From 2d0d5e6e3874edf90eff4792a62ebb8c89623538 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:51:58 +0000 Subject: [PATCH 01/26] safe sol_get_sysvar from packed reprs --- define-syscall/src/definitions.rs | 12 +-- define-syscall/src/lib.rs | 14 ++-- sysvar/src/clock.rs | 88 +++++++++++++++++++- sysvar/src/epoch_rewards.rs | 88 +++++++++++++++++++- sysvar/src/epoch_schedule.rs | 75 ++++++++++++++++- sysvar/src/fees.rs | 48 ++++++++++- sysvar/src/last_restart_slot.rs | 40 ++++++++- sysvar/src/lib.rs | 132 ++++++++++++++++++++++++++++++ sysvar/src/program_stubs.rs | 33 +------- sysvar/src/rent.rs | 77 ++++++++++++++++- 10 files changed, 555 insertions(+), 52 deletions(-) diff --git a/define-syscall/src/definitions.rs b/define-syscall/src/definitions.rs index 76e91df5c..651634b51 100644 --- a/define-syscall/src/definitions.rs +++ b/define-syscall/src/definitions.rs @@ -49,12 +49,12 @@ define_syscall!(fn sol_get_return_data(data: *mut u8, length: u64, program_id: * // - `is_writable` (`u8`): `true` if the instruction requires the account to be writable define_syscall!(fn sol_get_processed_sibling_instruction(index: u64, meta: *mut u8, program_id: *mut u8, data: *mut u8, accounts: *mut u8) -> u64); -// these are to be deprecated once they are superceded by sol_get_sysvar -define_syscall!(fn sol_get_clock_sysvar(addr: *mut u8) -> u64); -define_syscall!(fn sol_get_epoch_schedule_sysvar(addr: *mut u8) -> u64); -define_syscall!(fn sol_get_rent_sysvar(addr: *mut u8) -> u64); -define_syscall!(fn sol_get_last_restart_slot(addr: *mut u8) -> u64); -define_syscall!(fn sol_get_epoch_rewards_sysvar(addr: *mut u8) -> u64); +// these are deprecated - use sol_get_sysvar instead +define_syscall!(#[deprecated(since = "3.0.0", note = "Use `sol_get_sysvar` with `Clock` sysvar address instead")] fn sol_get_clock_sysvar(addr: *mut u8) -> u64); +define_syscall!(#[deprecated(since = "3.0.0", note = "Use `sol_get_sysvar` with `EpochSchedule` sysvar address instead")] fn sol_get_epoch_schedule_sysvar(addr: *mut u8) -> u64); +define_syscall!(#[deprecated(since = "3.0.0", note = "Use `sol_get_sysvar` with `Rent` sysvar address instead")] fn sol_get_rent_sysvar(addr: *mut u8) -> u64); +define_syscall!(#[deprecated(since = "3.0.0", note = "Use `sol_get_sysvar` with `LastRestartSlot` sysvar address instead")] fn sol_get_last_restart_slot(addr: *mut u8) -> u64); +define_syscall!(#[deprecated(since = "3.0.0", note = "Use `sol_get_sysvar` with `EpochRewards` sysvar address instead")] fn sol_get_epoch_rewards_sysvar(addr: *mut u8) -> u64); // this cannot go through sol_get_sysvar but can be removed once no longer in use define_syscall!(fn sol_get_fees_sysvar(addr: *mut u8) -> u64); diff --git a/define-syscall/src/lib.rs b/define-syscall/src/lib.rs index 47f8e5ea6..3d8e956c9 100644 --- a/define-syscall/src/lib.rs +++ b/define-syscall/src/lib.rs @@ -9,7 +9,8 @@ pub mod definitions; ))] #[macro_export] macro_rules! define_syscall { - (fn $name:ident($($arg:ident: $typ:ty),*) -> $ret:ty) => { + ($(#[$attr:meta])* fn $name:ident($($arg:ident: $typ:ty),*) -> $ret:ty) => { + $(#[$attr])* #[inline] pub unsafe fn $name($($arg: $typ),*) -> $ret { // this enum is used to force the hash to be computed in a const context @@ -23,8 +24,8 @@ macro_rules! define_syscall { } }; - (fn $name:ident($($arg:ident: $typ:ty),*)) => { - define_syscall!(fn $name($($arg: $typ),*) -> ()); + ($(#[$attr:meta])* fn $name:ident($($arg:ident: $typ:ty),*)) => { + define_syscall!($(#[$attr])* fn $name($($arg: $typ),*) -> ()); } } @@ -34,13 +35,14 @@ macro_rules! define_syscall { )))] #[macro_export] macro_rules! define_syscall { - (fn $name:ident($($arg:ident: $typ:ty),*) -> $ret:ty) => { + ($(#[$attr:meta])* fn $name:ident($($arg:ident: $typ:ty),*) -> $ret:ty) => { extern "C" { + $(#[$attr])* pub fn $name($($arg: $typ),*) -> $ret; } }; - (fn $name:ident($($arg:ident: $typ:ty),*)) => { - define_syscall!(fn $name($($arg: $typ),*) -> ()); + ($(#[$attr:meta])* fn $name:ident($($arg:ident: $typ:ty),*)) => { + define_syscall!($(#[$attr])* fn $name($($arg: $typ),*) -> ()); } } diff --git a/sysvar/src/clock.rs b/sysvar/src/clock.rs index bde88bab2..4bcf300a2 100644 --- a/sysvar/src/clock.rs +++ b/sysvar/src/clock.rs @@ -130,8 +130,94 @@ pub use { }; impl Sysvar for Clock { - impl_sysvar_get!(sol_get_clock_sysvar); + impl_sysvar_get!(id()); } #[cfg(feature = "bincode")] impl SysvarSerialize for Clock {} + +#[cfg(test)] +mod tests { + use {super::*, crate::tests::to_bytes, serial_test::serial}; + + #[test] + fn test_clock_size_matches_bincode() { + // Prove that Clock's in-memory layout matches its bincode serialization, + // so we do not need to use define_sysvar_wire. + let clock = Clock::default(); + let in_memory_size = core::mem::size_of::(); + + #[cfg(feature = "bincode")] + { + let bincode_size = bincode::serialized_size(&clock).unwrap() as usize; + assert_eq!( + in_memory_size, bincode_size, + "Clock in-memory size ({in_memory_size}) must match bincode size ({bincode_size})", + ); + } + + // Clock is 5 u64s = 40 bytes + assert_eq!(in_memory_size, 40); + } + + #[test] + #[serial] + fn test_clock_get_uses_sysvar_syscall() { + let expected = Clock { + slot: 1, + epoch_start_timestamp: 2, + epoch: 3, + leader_schedule_epoch: 4, + unix_timestamp: 5, + }; + + let data = to_bytes(&expected); + crate::tests::mock_get_sysvar_syscall(&data); + + let got = Clock::get().unwrap(); + assert_eq!(got, expected); + } + + struct ValidateIdSyscall { + data: Vec, + } + + impl crate::program_stubs::SyscallStubs for ValidateIdSyscall { + fn sol_get_sysvar( + &self, + sysvar_id_addr: *const u8, + var_addr: *mut u8, + offset: u64, + length: u64, + ) -> u64 { + // Validate that the macro passed the correct sysvar id pointer + let passed_id = unsafe { *(sysvar_id_addr as *const solana_pubkey::Pubkey) }; + assert_eq!(passed_id, id()); + + let slice = unsafe { std::slice::from_raw_parts_mut(var_addr, length as usize) }; + slice.copy_from_slice( + &self.data[offset as usize..(offset.saturating_add(length)) as usize], + ); + solana_program_entrypoint::SUCCESS + } + } + + #[test] + #[serial] + fn test_clock_get_passes_correct_sysvar_id() { + let expected = Clock { + slot: 11, + epoch_start_timestamp: 22, + epoch: 33, + leader_schedule_epoch: 44, + unix_timestamp: 55, + }; + let data = to_bytes(&expected); + let prev = crate::program_stubs::set_syscall_stubs(Box::new(ValidateIdSyscall { data })); + + let got = Clock::get().unwrap(); + assert_eq!(got, expected); + + let _ = crate::program_stubs::set_syscall_stubs(prev); + } +} diff --git a/sysvar/src/epoch_rewards.rs b/sysvar/src/epoch_rewards.rs index ae741e4ef..232716471 100755 --- a/sysvar/src/epoch_rewards.rs +++ b/sysvar/src/epoch_rewards.rs @@ -156,15 +156,99 @@ #[cfg(feature = "bincode")] use crate::SysvarSerialize; -use crate::{impl_sysvar_get, Sysvar}; +use crate::{get_sysvar_via_packed, sysvar_packed_struct, Sysvar}; pub use { solana_epoch_rewards::EpochRewards, solana_sdk_ids::sysvar::epoch_rewards::{check_id, id, ID}, }; +sysvar_packed_struct! { + struct EpochRewardsPacked(81) { + distribution_starting_block_height: u64, + num_partitions: u64, + parent_blockhash: [u8; 32], + total_points: u128, + total_rewards: u64, + distributed_rewards: u64, + active: u8, // bool as u8 + } +} + +impl From for EpochRewards { + fn from(p: EpochRewardsPacked) -> Self { + Self { + distribution_starting_block_height: p.distribution_starting_block_height, + num_partitions: p.num_partitions, + parent_blockhash: solana_hash::Hash::new_from_array(p.parent_blockhash), + total_points: p.total_points, + total_rewards: p.total_rewards, + distributed_rewards: p.distributed_rewards, + active: p.active != 0, + } + } +} + impl Sysvar for EpochRewards { - impl_sysvar_get!(sol_get_epoch_rewards_sysvar); + fn get() -> Result { + get_sysvar_via_packed::(&id()) + } } #[cfg(feature = "bincode")] impl SysvarSerialize for EpochRewards {} + +#[cfg(test)] +mod tests { + use {super::*, crate::Sysvar, serial_test::serial}; + + #[test] + fn test_epoch_rewards_packed_size() { + assert_eq!(core::mem::size_of::(), 81); + } + + #[test] + #[serial] + #[cfg(feature = "bincode")] + fn test_epoch_rewards_get() { + use { + crate::program_stubs::{set_syscall_stubs, SyscallStubs}, + solana_program_entrypoint::SUCCESS, + }; + + let expected = EpochRewards { + distribution_starting_block_height: 42, + num_partitions: 7, + parent_blockhash: solana_hash::Hash::new_unique(), + total_points: 1234567890, + total_rewards: 100, + distributed_rewards: 10, + active: true, + }; + + let data = bincode::serialize(&expected).unwrap(); + assert_eq!(data.len(), 81); + + struct MockSyscall { + data: Vec, + } + impl SyscallStubs for MockSyscall { + fn sol_get_sysvar( + &self, + _sysvar_id_addr: *const u8, + var_addr: *mut u8, + offset: u64, + length: u64, + ) -> u64 { + unsafe { + let slice = core::slice::from_raw_parts_mut(var_addr, length as usize); + slice.copy_from_slice(&self.data[offset as usize..(offset + length) as usize]); + } + SUCCESS + } + } + + set_syscall_stubs(Box::new(MockSyscall { data })); + let got = EpochRewards::get().unwrap(); + assert_eq!(got, expected); + } +} diff --git a/sysvar/src/epoch_schedule.rs b/sysvar/src/epoch_schedule.rs index d189434a3..2474197ab 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -121,15 +121,86 @@ //! ``` #[cfg(feature = "bincode")] use crate::SysvarSerialize; -use crate::{impl_sysvar_get, Sysvar}; +use crate::{get_sysvar_via_packed, sysvar_packed_struct, Sysvar}; pub use { solana_epoch_schedule::EpochSchedule, solana_sdk_ids::sysvar::epoch_schedule::{check_id, id, ID}, }; +sysvar_packed_struct! { + struct EpochSchedulePacked(33) { + slots_per_epoch: u64, + leader_schedule_slot_offset: u64, + warmup: u8, // bool as u8 + first_normal_epoch: u64, + first_normal_slot: u64, + } +} + +impl From for EpochSchedule { + fn from(p: EpochSchedulePacked) -> Self { + Self { + slots_per_epoch: p.slots_per_epoch, + leader_schedule_slot_offset: p.leader_schedule_slot_offset, + warmup: p.warmup != 0, + first_normal_epoch: p.first_normal_epoch, + first_normal_slot: p.first_normal_slot, + } + } +} + impl Sysvar for EpochSchedule { - impl_sysvar_get!(sol_get_epoch_schedule_sysvar); + fn get() -> Result { + get_sysvar_via_packed::(&id()) + } } #[cfg(feature = "bincode")] impl SysvarSerialize for EpochSchedule {} + +#[cfg(test)] +mod tests { + use {super::*, crate::Sysvar, serial_test::serial}; + + #[test] + fn test_epoch_schedule_packed_size() { + assert_eq!(core::mem::size_of::(), 33); + } + + #[test] + #[serial] + #[cfg(feature = "bincode")] + fn test_epoch_schedule_get() { + use { + crate::program_stubs::{set_syscall_stubs, SyscallStubs}, + solana_program_entrypoint::SUCCESS, + }; + + let expected = EpochSchedule::custom(1234, 5678, false); + let data = bincode::serialize(&expected).unwrap(); + assert_eq!(data.len(), 33); + + struct MockSyscall { + data: Vec, + } + impl SyscallStubs for MockSyscall { + fn sol_get_sysvar( + &self, + _sysvar_id_addr: *const u8, + var_addr: *mut u8, + offset: u64, + length: u64, + ) -> u64 { + unsafe { + let slice = core::slice::from_raw_parts_mut(var_addr, length as usize); + slice.copy_from_slice(&self.data[offset as usize..(offset + length) as usize]); + } + SUCCESS + } + } + + set_syscall_stubs(Box::new(MockSyscall { data })); + let got = EpochSchedule::get().unwrap(); + assert_eq!(got, expected); + } +} diff --git a/sysvar/src/fees.rs b/sysvar/src/fees.rs index cf40e48ed..1a634e598 100644 --- a/sysvar/src/fees.rs +++ b/sysvar/src/fees.rs @@ -64,7 +64,7 @@ impl SysvarSerialize for Fees {} #[cfg(test)] mod tests { - use super::*; + use {super::*, serial_test::serial}; #[test] fn test_clone() { @@ -76,4 +76,50 @@ mod tests { let cloned_fees = fees.clone(); assert_eq!(cloned_fees, fees); } + + struct MockFeesSyscall; + impl crate::program_stubs::SyscallStubs for MockFeesSyscall { + fn sol_get_fees_sysvar(&self, var_addr: *mut u8) -> u64 { + let fees = Fees { + fee_calculator: FeeCalculator { + lamports_per_signature: 42, + }, + }; + unsafe { + std::ptr::copy_nonoverlapping( + &fees as *const _ as *const u8, + var_addr, + core::mem::size_of::(), + ); + } + solana_program_entrypoint::SUCCESS + } + } + + #[test] + #[serial] + fn test_fees_get_deprecated_syscall_path() { + let _ = crate::program_stubs::set_syscall_stubs(Box::new(MockFeesSyscall)); + let got = Fees::get().unwrap(); + assert_eq!(got.fee_calculator.lamports_per_signature, 42); + } + + struct FailFeesSyscall; + impl crate::program_stubs::SyscallStubs for FailFeesSyscall { + fn sol_get_fees_sysvar(&self, _var_addr: *mut u8) -> u64 { + 9999 + } + } + + #[test] + #[serial] + fn test_fees_get_deprecated_non_success_maps_to_unsupported() { + let prev = crate::program_stubs::set_syscall_stubs(Box::new(FailFeesSyscall)); + let got = Fees::get(); + assert_eq!( + got, + Err(solana_program_error::ProgramError::UnsupportedSysvar) + ); + let _ = crate::program_stubs::set_syscall_stubs(prev); + } } diff --git a/sysvar/src/last_restart_slot.rs b/sysvar/src/last_restart_slot.rs index 5ee10b621..2c44b6621 100644 --- a/sysvar/src/last_restart_slot.rs +++ b/sysvar/src/last_restart_slot.rs @@ -45,8 +45,46 @@ pub use { }; impl Sysvar for LastRestartSlot { - impl_sysvar_get!(sol_get_last_restart_slot); + impl_sysvar_get!(id()); } #[cfg(feature = "bincode")] impl SysvarSerialize for LastRestartSlot {} + +#[cfg(test)] +mod tests { + use {super::*, crate::tests::to_bytes, serial_test::serial}; + + #[test] + fn test_last_restart_slot_size_matches_bincode() { + // Prove that Clock's in-memory layout matches its bincode serialization, + // so we do not need to use define_sysvar_wire. + let slot = LastRestartSlot::default(); + let in_memory_size = core::mem::size_of::(); + + #[cfg(feature = "bincode")] + { + let bincode_size = bincode::serialized_size(&slot).unwrap() as usize; + assert_eq!( + in_memory_size, bincode_size, + "LastRestartSlot in-memory size ({in_memory_size}) must match bincode size ({bincode_size})", + ); + } + + // LastRestartSlot is 1 u64 = 8 bytes + assert_eq!(in_memory_size, 8); + } + + #[test] + #[serial] + fn test_last_restart_slot_get_uses_sysvar_syscall() { + let expected = LastRestartSlot { + last_restart_slot: 9999, + }; + let data = to_bytes(&expected); + crate::tests::mock_get_sysvar_syscall(&data); + + let got = LastRestartSlot::get().unwrap(); + assert_eq!(got, expected); + } +} diff --git a/sysvar/src/lib.rs b/sysvar/src/lib.rs index 0e522f658..e2d6693ea 100644 --- a/sysvar/src/lib.rs +++ b/sysvar/src/lib.rs @@ -161,6 +161,9 @@ pub trait SysvarSerialize: /// Implements the [`Sysvar::get`] method for both SBF and host targets. #[macro_export] macro_rules! impl_sysvar_get { + // DEPRECATED: This variant is only for the deprecated Fees sysvar and should be + // removed once Fees is no longer in use. It uses the old-style direct syscall + // approach instead of the new sol_get_sysvar syscall. ($syscall_name:ident) => { fn get() -> Result { let mut var = Self::default(); @@ -179,6 +182,91 @@ macro_rules! impl_sysvar_get { } } }; + ($sysvar_id:expr) => { + fn get() -> Result { + // Allocate uninitialized memory for the sysvar struct + let mut uninit = core::mem::MaybeUninit::::uninit(); + let size = core::mem::size_of::() as u64; + // Safety: we build a mutable slice pointing to the uninitialized + // buffer. The `get_sysvar` syscall will fill exactly `size` + // bytes, after which the buffer is fully initialised. + let dst = unsafe { + core::slice::from_raw_parts_mut(uninit.as_mut_ptr() as *mut u8, size as usize) + }; + // Attempt to load the sysvar data using the provided sysvar id. + $crate::get_sysvar(dst, &$sysvar_id, 0, size)?; + // Safety: `get_sysvar` succeeded and initialised the buffer. + let var = unsafe { uninit.assume_init() }; + Ok(var) + } + }; +} + +/// Defines a `#[repr(C, packed)]` struct with compile-time size and alignment checking. +/// +/// Used for sysvars whose canonical `#[repr(C)]` layout contains padding +/// that doesn't match the runtime's compact serialization format. +/// +/// # Example +/// +/// ```ignore +/// sysvar_packed_struct! { +/// struct RentPacked(17) { +/// lamports_per_byte_year: u64, +/// exemption_threshold: [u8; 8], +/// burn_percent: u8, +/// } +/// } +/// ``` +#[macro_export] +macro_rules! sysvar_packed_struct { + ( + struct $name:ident($size:expr) { + $( $field:ident : $fty:ty ),* $(,)? + } + ) => { + #[repr(C, packed)] + #[derive(Clone, Copy)] + struct $name { + $( $field: $fty ),* + } + + const _: () = assert!(core::mem::size_of::<$name>() == $size); + const _: () = assert!(core::mem::align_of::<$name>() == 1); + }; +} + +/// Generic helper to load a sysvar via a packed representation. +/// +/// 1. Allocates uninitialized memory for the packed struct +/// 2. Loads sysvar bytes directly into it via `get_sysvar_unchecked` +/// 3. Converts the packed struct to the canonical type via `From` +/// +/// # Type Parameters +/// +/// - `T`: The canonical sysvar type +/// - `P`: The packed struct (must be `Copy` and `From

for T` must exist) +/// +/// # Safety +/// +/// The packed struct `P` should be `#[repr(C, packed)]` to match the runtime's +/// compact serialization format (no padding). +pub fn get_sysvar_via_packed(sysvar_id: &Pubkey) -> Result +where + P: Copy, + T: From

, +{ + let mut packed = core::mem::MaybeUninit::

::uninit(); + let size = core::mem::size_of::

(); + unsafe { + get_sysvar_unchecked( + packed.as_mut_ptr() as *mut u8, + sysvar_id as *const _ as *const u8, + 0, + size as u64, + )?; + Ok(T::from(packed.assume_init())) + } } /// Handler for retrieving a slice of sysvar data from the `sol_get_sysvar` @@ -215,6 +303,37 @@ pub fn get_sysvar( } } +/// Internal helper for retrieving sysvar data directly into a raw buffer. +/// +/// # Safety +/// +/// This function bypasses the slice-length check that `get_sysvar` performs. +/// The caller must ensure that `var_addr` points to a writable buffer of at +/// least `length` bytes. This is typically used with `MaybeUninit` to load +/// compact representations of sysvars. +#[doc(hidden)] +pub unsafe fn get_sysvar_unchecked( + var_addr: *mut u8, + sysvar_id: *const u8, + offset: u64, + length: u64, +) -> Result<(), solana_program_error::ProgramError> { + #[cfg(target_os = "solana")] + let result = + solana_define_syscall::definitions::sol_get_sysvar(sysvar_id, var_addr, offset, length); + + #[cfg(not(target_os = "solana"))] + let result = crate::program_stubs::sol_get_sysvar(sysvar_id, var_addr, offset, length); + + match result { + solana_program_entrypoint::SUCCESS => Ok(()), + OFFSET_LENGTH_EXCEEDS_SYSVAR => Err(solana_program_error::ProgramError::InvalidArgument), + SYSVAR_NOT_FOUND => Err(solana_program_error::ProgramError::UnsupportedSysvar), + // Unexpected errors are folded into `UnsupportedSysvar`. + _ => Err(solana_program_error::ProgramError::UnsupportedSysvar), + } +} + #[cfg(test)] mod tests { use { @@ -269,6 +388,19 @@ mod tests { })); } + /// Convert a value to its in-memory byte representation. + /// + /// Safety: This relies on the type's plain old data layout. Intended for tests. + pub fn to_bytes(value: &T) -> Vec { + unsafe { + let size = core::mem::size_of::(); + let ptr = (value as *const T) as *const u8; + let mut data = vec![0u8; size]; + std::ptr::copy_nonoverlapping(ptr, data.as_mut_ptr(), size); + data + } + } + #[test] fn test_sysvar_account_info_to_from() { let test_sysvar = TestSysvar::default(); diff --git a/sysvar/src/program_stubs.rs b/sysvar/src/program_stubs.rs index ba4180656..219cce02c 100644 --- a/sysvar/src/program_stubs.rs +++ b/sysvar/src/program_stubs.rs @@ -155,32 +155,6 @@ pub(crate) fn sol_get_sysvar( .sol_get_sysvar(sysvar_id_addr, var_addr, offset, length) } -pub(crate) fn sol_get_clock_sysvar(var_addr: *mut u8) -> u64 { - SYSCALL_STUBS.read().unwrap().sol_get_clock_sysvar(var_addr) -} - -pub(crate) fn sol_get_epoch_schedule_sysvar(var_addr: *mut u8) -> u64 { - SYSCALL_STUBS - .read() - .unwrap() - .sol_get_epoch_schedule_sysvar(var_addr) -} - -pub(crate) fn sol_get_fees_sysvar(var_addr: *mut u8) -> u64 { - SYSCALL_STUBS.read().unwrap().sol_get_fees_sysvar(var_addr) -} - -pub(crate) fn sol_get_rent_sysvar(var_addr: *mut u8) -> u64 { - SYSCALL_STUBS.read().unwrap().sol_get_rent_sysvar(var_addr) -} - -pub(crate) fn sol_get_last_restart_slot(var_addr: *mut u8) -> u64 { - SYSCALL_STUBS - .read() - .unwrap() - .sol_get_last_restart_slot(var_addr) -} - pub fn sol_get_epoch_stake(vote_address: *const u8) -> u64 { SYSCALL_STUBS .read() @@ -211,9 +185,6 @@ pub fn sol_get_stack_height() -> u64 { SYSCALL_STUBS.read().unwrap().sol_get_stack_height() } -pub(crate) fn sol_get_epoch_rewards_sysvar(var_addr: *mut u8) -> u64 { - SYSCALL_STUBS - .read() - .unwrap() - .sol_get_epoch_rewards_sysvar(var_addr) +pub(crate) fn sol_get_fees_sysvar(var_addr: *mut u8) -> u64 { + SYSCALL_STUBS.read().unwrap().sol_get_fees_sysvar(var_addr) } diff --git a/sysvar/src/rent.rs b/sysvar/src/rent.rs index 84f894cf6..f9e2a472f 100644 --- a/sysvar/src/rent.rs +++ b/sysvar/src/rent.rs @@ -123,14 +123,87 @@ //! ``` #[cfg(feature = "bincode")] use crate::SysvarSerialize; -use crate::{impl_sysvar_get, Sysvar}; +use crate::{get_sysvar_via_packed, sysvar_packed_struct, Sysvar}; pub use { solana_rent::Rent, solana_sdk_ids::sysvar::rent::{check_id, id, ID}, }; + +sysvar_packed_struct! { + struct RentPacked(17) { + lamports_per_byte_year: u64, + exemption_threshold: [u8; 8], // f64 as little-endian bytes + burn_percent: u8, + } +} + +impl From for Rent { + fn from(p: RentPacked) -> Self { + Self { + lamports_per_byte_year: p.lamports_per_byte_year, + exemption_threshold: f64::from_le_bytes(p.exemption_threshold), + burn_percent: p.burn_percent, + } + } +} + impl Sysvar for Rent { - impl_sysvar_get!(sol_get_rent_sysvar); + fn get() -> Result { + get_sysvar_via_packed::(&id()) + } } #[cfg(feature = "bincode")] impl SysvarSerialize for Rent {} + +#[cfg(test)] +mod tests { + use {super::*, crate::Sysvar, serial_test::serial}; + + #[test] + fn test_rent_packed_size() { + assert_eq!(core::mem::size_of::(), 17); + } + + #[test] + #[serial] + #[cfg(feature = "bincode")] + fn test_rent_get() { + use { + crate::program_stubs::{set_syscall_stubs, SyscallStubs}, + solana_program_entrypoint::SUCCESS, + }; + + let expected = Rent { + lamports_per_byte_year: 123, + exemption_threshold: 2.5, + burn_percent: 7, + }; + + let data = bincode::serialize(&expected).unwrap(); + assert_eq!(data.len(), 17); + + struct MockSyscall { + data: Vec, + } + impl SyscallStubs for MockSyscall { + fn sol_get_sysvar( + &self, + _sysvar_id_addr: *const u8, + var_addr: *mut u8, + offset: u64, + length: u64, + ) -> u64 { + unsafe { + let slice = core::slice::from_raw_parts_mut(var_addr, length as usize); + slice.copy_from_slice(&self.data[offset as usize..(offset + length) as usize]); + } + SUCCESS + } + } + + set_syscall_stubs(Box::new(MockSyscall { data })); + let got = Rent::get().unwrap(); + assert_eq!(got, expected); + } +} From 08936c40fa989d20365aa46a4bff625fe12bec69 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:23:21 +0000 Subject: [PATCH 02/26] use decorators instead of macro, more compile-time checks --- sysvar/src/clock.rs | 2 +- sysvar/src/epoch_rewards.rs | 49 +++++++++++++++++++++------------ sysvar/src/epoch_schedule.rs | 39 ++++++++++++++++---------- sysvar/src/last_restart_slot.rs | 4 +-- sysvar/src/lib.rs | 34 ----------------------- sysvar/src/rent.rs | 29 ++++++++++++------- 6 files changed, 78 insertions(+), 79 deletions(-) diff --git a/sysvar/src/clock.rs b/sysvar/src/clock.rs index 4bcf300a2..18923ec63 100644 --- a/sysvar/src/clock.rs +++ b/sysvar/src/clock.rs @@ -143,7 +143,7 @@ mod tests { #[test] fn test_clock_size_matches_bincode() { // Prove that Clock's in-memory layout matches its bincode serialization, - // so we do not need to use define_sysvar_wire. + // so we do not need to use sysvar_packed_struct. let clock = Clock::default(); let in_memory_size = core::mem::size_of::(); diff --git a/sysvar/src/epoch_rewards.rs b/sysvar/src/epoch_rewards.rs index 232716471..92ff0408c 100755 --- a/sysvar/src/epoch_rewards.rs +++ b/sysvar/src/epoch_rewards.rs @@ -156,34 +156,47 @@ #[cfg(feature = "bincode")] use crate::SysvarSerialize; -use crate::{get_sysvar_via_packed, sysvar_packed_struct, Sysvar}; +use crate::{get_sysvar_via_packed, Sysvar}; pub use { solana_epoch_rewards::EpochRewards, solana_sdk_ids::sysvar::epoch_rewards::{check_id, id, ID}, }; -sysvar_packed_struct! { - struct EpochRewardsPacked(81) { - distribution_starting_block_height: u64, - num_partitions: u64, - parent_blockhash: [u8; 32], - total_points: u128, - total_rewards: u64, - distributed_rewards: u64, - active: u8, // bool as u8 - } +#[repr(C, packed)] +#[derive(Clone, Copy)] +struct EpochRewardsPacked { + distribution_starting_block_height: u64, + num_partitions: u64, + parent_blockhash: [u8; 32], + total_points: u128, + total_rewards: u64, + distributed_rewards: u64, + active: u8, // bool as u8 } +const _: () = assert!(core::mem::size_of::() == 81); + impl From for EpochRewards { fn from(p: EpochRewardsPacked) -> Self { + // Ensure field parity at compile time + let EpochRewardsPacked { + distribution_starting_block_height, + num_partitions, + parent_blockhash, + total_points, + total_rewards, + distributed_rewards, + active, + } = p; + Self { - distribution_starting_block_height: p.distribution_starting_block_height, - num_partitions: p.num_partitions, - parent_blockhash: solana_hash::Hash::new_from_array(p.parent_blockhash), - total_points: p.total_points, - total_rewards: p.total_rewards, - distributed_rewards: p.distributed_rewards, - active: p.active != 0, + distribution_starting_block_height, + num_partitions, + parent_blockhash: solana_hash::Hash::new_from_array(parent_blockhash), + total_points, + total_rewards, + distributed_rewards, + active: active != 0, } } } diff --git a/sysvar/src/epoch_schedule.rs b/sysvar/src/epoch_schedule.rs index 2474197ab..6f132dae1 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -121,30 +121,41 @@ //! ``` #[cfg(feature = "bincode")] use crate::SysvarSerialize; -use crate::{get_sysvar_via_packed, sysvar_packed_struct, Sysvar}; +use crate::{get_sysvar_via_packed, Sysvar}; pub use { solana_epoch_schedule::EpochSchedule, solana_sdk_ids::sysvar::epoch_schedule::{check_id, id, ID}, }; -sysvar_packed_struct! { - struct EpochSchedulePacked(33) { - slots_per_epoch: u64, - leader_schedule_slot_offset: u64, - warmup: u8, // bool as u8 - first_normal_epoch: u64, - first_normal_slot: u64, - } +#[repr(C, packed)] +#[derive(Clone, Copy)] +struct EpochSchedulePacked { + slots_per_epoch: u64, + leader_schedule_slot_offset: u64, + warmup: u8, // bool as u8 + first_normal_epoch: u64, + first_normal_slot: u64, } +const _: () = assert!(core::mem::size_of::() == 33); + impl From for EpochSchedule { fn from(p: EpochSchedulePacked) -> Self { + // Ensure field parity at compile time + let EpochSchedulePacked { + slots_per_epoch, + leader_schedule_slot_offset, + warmup, + first_normal_epoch, + first_normal_slot, + } = p; + Self { - slots_per_epoch: p.slots_per_epoch, - leader_schedule_slot_offset: p.leader_schedule_slot_offset, - warmup: p.warmup != 0, - first_normal_epoch: p.first_normal_epoch, - first_normal_slot: p.first_normal_slot, + slots_per_epoch, + leader_schedule_slot_offset, + warmup: warmup != 0, + first_normal_epoch, + first_normal_slot, } } } diff --git a/sysvar/src/last_restart_slot.rs b/sysvar/src/last_restart_slot.rs index 2c44b6621..a2d5c8e00 100644 --- a/sysvar/src/last_restart_slot.rs +++ b/sysvar/src/last_restart_slot.rs @@ -57,8 +57,8 @@ mod tests { #[test] fn test_last_restart_slot_size_matches_bincode() { - // Prove that Clock's in-memory layout matches its bincode serialization, - // so we do not need to use define_sysvar_wire. + // Prove that LastRestartSlot's in-memory layout matches its bincode serialization, + // so we do not need to use sysvar_packed_struct. let slot = LastRestartSlot::default(); let in_memory_size = core::mem::size_of::(); diff --git a/sysvar/src/lib.rs b/sysvar/src/lib.rs index e2d6693ea..39888b083 100644 --- a/sysvar/src/lib.rs +++ b/sysvar/src/lib.rs @@ -202,40 +202,6 @@ macro_rules! impl_sysvar_get { }; } -/// Defines a `#[repr(C, packed)]` struct with compile-time size and alignment checking. -/// -/// Used for sysvars whose canonical `#[repr(C)]` layout contains padding -/// that doesn't match the runtime's compact serialization format. -/// -/// # Example -/// -/// ```ignore -/// sysvar_packed_struct! { -/// struct RentPacked(17) { -/// lamports_per_byte_year: u64, -/// exemption_threshold: [u8; 8], -/// burn_percent: u8, -/// } -/// } -/// ``` -#[macro_export] -macro_rules! sysvar_packed_struct { - ( - struct $name:ident($size:expr) { - $( $field:ident : $fty:ty ),* $(,)? - } - ) => { - #[repr(C, packed)] - #[derive(Clone, Copy)] - struct $name { - $( $field: $fty ),* - } - - const _: () = assert!(core::mem::size_of::<$name>() == $size); - const _: () = assert!(core::mem::align_of::<$name>() == 1); - }; -} - /// Generic helper to load a sysvar via a packed representation. /// /// 1. Allocates uninitialized memory for the packed struct diff --git a/sysvar/src/rent.rs b/sysvar/src/rent.rs index f9e2a472f..8fac70277 100644 --- a/sysvar/src/rent.rs +++ b/sysvar/src/rent.rs @@ -123,26 +123,35 @@ //! ``` #[cfg(feature = "bincode")] use crate::SysvarSerialize; -use crate::{get_sysvar_via_packed, sysvar_packed_struct, Sysvar}; +use crate::{get_sysvar_via_packed, Sysvar}; pub use { solana_rent::Rent, solana_sdk_ids::sysvar::rent::{check_id, id, ID}, }; -sysvar_packed_struct! { - struct RentPacked(17) { - lamports_per_byte_year: u64, - exemption_threshold: [u8; 8], // f64 as little-endian bytes - burn_percent: u8, - } +#[repr(C, packed)] +#[derive(Clone, Copy)] +struct RentPacked { + lamports_per_byte_year: u64, + exemption_threshold: [u8; 8], // f64 as little-endian bytes + burn_percent: u8, } +const _: () = assert!(core::mem::size_of::() == 17); + impl From for Rent { fn from(p: RentPacked) -> Self { + // Ensure field parity at compile time + let RentPacked { + lamports_per_byte_year, + exemption_threshold, + burn_percent, + } = p; + Self { - lamports_per_byte_year: p.lamports_per_byte_year, - exemption_threshold: f64::from_le_bytes(p.exemption_threshold), - burn_percent: p.burn_percent, + lamports_per_byte_year, + exemption_threshold: f64::from_le_bytes(exemption_threshold), + burn_percent, } } } From 70cae6a06a7b2938ddcb611897c33e253bc0e3f9 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:26:25 +0000 Subject: [PATCH 03/26] clean up to_bytes --- sysvar/src/lib.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/sysvar/src/lib.rs b/sysvar/src/lib.rs index 39888b083..de2472849 100644 --- a/sysvar/src/lib.rs +++ b/sysvar/src/lib.rs @@ -263,8 +263,6 @@ pub fn get_sysvar( match result { solana_program_entrypoint::SUCCESS => Ok(()), OFFSET_LENGTH_EXCEEDS_SYSVAR => Err(solana_program_error::ProgramError::InvalidArgument), - SYSVAR_NOT_FOUND => Err(solana_program_error::ProgramError::UnsupportedSysvar), - // Unexpected errors are folded into `UnsupportedSysvar`. _ => Err(solana_program_error::ProgramError::UnsupportedSysvar), } } @@ -356,15 +354,18 @@ mod tests { /// Convert a value to its in-memory byte representation. /// - /// Safety: This relies on the type's plain old data layout. Intended for tests. + /// # Safety + /// + /// This function is only safe for plain-old-data types with no padding. + /// Intended for test use only. pub fn to_bytes(value: &T) -> Vec { + let size = core::mem::size_of::(); + let ptr = (value as *const T) as *const u8; + let mut data = vec![0u8; size]; unsafe { - let size = core::mem::size_of::(); - let ptr = (value as *const T) as *const u8; - let mut data = vec![0u8; size]; std::ptr::copy_nonoverlapping(ptr, data.as_mut_ptr(), size); - data } + data } #[test] From 732d1e831200cf71d5d9ef40380d3a56cda8f26a Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:34:04 +0000 Subject: [PATCH 04/26] tests: rm redundant, clean up bincode gating --- sysvar/src/clock.rs | 17 ++++++----------- sysvar/src/epoch_rewards.rs | 5 ----- sysvar/src/epoch_schedule.rs | 5 ----- sysvar/src/last_restart_slot.rs | 17 ++++++----------- sysvar/src/rent.rs | 5 ----- 5 files changed, 12 insertions(+), 37 deletions(-) diff --git a/sysvar/src/clock.rs b/sysvar/src/clock.rs index 18923ec63..c6c90b3c7 100644 --- a/sysvar/src/clock.rs +++ b/sysvar/src/clock.rs @@ -141,23 +141,18 @@ mod tests { use {super::*, crate::tests::to_bytes, serial_test::serial}; #[test] + #[cfg(feature = "bincode")] fn test_clock_size_matches_bincode() { // Prove that Clock's in-memory layout matches its bincode serialization, // so we do not need to use sysvar_packed_struct. let clock = Clock::default(); let in_memory_size = core::mem::size_of::(); + let bincode_size = bincode::serialized_size(&clock).unwrap() as usize; - #[cfg(feature = "bincode")] - { - let bincode_size = bincode::serialized_size(&clock).unwrap() as usize; - assert_eq!( - in_memory_size, bincode_size, - "Clock in-memory size ({in_memory_size}) must match bincode size ({bincode_size})", - ); - } - - // Clock is 5 u64s = 40 bytes - assert_eq!(in_memory_size, 40); + assert_eq!( + in_memory_size, bincode_size, + "Clock in-memory size ({in_memory_size}) must match bincode size ({bincode_size})", + ); } #[test] diff --git a/sysvar/src/epoch_rewards.rs b/sysvar/src/epoch_rewards.rs index 92ff0408c..1d009286c 100755 --- a/sysvar/src/epoch_rewards.rs +++ b/sysvar/src/epoch_rewards.rs @@ -214,11 +214,6 @@ impl SysvarSerialize for EpochRewards {} mod tests { use {super::*, crate::Sysvar, serial_test::serial}; - #[test] - fn test_epoch_rewards_packed_size() { - assert_eq!(core::mem::size_of::(), 81); - } - #[test] #[serial] #[cfg(feature = "bincode")] diff --git a/sysvar/src/epoch_schedule.rs b/sysvar/src/epoch_schedule.rs index 6f132dae1..41c7d7e42 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -173,11 +173,6 @@ impl SysvarSerialize for EpochSchedule {} mod tests { use {super::*, crate::Sysvar, serial_test::serial}; - #[test] - fn test_epoch_schedule_packed_size() { - assert_eq!(core::mem::size_of::(), 33); - } - #[test] #[serial] #[cfg(feature = "bincode")] diff --git a/sysvar/src/last_restart_slot.rs b/sysvar/src/last_restart_slot.rs index a2d5c8e00..382e70e8f 100644 --- a/sysvar/src/last_restart_slot.rs +++ b/sysvar/src/last_restart_slot.rs @@ -56,23 +56,18 @@ mod tests { use {super::*, crate::tests::to_bytes, serial_test::serial}; #[test] + #[cfg(feature = "bincode")] fn test_last_restart_slot_size_matches_bincode() { // Prove that LastRestartSlot's in-memory layout matches its bincode serialization, // so we do not need to use sysvar_packed_struct. let slot = LastRestartSlot::default(); let in_memory_size = core::mem::size_of::(); + let bincode_size = bincode::serialized_size(&slot).unwrap() as usize; - #[cfg(feature = "bincode")] - { - let bincode_size = bincode::serialized_size(&slot).unwrap() as usize; - assert_eq!( - in_memory_size, bincode_size, - "LastRestartSlot in-memory size ({in_memory_size}) must match bincode size ({bincode_size})", - ); - } - - // LastRestartSlot is 1 u64 = 8 bytes - assert_eq!(in_memory_size, 8); + assert_eq!( + in_memory_size, bincode_size, + "LastRestartSlot in-memory size ({in_memory_size}) must match bincode size ({bincode_size})", + ); } #[test] diff --git a/sysvar/src/rent.rs b/sysvar/src/rent.rs index 8fac70277..f7fde46ab 100644 --- a/sysvar/src/rent.rs +++ b/sysvar/src/rent.rs @@ -169,11 +169,6 @@ impl SysvarSerialize for Rent {} mod tests { use {super::*, crate::Sysvar, serial_test::serial}; - #[test] - fn test_rent_packed_size() { - assert_eq!(core::mem::size_of::(), 17); - } - #[test] #[serial] #[cfg(feature = "bincode")] From 07e5fb1aeef9cb3a78ebbe6a69f7f8b5b316f5d4 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:41:35 +0000 Subject: [PATCH 05/26] rm test util duplication --- sysvar/src/clock.rs | 26 +--------------------- sysvar/src/epoch_rewards.rs | 26 +--------------------- sysvar/src/epoch_schedule.rs | 26 +--------------------- sysvar/src/lib.rs | 42 +++++++++++++++++++++++++++++++++++- sysvar/src/rent.rs | 26 +--------------------- 5 files changed, 45 insertions(+), 101 deletions(-) diff --git a/sysvar/src/clock.rs b/sysvar/src/clock.rs index c6c90b3c7..259a47533 100644 --- a/sysvar/src/clock.rs +++ b/sysvar/src/clock.rs @@ -173,30 +173,6 @@ mod tests { assert_eq!(got, expected); } - struct ValidateIdSyscall { - data: Vec, - } - - impl crate::program_stubs::SyscallStubs for ValidateIdSyscall { - fn sol_get_sysvar( - &self, - sysvar_id_addr: *const u8, - var_addr: *mut u8, - offset: u64, - length: u64, - ) -> u64 { - // Validate that the macro passed the correct sysvar id pointer - let passed_id = unsafe { *(sysvar_id_addr as *const solana_pubkey::Pubkey) }; - assert_eq!(passed_id, id()); - - let slice = unsafe { std::slice::from_raw_parts_mut(var_addr, length as usize) }; - slice.copy_from_slice( - &self.data[offset as usize..(offset.saturating_add(length)) as usize], - ); - solana_program_entrypoint::SUCCESS - } - } - #[test] #[serial] fn test_clock_get_passes_correct_sysvar_id() { @@ -208,7 +184,7 @@ mod tests { unix_timestamp: 55, }; let data = to_bytes(&expected); - let prev = crate::program_stubs::set_syscall_stubs(Box::new(ValidateIdSyscall { data })); + let prev = crate::tests::mock_get_sysvar_syscall_with_id(&data, &id()); let got = Clock::get().unwrap(); assert_eq!(got, expected); diff --git a/sysvar/src/epoch_rewards.rs b/sysvar/src/epoch_rewards.rs index 1d009286c..14bcc450c 100755 --- a/sysvar/src/epoch_rewards.rs +++ b/sysvar/src/epoch_rewards.rs @@ -218,11 +218,6 @@ mod tests { #[serial] #[cfg(feature = "bincode")] fn test_epoch_rewards_get() { - use { - crate::program_stubs::{set_syscall_stubs, SyscallStubs}, - solana_program_entrypoint::SUCCESS, - }; - let expected = EpochRewards { distribution_starting_block_height: 42, num_partitions: 7, @@ -236,26 +231,7 @@ mod tests { let data = bincode::serialize(&expected).unwrap(); assert_eq!(data.len(), 81); - struct MockSyscall { - data: Vec, - } - impl SyscallStubs for MockSyscall { - fn sol_get_sysvar( - &self, - _sysvar_id_addr: *const u8, - var_addr: *mut u8, - offset: u64, - length: u64, - ) -> u64 { - unsafe { - let slice = core::slice::from_raw_parts_mut(var_addr, length as usize); - slice.copy_from_slice(&self.data[offset as usize..(offset + length) as usize]); - } - SUCCESS - } - } - - set_syscall_stubs(Box::new(MockSyscall { data })); + crate::tests::mock_get_sysvar_syscall(&data); let got = EpochRewards::get().unwrap(); assert_eq!(got, expected); } diff --git a/sysvar/src/epoch_schedule.rs b/sysvar/src/epoch_schedule.rs index 41c7d7e42..637911d8a 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -177,35 +177,11 @@ mod tests { #[serial] #[cfg(feature = "bincode")] fn test_epoch_schedule_get() { - use { - crate::program_stubs::{set_syscall_stubs, SyscallStubs}, - solana_program_entrypoint::SUCCESS, - }; - let expected = EpochSchedule::custom(1234, 5678, false); let data = bincode::serialize(&expected).unwrap(); assert_eq!(data.len(), 33); - struct MockSyscall { - data: Vec, - } - impl SyscallStubs for MockSyscall { - fn sol_get_sysvar( - &self, - _sysvar_id_addr: *const u8, - var_addr: *mut u8, - offset: u64, - length: u64, - ) -> u64 { - unsafe { - let slice = core::slice::from_raw_parts_mut(var_addr, length as usize); - slice.copy_from_slice(&self.data[offset as usize..(offset + length) as usize]); - } - SUCCESS - } - } - - set_syscall_stubs(Box::new(MockSyscall { data })); + crate::tests::mock_get_sysvar_syscall(&data); let got = EpochSchedule::get().unwrap(); assert_eq!(got, expected); } diff --git a/sysvar/src/lib.rs b/sysvar/src/lib.rs index de2472849..441395563 100644 --- a/sysvar/src/lib.rs +++ b/sysvar/src/lib.rs @@ -328,10 +328,13 @@ mod tests { impl Sysvar for TestSysvar {} impl SysvarSerialize for TestSysvar {} - // NOTE tests that use this mock MUST carry the #[serial] attribute + // NOTE: Tests using these mocks MUST carry the #[serial] attribute + // because they modify global SYSCALL_STUBS state. + struct MockGetSysvarSyscall { data: Vec, } + impl SyscallStubs for MockGetSysvarSyscall { #[allow(clippy::arithmetic_side_effects)] fn sol_get_sysvar( @@ -346,12 +349,49 @@ mod tests { SUCCESS } } + + /// Mock syscall stub for tests. Requires `#[serial]` attribute. pub fn mock_get_sysvar_syscall(data: &[u8]) { set_syscall_stubs(Box::new(MockGetSysvarSyscall { data: data.to_vec(), })); } + struct ValidateIdSyscall { + data: Vec, + expected_id: Pubkey, + } + + impl SyscallStubs for ValidateIdSyscall { + #[allow(clippy::arithmetic_side_effects)] + fn sol_get_sysvar( + &self, + sysvar_id_addr: *const u8, + var_addr: *mut u8, + offset: u64, + length: u64, + ) -> u64 { + // Validate that the correct sysvar id pointer was passed + let passed_id = unsafe { *(sysvar_id_addr as *const Pubkey) }; + assert_eq!(passed_id, self.expected_id); + + let slice = unsafe { std::slice::from_raw_parts_mut(var_addr, length as usize) }; + slice.copy_from_slice(&self.data[offset as usize..(offset + length) as usize]); + SUCCESS + } + } + + /// Mock syscall stub that validates sysvar ID. Requires `#[serial]` attribute. + pub fn mock_get_sysvar_syscall_with_id( + data: &[u8], + expected_id: &Pubkey, + ) -> Box { + set_syscall_stubs(Box::new(ValidateIdSyscall { + data: data.to_vec(), + expected_id: *expected_id, + })) + } + /// Convert a value to its in-memory byte representation. /// /// # Safety diff --git a/sysvar/src/rent.rs b/sysvar/src/rent.rs index f7fde46ab..2bbbe5eac 100644 --- a/sysvar/src/rent.rs +++ b/sysvar/src/rent.rs @@ -173,11 +173,6 @@ mod tests { #[serial] #[cfg(feature = "bincode")] fn test_rent_get() { - use { - crate::program_stubs::{set_syscall_stubs, SyscallStubs}, - solana_program_entrypoint::SUCCESS, - }; - let expected = Rent { lamports_per_byte_year: 123, exemption_threshold: 2.5, @@ -187,26 +182,7 @@ mod tests { let data = bincode::serialize(&expected).unwrap(); assert_eq!(data.len(), 17); - struct MockSyscall { - data: Vec, - } - impl SyscallStubs for MockSyscall { - fn sol_get_sysvar( - &self, - _sysvar_id_addr: *const u8, - var_addr: *mut u8, - offset: u64, - length: u64, - ) -> u64 { - unsafe { - let slice = core::slice::from_raw_parts_mut(var_addr, length as usize); - slice.copy_from_slice(&self.data[offset as usize..(offset + length) as usize]); - } - SUCCESS - } - } - - set_syscall_stubs(Box::new(MockSyscall { data })); + crate::tests::mock_get_sysvar_syscall(&data); let got = Rent::get().unwrap(); assert_eq!(got, expected); } From a359358939f55a36030f181cfed6781955bd255e Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:49:29 +0000 Subject: [PATCH 06/26] rm from_raw_parts_mut ub --- sysvar/src/lib.rs | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/sysvar/src/lib.rs b/sysvar/src/lib.rs index 441395563..535500051 100644 --- a/sysvar/src/lib.rs +++ b/sysvar/src/lib.rs @@ -177,27 +177,24 @@ macro_rules! impl_sysvar_get { match result { $crate::__private::SUCCESS => Ok(var), - // Unexpected errors are folded into `UnsupportedSysvar`. _ => Err($crate::__private::ProgramError::UnsupportedSysvar), } } }; ($sysvar_id:expr) => { fn get() -> Result { - // Allocate uninitialized memory for the sysvar struct let mut uninit = core::mem::MaybeUninit::::uninit(); let size = core::mem::size_of::() as u64; - // Safety: we build a mutable slice pointing to the uninitialized - // buffer. The `get_sysvar` syscall will fill exactly `size` - // bytes, after which the buffer is fully initialised. - let dst = unsafe { - core::slice::from_raw_parts_mut(uninit.as_mut_ptr() as *mut u8, size as usize) - }; - // Attempt to load the sysvar data using the provided sysvar id. - $crate::get_sysvar(dst, &$sysvar_id, 0, size)?; - // Safety: `get_sysvar` succeeded and initialised the buffer. - let var = unsafe { uninit.assume_init() }; - Ok(var) + let sysvar_id_ptr = (&$sysvar_id) as *const _ as *const u8; + unsafe { + $crate::get_sysvar_unchecked( + uninit.as_mut_ptr() as *mut u8, + sysvar_id_ptr, + 0, + size, + )?; + Ok(uninit.assume_init()) + } } }; } @@ -292,8 +289,6 @@ pub unsafe fn get_sysvar_unchecked( match result { solana_program_entrypoint::SUCCESS => Ok(()), OFFSET_LENGTH_EXCEEDS_SYSVAR => Err(solana_program_error::ProgramError::InvalidArgument), - SYSVAR_NOT_FOUND => Err(solana_program_error::ProgramError::UnsupportedSysvar), - // Unexpected errors are folded into `UnsupportedSysvar`. _ => Err(solana_program_error::ProgramError::UnsupportedSysvar), } } From 6070ca09f75c8439513e865524d18fc75cab7752 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:52:44 +0000 Subject: [PATCH 07/26] readd intentionally redundant arm --- sysvar/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sysvar/src/lib.rs b/sysvar/src/lib.rs index 535500051..7c9e40605 100644 --- a/sysvar/src/lib.rs +++ b/sysvar/src/lib.rs @@ -289,6 +289,8 @@ pub unsafe fn get_sysvar_unchecked( match result { solana_program_entrypoint::SUCCESS => Ok(()), OFFSET_LENGTH_EXCEEDS_SYSVAR => Err(solana_program_error::ProgramError::InvalidArgument), + SYSVAR_NOT_FOUND => Err(solana_program_error::ProgramError::UnsupportedSysvar), + // Unexpected errors are folded into `UnsupportedSysvar`. _ => Err(solana_program_error::ProgramError::UnsupportedSysvar), } } From 201ffb1fe7ba6a1bcd45de3a78b0bbac8e43d5f0 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:20:59 +0000 Subject: [PATCH 08/26] rework sysvars to byte arrays and accessors --- epoch-rewards/src/lib.rs | 91 ++++++++++++++++++++++-------------- epoch-schedule/src/lib.rs | 87 ++++++++++++++++++++-------------- genesis-config/src/lib.rs | 6 +-- rent/src/lib.rs | 73 ++++++++++++++++++----------- sysvar/src/epoch_rewards.rs | 79 +++---------------------------- sysvar/src/epoch_schedule.rs | 39 +--------------- sysvar/src/lib.rs | 33 ------------- sysvar/src/rent.rs | 40 ++-------------- 8 files changed, 170 insertions(+), 278 deletions(-) diff --git a/epoch-rewards/src/lib.rs b/epoch-rewards/src/lib.rs index 5d581891a..749654e6b 100644 --- a/epoch-rewards/src/lib.rs +++ b/epoch-rewards/src/lib.rs @@ -19,18 +19,18 @@ extern crate std; use serde_derive::{Deserialize, Serialize}; use {solana_hash::Hash, solana_sdk_macro::CloneZeroed}; -#[repr(C, align(16))] +#[repr(C)] #[cfg_attr(feature = "frozen-abi", derive(solana_frozen_abi_macro::AbiExample))] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] #[derive(Debug, PartialEq, Eq, Default, CloneZeroed)] pub struct EpochRewards { /// The starting block height of the rewards distribution in the current /// epoch - pub distribution_starting_block_height: u64, + distribution_starting_block_height: [u8; 8], /// Number of partitions in the rewards distribution in the current epoch, /// used to generate an EpochRewardsHasher - pub num_partitions: u64, + num_partitions: [u8; 8], /// The blockhash of the parent block of the first block in the epoch, used /// to seed an EpochRewardsHasher @@ -39,64 +39,85 @@ pub struct EpochRewards { /// The total rewards points calculated for the current epoch, where points /// equals the sum of (delegated stake * credits observed) for all /// delegations - pub total_points: u128, + total_points: [u8; 16], /// The total rewards calculated for the current epoch. This may be greater /// than the total `distributed_rewards` at the end of the rewards period, /// due to rounding and inability to deliver rewards smaller than 1 lamport. - pub total_rewards: u64, + total_rewards: [u8; 8], /// The rewards currently distributed for the current epoch, in lamports - pub distributed_rewards: u64, + distributed_rewards: [u8; 8], /// Whether the rewards period (including calculation and distribution) is /// active - pub active: bool, + pub active: u8, } impl EpochRewards { - pub fn distribute(&mut self, amount: u64) { - let new_distributed_rewards = self.distributed_rewards.saturating_add(amount); - assert!(new_distributed_rewards <= self.total_rewards); - self.distributed_rewards = new_distributed_rewards; + pub fn distribution_starting_block_height(&self) -> u64 { + u64::from_le_bytes(self.distribution_starting_block_height) } -} -#[cfg(test)] -mod tests { - use super::*; + pub fn num_partitions(&self) -> u64 { + u64::from_le_bytes(self.num_partitions) + } + + pub fn parent_blockhash(&self) -> &Hash { + &self.parent_blockhash + } + + pub fn total_points(&self) -> u128 { + u128::from_le_bytes(self.total_points) + } + + pub fn total_rewards(&self) -> u64 { + u64::from_le_bytes(self.total_rewards) + } - impl EpochRewards { - pub fn new( - total_rewards: u64, - distributed_rewards: u64, - distribution_starting_block_height: u64, - ) -> Self { - Self { - total_rewards, - distributed_rewards, - distribution_starting_block_height, - ..Self::default() - } + pub fn distributed_rewards(&self) -> u64 { + u64::from_le_bytes(self.distributed_rewards) + } + + pub fn active(&self) -> bool { + match self.active { + 0 => false, + 1 => true, + _ => panic!("invalid active value"), } } - #[test] - fn test_epoch_rewards_new() { - let epoch_rewards = EpochRewards::new(100, 0, 64); + pub fn new( + total_rewards: u64, + distributed_rewards: u64, + distribution_starting_block_height: u64, + ) -> Self { + Self { + distribution_starting_block_height: distribution_starting_block_height.to_le_bytes(), + total_rewards: total_rewards.to_le_bytes(), + distributed_rewards: distributed_rewards.to_le_bytes(), + ..Self::default() + } + } - assert_eq!(epoch_rewards.total_rewards, 100); - assert_eq!(epoch_rewards.distributed_rewards, 0); - assert_eq!(epoch_rewards.distribution_starting_block_height, 64); + pub fn distribute(&mut self, amount: u64) { + let new_distributed_rewards = self.distributed_rewards().saturating_add(amount); + assert!(new_distributed_rewards <= self.total_rewards()); + self.distributed_rewards = new_distributed_rewards.to_le_bytes(); } +} + +#[cfg(test)] +mod tests { + use super::*; #[test] fn test_epoch_rewards_distribute() { let mut epoch_rewards = EpochRewards::new(100, 0, 64); epoch_rewards.distribute(100); - assert_eq!(epoch_rewards.total_rewards, 100); - assert_eq!(epoch_rewards.distributed_rewards, 100); + assert_eq!(epoch_rewards.total_rewards(), 100); + assert_eq!(epoch_rewards.distributed_rewards(), 100); } #[test] diff --git a/epoch-schedule/src/lib.rs b/epoch-schedule/src/lib.rs index a1ed5c16a..dfc7ba1a0 100644 --- a/epoch-schedule/src/lib.rs +++ b/epoch-schedule/src/lib.rs @@ -56,24 +56,24 @@ pub const MINIMUM_SLOTS_PER_EPOCH: u64 = 32; #[derive(Debug, CloneZeroed, PartialEq, Eq)] pub struct EpochSchedule { /// The maximum number of slots in each epoch. - pub slots_per_epoch: u64, + slots_per_epoch: [u8; 8], /// A number of slots before beginning of an epoch to calculate /// a leader schedule for that epoch. - pub leader_schedule_slot_offset: u64, + leader_schedule_slot_offset: [u8; 8], /// Whether epochs start short and grow. - pub warmup: bool, + pub warmup: u8, /// The first epoch after the warmup period. /// /// Basically: `log2(slots_per_epoch) - log2(MINIMUM_SLOTS_PER_EPOCH)`. - pub first_normal_epoch: u64, + first_normal_epoch: [u8; 8], /// The first slot after the warmup period. /// /// Basically: `MINIMUM_SLOTS_PER_EPOCH * (2.pow(first_normal_epoch) - 1)`. - pub first_normal_slot: u64, + first_normal_slot: [u8; 8], } impl Default for EpochSchedule { @@ -87,6 +87,30 @@ impl Default for EpochSchedule { } impl EpochSchedule { + pub fn slots_per_epoch(&self) -> u64 { + u64::from_le_bytes(self.slots_per_epoch) + } + + pub fn leader_schedule_slot_offset(&self) -> u64 { + u64::from_le_bytes(self.leader_schedule_slot_offset) + } + + pub fn warmup(&self) -> bool { + match self.warmup { + 0 => false, + 1 => true, + _ => panic!("invalid warmup value"), + } + } + + pub fn first_normal_epoch(&self) -> u64 { + u64::from_le_bytes(self.first_normal_epoch) + } + + pub fn first_normal_slot(&self) -> u64 { + u64::from_le_bytes(self.first_normal_slot) + } + pub fn new(slots_per_epoch: u64) -> Self { Self::custom(slots_per_epoch, slots_per_epoch, true) } @@ -113,40 +137,40 @@ impl EpochSchedule { (0, 0) }; EpochSchedule { - slots_per_epoch, - leader_schedule_slot_offset, - warmup, - first_normal_epoch, - first_normal_slot, + slots_per_epoch: slots_per_epoch.to_le_bytes(), + leader_schedule_slot_offset: leader_schedule_slot_offset.to_le_bytes(), + warmup: warmup as u8, + first_normal_epoch: first_normal_epoch.to_le_bytes(), + first_normal_slot: first_normal_slot.to_le_bytes(), } } /// get the length of the given epoch (in slots) pub fn get_slots_in_epoch(&self, epoch: u64) -> u64 { - if epoch < self.first_normal_epoch { + if epoch < self.first_normal_epoch() { 2u64.saturating_pow( (epoch as u32).saturating_add(MINIMUM_SLOTS_PER_EPOCH.trailing_zeros()), ) } else { - self.slots_per_epoch + self.slots_per_epoch() } } /// get the epoch for which the given slot should save off /// information about stakers pub fn get_leader_schedule_epoch(&self, slot: u64) -> u64 { - if slot < self.first_normal_slot { + if slot < self.first_normal_slot() { // until we get to normal slots, behave as if leader_schedule_slot_offset == slots_per_epoch self.get_epoch_and_slot_index(slot).0.saturating_add(1) } else { - let new_slots_since_first_normal_slot = slot.saturating_sub(self.first_normal_slot); - let new_first_normal_leader_schedule_slot = - new_slots_since_first_normal_slot.saturating_add(self.leader_schedule_slot_offset); + let new_slots_since_first_normal_slot = slot.saturating_sub(self.first_normal_slot()); + let new_first_normal_leader_schedule_slot = new_slots_since_first_normal_slot + .saturating_add(self.leader_schedule_slot_offset()); let new_epochs_since_first_normal_leader_schedule = new_first_normal_leader_schedule_slot - .checked_div(self.slots_per_epoch) + .checked_div(self.slots_per_epoch()) .unwrap_or(0); - self.first_normal_epoch + self.first_normal_epoch() .saturating_add(new_epochs_since_first_normal_leader_schedule) } } @@ -158,7 +182,7 @@ impl EpochSchedule { /// get epoch and offset into the epoch for the given slot pub fn get_epoch_and_slot_index(&self, slot: u64) -> (u64, u64) { - if slot < self.first_normal_slot { + if slot < self.first_normal_slot() { let epoch = slot .saturating_add(MINIMUM_SLOTS_PER_EPOCH) .saturating_add(1) @@ -175,28 +199,28 @@ impl EpochSchedule { slot.saturating_sub(epoch_len.saturating_sub(MINIMUM_SLOTS_PER_EPOCH)), ) } else { - let normal_slot_index = slot.saturating_sub(self.first_normal_slot); + let normal_slot_index = slot.saturating_sub(self.first_normal_slot()); let normal_epoch_index = normal_slot_index - .checked_div(self.slots_per_epoch) + .checked_div(self.slots_per_epoch()) .unwrap_or(0); - let epoch = self.first_normal_epoch.saturating_add(normal_epoch_index); + let epoch = self.first_normal_epoch().saturating_add(normal_epoch_index); let slot_index = normal_slot_index - .checked_rem(self.slots_per_epoch) + .checked_rem(self.slots_per_epoch()) .unwrap_or(0); (epoch, slot_index) } } pub fn get_first_slot_in_epoch(&self, epoch: u64) -> u64 { - if epoch <= self.first_normal_epoch { + if epoch <= self.first_normal_epoch() { 2u64.saturating_pow(epoch as u32) .saturating_sub(1) .saturating_mul(MINIMUM_SLOTS_PER_EPOCH) } else { epoch - .saturating_sub(self.first_normal_epoch) - .saturating_mul(self.slots_per_epoch) - .saturating_add(self.first_normal_slot) + .saturating_sub(self.first_normal_epoch()) + .saturating_mul(self.slots_per_epoch()) + .saturating_add(self.first_normal_slot()) } } @@ -271,13 +295,8 @@ mod tests { #[test] fn test_clone() { - let epoch_schedule = EpochSchedule { - slots_per_epoch: 1, - leader_schedule_slot_offset: 2, - warmup: true, - first_normal_epoch: 4, - first_normal_slot: 5, - }; + let epoch_schedule = + EpochSchedule::custom(MINIMUM_SLOTS_PER_EPOCH, MINIMUM_SLOTS_PER_EPOCH, true); #[allow(clippy::clone_on_copy)] let cloned_epoch_schedule = epoch_schedule.clone(); assert_eq!(cloned_epoch_schedule, epoch_schedule); diff --git a/genesis-config/src/lib.rs b/genesis-config/src/lib.rs index ce5d60806..bae448202 100644 --- a/genesis-config/src/lib.rs +++ b/genesis-config/src/lib.rs @@ -49,7 +49,7 @@ pub const UNUSED_DEFAULT: u64 = 1024; #[cfg_attr( feature = "frozen-abi", derive(AbiExample), - frozen_abi(digest = "3tUUJkZiUUGfuNCXbDuDR6KCQYPsh3m3cPw5vVUSt113") + frozen_abi(digest = "CR76YX4Y3eHSCoRzkBEaJv7p3vAShUT56fX8WERoLBAp") )] #[cfg_attr( feature = "serde", @@ -244,8 +244,8 @@ impl fmt::Display for GenesisConfig { self.ticks_per_slot, self.poh_config.hashes_per_tick, self.poh_config.target_tick_duration, - self.epoch_schedule.slots_per_epoch, - if self.epoch_schedule.warmup { + self.epoch_schedule.slots_per_epoch(), + if self.epoch_schedule.warmup() { "en" } else { "dis" diff --git a/rent/src/lib.rs b/rent/src/lib.rs index 8a70d2b99..43eb5c56b 100644 --- a/rent/src/lib.rs +++ b/rent/src/lib.rs @@ -32,17 +32,17 @@ static_assertions::const_assert_eq!( #[derive(PartialEq, CloneZeroed, Debug)] pub struct Rent { /// Rental rate in lamports/byte-year. - pub lamports_per_byte_year: u64, + lamports_per_byte_year: [u8; 8], /// Amount of time (in years) a balance must include rent for the account to /// be rent exempt. - pub exemption_threshold: f64, + exemption_threshold: [u8; 8], /// The percentage of collected rent that is burned. /// /// Valid values are in the range [0, 100]. The remaining percentage is /// distributed to validators. - pub burn_percent: u8, + burn_percent: u8, } /// Default rental rate in lamports/byte-year. @@ -73,14 +73,43 @@ pub const ACCOUNT_STORAGE_OVERHEAD: u64 = 128; impl Default for Rent { fn default() -> Self { Self { - lamports_per_byte_year: DEFAULT_LAMPORTS_PER_BYTE_YEAR, - exemption_threshold: DEFAULT_EXEMPTION_THRESHOLD, + lamports_per_byte_year: DEFAULT_LAMPORTS_PER_BYTE_YEAR.to_le_bytes(), + exemption_threshold: DEFAULT_EXEMPTION_THRESHOLD.to_le_bytes(), burn_percent: DEFAULT_BURN_PERCENT, } } } impl Rent { + pub fn lamports_per_byte_year(&self) -> u64 { + u64::from_le_bytes(self.lamports_per_byte_year) + } + + pub fn exemption_threshold(&self) -> f64 { + f64::from_le_bytes(self.exemption_threshold) + } + + pub fn burn_percent(&self) -> u8 { + self.burn_percent + } + + /// Creates a new `Rent` with the given parameters. + /// + /// # Panics + /// + /// Panics if `burn_percent` is not in the range [0, 100]. + pub fn new(lamports_per_byte_year: u64, exemption_threshold: f64, burn_percent: u8) -> Self { + assert!( + burn_percent <= 100, + "burn_percent must be in range [0, 100]" + ); + Self { + lamports_per_byte_year: lamports_per_byte_year.to_le_bytes(), + exemption_threshold: exemption_threshold.to_le_bytes(), + burn_percent, + } + } + /// Calculate how much rent to burn from the collected rent. /// /// The first value returned is the amount burned. The second is the amount @@ -93,8 +122,8 @@ impl Rent { /// Minimum balance due for rent-exemption of a given account data size. pub fn minimum_balance(&self, data_len: usize) -> u64 { let bytes = data_len as u64; - (((ACCOUNT_STORAGE_OVERHEAD + bytes) * self.lamports_per_byte_year) as f64 - * self.exemption_threshold) as u64 + (((ACCOUNT_STORAGE_OVERHEAD + bytes) * self.lamports_per_byte_year()) as f64 + * self.exemption_threshold()) as u64 } /// Whether a given balance and data length would be exempt. @@ -114,7 +143,7 @@ impl Rent { /// Rent due for account that is known to be not exempt. pub fn due_amount(&self, data_len: usize, years_elapsed: f64) -> u64 { let actual_data_len = data_len as u64 + ACCOUNT_STORAGE_OVERHEAD; - let lamports_per_year = self.lamports_per_byte_year * actual_data_len; + let lamports_per_year = self.lamports_per_byte_year() * actual_data_len; (lamports_per_year as f64 * years_elapsed) as u64 } @@ -123,7 +152,7 @@ impl Rent { /// This is used for testing. pub fn free() -> Self { Self { - lamports_per_byte_year: 0, + lamports_per_byte_year: 0u64.to_le_bytes(), ..Rent::default() } } @@ -135,11 +164,11 @@ impl Rent { let ratio = slots_per_epoch as f64 / DEFAULT_SLOTS_PER_EPOCH as f64; let exemption_threshold = DEFAULT_EXEMPTION_THRESHOLD * ratio; let lamports_per_byte_year = (DEFAULT_LAMPORTS_PER_BYTE_YEAR as f64 / ratio) as u64; - Self { + Self::new( lamports_per_byte_year, exemption_threshold, - ..Self::default() - } + DEFAULT_BURN_PERCENT, + ) } } @@ -195,24 +224,20 @@ mod tests { RentDue::Exempt, ); - let custom_rent = Rent { - lamports_per_byte_year: 5, - exemption_threshold: 2.5, - ..Rent::default() - }; + let custom_rent = Rent::new(5, 2.5, DEFAULT_BURN_PERCENT); assert_eq!( custom_rent.due(0, 2, 1.2), RentDue::Paying( - (((2 + ACCOUNT_STORAGE_OVERHEAD) * custom_rent.lamports_per_byte_year) as f64 * 1.2) - as u64, + (((2 + ACCOUNT_STORAGE_OVERHEAD) * custom_rent.lamports_per_byte_year()) as f64 + * 1.2) as u64, ) ); assert_eq!( custom_rent.due( - (((2 + ACCOUNT_STORAGE_OVERHEAD) * custom_rent.lamports_per_byte_year) as f64 - * custom_rent.exemption_threshold) as u64, + (((2 + ACCOUNT_STORAGE_OVERHEAD) * custom_rent.lamports_per_byte_year()) as f64 + * custom_rent.exemption_threshold()) as u64, 2, 1.2 ), @@ -236,11 +261,7 @@ mod tests { #[test] fn test_clone() { - let rent = Rent { - lamports_per_byte_year: 1, - exemption_threshold: 2.2, - burn_percent: 3, - }; + let rent = Rent::new(1, 2.2, 3); #[allow(clippy::clone_on_copy)] let cloned_rent = rent.clone(); assert_eq!(cloned_rent, rent); diff --git a/sysvar/src/epoch_rewards.rs b/sysvar/src/epoch_rewards.rs index 14bcc450c..9d55e6de9 100755 --- a/sysvar/src/epoch_rewards.rs +++ b/sysvar/src/epoch_rewards.rs @@ -49,13 +49,7 @@ //! # use solana_sysvar_id::SysvarId; //! # let p = EpochRewards::id(); //! # let l = &mut 1559040; -//! # let epoch_rewards = EpochRewards { -//! # distribution_starting_block_height: 42, -//! # total_rewards: 100, -//! # distributed_rewards: 10, -//! # active: true, -//! # ..EpochRewards::default() -//! # }; +//! # let epoch_rewards = EpochRewards::new(100, 10, 42); //! # let mut d: Vec = bincode::serialize(&epoch_rewards).unwrap(); //! # let a = AccountInfo::new(&p, false, false, l, &mut d, &p, false); //! # let accounts = &[a.clone(), a]; @@ -97,13 +91,7 @@ //! # use solana_sysvar_id::SysvarId; //! # let p = EpochRewards::id(); //! # let l = &mut 1559040; -//! # let epoch_rewards = EpochRewards { -//! # distribution_starting_block_height: 42, -//! # total_rewards: 100, -//! # distributed_rewards: 10, -//! # active: true, -//! # ..EpochRewards::default() -//! # }; +//! # let epoch_rewards = EpochRewards::new(100, 10, 42); //! # let mut d: Vec = bincode::serialize(&epoch_rewards).unwrap(); //! # let a = AccountInfo::new(&p, false, false, l, &mut d, &p, false); //! # let accounts = &[a.clone(), a]; @@ -127,13 +115,7 @@ //! # use anyhow::Result; //! # //! fn print_sysvar_epoch_rewards(client: &RpcClient) -> Result<()> { -//! # let epoch_rewards = EpochRewards { -//! # distribution_starting_block_height: 42, -//! # total_rewards: 100, -//! # distributed_rewards: 10, -//! # active: true, -//! # ..EpochRewards::default() -//! # }; +//! # let epoch_rewards = EpochRewards::new(100, 10, 42); //! # let data: Vec = bincode::serialize(&epoch_rewards)?; //! # client.set_get_account_response(epoch_rewards::ID, Account { //! # lamports: 1120560, @@ -156,55 +138,14 @@ #[cfg(feature = "bincode")] use crate::SysvarSerialize; -use crate::{get_sysvar_via_packed, Sysvar}; +use crate::{impl_sysvar_get, Sysvar}; pub use { solana_epoch_rewards::EpochRewards, solana_sdk_ids::sysvar::epoch_rewards::{check_id, id, ID}, }; -#[repr(C, packed)] -#[derive(Clone, Copy)] -struct EpochRewardsPacked { - distribution_starting_block_height: u64, - num_partitions: u64, - parent_blockhash: [u8; 32], - total_points: u128, - total_rewards: u64, - distributed_rewards: u64, - active: u8, // bool as u8 -} - -const _: () = assert!(core::mem::size_of::() == 81); - -impl From for EpochRewards { - fn from(p: EpochRewardsPacked) -> Self { - // Ensure field parity at compile time - let EpochRewardsPacked { - distribution_starting_block_height, - num_partitions, - parent_blockhash, - total_points, - total_rewards, - distributed_rewards, - active, - } = p; - - Self { - distribution_starting_block_height, - num_partitions, - parent_blockhash: solana_hash::Hash::new_from_array(parent_blockhash), - total_points, - total_rewards, - distributed_rewards, - active: active != 0, - } - } -} - impl Sysvar for EpochRewards { - fn get() -> Result { - get_sysvar_via_packed::(&id()) - } + impl_sysvar_get!(id()); } #[cfg(feature = "bincode")] @@ -218,15 +159,7 @@ mod tests { #[serial] #[cfg(feature = "bincode")] fn test_epoch_rewards_get() { - let expected = EpochRewards { - distribution_starting_block_height: 42, - num_partitions: 7, - parent_blockhash: solana_hash::Hash::new_unique(), - total_points: 1234567890, - total_rewards: 100, - distributed_rewards: 10, - active: true, - }; + let expected = EpochRewards::new(100, 10, 42); let data = bincode::serialize(&expected).unwrap(); assert_eq!(data.len(), 81); diff --git a/sysvar/src/epoch_schedule.rs b/sysvar/src/epoch_schedule.rs index 637911d8a..cdd2b2e0a 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -121,49 +121,14 @@ //! ``` #[cfg(feature = "bincode")] use crate::SysvarSerialize; -use crate::{get_sysvar_via_packed, Sysvar}; +use crate::{impl_sysvar_get, Sysvar}; pub use { solana_epoch_schedule::EpochSchedule, solana_sdk_ids::sysvar::epoch_schedule::{check_id, id, ID}, }; -#[repr(C, packed)] -#[derive(Clone, Copy)] -struct EpochSchedulePacked { - slots_per_epoch: u64, - leader_schedule_slot_offset: u64, - warmup: u8, // bool as u8 - first_normal_epoch: u64, - first_normal_slot: u64, -} - -const _: () = assert!(core::mem::size_of::() == 33); - -impl From for EpochSchedule { - fn from(p: EpochSchedulePacked) -> Self { - // Ensure field parity at compile time - let EpochSchedulePacked { - slots_per_epoch, - leader_schedule_slot_offset, - warmup, - first_normal_epoch, - first_normal_slot, - } = p; - - Self { - slots_per_epoch, - leader_schedule_slot_offset, - warmup: warmup != 0, - first_normal_epoch, - first_normal_slot, - } - } -} - impl Sysvar for EpochSchedule { - fn get() -> Result { - get_sysvar_via_packed::(&id()) - } + impl_sysvar_get!(id()); } #[cfg(feature = "bincode")] diff --git a/sysvar/src/lib.rs b/sysvar/src/lib.rs index 7c9e40605..abb3cd77b 100644 --- a/sysvar/src/lib.rs +++ b/sysvar/src/lib.rs @@ -199,39 +199,6 @@ macro_rules! impl_sysvar_get { }; } -/// Generic helper to load a sysvar via a packed representation. -/// -/// 1. Allocates uninitialized memory for the packed struct -/// 2. Loads sysvar bytes directly into it via `get_sysvar_unchecked` -/// 3. Converts the packed struct to the canonical type via `From` -/// -/// # Type Parameters -/// -/// - `T`: The canonical sysvar type -/// - `P`: The packed struct (must be `Copy` and `From

for T` must exist) -/// -/// # Safety -/// -/// The packed struct `P` should be `#[repr(C, packed)]` to match the runtime's -/// compact serialization format (no padding). -pub fn get_sysvar_via_packed(sysvar_id: &Pubkey) -> Result -where - P: Copy, - T: From

, -{ - let mut packed = core::mem::MaybeUninit::

::uninit(); - let size = core::mem::size_of::

(); - unsafe { - get_sysvar_unchecked( - packed.as_mut_ptr() as *mut u8, - sysvar_id as *const _ as *const u8, - 0, - size as u64, - )?; - Ok(T::from(packed.assume_init())) - } -} - /// Handler for retrieving a slice of sysvar data from the `sol_get_sysvar` /// syscall. pub fn get_sysvar( diff --git a/sysvar/src/rent.rs b/sysvar/src/rent.rs index 2bbbe5eac..fb816a689 100644 --- a/sysvar/src/rent.rs +++ b/sysvar/src/rent.rs @@ -123,43 +123,14 @@ //! ``` #[cfg(feature = "bincode")] use crate::SysvarSerialize; -use crate::{get_sysvar_via_packed, Sysvar}; +use crate::{impl_sysvar_get, Sysvar}; pub use { solana_rent::Rent, solana_sdk_ids::sysvar::rent::{check_id, id, ID}, }; -#[repr(C, packed)] -#[derive(Clone, Copy)] -struct RentPacked { - lamports_per_byte_year: u64, - exemption_threshold: [u8; 8], // f64 as little-endian bytes - burn_percent: u8, -} - -const _: () = assert!(core::mem::size_of::() == 17); - -impl From for Rent { - fn from(p: RentPacked) -> Self { - // Ensure field parity at compile time - let RentPacked { - lamports_per_byte_year, - exemption_threshold, - burn_percent, - } = p; - - Self { - lamports_per_byte_year, - exemption_threshold: f64::from_le_bytes(exemption_threshold), - burn_percent, - } - } -} - impl Sysvar for Rent { - fn get() -> Result { - get_sysvar_via_packed::(&id()) - } + impl_sysvar_get!(id()); } #[cfg(feature = "bincode")] @@ -173,12 +144,7 @@ mod tests { #[serial] #[cfg(feature = "bincode")] fn test_rent_get() { - let expected = Rent { - lamports_per_byte_year: 123, - exemption_threshold: 2.5, - burn_percent: 7, - }; - + let expected = Rent::new(123, 2.5, 7); let data = bincode::serialize(&expected).unwrap(); assert_eq!(data.len(), 17); From ff3c1f95016cd6f51ec86a6de4804c5016cf1044 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:29:24 +0000 Subject: [PATCH 09/26] rm outdated comment lines --- sysvar/src/clock.rs | 3 +-- sysvar/src/last_restart_slot.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/sysvar/src/clock.rs b/sysvar/src/clock.rs index 259a47533..dadb6335f 100644 --- a/sysvar/src/clock.rs +++ b/sysvar/src/clock.rs @@ -143,8 +143,7 @@ mod tests { #[test] #[cfg(feature = "bincode")] fn test_clock_size_matches_bincode() { - // Prove that Clock's in-memory layout matches its bincode serialization, - // so we do not need to use sysvar_packed_struct. + // Prove that Clock's in-memory layout matches its bincode serialization. let clock = Clock::default(); let in_memory_size = core::mem::size_of::(); let bincode_size = bincode::serialized_size(&clock).unwrap() as usize; diff --git a/sysvar/src/last_restart_slot.rs b/sysvar/src/last_restart_slot.rs index 382e70e8f..5689ddfb5 100644 --- a/sysvar/src/last_restart_slot.rs +++ b/sysvar/src/last_restart_slot.rs @@ -58,8 +58,7 @@ mod tests { #[test] #[cfg(feature = "bincode")] fn test_last_restart_slot_size_matches_bincode() { - // Prove that LastRestartSlot's in-memory layout matches its bincode serialization, - // so we do not need to use sysvar_packed_struct. + // Prove that LastRestartSlot's in-memory layout matches its bincode serialization. let slot = LastRestartSlot::default(); let in_memory_size = core::mem::size_of::(); let bincode_size = bincode::serialized_size(&slot).unwrap() as usize; From 7ef14b7b184b0f0e46a09862d6881734aa239757 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:00:21 +0000 Subject: [PATCH 10/26] custom rent, epoch sysvar pods --- define-syscall/src/definitions.rs | 10 +-- epoch-rewards/src/lib.rs | 62 ++++----------- epoch-schedule/src/lib.rs | 89 +++++++++------------- genesis-config/src/lib.rs | 6 +- rent/src/lib.rs | 42 ++++------- sysvar/src/epoch_rewards.rs | 121 +++++++++++++++++++++++++++++- sysvar/src/epoch_schedule.rs | 88 +++++++++++++++++++++- sysvar/src/rent.rs | 19 ++++- 8 files changed, 294 insertions(+), 143 deletions(-) diff --git a/define-syscall/src/definitions.rs b/define-syscall/src/definitions.rs index 651634b51..da614c6e0 100644 --- a/define-syscall/src/definitions.rs +++ b/define-syscall/src/definitions.rs @@ -50,11 +50,11 @@ define_syscall!(fn sol_get_return_data(data: *mut u8, length: u64, program_id: * define_syscall!(fn sol_get_processed_sibling_instruction(index: u64, meta: *mut u8, program_id: *mut u8, data: *mut u8, accounts: *mut u8) -> u64); // these are deprecated - use sol_get_sysvar instead -define_syscall!(#[deprecated(since = "3.0.0", note = "Use `sol_get_sysvar` with `Clock` sysvar address instead")] fn sol_get_clock_sysvar(addr: *mut u8) -> u64); -define_syscall!(#[deprecated(since = "3.0.0", note = "Use `sol_get_sysvar` with `EpochSchedule` sysvar address instead")] fn sol_get_epoch_schedule_sysvar(addr: *mut u8) -> u64); -define_syscall!(#[deprecated(since = "3.0.0", note = "Use `sol_get_sysvar` with `Rent` sysvar address instead")] fn sol_get_rent_sysvar(addr: *mut u8) -> u64); -define_syscall!(#[deprecated(since = "3.0.0", note = "Use `sol_get_sysvar` with `LastRestartSlot` sysvar address instead")] fn sol_get_last_restart_slot(addr: *mut u8) -> u64); -define_syscall!(#[deprecated(since = "3.0.0", note = "Use `sol_get_sysvar` with `EpochRewards` sysvar address instead")] fn sol_get_epoch_rewards_sysvar(addr: *mut u8) -> u64); +define_syscall!(#[deprecated(since = "4.1.0", note = "Use `sol_get_sysvar` with `Clock` sysvar address instead")] fn sol_get_clock_sysvar(addr: *mut u8) -> u64); +define_syscall!(#[deprecated(since = "4.1.0", note = "Use `sol_get_sysvar` with `EpochSchedule` sysvar address instead")] fn sol_get_epoch_schedule_sysvar(addr: *mut u8) -> u64); +define_syscall!(#[deprecated(since = "4.1.0", note = "Use `sol_get_sysvar` with `Rent` sysvar address instead")] fn sol_get_rent_sysvar(addr: *mut u8) -> u64); +define_syscall!(#[deprecated(since = "4.1.0", note = "Use `sol_get_sysvar` with `LastRestartSlot` sysvar address instead")] fn sol_get_last_restart_slot(addr: *mut u8) -> u64); +define_syscall!(#[deprecated(since = "4.1.0", note = "Use `sol_get_sysvar` with `EpochRewards` sysvar address instead")] fn sol_get_epoch_rewards_sysvar(addr: *mut u8) -> u64); // this cannot go through sol_get_sysvar but can be removed once no longer in use define_syscall!(fn sol_get_fees_sysvar(addr: *mut u8) -> u64); diff --git a/epoch-rewards/src/lib.rs b/epoch-rewards/src/lib.rs index 749654e6b..609ab8002 100644 --- a/epoch-rewards/src/lib.rs +++ b/epoch-rewards/src/lib.rs @@ -19,18 +19,18 @@ extern crate std; use serde_derive::{Deserialize, Serialize}; use {solana_hash::Hash, solana_sdk_macro::CloneZeroed}; -#[repr(C)] +#[repr(C, align(16))] #[cfg_attr(feature = "frozen-abi", derive(solana_frozen_abi_macro::AbiExample))] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] #[derive(Debug, PartialEq, Eq, Default, CloneZeroed)] pub struct EpochRewards { /// The starting block height of the rewards distribution in the current /// epoch - distribution_starting_block_height: [u8; 8], + pub distribution_starting_block_height: u64, /// Number of partitions in the rewards distribution in the current epoch, /// used to generate an EpochRewardsHasher - num_partitions: [u8; 8], + pub num_partitions: u64, /// The blockhash of the parent block of the first block in the epoch, used /// to seed an EpochRewardsHasher @@ -39,71 +39,39 @@ pub struct EpochRewards { /// The total rewards points calculated for the current epoch, where points /// equals the sum of (delegated stake * credits observed) for all /// delegations - total_points: [u8; 16], + pub total_points: u128, /// The total rewards calculated for the current epoch. This may be greater /// than the total `distributed_rewards` at the end of the rewards period, /// due to rounding and inability to deliver rewards smaller than 1 lamport. - total_rewards: [u8; 8], + pub total_rewards: u64, /// The rewards currently distributed for the current epoch, in lamports - distributed_rewards: [u8; 8], + pub distributed_rewards: u64, /// Whether the rewards period (including calculation and distribution) is /// active - pub active: u8, + pub active: bool, } impl EpochRewards { - pub fn distribution_starting_block_height(&self) -> u64 { - u64::from_le_bytes(self.distribution_starting_block_height) - } - - pub fn num_partitions(&self) -> u64 { - u64::from_le_bytes(self.num_partitions) - } - - pub fn parent_blockhash(&self) -> &Hash { - &self.parent_blockhash - } - - pub fn total_points(&self) -> u128 { - u128::from_le_bytes(self.total_points) - } - - pub fn total_rewards(&self) -> u64 { - u64::from_le_bytes(self.total_rewards) - } - - pub fn distributed_rewards(&self) -> u64 { - u64::from_le_bytes(self.distributed_rewards) - } - - pub fn active(&self) -> bool { - match self.active { - 0 => false, - 1 => true, - _ => panic!("invalid active value"), - } - } - pub fn new( total_rewards: u64, distributed_rewards: u64, distribution_starting_block_height: u64, ) -> Self { Self { - distribution_starting_block_height: distribution_starting_block_height.to_le_bytes(), - total_rewards: total_rewards.to_le_bytes(), - distributed_rewards: distributed_rewards.to_le_bytes(), + distribution_starting_block_height, + total_rewards, + distributed_rewards, ..Self::default() } } pub fn distribute(&mut self, amount: u64) { - let new_distributed_rewards = self.distributed_rewards().saturating_add(amount); - assert!(new_distributed_rewards <= self.total_rewards()); - self.distributed_rewards = new_distributed_rewards.to_le_bytes(); + let new_distributed_rewards = self.distributed_rewards.saturating_add(amount); + assert!(new_distributed_rewards <= self.total_rewards); + self.distributed_rewards = new_distributed_rewards; } } @@ -116,8 +84,8 @@ mod tests { let mut epoch_rewards = EpochRewards::new(100, 0, 64); epoch_rewards.distribute(100); - assert_eq!(epoch_rewards.total_rewards(), 100); - assert_eq!(epoch_rewards.distributed_rewards(), 100); + assert_eq!(epoch_rewards.total_rewards, 100); + assert_eq!(epoch_rewards.distributed_rewards, 100); } #[test] diff --git a/epoch-schedule/src/lib.rs b/epoch-schedule/src/lib.rs index dfc7ba1a0..e5b8f323a 100644 --- a/epoch-schedule/src/lib.rs +++ b/epoch-schedule/src/lib.rs @@ -56,24 +56,24 @@ pub const MINIMUM_SLOTS_PER_EPOCH: u64 = 32; #[derive(Debug, CloneZeroed, PartialEq, Eq)] pub struct EpochSchedule { /// The maximum number of slots in each epoch. - slots_per_epoch: [u8; 8], + pub slots_per_epoch: u64, /// A number of slots before beginning of an epoch to calculate /// a leader schedule for that epoch. - leader_schedule_slot_offset: [u8; 8], + pub leader_schedule_slot_offset: u64, /// Whether epochs start short and grow. - pub warmup: u8, + pub warmup: bool, /// The first epoch after the warmup period. /// /// Basically: `log2(slots_per_epoch) - log2(MINIMUM_SLOTS_PER_EPOCH)`. - first_normal_epoch: [u8; 8], + pub first_normal_epoch: u64, /// The first slot after the warmup period. /// /// Basically: `MINIMUM_SLOTS_PER_EPOCH * (2.pow(first_normal_epoch) - 1)`. - first_normal_slot: [u8; 8], + pub first_normal_slot: u64, } impl Default for EpochSchedule { @@ -87,33 +87,10 @@ impl Default for EpochSchedule { } impl EpochSchedule { - pub fn slots_per_epoch(&self) -> u64 { - u64::from_le_bytes(self.slots_per_epoch) - } - - pub fn leader_schedule_slot_offset(&self) -> u64 { - u64::from_le_bytes(self.leader_schedule_slot_offset) - } - - pub fn warmup(&self) -> bool { - match self.warmup { - 0 => false, - 1 => true, - _ => panic!("invalid warmup value"), - } - } - - pub fn first_normal_epoch(&self) -> u64 { - u64::from_le_bytes(self.first_normal_epoch) - } - - pub fn first_normal_slot(&self) -> u64 { - u64::from_le_bytes(self.first_normal_slot) - } - pub fn new(slots_per_epoch: u64) -> Self { Self::custom(slots_per_epoch, slots_per_epoch, true) } + pub fn without_warmup() -> Self { Self::custom( DEFAULT_SLOTS_PER_EPOCH, @@ -121,6 +98,7 @@ impl EpochSchedule { false, ) } + pub fn custom(slots_per_epoch: u64, leader_schedule_slot_offset: u64, warmup: bool) -> Self { assert!(slots_per_epoch >= MINIMUM_SLOTS_PER_EPOCH); let (first_normal_epoch, first_normal_slot) = if warmup { @@ -137,40 +115,40 @@ impl EpochSchedule { (0, 0) }; EpochSchedule { - slots_per_epoch: slots_per_epoch.to_le_bytes(), - leader_schedule_slot_offset: leader_schedule_slot_offset.to_le_bytes(), - warmup: warmup as u8, - first_normal_epoch: first_normal_epoch.to_le_bytes(), - first_normal_slot: first_normal_slot.to_le_bytes(), + slots_per_epoch, + leader_schedule_slot_offset, + warmup, + first_normal_epoch, + first_normal_slot, } } /// get the length of the given epoch (in slots) pub fn get_slots_in_epoch(&self, epoch: u64) -> u64 { - if epoch < self.first_normal_epoch() { + if epoch < self.first_normal_epoch { 2u64.saturating_pow( (epoch as u32).saturating_add(MINIMUM_SLOTS_PER_EPOCH.trailing_zeros()), ) } else { - self.slots_per_epoch() + self.slots_per_epoch } } /// get the epoch for which the given slot should save off /// information about stakers pub fn get_leader_schedule_epoch(&self, slot: u64) -> u64 { - if slot < self.first_normal_slot() { + if slot < self.first_normal_slot { // until we get to normal slots, behave as if leader_schedule_slot_offset == slots_per_epoch self.get_epoch_and_slot_index(slot).0.saturating_add(1) } else { - let new_slots_since_first_normal_slot = slot.saturating_sub(self.first_normal_slot()); - let new_first_normal_leader_schedule_slot = new_slots_since_first_normal_slot - .saturating_add(self.leader_schedule_slot_offset()); + let new_slots_since_first_normal_slot = slot.saturating_sub(self.first_normal_slot); + let new_first_normal_leader_schedule_slot = + new_slots_since_first_normal_slot.saturating_add(self.leader_schedule_slot_offset); let new_epochs_since_first_normal_leader_schedule = new_first_normal_leader_schedule_slot - .checked_div(self.slots_per_epoch()) + .checked_div(self.slots_per_epoch) .unwrap_or(0); - self.first_normal_epoch() + self.first_normal_epoch .saturating_add(new_epochs_since_first_normal_leader_schedule) } } @@ -182,7 +160,7 @@ impl EpochSchedule { /// get epoch and offset into the epoch for the given slot pub fn get_epoch_and_slot_index(&self, slot: u64) -> (u64, u64) { - if slot < self.first_normal_slot() { + if slot < self.first_normal_slot { let epoch = slot .saturating_add(MINIMUM_SLOTS_PER_EPOCH) .saturating_add(1) @@ -199,28 +177,28 @@ impl EpochSchedule { slot.saturating_sub(epoch_len.saturating_sub(MINIMUM_SLOTS_PER_EPOCH)), ) } else { - let normal_slot_index = slot.saturating_sub(self.first_normal_slot()); + let normal_slot_index = slot.saturating_sub(self.first_normal_slot); let normal_epoch_index = normal_slot_index - .checked_div(self.slots_per_epoch()) + .checked_div(self.slots_per_epoch) .unwrap_or(0); - let epoch = self.first_normal_epoch().saturating_add(normal_epoch_index); + let epoch = self.first_normal_epoch.saturating_add(normal_epoch_index); let slot_index = normal_slot_index - .checked_rem(self.slots_per_epoch()) + .checked_rem(self.slots_per_epoch) .unwrap_or(0); (epoch, slot_index) } } pub fn get_first_slot_in_epoch(&self, epoch: u64) -> u64 { - if epoch <= self.first_normal_epoch() { + if epoch <= self.first_normal_epoch { 2u64.saturating_pow(epoch as u32) .saturating_sub(1) .saturating_mul(MINIMUM_SLOTS_PER_EPOCH) } else { epoch - .saturating_sub(self.first_normal_epoch()) - .saturating_mul(self.slots_per_epoch()) - .saturating_add(self.first_normal_slot()) + .saturating_sub(self.first_normal_epoch) + .saturating_mul(self.slots_per_epoch) + .saturating_add(self.first_normal_slot) } } @@ -295,8 +273,13 @@ mod tests { #[test] fn test_clone() { - let epoch_schedule = - EpochSchedule::custom(MINIMUM_SLOTS_PER_EPOCH, MINIMUM_SLOTS_PER_EPOCH, true); + let epoch_schedule = EpochSchedule { + slots_per_epoch: 1, + leader_schedule_slot_offset: 2, + warmup: true, + first_normal_epoch: 4, + first_normal_slot: 5, + }; #[allow(clippy::clone_on_copy)] let cloned_epoch_schedule = epoch_schedule.clone(); assert_eq!(cloned_epoch_schedule, epoch_schedule); diff --git a/genesis-config/src/lib.rs b/genesis-config/src/lib.rs index bae448202..ce5d60806 100644 --- a/genesis-config/src/lib.rs +++ b/genesis-config/src/lib.rs @@ -49,7 +49,7 @@ pub const UNUSED_DEFAULT: u64 = 1024; #[cfg_attr( feature = "frozen-abi", derive(AbiExample), - frozen_abi(digest = "CR76YX4Y3eHSCoRzkBEaJv7p3vAShUT56fX8WERoLBAp") + frozen_abi(digest = "3tUUJkZiUUGfuNCXbDuDR6KCQYPsh3m3cPw5vVUSt113") )] #[cfg_attr( feature = "serde", @@ -244,8 +244,8 @@ impl fmt::Display for GenesisConfig { self.ticks_per_slot, self.poh_config.hashes_per_tick, self.poh_config.target_tick_duration, - self.epoch_schedule.slots_per_epoch(), - if self.epoch_schedule.warmup() { + self.epoch_schedule.slots_per_epoch, + if self.epoch_schedule.warmup { "en" } else { "dis" diff --git a/rent/src/lib.rs b/rent/src/lib.rs index 43eb5c56b..b0785009d 100644 --- a/rent/src/lib.rs +++ b/rent/src/lib.rs @@ -32,17 +32,17 @@ static_assertions::const_assert_eq!( #[derive(PartialEq, CloneZeroed, Debug)] pub struct Rent { /// Rental rate in lamports/byte-year. - lamports_per_byte_year: [u8; 8], + pub lamports_per_byte_year: u64, /// Amount of time (in years) a balance must include rent for the account to /// be rent exempt. - exemption_threshold: [u8; 8], + pub exemption_threshold: f64, /// The percentage of collected rent that is burned. /// /// Valid values are in the range [0, 100]. The remaining percentage is /// distributed to validators. - burn_percent: u8, + pub burn_percent: u8, } /// Default rental rate in lamports/byte-year. @@ -73,26 +73,14 @@ pub const ACCOUNT_STORAGE_OVERHEAD: u64 = 128; impl Default for Rent { fn default() -> Self { Self { - lamports_per_byte_year: DEFAULT_LAMPORTS_PER_BYTE_YEAR.to_le_bytes(), - exemption_threshold: DEFAULT_EXEMPTION_THRESHOLD.to_le_bytes(), + lamports_per_byte_year: DEFAULT_LAMPORTS_PER_BYTE_YEAR, + exemption_threshold: DEFAULT_EXEMPTION_THRESHOLD, burn_percent: DEFAULT_BURN_PERCENT, } } } impl Rent { - pub fn lamports_per_byte_year(&self) -> u64 { - u64::from_le_bytes(self.lamports_per_byte_year) - } - - pub fn exemption_threshold(&self) -> f64 { - f64::from_le_bytes(self.exemption_threshold) - } - - pub fn burn_percent(&self) -> u8 { - self.burn_percent - } - /// Creates a new `Rent` with the given parameters. /// /// # Panics @@ -104,8 +92,8 @@ impl Rent { "burn_percent must be in range [0, 100]" ); Self { - lamports_per_byte_year: lamports_per_byte_year.to_le_bytes(), - exemption_threshold: exemption_threshold.to_le_bytes(), + lamports_per_byte_year, + exemption_threshold, burn_percent, } } @@ -122,8 +110,8 @@ impl Rent { /// Minimum balance due for rent-exemption of a given account data size. pub fn minimum_balance(&self, data_len: usize) -> u64 { let bytes = data_len as u64; - (((ACCOUNT_STORAGE_OVERHEAD + bytes) * self.lamports_per_byte_year()) as f64 - * self.exemption_threshold()) as u64 + (((ACCOUNT_STORAGE_OVERHEAD + bytes) * self.lamports_per_byte_year) as f64 + * self.exemption_threshold) as u64 } /// Whether a given balance and data length would be exempt. @@ -143,7 +131,7 @@ impl Rent { /// Rent due for account that is known to be not exempt. pub fn due_amount(&self, data_len: usize, years_elapsed: f64) -> u64 { let actual_data_len = data_len as u64 + ACCOUNT_STORAGE_OVERHEAD; - let lamports_per_year = self.lamports_per_byte_year() * actual_data_len; + let lamports_per_year = self.lamports_per_byte_year * actual_data_len; (lamports_per_year as f64 * years_elapsed) as u64 } @@ -152,7 +140,7 @@ impl Rent { /// This is used for testing. pub fn free() -> Self { Self { - lamports_per_byte_year: 0u64.to_le_bytes(), + lamports_per_byte_year: 0, ..Rent::default() } } @@ -229,15 +217,15 @@ mod tests { assert_eq!( custom_rent.due(0, 2, 1.2), RentDue::Paying( - (((2 + ACCOUNT_STORAGE_OVERHEAD) * custom_rent.lamports_per_byte_year()) as f64 - * 1.2) as u64, + (((2 + ACCOUNT_STORAGE_OVERHEAD) * custom_rent.lamports_per_byte_year) as f64 * 1.2) + as u64, ) ); assert_eq!( custom_rent.due( - (((2 + ACCOUNT_STORAGE_OVERHEAD) * custom_rent.lamports_per_byte_year()) as f64 - * custom_rent.exemption_threshold()) as u64, + (((2 + ACCOUNT_STORAGE_OVERHEAD) * custom_rent.lamports_per_byte_year) as f64 + * custom_rent.exemption_threshold) as u64, 2, 1.2 ), diff --git a/sysvar/src/epoch_rewards.rs b/sysvar/src/epoch_rewards.rs index 9d55e6de9..4d4324b42 100755 --- a/sysvar/src/epoch_rewards.rs +++ b/sysvar/src/epoch_rewards.rs @@ -136,16 +136,97 @@ //! # Ok::<(), anyhow::Error>(()) //! ``` +use crate::Sysvar; #[cfg(feature = "bincode")] use crate::SysvarSerialize; -use crate::{impl_sysvar_get, Sysvar}; pub use { solana_epoch_rewards::EpochRewards, solana_sdk_ids::sysvar::epoch_rewards::{check_id, id, ID}, }; +/// Pod (Plain Old Data) representation of [`EpochRewards`] with no padding. +/// +/// This type can be safely loaded via `sol_get_sysvar` without undefined behavior. +/// Provides performant zero-copy accessors as an alternative to the `EpochRewards` type. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PodEpochRewards { + distribution_starting_block_height: [u8; 8], + num_partitions: [u8; 8], + parent_blockhash: [u8; 32], + total_points: [u8; 16], + total_rewards: [u8; 8], + distributed_rewards: [u8; 8], + active: u8, +} + +const _: () = assert!(core::mem::size_of::() == 81); + +impl PodEpochRewards { + pub fn fetch() -> Result { + let mut pod = core::mem::MaybeUninit::::uninit(); + unsafe { + crate::get_sysvar_unchecked( + pod.as_mut_ptr() as *mut u8, + (&id()) as *const _ as *const u8, + 0, + 81, + )?; + Ok(pod.assume_init()) + } + } + + pub fn distribution_starting_block_height(&self) -> u64 { + u64::from_le_bytes(self.distribution_starting_block_height) + } + + pub fn num_partitions(&self) -> u64 { + u64::from_le_bytes(self.num_partitions) + } + + pub fn parent_blockhash(&self) -> solana_hash::Hash { + solana_hash::Hash::new_from_array(self.parent_blockhash) + } + + pub fn total_points(&self) -> u128 { + u128::from_le_bytes(self.total_points) + } + + pub fn total_rewards(&self) -> u64 { + u64::from_le_bytes(self.total_rewards) + } + + pub fn distributed_rewards(&self) -> u64 { + u64::from_le_bytes(self.distributed_rewards) + } + + pub fn active(&self) -> bool { + match self.active { + 0 => false, + 1 => true, + _ => panic!("invalid active value"), + } + } +} + +impl From for EpochRewards { + fn from(pod: PodEpochRewards) -> Self { + Self { + distribution_starting_block_height: pod.distribution_starting_block_height(), + num_partitions: pod.num_partitions(), + parent_blockhash: pod.parent_blockhash(), + total_points: pod.total_points(), + total_rewards: pod.total_rewards(), + distributed_rewards: pod.distributed_rewards(), + active: pod.active(), + } + } +} + impl Sysvar for EpochRewards { - impl_sysvar_get!(id()); + fn get() -> Result { + Ok(Self::from(PodEpochRewards::fetch()?)) + } } #[cfg(feature = "bincode")] @@ -155,11 +236,45 @@ impl SysvarSerialize for EpochRewards {} mod tests { use {super::*, crate::Sysvar, serial_test::serial}; + #[test] + fn test_pod_epoch_rewards_conversion() { + let pod = PodEpochRewards { + distribution_starting_block_height: 42u64.to_le_bytes(), + num_partitions: 7u64.to_le_bytes(), + parent_blockhash: [0xAA; 32], + total_points: 1234567890u128.to_le_bytes(), + total_rewards: 100u64.to_le_bytes(), + distributed_rewards: 10u64.to_le_bytes(), + active: 1, + }; + + let epoch_rewards = EpochRewards::from(pod); + + assert_eq!(epoch_rewards.distribution_starting_block_height, 42); + assert_eq!(epoch_rewards.num_partitions, 7); + assert_eq!( + epoch_rewards.parent_blockhash, + solana_hash::Hash::new_from_array([0xAA; 32]) + ); + assert_eq!(epoch_rewards.total_points, 1234567890); + assert_eq!(epoch_rewards.total_rewards, 100); + assert_eq!(epoch_rewards.distributed_rewards, 10); + assert!(epoch_rewards.active); + } + #[test] #[serial] #[cfg(feature = "bincode")] fn test_epoch_rewards_get() { - let expected = EpochRewards::new(100, 10, 42); + let expected = EpochRewards { + distribution_starting_block_height: 42, + num_partitions: 7, + parent_blockhash: solana_hash::Hash::new_unique(), + total_points: 1234567890, + total_rewards: 100, + distributed_rewards: 10, + active: true, + }; let data = bincode::serialize(&expected).unwrap(); assert_eq!(data.len(), 81); diff --git a/sysvar/src/epoch_schedule.rs b/sysvar/src/epoch_schedule.rs index cdd2b2e0a..15ca208ba 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -119,16 +119,81 @@ //! # //! # Ok::<(), anyhow::Error>(()) //! ``` +use crate::Sysvar; #[cfg(feature = "bincode")] use crate::SysvarSerialize; -use crate::{impl_sysvar_get, Sysvar}; pub use { solana_epoch_schedule::EpochSchedule, solana_sdk_ids::sysvar::epoch_schedule::{check_id, id, ID}, }; +/// Pod (Plain Old Data) representation of [`EpochSchedule`] with no padding. +/// +/// This type can be safely loaded via `sol_get_sysvar` without undefined behavior. +/// Provides performant zero-copy accessors as an alternative to the `EpochSchedule` type. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PodEpochSchedule { + slots_per_epoch: [u8; 8], + leader_schedule_slot_offset: [u8; 8], + warmup: u8, + first_normal_epoch: [u8; 8], + first_normal_slot: [u8; 8], +} + +const _: () = assert!(core::mem::size_of::() == 33); + +impl PodEpochSchedule { + pub fn fetch() -> Result { + let mut pod = core::mem::MaybeUninit::::uninit(); + unsafe { + crate::get_sysvar_unchecked( + pod.as_mut_ptr() as *mut u8, + (&id()) as *const _ as *const u8, + 0, + 33, + )?; + Ok(pod.assume_init()) + } + } + + pub fn slots_per_epoch(&self) -> u64 { + u64::from_le_bytes(self.slots_per_epoch) + } + + pub fn leader_schedule_slot_offset(&self) -> u64 { + u64::from_le_bytes(self.leader_schedule_slot_offset) + } + + pub fn warmup(&self) -> bool { + self.warmup != 0 + } + + pub fn first_normal_epoch(&self) -> u64 { + u64::from_le_bytes(self.first_normal_epoch) + } + + pub fn first_normal_slot(&self) -> u64 { + u64::from_le_bytes(self.first_normal_slot) + } +} + +impl From for EpochSchedule { + fn from(pod: PodEpochSchedule) -> Self { + Self { + slots_per_epoch: pod.slots_per_epoch(), + leader_schedule_slot_offset: pod.leader_schedule_slot_offset(), + warmup: pod.warmup(), + first_normal_epoch: pod.first_normal_epoch(), + first_normal_slot: pod.first_normal_slot(), + } + } +} + impl Sysvar for EpochSchedule { - impl_sysvar_get!(id()); + fn get() -> Result { + Ok(Self::from(PodEpochSchedule::fetch()?)) + } } #[cfg(feature = "bincode")] @@ -138,6 +203,25 @@ impl SysvarSerialize for EpochSchedule {} mod tests { use {super::*, crate::Sysvar, serial_test::serial}; + #[test] + fn test_pod_epoch_schedule_conversion() { + let pod = PodEpochSchedule { + slots_per_epoch: 432000u64.to_le_bytes(), + leader_schedule_slot_offset: 432000u64.to_le_bytes(), + warmup: 1, + first_normal_epoch: 14u64.to_le_bytes(), + first_normal_slot: 524256u64.to_le_bytes(), + }; + + let epoch_schedule = EpochSchedule::from(pod); + + assert_eq!(epoch_schedule.slots_per_epoch, 432000); + assert_eq!(epoch_schedule.leader_schedule_slot_offset, 432000); + assert!(epoch_schedule.warmup); + assert_eq!(epoch_schedule.first_normal_epoch, 14); + assert_eq!(epoch_schedule.first_normal_slot, 524256); + } + #[test] #[serial] #[cfg(feature = "bincode")] diff --git a/sysvar/src/rent.rs b/sysvar/src/rent.rs index fb816a689..e6fc77fee 100644 --- a/sysvar/src/rent.rs +++ b/sysvar/src/rent.rs @@ -121,16 +121,25 @@ //! # //! # Ok::<(), anyhow::Error>(()) //! ``` +use crate::Sysvar; #[cfg(feature = "bincode")] use crate::SysvarSerialize; -use crate::{impl_sysvar_get, Sysvar}; pub use { solana_rent::Rent, solana_sdk_ids::sysvar::rent::{check_id, id, ID}, }; impl Sysvar for Rent { - impl_sysvar_get!(id()); + fn get() -> Result { + let mut var = core::mem::MaybeUninit::::uninit(); + let var_addr = var.as_mut_ptr() as *mut u8; + unsafe { + crate::get_sysvar_unchecked(var_addr, (&id()) as *const _ as *const u8, 0, 17)?; + // Zero the 7 bytes of padding (bytes 17-23) + var_addr.add(17).write_bytes(0, 7); + Ok(var.assume_init()) + } + } } #[cfg(feature = "bincode")] @@ -144,7 +153,11 @@ mod tests { #[serial] #[cfg(feature = "bincode")] fn test_rent_get() { - let expected = Rent::new(123, 2.5, 7); + let expected = Rent { + lamports_per_byte_year: 123, + exemption_threshold: 2.5, + burn_percent: 7, + }; let data = bincode::serialize(&expected).unwrap(); assert_eq!(data.len(), 17); From c438a3e94b3a54c7f2fefb3d18ba4da4b70a9d68 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:12:05 +0000 Subject: [PATCH 11/26] doc comments, misc --- epoch-rewards/src/lib.rs | 37 ++++++++++++++++++++++++------------- epoch-schedule/src/lib.rs | 2 -- sysvar/src/epoch_rewards.rs | 24 +++++++++++++++++++++--- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/epoch-rewards/src/lib.rs b/epoch-rewards/src/lib.rs index 609ab8002..5d581891a 100644 --- a/epoch-rewards/src/lib.rs +++ b/epoch-rewards/src/lib.rs @@ -55,19 +55,6 @@ pub struct EpochRewards { } impl EpochRewards { - pub fn new( - total_rewards: u64, - distributed_rewards: u64, - distribution_starting_block_height: u64, - ) -> Self { - Self { - distribution_starting_block_height, - total_rewards, - distributed_rewards, - ..Self::default() - } - } - pub fn distribute(&mut self, amount: u64) { let new_distributed_rewards = self.distributed_rewards.saturating_add(amount); assert!(new_distributed_rewards <= self.total_rewards); @@ -79,6 +66,30 @@ impl EpochRewards { mod tests { use super::*; + impl EpochRewards { + pub fn new( + total_rewards: u64, + distributed_rewards: u64, + distribution_starting_block_height: u64, + ) -> Self { + Self { + total_rewards, + distributed_rewards, + distribution_starting_block_height, + ..Self::default() + } + } + } + + #[test] + fn test_epoch_rewards_new() { + let epoch_rewards = EpochRewards::new(100, 0, 64); + + assert_eq!(epoch_rewards.total_rewards, 100); + assert_eq!(epoch_rewards.distributed_rewards, 0); + assert_eq!(epoch_rewards.distribution_starting_block_height, 64); + } + #[test] fn test_epoch_rewards_distribute() { let mut epoch_rewards = EpochRewards::new(100, 0, 64); diff --git a/epoch-schedule/src/lib.rs b/epoch-schedule/src/lib.rs index e5b8f323a..a1ed5c16a 100644 --- a/epoch-schedule/src/lib.rs +++ b/epoch-schedule/src/lib.rs @@ -90,7 +90,6 @@ impl EpochSchedule { pub fn new(slots_per_epoch: u64) -> Self { Self::custom(slots_per_epoch, slots_per_epoch, true) } - pub fn without_warmup() -> Self { Self::custom( DEFAULT_SLOTS_PER_EPOCH, @@ -98,7 +97,6 @@ impl EpochSchedule { false, ) } - pub fn custom(slots_per_epoch: u64, leader_schedule_slot_offset: u64, warmup: bool) -> Self { assert!(slots_per_epoch >= MINIMUM_SLOTS_PER_EPOCH); let (first_normal_epoch, first_normal_slot) = if warmup { diff --git a/sysvar/src/epoch_rewards.rs b/sysvar/src/epoch_rewards.rs index 4d4324b42..24ebe697b 100755 --- a/sysvar/src/epoch_rewards.rs +++ b/sysvar/src/epoch_rewards.rs @@ -49,7 +49,13 @@ //! # use solana_sysvar_id::SysvarId; //! # let p = EpochRewards::id(); //! # let l = &mut 1559040; -//! # let epoch_rewards = EpochRewards::new(100, 10, 42); +//! # let epoch_rewards = EpochRewards { +//! # distribution_starting_block_height: 42, +//! # total_rewards: 100, +//! # distributed_rewards: 10, +//! # active: true, +//! # ..EpochRewards::default() +//! # }; //! # let mut d: Vec = bincode::serialize(&epoch_rewards).unwrap(); //! # let a = AccountInfo::new(&p, false, false, l, &mut d, &p, false); //! # let accounts = &[a.clone(), a]; @@ -91,7 +97,13 @@ //! # use solana_sysvar_id::SysvarId; //! # let p = EpochRewards::id(); //! # let l = &mut 1559040; -//! # let epoch_rewards = EpochRewards::new(100, 10, 42); +//! # let epoch_rewards = EpochRewards { +//! # distribution_starting_block_height: 42, +//! # total_rewards: 100, +//! # distributed_rewards: 10, +//! # active: true, +//! # ..EpochRewards::default() +//! # }; //! # let mut d: Vec = bincode::serialize(&epoch_rewards).unwrap(); //! # let a = AccountInfo::new(&p, false, false, l, &mut d, &p, false); //! # let accounts = &[a.clone(), a]; @@ -115,7 +127,13 @@ //! # use anyhow::Result; //! # //! fn print_sysvar_epoch_rewards(client: &RpcClient) -> Result<()> { -//! # let epoch_rewards = EpochRewards::new(100, 10, 42); +//! # let epoch_rewards = EpochRewards { +//! # distribution_starting_block_height: 42, +//! # total_rewards: 100, +//! # distributed_rewards: 10, +//! # active: true, +//! # ..EpochRewards::default() +//! # }; //! # let data: Vec = bincode::serialize(&epoch_rewards)?; //! # client.set_get_account_response(epoch_rewards::ID, Account { //! # lamports: 1120560, From 17b02fcde6ad2c6e19c035fa44a9ab1fe347561e Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:16:01 +0000 Subject: [PATCH 12/26] safety comments --- sysvar/src/epoch_rewards.rs | 3 +++ sysvar/src/epoch_schedule.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/sysvar/src/epoch_rewards.rs b/sysvar/src/epoch_rewards.rs index 24ebe697b..7575b748d 100755 --- a/sysvar/src/epoch_rewards.rs +++ b/sysvar/src/epoch_rewards.rs @@ -183,6 +183,8 @@ const _: () = assert!(core::mem::size_of::() == 81); impl PodEpochRewards { pub fn fetch() -> Result { let mut pod = core::mem::MaybeUninit::::uninit(); + // SAFETY: `get_sysvar_unchecked` will initialize the buffer with the sysvar data, + // or return an error if unsuccessful. size_of::() is compile-time asserted to be 81. unsafe { crate::get_sysvar_unchecked( pod.as_mut_ptr() as *mut u8, @@ -190,6 +192,7 @@ impl PodEpochRewards { 0, 81, )?; + // SAFETY: Must be initialized by `get_sysvar_unchecked` if this point is reached. Ok(pod.assume_init()) } } diff --git a/sysvar/src/epoch_schedule.rs b/sysvar/src/epoch_schedule.rs index 15ca208ba..5723aa02b 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -146,6 +146,8 @@ const _: () = assert!(core::mem::size_of::() == 33); impl PodEpochSchedule { pub fn fetch() -> Result { let mut pod = core::mem::MaybeUninit::::uninit(); + // SAFETY: `get_sysvar_unchecked` will initialize the buffer with the sysvar data, + // or return an error if unsuccessful. size_of::() is compile-time asserted to be 33. unsafe { crate::get_sysvar_unchecked( pod.as_mut_ptr() as *mut u8, @@ -153,6 +155,7 @@ impl PodEpochSchedule { 0, 33, )?; + // SAFETY: Must be initialized by `get_sysvar_unchecked` if this point is reached. Ok(pod.assume_init()) } } From d3e51c99a7466357199a0ab1f58031e78c8cf0be Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:25:42 +0000 Subject: [PATCH 13/26] rm now unneeded Rent::new() --- rent/src/lib.rs | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/rent/src/lib.rs b/rent/src/lib.rs index b0785009d..8a70d2b99 100644 --- a/rent/src/lib.rs +++ b/rent/src/lib.rs @@ -81,23 +81,6 @@ impl Default for Rent { } impl Rent { - /// Creates a new `Rent` with the given parameters. - /// - /// # Panics - /// - /// Panics if `burn_percent` is not in the range [0, 100]. - pub fn new(lamports_per_byte_year: u64, exemption_threshold: f64, burn_percent: u8) -> Self { - assert!( - burn_percent <= 100, - "burn_percent must be in range [0, 100]" - ); - Self { - lamports_per_byte_year, - exemption_threshold, - burn_percent, - } - } - /// Calculate how much rent to burn from the collected rent. /// /// The first value returned is the amount burned. The second is the amount @@ -152,11 +135,11 @@ impl Rent { let ratio = slots_per_epoch as f64 / DEFAULT_SLOTS_PER_EPOCH as f64; let exemption_threshold = DEFAULT_EXEMPTION_THRESHOLD * ratio; let lamports_per_byte_year = (DEFAULT_LAMPORTS_PER_BYTE_YEAR as f64 / ratio) as u64; - Self::new( + Self { lamports_per_byte_year, exemption_threshold, - DEFAULT_BURN_PERCENT, - ) + ..Self::default() + } } } @@ -212,7 +195,11 @@ mod tests { RentDue::Exempt, ); - let custom_rent = Rent::new(5, 2.5, DEFAULT_BURN_PERCENT); + let custom_rent = Rent { + lamports_per_byte_year: 5, + exemption_threshold: 2.5, + ..Rent::default() + }; assert_eq!( custom_rent.due(0, 2, 1.2), @@ -249,7 +236,11 @@ mod tests { #[test] fn test_clone() { - let rent = Rent::new(1, 2.2, 3); + let rent = Rent { + lamports_per_byte_year: 1, + exemption_threshold: 2.2, + burn_percent: 3, + }; #[allow(clippy::clone_on_copy)] let cloned_rent = rent.clone(); assert_eq!(cloned_rent, rent); From 4b95d4d03b236819faa309754be51e9d9463c737 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:04:27 +0000 Subject: [PATCH 14/26] use common and size constants --- sysvar/src/epoch_rewards.rs | 18 ++++-------------- sysvar/src/epoch_schedule.rs | 18 ++++-------------- sysvar/src/lib.rs | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/sysvar/src/epoch_rewards.rs b/sysvar/src/epoch_rewards.rs index 7575b748d..6001907c1 100755 --- a/sysvar/src/epoch_rewards.rs +++ b/sysvar/src/epoch_rewards.rs @@ -178,23 +178,13 @@ pub struct PodEpochRewards { active: u8, } -const _: () = assert!(core::mem::size_of::() == 81); +const POD_EPOCH_REWARDS_SIZE: usize = 81; +const _: () = assert!(core::mem::size_of::() == POD_EPOCH_REWARDS_SIZE); impl PodEpochRewards { pub fn fetch() -> Result { - let mut pod = core::mem::MaybeUninit::::uninit(); - // SAFETY: `get_sysvar_unchecked` will initialize the buffer with the sysvar data, - // or return an error if unsuccessful. size_of::() is compile-time asserted to be 81. - unsafe { - crate::get_sysvar_unchecked( - pod.as_mut_ptr() as *mut u8, - (&id()) as *const _ as *const u8, - 0, - 81, - )?; - // SAFETY: Must be initialized by `get_sysvar_unchecked` if this point is reached. - Ok(pod.assume_init()) - } + // SAFETY: size is compile-time asserted above to be correct. + unsafe { crate::fetch_pod(&id(), POD_EPOCH_REWARDS_SIZE) } } pub fn distribution_starting_block_height(&self) -> u64 { diff --git a/sysvar/src/epoch_schedule.rs b/sysvar/src/epoch_schedule.rs index 5723aa02b..f72e06273 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -141,23 +141,13 @@ pub struct PodEpochSchedule { first_normal_slot: [u8; 8], } -const _: () = assert!(core::mem::size_of::() == 33); +const POD_EPOCH_SCHEDULE_SIZE: usize = 33; +const _: () = assert!(core::mem::size_of::() == POD_EPOCH_SCHEDULE_SIZE); impl PodEpochSchedule { pub fn fetch() -> Result { - let mut pod = core::mem::MaybeUninit::::uninit(); - // SAFETY: `get_sysvar_unchecked` will initialize the buffer with the sysvar data, - // or return an error if unsuccessful. size_of::() is compile-time asserted to be 33. - unsafe { - crate::get_sysvar_unchecked( - pod.as_mut_ptr() as *mut u8, - (&id()) as *const _ as *const u8, - 0, - 33, - )?; - // SAFETY: Must be initialized by `get_sysvar_unchecked` if this point is reached. - Ok(pod.assume_init()) - } + // SAFETY: size is compile-time asserted above to be correct. + unsafe { crate::fetch_pod(&id(), POD_EPOCH_SCHEDULE_SIZE) } } pub fn slots_per_epoch(&self) -> u64 { diff --git a/sysvar/src/lib.rs b/sysvar/src/lib.rs index abb3cd77b..261c7d94a 100644 --- a/sysvar/src/lib.rs +++ b/sysvar/src/lib.rs @@ -231,6 +231,25 @@ pub fn get_sysvar( } } +/// Generic helper to fetch a Pod sysvar type. +/// +/// Loads the sysvar data directly into an uninitialized Pod struct and returns it. +/// +/// # Safety +/// Caller must ensure that `size` is correct for `P`. +pub unsafe fn fetch_pod

(sysvar_id: &Pubkey, size: usize) -> Result { + let mut pod = core::mem::MaybeUninit::

::uninit(); + // SAFETY: `get_sysvar_unchecked` will initialize `pod` with the sysvar data, + // or return an error if unsuccessful. + get_sysvar_unchecked( + pod.as_mut_ptr() as *mut u8, + sysvar_id as *const _ as *const u8, + 0, + size as u64, + )?; + Ok(pod.assume_init()) +} + /// Internal helper for retrieving sysvar data directly into a raw buffer. /// /// # Safety From c52a6bfbc2b3b19dec9d32c40fa49f777f64635b Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 2 Dec 2025 19:25:15 +0000 Subject: [PATCH 15/26] rm unnecessary pod for EpochRewards, use pad fill approach --- sysvar/src/epoch_rewards.rs | 107 +++--------------------------------- 1 file changed, 8 insertions(+), 99 deletions(-) diff --git a/sysvar/src/epoch_rewards.rs b/sysvar/src/epoch_rewards.rs index 6001907c1..894d0c5d1 100755 --- a/sysvar/src/epoch_rewards.rs +++ b/sysvar/src/epoch_rewards.rs @@ -162,81 +162,16 @@ pub use { solana_sdk_ids::sysvar::epoch_rewards::{check_id, id, ID}, }; -/// Pod (Plain Old Data) representation of [`EpochRewards`] with no padding. -/// -/// This type can be safely loaded via `sol_get_sysvar` without undefined behavior. -/// Provides performant zero-copy accessors as an alternative to the `EpochRewards` type. -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct PodEpochRewards { - distribution_starting_block_height: [u8; 8], - num_partitions: [u8; 8], - parent_blockhash: [u8; 32], - total_points: [u8; 16], - total_rewards: [u8; 8], - distributed_rewards: [u8; 8], - active: u8, -} - -const POD_EPOCH_REWARDS_SIZE: usize = 81; -const _: () = assert!(core::mem::size_of::() == POD_EPOCH_REWARDS_SIZE); - -impl PodEpochRewards { - pub fn fetch() -> Result { - // SAFETY: size is compile-time asserted above to be correct. - unsafe { crate::fetch_pod(&id(), POD_EPOCH_REWARDS_SIZE) } - } - - pub fn distribution_starting_block_height(&self) -> u64 { - u64::from_le_bytes(self.distribution_starting_block_height) - } - - pub fn num_partitions(&self) -> u64 { - u64::from_le_bytes(self.num_partitions) - } - - pub fn parent_blockhash(&self) -> solana_hash::Hash { - solana_hash::Hash::new_from_array(self.parent_blockhash) - } - - pub fn total_points(&self) -> u128 { - u128::from_le_bytes(self.total_points) - } - - pub fn total_rewards(&self) -> u64 { - u64::from_le_bytes(self.total_rewards) - } - - pub fn distributed_rewards(&self) -> u64 { - u64::from_le_bytes(self.distributed_rewards) - } - - pub fn active(&self) -> bool { - match self.active { - 0 => false, - 1 => true, - _ => panic!("invalid active value"), - } - } -} - -impl From for EpochRewards { - fn from(pod: PodEpochRewards) -> Self { - Self { - distribution_starting_block_height: pod.distribution_starting_block_height(), - num_partitions: pod.num_partitions(), - parent_blockhash: pod.parent_blockhash(), - total_points: pod.total_points(), - total_rewards: pod.total_rewards(), - distributed_rewards: pod.distributed_rewards(), - active: pod.active(), - } - } -} - impl Sysvar for EpochRewards { fn get() -> Result { - Ok(Self::from(PodEpochRewards::fetch()?)) + let mut var = core::mem::MaybeUninit::::uninit(); + let var_addr = var.as_mut_ptr() as *mut u8; + unsafe { + crate::get_sysvar_unchecked(var_addr, (&id()) as *const _ as *const u8, 0, 81)?; + // Zero the 15 bytes of padding (bytes 81-95) + var_addr.add(81).write_bytes(0, 15); + Ok(var.assume_init()) + } } } @@ -247,32 +182,6 @@ impl SysvarSerialize for EpochRewards {} mod tests { use {super::*, crate::Sysvar, serial_test::serial}; - #[test] - fn test_pod_epoch_rewards_conversion() { - let pod = PodEpochRewards { - distribution_starting_block_height: 42u64.to_le_bytes(), - num_partitions: 7u64.to_le_bytes(), - parent_blockhash: [0xAA; 32], - total_points: 1234567890u128.to_le_bytes(), - total_rewards: 100u64.to_le_bytes(), - distributed_rewards: 10u64.to_le_bytes(), - active: 1, - }; - - let epoch_rewards = EpochRewards::from(pod); - - assert_eq!(epoch_rewards.distribution_starting_block_height, 42); - assert_eq!(epoch_rewards.num_partitions, 7); - assert_eq!( - epoch_rewards.parent_blockhash, - solana_hash::Hash::new_from_array([0xAA; 32]) - ); - assert_eq!(epoch_rewards.total_points, 1234567890); - assert_eq!(epoch_rewards.total_rewards, 100); - assert_eq!(epoch_rewards.distributed_rewards, 10); - assert!(epoch_rewards.active); - } - #[test] #[serial] #[cfg(feature = "bincode")] From 0baf904dc0ed3cf73a83c251db28bddc39fcc37c Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 2 Dec 2025 19:33:43 +0000 Subject: [PATCH 16/26] in-line now unnecessary helper --- sysvar/src/epoch_rewards.rs | 2 ++ sysvar/src/epoch_schedule.rs | 14 ++++++++++++-- sysvar/src/rent.rs | 2 ++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/sysvar/src/epoch_rewards.rs b/sysvar/src/epoch_rewards.rs index 894d0c5d1..23c8edfc8 100755 --- a/sysvar/src/epoch_rewards.rs +++ b/sysvar/src/epoch_rewards.rs @@ -166,6 +166,8 @@ impl Sysvar for EpochRewards { fn get() -> Result { let mut var = core::mem::MaybeUninit::::uninit(); let var_addr = var.as_mut_ptr() as *mut u8; + // Safety: `get_sysvar_unchecked` will initialize `var` with the sysvar data, + // and error if unsuccessful. unsafe { crate::get_sysvar_unchecked(var_addr, (&id()) as *const _ as *const u8, 0, 81)?; // Zero the 15 bytes of padding (bytes 81-95) diff --git a/sysvar/src/epoch_schedule.rs b/sysvar/src/epoch_schedule.rs index f72e06273..211367d73 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -146,8 +146,18 @@ const _: () = assert!(core::mem::size_of::() == POD_EPOCH_SCHE impl PodEpochSchedule { pub fn fetch() -> Result { - // SAFETY: size is compile-time asserted above to be correct. - unsafe { crate::fetch_pod(&id(), POD_EPOCH_SCHEDULE_SIZE) } + let mut pod = core::mem::MaybeUninit::::uninit(); + // Safety: `get_sysvar_unchecked` will initialize `pod` with the sysvar data, + // and error if unsuccessful. + unsafe { + crate::get_sysvar_unchecked( + pod.as_mut_ptr() as *mut u8, + (&id()) as *const _ as *const u8, + 0, + POD_EPOCH_SCHEDULE_SIZE as u64, + )?; + Ok(pod.assume_init()) + } } pub fn slots_per_epoch(&self) -> u64 { diff --git a/sysvar/src/rent.rs b/sysvar/src/rent.rs index e6fc77fee..77de985d0 100644 --- a/sysvar/src/rent.rs +++ b/sysvar/src/rent.rs @@ -133,6 +133,8 @@ impl Sysvar for Rent { fn get() -> Result { let mut var = core::mem::MaybeUninit::::uninit(); let var_addr = var.as_mut_ptr() as *mut u8; + // Safety: `get_sysvar_unchecked` will initialize `var` with the sysvar data, + // and error if unsuccessful. unsafe { crate::get_sysvar_unchecked(var_addr, (&id()) as *const _ as *const u8, 0, 17)?; // Zero the 7 bytes of padding (bytes 17-23) From 46fa5b113cd98d4457ab29e0553129c8043bce73 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 2 Dec 2025 19:34:32 +0000 Subject: [PATCH 17/26] doc comment --- sysvar/src/epoch_schedule.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sysvar/src/epoch_schedule.rs b/sysvar/src/epoch_schedule.rs index 211367d73..43f5f857e 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -145,6 +145,8 @@ const POD_EPOCH_SCHEDULE_SIZE: usize = 33; const _: () = assert!(core::mem::size_of::() == POD_EPOCH_SCHEDULE_SIZE); impl PodEpochSchedule { + /// Fetch the sysvar data using the `sol_get_sysvar` syscall. + /// This provides an alternative to `EpochSchedule` which provides zero-copy accessors. pub fn fetch() -> Result { let mut pod = core::mem::MaybeUninit::::uninit(); // Safety: `get_sysvar_unchecked` will initialize `pod` with the sysvar data, From a6e7297efded063ca375accf6d6f46540ac43eff Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:08:18 +0000 Subject: [PATCH 18/26] impl_sysvar_get takes optional padding --- sysvar/src/epoch_rewards.rs | 15 ++------------- sysvar/src/lib.rs | 23 +++++++++++++---------- sysvar/src/rent.rs | 15 ++------------- 3 files changed, 17 insertions(+), 36 deletions(-) diff --git a/sysvar/src/epoch_rewards.rs b/sysvar/src/epoch_rewards.rs index 23c8edfc8..1cac1c20a 100755 --- a/sysvar/src/epoch_rewards.rs +++ b/sysvar/src/epoch_rewards.rs @@ -154,27 +154,16 @@ //! # Ok::<(), anyhow::Error>(()) //! ``` -use crate::Sysvar; #[cfg(feature = "bincode")] use crate::SysvarSerialize; +use crate::{impl_sysvar_get, Sysvar}; pub use { solana_epoch_rewards::EpochRewards, solana_sdk_ids::sysvar::epoch_rewards::{check_id, id, ID}, }; impl Sysvar for EpochRewards { - fn get() -> Result { - let mut var = core::mem::MaybeUninit::::uninit(); - let var_addr = var.as_mut_ptr() as *mut u8; - // Safety: `get_sysvar_unchecked` will initialize `var` with the sysvar data, - // and error if unsuccessful. - unsafe { - crate::get_sysvar_unchecked(var_addr, (&id()) as *const _ as *const u8, 0, 81)?; - // Zero the 15 bytes of padding (bytes 81-95) - var_addr.add(81).write_bytes(0, 15); - Ok(var.assume_init()) - } - } + impl_sysvar_get!(id(), 15); } #[cfg(feature = "bincode")] diff --git a/sysvar/src/lib.rs b/sysvar/src/lib.rs index 261c7d94a..911f61270 100644 --- a/sysvar/src/lib.rs +++ b/sysvar/src/lib.rs @@ -181,22 +181,25 @@ macro_rules! impl_sysvar_get { } } }; - ($sysvar_id:expr) => { + // Variant for sysvars with padding at the end. Loads bincode-serialized data + // (size - padding bytes) and zeros the padding to avoid undefined behavior. + ($sysvar_id:expr, $padding:literal) => { fn get() -> Result { - let mut uninit = core::mem::MaybeUninit::::uninit(); - let size = core::mem::size_of::() as u64; + let mut var = core::mem::MaybeUninit::::uninit(); + let var_addr = var.as_mut_ptr() as *mut u8; + let length = core::mem::size_of::() - $padding; let sysvar_id_ptr = (&$sysvar_id) as *const _ as *const u8; unsafe { - $crate::get_sysvar_unchecked( - uninit.as_mut_ptr() as *mut u8, - sysvar_id_ptr, - 0, - size, - )?; - Ok(uninit.assume_init()) + $crate::get_sysvar_unchecked(var_addr, sysvar_id_ptr, 0, length as u64)?; + var_addr.add(length).write_bytes(0, $padding); + Ok(var.assume_init()) } } }; + // Variant for sysvars without padding (struct size matches bincode size). + ($sysvar_id:expr) => { + $crate::impl_sysvar_get!($sysvar_id, 0); + }; } /// Handler for retrieving a slice of sysvar data from the `sol_get_sysvar` diff --git a/sysvar/src/rent.rs b/sysvar/src/rent.rs index 77de985d0..72809c5ff 100644 --- a/sysvar/src/rent.rs +++ b/sysvar/src/rent.rs @@ -121,27 +121,16 @@ //! # //! # Ok::<(), anyhow::Error>(()) //! ``` -use crate::Sysvar; #[cfg(feature = "bincode")] use crate::SysvarSerialize; +use crate::{impl_sysvar_get, Sysvar}; pub use { solana_rent::Rent, solana_sdk_ids::sysvar::rent::{check_id, id, ID}, }; impl Sysvar for Rent { - fn get() -> Result { - let mut var = core::mem::MaybeUninit::::uninit(); - let var_addr = var.as_mut_ptr() as *mut u8; - // Safety: `get_sysvar_unchecked` will initialize `var` with the sysvar data, - // and error if unsuccessful. - unsafe { - crate::get_sysvar_unchecked(var_addr, (&id()) as *const _ as *const u8, 0, 17)?; - // Zero the 7 bytes of padding (bytes 17-23) - var_addr.add(17).write_bytes(0, 7); - Ok(var.assume_init()) - } - } + impl_sysvar_get!(id(), 7); } #[cfg(feature = "bincode")] From 2ce54928adad6b8da9ec2503056468ca6bb63ed0 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:13:45 +0000 Subject: [PATCH 19/26] safety comments --- sysvar/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sysvar/src/lib.rs b/sysvar/src/lib.rs index 911f61270..dbd1178be 100644 --- a/sysvar/src/lib.rs +++ b/sysvar/src/lib.rs @@ -183,15 +183,21 @@ macro_rules! impl_sysvar_get { }; // Variant for sysvars with padding at the end. Loads bincode-serialized data // (size - padding bytes) and zeros the padding to avoid undefined behavior. + // Only supports sysvars where padding is at the end of the layout. ($sysvar_id:expr, $padding:literal) => { fn get() -> Result { let mut var = core::mem::MaybeUninit::::uninit(); let var_addr = var.as_mut_ptr() as *mut u8; - let length = core::mem::size_of::() - $padding; + let length = core::mem::size_of::().saturating_sub($padding); let sysvar_id_ptr = (&$sysvar_id) as *const _ as *const u8; + // SAFETY: The allocation is valid for `size_of::()`. We load + // `(size - padding)` bytes from the syscall, which matches bincode + // serialization. The remaining `padding` bytes are then zeroed. unsafe { $crate::get_sysvar_unchecked(var_addr, sysvar_id_ptr, 0, length as u64)?; var_addr.add(length).write_bytes(0, $padding); + // SAFETY: All bytes now initialized: syscall filled data + // bytes and we zeroed padding. Ok(var.assume_init()) } } From f869967bafe6875ada432f52a8af573612ce8623 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:29:21 +0000 Subject: [PATCH 20/26] fold in sysvar errors as previously --- sysvar/src/lib.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/sysvar/src/lib.rs b/sysvar/src/lib.rs index dbd1178be..ff3cc3405 100644 --- a/sysvar/src/lib.rs +++ b/sysvar/src/lib.rs @@ -183,7 +183,8 @@ macro_rules! impl_sysvar_get { }; // Variant for sysvars with padding at the end. Loads bincode-serialized data // (size - padding bytes) and zeros the padding to avoid undefined behavior. - // Only supports sysvars where padding is at the end of the layout. + // Only supports sysvars where padding is at the end of the layout. Caller + // must supply the correct number of padding bytes. ($sysvar_id:expr, $padding:literal) => { fn get() -> Result { let mut var = core::mem::MaybeUninit::::uninit(); @@ -193,12 +194,19 @@ macro_rules! impl_sysvar_get { // SAFETY: The allocation is valid for `size_of::()`. We load // `(size - padding)` bytes from the syscall, which matches bincode // serialization. The remaining `padding` bytes are then zeroed. - unsafe { - $crate::get_sysvar_unchecked(var_addr, sysvar_id_ptr, 0, length as u64)?; - var_addr.add(length).write_bytes(0, $padding); - // SAFETY: All bytes now initialized: syscall filled data - // bytes and we zeroed padding. - Ok(var.assume_init()) + let result = + unsafe { $crate::get_sysvar_unchecked(var_addr, sysvar_id_ptr, 0, length as u64) }; + match result { + Ok(()) => { + // SAFETY: All bytes now initialized: syscall filled data + // bytes and we zeroed padding. + unsafe { + var_addr.add(length).write_bytes(0, $padding); + Ok(var.assume_init()) + } + } + // Unexpected errors are folded into `UnsupportedSysvar`. + Err(_) => Err($crate::__private::ProgramError::UnsupportedSysvar), } } }; From f5af4649b3325f7bee034e35b331314bbe084770 Mon Sep 17 00:00:00 2001 From: Peter Keay <96253492+rustopian@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:22:36 +0000 Subject: [PATCH 21/26] valid bool values only Co-authored-by: Fernando Otero --- sysvar/src/epoch_schedule.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sysvar/src/epoch_schedule.rs b/sysvar/src/epoch_schedule.rs index 43f5f857e..0c962e64b 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -170,8 +170,12 @@ impl PodEpochSchedule { u64::from_le_bytes(self.leader_schedule_slot_offset) } - pub fn warmup(&self) -> bool { - self.warmup != 0 + pub fn warmup(&self) -> Result { + match self.warmup { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(ProgramError::InvalidAccountData), + } } pub fn first_normal_epoch(&self) -> u64 { From b36f96e5416b9289d8ca9dfd3753cb56e064f747 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:57:46 +0000 Subject: [PATCH 22/26] specifier, fmt --- sysvar/src/epoch_schedule.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/sysvar/src/epoch_schedule.rs b/sysvar/src/epoch_schedule.rs index 0c962e64b..8f6f108ce 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -119,9 +119,9 @@ //! # //! # Ok::<(), anyhow::Error>(()) //! ``` -use crate::Sysvar; #[cfg(feature = "bincode")] use crate::SysvarSerialize; +use {crate::Sysvar, solana_program_error::ProgramError}; pub use { solana_epoch_schedule::EpochSchedule, solana_sdk_ids::sysvar::epoch_schedule::{check_id, id, ID}, @@ -187,21 +187,23 @@ impl PodEpochSchedule { } } -impl From for EpochSchedule { - fn from(pod: PodEpochSchedule) -> Self { - Self { +impl TryFrom for EpochSchedule { + type Error = ProgramError; + + fn try_from(pod: PodEpochSchedule) -> Result { + Ok(Self { slots_per_epoch: pod.slots_per_epoch(), leader_schedule_slot_offset: pod.leader_schedule_slot_offset(), - warmup: pod.warmup(), + warmup: pod.warmup()?, first_normal_epoch: pod.first_normal_epoch(), first_normal_slot: pod.first_normal_slot(), - } + }) } } impl Sysvar for EpochSchedule { fn get() -> Result { - Ok(Self::from(PodEpochSchedule::fetch()?)) + PodEpochSchedule::fetch()?.try_into() } } @@ -222,7 +224,7 @@ mod tests { first_normal_slot: 524256u64.to_le_bytes(), }; - let epoch_schedule = EpochSchedule::from(pod); + let epoch_schedule = EpochSchedule::try_from(pod).unwrap(); assert_eq!(epoch_schedule.slots_per_epoch, 432000); assert_eq!(epoch_schedule.leader_schedule_slot_offset, 432000); From e8ab6acc3b5beace745d961d2dd5bf6f7320f62e Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Fri, 5 Dec 2025 10:26:28 +0000 Subject: [PATCH 23/26] use bool directly, safety comments --- epoch-rewards/src/lib.rs | 3 +++ sysvar/src/epoch_rewards.rs | 4 ++++ sysvar/src/epoch_schedule.rs | 12 +++++------- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/epoch-rewards/src/lib.rs b/epoch-rewards/src/lib.rs index 5d581891a..c7ea303c8 100644 --- a/epoch-rewards/src/lib.rs +++ b/epoch-rewards/src/lib.rs @@ -51,6 +51,9 @@ pub struct EpochRewards { /// Whether the rewards period (including calculation and distribution) is /// active + /// + /// SAFETY: upstream invariant: the sysvar data is created exclusively + /// by the Solana runtime and serializes bool as 0x00 or 0x01. pub active: bool, } diff --git a/sysvar/src/epoch_rewards.rs b/sysvar/src/epoch_rewards.rs index 1cac1c20a..3e87254e9 100755 --- a/sysvar/src/epoch_rewards.rs +++ b/sysvar/src/epoch_rewards.rs @@ -163,6 +163,10 @@ pub use { }; impl Sysvar for EpochRewards { + // SAFETY: upstream invariant: the sysvar data is created exclusively + // by the Solana runtime and serializes bool as 0x00 or 0x01, so the final + // `bool` field of `EpochRewards` can be re-aligned with padding and read + // directly without validation. impl_sysvar_get!(id(), 15); } diff --git a/sysvar/src/epoch_schedule.rs b/sysvar/src/epoch_schedule.rs index 8f6f108ce..70cfb33f6 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -170,12 +170,10 @@ impl PodEpochSchedule { u64::from_le_bytes(self.leader_schedule_slot_offset) } - pub fn warmup(&self) -> Result { - match self.warmup { - 0 => Ok(false), - 1 => Ok(true), - _ => Err(ProgramError::InvalidAccountData), - } + pub fn warmup(&self) -> bool { + // SAFETY: upstream invariant: the sysvar data is created exclusively + // by the Solana runtime and serializes bool as 0x00 or 0x01. + self.warmup > 0 } pub fn first_normal_epoch(&self) -> u64 { @@ -194,7 +192,7 @@ impl TryFrom for EpochSchedule { Ok(Self { slots_per_epoch: pod.slots_per_epoch(), leader_schedule_slot_offset: pod.leader_schedule_slot_offset(), - warmup: pod.warmup()?, + warmup: pod.warmup(), first_normal_epoch: pod.first_normal_epoch(), first_normal_slot: pod.first_normal_slot(), }) From 190e5428418609531124058157fbc40a73a05a7c Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Fri, 5 Dec 2025 10:33:19 +0000 Subject: [PATCH 24/26] allow depr in test --- sysvar/src/rent.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/sysvar/src/rent.rs b/sysvar/src/rent.rs index 72809c5ff..a994dfaf0 100644 --- a/sysvar/src/rent.rs +++ b/sysvar/src/rent.rs @@ -143,6 +143,7 @@ mod tests { #[test] #[serial] #[cfg(feature = "bincode")] + #[allow(deprecated)] fn test_rent_get() { let expected = Rent { lamports_per_byte_year: 123, From 372c65be2d9cb650cf7e12112311fc849169ee59 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:57:06 +0000 Subject: [PATCH 25/26] now unneeded TryFrom -> From --- sysvar/src/epoch_schedule.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/sysvar/src/epoch_schedule.rs b/sysvar/src/epoch_schedule.rs index 70cfb33f6..2dca37de5 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -119,9 +119,9 @@ //! # //! # Ok::<(), anyhow::Error>(()) //! ``` +use crate::Sysvar; #[cfg(feature = "bincode")] use crate::SysvarSerialize; -use {crate::Sysvar, solana_program_error::ProgramError}; pub use { solana_epoch_schedule::EpochSchedule, solana_sdk_ids::sysvar::epoch_schedule::{check_id, id, ID}, @@ -185,23 +185,21 @@ impl PodEpochSchedule { } } -impl TryFrom for EpochSchedule { - type Error = ProgramError; - - fn try_from(pod: PodEpochSchedule) -> Result { - Ok(Self { +impl From for EpochSchedule { + fn from(pod: PodEpochSchedule) -> Self { + Self { slots_per_epoch: pod.slots_per_epoch(), leader_schedule_slot_offset: pod.leader_schedule_slot_offset(), warmup: pod.warmup(), first_normal_epoch: pod.first_normal_epoch(), first_normal_slot: pod.first_normal_slot(), - }) + } } } impl Sysvar for EpochSchedule { fn get() -> Result { - PodEpochSchedule::fetch()?.try_into() + Ok(PodEpochSchedule::fetch()?.into()) } } @@ -222,7 +220,7 @@ mod tests { first_normal_slot: 524256u64.to_le_bytes(), }; - let epoch_schedule = EpochSchedule::try_from(pod).unwrap(); + let epoch_schedule = EpochSchedule::from(pod); assert_eq!(epoch_schedule.slots_per_epoch, 432000); assert_eq!(epoch_schedule.leader_schedule_slot_offset, 432000); From 4f49270ead163816115e5b30ddae47f47ac8a207 Mon Sep 17 00:00:00 2001 From: rustopian <96253492+rustopian@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:59:24 +0000 Subject: [PATCH 26/26] rm now unused fetch_pod --- sysvar/src/lib.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/sysvar/src/lib.rs b/sysvar/src/lib.rs index ff3cc3405..e9288281d 100644 --- a/sysvar/src/lib.rs +++ b/sysvar/src/lib.rs @@ -248,25 +248,6 @@ pub fn get_sysvar( } } -/// Generic helper to fetch a Pod sysvar type. -/// -/// Loads the sysvar data directly into an uninitialized Pod struct and returns it. -/// -/// # Safety -/// Caller must ensure that `size` is correct for `P`. -pub unsafe fn fetch_pod

(sysvar_id: &Pubkey, size: usize) -> Result { - let mut pod = core::mem::MaybeUninit::

::uninit(); - // SAFETY: `get_sysvar_unchecked` will initialize `pod` with the sysvar data, - // or return an error if unsuccessful. - get_sysvar_unchecked( - pod.as_mut_ptr() as *mut u8, - sysvar_id as *const _ as *const u8, - 0, - size as u64, - )?; - Ok(pod.assume_init()) -} - /// Internal helper for retrieving sysvar data directly into a raw buffer. /// /// # Safety