From 52e97187946be6dcdc25d05ab4874ffe7bfbfb5b Mon Sep 17 00:00:00 2001 From: buto Date: Thu, 29 Feb 2024 18:08:39 +0200 Subject: [PATCH] feat: add epoch start timestamp (#23) ### Context: This PR introduces `epoch_start_timestamp` within `ClusterHistoryEntry` to enhance APY tracking, as detailed in https://github.com/jito-foundation/stakenet/issues/18 The `epoch_start_timestamp` is assigned during the execution of the `copy_cluster_info` ix for the current epoch ### Tests: A new function has been added to modify the Clock sysvar. This was necessary because `advance_num_epochs` did not update the Clock sysvar during tests ### Dependencies This PR depends on the successful merge of https://github.com/jito-foundation/stakenet/pull/22. After merge, I will update this accordingly. --- .../idl/validator_history.json | 6 +- .../src/instructions/copy_cluster_info.rs | 17 ++- programs/validator-history/src/state.rs | 38 +++++-- programs/validator-history/src/utils.rs | 10 +- tests/src/fixtures.rs | 57 +++++++--- tests/tests/test_cluster_history.rs | 104 +++++++++++++----- 6 files changed, 165 insertions(+), 67 deletions(-) diff --git a/programs/validator-history/idl/validator_history.json b/programs/validator-history/idl/validator_history.json index 99c4f04a..b06cfd67 100644 --- a/programs/validator-history/idl/validator_history.json +++ b/programs/validator-history/idl/validator_history.json @@ -649,6 +649,10 @@ "name": "totalBlocks", "type": "u32" }, + { + "name": "epochStartTimestamp", + "type": "u32" + }, { "name": "epoch", "type": "u16" @@ -658,7 +662,7 @@ "type": { "array": [ "u8", - 250 + 246 ] } } diff --git a/programs/validator-history/src/instructions/copy_cluster_info.rs b/programs/validator-history/src/instructions/copy_cluster_info.rs index 40c4b8ef..3389412f 100644 --- a/programs/validator-history/src/instructions/copy_cluster_info.rs +++ b/programs/validator-history/src/instructions/copy_cluster_info.rs @@ -1,10 +1,15 @@ -use anchor_lang::{ - prelude::*, - solana_program::{clock::Clock, slot_history::Check}, +use { + crate::{ + errors::ValidatorHistoryError, + utils::{cast_epoch, cast_epoch_start_timestamp}, + ClusterHistory, + }, + anchor_lang::{ + prelude::*, + solana_program::{clock::Clock, slot_history::Check}, + }, }; -use crate::{errors::ValidatorHistoryError, utils::cast_epoch, ClusterHistory}; - #[derive(Accounts)] pub struct CopyClusterInfo<'info> { #[account( @@ -29,12 +34,14 @@ pub fn handle_copy_cluster_info(ctx: Context) -> Result<()> { let epoch = cast_epoch(clock.epoch); + let epoch_start_timestamp = cast_epoch_start_timestamp(clock.epoch_start_timestamp); // Sets the slot history for the previous epoch, since the current epoch is not yet complete. if epoch > 0 { cluster_history_account .set_blocks(epoch - 1, blocks_in_epoch(epoch - 1, &slot_history)?)?; } cluster_history_account.set_blocks(epoch, blocks_in_epoch(epoch, &slot_history)?)?; + cluster_history_account.set_epoch_start_timestamp(epoch, epoch_start_timestamp)?; cluster_history_account.cluster_history_last_update_slot = clock.slot; diff --git a/programs/validator-history/src/state.rs b/programs/validator-history/src/state.rs index 5ad2fcfe..0ae26d00 100644 --- a/programs/validator-history/src/state.rs +++ b/programs/validator-history/src/state.rs @@ -1,13 +1,13 @@ -use std::{cmp::Ordering, collections::HashMap, mem::size_of, net::IpAddr}; - -use anchor_lang::prelude::*; -use borsh::{BorshDeserialize, BorshSerialize}; -use type_layout::TypeLayout; - -use crate::{ - crds_value::{ContactInfo, LegacyContactInfo, LegacyVersion, Version2}, - errors::ValidatorHistoryError, - utils::cast_epoch, +use { + crate::{ + crds_value::{ContactInfo, LegacyContactInfo, LegacyVersion, Version2}, + errors::ValidatorHistoryError, + utils::cast_epoch, + }, + anchor_lang::prelude::*, + borsh::{BorshDeserialize, BorshSerialize}, + std::{cmp::Ordering, collections::HashMap, mem::size_of, net::IpAddr}, + type_layout::TypeLayout, }; static_assertions::const_assert_eq!(size_of::(), 104); @@ -654,8 +654,9 @@ pub struct ClusterHistory { #[zero_copy] pub struct ClusterHistoryEntry { pub total_blocks: u32, + pub epoch_start_timestamp: u32, pub epoch: u16, - pub padding: [u8; 250], + pub padding: [u8; 246], } impl Default for ClusterHistoryEntry { @@ -663,7 +664,8 @@ impl Default for ClusterHistoryEntry { Self { total_blocks: u32::MAX, epoch: u16::MAX, - padding: [u8::MAX; 250], + epoch_start_timestamp: u32::MAX, + padding: [u8::MAX; 246], } } } @@ -793,6 +795,18 @@ impl ClusterHistory { Ok(()) } + pub fn set_epoch_start_timestamp( + &mut self, + epoch: u16, + epoch_start_timestamp: u32, + ) -> Result<()> { + if let Some(entry) = self.history.last_mut() { + if entry.epoch == epoch { + entry.epoch_start_timestamp = epoch_start_timestamp; + } + } + Ok(()) + } } #[cfg(test)] diff --git a/programs/validator-history/src/utils.rs b/programs/validator-history/src/utils.rs index cb21e086..a4bc34fc 100644 --- a/programs/validator-history/src/utils.rs +++ b/programs/validator-history/src/utils.rs @@ -1,10 +1,16 @@ -use anchor_lang::prelude::{AccountInfo, Pubkey}; -use anchor_lang::solana_program::native_token::lamports_to_sol; +use anchor_lang::{ + prelude::{AccountInfo, Pubkey}, + solana_program::native_token::lamports_to_sol, +}; pub fn cast_epoch(epoch: u64) -> u16 { (epoch % u16::MAX as u64).try_into().unwrap() } +pub fn cast_epoch_start_timestamp(start_timestamp: i64) -> u32 { + start_timestamp.try_into().unwrap() +} + pub fn fixed_point_sol(lamports: u64) -> u32 { // convert to sol let mut sol = lamports_to_sol(lamports); diff --git a/tests/src/fixtures.rs b/tests/src/fixtures.rs index 5cf2c054..2c13358e 100644 --- a/tests/src/fixtures.rs +++ b/tests/src/fixtures.rs @@ -1,24 +1,25 @@ #![allow(clippy::await_holding_refcell_ref)] -use anchor_lang::{ - solana_program::{ - clock::Clock, - pubkey::Pubkey, - vote::state::{VoteInit, VoteState, VoteStateVersions}, +use { + anchor_lang::{ + solana_program::{ + clock::Clock, + pubkey::Pubkey, + vote::state::{VoteInit, VoteState, VoteStateVersions}, + }, + AccountSerialize, InstructionData, ToAccountMetas, }, - AccountSerialize, InstructionData, ToAccountMetas, -}; -use solana_program_test::*; -use solana_sdk::{ - account::Account, epoch_schedule::EpochSchedule, instruction::Instruction, signature::Keypair, - signer::Signer, transaction::Transaction, -}; -use std::{cell::RefCell, rc::Rc}; - -use jito_tip_distribution::{ - sdk::derive_tip_distribution_account_address, - state::{MerkleRoot, TipDistributionAccount}, + jito_tip_distribution::{ + sdk::derive_tip_distribution_account_address, + state::{MerkleRoot, TipDistributionAccount}, + }, + solana_program_test::*, + solana_sdk::{ + account::Account, epoch_schedule::EpochSchedule, instruction::Instruction, + signature::Keypair, signer::Signer, transaction::Transaction, + }, + std::{cell::RefCell, rc::Rc}, + validator_history::{self, constants::MAX_ALLOC_BYTES, ClusterHistory, ValidatorHistory}, }; -use validator_history::{self, constants::MAX_ALLOC_BYTES, ClusterHistory, ValidatorHistory}; pub struct TestFixture { pub ctx: Rc>, @@ -248,6 +249,26 @@ impl TestFixture { .expect("Failed warping to future epoch"); } + pub async fn advance_clock(&self, num_epochs: u64, ms_per_slot: u64) -> u64 { + let mut clock: Clock = self + .ctx + .borrow_mut() + .banks_client + .get_sysvar() + .await + .expect("Failed getting clock"); + + let epoch_schedule: EpochSchedule = self.ctx.borrow().genesis_config().epoch_schedule; + let target_epoch = clock.epoch + num_epochs; + let dif_slots = epoch_schedule.get_first_slot_in_epoch(target_epoch) - clock.slot; + + clock.epoch_start_timestamp += (dif_slots * ms_per_slot) as i64; + clock.unix_timestamp += (dif_slots * ms_per_slot) as i64; + self.ctx.borrow_mut().set_sysvar(&clock); + + dif_slots + } + pub async fn submit_transaction_assert_success(&self, transaction: Transaction) { let mut ctx = self.ctx.borrow_mut(); if let Err(e) = ctx diff --git a/tests/tests/test_cluster_history.rs b/tests/tests/test_cluster_history.rs index c0054db2..e696357d 100644 --- a/tests/tests/test_cluster_history.rs +++ b/tests/tests/test_cluster_history.rs @@ -1,15 +1,41 @@ #![allow(clippy::await_holding_refcell_ref)] -use anchor_lang::{ - solana_program::{instruction::Instruction, slot_history::SlotHistory}, - InstructionData, ToAccountMetas, +use { + anchor_lang::{ + solana_program::{instruction::Instruction, slot_history::SlotHistory}, + InstructionData, ToAccountMetas, + }, + solana_program_test::*, + solana_sdk::{ + clock::Clock, compute_budget::ComputeBudgetInstruction, signer::Signer, + transaction::Transaction, + }, + tests::fixtures::TestFixture, + validator_history::ClusterHistory, }; -use solana_program_test::*; -use solana_sdk::{ - clock::Clock, compute_budget::ComputeBudgetInstruction, signer::Signer, - transaction::Transaction, -}; -use tests::fixtures::TestFixture; -use validator_history::ClusterHistory; + +const MS_PER_SLOT: u64 = 400; + +fn create_copy_cluster_history_transaction(fixture: &TestFixture) -> Transaction { + let instruction = Instruction { + program_id: validator_history::id(), + data: validator_history::instruction::CopyClusterInfo {}.data(), + accounts: validator_history::accounts::CopyClusterInfo { + cluster_history_account: fixture.cluster_history_account, + slot_history: anchor_lang::solana_program::sysvar::slot_history::id(), + signer: fixture.keypair.pubkey(), + } + .to_account_metas(None), + }; + let heap_request_ix = ComputeBudgetInstruction::request_heap_frame(256 * 1024); + let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_limit(300_000); + + Transaction::new_signed_with_payer( + &[heap_request_ix, compute_budget_ix, instruction], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + fixture.ctx.borrow().last_blockhash, + ) +} #[tokio::test] async fn test_copy_cluster_info() { @@ -32,25 +58,7 @@ async fn test_copy_cluster_info() { slot_history.add(latest_slot + 1); // Submit instruction - let instruction = Instruction { - program_id: validator_history::id(), - data: validator_history::instruction::CopyClusterInfo {}.data(), - accounts: validator_history::accounts::CopyClusterInfo { - cluster_history_account: fixture.cluster_history_account, - slot_history: anchor_lang::solana_program::sysvar::slot_history::id(), - signer: fixture.keypair.pubkey(), - } - .to_account_metas(None), - }; - let heap_request_ix = ComputeBudgetInstruction::request_heap_frame(256 * 1024); - let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_limit(300_000); - - let transaction = Transaction::new_signed_with_payer( - &[heap_request_ix, compute_budget_ix, instruction], - Some(&fixture.keypair.pubkey()), - &[&fixture.keypair], - ctx.borrow().last_blockhash, - ); + let transaction = create_copy_cluster_history_transaction(&fixture); ctx.borrow_mut().set_sysvar(&slot_history); fixture.submit_transaction_assert_success(transaction).await; @@ -74,3 +82,41 @@ async fn test_copy_cluster_info() { assert!(account.history.arr[1].total_blocks == 2); assert_eq!(account.cluster_history_last_update_slot, latest_slot) } + +#[tokio::test] +async fn test_start_epoch_timestamp() { + // Initialize + let fixture = TestFixture::new().await; + let ctx = &fixture.ctx; + fixture.initialize_config().await; + fixture.initialize_cluster_history_account().await; + + // Set SlotHistory sysvar + let slot_history = SlotHistory::default(); + ctx.borrow_mut().set_sysvar(&slot_history); + + // Submit epoch 0 instruction + let transaction = create_copy_cluster_history_transaction(&fixture); + fixture.submit_transaction_assert_success(transaction).await; + + // Change epoch and set clock timestamps in the future + fixture.advance_num_epochs(1).await; + let dif_slots = fixture.advance_clock(1, MS_PER_SLOT).await; + + // Submit epoch 1 instruction + let transaction = create_copy_cluster_history_transaction(&fixture); + fixture.submit_transaction_assert_success(transaction).await; + + let account: ClusterHistory = fixture + .load_and_deserialize(&fixture.cluster_history_account) + .await; + + assert_eq!(account.history.arr[0].epoch, 0); + assert_eq!(account.history.arr[1].epoch, 1); + assert_ne!(account.history.arr[0].epoch_start_timestamp, u32::MAX); + assert_ne!(account.history.arr[1].epoch_start_timestamp, u32::MAX); + assert_eq!( + account.history.arr[0].epoch_start_timestamp, + account.history.arr[1].epoch_start_timestamp - (dif_slots * MS_PER_SLOT) as u32 + ); +}