diff --git a/docs/src/developing/runtime-facilities/sysvars.md b/docs/src/developing/runtime-facilities/sysvars.md index 3b5ed443ae..97927a2e92 100644 --- a/docs/src/developing/runtime-facilities/sysvars.md +++ b/docs/src/developing/runtime-facilities/sysvars.md @@ -22,6 +22,7 @@ The following sysvars support `get`: - EpochSchedule - Fees - Rent +- EpochRewards The second is to pass the sysvar to the program as an account by including its address as one of the accounts in the `Instruction` and then deserializing the data during execution. Access to sysvars accounts is always _readonly_. @@ -140,3 +141,18 @@ and de-activations per epoch. It is updated at the start of every epoch. - Address: `SysvarStakeHistory1111111111111111111111111` - Layout: [StakeHistory](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/stake_history/struct.StakeHistory.html) + +## EpochRewards + +The EpochRewards sysvar tracks the progress of epoch rewards distribution. The +sysvar is created in the first block of the epoch, and lasts for several blocks +while paying out the rewards. When all rewards have been distributed, the sysvar +is deleted. Unlike other sysvars, which almost always exist on-chain, +EpochRewards sysvar only exists during the reward period. Therefore, calling +`EpochRewards::get()` on blocks that are outside of the reward period will +return an error, i.e. `UnsupportedSysvar`. This can serve as a method for +determining whether epoch rewards distribution has finished. + +- Address: `SysvarEpochRewards1111111111111111111111111` +- Layout: + [EpochRewards](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/epoch_rewards/struct.EpochRewards.html) \ No newline at end of file diff --git a/sdk/program/src/epoch_rewards.rs b/sdk/program/src/epoch_rewards.rs new file mode 100644 index 0000000000..015dd97ec3 --- /dev/null +++ b/sdk/program/src/epoch_rewards.rs @@ -0,0 +1,75 @@ +//! A type to hold data for the [`EpochRewards` sysvar][sv]. +//! +//! [sv]: https://docs.solana.com/developing/runtime-facilities/sysvars#EpochRewards +//! +//! The sysvar ID is declared in [`sysvar::epoch_rewards`]. +//! +//! [`sysvar::epoch_rewards`]: crate::sysvar::epoch_rewards + +use std::ops::AddAssign; +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default, Clone, Copy, AbiExample)] +pub struct EpochRewards { + /// total rewards for the current epoch, in lamports + pub total_rewards: u64, + + /// distributed rewards for the current epoch, in lamports + pub distributed_rewards: u64, + + /// distribution of all staking rewards for the current + /// epoch will be completed at this block height + pub distribution_complete_block_height: u64, +} + +impl EpochRewards { + pub fn distribute(&mut self, amount: u64) { + assert!(self.distributed_rewards.saturating_add(amount) <= self.total_rewards); + + self.distributed_rewards.add_assign(amount); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + impl EpochRewards { + pub fn new( + total_rewards: u64, + distributed_rewards: u64, + distribution_complete_block_height: u64, + ) -> Self { + Self { + total_rewards, + distributed_rewards, + distribution_complete_block_height, + } + } + } + + #[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_complete_block_height, 64); + } + + #[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); + } + + #[test] + #[should_panic( + expected = "assertion failed: self.distributed_rewards.saturating_add(amount) <= self.total_rewards" + )] + fn test_epoch_rewards_distribute_panic() { + let mut epoch_rewards = EpochRewards::new(100, 0, 64); + epoch_rewards.distribute(200); + } +} diff --git a/sdk/program/src/lib.rs b/sdk/program/src/lib.rs index e8a62ffbee..2bd93bae68 100644 --- a/sdk/program/src/lib.rs +++ b/sdk/program/src/lib.rs @@ -486,6 +486,7 @@ pub mod decode_error; pub mod ed25519_program; pub mod entrypoint; pub mod entrypoint_deprecated; +pub mod epoch_rewards; pub mod epoch_schedule; pub mod feature; pub mod fee_calculator; diff --git a/sdk/program/src/program_stubs.rs b/sdk/program/src/program_stubs.rs index 682be12ad2..e475882019 100644 --- a/sdk/program/src/program_stubs.rs +++ b/sdk/program/src/program_stubs.rs @@ -51,6 +51,10 @@ pub trait SyscallStubs: Sync + Send { fn sol_get_rent_sysvar(&self, _var_addr: *mut u8) -> u64 { UNSUPPORTED_SYSVAR } + fn sol_get_epoch_rewards_sysvar(&self, _var_addr: *mut u8) -> u64 { + UNSUPPORTED_SYSVAR + } + /// # Safety unsafe fn sol_memcpy(&self, dst: *mut u8, src: *const u8, n: usize) { // cannot be overlapping @@ -197,6 +201,13 @@ pub(crate) 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) +} + /// Check that two regions do not overlap. /// /// Hidden to share with bpf_loader without being part of the API surface. diff --git a/sdk/program/src/syscalls/definitions.rs b/sdk/program/src/syscalls/definitions.rs index 2926ab584a..d9292108b1 100644 --- a/sdk/program/src/syscalls/definitions.rs +++ b/sdk/program/src/syscalls/definitions.rs @@ -67,6 +67,7 @@ define_syscall!(fn sol_curve_multiscalar_mul(curve_id: u64, scalars_addr: *const define_syscall!(fn sol_curve_pairing_map(curve_id: u64, point: *const u8, result: *mut u8) -> u64); define_syscall!(fn sol_alt_bn128_group_op(group_op: u64, input: *const u8, input_size: u64, result: *mut u8) -> u64); define_syscall!(fn sol_big_mod_exp(params: *const u8, result: *mut u8) -> u64); +define_syscall!(fn sol_get_epoch_rewards_sysvar(addr: *mut u8) -> u64); #[cfg(target_feature = "static-syscalls")] pub const fn sys_hash(name: &str) -> usize { diff --git a/sdk/program/src/sysvar/epoch_rewards.rs b/sdk/program/src/sysvar/epoch_rewards.rs new file mode 100755 index 0000000000..1c588d052b --- /dev/null +++ b/sdk/program/src/sysvar/epoch_rewards.rs @@ -0,0 +1,139 @@ +//! Epoch rewards for current epoch +//! +//! The _epoch rewards_ sysvar provides access to the [`EpochRewards`] type, +//! which tracks the progress of epoch rewards distribution. It includes the +//! - total rewards for the current epoch, in lamports +//! - rewards for the current epoch distributed so far, in lamports +//! - distribution completed block height, i.e. distribution of all staking rewards for the current +//! epoch will be completed at this block height +//! +//! [`EpochRewards`] implements [`Sysvar::get`] and can be loaded efficiently without +//! passing the sysvar account ID to the program. +//! +//! See also the Solana [documentation on the epoch rewards sysvar][sdoc]. +//! +//! [sdoc]: https://docs.solana.com/developing/runtime-facilities/sysvars#epochrewards +//! +//! # Examples +//! +//! Accessing via on-chain program directly: +//! +//! ```no_run +//! # use solana_program::{ +//! # account_info::{AccountInfo, next_account_info}, +//! # entrypoint::ProgramResult, +//! # msg, +//! # program_error::ProgramError, +//! # pubkey::Pubkey, +//! # sysvar::epoch_rewards::{self, EpochRewards}, +//! # sysvar::Sysvar, +//! # }; +//! # +//! fn process_instruction( +//! program_id: &Pubkey, +//! accounts: &[AccountInfo], +//! instruction_data: &[u8], +//! ) -> ProgramResult { +//! +//! let epoch_rewards = EpochRewards::get()?; +//! msg!("epoch_rewards: {:#?}", epoch_rewards); +//! +//! Ok(()) +//! } +//! # +//! # use solana_program::sysvar::SysvarId; +//! # let p = EpochRewards::id(); +//! # let l = &mut 1120560; +//! # let d = &mut vec![0, 202, 154, 59, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0]; +//! # let a = AccountInfo::new(&p, false, false, l, d, &p, false, 0); +//! # let accounts = &[a.clone(), a]; +//! # process_instruction( +//! # &Pubkey::new_unique(), +//! # accounts, +//! # &[], +//! # )?; +//! # Ok::<(), ProgramError>(()) +//! ``` +//! +//! Accessing via on-chain program's account parameters: +//! +//! ``` +//! # use solana_program::{ +//! # account_info::{AccountInfo, next_account_info}, +//! # entrypoint::ProgramResult, +//! # msg, +//! # pubkey::Pubkey, +//! # sysvar::epoch_rewards::{self, EpochRewards}, +//! # sysvar::Sysvar, +//! # }; +//! # use solana_program::program_error::ProgramError; +//! # +//! fn process_instruction( +//! program_id: &Pubkey, +//! accounts: &[AccountInfo], +//! instruction_data: &[u8], +//! ) -> ProgramResult { +//! let account_info_iter = &mut accounts.iter(); +//! let epoch_rewards_account_info = next_account_info(account_info_iter)?; +//! +//! assert!(epoch_rewards::check_id(epoch_rewards_account_info.key)); +//! +//! let epoch_rewards = EpochRewards::from_account_info(epoch_rewards_account_info)?; +//! msg!("epoch_rewards: {:#?}", epoch_rewards); +//! +//! Ok(()) +//! } +//! # +//! # use solana_program::sysvar::SysvarId; +//! # let p = EpochRewards::id(); +//! # let l = &mut 1120560; +//! # let d = &mut vec![0, 202, 154, 59, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0]; +//! # let a = AccountInfo::new(&p, false, false, l, d, &p, false, 0); +//! # let accounts = &[a.clone(), a]; +//! # process_instruction( +//! # &Pubkey::new_unique(), +//! # accounts, +//! # &[], +//! # )?; +//! # Ok::<(), ProgramError>(()) +//! ``` +//! +//! Accessing via the RPC client: +//! +//! ``` +//! # use solana_program::example_mocks::solana_sdk; +//! # use solana_program::example_mocks::solana_rpc_client; +//! # use solana_sdk::account::Account; +//! # use solana_rpc_client::rpc_client::RpcClient; +//! # use solana_sdk::sysvar::epoch_rewards::{self, EpochRewards}; +//! # use anyhow::Result; +//! # +//! fn print_sysvar_epoch_rewards(client: &RpcClient) -> Result<()> { +//! # client.set_get_account_response(epoch_rewards::ID, Account { +//! # lamports: 1120560, +//! # data: vec![0, 202, 154, 59, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0], +//! # owner: solana_sdk::system_program::ID, +//! # executable: false, +//! # rent_epoch: 307, +//! # }); +//! # +//! let epoch_rewards = client.get_account(&epoch_rewards::ID)?; +//! let data: EpochRewards = bincode::deserialize(&epoch_rewards.data)?; +//! +//! Ok(()) +//! } +//! # +//! # let client = RpcClient::new(String::new()); +//! # print_sysvar_epoch_rewards(&client)?; +//! # +//! # Ok::<(), anyhow::Error>(()) +//! ``` + +pub use crate::epoch_rewards::EpochRewards; +use crate::{impl_sysvar_get, program_error::ProgramError, sysvar::Sysvar}; + +crate::declare_sysvar_id!("SysvarEpochRewards1111111111111111111111111", EpochRewards); + +impl Sysvar for EpochRewards { + impl_sysvar_get!(sol_get_epoch_rewards_sysvar); +} diff --git a/sdk/program/src/sysvar/mod.rs b/sdk/program/src/sysvar/mod.rs index 8ed8bfa3da..01376004c9 100644 --- a/sdk/program/src/sysvar/mod.rs +++ b/sdk/program/src/sysvar/mod.rs @@ -87,6 +87,7 @@ use { }; pub mod clock; +pub mod epoch_rewards; pub mod epoch_schedule; pub mod fees; pub mod instructions; @@ -111,6 +112,7 @@ lazy_static! { slot_history::id(), stake_history::id(), instructions::id(), + epoch_rewards::id(), ]; }