Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add MEV earned per epoch in the ValidatorHistoryAccount #13

Merged
merged 10 commits into from
Jan 26, 2024

Conversation

0xprames
Copy link
Contributor

@0xprames 0xprames commented Jan 8, 2024

I took a stab at #6 , saw it's been open for a few weeks

mainly out of wanting to learn, this is my first experience w/ Anchor, solana programs, instructions etc etc

I'd love a review to see if I thought about the issue correctly, but i'm happy to close it out or collaborate w/ the assignee of #6 to get this in

@buffalu buffalu requested a review from ebatsell January 8, 2024 02:38
epoch: u16,
commission: u16,
mev_earned: u64,
) -> Result<()> {
// check if entry exists for the epoch
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this logic is wrong. mentioned to @ebatsell in diff PR, but if you update epoch 500, then 501, then 500 you'll end up with two epoch 500 entries. the second 500 update should go back and find the first 500 entry

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i deleted my prev comment on this thread - since it was incorrect

you're right, with the existing logic, in the case where we try and edit an epoch seen prev, it will create another entry for it.

i went ahead and pushed up a fix for that

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high level:

  • the CircBuf struct should have at most one entry per epoch, and they should be laid out in ascending order
  • if for some reason an entry is missed, it's pretty messy to try to insert after the fact due to the need to shift every following epoch down by one.
  • a new entry should be appended if an entry doesn't exist for that epoch and it's greater than the most recent epoch (.last())

therefore the update pseudocode for all of these set_s should be something like:

if epoch > circ_buf.last().epoch {
  insert new entry
} else {
   if let Some(entry) = circ_buf.find(epoch) {
     modify entry
   }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah makes sense, should be taken care of for all the set_s now

programs/validator-history/src/state.rs Show resolved Hide resolved
epoch: u16,
commission: u16,
mev_earned: u64,
) -> Result<()> {
// check if entry exists for the epoch
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high level:

  • the CircBuf struct should have at most one entry per epoch, and they should be laid out in ascending order
  • if for some reason an entry is missed, it's pretty messy to try to insert after the fact due to the need to shift every following epoch down by one.
  • a new entry should be appended if an entry doesn't exist for that epoch and it's greater than the most recent epoch (.last())

therefore the update pseudocode for all of these set_s should be something like:

if epoch > circ_buf.last().epoch {
  insert new entry
} else {
   if let Some(entry) = circ_buf.find(epoch) {
     modify entry
   }
}

@@ -122,7 +124,7 @@ impl UpdateInstruction for ValidatorMevCommissionEntry {
signer: self.signer,
}
.to_account_metas(None),
data: validator_history::instruction::UpdateMevCommission {}.data(),
data: validator_history::instruction::UpdateMevCommission { epoch: self.epoch }.data(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merkle roots for a given epoch are uploaded typically within a few hours after the epoch ends, so to account for this, the keeper will need to submit UpdateMevCommission instructions for the previous epoch after its upload has completed. could achieve this by adding a separate loop in the main that submits these at say, the halfway point of the next epoch. Or better yet, continually checks the tip distribution accounts to see which merkle_roots are not None

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for sure, lmk if what i have works

i added a update_mev_earned loop similar to the existing update_mev_comission with the main difference that instead of just looking for existing TDAs, it attempts to TipDistributionAccount::try_deserialize on the account.data.data received from the gMA call and and check for Some(merkle_root) = tda.merkle_root

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perfect

pub padding0: u8,
// 0 if not a superminority validator, 1 if superminority validator
pub is_superminority: u8,
// rank of validator by stake amount
pub rank: u32,
// Most recent updated slot for epoch credits and commission
pub vote_account_last_update_slot: u64,
pub padding1: [u8; 88],
// MEV earned
pub mev_earned: u64,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the interest of using the remaining space here wisely, I think it'd be best to represent this as a fixed point decimal. if this is 4 bytes with 2 decimals of precision that gives us a max MEV earned of 42949672.95 SOL for 1 validator for 1 epoch which won't happen for.. a long time 😄

Copy link
Contributor Author

@0xprames 0xprames Jan 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for sure, lmk if what i pushed up is correct

the internal u32 representation / 100 should give the max mev earned in sol with 2 dec of precision

Copy link
Collaborator

@ebatsell ebatsell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're doing more than just setting mev_commission in this instruction, I think we should also rename the instruction to copy_tip_distribution_account

pub padding0: u8,
// 0 if not a superminority validator, 1 if superminority validator
pub is_superminority: u8,
// rank of validator by stake amount
pub rank: u32,
// Most recent updated slot for epoch credits and commission
pub vote_account_last_update_slot: u64,
pub padding1: [u8; 88],
// MEV earned
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// MEV earned
// MEV earned, stored as 1/100th SOL. mev_earned = 100 means 1 SOL earned

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -4,6 +4,16 @@ pub fn cast_epoch(epoch: u64) -> u16 {
(epoch % u16::MAX as u64).try_into().unwrap()
}

pub fn fixed_point_sol(lamports: u64) -> u32 {
// convert to sol
let mut sol = lamports as f64 / 1000000000.00;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can use anchor_lang::solana_program::native_token::lamports_to_sol

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

programs/validator-history/src/utils.rs Show resolved Hide resolved
let mut validator_history_account = ctx.accounts.validator_history_account.load_mut()?;

let mut tda_data: &[u8] = &ctx.accounts.tip_distribution_account.try_borrow_data()?;

let tip_distribution_account = TipDistributionAccount::try_deserialize(&mut tda_data)?;
let mev_commission_bps = tip_distribution_account.validator_commission_bps;
let epoch = cast_epoch(Clock::get()?.epoch);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to do a check here and anywhere else epoch is passed in that epoch is not greater than Clock::get().epoch

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, the other 2 instructions (update_stake_history and backfill_total_blocks) seem to have the check

Comment on lines 406 to 407
.filter(|entry| entry.epoch == epoch)
.for_each(|entry| {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this implies there may be multiple entries with the same epoch in the CircBuf, which shouldn't be possible given how we're inserting new entries - would prefer to use .find(|entry| entry.epoch == epoch) here instead of .filter().for_each(..)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense

@0xprames
Copy link
Contributor Author

Since we're doing more than just setting mev_commission in this instruction, I think we should also rename the instruction to copy_tip_distribution_account

done - UpdateMevComission -> CopyTipDistributionAccount

@ebatsell
Copy link
Collaborator

@0xprames looks great, once all tests and checks are passing I'll approve

Copy link
Collaborator

@buffalu buffalu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly stylistic comments!

// Continuously runs throughout an epoch, polling for tip distribution accounts from the prev epoch with uploaded merkle roots
// and submitting update_mev_earned (technically update_mev_comission) txs when the uploaded merkle roots are detected
match update_mev_earned(
client.clone(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use &client and &keypair here to avoid clones

@@ -190,6 +193,73 @@ pub async fn update_mev_commission(
submit_result.map_err(|(e, stats)| (e.into(), stats))
}

pub async fn update_mev_earned(
client: Arc<RpcClient>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can use & here and on keypair

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, i def should borrow more than move into when i can

@@ -369,5 +406,12 @@ async fn main() {
args.interval,
));

tokio::spawn(mev_earned_loop(
Arc::clone(&client),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be client.clone()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, modified in the existing code as well

.map(|vote_account| {
ValidatorMevCommissionEntry::new(
vote_account,
epoch - 1, // TDA derived from the prev epoch since the merkle roots are uploaded shortly after rollover
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ultra nit: saturating_sub for epoch 0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense

.collect::<Vec<ValidatorMevCommissionEntry>>();

let uploaded_merkleroot_entries =
get_entries_with_uploaded_merkleroot(client.clone(), &entries)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use &client

@@ -215,3 +286,33 @@ async fn get_existing_entries(
// Fetch existing tip distribution accounts for this epoch
Ok(result)
}

async fn get_entries_with_uploaded_merkleroot(
client: Arc<RpcClient>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use &Arc

@@ -48,16 +49,25 @@ pub struct UpdateMevCommission<'info> {
pub signer: Signer<'info>,
}

pub fn handler(ctx: Context<UpdateMevCommission>) -> Result<()> {
pub fn handler(ctx: Context<CopyTipDistributionAccount>, epoch: u64) -> Result<()> {
// Cannot set stake for future epochs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch

@ebatsell ebatsell merged commit 6236581 into jito-foundation:master Jan 26, 2024
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants