From b724a11b65bf6f883c6fb72f9d058649a48b0eab Mon Sep 17 00:00:00 2001 From: Tao Zhu Date: Thu, 29 Feb 2024 00:58:19 +0000 Subject: [PATCH 1/2] Add functions to collect executed transactions fee in details; --- runtime/src/bank.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++- sdk/src/fee.rs | 17 +++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index ccd3f7c522737f..4821232107acc5 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -121,7 +121,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, @@ -252,6 +252,24 @@ impl AddAssign for SquashTiming { } } +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CollectorFeeDetails { + pub transaction_fee: u64, + pub priority_fee: u64, +} + +impl CollectorFeeDetails { + pub(crate) fn add(&mut self, fee_details: &FeeDetails) { + self.transaction_fee = self + .transaction_fee + .saturating_add(fee_details.transaction_fee()); + self.priority_fee = self + .priority_fee + .saturating_add(fee_details.prioritization_fee()); + } +} + #[derive(Debug)] pub struct BankRc { /// where all the Accounts are stored @@ -546,6 +564,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. @@ -579,6 +598,8 @@ impl PartialEq for Bank { && *stakes_cache.stakes() == *other.stakes_cache.stakes() && epoch_stakes == &other.epoch_stakes && is_delta.load(Relaxed) == other.is_delta.load(Relaxed) + && *collector_fee_details.read().unwrap() + == *other.collector_fee_details.read().unwrap() } } @@ -806,6 +827,9 @@ pub struct Bank { epoch_reward_status: EpochRewardStatus, transaction_processor: TransactionBatchProcessor, + + /// Collected fee details + collector_fee_details: RwLock, } struct VoteWithStakeDelegations { @@ -992,6 +1016,7 @@ impl Bank { ))), epoch_reward_status: EpochRewardStatus::default(), transaction_processor: TransactionBatchProcessor::default(), + collector_fee_details: RwLock::new(CollectorFeeDetails::default()), }; bank.transaction_processor = TransactionBatchProcessor::new( @@ -1310,6 +1335,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: RwLock::new(CollectorFeeDetails::default()), }; new.transaction_processor = TransactionBatchProcessor::new( @@ -1827,6 +1853,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: RwLock::new(CollectorFeeDetails::default()), }; bank.transaction_processor = TransactionBatchProcessor::new( @@ -4864,6 +4892,66 @@ 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 + .write() + .unwrap() + .add(&accumulated_fee_details); + 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 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); From 958c9b52b271c0e4441f417fc47d9aadc824fe1e Mon Sep 17 00:00:00 2001 From: Tao Zhu Date: Fri, 1 Mar 2024 15:50:37 +0000 Subject: [PATCH 2/2] Add function to distribute fees that rewards full prio fee --- runtime/src/bank/fee_distribution.rs | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/runtime/src/bank/fee_distribution.rs b/runtime/src/bank/fee_distribution.rs index 5a53a1278881fa..faf937abea8d98 100644 --- a/runtime/src/bank/fee_distribution.rs +++ b/runtime/src/bank/fee_distribution.rs @@ -1,5 +1,6 @@ use { super::Bank, + crate::bank::CollectorFeeDetails, log::{debug, warn}, solana_sdk::{ account::{ReadableAccount, WritableAccount}, @@ -89,6 +90,70 @@ impl Bank { } } + // NOTE: to replace `distribute_transaction_fees()`, it applies different burn/reward rate + // on different fees: + // transaction fee: same fee_rate_governor rule + // priority fee: 100% reward + // next PR will call it behind a feature gate + #[allow(dead_code)] + pub(super) fn distribute_transaction_fee_details(&self) { + let CollectorFeeDetails { + transaction_fee, + priority_fee, + } = *self.collector_fee_details.read().unwrap(); + + if transaction_fee.saturating_add(priority_fee) == 0 { + // nothing to distribute, exit early + return; + } + + // apply distribution rules to fee details + let (mut deposit, mut burn) = if transaction_fee != 0 { + self.fee_rate_governor.burn(transaction_fee) + } else { + (0, 0) + }; + deposit = deposit.saturating_add(priority_fee); + + if deposit > 0 { + let validate_fee_collector = self.validate_fee_collector_account(); + match self.deposit_fees( + &self.collector_id, + deposit, + DepositFeeOptions { + check_account_owner: validate_fee_collector, + check_rent_paying: validate_fee_collector, + }, + ) { + Ok(post_balance) => { + self.rewards.write().unwrap().push(( + self.collector_id, + RewardInfo { + reward_type: RewardType::Fee, + lamports: deposit as i64, + post_balance, + commission: None, + }, + )); + } + Err(err) => { + debug!( + "Burned {} lamport tx fee instead of sending to {} due to {}", + deposit, self.collector_id, err + ); + datapoint_warn!( + "bank-burned_fee", + ("slot", self.slot(), i64), + ("num_lamports", deposit, i64), + ("error", err.to_string(), String), + ); + burn = burn.saturating_add(deposit); + } + } + } + self.capitalization.fetch_sub(burn, Relaxed); + } + // Deposits fees into a specified account and if successful, returns the new balance of that account fn deposit_fees( &self,