- update cost calculation to closely proposed fee schedule #16984
This commit is contained in:
parent
17314f4a95
commit
675fa6993b
|
@ -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<Pubkey>,
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<u64, CostModelError> {
|
||||
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())
|
||||
|
|
|
@ -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<u64> {
|
||||
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,
|
||||
|
|
|
@ -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<Pubkey, u64> = [
|
||||
(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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue