diff --git a/core/src/constants.rs b/core/src/constants.rs index 4868349d..01586c40 100644 --- a/core/src/constants.rs +++ b/core/src/constants.rs @@ -1 +1,3 @@ pub const MAX_FEE_BPS: u64 = 10_000; +pub const MAX_OPERATORS: usize = 256; +pub const MAX_VAULT_OPERATOR_DELEGATIONS: usize = 64; diff --git a/core/src/discriminators.rs b/core/src/discriminators.rs index 18f0d317..7df10f6e 100644 --- a/core/src/discriminators.rs +++ b/core/src/discriminators.rs @@ -1,5 +1,7 @@ #[repr(u8)] pub enum Discriminators { - Config = 1, + NCNConfig = 1, WeightTable = 2, + EpochSnapshot = 3, + OperatorSnapshot = 4, } diff --git a/core/src/epoch_snapshot.rs b/core/src/epoch_snapshot.rs index 8ec499af..7de3e24c 100644 --- a/core/src/epoch_snapshot.rs +++ b/core/src/epoch_snapshot.rs @@ -1,24 +1,26 @@ -use std::collections::HashSet; - use bytemuck::{Pod, Zeroable}; -use jito_bytemuck::{types::PodU64, AccountDeserialize, Discriminator}; +use jito_bytemuck::{ + types::{PodU128, PodU16, PodU64}, + AccountDeserialize, Discriminator, +}; +use jito_vault_core::delegation_state::DelegationState; use shank::{ShankAccount, ShankType}; -use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; +use solana_program::pubkey::Pubkey; -use crate::{discriminators::Discriminators, error::TipRouterError, weight_entry::WeightEntry}; +use crate::{discriminators::Discriminators, fees::Fees}; -// PDA'd ["WEIGHT_TABLE", NCN, NCN_EPOCH_SLOT] +// PDA'd ["EPOCH_SNAPSHOT", NCN, NCN_EPOCH_SLOT] #[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, AccountDeserialize, ShankAccount)] #[repr(C)] -pub struct WeightTable { +pub struct EpochSnapshot { /// The NCN on-chain program is the signer to create and update this account, /// this pushes the responsibility of managing the account to the NCN program. ncn: Pubkey, - /// The NCN epoch for which the weight table is valid + /// The NCN epoch for which the Epoch snapshot is valid ncn_epoch: PodU64, - /// Slot weight table was created + /// Slot Epoch snapshot was created slot_created: PodU64, /// Bump seed for the PDA @@ -27,324 +29,54 @@ pub struct WeightTable { /// Reserved space reserved: [u8; 128], - /// The weight table - table: [WeightEntry; 32], -} - -impl Discriminator for WeightTable { - const DISCRIMINATOR: u8 = Discriminators::WeightTable as u8; -} - -impl WeightTable { - pub const MAX_TABLE_ENTRIES: usize = 32; - - pub fn new(ncn: Pubkey, ncn_epoch: u64, slot_created: u64, bump: u8) -> Self { - Self { - ncn, - ncn_epoch: PodU64::from(ncn_epoch), - slot_created: PodU64::from(slot_created), - bump, - reserved: [0; 128], - table: [WeightEntry::default(); Self::MAX_TABLE_ENTRIES], - } - } - - pub fn seeds(ncn: &Pubkey, ncn_epoch: u64) -> Vec> { - Vec::from_iter( - [ - b"WEIGHT_TABLE".to_vec(), - ncn.to_bytes().to_vec(), - ncn_epoch.to_le_bytes().to_vec(), - ] - .iter() - .cloned(), - ) - } - - pub fn find_program_address( - program_id: &Pubkey, - ncn: &Pubkey, - ncn_epoch: u64, - ) -> (Pubkey, u8, Vec>) { - let seeds = Self::seeds(ncn, ncn_epoch); - let seeds_iter: Vec<_> = seeds.iter().map(|s| s.as_slice()).collect(); - let (pda, bump) = Pubkey::find_program_address(&seeds_iter, program_id); - (pda, bump, seeds) - } - - pub fn initalize_weight_table( - &mut self, - config_supported_mints: &[Pubkey], - ) -> Result<(), TipRouterError> { - if self.initialized() { - return Err(TipRouterError::WeightTableAlreadyInitialized); - } - - // Check for empty vector - if config_supported_mints.is_empty() { - return Err(TipRouterError::NoMintsInTable); - } - - // Check if vector exceeds maximum allowed entries - if config_supported_mints.len() > Self::MAX_TABLE_ENTRIES { - return Err(TipRouterError::TooManyMintsForTable); - } - - // Check for duplicates using nested iterators - let unique_mints: HashSet<_> = config_supported_mints.iter().collect(); - - if unique_mints.len() != config_supported_mints.len() { - return Err(TipRouterError::DuplicateMintsInTable); - } - - // Set table using iterator - self.table - .iter_mut() - .zip(config_supported_mints.iter()) - .for_each(|(entry, &mint)| { - *entry = WeightEntry::new(mint); - }); + ncn_fees: Fees, - self.check_initialized()?; + num_operators: PodU16, + operators_registered: PodU16, - Ok(()) - } - - pub fn set_weight( - &mut self, - mint: &Pubkey, - weight: u128, - current_slot: u64, - ) -> Result<(), TipRouterError> { - self.table - .iter_mut() - .find(|entry| entry.mint() == *mint) - .map_or(Err(TipRouterError::InvalidMintForWeightTable), |entry| { - entry.set_weight(weight, current_slot); - Ok(()) - }) - } - - pub fn get_weight(&self, mint: &Pubkey) -> Result { - self.table - .iter() - .find(|entry| entry.mint() == *mint) - .map_or(Err(TipRouterError::InvalidMintForWeightTable), |entry| { - Ok(entry.weight()) - }) - } - - pub fn get_mints(&self) -> Vec { - self.table - .iter() - .filter(|entry| !entry.is_empty()) - .map(|entry| entry.mint()) - .collect() - } - - pub fn find_weight(&self, mint: &Pubkey) -> Option { - self.table - .iter() - .find(|entry| entry.mint() == *mint) - .map(|entry| entry.weight()) - } - - pub fn mint_count(&self) -> usize { - self.table.iter().filter(|entry| !entry.is_empty()).count() - } - - pub fn weight_count(&self) -> usize { - self.table.iter().filter(|entry| !entry.is_set()).count() - } - - pub const fn ncn(&self) -> Pubkey { - self.ncn - } - - pub fn ncn_epoch(&self) -> u64 { - self.ncn_epoch.into() - } - - pub fn slot_created(&self) -> u64 { - self.slot_created.into() - } - - pub fn initialized(&self) -> bool { - self.mint_count() > 0 - } - - pub fn finalized(&self) -> bool { - self.initialized() && self.mint_count() == self.weight_count() - } - - pub fn check_initialized(&self) -> Result<(), TipRouterError> { - if !self.initialized() { - return Err(TipRouterError::NoMintsInTable); - } - Ok(()) - } - - pub fn load( - program_id: &Pubkey, - weight_table: &AccountInfo, - ncn: &AccountInfo, - ncn_epoch: u64, - expect_writable: bool, - ) -> Result<(), ProgramError> { - if weight_table.owner.ne(program_id) { - msg!("Weight table account is not owned by the program"); - return Err(ProgramError::InvalidAccountOwner); - } - if weight_table.data_is_empty() { - msg!("Weight table account is empty"); - return Err(ProgramError::InvalidAccountData); - } - if expect_writable && !weight_table.is_writable { - msg!("Weight table account is not writable"); - return Err(ProgramError::InvalidAccountData); - } - if weight_table.data.borrow()[0].ne(&Self::DISCRIMINATOR) { - msg!("Weight table account has an incorrect discriminator"); - return Err(ProgramError::InvalidAccountData); - } - let expected_pubkey = Self::find_program_address(program_id, ncn.key, ncn_epoch).0; - if weight_table.key.ne(&expected_pubkey) { - msg!("Weight table incorrect PDA"); - return Err(ProgramError::InvalidAccountData); - } - Ok(()) - } + /// Counted as each delegate gets added + total_votes: PodU128, } -#[cfg(test)] -mod tests { - use solana_program::pubkey::Pubkey; - - use super::*; - - fn get_test_pubkeys(count: usize) -> Vec { - (0..count).map(|_| Pubkey::new_unique()).collect() - } - - #[test] - fn test_initialize_table_success() { - let ncn = Pubkey::new_unique(); - let mut table = WeightTable::new(ncn, 0, 0, 0); - assert_eq!(table.mint_count(), 0); - - let mints = get_test_pubkeys(2); - table.initalize_weight_table(&mints).unwrap(); - assert_eq!(table.mint_count(), 2); - } - - #[test] - fn test_initialize_table_too_many() { - let ncn = Pubkey::new_unique(); - let mut table = WeightTable::new(ncn, 0, 0, 0); - let many_mints = get_test_pubkeys(WeightTable::MAX_TABLE_ENTRIES + 1); - assert_eq!( - table.initalize_weight_table(&many_mints), - Err(TipRouterError::TooManyMintsForTable) - ); - } - - #[test] - fn test_initialize_table_max() { - let ncn = Pubkey::new_unique(); - let mut table = WeightTable::new(ncn, 0, 0, 0); - let max_mints = get_test_pubkeys(WeightTable::MAX_TABLE_ENTRIES); - table.initalize_weight_table(&max_mints).unwrap(); - assert_eq!(table.mint_count(), WeightTable::MAX_TABLE_ENTRIES); - } - - #[test] - fn test_initialize_table_reinitialize() { - let ncn = Pubkey::new_unique(); - let mut table = WeightTable::new(ncn, 0, 0, 0); - let first_mints = get_test_pubkeys(2); - table.initalize_weight_table(&first_mints).unwrap(); - let second_mints = get_test_pubkeys(3); - - assert_eq!( - table.initalize_weight_table(&second_mints), - Err(TipRouterError::WeightTableAlreadyInitialized) - ); - } - - #[test] - fn test_set_weight_success() { - let ncn = Pubkey::new_unique(); - let mut table = WeightTable::new(ncn, 0, 0, 0); - let mints = get_test_pubkeys(2); - let mint = mints[0]; - - table.initalize_weight_table(&mints).unwrap(); - - table.set_weight(&mint, 100, 1).unwrap(); - assert_eq!(table.get_weight(&mint).unwrap(), 100); - } - - #[test] - fn test_set_weight_invalid_mint() { - let ncn = Pubkey::new_unique(); - let mut table = WeightTable::new(ncn, 0, 0, 0); - let mints = get_test_pubkeys(2); - - table.initalize_weight_table(&mints).unwrap(); - - let invalid_mint = Pubkey::new_unique(); - assert_eq!( - table.set_weight(&invalid_mint, 100, 1), - Err(TipRouterError::InvalidMintForWeightTable) - ); - } - - #[test] - fn test_set_weight_update_existing() { - let ncn = Pubkey::new_unique(); - let mut table = WeightTable::new(ncn, 0, 0, 0); - let mints = get_test_pubkeys(2); - let mint = mints[0]; - - table.initalize_weight_table(&mints).unwrap(); - - table.set_weight(&mint, 100, 1).unwrap(); - assert_eq!(table.get_weight(&mint).unwrap(), 100); - - table.set_weight(&mint, 200, 2).unwrap(); - assert_eq!(table.get_weight(&mint).unwrap(), 200); - } +impl Discriminator for EpochSnapshot { + const DISCRIMINATOR: u8 = Discriminators::EpochSnapshot as u8; +} - #[test] - fn test_set_weight_multiple_mints() { - let ncn = Pubkey::new_unique(); - let mut table = WeightTable::new(ncn, 0, 0, 0); - let mints = get_test_pubkeys(2); - let mint1 = mints[0]; - let mint2 = mints[1]; +// PDA'd ["OPERATOR_SNAPSHOT", OPERATOR, NCN, NCN_EPOCH_SLOT] +#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, AccountDeserialize, ShankAccount)] +#[repr(C)] +pub struct OperatorSnapshot { + operator: Pubkey, + ncn: Pubkey, + ncn_epoch: PodU64, + slot_created: PodU64, - table.initalize_weight_table(&mints).unwrap(); + bump: u8, - table.set_weight(&mint1, 100, 1).unwrap(); - table.set_weight(&mint2, 200, 1).unwrap(); + operator_fee_bps: PodU16, - assert_eq!(table.get_weight(&mint1).unwrap(), 100); - assert_eq!(table.get_weight(&mint2).unwrap(), 200); - } + total_votes: PodU128, - #[test] - fn test_set_weight_different_slots() { - let ncn = Pubkey::new_unique(); - let mut table = WeightTable::new(ncn, 0, 0, 0); - let mints = get_test_pubkeys(2); - let mint = mints[0]; + num_vault_operator_delegations: PodU16, + vault_operator_delegations_registered: PodU16, - table.initalize_weight_table(&mints).unwrap(); + slot_set: PodU64, + vault_operator_delegations: [VaultOperatorDelegationSnapshot; 256], +} - table.set_weight(&mint, 100, 1).unwrap(); - assert_eq!(table.get_weight(&mint).unwrap(), 100); +impl Discriminator for OperatorSnapshot { + const DISCRIMINATOR: u8 = Discriminators::OperatorSnapshot as u8; +} - table.set_weight(&mint, 200, 5).unwrap(); - assert_eq!(table.get_weight(&mint).unwrap(), 200); - } +// Operators effectively cast N types of votes, +// where N is the number of supported mints +#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod)] +#[repr(C)] +pub struct VaultOperatorDelegationSnapshot { + vault: Pubkey, + st_mint: Pubkey, + delegation_state: DelegationState, + total_votes: PodU128, + slot_set: PodU64, + reserved: [u8; 128], } diff --git a/core/src/ncn_config.rs b/core/src/ncn_config.rs index d7f72bb0..51765cb4 100644 --- a/core/src/ncn_config.rs +++ b/core/src/ncn_config.rs @@ -36,7 +36,7 @@ pub struct NcnConfig { } impl Discriminator for NcnConfig { - const DISCRIMINATOR: u8 = Discriminators::Config as u8; + const DISCRIMINATOR: u8 = Discriminators::NCNConfig as u8; } impl NcnConfig {