From 37a759045adfe852d9ee36a43b7b741de86b4d6e Mon Sep 17 00:00:00 2001 From: Tao Zhu <82401714+taozhu-chicago@users.noreply.github.com> Date: Thu, 8 Jun 2023 12:49:42 -0500 Subject: [PATCH] include cost of transaction's requested loaded accounts size in cost model (#31905) * include transaction requested loaded accounts size cost in cost model * move function to avoid circular dependency --- runtime/src/bank.rs | 20 +---- runtime/src/bank/tests.rs | 8 +- runtime/src/cost_model.rs | 151 ++++++++++++++++++++++++++++++-- runtime/src/transaction_cost.rs | 4 + 4 files changed, 155 insertions(+), 28 deletions(-) diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 8e840449ae..4cf5771259 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -57,6 +57,7 @@ use { bank::metrics::*, blockhash_queue::BlockhashQueue, builtins::{BuiltinPrototype, BUILTINS}, + cost_model::CostModel, cost_tracker::CostTracker, epoch_accounts_hash::{self, EpochAccountsHash}, epoch_stakes::{EpochStakes, NodeVoteAccounts}, @@ -4915,7 +4916,7 @@ impl Bank { // `compute_fee` covers costs for both requested_compute_units and // requested_loaded_account_data_size let loaded_accounts_data_size_cost = if include_loaded_account_data_size_in_fee { - Self::calculate_loaded_accounts_data_size_cost(&compute_budget) + CostModel::calculate_loaded_accounts_data_size_cost(&compute_budget) } else { 0_u64 }; @@ -4942,23 +4943,6 @@ impl Bank { .round() as u64 } - // Calculate cost of loaded accounts size in the same way heap cost is charged at - // rate of 8cu per 32K. Citing `program_runtime\src\compute_budget.rs`: "(cost of - // heap is about) 0.5us per 32k at 15 units/us rounded up" - // - // Before feature `support_set_loaded_accounts_data_size_limit_ix` is enabled, or - // if user doesn't use compute budget ix `set_loaded_accounts_data_size_limit_ix` - // to set limit, `compute_budget.loaded_accounts_data_size_limit` is set to default - // limit of 64MB; which will convert to (64M/32K)*8CU = 16_000 CUs - // - fn calculate_loaded_accounts_data_size_cost(compute_budget: &ComputeBudget) -> u64 { - const ACCOUNT_DATA_COST_PAGE_SIZE: u64 = 32_u64.saturating_mul(1024); - (compute_budget.loaded_accounts_data_size_limit as u64) - .saturating_add(ACCOUNT_DATA_COST_PAGE_SIZE.saturating_sub(1)) - .saturating_div(ACCOUNT_DATA_COST_PAGE_SIZE) - .saturating_mul(compute_budget.heap_cost) - } - fn filter_program_errors_and_collect_fee( &self, txs: &[SanitizedTransaction], diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 6e76b68571..217d08f838 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -12435,28 +12435,28 @@ fn test_calculate_loaded_accounts_data_size_cost() { compute_budget.loaded_accounts_data_size_limit = 31_usize * 1024; assert_eq!( compute_budget.heap_cost, - Bank::calculate_loaded_accounts_data_size_cost(&compute_budget) + CostModel::calculate_loaded_accounts_data_size_cost(&compute_budget) ); // ... requesting exact 32K should be charged as one block compute_budget.loaded_accounts_data_size_limit = 32_usize * 1024; assert_eq!( compute_budget.heap_cost, - Bank::calculate_loaded_accounts_data_size_cost(&compute_budget) + CostModel::calculate_loaded_accounts_data_size_cost(&compute_budget) ); // ... requesting slightly above 32K should be charged as 2 block compute_budget.loaded_accounts_data_size_limit = 33_usize * 1024; assert_eq!( compute_budget.heap_cost * 2, - Bank::calculate_loaded_accounts_data_size_cost(&compute_budget) + CostModel::calculate_loaded_accounts_data_size_cost(&compute_budget) ); // ... requesting exact 64K should be charged as 2 block compute_budget.loaded_accounts_data_size_limit = 64_usize * 1024; assert_eq!( compute_budget.heap_cost * 2, - Bank::calculate_loaded_accounts_data_size_cost(&compute_budget) + CostModel::calculate_loaded_accounts_data_size_cost(&compute_budget) ); } diff --git a/runtime/src/cost_model.rs b/runtime/src/cost_model.rs index 3e0bf0b251..36f9c52f73 100644 --- a/runtime/src/cost_model.rs +++ b/runtime/src/cost_model.rs @@ -13,8 +13,9 @@ use { }, solana_sdk::{ feature_set::{ - add_set_tx_loaded_accounts_data_size_instruction, remove_deprecated_request_unit_ix, - use_default_units_in_fee_calculation, FeatureSet, + add_set_tx_loaded_accounts_data_size_instruction, + include_loaded_accounts_data_size_in_fee_calculation, + remove_deprecated_request_unit_ix, use_default_units_in_fee_calculation, FeatureSet, }, instruction::CompiledInstruction, program_utils::limited_deserialize, @@ -25,6 +26,8 @@ use { }, }; +const ACCOUNT_DATA_COST_PAGE_SIZE: u64 = 32_u64.saturating_mul(1024); + pub struct CostModel; impl CostModel { @@ -44,6 +47,22 @@ impl CostModel { tx_cost } + // Calculate cost of loaded accounts size in the same way heap cost is charged at + // rate of 8cu per 32K. Citing `program_runtime\src\compute_budget.rs`: "(cost of + // heap is about) 0.5us per 32k at 15 units/us rounded up" + // + // Before feature `support_set_loaded_accounts_data_size_limit_ix` is enabled, or + // if user doesn't use compute budget ix `set_loaded_accounts_data_size_limit_ix` + // to set limit, `compute_budget.loaded_accounts_data_size_limit` is set to default + // limit of 64MB; which will convert to (64M/32K)*8CU = 16_000 CUs + // + pub fn calculate_loaded_accounts_data_size_cost(compute_budget: &ComputeBudget) -> u64 { + (compute_budget.loaded_accounts_data_size_limit as u64) + .saturating_add(ACCOUNT_DATA_COST_PAGE_SIZE.saturating_sub(1)) + .saturating_div(ACCOUNT_DATA_COST_PAGE_SIZE) + .saturating_mul(compute_budget.heap_cost) + } + fn get_signature_cost(transaction: &SanitizedTransaction) -> u64 { transaction.signatures().len() as u64 * SIGNATURE_COST } @@ -71,6 +90,7 @@ impl CostModel { ) { let mut builtin_costs = 0u64; let mut bpf_costs = 0u64; + let mut loaded_accounts_data_size_cost = 0u64; let mut data_bytes_len_total = 0u64; for (program_id, instruction) in transaction.message().program_instructions_iter() { @@ -85,7 +105,7 @@ impl CostModel { } // calculate bpf cost based on compute budget instructions - let mut budget = ComputeBudget::default(); + let mut compute_budget = ComputeBudget::default(); // Starting from v1.15, cost model uses compute_budget.set_compute_unit_limit to // measure bpf_costs (code below), vs earlier versions that use estimated @@ -94,7 +114,7 @@ impl CostModel { // will not impact consensus. So for v1.15+, should call compute budget with // the feature gate `enable_request_heap_frame_ix` enabled. let enable_request_heap_frame_ix = true; - let result = budget.process_instructions( + let result = compute_budget.process_instructions( transaction.message().program_instructions_iter(), feature_set.is_active(&use_default_units_in_fee_calculation::id()), !feature_set.is_active(&remove_deprecated_request_unit_ix::id()), @@ -108,7 +128,13 @@ impl CostModel { Ok(_) => { // if tx contained user-space instructions and a more accurate estimate available correct it if bpf_costs > 0 { - bpf_costs = budget.compute_unit_limit + bpf_costs = compute_budget.compute_unit_limit + } + if feature_set + .is_active(&include_loaded_accounts_data_size_in_fee_calculation::id()) + { + loaded_accounts_data_size_cost = + Self::calculate_loaded_accounts_data_size_cost(&compute_budget); } } Err(_) => { @@ -119,6 +145,7 @@ impl CostModel { tx_cost.builtins_execution_cost = builtin_costs; tx_cost.bpf_execution_cost = bpf_costs; + tx_cost.loaded_accounts_data_size_cost = loaded_accounts_data_size_cost; tx_cost.data_bytes_cost = data_bytes_len_total / INSTRUCTION_DATA_BYTES_COST; } @@ -494,7 +521,7 @@ mod tests { } #[test] - fn test_cost_model_calculate_cost() { + fn test_cost_model_calculate_cost_all_default() { let (mint_keypair, start_hash) = test_setup(); let tx = SanitizedTransaction::from_transaction_for_tests(system_transaction::transfer( &mint_keypair, @@ -507,10 +534,122 @@ mod tests { let expected_execution_cost = BUILT_IN_INSTRUCTION_COSTS .get(&system_program::id()) .unwrap(); + // feature `include_loaded_accounts_data_size_in_fee_calculation` enabled, using + // default loaded_accounts_data_size_limit + const DEFAULT_PAGE_COST: u64 = 8; + let expected_loaded_accounts_data_size_cost = + solana_program_runtime::compute_budget::MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES as u64 + / ACCOUNT_DATA_COST_PAGE_SIZE + * DEFAULT_PAGE_COST; let tx_cost = CostModel::calculate_cost(&tx, &FeatureSet::all_enabled()); assert_eq!(expected_account_cost, tx_cost.write_lock_cost); assert_eq!(*expected_execution_cost, tx_cost.builtins_execution_cost); assert_eq!(2, tx_cost.writable_accounts.len()); + assert_eq!( + expected_loaded_accounts_data_size_cost, + tx_cost.loaded_accounts_data_size_cost + ); + } + + #[test] + fn test_cost_model_calculate_cost_disabled_feature() { + let (mint_keypair, start_hash) = test_setup(); + let tx = SanitizedTransaction::from_transaction_for_tests(system_transaction::transfer( + &mint_keypair, + &Keypair::new().pubkey(), + 2, + start_hash, + )); + + let feature_set = FeatureSet::default(); + assert!(!feature_set.is_active(&include_loaded_accounts_data_size_in_fee_calculation::id())); + let expected_account_cost = WRITE_LOCK_UNITS * 2; + let expected_execution_cost = BUILT_IN_INSTRUCTION_COSTS + .get(&system_program::id()) + .unwrap(); + // feature `include_loaded_accounts_data_size_in_fee_calculation` not enabled + let expected_loaded_accounts_data_size_cost = 0; + + let tx_cost = CostModel::calculate_cost(&tx, &feature_set); + assert_eq!(expected_account_cost, tx_cost.write_lock_cost); + assert_eq!(*expected_execution_cost, tx_cost.builtins_execution_cost); + assert_eq!(2, tx_cost.writable_accounts.len()); + assert_eq!( + expected_loaded_accounts_data_size_cost, + tx_cost.loaded_accounts_data_size_cost + ); + } + + #[test] + fn test_cost_model_calculate_cost_enabled_feature_with_limit() { + let (mint_keypair, start_hash) = test_setup(); + let to_keypair = Keypair::new(); + let data_limit = 32 * 1024u32; + let tx = + SanitizedTransaction::from_transaction_for_tests(Transaction::new_signed_with_payer( + &[ + system_instruction::transfer(&mint_keypair.pubkey(), &to_keypair.pubkey(), 2), + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_limit), + ], + Some(&mint_keypair.pubkey()), + &[&mint_keypair], + start_hash, + )); + + let feature_set = FeatureSet::all_enabled(); + assert!(feature_set.is_active(&include_loaded_accounts_data_size_in_fee_calculation::id())); + let expected_account_cost = WRITE_LOCK_UNITS * 2; + let expected_execution_cost = BUILT_IN_INSTRUCTION_COSTS + .get(&system_program::id()) + .unwrap() + + BUILT_IN_INSTRUCTION_COSTS + .get(&compute_budget::id()) + .unwrap(); + // feature `include_loaded_accounts_data_size_in_fee_calculation` is enabled, accounts data + // size limit is set. + let expected_loaded_accounts_data_size_cost = (data_limit as u64) / (32 * 1024) * 8; + + let tx_cost = CostModel::calculate_cost(&tx, &feature_set); + assert_eq!(expected_account_cost, tx_cost.write_lock_cost); + assert_eq!(expected_execution_cost, tx_cost.builtins_execution_cost); + assert_eq!(2, tx_cost.writable_accounts.len()); + assert_eq!( + expected_loaded_accounts_data_size_cost, + tx_cost.loaded_accounts_data_size_cost + ); + } + + #[test] + fn test_cost_model_calculate_cost_disabled_feature_with_limit() { + let (mint_keypair, start_hash) = test_setup(); + let to_keypair = Keypair::new(); + let data_limit = 32 * 1024u32; + let tx = + SanitizedTransaction::from_transaction_for_tests(Transaction::new_signed_with_payer( + &[ + system_instruction::transfer(&mint_keypair.pubkey(), &to_keypair.pubkey(), 2), + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_limit), + ], + Some(&mint_keypair.pubkey()), + &[&mint_keypair], + start_hash, + )); + + let feature_set = FeatureSet::default(); + assert!(!feature_set.is_active(&include_loaded_accounts_data_size_in_fee_calculation::id())); + let expected_account_cost = WRITE_LOCK_UNITS * 2; + // with features all disabled, builtins and loaded account size don't cost CU + let expected_execution_cost = 0; + let expected_loaded_accounts_data_size_cost = 0; + + let tx_cost = CostModel::calculate_cost(&tx, &feature_set); + assert_eq!(expected_account_cost, tx_cost.write_lock_cost); + assert_eq!(expected_execution_cost, tx_cost.builtins_execution_cost); + assert_eq!(2, tx_cost.writable_accounts.len()); + assert_eq!( + expected_loaded_accounts_data_size_cost, + tx_cost.loaded_accounts_data_size_cost + ); } } diff --git a/runtime/src/transaction_cost.rs b/runtime/src/transaction_cost.rs index 44efb033f0..e44014b3c9 100644 --- a/runtime/src/transaction_cost.rs +++ b/runtime/src/transaction_cost.rs @@ -11,6 +11,7 @@ pub struct TransactionCost { pub data_bytes_cost: u64, pub builtins_execution_cost: u64, pub bpf_execution_cost: u64, + pub loaded_accounts_data_size_cost: u64, pub account_data_size: u64, pub is_simple_vote: bool, } @@ -24,6 +25,7 @@ impl Default for TransactionCost { data_bytes_cost: 0u64, builtins_execution_cost: 0u64, bpf_execution_cost: 0u64, + loaded_accounts_data_size_cost: 0u64, account_data_size: 0u64, is_simple_vote: false, } @@ -42,6 +44,7 @@ impl PartialEq for TransactionCost { && self.data_bytes_cost == other.data_bytes_cost && self.builtins_execution_cost == other.builtins_execution_cost && self.bpf_execution_cost == other.bpf_execution_cost + && self.loaded_accounts_data_size_cost == other.loaded_accounts_data_size_cost && self.account_data_size == other.account_data_size && self.is_simple_vote == other.is_simple_vote && to_hash_set(&self.writable_accounts) == to_hash_set(&other.writable_accounts) @@ -69,5 +72,6 @@ impl TransactionCost { .saturating_add(self.data_bytes_cost) .saturating_add(self.builtins_execution_cost) .saturating_add(self.bpf_execution_cost) + .saturating_add(self.loaded_accounts_data_size_cost) } }