From 4383894b4dbd605f5ab4935a612759480d9b711b Mon Sep 17 00:00:00 2001 From: Christian Krueger Date: Tue, 19 Nov 2024 13:52:13 -0600 Subject: [PATCH 1/5] adding in helper functions --- core/src/ballot_box.rs | 280 +++++++++++++++++++++++++++++++++++++ core/src/discriminators.rs | 16 ++- core/src/epoch_snapshot.rs | 4 +- core/src/lib.rs | 1 + 4 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 core/src/ballot_box.rs diff --git a/core/src/ballot_box.rs b/core/src/ballot_box.rs new file mode 100644 index 00000000..10bb1e5f --- /dev/null +++ b/core/src/ballot_box.rs @@ -0,0 +1,280 @@ +use bytemuck::{Pod, Zeroable}; +use jito_bytemuck::{ + types::{PodBool, PodU128, PodU64}, + AccountDeserialize, Discriminator, +}; +use shank::{ShankAccount, ShankType}; +use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; + +use crate::{ + constants::MAX_OPERATORS, discriminators::Discriminators, error::TipRouterError, fees::Fees, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Copy, Zeroable, ShankType, Pod, ShankType)] +#[repr(C)] +pub struct MerkleRoot { + root: [u8; 32], + max_total_claim: PodU64, + max_num_nodes: PodU64, + reserved: [u8; 64], +} + +impl Default for MerkleRoot { + fn default() -> Self { + Self { + root: [0; 32], + max_total_claim: PodU64::from(0), + max_num_nodes: PodU64::from(0), + reserved: [0; 64], + } + } +} + +impl MerkleRoot { + pub fn new(root: [u8; 32], max_total_claim: u64, max_num_nodes: u64) -> Self { + Self { + root, + max_total_claim: PodU64::from(max_total_claim), + max_num_nodes: PodU64::from(max_num_nodes), + reserved: [0; 64], + } + } + + pub fn root(&self) -> [u8; 32] { + self.root + } + + pub fn max_total_claim(&self) -> u64 { + self.max_total_claim.into() + } + + pub fn max_num_nodes(&self) -> u64 { + self.max_num_nodes.into() + } +} + +#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, ShankType)] +#[repr(C)] +pub struct MerkleRootTally { + merkle_root: MerkleRoot, + stake_weight: PodU128, + vote_count: PodU64, + reserved: [u8; 64], +} + +impl Default for MerkleRootTally { + fn default() -> Self { + Self { + merkle_root: MerkleRoot::default(), + stake_weight: PodU128::from(0), + vote_count: PodU64::from(0), + reserved: [0; 64], + } + } +} + +impl MerkleRootTally { + pub fn new( + root: [u8; 32], + max_total_claim: u64, + max_num_nodes: u64, + stake_weight: u128, + ) -> Self { + Self { + merkle_root: MerkleRoot::new(root, max_total_claim, max_num_nodes), + stake_weight: PodU128::from(stake_weight), + vote_count: PodU64::from(1), + reserved: [0; 64], + } + } + + pub fn merkle_root(&self) -> MerkleRoot { + self.merkle_root + } + + pub fn stake_weight(&self) -> u128 { + self.stake_weight.into() + } + + pub fn vote_count(&self) -> u64 { + self.vote_count.into() + } + + pub fn increment_tally(&mut self, stake_weight: u128) -> Result<(), TipRouterError> { + self.stake_weight = PodU128::from( + self.stake_weight() + .checked_add(stake_weight) + .ok_or(TipRouterError::ArithmeticOverflow)?, + ); + self.vote_count = PodU64::from( + self.vote_count() + .checked_add(1) + .ok_or(TipRouterError::ArithmeticOverflow)?, + ); + + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, ShankType)] +#[repr(C)] +pub struct OperatorVote { + operator: Pubkey, + slot_voted: PodU64, + stake_weight: PodU128, + merkle_root: MerkleRoot, + reserved: [u8; 64], +} + +impl Default for OperatorVote { + fn default() -> Self { + Self { + operator: Pubkey::default(), + slot_voted: PodU64::from(0), + stake_weight: PodU128::from(0), + merkle_root: MerkleRoot::default(), + reserved: [0; 64], + } + } +} + +impl OperatorVote { + pub fn new( + root: [u8; 32], + max_total_claim: u64, + max_num_nodes: u64, + operator: Pubkey, + current_slot: u64, + stake_weight: u128, + ) -> Self { + Self { + operator, + merkle_root: MerkleRoot::new(root, max_total_claim, max_num_nodes), + slot_voted: PodU64::from(current_slot), + stake_weight: PodU128::from(stake_weight), + reserved: [0; 64], + } + } + + pub fn operator(&self) -> Pubkey { + self.operator + } + + pub fn slot_voted(&self) -> u64 { + self.slot_voted.into() + } + + pub fn stake_weight(&self) -> u128 { + self.stake_weight.into() + } + + pub fn merkle_root(&self) -> MerkleRoot { + self.merkle_root + } +} + +// PDA'd ["epoch_snapshot", NCN, NCN_EPOCH_SLOT] +#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, AccountDeserialize, ShankAccount)] +#[repr(C)] +pub struct BallotBox { + ncn: Pubkey, + + ncn_epoch: PodU64, + + bump: u8, + + slot_created: PodU64, + slot_consensus_reached: PodU64, + + reserved: [u8; 128], + + operators_voted: PodU64, + unique_merkle_roots: PodU64, + + operator_votes: [OperatorVote; 256], + merkle_root_tallies: [MerkleRootTally; 256], +} + +impl Discriminator for BallotBox { + const DISCRIMINATOR: u8 = Discriminators::EpochSnapshot as u8; +} + +impl BallotBox { + pub fn new(ncn: Pubkey, ncn_epoch: u64, bump: u8, current_slot: u64) -> Self { + Self { + ncn, + ncn_epoch: PodU64::from(ncn_epoch), + bump, + slot_created: PodU64::from(current_slot), + slot_consensus_reached: PodU64::from(0), + operators_voted: PodU64::from(0), + unique_merkle_roots: PodU64::from(0), + operator_votes: [OperatorVote::default(); MAX_OPERATORS], + merkle_root_tallies: [MerkleRootTally::default(); MAX_OPERATORS], + reserved: [0; 128], + } + } + + pub fn seeds(ncn: &Pubkey, ncn_epoch: u64) -> Vec> { + Vec::from_iter( + [ + b"ballot_box".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 load( + program_id: &Pubkey, + ncn: &Pubkey, + ncn_epoch: u64, + epoch_snapshot: &AccountInfo, + expect_writable: bool, + ) -> Result<(), ProgramError> { + if epoch_snapshot.owner.ne(program_id) { + msg!("Ballot box account has an invalid owner"); + return Err(ProgramError::InvalidAccountOwner); + } + if epoch_snapshot.data_is_empty() { + msg!("Ballot box account data is empty"); + return Err(ProgramError::InvalidAccountData); + } + if expect_writable && !epoch_snapshot.is_writable { + msg!("Ballot box account is not writable"); + return Err(ProgramError::InvalidAccountData); + } + if epoch_snapshot.data.borrow()[0].ne(&Self::DISCRIMINATOR) { + msg!("Ballot box account discriminator is invalid"); + return Err(ProgramError::InvalidAccountData); + } + if epoch_snapshot + .key + .ne(&Self::find_program_address(program_id, ncn, ncn_epoch).0) + { + msg!("Ballot box account is not at the correct PDA"); + return Err(ProgramError::InvalidAccountData); + } + Ok(()) + } + + fn insert_or_create_merkle_root_tally( + &mut self, + merkle_root: &MerkleRoot, + ) -> Result<(), TipRouterError> { + Ok(()) + } +} diff --git a/core/src/discriminators.rs b/core/src/discriminators.rs index 65111c2b..15b8bdae 100644 --- a/core/src/discriminators.rs +++ b/core/src/discriminators.rs @@ -1,9 +1,13 @@ #[repr(u8)] pub enum Discriminators { - NCNConfig = 1, - WeightTable = 2, - TrackedMints = 3, - EpochSnapshot = 4, - OperatorSnapshot = 5, - VaultOperatorDelegationSnapshot = 6, + // Configs + NCNConfig = 0x01, + TrackedMints = 0x02, + // Snapshots + WeightTable = 0x10, + EpochSnapshot = 0x11, + OperatorSnapshot = 0x12, + // Voting + BallotBox = 0x20, + // Distribution } diff --git a/core/src/epoch_snapshot.rs b/core/src/epoch_snapshot.rs index db300f6f..9bf5dbba 100644 --- a/core/src/epoch_snapshot.rs +++ b/core/src/epoch_snapshot.rs @@ -12,7 +12,7 @@ use crate::{ discriminators::Discriminators, error::TipRouterError, fees::Fees, weight_table::WeightTable, }; -// PDA'd ["EPOCH_SNAPSHOT", 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 EpochSnapshot { @@ -191,7 +191,7 @@ impl EpochSnapshot { } } -// PDA'd ["OPERATOR_SNAPSHOT", OPERATOR, NCN, NCN_EPOCH_SLOT] +// PDA'd ["operator_snapshot", OPERATOR, NCN, NCN_EPOCH_SLOT] #[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, AccountDeserialize, ShankAccount)] #[repr(C)] pub struct OperatorSnapshot { diff --git a/core/src/lib.rs b/core/src/lib.rs index 8a15379f..ac782715 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,3 +1,4 @@ +pub mod ballot_box; pub mod constants; pub mod discriminators; pub mod epoch_snapshot; From c25ce692622940c1fa03e7fc043a6b302b186d80 Mon Sep 17 00:00:00 2001 From: Christian Krueger Date: Tue, 19 Nov 2024 14:36:13 -0600 Subject: [PATCH 2/5] structs there --- core/src/ballot_box.rs | 204 ++++++++++++++++++++++++++++++----------- core/src/constants.rs | 1 + core/src/error.rs | 6 ++ 3 files changed, 157 insertions(+), 54 deletions(-) diff --git a/core/src/ballot_box.rs b/core/src/ballot_box.rs index 10bb1e5f..2c330317 100644 --- a/core/src/ballot_box.rs +++ b/core/src/ballot_box.rs @@ -1,103 +1,105 @@ use bytemuck::{Pod, Zeroable}; use jito_bytemuck::{ - types::{PodBool, PodU128, PodU64}, + types::{PodU128, PodU64}, AccountDeserialize, Discriminator, }; use shank::{ShankAccount, ShankType}; use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; +use spl_math::precise_number::PreciseNumber; use crate::{ - constants::MAX_OPERATORS, discriminators::Discriminators, error::TipRouterError, fees::Fees, + constants::{MAX_OPERATORS, PRECISE_CONSENSUS}, + discriminators::Discriminators, + error::TipRouterError, }; #[derive(Debug, Clone, PartialEq, Eq, Copy, Zeroable, ShankType, Pod, ShankType)] #[repr(C)] -pub struct MerkleRoot { - root: [u8; 32], +pub struct Ballot { + merkle_root: [u8; 32], max_total_claim: PodU64, - max_num_nodes: PodU64, + max_node_count: PodU64, reserved: [u8; 64], } -impl Default for MerkleRoot { +impl Default for Ballot { fn default() -> Self { Self { - root: [0; 32], + merkle_root: [0; 32], max_total_claim: PodU64::from(0), - max_num_nodes: PodU64::from(0), + max_node_count: PodU64::from(0), reserved: [0; 64], } } } -impl MerkleRoot { +impl Ballot { pub fn new(root: [u8; 32], max_total_claim: u64, max_num_nodes: u64) -> Self { Self { - root, + merkle_root: root, max_total_claim: PodU64::from(max_total_claim), - max_num_nodes: PodU64::from(max_num_nodes), + max_node_count: PodU64::from(max_num_nodes), reserved: [0; 64], } } pub fn root(&self) -> [u8; 32] { - self.root + self.merkle_root } pub fn max_total_claim(&self) -> u64 { self.max_total_claim.into() } - pub fn max_num_nodes(&self) -> u64 { - self.max_num_nodes.into() + pub fn max_node_count(&self) -> u64 { + self.max_node_count.into() } } #[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, ShankType)] #[repr(C)] -pub struct MerkleRootTally { - merkle_root: MerkleRoot, +pub struct BallotTally { + ballot: Ballot, stake_weight: PodU128, - vote_count: PodU64, + tally: PodU64, reserved: [u8; 64], } -impl Default for MerkleRootTally { +impl Default for BallotTally { fn default() -> Self { Self { - merkle_root: MerkleRoot::default(), + ballot: Ballot::default(), stake_weight: PodU128::from(0), - vote_count: PodU64::from(0), + tally: PodU64::from(0), reserved: [0; 64], } } } -impl MerkleRootTally { - pub fn new( - root: [u8; 32], - max_total_claim: u64, - max_num_nodes: u64, - stake_weight: u128, - ) -> Self { +impl BallotTally { + pub fn new(ballot: Ballot, stake_weight: u128) -> Self { Self { - merkle_root: MerkleRoot::new(root, max_total_claim, max_num_nodes), + ballot, stake_weight: PodU128::from(stake_weight), - vote_count: PodU64::from(1), + tally: PodU64::from(1), reserved: [0; 64], } } - pub fn merkle_root(&self) -> MerkleRoot { - self.merkle_root + pub fn ballot(&self) -> Ballot { + self.ballot } pub fn stake_weight(&self) -> u128 { self.stake_weight.into() } - pub fn vote_count(&self) -> u64 { - self.vote_count.into() + pub fn tally(&self) -> u64 { + self.tally.into() + } + + pub fn is_empty(&self) -> bool { + self.stake_weight() == 0 } pub fn increment_tally(&mut self, stake_weight: u128) -> Result<(), TipRouterError> { @@ -106,8 +108,8 @@ impl MerkleRootTally { .checked_add(stake_weight) .ok_or(TipRouterError::ArithmeticOverflow)?, ); - self.vote_count = PodU64::from( - self.vote_count() + self.tally = PodU64::from( + self.tally() .checked_add(1) .ok_or(TipRouterError::ArithmeticOverflow)?, ); @@ -122,7 +124,7 @@ pub struct OperatorVote { operator: Pubkey, slot_voted: PodU64, stake_weight: PodU128, - merkle_root: MerkleRoot, + ballot: Ballot, reserved: [u8; 64], } @@ -132,24 +134,17 @@ impl Default for OperatorVote { operator: Pubkey::default(), slot_voted: PodU64::from(0), stake_weight: PodU128::from(0), - merkle_root: MerkleRoot::default(), + ballot: Ballot::default(), reserved: [0; 64], } } } impl OperatorVote { - pub fn new( - root: [u8; 32], - max_total_claim: u64, - max_num_nodes: u64, - operator: Pubkey, - current_slot: u64, - stake_weight: u128, - ) -> Self { + pub fn new(ballot: Ballot, operator: Pubkey, current_slot: u64, stake_weight: u128) -> Self { Self { operator, - merkle_root: MerkleRoot::new(root, max_total_claim, max_num_nodes), + ballot, slot_voted: PodU64::from(current_slot), stake_weight: PodU128::from(stake_weight), reserved: [0; 64], @@ -168,8 +163,12 @@ impl OperatorVote { self.stake_weight.into() } - pub fn merkle_root(&self) -> MerkleRoot { - self.merkle_root + pub fn ballot(&self) -> Ballot { + self.ballot + } + + pub fn is_empty(&self) -> bool { + self.stake_weight() == 0 } } @@ -189,10 +188,10 @@ pub struct BallotBox { reserved: [u8; 128], operators_voted: PodU64, - unique_merkle_roots: PodU64, + unique_ballots: PodU64, operator_votes: [OperatorVote; 256], - merkle_root_tallies: [MerkleRootTally; 256], + ballot_tallies: [BallotTally; 256], } impl Discriminator for BallotBox { @@ -208,9 +207,9 @@ impl BallotBox { slot_created: PodU64::from(current_slot), slot_consensus_reached: PodU64::from(0), operators_voted: PodU64::from(0), - unique_merkle_roots: PodU64::from(0), + unique_ballots: PodU64::from(0), operator_votes: [OperatorVote::default(); MAX_OPERATORS], - merkle_root_tallies: [MerkleRootTally::default(); MAX_OPERATORS], + ballot_tallies: [BallotTally::default(); MAX_OPERATORS], reserved: [0; 128], } } @@ -271,10 +270,107 @@ impl BallotBox { Ok(()) } - fn insert_or_create_merkle_root_tally( + fn slot_consensus_reached(&self) -> u64 { + self.slot_consensus_reached.into() + } + + fn unique_ballots(&self) -> u64 { + self.unique_ballots.into() + } + + fn operators_voted(&self) -> u64 { + self.operators_voted.into() + } + + fn increment_or_create_ballot_tally( &mut self, - merkle_root: &MerkleRoot, + operator_vote: &OperatorVote, ) -> Result<(), TipRouterError> { + for tally in self.ballot_tallies.iter_mut() { + if tally.ballot.root().eq(&operator_vote.ballot().root()) { + tally.increment_tally(operator_vote.stake_weight())?; + return Ok(()); + } + + if tally.is_empty() { + *tally = BallotTally::new(operator_vote.ballot(), operator_vote.stake_weight()); + + self.unique_ballots = PodU64::from( + self.unique_ballots() + .checked_add(1) + .ok_or(TipRouterError::ArithmeticOverflow)?, + ); + + return Ok(()); + } + } + + Err(TipRouterError::BallotTallyFull.into()) + } + + pub fn cast_vote( + &mut self, + operator: Pubkey, + ballot: Ballot, + stake_weight: u128, + current_slot: u64, + ) -> Result<(), TipRouterError> { + for vote in self.operator_votes.iter_mut() { + if vote.operator().eq(&operator) { + return Err(TipRouterError::DuplicateVoteCast.into()); + } + + if vote.is_empty() { + let operator_vote = OperatorVote::new(ballot, operator, current_slot, stake_weight); + *vote = operator_vote; + + self.increment_or_create_ballot_tally(&operator_vote)?; + + self.operators_voted = PodU64::from( + self.operators_voted() + .checked_add(1) + .ok_or(TipRouterError::ArithmeticOverflow)?, + ); + + return Ok(()); + } + } + + Err(TipRouterError::OperatorVotesFull.into()) + } + + //Not sure where/how this should be used + pub fn tally_votes( + &mut self, + total_stake_weight: u128, + current_slot: u64, + ) -> Result<(), TipRouterError> { + let max_tally = self + .ballot_tallies + .iter() + .max_by_key(|t| t.stake_weight()) + .unwrap(); + + let ballot_stake_weight = max_tally.stake_weight(); + let precise_ballot_stake_weight = + PreciseNumber::new(ballot_stake_weight).ok_or(TipRouterError::NewPreciseNumberError)?; + let precise_total_stake_weight = + PreciseNumber::new(total_stake_weight).ok_or(TipRouterError::NewPreciseNumberError)?; + + let ballot_percentage_of_total = precise_ballot_stake_weight + .checked_div(&precise_total_stake_weight) + .ok_or(TipRouterError::DenominatorIsZero)?; + + let target_precise_percentage = + PreciseNumber::new(PRECISE_CONSENSUS).ok_or(TipRouterError::NewPreciseNumberError)?; + + let consensus_reached = + ballot_percentage_of_total.greater_than_or_equal(&target_precise_percentage); + + if consensus_reached && self.slot_consensus_reached() != 0 { + self.slot_consensus_reached = PodU64::from(current_slot); + } + Ok(()) } } diff --git a/core/src/constants.rs b/core/src/constants.rs index 01586c40..df8fbe01 100644 --- a/core/src/constants.rs +++ b/core/src/constants.rs @@ -1,3 +1,4 @@ pub const MAX_FEE_BPS: u64 = 10_000; pub const MAX_OPERATORS: usize = 256; pub const MAX_VAULT_OPERATOR_DELEGATIONS: usize = 64; +pub const PRECISE_CONSENSUS: u128 = 666_666_666_666; diff --git a/core/src/error.rs b/core/src/error.rs index b7ba61b0..74e79908 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -64,6 +64,12 @@ pub enum TipRouterError { TooManyVaultOperatorDelegations, #[error("Duplicate vault operator delegation")] DuplicateVaultOperatorDelegation, + #[error("Duplicate Vote Cast")] + DuplicateVoteCast, + #[error("Operator votes full")] + OperatorVotesFull, + #[error("Merkle root tally full")] + BallotTallyFull, } impl DecodeError for TipRouterError { From 60a6d6821157bcc3e834bcf34eea0da77b7eba71 Mon Sep 17 00:00:00 2001 From: Christian Krueger Date: Tue, 19 Nov 2024 14:41:13 -0600 Subject: [PATCH 3/5] structs there --- .../js/jito_tip_router/accounts/ballotBox.ts | 163 ++++++++++++++ clients/js/jito_tip_router/accounts/index.ts | 1 + .../jito_tip_router/errors/jitoTipRouter.ts | 12 ++ .../jito_tip_router/programs/jitoTipRouter.ts | 1 + clients/js/jito_tip_router/types/ballot.ts | 59 +++++ .../js/jito_tip_router/types/ballotTally.ts | 67 ++++++ clients/js/jito_tip_router/types/index.ts | 3 + .../js/jito_tip_router/types/operatorVote.ts | 74 +++++++ .../src/generated/accounts/ballot_box.rs | 75 +++++++ .../src/generated/accounts/mod.rs | 5 +- .../src/generated/errors/jito_tip_router.rs | 9 + .../src/generated/types/ballot.rs | 17 ++ .../src/generated/types/ballot_tally.rs | 19 ++ .../src/generated/types/mod.rs | 7 +- .../src/generated/types/operator_vote.rs | 25 +++ core/src/ballot_box.rs | 36 ++-- core/src/error.rs | 2 + idl/jito_tip_router.json | 204 ++++++++++++++++++ 18 files changed, 758 insertions(+), 21 deletions(-) create mode 100644 clients/js/jito_tip_router/accounts/ballotBox.ts create mode 100644 clients/js/jito_tip_router/types/ballot.ts create mode 100644 clients/js/jito_tip_router/types/ballotTally.ts create mode 100644 clients/js/jito_tip_router/types/operatorVote.ts create mode 100644 clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs create mode 100644 clients/rust/jito_tip_router/src/generated/types/ballot.rs create mode 100644 clients/rust/jito_tip_router/src/generated/types/ballot_tally.rs create mode 100644 clients/rust/jito_tip_router/src/generated/types/operator_vote.rs diff --git a/clients/js/jito_tip_router/accounts/ballotBox.ts b/clients/js/jito_tip_router/accounts/ballotBox.ts new file mode 100644 index 00000000..5c2ec9df --- /dev/null +++ b/clients/js/jito_tip_router/accounts/ballotBox.ts @@ -0,0 +1,163 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + assertAccountExists, + assertAccountsExist, + combineCodec, + decodeAccount, + fetchEncodedAccount, + fetchEncodedAccounts, + getAddressDecoder, + getAddressEncoder, + getArrayDecoder, + getArrayEncoder, + getStructDecoder, + getStructEncoder, + getU64Decoder, + getU64Encoder, + getU8Decoder, + getU8Encoder, + type Account, + type Address, + type Codec, + type Decoder, + type EncodedAccount, + type Encoder, + type FetchAccountConfig, + type FetchAccountsConfig, + type MaybeAccount, + type MaybeEncodedAccount, +} from '@solana/web3.js'; +import { + getBallotTallyDecoder, + getBallotTallyEncoder, + getOperatorVoteDecoder, + getOperatorVoteEncoder, + type BallotTally, + type BallotTallyArgs, + type OperatorVote, + type OperatorVoteArgs, +} from '../types'; + +export type BallotBox = { + discriminator: bigint; + ncn: Address; + ncnEpoch: bigint; + bump: number; + slotCreated: bigint; + slotConsensusReached: bigint; + reserved: Array; + operatorsVoted: bigint; + uniqueBallots: bigint; + operatorVotes: Array; + ballotTallies: Array; +}; + +export type BallotBoxArgs = { + discriminator: number | bigint; + ncn: Address; + ncnEpoch: number | bigint; + bump: number; + slotCreated: number | bigint; + slotConsensusReached: number | bigint; + reserved: Array; + operatorsVoted: number | bigint; + uniqueBallots: number | bigint; + operatorVotes: Array; + ballotTallies: Array; +}; + +export function getBallotBoxEncoder(): Encoder { + return getStructEncoder([ + ['discriminator', getU64Encoder()], + ['ncn', getAddressEncoder()], + ['ncnEpoch', getU64Encoder()], + ['bump', getU8Encoder()], + ['slotCreated', getU64Encoder()], + ['slotConsensusReached', getU64Encoder()], + ['reserved', getArrayEncoder(getU8Encoder(), { size: 128 })], + ['operatorsVoted', getU64Encoder()], + ['uniqueBallots', getU64Encoder()], + ['operatorVotes', getArrayEncoder(getOperatorVoteEncoder(), { size: 32 })], + ['ballotTallies', getArrayEncoder(getBallotTallyEncoder(), { size: 32 })], + ]); +} + +export function getBallotBoxDecoder(): Decoder { + return getStructDecoder([ + ['discriminator', getU64Decoder()], + ['ncn', getAddressDecoder()], + ['ncnEpoch', getU64Decoder()], + ['bump', getU8Decoder()], + ['slotCreated', getU64Decoder()], + ['slotConsensusReached', getU64Decoder()], + ['reserved', getArrayDecoder(getU8Decoder(), { size: 128 })], + ['operatorsVoted', getU64Decoder()], + ['uniqueBallots', getU64Decoder()], + ['operatorVotes', getArrayDecoder(getOperatorVoteDecoder(), { size: 32 })], + ['ballotTallies', getArrayDecoder(getBallotTallyDecoder(), { size: 32 })], + ]); +} + +export function getBallotBoxCodec(): Codec { + return combineCodec(getBallotBoxEncoder(), getBallotBoxDecoder()); +} + +export function decodeBallotBox( + encodedAccount: EncodedAccount +): Account; +export function decodeBallotBox( + encodedAccount: MaybeEncodedAccount +): MaybeAccount; +export function decodeBallotBox( + encodedAccount: EncodedAccount | MaybeEncodedAccount +): Account | MaybeAccount { + return decodeAccount( + encodedAccount as MaybeEncodedAccount, + getBallotBoxDecoder() + ); +} + +export async function fetchBallotBox( + rpc: Parameters[0], + address: Address, + config?: FetchAccountConfig +): Promise> { + const maybeAccount = await fetchMaybeBallotBox(rpc, address, config); + assertAccountExists(maybeAccount); + return maybeAccount; +} + +export async function fetchMaybeBallotBox( + rpc: Parameters[0], + address: Address, + config?: FetchAccountConfig +): Promise> { + const maybeAccount = await fetchEncodedAccount(rpc, address, config); + return decodeBallotBox(maybeAccount); +} + +export async function fetchAllBallotBox( + rpc: Parameters[0], + addresses: Array
, + config?: FetchAccountsConfig +): Promise[]> { + const maybeAccounts = await fetchAllMaybeBallotBox(rpc, addresses, config); + assertAccountsExist(maybeAccounts); + return maybeAccounts; +} + +export async function fetchAllMaybeBallotBox( + rpc: Parameters[0], + addresses: Array
, + config?: FetchAccountsConfig +): Promise[]> { + const maybeAccounts = await fetchEncodedAccounts(rpc, addresses, config); + return maybeAccounts.map((maybeAccount) => decodeBallotBox(maybeAccount)); +} diff --git a/clients/js/jito_tip_router/accounts/index.ts b/clients/js/jito_tip_router/accounts/index.ts index 549d9e82..9e306afb 100644 --- a/clients/js/jito_tip_router/accounts/index.ts +++ b/clients/js/jito_tip_router/accounts/index.ts @@ -6,6 +6,7 @@ * @see https://github.com/kinobi-so/kinobi */ +export * from './ballotBox'; export * from './epochSnapshot'; export * from './ncnConfig'; export * from './operatorSnapshot'; diff --git a/clients/js/jito_tip_router/errors/jitoTipRouter.ts b/clients/js/jito_tip_router/errors/jitoTipRouter.ts index 45acb0fa..ea089024 100644 --- a/clients/js/jito_tip_router/errors/jitoTipRouter.ts +++ b/clients/js/jito_tip_router/errors/jitoTipRouter.ts @@ -74,9 +74,16 @@ export const JITO_TIP_ROUTER_ERROR__OPERATOR_FINALIZED = 0x2216; // 8726 export const JITO_TIP_ROUTER_ERROR__TOO_MANY_VAULT_OPERATOR_DELEGATIONS = 0x2217; // 8727 /** DuplicateVaultOperatorDelegation: Duplicate vault operator delegation */ export const JITO_TIP_ROUTER_ERROR__DUPLICATE_VAULT_OPERATOR_DELEGATION = 0x2218; // 8728 +/** DuplicateVoteCast: Duplicate Vote Cast */ +export const JITO_TIP_ROUTER_ERROR__DUPLICATE_VOTE_CAST = 0x2219; // 8729 +/** OperatorVotesFull: Operator votes full */ +export const JITO_TIP_ROUTER_ERROR__OPERATOR_VOTES_FULL = 0x221a; // 8730 +/** BallotTallyFull: Merkle root tally full */ +export const JITO_TIP_ROUTER_ERROR__BALLOT_TALLY_FULL = 0x221b; // 8731 export type JitoTipRouterError = | typeof JITO_TIP_ROUTER_ERROR__ARITHMETIC_OVERFLOW + | typeof JITO_TIP_ROUTER_ERROR__BALLOT_TALLY_FULL | typeof JITO_TIP_ROUTER_ERROR__CANNOT_CREATE_FUTURE_WEIGHT_TABLES | typeof JITO_TIP_ROUTER_ERROR__CAST_TO_IMPRECISE_NUMBER_ERROR | typeof JITO_TIP_ROUTER_ERROR__CONFIG_MINT_LIST_FULL @@ -84,6 +91,7 @@ export type JitoTipRouterError = | typeof JITO_TIP_ROUTER_ERROR__DENOMINATOR_IS_ZERO | typeof JITO_TIP_ROUTER_ERROR__DUPLICATE_MINTS_IN_TABLE | typeof JITO_TIP_ROUTER_ERROR__DUPLICATE_VAULT_OPERATOR_DELEGATION + | typeof JITO_TIP_ROUTER_ERROR__DUPLICATE_VOTE_CAST | typeof JITO_TIP_ROUTER_ERROR__FEE_CAP_EXCEEDED | typeof JITO_TIP_ROUTER_ERROR__INCORRECT_FEE_ADMIN | typeof JITO_TIP_ROUTER_ERROR__INCORRECT_NCN @@ -95,6 +103,7 @@ export type JitoTipRouterError = | typeof JITO_TIP_ROUTER_ERROR__NO_MINTS_IN_TABLE | typeof JITO_TIP_ROUTER_ERROR__NO_OPERATORS | typeof JITO_TIP_ROUTER_ERROR__OPERATOR_FINALIZED + | typeof JITO_TIP_ROUTER_ERROR__OPERATOR_VOTES_FULL | typeof JITO_TIP_ROUTER_ERROR__TOO_MANY_MINTS_FOR_TABLE | typeof JITO_TIP_ROUTER_ERROR__TOO_MANY_VAULT_OPERATOR_DELEGATIONS | typeof JITO_TIP_ROUTER_ERROR__TRACKED_MINT_LIST_FULL @@ -111,6 +120,7 @@ let jitoTipRouterErrorMessages: Record | undefined; if (process.env.NODE_ENV !== 'production') { jitoTipRouterErrorMessages = { [JITO_TIP_ROUTER_ERROR__ARITHMETIC_OVERFLOW]: `Overflow`, + [JITO_TIP_ROUTER_ERROR__BALLOT_TALLY_FULL]: `Merkle root tally full`, [JITO_TIP_ROUTER_ERROR__CANNOT_CREATE_FUTURE_WEIGHT_TABLES]: `Cannnot create future weight tables`, [JITO_TIP_ROUTER_ERROR__CAST_TO_IMPRECISE_NUMBER_ERROR]: `Cast to imprecise number error`, [JITO_TIP_ROUTER_ERROR__CONFIG_MINT_LIST_FULL]: `NCN config vaults are at capacity`, @@ -118,6 +128,7 @@ if (process.env.NODE_ENV !== 'production') { [JITO_TIP_ROUTER_ERROR__DENOMINATOR_IS_ZERO]: `Zero in the denominator`, [JITO_TIP_ROUTER_ERROR__DUPLICATE_MINTS_IN_TABLE]: `Duplicate mints in table`, [JITO_TIP_ROUTER_ERROR__DUPLICATE_VAULT_OPERATOR_DELEGATION]: `Duplicate vault operator delegation`, + [JITO_TIP_ROUTER_ERROR__DUPLICATE_VOTE_CAST]: `Duplicate Vote Cast`, [JITO_TIP_ROUTER_ERROR__FEE_CAP_EXCEEDED]: `Fee cap exceeded`, [JITO_TIP_ROUTER_ERROR__INCORRECT_FEE_ADMIN]: `Incorrect fee admin`, [JITO_TIP_ROUTER_ERROR__INCORRECT_NCN]: `Incorrect NCN`, @@ -129,6 +140,7 @@ if (process.env.NODE_ENV !== 'production') { [JITO_TIP_ROUTER_ERROR__NO_MINTS_IN_TABLE]: `There are no mints in the table`, [JITO_TIP_ROUTER_ERROR__NO_OPERATORS]: `No operators in ncn`, [JITO_TIP_ROUTER_ERROR__OPERATOR_FINALIZED]: `Operator is already finalized - should not happen`, + [JITO_TIP_ROUTER_ERROR__OPERATOR_VOTES_FULL]: `Operator votes full`, [JITO_TIP_ROUTER_ERROR__TOO_MANY_MINTS_FOR_TABLE]: `Too many mints for table`, [JITO_TIP_ROUTER_ERROR__TOO_MANY_VAULT_OPERATOR_DELEGATIONS]: `Too many vault operator delegations`, [JITO_TIP_ROUTER_ERROR__TRACKED_MINT_LIST_FULL]: `Tracked mints are at capacity`, diff --git a/clients/js/jito_tip_router/programs/jitoTipRouter.ts b/clients/js/jito_tip_router/programs/jitoTipRouter.ts index 29a98a9d..3c570619 100644 --- a/clients/js/jito_tip_router/programs/jitoTipRouter.ts +++ b/clients/js/jito_tip_router/programs/jitoTipRouter.ts @@ -29,6 +29,7 @@ export const JITO_TIP_ROUTER_PROGRAM_ADDRESS = 'Fv9aHCgvPQSr4jg9W8eTS6Ys1SNmh2qjyATrbsjEMaSH' as Address<'Fv9aHCgvPQSr4jg9W8eTS6Ys1SNmh2qjyATrbsjEMaSH'>; export enum JitoTipRouterAccount { + BallotBox, EpochSnapshot, OperatorSnapshot, NcnConfig, diff --git a/clients/js/jito_tip_router/types/ballot.ts b/clients/js/jito_tip_router/types/ballot.ts new file mode 100644 index 00000000..c2ee14f7 --- /dev/null +++ b/clients/js/jito_tip_router/types/ballot.ts @@ -0,0 +1,59 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + combineCodec, + fixDecoderSize, + fixEncoderSize, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + getU64Decoder, + getU64Encoder, + type Codec, + type Decoder, + type Encoder, + type ReadonlyUint8Array, +} from '@solana/web3.js'; + +export type Ballot = { + merkleRoot: ReadonlyUint8Array; + maxTotalClaim: bigint; + maxNodeCount: bigint; + reserved: ReadonlyUint8Array; +}; + +export type BallotArgs = { + merkleRoot: ReadonlyUint8Array; + maxTotalClaim: number | bigint; + maxNodeCount: number | bigint; + reserved: ReadonlyUint8Array; +}; + +export function getBallotEncoder(): Encoder { + return getStructEncoder([ + ['merkleRoot', fixEncoderSize(getBytesEncoder(), 32)], + ['maxTotalClaim', getU64Encoder()], + ['maxNodeCount', getU64Encoder()], + ['reserved', fixEncoderSize(getBytesEncoder(), 64)], + ]); +} + +export function getBallotDecoder(): Decoder { + return getStructDecoder([ + ['merkleRoot', fixDecoderSize(getBytesDecoder(), 32)], + ['maxTotalClaim', getU64Decoder()], + ['maxNodeCount', getU64Decoder()], + ['reserved', fixDecoderSize(getBytesDecoder(), 64)], + ]); +} + +export function getBallotCodec(): Codec { + return combineCodec(getBallotEncoder(), getBallotDecoder()); +} diff --git a/clients/js/jito_tip_router/types/ballotTally.ts b/clients/js/jito_tip_router/types/ballotTally.ts new file mode 100644 index 00000000..99e151e1 --- /dev/null +++ b/clients/js/jito_tip_router/types/ballotTally.ts @@ -0,0 +1,67 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + combineCodec, + fixDecoderSize, + fixEncoderSize, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + getU128Decoder, + getU128Encoder, + getU64Decoder, + getU64Encoder, + type Codec, + type Decoder, + type Encoder, + type ReadonlyUint8Array, +} from '@solana/web3.js'; +import { + getBallotDecoder, + getBallotEncoder, + type Ballot, + type BallotArgs, +} from '.'; + +export type BallotTally = { + ballot: Ballot; + stakeWeight: bigint; + tally: bigint; + reserved: ReadonlyUint8Array; +}; + +export type BallotTallyArgs = { + ballot: BallotArgs; + stakeWeight: number | bigint; + tally: number | bigint; + reserved: ReadonlyUint8Array; +}; + +export function getBallotTallyEncoder(): Encoder { + return getStructEncoder([ + ['ballot', getBallotEncoder()], + ['stakeWeight', getU128Encoder()], + ['tally', getU64Encoder()], + ['reserved', fixEncoderSize(getBytesEncoder(), 64)], + ]); +} + +export function getBallotTallyDecoder(): Decoder { + return getStructDecoder([ + ['ballot', getBallotDecoder()], + ['stakeWeight', getU128Decoder()], + ['tally', getU64Decoder()], + ['reserved', fixDecoderSize(getBytesDecoder(), 64)], + ]); +} + +export function getBallotTallyCodec(): Codec { + return combineCodec(getBallotTallyEncoder(), getBallotTallyDecoder()); +} diff --git a/clients/js/jito_tip_router/types/index.ts b/clients/js/jito_tip_router/types/index.ts index e93016f1..b49207d9 100644 --- a/clients/js/jito_tip_router/types/index.ts +++ b/clients/js/jito_tip_router/types/index.ts @@ -6,9 +6,12 @@ * @see https://github.com/kinobi-so/kinobi */ +export * from './ballot'; +export * from './ballotTally'; export * from './configAdminRole'; export * from './fee'; export * from './fees'; export * from './mintEntry'; +export * from './operatorVote'; export * from './vaultOperatorStakeWeight'; export * from './weightEntry'; diff --git a/clients/js/jito_tip_router/types/operatorVote.ts b/clients/js/jito_tip_router/types/operatorVote.ts new file mode 100644 index 00000000..a082c6a6 --- /dev/null +++ b/clients/js/jito_tip_router/types/operatorVote.ts @@ -0,0 +1,74 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + combineCodec, + fixDecoderSize, + fixEncoderSize, + getAddressDecoder, + getAddressEncoder, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + getU128Decoder, + getU128Encoder, + getU64Decoder, + getU64Encoder, + type Address, + type Codec, + type Decoder, + type Encoder, + type ReadonlyUint8Array, +} from '@solana/web3.js'; +import { + getBallotDecoder, + getBallotEncoder, + type Ballot, + type BallotArgs, +} from '.'; + +export type OperatorVote = { + operator: Address; + slotVoted: bigint; + stakeWeight: bigint; + ballot: Ballot; + reserved: ReadonlyUint8Array; +}; + +export type OperatorVoteArgs = { + operator: Address; + slotVoted: number | bigint; + stakeWeight: number | bigint; + ballot: BallotArgs; + reserved: ReadonlyUint8Array; +}; + +export function getOperatorVoteEncoder(): Encoder { + return getStructEncoder([ + ['operator', getAddressEncoder()], + ['slotVoted', getU64Encoder()], + ['stakeWeight', getU128Encoder()], + ['ballot', getBallotEncoder()], + ['reserved', fixEncoderSize(getBytesEncoder(), 64)], + ]); +} + +export function getOperatorVoteDecoder(): Decoder { + return getStructDecoder([ + ['operator', getAddressDecoder()], + ['slotVoted', getU64Decoder()], + ['stakeWeight', getU128Decoder()], + ['ballot', getBallotDecoder()], + ['reserved', fixDecoderSize(getBytesDecoder(), 64)], + ]); +} + +export function getOperatorVoteCodec(): Codec { + return combineCodec(getOperatorVoteEncoder(), getOperatorVoteDecoder()); +} diff --git a/clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs b/clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs new file mode 100644 index 00000000..640dc29a --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs @@ -0,0 +1,75 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! + +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::pubkey::Pubkey; + +use crate::generated::types::{BallotTally, OperatorVote}; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BallotBox { + pub discriminator: u64, + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::") + )] + pub ncn: Pubkey, + pub ncn_epoch: u64, + pub bump: u8, + pub slot_created: u64, + pub slot_consensus_reached: u64, + #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] + pub reserved: [u8; 128], + pub operators_voted: u64, + pub unique_ballots: u64, + pub operator_votes: [OperatorVote; 32], + pub ballot_tallies: [BallotTally; 32], +} + +impl BallotBox { + #[inline(always)] + pub fn from_bytes(data: &[u8]) -> Result { + let mut data = data; + Self::deserialize(&mut data) + } +} + +impl<'a> TryFrom<&solana_program::account_info::AccountInfo<'a>> for BallotBox { + type Error = std::io::Error; + + fn try_from( + account_info: &solana_program::account_info::AccountInfo<'a>, + ) -> Result { + let mut data: &[u8] = &(*account_info.data).borrow(); + Self::deserialize(&mut data) + } +} + +#[cfg(feature = "anchor")] +impl anchor_lang::AccountDeserialize for BallotBox { + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + Ok(Self::deserialize(buf)?) + } +} + +#[cfg(feature = "anchor")] +impl anchor_lang::AccountSerialize for BallotBox {} + +#[cfg(feature = "anchor")] +impl anchor_lang::Owner for BallotBox { + fn owner() -> Pubkey { + crate::JITO_TIP_ROUTER_ID + } +} + +#[cfg(feature = "anchor-idl-build")] +impl anchor_lang::IdlBuild for BallotBox {} + +#[cfg(feature = "anchor-idl-build")] +impl anchor_lang::Discriminator for BallotBox { + const DISCRIMINATOR: [u8; 8] = [0; 8]; +} diff --git a/clients/rust/jito_tip_router/src/generated/accounts/mod.rs b/clients/rust/jito_tip_router/src/generated/accounts/mod.rs index 7b099a2d..aa0f2538 100644 --- a/clients/rust/jito_tip_router/src/generated/accounts/mod.rs +++ b/clients/rust/jito_tip_router/src/generated/accounts/mod.rs @@ -4,6 +4,7 @@ //! //! +pub(crate) mod r#ballot_box; pub(crate) mod r#epoch_snapshot; pub(crate) mod r#ncn_config; pub(crate) mod r#operator_snapshot; @@ -11,6 +12,6 @@ pub(crate) mod r#tracked_mints; pub(crate) mod r#weight_table; pub use self::{ - r#epoch_snapshot::*, r#ncn_config::*, r#operator_snapshot::*, r#tracked_mints::*, - r#weight_table::*, + r#ballot_box::*, r#epoch_snapshot::*, r#ncn_config::*, r#operator_snapshot::*, + r#tracked_mints::*, r#weight_table::*, }; diff --git a/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs b/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs index 60dd7d63..491e234a 100644 --- a/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs +++ b/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs @@ -99,6 +99,15 @@ pub enum JitoTipRouterError { /// 8728 - Duplicate vault operator delegation #[error("Duplicate vault operator delegation")] DuplicateVaultOperatorDelegation = 0x2218, + /// 8729 - Duplicate Vote Cast + #[error("Duplicate Vote Cast")] + DuplicateVoteCast = 0x2219, + /// 8730 - Operator votes full + #[error("Operator votes full")] + OperatorVotesFull = 0x221A, + /// 8731 - Merkle root tally full + #[error("Merkle root tally full")] + BallotTallyFull = 0x221B, } impl solana_program::program_error::PrintProgramError for JitoTipRouterError { diff --git a/clients/rust/jito_tip_router/src/generated/types/ballot.rs b/clients/rust/jito_tip_router/src/generated/types/ballot.rs new file mode 100644 index 00000000..1cd5b532 --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/types/ballot.rs @@ -0,0 +1,17 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! + +use borsh::{BorshDeserialize, BorshSerialize}; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Ballot { + pub merkle_root: [u8; 32], + pub max_total_claim: u64, + pub max_node_count: u64, + #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] + pub reserved: [u8; 64], +} diff --git a/clients/rust/jito_tip_router/src/generated/types/ballot_tally.rs b/clients/rust/jito_tip_router/src/generated/types/ballot_tally.rs new file mode 100644 index 00000000..265dcf1f --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/types/ballot_tally.rs @@ -0,0 +1,19 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! + +use borsh::{BorshDeserialize, BorshSerialize}; + +use crate::generated::types::Ballot; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BallotTally { + pub ballot: Ballot, + pub stake_weight: u128, + pub tally: u64, + #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] + pub reserved: [u8; 64], +} diff --git a/clients/rust/jito_tip_router/src/generated/types/mod.rs b/clients/rust/jito_tip_router/src/generated/types/mod.rs index f19946a2..82ebe6f4 100644 --- a/clients/rust/jito_tip_router/src/generated/types/mod.rs +++ b/clients/rust/jito_tip_router/src/generated/types/mod.rs @@ -4,14 +4,17 @@ //! //! +pub(crate) mod r#ballot; +pub(crate) mod r#ballot_tally; pub(crate) mod r#config_admin_role; pub(crate) mod r#fee; pub(crate) mod r#fees; pub(crate) mod r#mint_entry; +pub(crate) mod r#operator_vote; pub(crate) mod r#vault_operator_stake_weight; pub(crate) mod r#weight_entry; pub use self::{ - r#config_admin_role::*, r#fee::*, r#fees::*, r#mint_entry::*, r#vault_operator_stake_weight::*, - r#weight_entry::*, + r#ballot::*, r#ballot_tally::*, r#config_admin_role::*, r#fee::*, r#fees::*, r#mint_entry::*, + r#operator_vote::*, r#vault_operator_stake_weight::*, r#weight_entry::*, }; diff --git a/clients/rust/jito_tip_router/src/generated/types/operator_vote.rs b/clients/rust/jito_tip_router/src/generated/types/operator_vote.rs new file mode 100644 index 00000000..f35d8d65 --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/types/operator_vote.rs @@ -0,0 +1,25 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! + +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::pubkey::Pubkey; + +use crate::generated::types::Ballot; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct OperatorVote { + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::") + )] + pub operator: Pubkey, + pub slot_voted: u64, + pub stake_weight: u128, + pub ballot: Ballot, + #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] + pub reserved: [u8; 64], +} diff --git a/core/src/ballot_box.rs b/core/src/ballot_box.rs index 2c330317..f449bb24 100644 --- a/core/src/ballot_box.rs +++ b/core/src/ballot_box.rs @@ -7,11 +7,7 @@ use shank::{ShankAccount, ShankType}; use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; use spl_math::precise_number::PreciseNumber; -use crate::{ - constants::{MAX_OPERATORS, PRECISE_CONSENSUS}, - discriminators::Discriminators, - error::TipRouterError, -}; +use crate::{constants::PRECISE_CONSENSUS, discriminators::Discriminators, error::TipRouterError}; #[derive(Debug, Clone, PartialEq, Eq, Copy, Zeroable, ShankType, Pod, ShankType)] #[repr(C)] @@ -43,7 +39,7 @@ impl Ballot { } } - pub fn root(&self) -> [u8; 32] { + pub const fn root(&self) -> [u8; 32] { self.merkle_root } @@ -86,7 +82,7 @@ impl BallotTally { } } - pub fn ballot(&self) -> Ballot { + pub const fn ballot(&self) -> Ballot { self.ballot } @@ -151,7 +147,7 @@ impl OperatorVote { } } - pub fn operator(&self) -> Pubkey { + pub const fn operator(&self) -> Pubkey { self.operator } @@ -163,7 +159,7 @@ impl OperatorVote { self.stake_weight.into() } - pub fn ballot(&self) -> Ballot { + pub const fn ballot(&self) -> Ballot { self.ballot } @@ -190,8 +186,9 @@ pub struct BallotBox { operators_voted: PodU64, unique_ballots: PodU64, - operator_votes: [OperatorVote; 256], - ballot_tallies: [BallotTally; 256], + //TODO fix 32 -> MAX_OPERATORS + operator_votes: [OperatorVote; 32], + ballot_tallies: [BallotTally; 32], } impl Discriminator for BallotBox { @@ -208,8 +205,9 @@ impl BallotBox { slot_consensus_reached: PodU64::from(0), operators_voted: PodU64::from(0), unique_ballots: PodU64::from(0), - operator_votes: [OperatorVote::default(); MAX_OPERATORS], - ballot_tallies: [BallotTally::default(); MAX_OPERATORS], + //TODO fix 32 -> MAX_OPERATORS + operator_votes: [OperatorVote::default(); 32], + ballot_tallies: [BallotTally::default(); 32], reserved: [0; 128], } } @@ -305,7 +303,7 @@ impl BallotBox { } } - Err(TipRouterError::BallotTallyFull.into()) + Err(TipRouterError::BallotTallyFull) } pub fn cast_vote( @@ -317,7 +315,7 @@ impl BallotBox { ) -> Result<(), TipRouterError> { for vote in self.operator_votes.iter_mut() { if vote.operator().eq(&operator) { - return Err(TipRouterError::DuplicateVoteCast.into()); + return Err(TipRouterError::DuplicateVoteCast); } if vote.is_empty() { @@ -336,7 +334,7 @@ impl BallotBox { } } - Err(TipRouterError::OperatorVotesFull.into()) + Err(TipRouterError::OperatorVotesFull) } //Not sure where/how this should be used @@ -345,6 +343,10 @@ impl BallotBox { total_stake_weight: u128, current_slot: u64, ) -> Result<(), TipRouterError> { + if self.slot_consensus_reached() != 0 { + return Err(TipRouterError::ConsensusAlreadyReached); + } + let max_tally = self .ballot_tallies .iter() @@ -367,7 +369,7 @@ impl BallotBox { let consensus_reached = ballot_percentage_of_total.greater_than_or_equal(&target_precise_percentage); - if consensus_reached && self.slot_consensus_reached() != 0 { + if consensus_reached { self.slot_consensus_reached = PodU64::from(current_slot); } diff --git a/core/src/error.rs b/core/src/error.rs index 74e79908..b133ff12 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -70,6 +70,8 @@ pub enum TipRouterError { OperatorVotesFull, #[error("Merkle root tally full")] BallotTallyFull, + #[error("Consensus already reached")] + ConsensusAlreadyReached, } impl DecodeError for TipRouterError { diff --git a/idl/jito_tip_router.json b/idl/jito_tip_router.json index 7f59e84b..1f9c8035 100644 --- a/idl/jito_tip_router.json +++ b/idl/jito_tip_router.json @@ -566,6 +566,83 @@ } ], "accounts": [ + { + "name": "BallotBox", + "type": { + "kind": "struct", + "fields": [ + { + "name": "ncn", + "type": "publicKey" + }, + { + "name": "ncnEpoch", + "type": { + "defined": "PodU64" + } + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "slotCreated", + "type": { + "defined": "PodU64" + } + }, + { + "name": "slotConsensusReached", + "type": { + "defined": "PodU64" + } + }, + { + "name": "reserved", + "type": { + "array": [ + "u8", + 128 + ] + } + }, + { + "name": "operatorsVoted", + "type": { + "defined": "PodU64" + } + }, + { + "name": "uniqueBallots", + "type": { + "defined": "PodU64" + } + }, + { + "name": "operatorVotes", + "type": { + "array": [ + { + "defined": "OperatorVote" + }, + 32 + ] + } + }, + { + "name": "ballotTallies", + "type": { + "array": [ + { + "defined": "BallotTally" + }, + 32 + ] + } + } + ] + } + }, { "name": "EpochSnapshot", "type": { @@ -870,6 +947,118 @@ } ], "types": [ + { + "name": "Ballot", + "type": { + "kind": "struct", + "fields": [ + { + "name": "merkleRoot", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "maxTotalClaim", + "type": { + "defined": "PodU64" + } + }, + { + "name": "maxNodeCount", + "type": { + "defined": "PodU64" + } + }, + { + "name": "reserved", + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + } + }, + { + "name": "BallotTally", + "type": { + "kind": "struct", + "fields": [ + { + "name": "ballot", + "type": { + "defined": "Ballot" + } + }, + { + "name": "stakeWeight", + "type": { + "defined": "PodU128" + } + }, + { + "name": "tally", + "type": { + "defined": "PodU64" + } + }, + { + "name": "reserved", + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + } + }, + { + "name": "OperatorVote", + "type": { + "kind": "struct", + "fields": [ + { + "name": "operator", + "type": "publicKey" + }, + { + "name": "slotVoted", + "type": { + "defined": "PodU64" + } + }, + { + "name": "stakeWeight", + "type": { + "defined": "PodU128" + } + }, + { + "name": "ballot", + "type": { + "defined": "Ballot" + } + }, + { + "name": "reserved", + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + } + }, { "name": "VaultOperatorStakeWeight", "type": { @@ -1190,6 +1379,21 @@ "code": 8728, "name": "DuplicateVaultOperatorDelegation", "msg": "Duplicate vault operator delegation" + }, + { + "code": 8729, + "name": "DuplicateVoteCast", + "msg": "Duplicate Vote Cast" + }, + { + "code": 8730, + "name": "OperatorVotesFull", + "msg": "Operator votes full" + }, + { + "code": 8731, + "name": "BallotTallyFull", + "msg": "Merkle root tally full" } ], "metadata": { From 42510f24e3606301c5c30316884c9d4576997728 Mon Sep 17 00:00:00 2001 From: Christian Krueger Date: Tue, 19 Nov 2024 16:30:25 -0600 Subject: [PATCH 4/5] good start --- .../js/jito_tip_router/accounts/ballotBox.ts | 8 ++ .../jito_tip_router/errors/jitoTipRouter.ts | 8 ++ clients/js/jito_tip_router/types/ballot.ts | 20 ++-- .../js/jito_tip_router/types/operatorVote.ts | 16 ++-- .../src/generated/accounts/ballot_box.rs | 3 +- .../src/generated/errors/jito_tip_router.rs | 6 ++ .../src/generated/types/ballot.rs | 3 +- .../src/generated/types/operator_vote.rs | 4 +- core/src/ballot_box.rs | 92 ++++++++++++------- core/src/error.rs | 2 + idl/jito_tip_router.json | 30 ++++-- 11 files changed, 117 insertions(+), 75 deletions(-) diff --git a/clients/js/jito_tip_router/accounts/ballotBox.ts b/clients/js/jito_tip_router/accounts/ballotBox.ts index 5c2ec9df..447e1b51 100644 --- a/clients/js/jito_tip_router/accounts/ballotBox.ts +++ b/clients/js/jito_tip_router/accounts/ballotBox.ts @@ -35,10 +35,14 @@ import { type MaybeEncodedAccount, } from '@solana/web3.js'; import { + getBallotDecoder, + getBallotEncoder, getBallotTallyDecoder, getBallotTallyEncoder, getOperatorVoteDecoder, getOperatorVoteEncoder, + type Ballot, + type BallotArgs, type BallotTally, type BallotTallyArgs, type OperatorVote, @@ -55,6 +59,7 @@ export type BallotBox = { reserved: Array; operatorsVoted: bigint; uniqueBallots: bigint; + winningBallot: Ballot; operatorVotes: Array; ballotTallies: Array; }; @@ -69,6 +74,7 @@ export type BallotBoxArgs = { reserved: Array; operatorsVoted: number | bigint; uniqueBallots: number | bigint; + winningBallot: BallotArgs; operatorVotes: Array; ballotTallies: Array; }; @@ -84,6 +90,7 @@ export function getBallotBoxEncoder(): Encoder { ['reserved', getArrayEncoder(getU8Encoder(), { size: 128 })], ['operatorsVoted', getU64Encoder()], ['uniqueBallots', getU64Encoder()], + ['winningBallot', getBallotEncoder()], ['operatorVotes', getArrayEncoder(getOperatorVoteEncoder(), { size: 32 })], ['ballotTallies', getArrayEncoder(getBallotTallyEncoder(), { size: 32 })], ]); @@ -100,6 +107,7 @@ export function getBallotBoxDecoder(): Decoder { ['reserved', getArrayDecoder(getU8Decoder(), { size: 128 })], ['operatorsVoted', getU64Decoder()], ['uniqueBallots', getU64Decoder()], + ['winningBallot', getBallotDecoder()], ['operatorVotes', getArrayDecoder(getOperatorVoteDecoder(), { size: 32 })], ['ballotTallies', getArrayDecoder(getBallotTallyDecoder(), { size: 32 })], ]); diff --git a/clients/js/jito_tip_router/errors/jitoTipRouter.ts b/clients/js/jito_tip_router/errors/jitoTipRouter.ts index ea089024..74b44962 100644 --- a/clients/js/jito_tip_router/errors/jitoTipRouter.ts +++ b/clients/js/jito_tip_router/errors/jitoTipRouter.ts @@ -80,6 +80,10 @@ export const JITO_TIP_ROUTER_ERROR__DUPLICATE_VOTE_CAST = 0x2219; // 8729 export const JITO_TIP_ROUTER_ERROR__OPERATOR_VOTES_FULL = 0x221a; // 8730 /** BallotTallyFull: Merkle root tally full */ export const JITO_TIP_ROUTER_ERROR__BALLOT_TALLY_FULL = 0x221b; // 8731 +/** ConsensusAlreadyReached: Consensus already reached */ +export const JITO_TIP_ROUTER_ERROR__CONSENSUS_ALREADY_REACHED = 0x221c; // 8732 +/** ConsensusNotReached: Consensus not reached */ +export const JITO_TIP_ROUTER_ERROR__CONSENSUS_NOT_REACHED = 0x221d; // 8733 export type JitoTipRouterError = | typeof JITO_TIP_ROUTER_ERROR__ARITHMETIC_OVERFLOW @@ -88,6 +92,8 @@ export type JitoTipRouterError = | typeof JITO_TIP_ROUTER_ERROR__CAST_TO_IMPRECISE_NUMBER_ERROR | typeof JITO_TIP_ROUTER_ERROR__CONFIG_MINT_LIST_FULL | typeof JITO_TIP_ROUTER_ERROR__CONFIG_MINTS_NOT_UPDATED + | typeof JITO_TIP_ROUTER_ERROR__CONSENSUS_ALREADY_REACHED + | typeof JITO_TIP_ROUTER_ERROR__CONSENSUS_NOT_REACHED | typeof JITO_TIP_ROUTER_ERROR__DENOMINATOR_IS_ZERO | typeof JITO_TIP_ROUTER_ERROR__DUPLICATE_MINTS_IN_TABLE | typeof JITO_TIP_ROUTER_ERROR__DUPLICATE_VAULT_OPERATOR_DELEGATION @@ -125,6 +131,8 @@ if (process.env.NODE_ENV !== 'production') { [JITO_TIP_ROUTER_ERROR__CAST_TO_IMPRECISE_NUMBER_ERROR]: `Cast to imprecise number error`, [JITO_TIP_ROUTER_ERROR__CONFIG_MINT_LIST_FULL]: `NCN config vaults are at capacity`, [JITO_TIP_ROUTER_ERROR__CONFIG_MINTS_NOT_UPDATED]: `Config supported mints do not match NCN Vault Count`, + [JITO_TIP_ROUTER_ERROR__CONSENSUS_ALREADY_REACHED]: `Consensus already reached`, + [JITO_TIP_ROUTER_ERROR__CONSENSUS_NOT_REACHED]: `Consensus not reached`, [JITO_TIP_ROUTER_ERROR__DENOMINATOR_IS_ZERO]: `Zero in the denominator`, [JITO_TIP_ROUTER_ERROR__DUPLICATE_MINTS_IN_TABLE]: `Duplicate mints in table`, [JITO_TIP_ROUTER_ERROR__DUPLICATE_VAULT_OPERATOR_DELEGATION]: `Duplicate vault operator delegation`, diff --git a/clients/js/jito_tip_router/types/ballot.ts b/clients/js/jito_tip_router/types/ballot.ts index c2ee14f7..a6e44013 100644 --- a/clients/js/jito_tip_router/types/ballot.ts +++ b/clients/js/jito_tip_router/types/ballot.ts @@ -10,12 +10,12 @@ import { combineCodec, fixDecoderSize, fixEncoderSize, + getBoolDecoder, + getBoolEncoder, getBytesDecoder, getBytesEncoder, getStructDecoder, getStructEncoder, - getU64Decoder, - getU64Encoder, type Codec, type Decoder, type Encoder, @@ -24,23 +24,16 @@ import { export type Ballot = { merkleRoot: ReadonlyUint8Array; - maxTotalClaim: bigint; - maxNodeCount: bigint; + isCast: number; reserved: ReadonlyUint8Array; }; -export type BallotArgs = { - merkleRoot: ReadonlyUint8Array; - maxTotalClaim: number | bigint; - maxNodeCount: number | bigint; - reserved: ReadonlyUint8Array; -}; +export type BallotArgs = Ballot; export function getBallotEncoder(): Encoder { return getStructEncoder([ ['merkleRoot', fixEncoderSize(getBytesEncoder(), 32)], - ['maxTotalClaim', getU64Encoder()], - ['maxNodeCount', getU64Encoder()], + ['isCast', getBoolEncoder()], ['reserved', fixEncoderSize(getBytesEncoder(), 64)], ]); } @@ -48,8 +41,7 @@ export function getBallotEncoder(): Encoder { export function getBallotDecoder(): Decoder { return getStructDecoder([ ['merkleRoot', fixDecoderSize(getBytesDecoder(), 32)], - ['maxTotalClaim', getU64Decoder()], - ['maxNodeCount', getU64Decoder()], + ['isCast', getBoolDecoder()], ['reserved', fixDecoderSize(getBytesDecoder(), 64)], ]); } diff --git a/clients/js/jito_tip_router/types/operatorVote.ts b/clients/js/jito_tip_router/types/operatorVote.ts index a082c6a6..f594c9d6 100644 --- a/clients/js/jito_tip_router/types/operatorVote.ts +++ b/clients/js/jito_tip_router/types/operatorVote.ts @@ -18,6 +18,8 @@ import { getStructEncoder, getU128Decoder, getU128Encoder, + getU16Decoder, + getU16Encoder, getU64Decoder, getU64Encoder, type Address, @@ -26,18 +28,12 @@ import { type Encoder, type ReadonlyUint8Array, } from '@solana/web3.js'; -import { - getBallotDecoder, - getBallotEncoder, - type Ballot, - type BallotArgs, -} from '.'; export type OperatorVote = { operator: Address; slotVoted: bigint; stakeWeight: bigint; - ballot: Ballot; + ballotIndex: number; reserved: ReadonlyUint8Array; }; @@ -45,7 +41,7 @@ export type OperatorVoteArgs = { operator: Address; slotVoted: number | bigint; stakeWeight: number | bigint; - ballot: BallotArgs; + ballotIndex: number; reserved: ReadonlyUint8Array; }; @@ -54,7 +50,7 @@ export function getOperatorVoteEncoder(): Encoder { ['operator', getAddressEncoder()], ['slotVoted', getU64Encoder()], ['stakeWeight', getU128Encoder()], - ['ballot', getBallotEncoder()], + ['ballotIndex', getU16Encoder()], ['reserved', fixEncoderSize(getBytesEncoder(), 64)], ]); } @@ -64,7 +60,7 @@ export function getOperatorVoteDecoder(): Decoder { ['operator', getAddressDecoder()], ['slotVoted', getU64Decoder()], ['stakeWeight', getU128Decoder()], - ['ballot', getBallotDecoder()], + ['ballotIndex', getU16Decoder()], ['reserved', fixDecoderSize(getBytesDecoder(), 64)], ]); } diff --git a/clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs b/clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs index 640dc29a..01c1b216 100644 --- a/clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs +++ b/clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs @@ -7,7 +7,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::pubkey::Pubkey; -use crate::generated::types::{BallotTally, OperatorVote}; +use crate::generated::types::{Ballot, BallotTally, OperatorVote}; #[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -26,6 +26,7 @@ pub struct BallotBox { pub reserved: [u8; 128], pub operators_voted: u64, pub unique_ballots: u64, + pub winning_ballot: Ballot, pub operator_votes: [OperatorVote; 32], pub ballot_tallies: [BallotTally; 32], } diff --git a/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs b/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs index 491e234a..ce806cbf 100644 --- a/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs +++ b/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs @@ -108,6 +108,12 @@ pub enum JitoTipRouterError { /// 8731 - Merkle root tally full #[error("Merkle root tally full")] BallotTallyFull = 0x221B, + /// 8732 - Consensus already reached + #[error("Consensus already reached")] + ConsensusAlreadyReached = 0x221C, + /// 8733 - Consensus not reached + #[error("Consensus not reached")] + ConsensusNotReached = 0x221D, } impl solana_program::program_error::PrintProgramError for JitoTipRouterError { diff --git a/clients/rust/jito_tip_router/src/generated/types/ballot.rs b/clients/rust/jito_tip_router/src/generated/types/ballot.rs index 1cd5b532..8fdc815c 100644 --- a/clients/rust/jito_tip_router/src/generated/types/ballot.rs +++ b/clients/rust/jito_tip_router/src/generated/types/ballot.rs @@ -10,8 +10,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Ballot { pub merkle_root: [u8; 32], - pub max_total_claim: u64, - pub max_node_count: u64, + pub is_cast: bool, #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] pub reserved: [u8; 64], } diff --git a/clients/rust/jito_tip_router/src/generated/types/operator_vote.rs b/clients/rust/jito_tip_router/src/generated/types/operator_vote.rs index f35d8d65..341cb174 100644 --- a/clients/rust/jito_tip_router/src/generated/types/operator_vote.rs +++ b/clients/rust/jito_tip_router/src/generated/types/operator_vote.rs @@ -7,8 +7,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::pubkey::Pubkey; -use crate::generated::types::Ballot; - #[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OperatorVote { @@ -19,7 +17,7 @@ pub struct OperatorVote { pub operator: Pubkey, pub slot_voted: u64, pub stake_weight: u128, - pub ballot: Ballot, + pub ballot_index: u16, #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] pub reserved: [u8; 64], } diff --git a/core/src/ballot_box.rs b/core/src/ballot_box.rs index f449bb24..6a4e79db 100644 --- a/core/src/ballot_box.rs +++ b/core/src/ballot_box.rs @@ -1,6 +1,6 @@ use bytemuck::{Pod, Zeroable}; use jito_bytemuck::{ - types::{PodU128, PodU64}, + types::{PodBool, PodU128, PodU16, PodU64}, AccountDeserialize, Discriminator, }; use shank::{ShankAccount, ShankType}; @@ -13,8 +13,7 @@ use crate::{constants::PRECISE_CONSENSUS, discriminators::Discriminators, error: #[repr(C)] pub struct Ballot { merkle_root: [u8; 32], - max_total_claim: PodU64, - max_node_count: PodU64, + is_cast: PodBool, reserved: [u8; 64], } @@ -22,19 +21,17 @@ impl Default for Ballot { fn default() -> Self { Self { merkle_root: [0; 32], - max_total_claim: PodU64::from(0), - max_node_count: PodU64::from(0), + is_cast: PodBool::from(false), reserved: [0; 64], } } } impl Ballot { - pub fn new(root: [u8; 32], max_total_claim: u64, max_num_nodes: u64) -> Self { + pub fn new(root: [u8; 32]) -> Self { Self { merkle_root: root, - max_total_claim: PodU64::from(max_total_claim), - max_node_count: PodU64::from(max_num_nodes), + is_cast: PodBool::from(true), reserved: [0; 64], } } @@ -43,12 +40,8 @@ impl Ballot { self.merkle_root } - pub fn max_total_claim(&self) -> u64 { - self.max_total_claim.into() - } - - pub fn max_node_count(&self) -> u64 { - self.max_node_count.into() + pub fn is_cast(&self) -> bool { + self.is_cast.into() } } @@ -120,7 +113,7 @@ pub struct OperatorVote { operator: Pubkey, slot_voted: PodU64, stake_weight: PodU128, - ballot: Ballot, + ballot_index: PodU16, reserved: [u8; 64], } @@ -130,17 +123,22 @@ impl Default for OperatorVote { operator: Pubkey::default(), slot_voted: PodU64::from(0), stake_weight: PodU128::from(0), - ballot: Ballot::default(), + ballot_index: PodU16::from(0), reserved: [0; 64], } } } impl OperatorVote { - pub fn new(ballot: Ballot, operator: Pubkey, current_slot: u64, stake_weight: u128) -> Self { + pub fn new( + ballot_index: usize, + operator: Pubkey, + current_slot: u64, + stake_weight: u128, + ) -> Self { Self { operator, - ballot, + ballot_index: PodU16::from(ballot_index as u16), slot_voted: PodU64::from(current_slot), stake_weight: PodU128::from(stake_weight), reserved: [0; 64], @@ -159,8 +157,8 @@ impl OperatorVote { self.stake_weight.into() } - pub const fn ballot(&self) -> Ballot { - self.ballot + pub fn ballot_index(&self) -> u16 { + self.ballot_index.into() } pub fn is_empty(&self) -> bool { @@ -186,6 +184,8 @@ pub struct BallotBox { operators_voted: PodU64, unique_ballots: PodU64, + winning_ballot: Ballot, + //TODO fix 32 -> MAX_OPERATORS operator_votes: [OperatorVote; 32], ballot_tallies: [BallotTally; 32], @@ -205,6 +205,7 @@ impl BallotBox { slot_consensus_reached: PodU64::from(0), operators_voted: PodU64::from(0), unique_ballots: PodU64::from(0), + winning_ballot: Ballot::default(), //TODO fix 32 -> MAX_OPERATORS operator_votes: [OperatorVote::default(); 32], ballot_tallies: [BallotTally::default(); 32], @@ -268,30 +269,44 @@ impl BallotBox { Ok(()) } - fn slot_consensus_reached(&self) -> u64 { + pub fn slot_consensus_reached(&self) -> u64 { self.slot_consensus_reached.into() } - fn unique_ballots(&self) -> u64 { + pub fn unique_ballots(&self) -> u64 { self.unique_ballots.into() } - fn operators_voted(&self) -> u64 { + pub fn operators_voted(&self) -> u64 { self.operators_voted.into() } + pub fn is_consensus_reached(&self) -> bool { + self.slot_consensus_reached() == 0 + } + + pub fn get_winning_ballot(&self) -> Result { + if self.winning_ballot.is_cast() { + Ok(self.winning_ballot) + } else { + Err(TipRouterError::ConsensusNotReached) + } + } + fn increment_or_create_ballot_tally( &mut self, - operator_vote: &OperatorVote, - ) -> Result<(), TipRouterError> { + ballot: &Ballot, + stake_weight: u128, + ) -> Result { + let mut tally_index: usize = 0; for tally in self.ballot_tallies.iter_mut() { - if tally.ballot.root().eq(&operator_vote.ballot().root()) { - tally.increment_tally(operator_vote.stake_weight())?; - return Ok(()); + if tally.ballot.eq(ballot) { + tally.increment_tally(stake_weight)?; + return Ok(tally_index); } if tally.is_empty() { - *tally = BallotTally::new(operator_vote.ballot(), operator_vote.stake_weight()); + *tally = BallotTally::new(*ballot, stake_weight); self.unique_ballots = PodU64::from( self.unique_ballots() @@ -299,8 +314,12 @@ impl BallotBox { .ok_or(TipRouterError::ArithmeticOverflow)?, ); - return Ok(()); + return Ok(tally_index); } + + tally_index = tally_index + .checked_add(1) + .ok_or(TipRouterError::ArithmeticOverflow)?; } Err(TipRouterError::BallotTallyFull) @@ -313,17 +332,18 @@ impl BallotBox { stake_weight: u128, current_slot: u64, ) -> Result<(), TipRouterError> { + let ballot_index = self.increment_or_create_ballot_tally(&ballot, stake_weight)?; + for vote in self.operator_votes.iter_mut() { if vote.operator().eq(&operator) { return Err(TipRouterError::DuplicateVoteCast); } if vote.is_empty() { - let operator_vote = OperatorVote::new(ballot, operator, current_slot, stake_weight); + let operator_vote = + OperatorVote::new(ballot_index, operator, current_slot, stake_weight); *vote = operator_vote; - self.increment_or_create_ballot_tally(&operator_vote)?; - self.operators_voted = PodU64::from( self.operators_voted() .checked_add(1) @@ -337,14 +357,14 @@ impl BallotBox { Err(TipRouterError::OperatorVotesFull) } - //Not sure where/how this should be used + // Should be called anytime a new vote is cast pub fn tally_votes( &mut self, total_stake_weight: u128, current_slot: u64, ) -> Result<(), TipRouterError> { if self.slot_consensus_reached() != 0 { - return Err(TipRouterError::ConsensusAlreadyReached); + return Ok(()); } let max_tally = self @@ -371,6 +391,8 @@ impl BallotBox { if consensus_reached { self.slot_consensus_reached = PodU64::from(current_slot); + + self.winning_ballot = max_tally.ballot(); } Ok(()) diff --git a/core/src/error.rs b/core/src/error.rs index b133ff12..4c5f7da6 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -72,6 +72,8 @@ pub enum TipRouterError { BallotTallyFull, #[error("Consensus already reached")] ConsensusAlreadyReached, + #[error("Consensus not reached")] + ConsensusNotReached, } impl DecodeError for TipRouterError { diff --git a/idl/jito_tip_router.json b/idl/jito_tip_router.json index 1f9c8035..ce1a40e5 100644 --- a/idl/jito_tip_router.json +++ b/idl/jito_tip_router.json @@ -618,6 +618,12 @@ "defined": "PodU64" } }, + { + "name": "winningBallot", + "type": { + "defined": "Ballot" + } + }, { "name": "operatorVotes", "type": { @@ -962,15 +968,9 @@ } }, { - "name": "maxTotalClaim", - "type": { - "defined": "PodU64" - } - }, - { - "name": "maxNodeCount", + "name": "isCast", "type": { - "defined": "PodU64" + "defined": "PodBool" } }, { @@ -1042,9 +1042,9 @@ } }, { - "name": "ballot", + "name": "ballotIndex", "type": { - "defined": "Ballot" + "defined": "PodU16" } }, { @@ -1394,6 +1394,16 @@ "code": 8731, "name": "BallotTallyFull", "msg": "Merkle root tally full" + }, + { + "code": 8732, + "name": "ConsensusAlreadyReached", + "msg": "Consensus already reached" + }, + { + "code": 8733, + "name": "ConsensusNotReached", + "msg": "Consensus not reached" } ], "metadata": { From ed4ad08a90e179e144d1c88822859557fd09da20 Mon Sep 17 00:00:00 2001 From: Christian Krueger Date: Tue, 19 Nov 2024 18:48:16 -0600 Subject: [PATCH 5/5] addressed comments --- .../jito_tip_router/accounts/operatorSnapshot.ts | 4 ++++ clients/js/jito_tip_router/types/ballot.ts | 5 ----- .../src/generated/accounts/operator_snapshot.rs | 1 + .../jito_tip_router/src/generated/types/ballot.rs | 1 - core/src/ballot_box.rs | 15 ++++++--------- core/src/epoch_snapshot.rs | 7 +++++++ idl/jito_tip_router.json | 12 ++++++------ program/src/initialize_operator_snapshot.rs | 8 ++++++-- 8 files changed, 30 insertions(+), 23 deletions(-) diff --git a/clients/js/jito_tip_router/accounts/operatorSnapshot.ts b/clients/js/jito_tip_router/accounts/operatorSnapshot.ts index 32b0633e..76a521ba 100644 --- a/clients/js/jito_tip_router/accounts/operatorSnapshot.ts +++ b/clients/js/jito_tip_router/accounts/operatorSnapshot.ts @@ -56,6 +56,7 @@ export type OperatorSnapshot = { slotCreated: bigint; slotFinalized: bigint; isActive: number; + ncnOperatorIndex: bigint; operatorIndex: bigint; operatorFeeBps: number; vaultOperatorDelegationCount: bigint; @@ -75,6 +76,7 @@ export type OperatorSnapshotArgs = { slotCreated: number | bigint; slotFinalized: number | bigint; isActive: number; + ncnOperatorIndex: number | bigint; operatorIndex: number | bigint; operatorFeeBps: number; vaultOperatorDelegationCount: number | bigint; @@ -95,6 +97,7 @@ export function getOperatorSnapshotEncoder(): Encoder { ['slotCreated', getU64Encoder()], ['slotFinalized', getU64Encoder()], ['isActive', getBoolEncoder()], + ['ncnOperatorIndex', getU64Encoder()], ['operatorIndex', getU64Encoder()], ['operatorFeeBps', getU16Encoder()], ['vaultOperatorDelegationCount', getU64Encoder()], @@ -119,6 +122,7 @@ export function getOperatorSnapshotDecoder(): Decoder { ['slotCreated', getU64Decoder()], ['slotFinalized', getU64Decoder()], ['isActive', getBoolDecoder()], + ['ncnOperatorIndex', getU64Decoder()], ['operatorIndex', getU64Decoder()], ['operatorFeeBps', getU16Decoder()], ['vaultOperatorDelegationCount', getU64Decoder()], diff --git a/clients/js/jito_tip_router/types/ballot.ts b/clients/js/jito_tip_router/types/ballot.ts index a6e44013..b268678a 100644 --- a/clients/js/jito_tip_router/types/ballot.ts +++ b/clients/js/jito_tip_router/types/ballot.ts @@ -10,8 +10,6 @@ import { combineCodec, fixDecoderSize, fixEncoderSize, - getBoolDecoder, - getBoolEncoder, getBytesDecoder, getBytesEncoder, getStructDecoder, @@ -24,7 +22,6 @@ import { export type Ballot = { merkleRoot: ReadonlyUint8Array; - isCast: number; reserved: ReadonlyUint8Array; }; @@ -33,7 +30,6 @@ export type BallotArgs = Ballot; export function getBallotEncoder(): Encoder { return getStructEncoder([ ['merkleRoot', fixEncoderSize(getBytesEncoder(), 32)], - ['isCast', getBoolEncoder()], ['reserved', fixEncoderSize(getBytesEncoder(), 64)], ]); } @@ -41,7 +37,6 @@ export function getBallotEncoder(): Encoder { export function getBallotDecoder(): Decoder { return getStructDecoder([ ['merkleRoot', fixDecoderSize(getBytesDecoder(), 32)], - ['isCast', getBoolDecoder()], ['reserved', fixDecoderSize(getBytesDecoder(), 64)], ]); } diff --git a/clients/rust/jito_tip_router/src/generated/accounts/operator_snapshot.rs b/clients/rust/jito_tip_router/src/generated/accounts/operator_snapshot.rs index d392fc61..a30fbde3 100644 --- a/clients/rust/jito_tip_router/src/generated/accounts/operator_snapshot.rs +++ b/clients/rust/jito_tip_router/src/generated/accounts/operator_snapshot.rs @@ -28,6 +28,7 @@ pub struct OperatorSnapshot { pub slot_created: u64, pub slot_finalized: u64, pub is_active: bool, + pub ncn_operator_index: u64, pub operator_index: u64, pub operator_fee_bps: u16, pub vault_operator_delegation_count: u64, diff --git a/clients/rust/jito_tip_router/src/generated/types/ballot.rs b/clients/rust/jito_tip_router/src/generated/types/ballot.rs index 8fdc815c..b954536b 100644 --- a/clients/rust/jito_tip_router/src/generated/types/ballot.rs +++ b/clients/rust/jito_tip_router/src/generated/types/ballot.rs @@ -10,7 +10,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Ballot { pub merkle_root: [u8; 32], - pub is_cast: bool, #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] pub reserved: [u8; 64], } diff --git a/core/src/ballot_box.rs b/core/src/ballot_box.rs index 6a4e79db..0895acc4 100644 --- a/core/src/ballot_box.rs +++ b/core/src/ballot_box.rs @@ -1,6 +1,6 @@ use bytemuck::{Pod, Zeroable}; use jito_bytemuck::{ - types::{PodBool, PodU128, PodU16, PodU64}, + types::{PodU128, PodU16, PodU64}, AccountDeserialize, Discriminator, }; use shank::{ShankAccount, ShankType}; @@ -13,7 +13,6 @@ use crate::{constants::PRECISE_CONSENSUS, discriminators::Discriminators, error: #[repr(C)] pub struct Ballot { merkle_root: [u8; 32], - is_cast: PodBool, reserved: [u8; 64], } @@ -21,17 +20,15 @@ impl Default for Ballot { fn default() -> Self { Self { merkle_root: [0; 32], - is_cast: PodBool::from(false), reserved: [0; 64], } } } impl Ballot { - pub fn new(root: [u8; 32]) -> Self { + pub const fn new(root: [u8; 32]) -> Self { Self { merkle_root: root, - is_cast: PodBool::from(true), reserved: [0; 64], } } @@ -40,8 +37,8 @@ impl Ballot { self.merkle_root } - pub fn is_cast(&self) -> bool { - self.is_cast.into() + pub fn is_valid(&self) -> bool { + self.merkle_root.iter().any(|&b| b != 0) } } @@ -282,11 +279,11 @@ impl BallotBox { } pub fn is_consensus_reached(&self) -> bool { - self.slot_consensus_reached() == 0 + self.slot_consensus_reached() > 0 } pub fn get_winning_ballot(&self) -> Result { - if self.winning_ballot.is_cast() { + if self.winning_ballot.is_valid() { Ok(self.winning_ballot) } else { Err(TipRouterError::ConsensusNotReached) diff --git a/core/src/epoch_snapshot.rs b/core/src/epoch_snapshot.rs index 9bf5dbba..64278019 100644 --- a/core/src/epoch_snapshot.rs +++ b/core/src/epoch_snapshot.rs @@ -205,6 +205,7 @@ pub struct OperatorSnapshot { is_active: PodBool, + ncn_operator_index: PodU64, operator_index: PodU64, operator_fee_bps: PodU16, @@ -277,6 +278,7 @@ impl OperatorSnapshot { bump: u8, current_slot: u64, is_active: bool, + ncn_operator_index: u64, operator_index: u64, operator_fee_bps: u16, vault_operator_delegation_count: u64, @@ -293,6 +295,7 @@ impl OperatorSnapshot { slot_created: PodU64::from(current_slot), slot_finalized: PodU64::from(0), is_active: PodBool::from(is_active), + ncn_operator_index: PodU64::from(ncn_operator_index), operator_index: PodU64::from(operator_index), operator_fee_bps: PodU16::from(operator_fee_bps), vault_operator_delegation_count: PodU64::from(vault_operator_delegation_count), @@ -311,6 +314,7 @@ impl OperatorSnapshot { ncn_epoch: u64, bump: u8, current_slot: u64, + ncn_operator_index: u64, operator_index: u64, operator_fee_bps: u16, vault_count: u64, @@ -322,6 +326,7 @@ impl OperatorSnapshot { bump, current_slot, true, + ncn_operator_index, operator_index, operator_fee_bps, vault_count, @@ -334,6 +339,7 @@ impl OperatorSnapshot { ncn_epoch: u64, bump: u8, current_slot: u64, + ncn_operator_index: u64, operator_index: u64, ) -> Result { let mut snapshot = Self::new( @@ -343,6 +349,7 @@ impl OperatorSnapshot { bump, current_slot, false, + ncn_operator_index, operator_index, 0, 0, diff --git a/idl/jito_tip_router.json b/idl/jito_tip_router.json index ce1a40e5..dbac2f45 100644 --- a/idl/jito_tip_router.json +++ b/idl/jito_tip_router.json @@ -769,6 +769,12 @@ "defined": "PodBool" } }, + { + "name": "ncnOperatorIndex", + "type": { + "defined": "PodU64" + } + }, { "name": "operatorIndex", "type": { @@ -967,12 +973,6 @@ ] } }, - { - "name": "isCast", - "type": { - "defined": "PodBool" - } - }, { "name": "reserved", "type": { diff --git a/program/src/initialize_operator_snapshot.rs b/program/src/initialize_operator_snapshot.rs index 0c2d84ef..a2d72c8f 100644 --- a/program/src/initialize_operator_snapshot.rs +++ b/program/src/initialize_operator_snapshot.rs @@ -84,7 +84,7 @@ pub fn process_initialize_operator_snapshot( )?; //TODO move to helper function - let is_active: bool = { + let (is_active, ncn_operator_index): (bool, u64) = { let ncn_operator_state_data = ncn_operator_state.data.borrow(); let ncn_operator_state_account = NcnOperatorState::try_from_slice_unchecked(&ncn_operator_state_data)?; @@ -97,7 +97,9 @@ pub fn process_initialize_operator_snapshot( .operator_opt_in_state .is_active(current_slot, ncn_epoch_length); - ncn_operator_okay && operator_ncn_okay + let ncn_operator_index = ncn_operator_state_account.index(); + + (ncn_operator_okay && operator_ncn_okay, ncn_operator_index) }; let vault_count = { @@ -128,6 +130,7 @@ pub fn process_initialize_operator_snapshot( ncn_epoch, operator_snapshot_bump, current_slot, + ncn_operator_index, operator_index, operator_fee_bps, vault_count, @@ -139,6 +142,7 @@ pub fn process_initialize_operator_snapshot( ncn_epoch, operator_snapshot_bump, current_slot, + ncn_operator_index, operator_index, )? };