diff --git a/Cargo.lock b/Cargo.lock index 2778c38428..66ddcc3e0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6685,8 +6685,8 @@ dependencies = [ "solana-genesis-utils", "solana-ledger", "solana-merkle-tree", + "solana-metrics", "solana-program 1.14.22", - "solana-rpc", "solana-runtime", "solana-sdk 1.14.22", "solana-stake-program", diff --git a/tip-distributor/Cargo.toml b/tip-distributor/Cargo.toml index c87ca5965d..150b7944f7 100644 --- a/tip-distributor/Cargo.toml +++ b/tip-distributor/Cargo.toml @@ -7,7 +7,7 @@ description = "Collection of binaries used to distribute MEV rewards to delegato [dependencies] anchor-lang = { path = "../anchor/lang" } -clap = { version = "3", features = ["derive", "env"] } +clap = { version = "3.1.18", features = ["derive", "env"] } env_logger = "0.9.0" futures = "0.3.21" im = "15.1.0" @@ -20,8 +20,8 @@ solana-client = { path = "../client", version = "=1.14.22" } solana-genesis-utils = { path = "../genesis-utils", version = "=1.14.22" } solana-ledger = { path = "../ledger", version = "=1.14.22" } solana-merkle-tree = { path = "../merkle-tree", version = "=1.14.22" } +solana-metrics = { path = "../metrics", version = "=1.14.22" } solana-program = { path = "../sdk/program", version = "=1.14.22" } -solana-rpc = { path = "../rpc", version = "=1.14.22" } solana-runtime = { path = "../runtime", version = "=1.14.22" } solana-sdk = { path = "../sdk", version = "=1.14.22" } solana-stake-program = { path = "../programs/stake", version = "=1.14.22" } diff --git a/tip-distributor/src/bin/merkle-root-generator.rs b/tip-distributor/src/bin/merkle-root-generator.rs index f9e9072bed..bbf4105503 100644 --- a/tip-distributor/src/bin/merkle-root-generator.rs +++ b/tip-distributor/src/bin/merkle-root-generator.rs @@ -14,6 +14,10 @@ struct Args { #[clap(long, env)] stake_meta_coll_path: PathBuf, + /// RPC to send transactions through. Used to validate what's being claimed is equal to TDA balance minus rent. + #[clap(long, env)] + rpc_url: String, + /// Path to JSON file to get populated with tree node data. #[clap(long, env)] out_path: PathBuf, @@ -24,6 +28,7 @@ fn main() { info!("Starting merkle-root-generator workflow..."); let args: Args = Args::parse(); - generate_merkle_root(&args.stake_meta_coll_path, &args.out_path).expect("merkle tree produced"); + generate_merkle_root(&args.stake_meta_coll_path, &args.out_path, &args.rpc_url) + .expect("merkle tree produced"); info!("saved merkle roots to {:?}", args.stake_meta_coll_path); } diff --git a/tip-distributor/src/bin/reclaim-rent.rs b/tip-distributor/src/bin/reclaim-rent.rs index 2d25744c6d..a86491f4e3 100644 --- a/tip-distributor/src/bin/reclaim-rent.rs +++ b/tip-distributor/src/bin/reclaim-rent.rs @@ -29,6 +29,10 @@ struct Args { #[clap(long, env)] keypair_path: PathBuf, + /// High timeout b/c of get_program_accounts call + #[clap(long, env, default_value_t = 180)] + rpc_timeout_secs: u64, + /// Specifies whether to reclaim rent on behalf of validators from respective TDAs. #[clap(long, env)] should_reclaim_tdas: bool, @@ -44,8 +48,7 @@ fn main() { if let Err(e) = runtime.block_on(reclaim_rent( RpcClient::new_with_timeout_and_commitment( args.rpc_url, - // High timeout b/c of get_program_accounts call - Duration::from_secs(60), + Duration::from_secs(args.rpc_timeout_secs), CommitmentConfig::confirmed(), ), args.tip_distribution_program_id, diff --git a/tip-distributor/src/claim_mev_workflow.rs b/tip-distributor/src/claim_mev_workflow.rs index e5551ee944..d6ba42899e 100644 --- a/tip-distributor/src/claim_mev_workflow.rs +++ b/tip-distributor/src/claim_mev_workflow.rs @@ -1,8 +1,10 @@ use { - crate::{read_json_from_file, send_transactions_with_retry, GeneratedMerkleTreeCollection}, + crate::{ + read_json_from_file, sign_and_send_transactions_with_retries, GeneratedMerkleTreeCollection, + }, anchor_lang::{AccountDeserialize, InstructionData, ToAccountMetas}, log::{debug, info, warn}, - solana_client::{nonblocking::rpc_client::RpcClient, rpc_request::RpcError}, + solana_client::{client_error, 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, @@ -35,8 +37,7 @@ pub fn claim_mev_tips( tip_distribution_program_id: &Pubkey, keypair_path: &PathBuf, ) -> Result<(), ClaimMevError> { - // roughly how long before blockhash expires - const MAX_RETRY_DURATION: Duration = Duration::from_secs(60); + const MAX_RETRY_DURATION: Duration = Duration::from_secs(600); let merkle_trees: GeneratedMerkleTreeCollection = read_json_from_file(merkle_root_path).expect("read GeneratedMerkleTreeCollection"); @@ -46,7 +47,7 @@ pub fn claim_mev_tips( Pubkey::find_program_address(&[Config::SEED], tip_distribution_program_id).0; let rpc_client = - RpcClient::new_with_commitment(rpc_url.to_string(), CommitmentConfig::confirmed()); + RpcClient::new_with_commitment(rpc_url.to_string(), CommitmentConfig::finalized()); let runtime = Builder::new_multi_thread() .worker_threads(16) @@ -54,10 +55,9 @@ pub fn claim_mev_tips( .build() .unwrap(); - let mut transactions = Vec::new(); + let mut instructions = Vec::new(); runtime.block_on(async move { - 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 { @@ -99,7 +99,7 @@ pub fn claim_mev_tips( continue; } // 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(client_error::ClientError { kind: client_error::ClientErrorKind::RpcError(RpcError::ForUser(err)), .. }) if err.starts_with("AccountNotFound") => {} Err(err) => panic!("Unexpected RPC Error: {}", err), } @@ -112,7 +112,7 @@ pub fn claim_mev_tips( below_min_rent_count = below_min_rent_count.checked_add(1).unwrap(); continue; } - let ix = Instruction { + instructions.push(Instruction { program_id: *tip_distribution_program_id, data: tip_distribution::instruction::Claim { proof: node.proof.unwrap(), @@ -127,23 +127,23 @@ pub fn claim_mev_tips( 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); + }); } } + let transactions = instructions.into_iter().map(|ix|{ + Transaction::new_with_payer( + &[ix], + Some(&keypair.pubkey()), + ) + }).collect::>(); + info!("Sending {} tip claim transactions. {} tried sending zero lamports, {} would be below minimum rent", &transactions.len(), zero_lamports_count, below_min_rent_count); - let num_failed_txs = send_transactions_with_retry(&rpc_client, &transactions, MAX_RETRY_DURATION).await; - if num_failed_txs != 0 { - panic!("failed to send {num_failed_txs} transactions"); + + let failed_transactions = sign_and_send_transactions_with_retries(&keypair, &rpc_client, transactions, MAX_RETRY_DURATION).await; + if !failed_transactions.is_empty() { + panic!("failed to send {} transactions", failed_transactions.len()); } }); diff --git a/tip-distributor/src/lib.rs b/tip-distributor/src/lib.rs index 14036cc927..6dd0adbc58 100644 --- a/tip-distributor/src/lib.rs +++ b/tip-distributor/src/lib.rs @@ -10,24 +10,31 @@ use { stake_meta_generator_workflow::StakeMetaGeneratorError::CheckedMathError, }, anchor_lang::Id, - log::{error, info}, + log::*, serde::{de::DeserializeOwned, Deserialize, Serialize}, - solana_client::nonblocking::rpc_client::RpcClient, + solana_client::{ + client_error::{ClientError, ClientErrorKind}, + nonblocking::rpc_client::RpcClient, + rpc_client::RpcClient as SyncRpcClient, + rpc_request::RpcRequest, + }, solana_merkle_tree::MerkleTree, + solana_metrics::{datapoint_error, datapoint_warn}, solana_sdk::{ account::{AccountSharedData, ReadableAccount}, clock::Slot, hash::{Hash, Hasher}, pubkey::Pubkey, - signature::Signature, + signature::{Keypair, Signature}, stake_history::Epoch, - transaction::Transaction, + transaction::{Transaction, TransactionError::AlreadyProcessed}, }, std::{ collections::HashMap, fs::File, io::BufReader, path::PathBuf, + sync::Arc, time::{Duration, Instant}, }, tip_distribution::{ @@ -67,9 +74,47 @@ pub struct TipPaymentPubkeys { tip_pdas: Vec, } +fn emit_inconsistent_tree_node_amount_dp( + tree_nodes: &[TreeNode], + tip_distribution_account: &Pubkey, + rpc_client: &SyncRpcClient, +) { + let actual_claims: u64 = tree_nodes.iter().map(|t| t.amount).sum(); + let tda = rpc_client.get_account(tip_distribution_account).unwrap(); + let min_rent = rpc_client + .get_minimum_balance_for_rent_exemption(tda.data.len()) + .unwrap(); + + let expected_claims = tda.lamports.checked_sub(min_rent).unwrap(); + if actual_claims == expected_claims { + return; + } + + if actual_claims > expected_claims { + datapoint_error!( + "tip-distributor", + ( + "actual_claims_exceeded", + format!("tip_distribution_account={tip_distribution_account},actual_claims={actual_claims}, expected_claims={expected_claims}"), + String + ), + ); + } else { + datapoint_warn!( + "tip-distributor", + ( + "actual_claims_below", + format!("tip_distribution_account={tip_distribution_account},actual_claims={actual_claims}, expected_claims={expected_claims}"), + String + ), + ); + } +} + impl GeneratedMerkleTreeCollection { pub fn new_from_stake_meta_collection( stake_meta_coll: StakeMetaCollection, + maybe_rpc_client: Option, ) -> Result { let generated_merkle_trees = stake_meta_coll .stake_metas @@ -81,6 +126,16 @@ impl GeneratedMerkleTreeCollection { Ok(maybe_tree_nodes) => maybe_tree_nodes, }?; + if let Some(rpc_client) = &maybe_rpc_client { + if let Some(tda) = stake_meta.maybe_tip_distribution_meta.as_ref() { + emit_inconsistent_tree_node_amount_dp( + &tree_nodes[..], + &tda.tip_distribution_pubkey, + rpc_client, + ); + } + } + let hashed_nodes: Vec<[u8; 32]> = tree_nodes.iter().map(|n| n.hash().to_bytes()).collect(); @@ -239,11 +294,6 @@ impl TreeNode { .collect::, MerkleRootGeneratorError>>()?, ); - let total_claim_amount = tree_nodes.iter().fold(0u64, |sum, tree_node| { - sum.checked_add(tree_node.amount).unwrap() - }); - assert!(total_claim_amount < stake_meta.total_delegated); - Ok(Some(tree_nodes)) } else { Ok(None) @@ -282,6 +332,9 @@ pub struct StakeMeta { #[serde(with = "pubkey_string_conversion")] pub validator_vote_account: Pubkey, + #[serde(with = "pubkey_string_conversion")] + pub validator_node_pubkey: Pubkey, + /// The validator's tip-distribution meta if it exists. pub maybe_tip_distribution_meta: Option, @@ -408,59 +461,106 @@ pub fn derive_tip_distribution_account_address( ) } -pub async fn send_transactions_with_retry( +pub async fn sign_and_send_transactions_with_retries( + signer: &Keypair, rpc_client: &RpcClient, - transactions: &[Transaction], - max_send_duration: Duration, -) -> usize { - let mut transactions_to_send: HashMap = transactions - .iter() - .map(|tx| (tx.signatures[0], tx.clone())) - .collect(); + transactions: Vec, + max_retry_duration: Duration, +) -> HashMap { + use tokio::sync::Semaphore; + const MAX_CONCURRENT_RPC_CALLS: usize = 50; + let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_RPC_CALLS)); + + let mut errors = HashMap::default(); + let mut blockhash = rpc_client + .get_latest_blockhash() + .await + .expect("fetch latest blockhash"); + + let mut signatures_to_transactions = transactions + .into_iter() + .map(|mut tx| { + tx.sign(&[signer], blockhash); + (tx.signatures[0], tx) + }) + .collect::>(); let start = Instant::now(); - while !transactions_to_send.is_empty() && start.elapsed() < max_send_duration { - info!("sending {} transactions", transactions_to_send.len()); - - for (signature, tx) in &transactions_to_send { - match rpc_client.send_transaction(tx).await { - Ok(send_tx_sig) => { - info!("send transaction: {:?}", send_tx_sig); - } - Err(e) => { - error!("error sending transaction {:?} error: {:?}", signature, e); - } - } + while start.elapsed() < max_retry_duration && !signatures_to_transactions.is_empty() { + if start.elapsed() > Duration::from_secs(60) { + blockhash = rpc_client + .get_latest_blockhash() + .await + .expect("fetch latest blockhash"); + signatures_to_transactions + .iter_mut() + .for_each(|(_sig, tx)| { + *tx = Transaction::new_unsigned(tx.message.clone()); + tx.sign(&[signer], blockhash); + }); } - sleep(Duration::from_secs(10)).await; + let futs = signatures_to_transactions.iter().map(|(sig, tx)| { + let semaphore = semaphore.clone(); + async move { + let permit = semaphore.clone().acquire_owned().await.unwrap(); + let res = match rpc_client.send_transaction(tx).await { + Ok(_sig) => { + info!("sent transaction: {_sig:?}"); + drop(permit); + sleep(Duration::from_secs(10)).await; + + let _permit = semaphore.acquire_owned().await.unwrap(); + match rpc_client.confirm_transaction(sig).await { + Ok(true) => Ok(()), + Ok(false) => Err(ClientError::new_with_request( + ClientErrorKind::Custom( + "transaction failed to confirm".to_string(), + ), + RpcRequest::SendTransaction, + )), + Err(e) => Err(e), + } + } + Err(e) => Err(e), + }; + + let res = res + .err() + .map(|e| { + if let ClientErrorKind::TransactionError(AlreadyProcessed) = e.kind { + Ok(()) + } else { + error!("error sending transaction {sig:?} error: {e:?}"); + Err(e) + } + }) + .unwrap_or(Ok(())); - let mut signatures_confirmed: Vec = Vec::new(); - for signature in transactions_to_send.keys() { - match rpc_client.confirm_transaction(signature).await { - Ok(true) => { - info!("confirmed signature: {:?}", signature); - signatures_confirmed.push(*signature); - } - Ok(false) => { - info!("didn't confirmed signature: {:?}", signature); - } - Err(e) => { - error!( - "error confirming signature: {:?}, signature: {:?}", - signature, e - ); - } + (*sig, res) } - } + }); - info!("confirmed #{} signatures", signatures_confirmed.len()); - for sig in signatures_confirmed { - transactions_to_send.remove(&sig); - } + errors = futures::future::join_all(futs) + .await + .into_iter() + .filter(|(sig, result)| { + if result.is_err() { + true + } else { + let _ = signatures_to_transactions.remove(sig); + false + } + }) + .map(|(sig, result)| { + let e = result.err().unwrap(); + warn!("error sending transaction: [error={e}, signature={sig}]"); + (sig, e) + }) + .collect::>(); } - transactions_to_send.len() + errors } mod pubkey_string_conversion { @@ -569,10 +669,14 @@ mod tests { let validator_vote_account_0 = Pubkey::new_unique(); let validator_vote_account_1 = Pubkey::new_unique(); + let validator_id_0 = Pubkey::new_unique(); + let validator_id_1 = Pubkey::new_unique(); + let stake_meta_collection = StakeMetaCollection { stake_metas: vec![ StakeMeta { validator_vote_account: validator_vote_account_0, + validator_node_pubkey: validator_id_0, maybe_tip_distribution_meta: Some(TipDistributionMeta { merkle_root_upload_authority, tip_distribution_pubkey: tda_0, @@ -598,6 +702,7 @@ mod tests { }, StakeMeta { validator_vote_account: validator_vote_account_1, + validator_node_pubkey: validator_id_1, maybe_tip_distribution_meta: Some(TipDistributionMeta { merkle_root_upload_authority, tip_distribution_pubkey: tda_1, @@ -623,13 +728,14 @@ mod tests { }, ], tip_distribution_program_id: Pubkey::new_unique(), - bank_hash: solana_sdk::hash::Hash::new_unique().to_string(), + bank_hash: Hash::new_unique().to_string(), epoch: 100, slot: 2_000_000, }; let merkle_tree_collection = GeneratedMerkleTreeCollection::new_from_stake_meta_collection( stake_meta_collection.clone(), + None, ) .unwrap(); diff --git a/tip-distributor/src/merkle_root_generator_workflow.rs b/tip-distributor/src/merkle_root_generator_workflow.rs index 0da8c1fdb0..7d9c51ee99 100644 --- a/tip-distributor/src/merkle_root_generator_workflow.rs +++ b/tip-distributor/src/merkle_root_generator_workflow.rs @@ -1,6 +1,7 @@ use { crate::{read_json_from_file, GeneratedMerkleTreeCollection, StakeMetaCollection}, log::*, + solana_client::rpc_client::RpcClient, std::{ fmt::Debug, fs::File, @@ -25,11 +26,15 @@ pub enum MerkleRootGeneratorError { pub fn generate_merkle_root( stake_meta_coll_path: &PathBuf, out_path: &PathBuf, + rpc_url: &str, ) -> Result<(), MerkleRootGeneratorError> { let stake_meta_coll: StakeMetaCollection = read_json_from_file(stake_meta_coll_path)?; - let merkle_tree_coll = - GeneratedMerkleTreeCollection::new_from_stake_meta_collection(stake_meta_coll)?; + let rpc_client = RpcClient::new(rpc_url); + let merkle_tree_coll = GeneratedMerkleTreeCollection::new_from_stake_meta_collection( + stake_meta_coll, + Some(rpc_client), + )?; write_to_json_file(&merkle_tree_coll, out_path)?; Ok(()) @@ -42,7 +47,7 @@ fn write_to_json_file( let file = File::create(file_path)?; let mut writer = BufWriter::new(file); let json = serde_json::to_string_pretty(&merkle_tree_coll).unwrap(); - let _ = writer.write(json.as_bytes())?; + let _ = writer.write_all(json.as_bytes())?; writer.flush()?; Ok(()) diff --git a/tip-distributor/src/merkle_root_upload_workflow.rs b/tip-distributor/src/merkle_root_upload_workflow.rs index 447a621418..14ce93b5cc 100644 --- a/tip-distributor/src/merkle_root_upload_workflow.rs +++ b/tip-distributor/src/merkle_root_upload_workflow.rs @@ -1,6 +1,6 @@ use { crate::{ - read_json_from_file, send_transactions_with_retry, GeneratedMerkleTree, + read_json_from_file, sign_and_send_transactions_with_retries, GeneratedMerkleTree, GeneratedMerkleTreeCollection, }, anchor_lang::AccountDeserialize, @@ -39,8 +39,7 @@ pub fn upload_merkle_root( rpc_url: &str, tip_distribution_program_id: &Pubkey, ) -> Result<(), MerkleRootUploadError> { - // max amount of time before blockhash expires - const MAX_RETRY_DURATION: Duration = Duration::from_secs(60); + const MAX_RETRY_DURATION: Duration = Duration::from_secs(600); let merkle_tree: GeneratedMerkleTreeCollection = read_json_from_file(merkle_root_path).expect("read GeneratedMerkleTreeCollection"); @@ -58,11 +57,6 @@ pub fn upload_merkle_root( runtime.block_on(async move { let rpc_client = RpcClient::new_with_commitment(rpc_url.to_string(), CommitmentConfig::confirmed()); - let recent_blockhash = rpc_client - .get_latest_blockhash() - .await - .expect("get blockhash"); - let trees: Vec = merkle_tree .generated_merkle_trees .into_iter() @@ -124,17 +118,15 @@ pub fn upload_merkle_root( tip_distribution_account: tree.tip_distribution_account, }, ); - Transaction::new_signed_with_payer( + Transaction::new_with_payer( &[ix], Some(&keypair.pubkey()), - &[&keypair], - recent_blockhash, ) }) .collect(); - let num_failed_txs = send_transactions_with_retry(&rpc_client, &transactions, MAX_RETRY_DURATION).await; - if num_failed_txs != 0 { - panic!("failed to send {num_failed_txs} transactions"); + let failed_transactions = sign_and_send_transactions_with_retries(&keypair, &rpc_client, transactions, MAX_RETRY_DURATION).await; + if !failed_transactions.is_empty() { + panic!("failed to send {} transactions", failed_transactions.len()); } }); diff --git a/tip-distributor/src/reclaim_rent_workflow.rs b/tip-distributor/src/reclaim_rent_workflow.rs index e2f066926b..4d39bf8d5a 100644 --- a/tip-distributor/src/reclaim_rent_workflow.rs +++ b/tip-distributor/src/reclaim_rent_workflow.rs @@ -1,5 +1,5 @@ use { - crate::send_transactions_with_retry, + crate::sign_and_send_transactions_with_retries, anchor_lang::AccountDeserialize, log::info, solana_client::nonblocking::rpc_client::RpcClient, @@ -145,14 +145,7 @@ pub async fn reclaim_rent( ) .collect::>() .chunks(4) - .map(|instructions| { - Transaction::new_signed_with_payer( - instructions, - Some(&signer.pubkey()), - &[&signer], - recent_blockhash, - ) - }) + .map(|instructions| Transaction::new_with_payer(instructions, Some(&signer.pubkey()))) .collect::>(); info!("create close_tip_distribution_account transactions took {}us, closing {} tip distribution accounts", now.elapsed().as_micros(), close_tda_txs.len()); @@ -160,14 +153,15 @@ pub async fn reclaim_rent( } info!("sending {} transactions", transactions.len()); - let num_failed_txs = send_transactions_with_retry( + let failed_txs = sign_and_send_transactions_with_retries( + &signer, &rpc_client, - transactions.as_slice(), - Duration::from_secs(60), + transactions, + Duration::from_secs(300), ) .await; - if num_failed_txs != 0 { - panic!("failed to send {num_failed_txs} transactions"); + if !failed_txs.is_empty() { + panic!("failed to send {} transactions", failed_txs.len()); } Ok(()) diff --git a/tip-distributor/src/stake_meta_generator_workflow.rs b/tip-distributor/src/stake_meta_generator_workflow.rs index 91bb2cc9f0..35bfaedde3 100644 --- a/tip-distributor/src/stake_meta_generator_workflow.rs +++ b/tip-distributor/src/stake_meta_generator_workflow.rs @@ -30,6 +30,7 @@ use { fmt::{Debug, Display, Formatter}, fs::File, io::{BufWriter, Write}, + mem::size_of, path::{Path, PathBuf}, sync::Arc, }, @@ -130,7 +131,7 @@ fn write_to_json_file( let file = File::create(out_path)?; let mut writer = BufWriter::new(file); let json = serde_json::to_string_pretty(&stake_meta_coll).unwrap(); - let _ = writer.write(json.as_bytes())?; + let _ = writer.write_all(json.as_bytes())?; writer.flush()?; Ok(()) @@ -206,12 +207,12 @@ pub fn generate_stake_meta_collection( bank.epoch(), ) .0; - let tda = bank - .get_account(&tip_distribution_pubkey) - .map(|mut account_data| { - let tip_distribution_account = - TipDistributionAccount::try_deserialize(&mut account_data.data()) - .expect("deserialized TipDistributionAccount"); + let tda = if let Some(mut account_data) = bank.get_account(&tip_distribution_pubkey) { + // TDAs may be funded with lamports and therefore exist in the bank, but would fail the deserialization step + // if the buffer is yet to be allocated thru the init call to the program. + if let Ok(tip_distribution_account) = + TipDistributionAccount::try_deserialize(&mut account_data.data()) + { // this snapshot might have tips that weren't claimed by the time the epoch is over // assume that it will eventually be cranked and credit the excess to this account if tip_distribution_pubkey == tip_receiver { @@ -222,12 +223,17 @@ pub fn generate_stake_meta_collection( .expect("tip overflow"), ); } - TipDistributionAccountWrapper { + Some(TipDistributionAccountWrapper { tip_distribution_account, account_data, tip_distribution_pubkey, - } - }); + }) + } else { + None + } + } else { + None + }; Ok(((*vote_pubkey, vote_account), tda)) }) .collect::>()?; @@ -240,6 +246,12 @@ pub fn generate_stake_meta_collection( }); let maybe_tip_distribution_meta = if let Some(tda) = maybe_tda { + let actual_len = tda.account_data.data().len(); + let expected_len: usize = + 8_usize.saturating_add(size_of::()); + if actual_len != expected_len { + warn!("len mismatch actual={actual_len}, expected={expected_len}"); + } let rent_exempt_amount = bank.get_minimum_balance_for_rent_exemption(tda.account_data.data().len()); @@ -251,13 +263,15 @@ pub fn generate_stake_meta_collection( None }; + let vote_state = vote_account.vote_state().as_ref().unwrap(); delegations.sort(); stake_metas.push(StakeMeta { maybe_tip_distribution_meta, + validator_node_pubkey: vote_state.node_pubkey, validator_vote_account: vote_pubkey, delegations, total_delegated, - commission: vote_account.vote_state().as_ref().unwrap().commission, + commission: vote_state.commission, }); } else { warn!( @@ -684,6 +698,7 @@ mod tests { validator_fee_bps: tda_0_fields.1, }), commission: 0, + validator_node_pubkey: validator_keypairs_0.node_keypair.pubkey(), }, ); expected_stake_metas.insert( @@ -709,6 +724,7 @@ mod tests { validator_fee_bps: tda_1_fields.1, }), commission: 0, + validator_node_pubkey: validator_keypairs_1.node_keypair.pubkey(), }, ); expected_stake_metas.insert( @@ -734,6 +750,7 @@ mod tests { validator_fee_bps: tda_2_fields.1, }), commission: 0, + validator_node_pubkey: validator_keypairs_2.node_keypair.pubkey(), }, );