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 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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;