diff --git a/define-syscall/src/definitions.rs b/define-syscall/src/definitions.rs index 76e91df5c..da614c6e0 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 = "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/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/epoch-rewards/src/lib.rs b/epoch-rewards/src/lib.rs index 5d581891a..e6d00693d 100644 --- a/epoch-rewards/src/lib.rs +++ b/epoch-rewards/src/lib.rs @@ -54,7 +54,53 @@ pub struct EpochRewards { pub active: bool, } +/// Pod (Plain Old Data) representation of [`EpochRewards`] with no padding. +/// +/// This type can be safely loaded via `sol_get_sysvar` without undefined behavior. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PodEpochRewards { + pub distribution_starting_block_height: [u8; 8], + pub num_partitions: [u8; 8], + pub parent_blockhash: [u8; 32], + pub total_points: [u8; 16], + pub total_rewards: [u8; 8], + pub distributed_rewards: [u8; 8], + pub active: u8, +} + +const _: () = assert!(core::mem::size_of::() == 81); + +impl From for EpochRewards { + fn from(pod: PodEpochRewards) -> Self { + Self { + distribution_starting_block_height: u64::from_le_bytes( + pod.distribution_starting_block_height, + ), + num_partitions: u64::from_le_bytes(pod.num_partitions), + parent_blockhash: Hash::new_from_array(pod.parent_blockhash), + total_points: u128::from_le_bytes(pod.total_points), + total_rewards: u64::from_le_bytes(pod.total_rewards), + distributed_rewards: u64::from_le_bytes(pod.distributed_rewards), + active: pod.active != 0, + } + } +} + 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); @@ -66,30 +112,6 @@ 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 a1ed5c16a..d212504e6 100644 --- a/epoch-schedule/src/lib.rs +++ b/epoch-schedule/src/lib.rs @@ -76,6 +76,33 @@ pub struct EpochSchedule { pub first_normal_slot: u64, } +/// Pod (Plain Old Data) representation of [`EpochSchedule`] with no padding. +/// +/// This type can be safely loaded via `sol_get_sysvar` without undefined behavior. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PodEpochSchedule { + pub slots_per_epoch: [u8; 8], + pub leader_schedule_slot_offset: [u8; 8], + pub warmup: u8, + pub first_normal_epoch: [u8; 8], + pub first_normal_slot: [u8; 8], +} + +const _: () = assert!(core::mem::size_of::() == 33); + +impl From for EpochSchedule { + fn from(pod: PodEpochSchedule) -> Self { + Self { + slots_per_epoch: u64::from_le_bytes(pod.slots_per_epoch), + leader_schedule_slot_offset: u64::from_le_bytes(pod.leader_schedule_slot_offset), + warmup: pod.warmup != 0, + first_normal_epoch: u64::from_le_bytes(pod.first_normal_epoch), + first_normal_slot: u64::from_le_bytes(pod.first_normal_slot), + } + } +} + impl Default for EpochSchedule { fn default() -> Self { Self::custom( @@ -90,6 +117,7 @@ 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, @@ -97,6 +125,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 { diff --git a/rent/src/lib.rs b/rent/src/lib.rs index 8a70d2b99..b0785009d 100644 --- a/rent/src/lib.rs +++ b/rent/src/lib.rs @@ -81,6 +81,23 @@ 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 @@ -135,11 +152,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,11 +212,7 @@ 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), @@ -236,11 +249,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/clock.rs b/sysvar/src/clock.rs index bde88bab2..dadb6335f 100644 --- a/sysvar/src/clock.rs +++ b/sysvar/src/clock.rs @@ -130,8 +130,64 @@ 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] + #[cfg(feature = "bincode")] + fn test_clock_size_matches_bincode() { + // 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; + + assert_eq!( + in_memory_size, bincode_size, + "Clock in-memory size ({in_memory_size}) must match bincode size ({bincode_size})", + ); + } + + #[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); + } + + #[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::tests::mock_get_sysvar_syscall_with_id(&data, &id()); + + 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..2401a5235 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, @@ -154,17 +136,55 @@ //! # 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_epoch_rewards::{EpochRewards, PodEpochRewards}, solana_sdk_ids::sysvar::epoch_rewards::{check_id, id, ID}, }; impl Sysvar for EpochRewards { - impl_sysvar_get!(sol_get_epoch_rewards_sysvar); + fn get() -> 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(Self::from(pod.assume_init())) + } + } } #[cfg(feature = "bincode")] impl SysvarSerialize for EpochRewards {} + +#[cfg(test)] +mod tests { + use {super::*, crate::Sysvar, serial_test::serial}; + + #[test] + #[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 data = bincode::serialize(&expected).unwrap(); + assert_eq!(data.len(), 81); + + 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 d189434a3..9a33b70c0 100644 --- a/sysvar/src/epoch_schedule.rs +++ b/sysvar/src/epoch_schedule.rs @@ -119,17 +119,46 @@ //! # //! # 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_epoch_schedule::{EpochSchedule, PodEpochSchedule}, solana_sdk_ids::sysvar::epoch_schedule::{check_id, id, ID}, }; impl Sysvar for EpochSchedule { - impl_sysvar_get!(sol_get_epoch_schedule_sysvar); + fn get() -> 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(Self::from(pod.assume_init())) + } + } } #[cfg(feature = "bincode")] impl SysvarSerialize for EpochSchedule {} + +#[cfg(test)] +mod tests { + use {super::*, crate::Sysvar, serial_test::serial}; + + #[test] + #[serial] + #[cfg(feature = "bincode")] + fn test_epoch_schedule_get() { + let expected = EpochSchedule::custom(1234, 5678, false); + let data = bincode::serialize(&expected).unwrap(); + assert_eq!(data.len(), 33); + + crate::tests::mock_get_sysvar_syscall(&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..5689ddfb5 100644 --- a/sysvar/src/last_restart_slot.rs +++ b/sysvar/src/last_restart_slot.rs @@ -45,8 +45,40 @@ 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] + #[cfg(feature = "bincode")] + fn test_last_restart_slot_size_matches_bincode() { + // 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; + + assert_eq!( + in_memory_size, bincode_size, + "LastRestartSlot in-memory size ({in_memory_size}) must match bincode size ({bincode_size})", + ); + } + + #[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..abb3cd77b 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(); @@ -174,11 +177,26 @@ 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 { + let mut uninit = core::mem::MaybeUninit::::uninit(); + let size = core::mem::size_of::() as u64; + 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()) + } + } + }; } /// Handler for retrieving a slice of sysvar data from the `sol_get_sysvar` @@ -206,6 +224,35 @@ pub fn get_sysvar( #[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), + _ => Err(solana_program_error::ProgramError::UnsupportedSysvar), + } +} + +/// 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), @@ -245,10 +292,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( @@ -263,12 +313,65 @@ 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 + /// + /// 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 { + 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..e6fc77fee 100644 --- a/sysvar/src/rent.rs +++ b/sysvar/src/rent.rs @@ -121,16 +121,48 @@ //! # //! # 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!(sol_get_rent_sysvar); + 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")] impl SysvarSerialize for Rent {} + +#[cfg(test)] +mod tests { + use {super::*, crate::Sysvar, serial_test::serial}; + + #[test] + #[serial] + #[cfg(feature = "bincode")] + fn test_rent_get() { + 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); + + crate::tests::mock_get_sysvar_syscall(&data); + let got = Rent::get().unwrap(); + assert_eq!(got, expected); + } +}