diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index d1a1805d0d3a20..15816f746cab45 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -122,7 +122,7 @@ use { self, include_loaded_accounts_data_size_in_fee_calculation, remove_rounding_in_fee_calculation, FeatureSet, }, - fee::FeeStructure, + fee::{FeeDetails, FeeStructure}, fee_calculator::{FeeCalculator, FeeRateGovernor}, genesis_config::{ClusterType, GenesisConfig}, hard_forks::HardForks, @@ -253,6 +253,12 @@ impl AddAssign for SquashTiming { } } +#[derive(AbiExample, Debug, Default)] +pub(crate) struct CollectorFeeDetails { + pub transaction_fee: AtomicU64, + pub priority_fee: AtomicU64, +} + #[derive(Debug)] pub struct BankRc { /// where all the Accounts are stored @@ -548,6 +554,7 @@ impl PartialEq for Bank { loaded_programs_cache: _, epoch_reward_status: _, transaction_processor: _, + collector_fee_details: _, // Ignore new fields explicitly if they do not impact PartialEq. // Adding ".." will remove compile-time checks that if a new field // is added to the struct, this PartialEq is accordingly updated. @@ -808,6 +815,9 @@ pub struct Bank { epoch_reward_status: EpochRewardStatus, transaction_processor: TransactionBatchProcessor, + + /// Collected fee details + collector_fee_details: Arc, } struct VoteWithStakeDelegations { @@ -994,6 +1004,7 @@ impl Bank { ))), epoch_reward_status: EpochRewardStatus::default(), transaction_processor: TransactionBatchProcessor::default(), + collector_fee_details: Arc::new(CollectorFeeDetails::default()), }; bank.transaction_processor = TransactionBatchProcessor::new( @@ -1312,6 +1323,7 @@ impl Bank { loaded_programs_cache: parent.loaded_programs_cache.clone(), epoch_reward_status: parent.epoch_reward_status.clone(), transaction_processor: TransactionBatchProcessor::default(), + collector_fee_details: Arc::new(CollectorFeeDetails::default()), }; new.transaction_processor = TransactionBatchProcessor::new( @@ -1832,6 +1844,8 @@ impl Bank { ))), epoch_reward_status: fields.epoch_reward_status, transaction_processor: TransactionBatchProcessor::default(), + // collector_fee_details is not serialized to snapshot + collector_fee_details: Arc::new(CollectorFeeDetails::default()), }; bank.transaction_processor = TransactionBatchProcessor::new( @@ -3685,6 +3699,12 @@ impl Bank { self.update_slot_history(); self.run_incinerator(); + // to bench target function + { + let _transaction_fee = self.collector_fee_details.transaction_fee.load(Relaxed); + let _priority_fee = self.collector_fee_details.priority_fee.load(Relaxed); + } + // freeze is a one-way trip, idempotent self.freeze_started.store(true, Relaxed); *hash = self.hash_internal_state(); @@ -4871,6 +4891,64 @@ impl Bank { results } + // Note: this function is not yet used; next PR will call it behind a feature gate + #[allow(dead_code)] + fn filter_program_errors_and_collect_fee_details( + &self, + txs: &[SanitizedTransaction], + execution_results: &[TransactionExecutionResult], + ) -> Vec> { + let mut accumulated_fee_details = FeeDetails::default(); + + let results = txs + .iter() + .zip(execution_results) + .map(|(tx, execution_result)| { + let (execution_status, durable_nonce_fee) = match &execution_result { + TransactionExecutionResult::Executed { details, .. } => { + Ok((&details.status, details.durable_nonce_fee.as_ref())) + } + TransactionExecutionResult::NotExecuted(err) => Err(err.clone()), + }?; + let is_nonce = durable_nonce_fee.is_some(); + + let message = tx.message(); + let fee_details = self.fee_structure.calculate_fee_details( + message, + &process_compute_budget_instructions(message.program_instructions_iter()) + .unwrap_or_default() + .into(), + self.feature_set + .is_active(&include_loaded_accounts_data_size_in_fee_calculation::id()), + ); + + // In case of instruction error, even though no accounts + // were stored we still need to charge the payer the + // fee. + // + //...except nonce accounts, which already have their + // post-load, fee deducted, pre-execute account state + // stored + if execution_status.is_err() && !is_nonce { + self.withdraw( + tx.message().fee_payer(), + fee_details.total_fee( + self.feature_set + .is_active(&remove_rounding_in_fee_calculation::id()), + ), + )?; + } + + accumulated_fee_details.accumulate(&fee_details); + Ok(()) + }) + .collect(); + + self.collector_fee_details.transaction_fee.fetch_add(accumulated_fee_details.transaction_fee(), Relaxed); + self.collector_fee_details.priority_fee.fetch_add(accumulated_fee_details.prioritization_fee(), Relaxed); + results + } + /// `committed_transactions_count` is the number of transactions out of `sanitized_txs` /// that was executed. Of those, `committed_transactions_count`, /// `committed_with_failure_result_count` is the number of executed transactions that returned @@ -4984,6 +5062,8 @@ impl Bank { self.update_transaction_statuses(sanitized_txs, &execution_results); let fee_collection_results = self.filter_program_errors_and_collect_fee(sanitized_txs, &execution_results); + // to bench target function + let _ = self.filter_program_errors_and_collect_fee_details(sanitized_txs, &execution_results); update_transaction_statuses_time.stop(); timings.saturating_add_in_place( ExecuteTimingType::UpdateTransactionStatuses, diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 29dbdc2e5aeacd..56ebd51916ebf7 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -13794,3 +13794,73 @@ fn test_failed_simulation_compute_units() { let simulation = bank.simulate_transaction(&sanitized, false); assert_eq!(expected_consumed_units, simulation.units_consumed); } + +#[test] +fn test_filter_program_errors_and_collect_fee_details() { + // TX | EXECUTION RESULT | COLLECT | ADDITIONAL | COLLECT + // | (TX_FEE, PRIO_FEE) | WITHDRAW FROM PAYER | RESULT + // -------------------------------------------------------------------------------------------------- + // tx1 | executed and no error | (5_000, 1_000) | 0 | Ok + // tx2 | executed has error | (5_000, 1_000) | 6_000 | Ok + // tx3 | not executed | (0 , 0) | 0 | Original Err + // tx4 | executed error, payer insufficient fund | (0 , 0) | 0 | InsufficientFundsForFee + // + let initial_payer_balance = 7_000; + let additional_payer_withdraw = 6_000; + let expected_collected_fee_details = CollectorFeeDetails { + transaction_fee: 10_000, + priority_fee: 2_000, + }; + + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_leader(initial_payer_balance, &Pubkey::new_unique(), 3); + genesis_config.fee_rate_governor = FeeRateGovernor::new(5000, 0); + let bank = Bank::new_for_tests(&genesis_config); + + let tx = SanitizedTransaction::from_transaction_for_tests(Transaction::new_signed_with_payer( + &[ + system_instruction::transfer(&mint_keypair.pubkey(), &Pubkey::new_unique(), 2), + ComputeBudgetInstruction::set_compute_unit_limit(1_000), + ComputeBudgetInstruction::set_compute_unit_price(1_000_000), + ], + Some(&mint_keypair.pubkey()), + &[&mint_keypair], + genesis_config.hash(), + )); + let txs = vec![tx.clone(), tx.clone(), tx.clone(), tx]; + + let results = vec![ + new_execution_result(Ok(()), None), + new_execution_result( + Err(TransactionError::InstructionError( + 1, + SystemError::ResultWithNegativeLamports.into(), + )), + None, + ), + TransactionExecutionResult::NotExecuted(TransactionError::AccountNotFound), + new_execution_result(Err(TransactionError::AccountNotFound), None), + ]; + + let expected_collect_results = vec![ + Ok(()), + Ok(()), + Err(TransactionError::AccountNotFound), + Err(TransactionError::InsufficientFundsForFee), + ]; + + let results = bank.filter_program_errors_and_collect_fee_details(&txs, &results); + + assert_eq!( + expected_collected_fee_details, + *bank.collector_fee_details.read().unwrap() + ); + assert_eq!( + initial_payer_balance - additional_payer_withdraw, + bank.get_balance(&mint_keypair.pubkey()) + ); + assert_eq!(expected_collect_results, results); +} diff --git a/sdk/src/fee.rs b/sdk/src/fee.rs index b325a23ac08d9d..25c4b62acdf61b 100644 --- a/sdk/src/fee.rs +++ b/sdk/src/fee.rs @@ -47,6 +47,23 @@ impl FeeDetails { (total_fee as f64).round() as u64 } } + + pub fn accumulate(&mut self, fee_details: &FeeDetails) { + self.transaction_fee = self + .transaction_fee + .saturating_add(fee_details.transaction_fee); + self.prioritization_fee = self + .prioritization_fee + .saturating_add(fee_details.prioritization_fee) + } + + pub fn transaction_fee(&self) -> u64 { + self.transaction_fee + } + + pub fn prioritization_fee(&self) -> u64 { + self.prioritization_fee + } } pub const ACCOUNT_DATA_COST_PAGE_SIZE: u64 = 32_u64.saturating_mul(1024);