diff --git a/.gitignore b/.gitignore index ea8c4bf..5626169 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target + +.developer \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 4bde27f..af80bda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2619,6 +2619,8 @@ dependencies = [ "assert_matches", "borsh 0.10.4", "bytemuck", + "cfg-if", + "const_str_to_pubkey", "jito-bytemuck", "jito-jsm-core", "jito-restaking-core", @@ -2628,8 +2630,10 @@ dependencies = [ "jito-vault-core", "jito-vault-sdk", "jito-weight-table-core", + "jito-weight-table-sdk", "shank", "solana-program", + "solana-security-txt", "spl-associated-token-account", "spl-token", "thiserror", diff --git a/weight_table_core/src/error.rs b/weight_table_core/src/error.rs index dcb686f..79b4ebd 100644 --- a/weight_table_core/src/error.rs +++ b/weight_table_core/src/error.rs @@ -14,6 +14,8 @@ pub enum WeightTableError { #[error("Incorrect weight table admin")] IncorrectWeightTableAdmin = 0x2200, + #[error("Cannnot create future weight tables")] + CannotCreateFutureWeightTables = 0x2201, } impl DecodeError for WeightTableError { diff --git a/weight_table_core/src/weight_table.rs b/weight_table_core/src/weight_table.rs index 47b5c8b..15a61ec 100644 --- a/weight_table_core/src/weight_table.rs +++ b/weight_table_core/src/weight_table.rs @@ -1,7 +1,7 @@ use bytemuck::{Pod, Zeroable}; use jito_bytemuck::{types::PodU64, AccountDeserialize, Discriminator}; use shank::{ShankAccount, ShankType}; -use solana_program::pubkey::Pubkey; +use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; use crate::{discriminators::WEIGHT_TABLE_DISCRIMINATOR, error::WeightTableError, weight::Weight}; @@ -16,8 +16,11 @@ pub struct WeightTable { /// The NCN epoch for which the weight table is valid pub ncn_epoch: PodU64, - /// Anything non-zero means the table is finalized and cannot be updated. - finalized: u8, + /// Slot weight table was created + slot_created: PodU64, + + /// Slot weight table was finalized + slot_finalized: PodU64, /// Bump seed for the PDA pub bump: u8, @@ -35,14 +38,14 @@ impl Discriminator for WeightTable { impl WeightTable { pub const MAX_TABLE_ENTRIES: usize = 32; - pub const NOT_FINALIZED: u8 = 0; - pub const FINALIZED: u8 = 0xFF; + pub const NOT_FINALIZED: u64 = u64::MAX; - pub fn new(ncn: Pubkey, ncn_epoch: u64, bump: u8) -> Self { + pub fn new(ncn: Pubkey, ncn_epoch: u64, slot_created: u64, bump: u8) -> Self { Self { ncn, ncn_epoch: PodU64::from(ncn_epoch), - finalized: Self::NOT_FINALIZED, + slot_created: PodU64::from(slot_created), + slot_finalized: PodU64::from(Self::NOT_FINALIZED), bump, reserved: [0; 128], table: [WeightEntry::default(); Self::MAX_TABLE_ENTRIES], @@ -103,12 +106,51 @@ impl WeightTable { Ok(()) } + pub fn slot_created(&self) -> u64 { + self.slot_created.into() + } + + pub fn slot_finalized(&self) -> u64 { + self.slot_finalized.into() + } + pub fn finalized(&self) -> bool { - self.finalized != Self::NOT_FINALIZED + self.slot_finalized != PodU64::from(Self::NOT_FINALIZED) } - pub fn finalize(&mut self) { - self.finalized = Self::FINALIZED; + pub fn finalize(&mut self, current_slot: u64) { + self.slot_finalized = PodU64::from(current_slot); + } + + 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 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(()) } } @@ -138,7 +180,7 @@ mod tests { #[test] fn test_weight_table_new() { let ncn = Pubkey::new_unique(); - let table = WeightTable::new(ncn, 0, 0); + let table = WeightTable::new(ncn, 0, 0, 0); assert_eq!(table.entry_count(), 0); } @@ -146,7 +188,7 @@ mod tests { fn test_weight_table_entry_count() { let ncn = Pubkey::new_unique(); - let mut table = WeightTable::new(ncn, 0, 0); + let mut table = WeightTable::new(ncn, 0, 0, 0); let mint1 = Pubkey::new_unique(); let mint2 = Pubkey::new_unique(); @@ -165,7 +207,7 @@ mod tests { fn test_weight_table_find_weight() { let ncn = Pubkey::new_unique(); - let mut table = WeightTable::new(ncn, 0, 0); + let mut table = WeightTable::new(ncn, 0, 0, 0); let mint1 = Pubkey::new_unique(); let mint2 = Pubkey::new_unique(); @@ -185,7 +227,7 @@ mod tests { fn test_weight_table_set_weight() { let ncn = Pubkey::new_unique(); - let mut table = WeightTable::new(ncn, 0, 0); + let mut table = WeightTable::new(ncn, 0, 0, 0); let mint = Pubkey::new_unique(); // Set initial weight @@ -225,4 +267,15 @@ mod tests { let non_empty_entry = WeightEntry::new(mint, weight); assert!(!non_empty_entry.is_empty()); } + + #[test] + fn test_weight_table_finalize() { + let mut weight_table = WeightTable::new(Pubkey::new_unique(), 0, 0, 0); + + assert!(!weight_table.finalized()); + assert_eq!(weight_table.slot_finalized(), WeightTable::NOT_FINALIZED); + + weight_table.finalize(0); + assert!(weight_table.finalized()); + } } diff --git a/weight_table_program/Cargo.toml b/weight_table_program/Cargo.toml index 5313038..d3f99d9 100644 --- a/weight_table_program/Cargo.toml +++ b/weight_table_program/Cargo.toml @@ -9,9 +9,22 @@ license = { workspace = true } edition = { workspace = true } readme = { workspace = true } +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] +mainnet-beta = [] +testnet = [] +devnet = [] +localhost = [] + [dependencies] borsh = { workspace = true } bytemuck = { workspace = true } +cfg-if = { workspace = true } +const_str_to_pubkey = { workspace = true } jito-reward-core = { workspace = true } jito-bytemuck = { workspace = true } jito-jsm-core = { workspace = true } @@ -21,11 +34,13 @@ jito-restaking-sdk = { workspace = true } jito-restaking-core= { workspace = true } jito-restaking-program = { workspace = true } jito-weight-table-core = { workspace = true } +jito-weight-table-sdk = { workspace = true } shank = { workspace = true } solana-program = { workspace = true } spl-associated-token-account = { workspace = true } spl-token = { workspace = true } thiserror = { workspace = true } +solana-security-txt = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/weight_table_program/src/copy_weight_table.rs b/weight_table_program/src/copy_weight_table.rs deleted file mode 100644 index ecde1b4..0000000 --- a/weight_table_program/src/copy_weight_table.rs +++ /dev/null @@ -1 +0,0 @@ -// Copy contents from another NCN's weight table diff --git a/weight_table_program/src/finalize_weight_table.rs b/weight_table_program/src/finalize_weight_table.rs index 2a943ee..e4ed9ad 100644 --- a/weight_table_program/src/finalize_weight_table.rs +++ b/weight_table_program/src/finalize_weight_table.rs @@ -1 +1,48 @@ -// Permissioned update table +use jito_bytemuck::AccountDeserialize; +use jito_jsm_core::loader::load_signer; +use jito_restaking_core::ncn::Ncn; +use jito_weight_table_core::{error::WeightTableError, weight_table::WeightTable}; +use solana_program::{ + account_info::AccountInfo, clock::Clock, entrypoint::ProgramResult, msg, + program_error::ProgramError, pubkey::Pubkey, sysvar::Sysvar, +}; + +/// Initializes a Weight Table +pub fn process_finalize_weight_table( + program_id: &Pubkey, + accounts: &[AccountInfo], + ncn_epoch: u64, +) -> ProgramResult { + let [ncn, weight_table, weight_table_admin, restaking_program_id] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + Ncn::load(restaking_program_id.key, ncn, false)?; + let ncn_weight_table_admin = { + //TODO switch to weight table admin when that is merged + let ncn_data = ncn.data.borrow(); + let ncn = Ncn::try_from_slice_unchecked(&ncn_data)?; + ncn.admin + }; + + load_signer(weight_table_admin, true)?; + WeightTable::load(program_id, weight_table, ncn, ncn_epoch, true)?; + + if restaking_program_id.key.ne(&jito_restaking_program::id()) { + msg!("Incorrect restaking program ID"); + return Err(ProgramError::InvalidAccountData); + } + + if ncn_weight_table_admin.ne(&weight_table_admin.key) { + msg!("Vault update delegations ticket is not at the correct PDA"); + return Err(WeightTableError::IncorrectWeightTableAdmin.into()); + } + + let mut weight_table_data = weight_table.try_borrow_mut_data()?; + let weight_table_account = WeightTable::try_from_slice_unchecked_mut(&mut weight_table_data)?; + + let current_slot = Clock::get()?.slot; + weight_table_account.finalize(current_slot); + + Ok(()) +} diff --git a/weight_table_program/src/initialize_weight_table.rs b/weight_table_program/src/initialize_weight_table.rs index 044b282..54dc125 100644 --- a/weight_table_program/src/initialize_weight_table.rs +++ b/weight_table_program/src/initialize_weight_table.rs @@ -11,9 +11,11 @@ use solana_program::{ }; /// Initializes a Weight Table +/// Can be backfilled for previous epochs pub fn process_initialize_weight_table( program_id: &Pubkey, accounts: &[AccountInfo], + first_slot_of_ncn_epoch: Option, ) -> ProgramResult { let [restaking_config, ncn, weight_table, weight_table_admin, restaking_program_id, system_program] = accounts @@ -50,10 +52,20 @@ pub fn process_initialize_weight_table( } let current_slot = Clock::get()?.slot; - let ncn_epoch = current_slot + let current_ncn_epoch = current_slot .checked_div(ncn_epoch_length) .ok_or(WeightTableError::DenominatorIsZero)?; + let ncn_epoch_slot = first_slot_of_ncn_epoch.unwrap_or(current_slot); + let ncn_epoch = ncn_epoch_slot + .checked_div(ncn_epoch_length) + .ok_or(WeightTableError::DenominatorIsZero)?; + + if ncn_epoch > current_ncn_epoch { + msg!("Weight tables can only be initialized for current or past epochs"); + return Err(WeightTableError::CannotCreateFutureWeightTables.into()); + } + let (weight_table_pubkey, weight_table_bump, mut weight_table_seeds) = WeightTable::find_program_address(program_id, ncn.key, ncn_epoch); weight_table_seeds.push(vec![weight_table_bump]); @@ -82,7 +94,8 @@ pub fn process_initialize_weight_table( let mut weight_table_data = weight_table.try_borrow_mut_data()?; weight_table_data[0] = WeightTable::DISCRIMINATOR; let weight_table_account = WeightTable::try_from_slice_unchecked_mut(&mut weight_table_data)?; - *weight_table_account = WeightTable::new(*ncn.key, ncn_epoch, weight_table_bump); + + *weight_table_account = WeightTable::new(*ncn.key, ncn_epoch, current_slot, weight_table_bump); Ok(()) } diff --git a/weight_table_program/src/lib.rs b/weight_table_program/src/lib.rs index 32db3d3..b20139a 100644 --- a/weight_table_program/src/lib.rs +++ b/weight_table_program/src/lib.rs @@ -1 +1,84 @@ -pub mod initialize_weight_table; +mod finalize_weight_table; +mod initialize_weight_table; +mod update_weight_table; + +use borsh::BorshDeserialize; +use const_str_to_pubkey::str_to_pubkey; +use jito_weight_table_sdk::instruction::WeightTableInstruction; +use solana_program::{ + account_info::AccountInfo, declare_id, entrypoint::ProgramResult, msg, + program_error::ProgramError, pubkey::Pubkey, +}; +#[cfg(not(feature = "no-entrypoint"))] +use solana_security_txt::security_txt; + +use crate::{ + finalize_weight_table::process_finalize_weight_table, + initialize_weight_table::process_initialize_weight_table, + update_weight_table::process_update_weight_table, +}; + +declare_id!(str_to_pubkey(env!("WEIGHT_TABLE_ID"))); + +#[cfg(not(feature = "no-entrypoint"))] +security_txt! { + // Required fields + name: "Jito's Weight Table Program", + project_url: "https://jito.network/", + contacts: "email:team@jito.network", + policy: "https://github.com/jito-foundation/jito-rewards-ncn", + // Optional Fields + preferred_languages: "en", + source_code: "https://github.com/jito-foundation/jito-rewards-ncn" +} + +#[cfg(not(feature = "no-entrypoint"))] +solana_program::entrypoint!(process_instruction); + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + if *program_id != id() { + return Err(ProgramError::IncorrectProgramId); + } + + let instruction = WeightTableInstruction::try_from_slice(instruction_data)?; + + match instruction { + // ------------------------------------------ + // Initialization + // ------------------------------------------ + WeightTableInstruction::InitializeWeightTable { + first_slot_of_ncn_epoch, + } => { + msg!("Instruction: InitializeWeightTable"); + process_initialize_weight_table(program_id, accounts, first_slot_of_ncn_epoch) + } + // ------------------------------------------ + // Update + // ------------------------------------------ + WeightTableInstruction::UpdateWeightTable { + ncn_epoch, + weight_numerator, + weight_denominator, + } => { + msg!("Instruction: UpdateWeightTable"); + process_update_weight_table( + program_id, + accounts, + ncn_epoch, + weight_numerator, + weight_denominator, + ) + } + // ------------------------------------------ + // Finalization + // ------------------------------------------ + WeightTableInstruction::FinalizeWeightTable { ncn_epoch } => { + msg!("Instruction: FinalizeWeightTable"); + process_finalize_weight_table(program_id, accounts, ncn_epoch) + } + } +} diff --git a/weight_table_program/src/update_weight_table.rs b/weight_table_program/src/update_weight_table.rs index 2a943ee..873da6a 100644 --- a/weight_table_program/src/update_weight_table.rs +++ b/weight_table_program/src/update_weight_table.rs @@ -1 +1,52 @@ -// Permissioned update table +use jito_bytemuck::AccountDeserialize; +use jito_jsm_core::loader::{load_signer, load_token_mint}; +use jito_restaking_core::ncn::Ncn; +use jito_weight_table_core::{error::WeightTableError, weight::Weight, weight_table::WeightTable}; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, + pubkey::Pubkey, +}; + +/// Initializes a Weight Table +pub fn process_update_weight_table( + program_id: &Pubkey, + accounts: &[AccountInfo], + ncn_epoch: u64, + weight_numerator: u64, + weight_denominator: u64, +) -> ProgramResult { + let [ncn, weight_table, weight_table_admin, mint, restaking_program_id] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + Ncn::load(restaking_program_id.key, ncn, false)?; + let ncn_weight_table_admin = { + //TODO switch to weight table admin when that is merged + let ncn_data = ncn.data.borrow(); + let ncn = Ncn::try_from_slice_unchecked(&ncn_data)?; + ncn.admin + }; + + load_signer(weight_table_admin, true)?; + load_token_mint(mint)?; + WeightTable::load(program_id, weight_table, ncn, ncn_epoch, true)?; + + if restaking_program_id.key.ne(&jito_restaking_program::id()) { + msg!("Incorrect restaking program ID"); + return Err(ProgramError::InvalidAccountData); + } + + if ncn_weight_table_admin.ne(&weight_table_admin.key) { + msg!("Vault update delegations ticket is not at the correct PDA"); + return Err(WeightTableError::IncorrectWeightTableAdmin.into()); + } + + let mut weight_table_data = weight_table.try_borrow_mut_data()?; + let weight_table_account = WeightTable::try_from_slice_unchecked_mut(&mut weight_table_data)?; + + let weight = Weight::new(weight_numerator, weight_denominator)?; + + weight_table_account.set_weight(mint.key, weight)?; + + Ok(()) +} diff --git a/weight_table_sdk/src/instruction.rs b/weight_table_sdk/src/instruction.rs new file mode 100644 index 0000000..4280e99 --- /dev/null +++ b/weight_table_sdk/src/instruction.rs @@ -0,0 +1,38 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use shank::ShankInstruction; + +#[rustfmt::skip] +#[derive(Debug, BorshSerialize, BorshDeserialize, ShankInstruction)] +pub enum WeightTableInstruction { + + /// Initializes global configuration + #[account(0, name = "restaking_config")] + #[account(1, name = "ncn")] + #[account(2, writable, signer, name = "weight_table")] + #[account(3, writable, signer, name = "weight_table_admin")] + #[account(4, name = "restaking_program_id")] + #[account(5, name = "system_program")] + InitializeWeightTable{ + first_slot_of_ncn_epoch: Option, + }, + + /// Updates the weight table + #[account(0, name = "ncn")] + #[account(1, writable, name = "weight_table")] + #[account(2, signer, name = "weight_table_admin")] + #[account(3, name = "restaking_program_id")] + UpdateWeightTable{ + ncn_epoch: u64, + weight_numerator: u64, + weight_denominator: u64, + }, + + #[account(0, name = "ncn")] + #[account(1, writable, name = "weight_table")] + #[account(2, signer, name = "weight_table_admin")] + #[account(3, name = "restaking_program_id")] + FinalizeWeightTable{ + ncn_epoch: u64, + }, + +} diff --git a/weight_table_sdk/src/lib.rs b/weight_table_sdk/src/lib.rs index e69de29..ca4b15f 100644 --- a/weight_table_sdk/src/lib.rs +++ b/weight_table_sdk/src/lib.rs @@ -0,0 +1 @@ +pub mod instruction;