Skip to content

Commit

Permalink
[JIT-460] When claiming mev tips, skip accounts that won't have min r…
Browse files Browse the repository at this point in the history
…ent exempt amount after claiming (#203)
  • Loading branch information
esemeniuc authored Nov 24, 2022
1 parent 7fb3e28 commit be7a115
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 72 deletions.
2 changes: 1 addition & 1 deletion tip-distributor/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Tip Distributor
This library and collection of binaries are responsible for generating and uploading merkle roots to the on-chain
tip-distribution program found [here](https://github.com/jito-labs/jito-programs/blob/a450ef006e60e10894c02269ec8a301b81a083a0/tip-payment/programs/tip-distribution/src/lib.rs).
tip-distribution program found [here](https://github.com/jito-foundation/jito-programs/blob/submodule/tip-payment/programs/tip-distribution/src/lib.rs).

## Background
Each individual validator is assigned a new PDA per epoch where their share of tips, in lamports, will be stored.
Expand Down
144 changes: 77 additions & 67 deletions tip-distributor/src/claim_mev_workflow.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use {
crate::{read_json_from_file, send_transactions_with_retry, GeneratedMerkleTreeCollection},
anchor_lang::{AccountDeserialize, InstructionData, ToAccountMetas},
log::{debug, info},
log::{debug, info, warn},
solana_client::{nonblocking::rpc_client::RpcClient, rpc_request::RpcError},
solana_program::system_program,
solana_rpc_client_api::client_error::ErrorKind,
solana_program::{
fee_calculator::DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE, stake::state::StakeState,
system_program,
},
solana_rpc_client_api::client_error,
solana_sdk::{
commitment_config::CommitmentConfig,
instruction::Instruction,
Expand Down Expand Up @@ -55,13 +58,35 @@ pub fn claim_mev_tips(
let mut transactions = Vec::new();

runtime.block_on(async move {
let blockhash = rpc_client
.get_latest_blockhash()
.await
.expect("read blockhash");

let blockhash = rpc_client.get_latest_blockhash().await.expect("read blockhash");
let balance = rpc_client.get_balance(&keypair.pubkey()).await.expect("failed to get balance");
// heuristic to make sure we have enough funds to cover the rent costs if epoch has many validators
{
let node_count = merkle_trees.generated_merkle_trees.iter().flat_map(|tree| &tree.tree_nodes).count();
let min_rent_per_claim = rpc_client.get_minimum_balance_for_rent_exemption(ClaimStatus::SIZE).await.expect("Failed to calculate min rent");
assert!(balance >= node_count as u64 * (min_rent_per_claim + DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE));
}
let stake_acct_min_rent = rpc_client.get_minimum_balance_for_rent_exemption(StakeState::size_of()).await.expect("Failed to calculate min rent");
let mut below_min_rent_count = 0;
let mut zero_lamports_count = 0;
for tree in merkle_trees.generated_merkle_trees {
// only claim for ones that have merkle root on-chain

let account = rpc_client.get_account(&tree.tip_distribution_account).await.expect("expected to fetch tip distribution account");
let fetched_tip_distribution_account = TipDistributionAccount::try_deserialize(&mut account.data.as_slice()).expect("failed to deserialize tip_distribution_account state");
if fetched_tip_distribution_account.merkle_root.is_none() {
info!(
"not claiming because merkle root isn't uploaded yet. skipped {} claimants for tda: {:?}",
tree.tree_nodes.len(),
tree.tip_distribution_account
);
continue;
}
for node in tree.tree_nodes {
if node.amount == 0 {
zero_lamports_count += 1;
continue;
}
let (claim_status_pubkey, claim_status_bump) = Pubkey::find_program_address(
&[
ClaimStatus::SEED,
Expand All @@ -71,70 +96,55 @@ pub fn claim_mev_tips(
tip_distribution_program_id,
);

// only claim for ones that have merkle root on-chain
let account = rpc_client
.get_account(&tree.tip_distribution_account)
.await
.expect("expected to fetch tip distribution account");

let mut data = account.data.as_slice();
let fetched_tip_distribution_account =
TipDistributionAccount::try_deserialize(&mut data)
.expect("failed to deserialize tip_distribution_account state");

if fetched_tip_distribution_account.merkle_root.is_some() {
match rpc_client.get_account(&claim_status_pubkey).await {
Ok(_) => {
debug!(
"claim status account already exists: {:?}",
claim_status_pubkey
);
}
Err(e) => {
if matches!(e.kind(), ErrorKind::RpcError(RpcError::ForUser(_))) {
info!("claiming for public key: {:?}", node.claimant);

let ix = Instruction {
program_id: *tip_distribution_program_id,
data: tip_distribution::instruction::Claim {
proof: node.proof.unwrap(),
amount: node.amount,
bump: claim_status_bump,
}
.data(),
accounts: tip_distribution::accounts::Claim {
config: tip_distribution_config,
tip_distribution_account: tree.tip_distribution_account,
claimant: node.claimant,
claim_status: claim_status_pubkey,
payer: keypair.pubkey(),
system_program: system_program::id(),
}
.to_account_metas(None),
};
let transaction = Transaction::new_signed_with_payer(
&[ix],
Some(&keypair.pubkey()),
&[&keypair],
blockhash,
);
info!("tx: {:?}", transaction);
transactions.push(transaction);
} else {
panic!("unexpected rpc error: {:?}", e);
}
}
// make sure not previously claimed
match rpc_client.get_account(&claim_status_pubkey).await {
Ok(_) => {
debug!("claim status account already exists, skipping pubkey {:?}.", claim_status_pubkey);
continue;
}
} else {
info!(
"not claiming because merkle root isn't uploaded yet claimant: {:?} tda: {:?}",
node.claimant,
tree.tip_distribution_account
);
// expected to not find ClaimStatus account, don't skip
Err(client_error::Error { kind: client_error::ErrorKind::RpcError(RpcError::ForUser(err)), .. }) if err.starts_with("AccountNotFound") => {}
Err(err) => panic!("Unexpected RPC Error: {}", err),
}

let current_balance = rpc_client.get_balance(&node.claimant).await.expect("Failed to get balance");
// some older accounts can be rent-paying
// any new transfers will need to make the account rent-exempt (runtime enforced)
if current_balance + node.amount < stake_acct_min_rent {
warn!("Current balance + tip claim amount of {} is less than required rent-exempt of {} for pubkey: {}. Skipping.",
current_balance + node.amount, stake_acct_min_rent, node.claimant);
below_min_rent_count += 1;
continue;
}
let ix = Instruction {
program_id: *tip_distribution_program_id,
data: tip_distribution::instruction::Claim {
proof: node.proof.unwrap(),
amount: node.amount,
bump: claim_status_bump,
}.data(),
accounts: tip_distribution::accounts::Claim {
config: tip_distribution_config,
tip_distribution_account: tree.tip_distribution_account,
claimant: node.claimant,
claim_status: claim_status_pubkey,
payer: keypair.pubkey(),
system_program: system_program::id(),
}.to_account_metas(None),
};
let transaction = Transaction::new_signed_with_payer(
&[ix],
Some(&keypair.pubkey()),
&[&keypair],
blockhash,
);
info!("claiming for pubkey: {}, tx: {:?}", node.claimant, transaction);
transactions.push(transaction);
}
}

info!("Sending {} tip claim transactions. {} tried sending zero lamports, {} would be below minimum rent",
&transactions.len(), zero_lamports_count, below_min_rent_count);
send_transactions_with_retry(&rpc_client, &transactions, MAX_RETRY_DURATION).await;
});

Expand Down
11 changes: 7 additions & 4 deletions tip-distributor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,10 +430,13 @@ pub async fn send_transactions_with_retry(
}
}

assert!(
transactions_to_send.is_empty(),
"all transactions failed to send"
);
if !transactions_to_send.is_empty() {
panic!(
"failed to send {} of {} transactions",
transactions_to_send.len(),
transactions.len()
);
}
}

mod pubkey_string_conversion {
Expand Down

0 comments on commit be7a115

Please sign in to comment.