From 675fa6993b87e3e96d9a2119fc5bbff0d1ee7ea8 Mon Sep 17 00:00:00 2001 From: Tao Zhu <82401714+taozhu-chicago@users.noreply.github.com> Date: Fri, 8 Oct 2021 14:48:50 -0500 Subject: [PATCH] - update const cost values with data collected by #19627 (#20314) - update cost calculation to closely proposed fee schedule #16984 --- core/src/cost_model.rs | 173 +++++++++++++++++------------ core/src/cost_tracker.rs | 16 +-- core/src/execute_cost_table.rs | 4 +- ledger/src/block_cost_limits.rs | 80 ++++++++----- ledger/src/blockstore_processor.rs | 5 +- 5 files changed, 167 insertions(+), 111 deletions(-) diff --git a/core/src/cost_model.rs b/core/src/cost_model.rs index ade82ea8e..43c41cacc 100644 --- a/core/src/cost_model.rs +++ b/core/src/cost_model.rs @@ -1,10 +1,7 @@ //! 'cost_model` provides service to estimate a transaction's cost -//! It does so by analyzing accounts the transaction touches, and instructions -//! it includes. Using historical data as guideline, it estimates cost of -//! reading/writing account, the sum of that comes up to "account access cost"; -//! Instructions take time to execute, both historical and runtime data are -//! used to determine each instruction's execution time, the sum of that -//! is transaction's "execution cost" +//! following proposed fee schedule #16984; Relevant cluster cost +//! measuring is described by #19627 +//! //! The main function is `calculate_cost` which returns &TransactionCost. //! use crate::execute_cost_table::ExecuteCostTable; @@ -28,16 +25,12 @@ pub enum CostModelError { WouldExceedAccountMaxLimit, } -// cost of transaction is made of account_access_cost and instruction execution_cost -// where -// account_access_cost is the sum of read/write/sign all accounts included in the transaction -// read is cheaper than write. -// execution_cost is the sum of all instructions execution cost, which is -// observed during runtime and feedback by Replay #[derive(Default, Debug)] pub struct TransactionCost { pub writable_accounts: Vec, - pub account_access_cost: u64, + pub signature_cost: u64, + pub write_lock_cost: u64, + pub data_bytes_cost: u64, pub execution_cost: u64, } @@ -51,9 +44,15 @@ impl TransactionCost { pub fn reset(&mut self) { self.writable_accounts.clear(); - self.account_access_cost = 0; + self.signature_cost = 0; + self.write_lock_cost = 0; + self.data_bytes_cost = 0; self.execution_cost = 0; } + + pub fn sum(&self) -> u64 { + self.signature_cost + self.write_lock_cost + self.data_bytes_cost + self.execution_cost + } } #[derive(Debug)] @@ -68,7 +67,7 @@ pub struct CostModel { impl Default for CostModel { fn default() -> Self { - CostModel::new(ACCOUNT_COST_MAX, BLOCK_COST_MAX) + CostModel::new(MAX_WRITABLE_ACCOUNT_UNITS, MAX_BLOCK_UNITS) } } @@ -91,22 +90,29 @@ impl CostModel { } pub fn initialize_cost_table(&mut self, cost_table: &[(Pubkey, u64)]) { - for (program_id, cost) in cost_table { - match self.upsert_instruction_cost(program_id, *cost) { - Ok(c) => { - debug!( - "initiating cost table, instruction {:?} has cost {}", - program_id, c - ); + cost_table + .iter() + .map(|(key, cost)| (key, cost)) + .chain(BUILT_IN_INSTRUCTION_COSTS.iter()) + .for_each(|(program_id, cost)| { + match self + .instruction_execution_cost_table + .upsert(program_id, *cost) + { + Some(c) => { + debug!( + "initiating cost table, instruction {:?} has cost {}", + program_id, c + ); + } + None => { + debug!( + "initiating cost table, failed for instruction {:?}", + program_id + ); + } } - Err(err) => { - debug!( - "initiating cost table, failed for instruction {:?}, err: {}", - program_id, err - ); - } - } - } + }); debug!( "restored cost model instruction cost table from blockstore, current values: {:?}", self.get_instruction_cost_table() @@ -120,21 +126,11 @@ impl CostModel { ) -> &TransactionCost { self.transaction_cost.reset(); - // calculate transaction exeution cost - self.transaction_cost.execution_cost = self.find_transaction_cost(transaction); + self.transaction_cost.signature_cost = self.get_signature_cost(transaction); + self.get_write_lock_cost(transaction, demote_program_write_locks); + self.transaction_cost.data_bytes_cost = self.get_data_bytes_cost(transaction); + self.transaction_cost.execution_cost = self.get_transaction_cost(transaction); - // calculate account access cost - let message = transaction.message(); - message.account_keys_iter().enumerate().for_each(|(i, k)| { - let is_writable = message.is_writable(i, demote_program_write_locks); - - if is_writable { - self.transaction_cost.writable_accounts.push(*k); - self.transaction_cost.account_access_cost += ACCOUNT_WRITE_COST; - } else { - self.transaction_cost.account_access_cost += ACCOUNT_READ_COST; - } - }); debug!( "transaction {:?} has cost {:?}", transaction, self.transaction_cost @@ -142,7 +138,6 @@ impl CostModel { &self.transaction_cost } - // To update or insert instruction cost to table. pub fn upsert_instruction_cost( &mut self, program_key: &Pubkey, @@ -160,6 +155,52 @@ impl CostModel { self.instruction_execution_cost_table.get_cost_table() } + fn get_signature_cost(&self, transaction: &SanitizedTransaction) -> u64 { + transaction.signatures().len() as u64 * SIGNATURE_COST + } + + fn get_write_lock_cost( + &mut self, + transaction: &SanitizedTransaction, + demote_program_write_locks: bool, + ) { + let message = transaction.message(); + message.account_keys_iter().enumerate().for_each(|(i, k)| { + let is_writable = message.is_writable(i, demote_program_write_locks); + + if is_writable { + self.transaction_cost.writable_accounts.push(*k); + self.transaction_cost.write_lock_cost += WRITE_LOCK_UNITS; + } + }); + } + + fn get_data_bytes_cost(&self, transaction: &SanitizedTransaction) -> u64 { + let mut data_bytes_cost: u64 = 0; + transaction + .message() + .program_instructions_iter() + .for_each(|(_, ix)| { + data_bytes_cost += ix.data.len() as u64 / DATA_BYTES_UNITS; + }); + data_bytes_cost + } + + fn get_transaction_cost(&self, transaction: &SanitizedTransaction) -> u64 { + let mut cost: u64 = 0; + + for (program_id, instruction) in transaction.message().program_instructions_iter() { + let instruction_cost = self.find_instruction_cost(program_id); + trace!( + "instruction {:?} has cost of {}", + instruction, + instruction_cost + ); + cost = cost.saturating_add(instruction_cost); + } + cost + } + fn find_instruction_cost(&self, program_key: &Pubkey) -> u64 { match self.instruction_execution_cost_table.get_cost(program_key) { Some(cost) => *cost, @@ -173,21 +214,6 @@ impl CostModel { } } } - - fn find_transaction_cost(&self, transaction: &SanitizedTransaction) -> u64 { - let mut cost: u64 = 0; - - for (program_id, instruction) in transaction.message().program_instructions_iter() { - let instruction_cost = self.find_instruction_cost(program_id); - trace!( - "instruction {:?} has cost of {}", - instruction, - instruction_cost - ); - cost += instruction_cost; - } - cost - } } #[cfg(test)] @@ -272,7 +298,7 @@ mod tests { .unwrap(); assert_eq!( expected_cost, - testee.find_transaction_cost(&simple_transaction) + testee.get_transaction_cost(&simple_transaction) ); } @@ -298,7 +324,7 @@ mod tests { testee .upsert_instruction_cost(&system_program::id(), program_cost) .unwrap(); - assert_eq!(expected_cost, testee.find_transaction_cost(&tx)); + assert_eq!(expected_cost, testee.get_transaction_cost(&tx)); } #[test] @@ -326,7 +352,7 @@ mod tests { debug!("many random transaction {:?}", tx); let testee = CostModel::default(); - let result = testee.find_transaction_cost(&tx); + let result = testee.get_transaction_cost(&tx); // expected cost for two random/unknown program is let expected_cost = testee.instruction_execution_cost_table.get_mode() * 2; @@ -392,7 +418,7 @@ mod tests { .try_into() .unwrap(); - let expected_account_cost = ACCOUNT_WRITE_COST + ACCOUNT_WRITE_COST + ACCOUNT_READ_COST; + let expected_account_cost = WRITE_LOCK_UNITS * 2; let expected_execution_cost = 8; let mut cost_model = CostModel::default(); @@ -400,7 +426,7 @@ mod tests { .upsert_instruction_cost(&system_program::id(), expected_execution_cost) .unwrap(); let tx_cost = cost_model.calculate_cost(&tx, /*demote_program_write_locks=*/ true); - assert_eq!(expected_account_cost, tx_cost.account_access_cost); + assert_eq!(expected_account_cost, tx_cost.write_lock_cost); assert_eq!(expected_execution_cost, tx_cost.execution_cost); assert_eq!(2, tx_cost.writable_accounts.len()); } @@ -447,8 +473,7 @@ mod tests { ); let number_threads = 10; - let expected_account_cost = - ACCOUNT_WRITE_COST + ACCOUNT_WRITE_COST * 2 + ACCOUNT_READ_COST * 2; + let expected_account_cost = WRITE_LOCK_UNITS * 3; let cost1 = 100; let cost2 = 200; // execution cost can be either 2 * Default (before write) or cost1+cost2 (after write) @@ -472,7 +497,7 @@ mod tests { let tx_cost = cost_model .calculate_cost(&tx, /*demote_program_write_locks=*/ true); assert_eq!(3, tx_cost.writable_accounts.len()); - assert_eq!(expected_account_cost, tx_cost.account_access_cost); + assert_eq!(expected_account_cost, tx_cost.write_lock_cost); }) } }) @@ -484,7 +509,7 @@ mod tests { } #[test] - fn test_cost_model_init_cost_table() { + fn test_initialize_cost_table() { // build cost table let cost_table = vec![ (Pubkey::new_unique(), 10), @@ -500,5 +525,15 @@ mod tests { for (id, cost) in cost_table.iter() { assert_eq!(*cost, cost_model.find_instruction_cost(id)); } + + // verify built-in programs + assert!(cost_model + .instruction_execution_cost_table + .get_cost(&system_program::id()) + .is_some()); + assert!(cost_model + .instruction_execution_cost_table + .get_cost(&solana_vote_program::id()) + .is_some()); } } diff --git a/core/src/cost_tracker.rs b/core/src/cost_tracker.rs index f5051d5c5..645a81469 100644 --- a/core/src/cost_tracker.rs +++ b/core/src/cost_tracker.rs @@ -52,11 +52,7 @@ impl CostTracker { ) -> Result<(), CostModelError> { let mut cost_model = self.cost_model.write().unwrap(); let tx_cost = cost_model.calculate_cost(transaction, demote_program_write_locks); - self.would_fit( - &tx_cost.writable_accounts, - &(tx_cost.account_access_cost + tx_cost.execution_cost), - stats, - ) + self.would_fit(&tx_cost.writable_accounts, &tx_cost.sum(), stats) } pub fn add_transaction_cost( @@ -67,7 +63,7 @@ impl CostTracker { ) { let mut cost_model = self.cost_model.write().unwrap(); let tx_cost = cost_model.calculate_cost(transaction, demote_program_write_locks); - let cost = tx_cost.account_access_cost + tx_cost.execution_cost; + let cost = tx_cost.sum(); for account_key in tx_cost.writable_accounts.iter() { *self .cost_by_writable_accounts @@ -103,7 +99,7 @@ impl CostTracker { transaction_cost: &TransactionCost, stats: &mut CostTrackerStats, ) -> Result { - let cost = transaction_cost.account_access_cost + transaction_cost.execution_cost; + let cost = transaction_cost.sum(); self.would_fit(&transaction_cost.writable_accounts, &cost, stats)?; self.add_transaction(&transaction_cost.writable_accounts, &cost); @@ -428,8 +424,8 @@ mod tests { { let tx_cost = TransactionCost { writable_accounts: vec![acct1, acct2, acct3], - account_access_cost: 0, execution_cost: cost, + ..TransactionCost::default() }; assert!(testee .try_add(&tx_cost, &mut CostTrackerStats::default()) @@ -448,8 +444,8 @@ mod tests { { let tx_cost = TransactionCost { writable_accounts: vec![acct2], - account_access_cost: 0, execution_cost: cost, + ..TransactionCost::default() }; assert!(testee .try_add(&tx_cost, &mut CostTrackerStats::default()) @@ -470,8 +466,8 @@ mod tests { { let tx_cost = TransactionCost { writable_accounts: vec![acct1, acct2], - account_access_cost: 0, execution_cost: cost, + ..TransactionCost::default() }; assert!(testee .try_add(&tx_cost, &mut CostTrackerStats::default()) diff --git a/core/src/execute_cost_table.rs b/core/src/execute_cost_table.rs index fd0c67a9a..cef757a02 100644 --- a/core/src/execute_cost_table.rs +++ b/core/src/execute_cost_table.rs @@ -78,7 +78,7 @@ impl ExecuteCostTable { self.table.get(key) } - pub fn upsert(&mut self, key: &Pubkey, value: u64) { + pub fn upsert(&mut self, key: &Pubkey, value: u64) -> Option { let need_to_add = self.table.get(key).is_none(); let current_size = self.get_count(); if current_size == self.capacity && need_to_add { @@ -94,6 +94,8 @@ impl ExecuteCostTable { .or_insert((0, SystemTime::now())); *count += 1; *timestamp = SystemTime::now(); + + Some(*program_cost) } // prune the old programs so the table contains `new_size` of records, diff --git a/ledger/src/block_cost_limits.rs b/ledger/src/block_cost_limits.rs index 89ed44593..0f263300d 100644 --- a/ledger/src/block_cost_limits.rs +++ b/ledger/src/block_cost_limits.rs @@ -1,34 +1,56 @@ //! defines block cost related limits //! +use lazy_static::lazy_static; +use solana_sdk::{ + feature, incinerator, native_loader, pubkey::Pubkey, secp256k1_program, system_program, +}; +use std::collections::HashMap; -// see https://github.com/solana-labs/solana/issues/18944 -// and https://github.com/solana-labs/solana/pull/18994#issuecomment-896128992 -// -pub const MAX_BLOCK_TIME_US: u64 = 400_000; // aiming at 400ms/block max time -pub const AVG_INSTRUCTION_TIME_US: u64 = 1_000; // average instruction execution time -pub const SYSTEM_PARALLELISM: u64 = 10; -pub const MAX_INSTRUCTION_COST: u64 = 200_000; -pub const MAX_NUMBER_BPF_INSTRUCTIONS_PER_ACCOUNT: u64 = 200; +/// Static configurations: +/// +/// Number of microseconds replaying a block should take, 400 millisecond block times +/// is curerntly publicly communicated on solana.com +pub const MAX_BLOCK_REPLAY_TIME_US: u64 = 400_000; +/// number of concurrent processes, +pub const MAX_CONCURRENCY: u64 = 10; -// 4_000 -pub const MAX_INSTRUCTIONS_PER_BLOCK: u64 = - (MAX_BLOCK_TIME_US / AVG_INSTRUCTION_TIME_US) * SYSTEM_PARALLELISM; +/// Cluster data, method of collecting at https://github.com/solana-labs/solana/issues/19627 +/// +/// cluster avergaed compute unit to microsec conversion rate +pub const COMPUTE_UNIT_TO_US_RATIO: u64 = 40; +/// Number of compute units for one signature verification. +pub const SIGNATURE_COST: u64 = COMPUTE_UNIT_TO_US_RATIO * 175; +/// Number of compute units for one write lock +pub const WRITE_LOCK_UNITS: u64 = COMPUTE_UNIT_TO_US_RATIO * 20; +/// Number of data bytes per compute units +pub const DATA_BYTES_UNITS: u64 = 220 /*bytes per us*/ / COMPUTE_UNIT_TO_US_RATIO; +// Number of compute units for each built-in programs +lazy_static! { + /// Number of compute units for each built-in programs + pub static ref BUILT_IN_INSTRUCTION_COSTS: HashMap = [ + (feature::id(), COMPUTE_UNIT_TO_US_RATIO * 2), + (incinerator::id(), COMPUTE_UNIT_TO_US_RATIO * 2), + (native_loader::id(), COMPUTE_UNIT_TO_US_RATIO * 2), + (solana_sdk::stake::config::id(), COMPUTE_UNIT_TO_US_RATIO * 2), + (solana_sdk::stake::program::id(), COMPUTE_UNIT_TO_US_RATIO * 50), + (solana_vote_program::id(), COMPUTE_UNIT_TO_US_RATIO * 200), + (secp256k1_program::id(), COMPUTE_UNIT_TO_US_RATIO * 4), + (system_program::id(), COMPUTE_UNIT_TO_US_RATIO * 15), + ] + .iter() + .cloned() + .collect(); +} -// 8_000_000_000 -pub const BLOCK_COST_MAX: u64 = MAX_INSTRUCTION_COST * MAX_INSTRUCTIONS_PER_BLOCK * 10; - -// 800_000_000 -pub const ACCOUNT_COST_MAX: u64 = MAX_INSTRUCTION_COST * MAX_INSTRUCTIONS_PER_BLOCK; - -// 2_000 -pub const COMPUTE_UNIT_TO_US_RATIO: u64 = - (MAX_INSTRUCTION_COST / AVG_INSTRUCTION_TIME_US) * SYSTEM_PARALLELISM; - -// signature takes average 10us, or 20K CU -pub const SIGNATURE_COST: u64 = COMPUTE_UNIT_TO_US_RATIO * 10; - -// read account averages 5us, or 10K CU -pub const ACCOUNT_READ_COST: u64 = COMPUTE_UNIT_TO_US_RATIO * 5; - -// write account averages 25us, or 50K CU -pub const ACCOUNT_WRITE_COST: u64 = COMPUTE_UNIT_TO_US_RATIO * 25; +/// Statically computed data: +/// +/// Number of compute units that a block is allowed. A block's compute units are +/// accumualted by Transactions added to it; A transaction's compute units are +/// calculated by cost_model, based on transaction's signarures, write locks, +/// data size and built-in and BPF instructinos. +pub const MAX_BLOCK_UNITS: u64 = + MAX_BLOCK_REPLAY_TIME_US * COMPUTE_UNIT_TO_US_RATIO * MAX_CONCURRENCY; +/// Number of compute units that a writable account in a block is allowed. The +/// limit is to prevent too many transactions write to same account, threrefore +/// reduce block's paralellism. +pub const MAX_WRITABLE_ACCOUNT_UNITS: u64 = MAX_BLOCK_REPLAY_TIME_US * COMPUTE_UNIT_TO_US_RATIO; diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 9ddf52211..cf155331c 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -66,7 +66,7 @@ pub struct BlockCostCapacityMeter { impl Default for BlockCostCapacityMeter { fn default() -> Self { - BlockCostCapacityMeter::new(BLOCK_COST_MAX) + BlockCostCapacityMeter::new(MAX_BLOCK_UNITS) } } @@ -143,7 +143,8 @@ fn aggregate_total_execution_units(execute_timings: &ExecuteTimings) -> u64 { if timing.count < 1 { continue; } - execute_cost_units += timing.accumulated_units / timing.count as u64; + execute_cost_units = + execute_cost_units.saturating_add(timing.accumulated_units / timing.count as u64); trace!("aggregated execution cost of {:?} {:?}", program_id, timing); } execute_cost_units