Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Implement fee schedule in cost calculation #20314

Merged
merged 1 commit into from
Oct 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 102 additions & 67 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 @@ -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,
}

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

Expand All @@ -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
);
}
Err(err) => {
debug!(
"initiating cost table, failed for instruction {:?}, err: {}",
program_id, err
);
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
);
}
}
}
}
});
debug!(
"restored cost model instruction cost table from blockstore, current values: {:?}",
self.get_instruction_cost_table()
Expand All @@ -120,29 +126,18 @@ impl CostModel {
) -> &TransactionCost {
self.transaction_cost.reset();

// calculate transaction exeution cost
self.transaction_cost.execution_cost = self.find_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);
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;
}
});
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 @@ -160,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 find_transaction_cost(&self, transaction: &SanitizedTransaction) -> u64 {
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() {
Expand All @@ -184,10 +196,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 @@ -272,7 +298,7 @@ mod tests {
.unwrap();
assert_eq!(
expected_cost,
testee.find_transaction_cost(&simple_transaction)
testee.get_transaction_cost(&simple_transaction)
);
}

Expand All @@ -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]
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -392,15 +418,15 @@ 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();
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 @@ -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)
Expand All @@ -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);
})
}
})
Expand All @@ -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),
Expand All @@ -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());
}
}
16 changes: 6 additions & 10 deletions core/src/cost_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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())
Expand All @@ -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())
Expand All @@ -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())
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