diff --git a/client/src/rpc_request.rs b/client/src/rpc_request.rs index 37ce08dd7..02c8e60ed 100644 --- a/client/src/rpc_request.rs +++ b/client/src/rpc_request.rs @@ -32,9 +32,12 @@ pub struct RpcConfirmedBlock { } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct RpcTransactionStatus { pub status: Result<()>, pub fee: u64, + pub pre_balances: Vec, + pub post_balances: Vec, } #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index 43aa7a788..1f22b2d14 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -22,7 +22,7 @@ use solana_metrics::{inc_new_counter_debug, inc_new_counter_info, inc_new_counte use solana_perf::{cuda_runtime::PinnedVec, perf_libs}; use solana_runtime::{ accounts_db::ErrorCounters, - bank::{Bank, TransactionProcessResult}, + bank::{Bank, TransactionBalancesSet, TransactionProcessResult}, transaction_batch::TransactionBatch, }; use solana_sdk::{ @@ -511,6 +511,11 @@ impl BankingStage { // TODO: Banking stage threads should be prioritized to complete faster then this queue // expires. let txs = batch.transactions(); + let pre_balances = if transaction_status_sender.is_some() { + bank.collect_balances(txs) + } else { + vec![] + }; let (mut loaded_accounts, results, mut retryable_txs, tx_count, signature_count) = bank.load_and_execute_transactions(batch, MAX_PROCESSING_AGE); load_execute_time.stop(); @@ -541,11 +546,14 @@ impl BankingStage { signature_count, ) .processing_results; + if let Some(sender) = transaction_status_sender { + let post_balances = bank.collect_balances(txs); send_transaction_status_batch( bank.clone(), batch.transactions(), transaction_statuses, + TransactionBalancesSet::new(pre_balances, post_balances), sender, ); } diff --git a/core/src/transaction_status_service.rs b/core/src/transaction_status_service.rs index c3c4b9fba..ba6c06a52 100644 --- a/core/src/transaction_status_service.rs +++ b/core/src/transaction_status_service.rs @@ -53,10 +53,16 @@ impl TransactionStatusService { bank, transactions, statuses, + balances, } = write_transaction_status_receiver.recv_timeout(Duration::from_secs(1))?; let slot = bank.slot(); - for (transaction, (status, hash_age_kind)) in transactions.iter().zip(statuses) { + for (((transaction, (status, hash_age_kind)), pre_balances), post_balances) in transactions + .iter() + .zip(statuses) + .zip(balances.pre_balances) + .zip(balances.post_balances) + { if Bank::can_commit(&status) && !transaction.signatures.is_empty() { let fee_hash = if let Some(HashAgeKind::DurableNonce) = hash_age_kind { bank.last_blockhash() @@ -70,7 +76,12 @@ impl TransactionStatusService { blocktree .write_transaction_status( (slot, transaction.signatures[0]), - &RpcTransactionStatus { status, fee }, + &RpcTransactionStatus { + status, + fee, + pre_balances, + post_balances, + }, ) .expect("Expect database write to succeed"); } diff --git a/ledger/src/blocktree.rs b/ledger/src/blocktree.rs index 168a0e7f9..2e0b26de6 100644 --- a/ledger/src/blocktree.rs +++ b/ledger/src/blocktree.rs @@ -4550,6 +4550,12 @@ pub mod tests { .filter(|entry| !entry.is_tick()) .flat_map(|entry| entry.transactions) .map(|transaction| { + let mut pre_balances: Vec = vec![]; + let mut post_balances: Vec = vec![]; + for (i, _account_key) in transaction.message.account_keys.iter().enumerate() { + pre_balances.push(i as u64 * 10); + post_balances.push(i as u64 * 11); + } let signature = transaction.signatures[0]; ledger .transaction_status_cf @@ -4558,6 +4564,8 @@ pub mod tests { &RpcTransactionStatus { status: Ok(()), fee: 42, + pre_balances: pre_balances.clone(), + post_balances: post_balances.clone(), }, ) .unwrap(); @@ -4568,6 +4576,8 @@ pub mod tests { &RpcTransactionStatus { status: Ok(()), fee: 42, + pre_balances: pre_balances.clone(), + post_balances: post_balances.clone(), }, ) .unwrap(); @@ -4576,6 +4586,8 @@ pub mod tests { Some(RpcTransactionStatus { status: Ok(()), fee: 42, + pre_balances, + post_balances, }), ) }) @@ -4694,6 +4706,9 @@ pub mod tests { let blocktree = Blocktree::open(&blocktree_path).unwrap(); let transaction_status_cf = blocktree.db.column::(); + let pre_balances_vec = vec![1, 2, 3]; + let post_balances_vec = vec![3, 2, 1]; + // result not found assert!(transaction_status_cf .get((0, Signature::default())) @@ -4708,18 +4723,27 @@ pub mod tests { status: solana_sdk::transaction::Result::<()>::Err( TransactionError::AccountNotFound ), - fee: 5u64 + fee: 5u64, + pre_balances: pre_balances_vec.clone(), + post_balances: post_balances_vec.clone(), }, ) .is_ok()); // result found - let RpcTransactionStatus { status, fee } = transaction_status_cf + let RpcTransactionStatus { + status, + fee, + pre_balances, + post_balances, + } = transaction_status_cf .get((0, Signature::default())) .unwrap() .unwrap(); assert_eq!(status, Err(TransactionError::AccountNotFound)); assert_eq!(fee, 5u64); + assert_eq!(pre_balances, pre_balances_vec); + assert_eq!(post_balances, post_balances_vec); // insert value assert!(transaction_status_cf @@ -4727,13 +4751,20 @@ pub mod tests { (9, Signature::default()), &RpcTransactionStatus { status: solana_sdk::transaction::Result::<()>::Ok(()), - fee: 9u64 + fee: 9u64, + pre_balances: pre_balances_vec.clone(), + post_balances: post_balances_vec.clone(), }, ) .is_ok()); // result found - let RpcTransactionStatus { status, fee } = transaction_status_cf + let RpcTransactionStatus { + status, + fee, + pre_balances, + post_balances, + } = transaction_status_cf .get((9, Signature::default())) .unwrap() .unwrap(); @@ -4741,6 +4772,8 @@ pub mod tests { // deserialize assert_eq!(status, Ok(())); assert_eq!(fee, 9u64); + assert_eq!(pre_balances, pre_balances_vec); + assert_eq!(post_balances, post_balances_vec); } Blocktree::destroy(&blocktree_path).expect("Expected successful database destruction"); } @@ -4786,6 +4819,8 @@ pub mod tests { TransactionError::AccountNotFound, ), fee: x, + pre_balances: vec![], + post_balances: vec![], }, ) .unwrap(); diff --git a/ledger/src/blocktree_processor.rs b/ledger/src/blocktree_processor.rs index 927fb3464..a2e0b3358 100644 --- a/ledger/src/blocktree_processor.rs +++ b/ledger/src/blocktree_processor.rs @@ -14,7 +14,7 @@ use rayon::{prelude::*, ThreadPool}; use solana_metrics::{datapoint, datapoint_error, inc_new_counter_debug}; use solana_rayon_threadlimit::get_thread_count; use solana_runtime::{ - bank::{Bank, TransactionProcessResult, TransactionResults}, + bank::{Bank, TransactionBalancesSet, TransactionProcessResult, TransactionResults}, transaction_batch::TransactionBatch, }; use solana_sdk::{ @@ -54,18 +54,24 @@ fn execute_batch( bank: &Arc, transaction_status_sender: Option, ) -> Result<()> { - let TransactionResults { - fee_collection_results, - processing_results, - } = batch - .bank() - .load_execute_and_commit_transactions(batch, MAX_RECENT_BLOCKHASHES); + let ( + TransactionResults { + fee_collection_results, + processing_results, + }, + balances, + ) = batch.bank().load_execute_and_commit_transactions( + batch, + MAX_RECENT_BLOCKHASHES, + transaction_status_sender.is_some(), + ); if let Some(sender) = transaction_status_sender { send_transaction_status_batch( bank.clone(), batch.transactions(), processing_results, + balances, sender, ); } @@ -560,6 +566,7 @@ pub struct TransactionStatusBatch { pub bank: Arc, pub transactions: Vec, pub statuses: Vec, + pub balances: TransactionBalancesSet, } pub type TransactionStatusSender = Sender; @@ -567,6 +574,7 @@ pub fn send_transaction_status_batch( bank: Arc, transactions: &[Transaction], statuses: Vec, + balances: TransactionBalancesSet, transaction_status_sender: TransactionStatusSender, ) { let slot = bank.slot(); @@ -574,6 +582,7 @@ pub fn send_transaction_status_batch( bank, transactions: transactions.to_vec(), statuses, + balances, }) { trace!( "Slot {} transaction_status send batch failed: {:?}", diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 4144c0ecd..882e66a9b 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -161,6 +161,20 @@ pub struct TransactionResults { pub fee_collection_results: Vec>, pub processing_results: Vec, } +pub struct TransactionBalancesSet { + pub pre_balances: TransactionBalances, + pub post_balances: TransactionBalances, +} +impl TransactionBalancesSet { + pub fn new(pre_balances: TransactionBalances, post_balances: TransactionBalances) -> Self { + assert_eq!(pre_balances.len(), post_balances.len()); + Self { + pre_balances, + post_balances, + } + } +} +pub type TransactionBalances = Vec>; #[derive(Clone, Debug, Eq, PartialEq)] pub enum HashAgeKind { @@ -1030,6 +1044,18 @@ impl Bank { self.check_signatures(txs, iteration_order, age_results, &mut error_counters) } + pub fn collect_balances(&self, batch: &[Transaction]) -> TransactionBalances { + let mut balances: TransactionBalances = vec![]; + for transaction in batch.iter() { + let mut transaction_balances: Vec = vec![]; + for account_key in transaction.message.account_keys.iter() { + transaction_balances.push(self.get_balance(account_key)); + } + balances.push(transaction_balances); + } + balances + } + fn update_error_counters(error_counters: &ErrorCounters) { if 0 != error_counters.blockhash_not_found { inc_new_counter_error!( @@ -1372,24 +1398,40 @@ impl Bank { &self, batch: &TransactionBatch, max_age: usize, - ) -> TransactionResults { + collect_balances: bool, + ) -> (TransactionResults, TransactionBalancesSet) { + let pre_balances = if collect_balances { + self.collect_balances(batch.transactions()) + } else { + vec![] + }; let (mut loaded_accounts, executed, _, tx_count, signature_count) = self.load_and_execute_transactions(batch, max_age); - self.commit_transactions( + let results = self.commit_transactions( batch.transactions(), batch.iteration_order(), &mut loaded_accounts, &executed, tx_count, signature_count, + ); + let post_balances = if collect_balances { + self.collect_balances(batch.transactions()) + } else { + vec![] + }; + ( + results, + TransactionBalancesSet::new(pre_balances, post_balances), ) } #[must_use] pub fn process_transactions(&self, txs: &[Transaction]) -> Vec> { let batch = self.prepare_batch(txs, None); - self.load_execute_and_commit_transactions(&batch, MAX_RECENT_BLOCKHASHES) + self.load_execute_and_commit_transactions(&batch, MAX_RECENT_BLOCKHASHES, false) + .0 .fee_collection_results } @@ -1816,7 +1858,7 @@ mod tests { clock::DEFAULT_TICKS_PER_SLOT, epoch_schedule::MINIMUM_SLOTS_PER_EPOCH, genesis_config::create_genesis_config, - instruction::{Instruction, InstructionError}, + instruction::{CompiledInstruction, Instruction, InstructionError}, message::{Message, MessageHeader}, nonce_instruction, nonce_state, poh_config::PohConfig, @@ -3221,7 +3263,8 @@ mod tests { let lock_result = bank.prepare_batch(&pay_alice, None); let results_alice = bank - .load_execute_and_commit_transactions(&lock_result, MAX_RECENT_BLOCKHASHES) + .load_execute_and_commit_transactions(&lock_result, MAX_RECENT_BLOCKHASHES, false) + .0 .fee_collection_results; assert_eq!(results_alice[0], Ok(())); @@ -4731,4 +4774,94 @@ mod tests { /* Check fee charged */ assert_eq!(bank.get_balance(&custodian_pubkey), 4_630_000); } + + #[test] + fn test_collect_balances() { + let (genesis_config, _mint_keypair) = create_genesis_config(500); + let parent = Arc::new(Bank::new(&genesis_config)); + let bank0 = Arc::new(new_from_parent(&parent)); + + let keypair = Keypair::new(); + let pubkey0 = Pubkey::new_rand(); + let pubkey1 = Pubkey::new_rand(); + let program_id = Pubkey::new(&[2; 32]); + let keypair_account = Account::new(8, 0, &program_id); + let account0 = Account::new(11, 0, &program_id); + let program_account = Account::new(1, 10, &Pubkey::default()); + bank0.store_account(&keypair.pubkey(), &keypair_account); + bank0.store_account(&pubkey0, &account0); + bank0.store_account(&program_id, &program_account); + + let instructions = vec![CompiledInstruction::new(1, &(), vec![0])]; + let tx0 = Transaction::new_with_compiled_instructions( + &[&keypair], + &[pubkey0], + Hash::default(), + vec![program_id], + instructions, + ); + let instructions = vec![CompiledInstruction::new(1, &(), vec![0])]; + let tx1 = Transaction::new_with_compiled_instructions( + &[&keypair], + &[pubkey1], + Hash::default(), + vec![program_id], + instructions, + ); + let balances = bank0.collect_balances(&[tx0, tx1]); + assert_eq!(balances.len(), 2); + assert_eq!(balances[0], vec![8, 11, 1]); + assert_eq!(balances[1], vec![8, 0, 1]); + } + + #[test] + fn test_pre_post_transaction_balances() { + let (mut genesis_config, _mint_keypair) = create_genesis_config(500); + let fee_calculator = FeeCalculator::new(1, 0); + genesis_config.fee_calculator = fee_calculator; + let parent = Arc::new(Bank::new(&genesis_config)); + let bank0 = Arc::new(new_from_parent(&parent)); + + let keypair0 = Keypair::new(); + let keypair1 = Keypair::new(); + let pubkey0 = Pubkey::new_rand(); + let pubkey1 = Pubkey::new_rand(); + let pubkey2 = Pubkey::new_rand(); + let keypair0_account = Account::new(8, 0, &Pubkey::default()); + let keypair1_account = Account::new(9, 0, &Pubkey::default()); + let account0 = Account::new(11, 0, &&Pubkey::default()); + bank0.store_account(&keypair0.pubkey(), &keypair0_account); + bank0.store_account(&keypair1.pubkey(), &keypair1_account); + bank0.store_account(&pubkey0, &account0); + + let blockhash = bank0.last_blockhash(); + + let tx0 = system_transaction::transfer(&keypair0, &pubkey0, 2, blockhash.clone()); + let tx1 = system_transaction::transfer(&Keypair::new(), &pubkey1, 2, blockhash.clone()); + let tx2 = system_transaction::transfer(&keypair1, &pubkey2, 12, blockhash.clone()); + let txs = vec![tx0, tx1, tx2]; + + let lock_result = bank0.prepare_batch(&txs, None); + let (transaction_results, transaction_balances_set) = + bank0.load_execute_and_commit_transactions(&lock_result, MAX_RECENT_BLOCKHASHES, true); + + assert_eq!(transaction_balances_set.pre_balances.len(), 3); + assert_eq!(transaction_balances_set.post_balances.len(), 3); + + assert!(transaction_results.processing_results[0].0.is_ok()); + assert_eq!(transaction_balances_set.pre_balances[0], vec![8, 11, 1]); + assert_eq!(transaction_balances_set.post_balances[0], vec![5, 13, 1]); + + // Failed transactions still produce balance sets + // This is a TransactionError - not possible to charge fees + assert!(transaction_results.processing_results[1].0.is_err()); + assert_eq!(transaction_balances_set.pre_balances[1], vec![0, 0, 1]); + assert_eq!(transaction_balances_set.post_balances[1], vec![0, 0, 1]); + + // Failed transactions still produce balance sets + // This is an InstructionError - fees charged + assert!(transaction_results.processing_results[2].0.is_err()); + assert_eq!(transaction_balances_set.pre_balances[2], vec![9, 0, 1]); + assert_eq!(transaction_balances_set.post_balances[2], vec![8, 0, 1]); + } }