From 694ff2ff034d6ef213e4ce58b8bd4958c1e42334 Mon Sep 17 00:00:00 2001 From: Eric Semeniuc <3838856+esemeniuc@users.noreply.github.com> Date: Mon, 12 Dec 2022 13:50:29 -0800 Subject: [PATCH] [JIT-519] Store ClaimStatus address in merkle-root-json (#210) * add files * switch to include bump --- Cargo.lock | 13 - sdk/program/src/stake/state.rs | 5 + tip-distributor/Cargo.toml | 2 - tip-distributor/README.md | 2 +- tip-distributor/src/claim_mev_workflow.rs | 157 +++++----- tip-distributor/src/lib.rs | 290 +++++++++--------- .../src/merkle_root_upload_workflow.rs | 13 + .../src/stake_meta_generator_workflow.rs | 27 +- 8 files changed, 270 insertions(+), 239 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88a590e1f4..24d6468927 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,17 +487,6 @@ version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "874f8444adcb4952a8bc51305c8be95c8ec8237bb0d2e78d2e039f771f8828a0" -[[package]] -name = "bigdecimal" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aaf33151a6429fe9211d1b276eafdf70cdff28b071e76c0b0e1503221ea3744" -dependencies = [ - "num-bigint 0.4.2", - "num-integer", - "num-traits", -] - [[package]] name = "bincode" version = "1.3.3" @@ -6651,7 +6640,6 @@ name = "solana-tip-distributor" version = "1.13.7" dependencies = [ "anchor-lang", - "bigdecimal", "clap 3.2.23", "env_logger", "futures 0.3.21", @@ -6666,7 +6654,6 @@ dependencies = [ "solana-ledger", "solana-merkle-tree", "solana-program 1.13.7", - "solana-rpc", "solana-runtime", "solana-sdk 1.13.7", "solana-stake-program", diff --git a/sdk/program/src/stake/state.rs b/sdk/program/src/stake/state.rs index 5b34634271..fed8b92cd1 100644 --- a/sdk/program/src/stake/state.rs +++ b/sdk/program/src/stake/state.rs @@ -74,6 +74,11 @@ impl Default for StakeState { } impl StakeState { + /// The fixed number of bytes used to serialize each stake account + pub const fn size_of() -> usize { + 200 + } + pub fn get_rent_exempt_reserve(rent: &Rent) -> u64 { rent.minimum_balance(std::mem::size_of::()) } diff --git a/tip-distributor/Cargo.toml b/tip-distributor/Cargo.toml index 91f85c6363..b531e2ab97 100644 --- a/tip-distributor/Cargo.toml +++ b/tip-distributor/Cargo.toml @@ -7,7 +7,6 @@ description = "Collection of binaries used to distribute MEV rewards to delegato [dependencies] anchor-lang = { path = "../anchor/lang" } -bigdecimal = "0.3.0" clap = { version = "3.2.5", features = ["derive", "env"] } env_logger = "0.9.0" futures = "0.3.21" @@ -22,7 +21,6 @@ solana-genesis-utils = { path = "../genesis-utils", version = "=1.13.7" } solana-ledger = { path = "../ledger", version = "=1.13.7" } solana-merkle-tree = { path = "../merkle-tree", version = "=1.13.7" } solana-program = { path = "../sdk/program", version = "=1.13.7" } -solana-rpc = { path = "../rpc", version = "=1.13.7" } solana-runtime = { path = "../runtime", version = "=1.13.7" } solana-sdk = { path = "../sdk", version = "=1.13.7" } solana-stake-program = { path = "../programs/stake", version = "=1.13.7" } diff --git a/tip-distributor/README.md b/tip-distributor/README.md index 018cef98ac..c100843a41 100644 --- a/tip-distributor/README.md +++ b/tip-distributor/README.md @@ -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. diff --git a/tip-distributor/src/claim_mev_workflow.rs b/tip-distributor/src/claim_mev_workflow.rs index 3a2fd19bd8..cc991c78da 100644 --- a/tip-distributor/src/claim_mev_workflow.rs +++ b/tip-distributor/src/claim_mev_workflow.rs @@ -1,11 +1,12 @@ use { crate::{read_json_from_file, send_transactions_with_retry, GeneratedMerkleTreeCollection}, anchor_lang::{AccountDeserialize, InstructionData, ToAccountMetas}, - log::{debug, info}, - solana_client::{ - client_error::ClientErrorKind, nonblocking::rpc_client::RpcClient, rpc_request::RpcError, + log::{debug, info, warn}, + solana_client::{nonblocking::rpc_client::RpcClient, rpc_request::RpcError}, + solana_program::{ + fee_calculator::DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE, native_token::LAMPORTS_PER_SOL, + stake::state::StakeState, system_program, }, - solana_program::system_program, solana_sdk::{ commitment_config::CommitmentConfig, instruction::Instruction, @@ -56,86 +57,90 @@ 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 start_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 + { + // most amounts are for 0 lamports. had 1736 non-zero claims out of 164742 + let node_count = merkle_trees.generated_merkle_trees.iter().flat_map(|tree| &tree.tree_nodes).filter(|node| node.amount > 0).count(); + let min_rent_per_claim = rpc_client.get_minimum_balance_for_rent_exemption(ClaimStatus::SIZE).await.expect("Failed to calculate min rent"); + let desired_balance = (node_count as u64).checked_mul(min_rent_per_claim.checked_add(DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE).unwrap()).unwrap(); + if start_balance < desired_balance { + let sol_to_deposit = desired_balance.checked_sub(start_balance).unwrap().checked_add(LAMPORTS_PER_SOL).unwrap().checked_sub(1).unwrap().checked_div(LAMPORTS_PER_SOL).unwrap(); // rounds up to nearest sol + panic!("Expected to have at least {} lamports in {}, current balance is {} lamports, deposit {} SOL to continue.", + desired_balance, &keypair.pubkey(), start_balance, sol_to_deposit) + } + } + 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: usize = 0; + let mut zero_lamports_count: usize = 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 { - let (claim_status_pubkey, claim_status_bump) = Pubkey::find_program_address( - &[ - ClaimStatus::SEED, - node.claimant.as_ref(), // ordering matters here - tree.tip_distribution_account.as_ref(), - ], - 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(), ClientErrorKind::RpcError(RpcError::ForUser(_))) { - info!("claiming for public key: {:?}", node.claimant); + if node.amount == 0 { + zero_lamports_count = zero_lamports_count.checked_add(1).unwrap(); + 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!("tx: {:?}", transaction); - transactions.push(transaction); - } else { - panic!("unexpected rpc error: {:?}", e); - } - } + // make sure not previously claimed + match rpc_client.get_account(&node.claim_status_pubkey).await { + Ok(_) => { + debug!("claim status account already exists, skipping pubkey {:?}.", node.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(solana_client::client_error::ClientError { kind: solana_client::client_error::ClientErrorKind::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.checked_add(node.amount).unwrap() < stake_acct_min_rent { + warn!("Current balance + tip claim amount of {} is less than required rent-exempt of {} for pubkey: {}. Skipping.", + current_balance.checked_add(node.amount).unwrap(), stake_acct_min_rent, node.claimant); + below_min_rent_count = below_min_rent_count.checked_add(1).unwrap(); + continue; } + let ix = Instruction { + program_id: *tip_distribution_program_id, + data: tip_distribution::instruction::Claim { + proof: node.proof.unwrap(), + amount: node.amount, + bump: node.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: node.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; }); diff --git a/tip-distributor/src/lib.rs b/tip-distributor/src/lib.rs index 4ccde97ff0..a576b33587 100644 --- a/tip-distributor/src/lib.rs +++ b/tip-distributor/src/lib.rs @@ -8,9 +8,8 @@ use { merkle_root_generator_workflow::MerkleRootGeneratorError, stake_meta_generator_workflow::StakeMetaGeneratorError::CheckedMathError, }, - bigdecimal::{num_bigint::BigUint, BigDecimal}, + anchor_lang::Id, log::{error, info}, - num_traits::{CheckedDiv, CheckedMul, ToPrimitive}, serde::{de::DeserializeOwned, Deserialize, Serialize}, solana_client::nonblocking::rpc_client::RpcClient, solana_merkle_tree::MerkleTree, @@ -27,11 +26,13 @@ use { collections::HashMap, fs::File, io::BufReader, - ops::{Div, Mul}, path::PathBuf, time::{Duration, Instant}, }, - tip_distribution::state::TipDistributionAccount, + tip_distribution::{ + program::TipDistribution, + state::{ClaimStatus, TipDistributionAccount}, + }, tip_payment::{ Config, CONFIG_ACCOUNT_SEED, TIP_ACCOUNT_SEED_0, TIP_ACCOUNT_SEED_1, TIP_ACCOUNT_SEED_2, TIP_ACCOUNT_SEED_3, TIP_ACCOUNT_SEED_4, TIP_ACCOUNT_SEED_5, TIP_ACCOUNT_SEED_6, @@ -152,6 +153,13 @@ pub struct TreeNode { #[serde(with = "pubkey_string_conversion")] pub claimant: Pubkey, + /// Pubkey of the ClaimStatus PDA account, this account should be closed to reclaim rent. + #[serde(with = "pubkey_string_conversion")] + pub claim_status_pubkey: Pubkey, + + /// Bump of the ClaimStatus PDA account + pub claim_status_bump: u8, + #[serde(with = "pubkey_string_conversion")] pub staker_pubkey: Pubkey, @@ -170,72 +178,65 @@ impl TreeNode { stake_meta: &StakeMeta, ) -> Result>, MerkleRootGeneratorError> { if let Some(tip_distribution_meta) = stake_meta.maybe_tip_distribution_meta.as_ref() { - let validator_fee = calc_validator_fee( - tip_distribution_meta.total_tips, - tip_distribution_meta.validator_fee_bps, + let validator_amount = (tip_distribution_meta.total_tips as u128) + .checked_mul(tip_distribution_meta.validator_fee_bps as u128) + .unwrap() + .checked_div(10_000) + .unwrap() as u64; + let (claim_status_pubkey, claim_status_bump) = Pubkey::find_program_address( + &[ + ClaimStatus::SEED, + &stake_meta.validator_vote_account.to_bytes(), + &tip_distribution_meta.tip_distribution_pubkey.to_bytes(), + ], + &TipDistribution::id(), ); let mut tree_nodes = vec![TreeNode { claimant: stake_meta.validator_vote_account, + claim_status_pubkey, + claim_status_bump, staker_pubkey: Pubkey::default(), withdrawer_pubkey: Pubkey::default(), - amount: validator_fee, + amount: validator_amount, proof: None, }]; - let remaining_tips = tip_distribution_meta + let remaining_total_rewards = tip_distribution_meta .total_tips - .checked_sub(validator_fee) - .unwrap(); - - // The theoretically smallest weight an account can have is (1 / SOL_TOTAL_SUPPLY_IN_LAMPORTS) - // where we round SOL_TOTAL_SUPPLY is rounded to 500_000_000. We use u64::MAX. This gives a reasonable - // guarantee that everyone gets paid out regardless of weight, as long as some non-zero amount of - // lamports were delegated. - let uint_precision_multiplier = BigUint::from(u64::MAX); - let f64_precision_multiplier = BigDecimal::try_from(u64::MAX as f64).unwrap(); - - let total_delegated = BigDecimal::try_from(stake_meta.total_delegated as f64) - .expect("failed to convert total_delegated to BigDecimal"); - tree_nodes.extend(stake_meta - .delegations - .iter() - .map(|delegation| { - // TODO(seg): Check this math! - let amount_delegated = BigDecimal::try_from(delegation.lamports_delegated as f64) - .expect(&*format!( - "failed to convert amount_delegated to BigDecimal [stake_account={}, amount_delegated={}]", - delegation.stake_account_pubkey, - delegation.lamports_delegated, - )); - let mut weight = amount_delegated.div(&total_delegated); - - let use_multiplier = weight < f64_precision_multiplier; - - if use_multiplier { - weight = weight.mul(&f64_precision_multiplier); - } - - let truncated_weight = weight.to_u128() - .expect(&*format!("failed to convert weight to u128 [stake_account={}, weight={}]", delegation.stake_account_pubkey, weight)); - let truncated_weight = BigUint::from(truncated_weight); - - let mut amount = truncated_weight - .checked_mul(&BigUint::from(remaining_tips)) - .unwrap(); - - if use_multiplier { - amount = amount.checked_div(&uint_precision_multiplier).unwrap(); - } - - Ok(TreeNode { - claimant: delegation.stake_account_pubkey, - staker_pubkey: delegation.staker_pubkey, - withdrawer_pubkey: delegation.withdrawer_pubkey, - amount: amount.to_u64().unwrap(), - proof: None + .checked_sub(validator_amount) + .unwrap() as u128; + + let total_delegated = stake_meta.total_delegated as u128; + tree_nodes.extend( + stake_meta + .delegations + .iter() + .map(|delegation| { + let amount_delegated = delegation.lamports_delegated as u128; + let reward_amount = (amount_delegated.checked_mul(remaining_total_rewards)) + .unwrap() + .checked_div(total_delegated) + .unwrap(); + let (claim_status_pubkey, claim_status_bump) = Pubkey::find_program_address( + &[ + ClaimStatus::SEED, + &delegation.stake_account_pubkey.to_bytes(), + &tip_distribution_meta.tip_distribution_pubkey.to_bytes(), + ], + &TipDistribution::id(), + ); + Ok(TreeNode { + claimant: delegation.stake_account_pubkey, + claim_status_pubkey, + claim_status_bump, + staker_pubkey: delegation.staker_pubkey, + withdrawer_pubkey: delegation.withdrawer_pubkey, + amount: reward_amount as u64, + proof: None, + }) }) - }) - .collect::, MerkleRootGeneratorError>>()?); + .collect::, MerkleRootGeneratorError>>()?, + ); let total_claim_amount = tree_nodes.iter().fold(0u64, |sum, tree_node| { sum.checked_add(tree_node.amount).unwrap() @@ -293,6 +294,19 @@ pub struct StakeMeta { pub commission: u8, } +impl Ord for StakeMeta { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.validator_vote_account + .cmp(&other.validator_vote_account) + } +} + +impl PartialOrd for StakeMeta { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] pub struct TipDistributionMeta { #[serde(with = "pubkey_string_conversion")] @@ -347,6 +361,29 @@ pub struct Delegation { pub lamports_delegated: u64, } +impl Ord for Delegation { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + ( + self.stake_account_pubkey, + self.withdrawer_pubkey, + self.staker_pubkey, + self.lamports_delegated, + ) + .cmp(&( + other.stake_account_pubkey, + other.withdrawer_pubkey, + other.staker_pubkey, + other.lamports_delegated, + )) + } +} + +impl PartialOrd for Delegation { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + /// Convenience wrapper around [TipDistributionAccount] pub struct TipDistributionAccountWrapper { pub tip_distribution_account: TipDistributionAccount, @@ -370,18 +407,6 @@ pub fn derive_tip_distribution_account_address( ) } -/// Calculate validator fee denominated in lamports -pub fn calc_validator_fee(total_tips: u64, validator_commission_bps: u16) -> u64 { - let validator_commission_rate = - math::fee_tenth_of_bps(((validator_commission_bps as u64).checked_mul(10).unwrap()) as u64); - let validator_fee: math::U64F64 = validator_commission_rate.mul_u64(total_tips); - - validator_fee - .floor() - .checked_add((validator_fee.frac_part() != 0) as u64) - .unwrap() -} - pub async fn send_transactions_with_retry( rpc_client: &RpcClient, transactions: &[Transaction], @@ -434,61 +459,12 @@ pub async fn send_transactions_with_retry( } } - assert!( - transactions_to_send.is_empty(), - "all transactions failed to send" - ); -} - -mod math { - /// copy-pasta from [here](https://github.com/project-serum/serum-dex/blob/e00bb9e6dac0a1fff295acb034722be9afc1eba3/dex/src/fees.rs#L43) - #[repr(transparent)] - #[derive(Copy, Clone)] - pub(crate) struct U64F64(u128); - - #[allow(dead_code)] - impl U64F64 { - const ONE: Self = U64F64(1 << 64); - - pub(crate) fn add(self, other: U64F64) -> U64F64 { - U64F64(self.0.checked_add(other.0).unwrap()) - } - - pub(crate) fn div(self, other: U64F64) -> u128 { - self.0.checked_div(other.0).unwrap() - } - - pub(crate) fn mul_u64(self, other: u64) -> U64F64 { - U64F64(self.0.checked_mul(other as u128).unwrap()) - } - - /// right shift 64 - pub(crate) fn floor(self) -> u64 { - (self.0.checked_div(2u128.checked_pow(64).unwrap()).unwrap()) as u64 - } - - pub(crate) fn frac_part(self) -> u64 { - self.0 as u64 - } - - /// left shift 64 - pub(crate) fn from_int(n: u64) -> Self { - U64F64( - (n as u128) - .checked_mul(2u128.checked_pow(64).unwrap()) - .unwrap(), - ) - } - } - - pub(crate) fn fee_tenth_of_bps(tenth_of_bps: u64) -> U64F64 { - U64F64( - ((tenth_of_bps as u128) - .checked_mul(2u128.checked_pow(64).unwrap()) - .unwrap()) - .checked_div(100_000) - .unwrap(), - ) + if !transactions_to_send.is_empty() { + panic!( + "failed to send {} of {} transactions", + transactions_to_send.len(), + transactions.len() + ); } } @@ -526,24 +502,36 @@ where #[cfg(test)] mod tests { - use {super::*, solana_sdk::bs58, tip_distribution::merkle_proof}; + use {super::*, tip_distribution::merkle_proof}; #[test] fn test_merkle_tree_verify() { // Create the merkle tree and proofs - let acct_0 = bs58::encode(Pubkey::new_unique().as_ref()).into_string(); - let acct_1 = bs58::encode(Pubkey::new_unique().as_ref()).into_string(); - + let tda = Pubkey::new_unique(); + let (acct_0, acct_1) = (Pubkey::new_unique(), Pubkey::new_unique()); + let claim_statuses = &[(acct_0, tda), (acct_1, tda)] + .iter() + .map(|(claimant, tda)| { + Pubkey::find_program_address( + &[ClaimStatus::SEED, &claimant.to_bytes(), &tda.to_bytes()], + &TipDistribution::id(), + ) + }) + .collect::>(); let tree_nodes = vec![ TreeNode { - claimant: acct_0.parse().unwrap(), + claimant: acct_0, + claim_status_pubkey: claim_statuses[0].0, + claim_status_bump: claim_statuses[0].1, staker_pubkey: Pubkey::default(), withdrawer_pubkey: Pubkey::default(), amount: 151_507, proof: None, }, TreeNode { - claimant: acct_1.parse().unwrap(), + claimant: acct_1, + claim_status_pubkey: claim_statuses[1].0, + claim_status_bump: claim_statuses[1].1, staker_pubkey: Pubkey::default(), withdrawer_pubkey: Pubkey::default(), amount: 176_624, @@ -586,11 +574,6 @@ mod tests { let validator_vote_account_0 = Pubkey::new_unique(); let validator_vote_account_1 = Pubkey::new_unique(); - println!("test stake_account {}", stake_account_0); - println!("test stake_account {}", stake_account_1); - println!("test stake_account {}", stake_account_2); - println!("test stake_account {}", stake_account_3); - let stake_meta_collection = StakeMetaCollection { stake_metas: vec![ StakeMeta { @@ -665,10 +648,27 @@ mod tests { stake_meta_collection.stake_metas.len(), merkle_tree_collection.generated_merkle_trees.len() ); - + let claim_statuses = &[ + (validator_vote_account_0, tda_0), + (stake_account_0, tda_0), + (stake_account_1, tda_0), + (validator_vote_account_1, tda_1), + (stake_account_2, tda_1), + (stake_account_3, tda_1), + ] + .iter() + .map(|(claimant, tda)| { + Pubkey::find_program_address( + &[ClaimStatus::SEED, &claimant.to_bytes(), &tda.to_bytes()], + &TipDistribution::id(), + ) + }) + .collect::>(); let tree_nodes = vec![ TreeNode { claimant: validator_vote_account_0, + claim_status_pubkey: claim_statuses[0].0, + claim_status_bump: claim_statuses[0].1, staker_pubkey: Pubkey::default(), withdrawer_pubkey: Pubkey::default(), amount: 19_001_221_110, @@ -676,6 +676,8 @@ mod tests { }, TreeNode { claimant: stake_account_0, + claim_status_pubkey: claim_statuses[1].0, + claim_status_bump: claim_statuses[1].1, staker_pubkey: Pubkey::default(), withdrawer_pubkey: Pubkey::default(), amount: 149_992, @@ -683,6 +685,8 @@ mod tests { }, TreeNode { claimant: stake_account_1, + claim_status_pubkey: claim_statuses[2].0, + claim_status_bump: claim_statuses[2].1, staker_pubkey: Pubkey::default(), withdrawer_pubkey: Pubkey::default(), amount: 174_858, @@ -707,13 +711,17 @@ mod tests { let tree_nodes = vec![ TreeNode { claimant: validator_vote_account_1, + claim_status_pubkey: claim_statuses[3].0, + claim_status_bump: claim_statuses[3].1, staker_pubkey: Pubkey::default(), withdrawer_pubkey: Pubkey::default(), - amount: 38_002_442_227, + amount: 38_002_442_226, proof: None, }, TreeNode { claimant: stake_account_2, + claim_status_pubkey: claim_statuses[4].0, + claim_status_bump: claim_statuses[4].1, staker_pubkey: Pubkey::default(), withdrawer_pubkey: Pubkey::default(), amount: 163_000, @@ -721,6 +729,8 @@ mod tests { }, TreeNode { claimant: stake_account_3, + claim_status_pubkey: claim_statuses[5].0, + claim_status_bump: claim_statuses[5].1, staker_pubkey: Pubkey::default(), withdrawer_pubkey: Pubkey::default(), amount: 508_762_900, diff --git a/tip-distributor/src/merkle_root_upload_workflow.rs b/tip-distributor/src/merkle_root_upload_workflow.rs index 4925087fae..24d4ab62dd 100644 --- a/tip-distributor/src/merkle_root_upload_workflow.rs +++ b/tip-distributor/src/merkle_root_upload_workflow.rs @@ -6,6 +6,9 @@ use { anchor_lang::AccountDeserialize, log::{error, info}, solana_client::nonblocking::rpc_client::RpcClient, + solana_program::{ + fee_calculator::DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE, native_token::LAMPORTS_PER_SOL, + }, solana_sdk::{ commitment_config::CommitmentConfig, pubkey::Pubkey, @@ -68,6 +71,16 @@ pub fn upload_merkle_root( info!("num trees to upload: {:?}", trees.len()); + // heuristic to make sure we have enough funds to cover execution, assumes all trees need updating + { + let initial_balance = rpc_client.get_balance(&keypair.pubkey()).await.expect("failed to get balance"); + let desired_balance = (trees.len() as u64).checked_mul(DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE).unwrap(); + if initial_balance < desired_balance { + let sol_to_deposit = desired_balance.checked_sub(initial_balance).unwrap().checked_add(LAMPORTS_PER_SOL).unwrap().checked_sub(1).unwrap().checked_div(LAMPORTS_PER_SOL).unwrap(); // rounds up to nearest sol + panic!("Expected to have at least {} lamports in {}, current balance is {} lamports, deposit {} SOL to continue.", + desired_balance, &keypair.pubkey(), initial_balance, sol_to_deposit) + } + } let mut trees_needing_update: Vec = vec![]; for tree in trees { let account = rpc_client diff --git a/tip-distributor/src/stake_meta_generator_workflow.rs b/tip-distributor/src/stake_meta_generator_workflow.rs index f8341689ca..a73951809d 100644 --- a/tip-distributor/src/stake_meta_generator_workflow.rs +++ b/tip-distributor/src/stake_meta_generator_workflow.rs @@ -147,11 +147,13 @@ pub fn generate_stake_meta_collection( ) -> Result { assert!(bank.is_frozen()); - let epoch_vote_accounts = bank.epoch_vote_accounts(bank.epoch()).expect(&*format!( - "No epoch_vote_accounts found for slot {} at epoch {}", - bank.slot(), - bank.epoch() - )); + let epoch_vote_accounts = bank.epoch_vote_accounts(bank.epoch()).unwrap_or_else(|| { + panic!( + "No epoch_vote_accounts found for slot {} at epoch {}", + bank.slot(), + bank.epoch() + ) + }); let l_stakes = bank.stakes_cache.stakes(); let delegations = l_stakes.stake_delegations(); @@ -225,7 +227,7 @@ pub fn generate_stake_meta_collection( let mut stake_metas = vec![]; for ((vote_pubkey, vote_account), maybe_tda) in vote_pk_and_maybe_tdas { - if let Some(delegations) = voter_pubkey_to_delegations.get(&vote_pubkey).cloned() { + if let Some(mut delegations) = voter_pubkey_to_delegations.get(&vote_pubkey).cloned() { let total_delegated = delegations.iter().fold(0u64, |sum, delegation| { sum.checked_add(delegation.lamports_delegated).unwrap() }); @@ -242,10 +244,11 @@ pub fn generate_stake_meta_collection( None }; + delegations.sort(); stake_metas.push(StakeMeta { maybe_tip_distribution_meta, validator_vote_account: vote_pubkey, - delegations: delegations.clone(), + delegations, total_delegated, commission: vote_account.vote_state().as_ref().unwrap().commission, }); @@ -256,6 +259,7 @@ pub fn generate_stake_meta_collection( ); } } + stake_metas.sort(); Ok(StakeMetaCollection { stake_metas, @@ -786,6 +790,15 @@ mod tests { vote_account: &Pubkey, delegation_amount: u64, ) -> Pubkey { + let minimum_delegation = solana_stake_program::get_minimum_delegation(&*bank.feature_set); + assert!( + delegation_amount >= minimum_delegation, + "{}", + format!( + "received delegation_amount {}, must be at least {}", + delegation_amount, minimum_delegation + ) + ); if let Some(from_account) = bank.get_account(&from_keypair.pubkey()) { assert_eq!(from_account.owner(), &solana_sdk::system_program::id()); } else {