Skip to content

Commit

Permalink
- update const cost values with data collected by solana-labs#19627
Browse files Browse the repository at this point in the history
- update cost calculation to closely proposed fee schedule solana-labs#16984
  • Loading branch information
tao-stones committed Sep 29, 2021
1 parent 0d6f6e1 commit ddceaa2
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 108 deletions.
179 changes: 115 additions & 64 deletions core/src/cost_model.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,16 +12,25 @@ use std::collections::HashMap;

const MAX_WRITABLE_ACCOUNTS: usize = 256;

// 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(Debug, Clone)]
pub enum CostModelError {
/// transaction that would fail sanitize, cost model is not able to process
/// such transaction.
InvalidTransaction,

/// would exceed block max limit
WouldExceedBlockMaxLimit,

/// would exceed account max limit
WouldExceedAccountMaxLimit,
}

#[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,
}

Expand All @@ -38,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)]
Expand All @@ -55,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)
}
}

Expand All @@ -78,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()
Expand All @@ -107,26 +126,18 @@ impl CostModel {
) -> &TransactionCost {
self.transaction_cost.reset();

let message = transaction.message();
message.account_keys.iter().enumerate().for_each(|(i, k)| {
let is_writable = message.is_writable(i, demote_program_write_locks);
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);

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;
}
});
self.transaction_cost.execution_cost = self.find_transaction_cost(transaction);
debug!(
"transaction {:?} has cost {:?}",
transaction, self.transaction_cost
);
&self.transaction_cost
}

// To update or insert instruction cost to table.
pub fn upsert_instruction_cost(
&mut self,
program_key: &Pubkey,
Expand All @@ -144,21 +155,38 @@ impl CostModel {
self.instruction_execution_cost_table.get_cost_table()
}

fn find_instruction_cost(&self, program_key: &Pubkey) -> u64 {
match self.instruction_execution_cost_table.get_cost(&program_key) {
Some(cost) => *cost,
None => {
let default_value = self.instruction_execution_cost_table.get_mode();
debug!(
"Program key {:?} does not have assigned cost, using mode {}",
program_key, default_value
);
default_value
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 find_transaction_cost(&self, transaction: &Transaction) -> u64 {
fn get_transaction_cost(&self, transaction: &SanitizedTransaction) -> u64 {
let mut cost: u64 = 0;

for instruction in &transaction.message().instructions {
Expand All @@ -170,10 +198,24 @@ impl CostModel {
instruction,
instruction_cost
);
cost += 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,
None => {
let default_value = self.instruction_execution_cost_table.get_mode();
debug!(
"Program key {:?} does not have assigned cost, using mode {}",
program_key, default_value
);
default_value
}
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -254,7 +296,7 @@ mod tests {
.unwrap();
assert_eq!(
expected_cost,
testee.find_transaction_cost(&simple_transaction)
testee.get_transaction_cost(&simple_transaction)
);
}

Expand All @@ -278,7 +320,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]
Expand All @@ -304,7 +346,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;
Expand Down Expand Up @@ -366,15 +408,15 @@ mod tests {
let tx =
system_transaction::transfer(&mint_keypair, &Keypair::new().pubkey(), 2, start_hash);

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();
cost_model
.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());
}
Expand Down Expand Up @@ -418,8 +460,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)
Expand All @@ -443,7 +484,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);
})
}
})
Expand All @@ -455,7 +496,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),
Expand All @@ -471,5 +512,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());
}
}
15 changes: 6 additions & 9 deletions core/src/cost_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,7 @@ impl CostTracker {
) -> Result<(), &'static str> {
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),
)
self.would_fit(&tx_cost.writable_accounts, &tx_cost.sum())
}

pub fn add_transaction_cost(
Expand All @@ -63,7 +60,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
Expand All @@ -82,7 +79,7 @@ impl CostTracker {
}

pub fn try_add(&mut self, transaction_cost: &TransactionCost) -> Result<u64, &'static str> {
let cost = transaction_cost.account_access_cost + transaction_cost.execution_cost;
let cost = transaction_cost.sum();
self.would_fit(&transaction_cost.writable_accounts, &cost)?;

self.add_transaction(&transaction_cost.writable_accounts, &cost);
Expand Down Expand Up @@ -371,8 +368,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).is_ok());
let stat = testee.get_stats();
Expand All @@ -389,8 +386,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).is_ok());
let stat = testee.get_stats();
Expand All @@ -409,8 +406,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).is_err());
let stat = testee.get_stats();
Expand Down
4 changes: 3 additions & 1 deletion core/src/execute_cost_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
Loading

0 comments on commit ddceaa2

Please sign in to comment.