diff --git a/programs/validator-history/idl/validator_history.json b/programs/validator-history/idl/validator_history.json index 82d5f431..0b8bf879 100644 --- a/programs/validator-history/idl/validator_history.json +++ b/programs/validator-history/idl/validator_history.json @@ -903,7 +903,7 @@ { "code": 6001, "name": "InvalidEpochCredits", - "msg": "Invalid epoch credits, credits must be greater than previous credits" + "msg": "Invalid epoch credits, credits must exist and each value must be greater than previous credits" }, { "code": 6002, @@ -928,7 +928,7 @@ { "code": 6006, "name": "NotEnoughVotingHistory", - "msg": "Not enough voting history to create account. Minimum 10 epochs required" + "msg": "Not enough voting history to create account. Minimum 5 epochs required" }, { "code": 6007, @@ -954,6 +954,11 @@ "code": 6011, "name": "EpochTooLarge", "msg": "Epoch larger than 65535, cannot be stored" + }, + { + "code": 6012, + "name": "DuplicateEpoch", + "msg": "Inserting duplicate epoch" } ] } \ No newline at end of file diff --git a/programs/validator-history/src/errors.rs b/programs/validator-history/src/errors.rs index b2679c31..e1cc9bad 100644 --- a/programs/validator-history/src/errors.rs +++ b/programs/validator-history/src/errors.rs @@ -4,7 +4,9 @@ use anchor_lang::prelude::*; pub enum ValidatorHistoryError { #[msg("Account already reached proper size, no more allocations allowed")] AccountFullySized, - #[msg("Invalid epoch credits, credits must be greater than previous credits")] + #[msg( + "Invalid epoch credits, credits must exist and each value must be greater than previous credits" + )] InvalidEpochCredits, #[msg("Epoch is out of range of history")] EpochOutOfRange, @@ -14,7 +16,7 @@ pub enum ValidatorHistoryError { GossipDataInvalid, #[msg("Unsupported IP Format, only IpAddr::V4 is supported")] UnsupportedIpFormat, - #[msg("Not enough voting history to create account. Minimum 10 epochs required")] + #[msg("Not enough voting history to create account. Minimum 5 epochs required")] NotEnoughVotingHistory, #[msg( "Gossip data too old. Data cannot be older than the last recorded timestamp for a field" @@ -28,4 +30,6 @@ pub enum ValidatorHistoryError { SlotHistoryOutOfDate, #[msg("Epoch larger than 65535, cannot be stored")] EpochTooLarge, + #[msg("Inserting duplicate epoch")] + DuplicateEpoch, } diff --git a/programs/validator-history/src/instructions/copy_vote_account.rs b/programs/validator-history/src/instructions/copy_vote_account.rs index 371a1850..4d48b850 100644 --- a/programs/validator-history/src/instructions/copy_vote_account.rs +++ b/programs/validator-history/src/instructions/copy_vote_account.rs @@ -31,6 +31,7 @@ pub fn handle_copy_vote_account(ctx: Context) -> Result<()> { validator_history_account.set_commission_and_slot(epoch, commission, clock.slot)?; let epoch_credits = VoteStateVersions::deserialize_epoch_credits(&ctx.accounts.vote_account)?; + validator_history_account.insert_missing_entries(&epoch_credits)?; validator_history_account.set_epoch_credits(&epoch_credits)?; Ok(()) diff --git a/programs/validator-history/src/state.rs b/programs/validator-history/src/state.rs index 184ba815..f5a3b9ea 100644 --- a/programs/validator-history/src/state.rs +++ b/programs/validator-history/src/state.rs @@ -2,7 +2,7 @@ use { crate::{ crds_value::{ContactInfo, LegacyContactInfo, LegacyVersion, Version2}, errors::ValidatorHistoryError, - utils::cast_epoch, + utils::{cast_epoch, find_insert_position, get_max_epoch, get_min_epoch}, }, anchor_lang::prelude::*, borsh::{BorshDeserialize, BorshSerialize}, @@ -184,8 +184,53 @@ impl CircBuf { &mut self.arr } - /// Returns &ValidatorHistoryEntry for each existing entry in range [start_epoch, end_epoch], factoring for wraparound - /// Returns None if either start_epoch or end_epoch is not in the CircBuf + /// Given a new entry and epoch, inserts the entry into the buffer in sorted order + /// Will not insert if the epoch is out of range or already exists in the buffer + fn insert(&mut self, entry: ValidatorHistoryEntry, epoch: u16) -> Result<()> { + if self.is_empty() { + return Err(ValidatorHistoryError::EpochOutOfRange.into()); + } + + // Find the lowest epoch in the buffer to ensure the new epoch is valid + let min_epoch = { + let next_i = (self.idx as usize + 1) % self.arr.len(); + if self.arr[next_i].epoch == ValidatorHistoryEntry::default().epoch { + self.arr[0].epoch + } else { + self.arr[next_i].epoch + } + }; + + // If epoch is less than min_epoch or greater than max_epoch in the buffer, return error + if epoch < min_epoch || epoch > self.arr[self.idx as usize].epoch { + return Err(ValidatorHistoryError::EpochOutOfRange.into()); + } + + let insert_pos = find_insert_position(&self.arr, self.idx as usize, epoch) + .ok_or(ValidatorHistoryError::DuplicateEpoch)?; + + // If idx < insert_pos, the shifting needs to wrap around + let end_index = if self.idx < insert_pos as u64 { + self.idx as usize + self.arr.len() + } else { + self.idx as usize + }; + + // Shift all elements to the right to make space for the new entry, starting with current idx + for i in (insert_pos..=end_index).rev() { + let i = i % self.arr.len(); + let next_i = (i + 1) % self.arr.len(); + self.arr[next_i] = self.arr[i]; + } + + self.arr[insert_pos] = entry; + + self.idx = (self.idx + 1) % self.arr.len() as u64; + Ok(()) + } + + /// Returns &ValidatorHistoryEntry for each existing entry in range [start_epoch, end_epoch] inclusive, factoring for wraparound + /// Returns None for each epoch that doesn't exist in the CircBuf pub fn epoch_range( &self, start_epoch: u16, @@ -364,6 +409,53 @@ impl ValidatorHistory { Ok(()) } + /// Given epoch credits from the vote account, determines which entries do not exist in the history and inserts them. + /// Shifts all existing entries that come later in the history and evicts the oldest entries if the buffer is full. + /// Skips entries which are not already in the (min_epoch, max_epoch) range of the buffer. + pub fn insert_missing_entries( + &mut self, + epoch_credits: &[( + u64, /* epoch */ + u64, /* epoch cumulative votes */ + u64, /* prev epoch cumulative votes */ + )], + ) -> Result<()> { + // For each epoch in the list, insert a new entry if it doesn't exist + let start_epoch = get_min_epoch(epoch_credits)?; + let end_epoch = get_max_epoch(epoch_credits)?; + + let entries = self + .history + .epoch_range(start_epoch, end_epoch) + .iter() + .map(|entry| entry.is_some()) + .collect::>(); + + let epoch_credits_map: HashMap = + HashMap::from_iter(epoch_credits.iter().map(|(epoch, cur, prev)| { + ( + cast_epoch(*epoch).unwrap(), // all epochs in list will be valid if current epoch is valid + (cur.checked_sub(*prev) + .ok_or(ValidatorHistoryError::InvalidEpochCredits) + .unwrap() as u32), + ) + })); + + for (entry_is_some, epoch) in entries.iter().zip(start_epoch as u16..=end_epoch) { + if !*entry_is_some && epoch_credits_map.contains_key(&epoch) { + // Inserts blank entry that will have credits copied to it later + let entry = ValidatorHistoryEntry { + epoch, + ..ValidatorHistoryEntry::default() + }; + // Skips if epoch is out of range or duplicate + self.history.insert(entry, epoch).unwrap_or_default(); + } + } + + Ok(()) + } + pub fn set_epoch_credits( &mut self, epoch_credits: &[( @@ -387,20 +479,17 @@ impl ValidatorHistory { ) })); - // Traverses entries in reverse order, breaking once we either: - // 1) Start seeing identical epoch credit values - // 2) See an epoch not in validator epoch credits (uninitialized or out of range) + let min_epoch = get_min_epoch(epoch_credits)?; + + // Traverses entries in reverse order, breaking once we hit the lowest epoch in epoch_credits let len = self.history.arr.len(); for i in 0..len { let position = (self.history.idx as usize + len - i) % len; let entry = &mut self.history.arr[position]; if let Some(&epoch_credits) = epoch_credits_map.get(&entry.epoch) { - if epoch_credits != entry.epoch_credits { - entry.epoch_credits = epoch_credits; - } else { - break; - } - } else { + entry.epoch_credits = epoch_credits; + } + if entry.epoch == min_epoch { break; } } @@ -726,7 +815,7 @@ impl CircBufCluster { } /// Returns &ClusterHistoryEntry for each existing entry in range [start_epoch, end_epoch], factoring for wraparound - /// Returns None if either start_epoch or end_epoch is not in the CircBuf + /// Returns None for each epoch that doesn't exist in the CircBuf pub fn epoch_range( &self, start_epoch: u16, @@ -974,4 +1063,189 @@ mod tests { vec![Some(0), Some(1), None, Some(3)] ); } + + #[test] + fn test_insert() { + let mut default_circ_buf = CircBuf { + idx: MAX_ITEMS as u64 - 1, + ..Default::default() + }; + for _ in 0..MAX_ITEMS { + let entry = ValidatorHistoryEntry { + ..ValidatorHistoryEntry::default() + }; + default_circ_buf.push(entry); + } + default_circ_buf.is_empty = 1; + + // Test partially full CircBuf + let mut circ_buf = default_circ_buf; + for i in 0..MAX_ITEMS / 2 { + let entry = ValidatorHistoryEntry { + epoch: i as u16, + ..ValidatorHistoryEntry::default() + }; + // Skip an entry + if i != 100 { + circ_buf.push(entry); + } + } + + // Insert an entry at epoch 100 + let entry = ValidatorHistoryEntry { + epoch: 100, + ..ValidatorHistoryEntry::default() + }; + circ_buf.insert(entry, 100).unwrap(); + + // Check that the entry was inserted + let range = circ_buf.epoch_range(99, 101); + let epochs = range + .iter() + .filter_map(|maybe_e| maybe_e.map(|e| e.epoch)) + .collect::>(); + assert_eq!(epochs, vec![99, 100, 101]); + + // Test full CircBuf with wraparound. Will contain epochs 512-1023, skipping 600 - 610 + let mut circ_buf = default_circ_buf; + for i in 0..MAX_ITEMS * 2 { + let entry = ValidatorHistoryEntry { + epoch: i as u16, + ..ValidatorHistoryEntry::default() + }; + if !(600..=610).contains(&i) { + circ_buf.push(entry); + } + } + + // Insert an entry where there are valid entries after idx and insertion position < idx + let entry = ValidatorHistoryEntry { + epoch: 600, + ..ValidatorHistoryEntry::default() + }; + circ_buf.insert(entry, 600).unwrap(); + + let range = circ_buf.epoch_range(599, 601); + let epochs = range + .iter() + .filter_map(|maybe_e| maybe_e.map(|e| e.epoch)) + .collect::>(); + assert_eq!(epochs, vec![599, 600]); + + // Insert an entry where insertion position > idx + let mut circ_buf = default_circ_buf; + for i in 0..MAX_ITEMS * 3 / 2 { + let entry = ValidatorHistoryEntry { + epoch: i as u16, + ..ValidatorHistoryEntry::default() + }; + if i != 500 { + circ_buf.push(entry); + } + } + assert!(circ_buf.last().unwrap().epoch == 767); + assert!(circ_buf.idx == 254); + + let entry = ValidatorHistoryEntry { + epoch: 500, + ..ValidatorHistoryEntry::default() + }; + circ_buf.insert(entry, 500).unwrap(); + + let range = circ_buf.epoch_range(256, 767); + assert!(range.iter().all(|maybe_e| maybe_e.is_some())); + + // Test wraparound correctly when inserting at the end + let mut circ_buf = default_circ_buf; + for i in 0..2 * MAX_ITEMS - 1 { + let entry = ValidatorHistoryEntry { + epoch: i as u16, + ..ValidatorHistoryEntry::default() + }; + circ_buf.push(entry); + } + circ_buf.push(ValidatorHistoryEntry { + epoch: 2 * MAX_ITEMS as u16, + ..ValidatorHistoryEntry::default() + }); + + circ_buf + .insert( + ValidatorHistoryEntry { + epoch: 2 * MAX_ITEMS as u16 - 1, + ..ValidatorHistoryEntry::default() + }, + 2 * MAX_ITEMS as u16 - 1, + ) + .unwrap(); + let range = circ_buf.epoch_range(MAX_ITEMS as u16 + 1, 2 * MAX_ITEMS as u16); + + assert!(range.iter().all(|maybe_e| maybe_e.is_some())); + } + + #[test] + fn test_insert_errors() { + // test insert empty + let mut circ_buf = CircBuf { + idx: 0, + is_empty: 1, + padding: [0; 7], + arr: [ValidatorHistoryEntry::default(); MAX_ITEMS], + }; + + let entry = ValidatorHistoryEntry { + epoch: 10, + ..ValidatorHistoryEntry::default() + }; + + assert!( + circ_buf.insert(entry, 10) == Err(Error::from(ValidatorHistoryError::EpochOutOfRange)) + ); + + let mut circ_buf = CircBuf { + idx: 4, + is_empty: 0, + padding: [0; 7], + arr: [ValidatorHistoryEntry::default(); MAX_ITEMS], + }; + + for i in 0..5 { + circ_buf.arr[i] = ValidatorHistoryEntry { + epoch: (i * 10) as u16 + 6, + ..ValidatorHistoryEntry::default() + }; + } + + let entry = ValidatorHistoryEntry { + epoch: 5, + ..ValidatorHistoryEntry::default() + }; + + assert!( + circ_buf.insert(entry, 5) == Err(Error::from(ValidatorHistoryError::EpochOutOfRange)) + ); + + let mut circ_buf = CircBuf { + idx: 4, + is_empty: 0, + padding: [0; 7], + arr: [ValidatorHistoryEntry::default(); MAX_ITEMS], + }; + + for i in 0..5 { + circ_buf.arr[i] = ValidatorHistoryEntry { + epoch: (i * 10) as u16, + ..ValidatorHistoryEntry::default() + }; + } + + let entry = ValidatorHistoryEntry { + epoch: 50, + ..ValidatorHistoryEntry::default() + }; + + assert!( + circ_buf.insert(entry, 50) == Err(Error::from(ValidatorHistoryError::EpochOutOfRange)) + ); + } } diff --git a/programs/validator-history/src/utils.rs b/programs/validator-history/src/utils.rs index 13c44da1..2c2bbd53 100644 --- a/programs/validator-history/src/utils.rs +++ b/programs/validator-history/src/utils.rs @@ -4,7 +4,7 @@ use anchor_lang::{ solana_program::native_token::lamports_to_sol, }; -use crate::errors::ValidatorHistoryError; +use crate::{errors::ValidatorHistoryError, ValidatorHistoryEntry}; pub fn cast_epoch(epoch: u64) -> Result { require!( @@ -15,6 +15,38 @@ pub fn cast_epoch(epoch: u64) -> Result { Ok(epoch_u16) } +pub fn get_min_epoch( + epoch_credits: &[( + u64, /* epoch */ + u64, /* epoch cumulative votes */ + u64, /* prev epoch cumulative votes */ + )], +) -> Result { + cast_epoch( + epoch_credits + .iter() + .min_by_key(|(epoch, _, _)| *epoch) + .ok_or(ValidatorHistoryError::InvalidEpochCredits)? + .0, + ) +} + +pub fn get_max_epoch( + epoch_credits: &[( + u64, /* epoch */ + u64, /* epoch cumulative votes */ + u64, /* prev epoch cumulative votes */ + )], +) -> Result { + cast_epoch( + epoch_credits + .iter() + .max_by_key(|(epoch, _, _)| *epoch) + .ok_or(ValidatorHistoryError::InvalidEpochCredits)? + .0, + ) +} + pub fn cast_epoch_start_timestamp(start_timestamp: i64) -> u64 { start_timestamp.try_into().unwrap() } @@ -36,6 +68,55 @@ pub fn get_vote_account(validator_history_account_info: &AccountInfo) -> Pubkey Pubkey::from(data) } +/// Finds the position to insert a new entry with the given epoch, where the epoch is greater than the previous entry and less than the next entry. +/// Assumes entries are in sorted order (according to CircBuf ordering), and there are no duplicate epochs. +pub fn find_insert_position( + arr: &[ValidatorHistoryEntry], + idx: usize, + epoch: u16, +) -> Option { + let len = arr.len(); + if len == 0 { + return None; + } + + let insert_pos = + if idx != len - 1 && arr[idx + 1].epoch == ValidatorHistoryEntry::default().epoch { + // If the circ buf still has default values in it, we do a normal binary search without factoring for wraparound. + let len = idx + 1; + let mut left = 0; + let mut right = len; + while left < right { + let mid = (left + right) / 2; + match arr[mid].epoch.cmp(&epoch) { + std::cmp::Ordering::Equal => return None, + std::cmp::Ordering::Less => left = mid + 1, + std::cmp::Ordering::Greater => right = mid, + } + } + left % arr.len() + } else { + // Binary search with wraparound + let mut left = 0; + let mut right = len; + while left < right { + let mid = (left + right) / 2; + // idx + 1 is the index of the smallest epoch in the array + let mid_idx = ((idx + 1) + mid) % len; + match arr[mid_idx].epoch.cmp(&epoch) { + std::cmp::Ordering::Equal => return None, + std::cmp::Ordering::Less => left = mid + 1, + std::cmp::Ordering::Greater => right = mid, + } + } + ((idx + 1) + left) % len + }; + if arr[insert_pos].epoch == epoch { + return None; + } + Some(insert_pos) +} + #[cfg(test)] mod tests { use super::*; @@ -47,4 +128,80 @@ mod tests { assert_eq!(fixed_point_sol(429_496_729_600_000_000), 4294967295) } + + #[test] + fn test_find_insert_position() { + // Test empty + let arr = vec![]; + assert_eq!(find_insert_position(&arr, 0, 5), None); + + // Test single element + let arr = vec![ValidatorHistoryEntry { + epoch: 10, + ..Default::default() + }]; + assert_eq!(find_insert_position(&arr, 0, 5), Some(0)); + assert_eq!(find_insert_position(&arr, 0, 15), Some(0)); + + // Test multiple elements + let arr = vec![ + ValidatorHistoryEntry { + epoch: 5, + ..Default::default() + }, + ValidatorHistoryEntry { + epoch: 10, + ..Default::default() + }, + ValidatorHistoryEntry { + epoch: 15, + ..Default::default() + }, + ValidatorHistoryEntry { + epoch: 20, + ..Default::default() + }, + ValidatorHistoryEntry::default(), + ValidatorHistoryEntry::default(), + ValidatorHistoryEntry::default(), + ]; + + let idx = 3; + assert_eq!(find_insert_position(&arr, idx, 0), Some(0)); + assert_eq!(find_insert_position(&arr, idx, 12), Some(2)); + assert_eq!(find_insert_position(&arr, idx, 25), Some(4)); + + // Test wraparound + let arr = vec![ + ValidatorHistoryEntry { + epoch: 15, + ..Default::default() + }, + ValidatorHistoryEntry { + epoch: 20, + ..Default::default() + }, + ValidatorHistoryEntry { + epoch: 25, + ..Default::default() + }, + ValidatorHistoryEntry { + epoch: 5, + ..Default::default() + }, + ValidatorHistoryEntry { + epoch: 10, + ..Default::default() + }, + ]; + + let idx = 2; + assert_eq!(find_insert_position(&arr, idx, 0), Some(3)); + assert_eq!(find_insert_position(&arr, idx, 12), Some(0)); + assert_eq!(find_insert_position(&arr, idx, 17), Some(1)); + assert_eq!(find_insert_position(&arr, idx, 22), Some(2)); + + // Test duplicate + assert_eq!(find_insert_position(&arr, idx, 10), None); + } } diff --git a/tests/tests/test_vote_account.rs b/tests/tests/test_vote_account.rs index 0e94692b..7b577792 100644 --- a/tests/tests/test_vote_account.rs +++ b/tests/tests/test_vote_account.rs @@ -1,9 +1,15 @@ #![allow(clippy::await_holding_refcell_ref)] -use anchor_lang::{solana_program::instruction::Instruction, InstructionData, ToAccountMetas}; +use anchor_lang::{ + solana_program::instruction::Instruction, AnchorSerialize, Discriminator, InstructionData, + ToAccountMetas, +}; use solana_program_test::*; -use solana_sdk::{clock::Clock, signer::Signer, transaction::Transaction}; +use solana_sdk::{ + account::Account, clock::Clock, compute_budget::ComputeBudgetInstruction, signer::Signer, + transaction::Transaction, vote::state::MAX_EPOCH_CREDITS_HISTORY, +}; use tests::fixtures::{new_vote_account, TestFixture}; -use validator_history::ValidatorHistory; +use validator_history::{ValidatorHistory, ValidatorHistoryEntry}; #[tokio::test] async fn test_copy_vote_account() { @@ -64,9 +70,9 @@ async fn test_copy_vote_account() { assert!(account.history.arr[0].epoch_credits == 10); assert!(account.history.arr[0].commission == 9); + // Skips epoch fixture.advance_num_epochs(2).await; - - let epoch_credits = vec![(0, 22, 10), (1, 34, 22), (2, 46, 34)]; + let epoch_credits = vec![(0, 22, 10), (1, 35, 22), (2, 49, 35)]; ctx.borrow_mut().set_account( &fixture.vote_account, @@ -103,10 +109,272 @@ async fn test_copy_vote_account() { .load_and_deserialize(&fixture.validator_history_account) .await; - // check new epoch 0 values get copied over, but epoch 1 should be skipped - assert!(account.history.idx == 1); - assert!(account.history.arr[1].epoch == 2); - assert!(account.history.arr[1].commission == 8); - assert!(account.history.arr[1].epoch_credits == 12); + // Check that skipped epoch and new epoch entries + credits are added + // But skipped epoch commission not added + assert!(account.history.idx == 2); + assert!(account.history.arr[2].epoch == 2); + assert!(account.history.arr[1].epoch == 1); + assert!(account.history.arr[0].epoch == 0); + assert!(account.history.arr[2].commission == 8); + assert!(account.history.arr[1].commission == ValidatorHistoryEntry::default().commission); + assert!(account.history.arr[0].commission == 9); + assert!(account.history.arr[2].epoch_credits == 14); + assert!(account.history.arr[1].epoch_credits == 13); assert!(account.history.arr[0].epoch_credits == 12); } + +#[tokio::test] +async fn test_insert_missing_entries_compute() { + // Initialize a ValidatorHistoryAccount with one entry for epoch 0, one entry for epoch 1000, and a vote account with 64 epochs of sparse credits in between + // Expect that all 64 epochs of credits are copied over to the ValidatorHistoryAccount + // Make sure we are within compute budget + + let fixture = TestFixture::new().await; + let ctx = &fixture.ctx; + fixture.initialize_config().await; + fixture.initialize_validator_history_account().await; + let initial_epoch_credits = vec![(0, 10, 0)]; + ctx.borrow_mut().set_account( + &fixture.vote_account, + &new_vote_account( + fixture.vote_account, + fixture.vote_account, + 9, + Some(initial_epoch_credits), + ) + .into(), + ); + let instruction = Instruction { + program_id: validator_history::id(), + data: validator_history::instruction::CopyVoteAccount {}.data(), + accounts: validator_history::accounts::CopyVoteAccount { + validator_history_account: fixture.validator_history_account, + vote_account: fixture.vote_account, + signer: fixture.keypair.pubkey(), + } + .to_account_metas(None), + }; + + let transaction = Transaction::new_signed_with_payer( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), + instruction, + ], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + ctx.borrow().last_blockhash, + ); + fixture.submit_transaction_assert_success(transaction).await; + fixture.advance_num_epochs(1000).await; + + let initial_epoch_credits = vec![(0, 10, 0), (1000, 10, 0)]; + ctx.borrow_mut().set_account( + &fixture.vote_account, + &new_vote_account( + fixture.vote_account, + fixture.vote_account, + 9, + Some(initial_epoch_credits), + ) + .into(), + ); + + let instruction = Instruction { + program_id: validator_history::id(), + data: validator_history::instruction::CopyVoteAccount {}.data(), + accounts: validator_history::accounts::CopyVoteAccount { + validator_history_account: fixture.validator_history_account, + vote_account: fixture.vote_account, + signer: fixture.keypair.pubkey(), + } + .to_account_metas(None), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), + instruction, + ], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(transaction).await; + + // Fake scenario: lots of new entries that were never picked up initially + // Extreme case: validator votes once every 10 epochs + let mut epoch_credits: Vec<(u64, u64, u64)> = vec![]; + for (i, epoch) in (1..MAX_EPOCH_CREDITS_HISTORY + 1).enumerate() { + let i = i as u64; + let epoch = epoch as u64; + epoch_credits.push((epoch * 10, (i + 1) * 10, i * 10)); + } + ctx.borrow_mut().set_account( + &fixture.vote_account, + &new_vote_account( + fixture.vote_account, + fixture.vote_account, + 9, + Some(epoch_credits), + ) + .into(), + ); + + let instruction = Instruction { + program_id: validator_history::id(), + data: validator_history::instruction::CopyVoteAccount {}.data(), + accounts: validator_history::accounts::CopyVoteAccount { + validator_history_account: fixture.validator_history_account, + vote_account: fixture.vote_account, + signer: fixture.keypair.pubkey(), + } + .to_account_metas(None), + }; + let blockhash = ctx.borrow_mut().get_new_latest_blockhash().await.unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[ + // Inserting 64 entries uses ~230k compute units, slightly above default + ComputeBudgetInstruction::set_compute_unit_limit(300_000), + instruction, + ], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + blockhash, + ); + fixture.submit_transaction_assert_success(transaction).await; + + let account: ValidatorHistory = fixture + .load_and_deserialize(&fixture.validator_history_account) + .await; + + // Check that all 64 epochs of credits were copied over and original entries were preserved + let end_idx = MAX_EPOCH_CREDITS_HISTORY + 1; + assert!(account.history.idx == end_idx as u64); + for i in 1..end_idx { + assert!(account.history.arr[i].epoch == 10 * i as u16); + assert!(account.history.arr[i].epoch_credits == 10); + assert!(account.history.arr[i].commission == ValidatorHistoryEntry::default().commission); + } + assert!(account.history.arr[0].epoch == 0); + assert!(account.history.arr[0].epoch_credits == 10); + assert!(account.history.arr[0].commission == 9); + assert!(account.history.arr[end_idx].epoch == 1000); + assert!(account.history.arr[end_idx].epoch_credits == 10); + assert!(account.history.arr[end_idx].commission == 9); + for i in end_idx + 1..ValidatorHistory::MAX_ITEMS { + assert!(account.history.arr[i].epoch == ValidatorHistoryEntry::default().epoch); + assert!( + account.history.arr[i].epoch_credits == ValidatorHistoryEntry::default().epoch_credits + ); + assert!(account.history.arr[i].commission == ValidatorHistoryEntry::default().commission); + } + + drop(fixture); +} + +fn serialized_validator_history_account(validator_history: ValidatorHistory) -> Account { + let mut data = vec![]; + validator_history.serialize(&mut data).unwrap(); + for byte in ValidatorHistory::discriminator().into_iter().rev() { + data.insert(0, byte); + } + Account { + lamports: 1_000_000_000, + data, + owner: validator_history::id(), + ..Account::default() + } +} + +#[tokio::test] +async fn test_insert_missing_entries_wraparound() { + // initialize validator history account with > 600 epochs of entries, missing one. This will force wraparound + // + // initialize vote account with 64 epochs, filling in the missing one + + let fixture = TestFixture::new().await; + let ctx = &fixture.ctx; + fixture.initialize_config().await; + fixture.initialize_validator_history_account().await; + + fixture.advance_num_epochs(610).await; + + let mut validator_history: ValidatorHistory = fixture + .load_and_deserialize(&fixture.validator_history_account) + .await; + + // Fill in 600 epochs of entries, skipping 590 + // This will create wraparound, storing epochs 87 - 599 + for i in 0..600 { + if i == 590 { + continue; + } + validator_history.history.push(ValidatorHistoryEntry { + epoch: i as u16, + epoch_credits: 10, + commission: 9, + vote_account_last_update_slot: 0, + ..Default::default() + }); + } + + ctx.borrow_mut().set_account( + &fixture.validator_history_account, + &serialized_validator_history_account(validator_history).into(), + ); + + // New vote account with epochs 580 - 610 + // 11 credits per epoch + let epoch_credits: Vec<(u64, u64, u64)> = (580..611).map(|i| (i, 11, 0)).collect(); + + let vote_account = new_vote_account( + fixture.vote_account, + fixture.vote_account, + 10, + Some(epoch_credits.clone()), + ); + + ctx.borrow_mut() + .set_account(&fixture.vote_account, &vote_account.into()); + + let instruction = Instruction { + program_id: validator_history::id(), + data: validator_history::instruction::CopyVoteAccount {}.data(), + accounts: validator_history::accounts::CopyVoteAccount { + validator_history_account: fixture.validator_history_account, + vote_account: fixture.vote_account, + signer: fixture.keypair.pubkey(), + } + .to_account_metas(None), + }; + + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&fixture.keypair.pubkey()), + &[&fixture.keypair], + ctx.borrow().last_blockhash, + ); + fixture.submit_transaction_assert_success(transaction).await; + + let account: ValidatorHistory = fixture + .load_and_deserialize(&fixture.validator_history_account) + .await; + + // 610 % 512 == 98 + assert_eq!(account.history.idx, 98); + + // Ensures that all entries exist, including missing 590 + // and entries 600 - 610 inserted after last entry + // And that epoch credits were updated properly + for i in account.history.idx as usize + 1..611 { + let index = i % ValidatorHistory::MAX_ITEMS; + assert_eq!(account.history.arr[index].epoch, i as u16,); + if i >= 580 { + assert_eq!(account.history.arr[index].epoch_credits, 11); + } else { + assert_eq!(account.history.arr[index].epoch_credits, 10); + } + } + + drop(fixture); +}