From 63a23391a3d7cdd4132b687f5e1893d8532be342 Mon Sep 17 00:00:00 2001 From: Tao Zhu Date: Sat, 8 May 2021 00:21:38 -0500 Subject: [PATCH] moving cost model to bank --- core/src/replay_stage.rs | 9 + runtime/src/bank.rs | 88 +++- runtime/src/cost_model.rs | 458 ++++++++++++++++++ runtime/src/cost_tracker.rs | 247 ++++++++++ runtime/src/lib.rs | 2 + sdk/src/transaction.rs | 4 + .../solana.storage.transaction_by_addr.rs | 1 + storage-proto/src/convert.rs | 4 + 8 files changed, 810 insertions(+), 3 deletions(-) create mode 100644 runtime/src/cost_model.rs create mode 100644 runtime/src/cost_tracker.rs diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 4cc06f7939ac11..27f66822289bb9 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -1177,6 +1177,15 @@ impl ReplayStage { ); let tpu_bank = bank_forks.write().unwrap().insert(tpu_bank); + + if let Some(bank) = poh_recorder.lock().unwrap().bank() { + debug!( + "final cost model for bank {:?} is {:?}", + bank, + bank.cost_model_stats() + ); + } + poh_recorder.lock().unwrap().set_bank(&tpu_bank); } else { error!("{} No next leader found", my_pubkey); diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 298d5846f19ed0..d830f0cf2340f4 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -42,6 +42,7 @@ use crate::{ accounts_index::{AccountIndex, Ancestors, IndexKey}, blockhash_queue::BlockhashQueue, builtins::{self, ActivationType}, + cost_model::{CostModel, CostModelStats}, epoch_stakes::{EpochStakes, NodeVoteAccounts}, hashed_transaction::{HashedTransaction, HashedTransactionSlice}, inline_spl_token_v2_0, @@ -120,7 +121,7 @@ use std::{ rc::Rc, sync::{ atomic::{AtomicBool, AtomicU64, Ordering::Relaxed}, - LockResult, RwLockWriteGuard, {Arc, RwLock, RwLockReadGuard}, + LockResult, RwLockWriteGuard, {Arc, Mutex, RwLock, RwLockReadGuard}, }, time::Duration, time::Instant, @@ -916,6 +917,9 @@ pub struct Bank { pub drop_callback: RwLock, pub freeze_started: AtomicBool, + + /// cost model to be private member of bank + cost_model: Arc>, } impl Default for BlockhashQueue { @@ -1121,6 +1125,7 @@ impl Bank { .map(|drop_callback| drop_callback.clone_box()), )), freeze_started: AtomicBool::new(false), + cost_model: Arc::new(Mutex::new(CostModel::new())), }; datapoint_info!( @@ -1268,6 +1273,7 @@ impl Bank { feature_set: new(), drop_callback: RwLock::new(OptionalDropCallback(None)), freeze_started: AtomicBool::new(fields.hash != Hash::default()), + cost_model: Arc::new(Mutex::new(CostModel::new())), }; bank.finish_init(genesis_config, additional_builtins); @@ -2704,6 +2710,7 @@ impl Bank { &mut error_counters, ); let cache_results = self.check_status_cache(hashed_txs, age_results, &mut error_counters); + if self.upgrade_epoch() { // Reject all non-vote transactions self.filter_by_vote_transactions( @@ -2728,6 +2735,37 @@ impl Bank { balances } + pub fn cost_model_stats(&self) -> CostModelStats { + self.cost_model.lock().unwrap().get_stats() + } + + fn filter_transactions_by_cost<'a>( + &self, + hashed_txs: impl Iterator, + check_results: Vec, + ) -> (Vec, Vec) { + let mut retryable_txs: Vec<_> = vec![]; + let results = hashed_txs + .zip(check_results) + .enumerate() + .map(|(index, (tx, check_result))| { + if check_result.0.is_ok() { + match self.cost_model.lock().unwrap().try_to_add_transaction(tx) { + Some(_) => { + return check_result; + } + None => { + retryable_txs.push(index); + return (Err(TransactionError::CostExceedsLimit), check_result.1); + } + } + } + check_result + }) + .collect(); + (retryable_txs, results) + } + #[allow(clippy::cognitive_complexity)] fn update_error_counters(error_counters: &ErrorCounters) { if 0 != error_counters.total { @@ -2950,7 +2988,7 @@ impl Bank { inc_new_counter_info!("bank-process_transactions", hashed_txs.len()); let mut error_counters = ErrorCounters::default(); - let retryable_txs: Vec<_> = batch + let mut retryable_txs: Vec<_> = batch .lock_results() .iter() .enumerate() @@ -2971,13 +3009,19 @@ impl Bank { max_age, &mut error_counters, ); + + // check transactions against cost_model, those not be accepted will be returned + // as retryable transactions, and excluded from downstream process (eg execution). + let (retryable_txs_by_cost_model, check_and_filtered_results) = + self.filter_transactions_by_cost(hashed_txs.as_transactions_iter(), check_results); + retryable_txs.extend(retryable_txs_by_cost_model); check_time.stop(); let mut load_time = Measure::start("accounts_load"); let mut loaded_accounts = self.rc.accounts.load_accounts( &self.ancestors, hashed_txs.as_transactions_iter(), - check_results, + check_and_filtered_results, &self.blockhash_queue.read().unwrap(), &mut error_counters, &self.rent_collector, @@ -12690,4 +12734,42 @@ pub(crate) mod tests { 0 ); } + + #[test] + fn test_bank_fitler_transactions_by_cost_ok() { + solana_logger::setup(); + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_leader( + 1_000_000_000_000_000, + &Pubkey::new_unique(), + bootstrap_validator_stake_lamports(), + ); + let bank = Bank::new(&genesis_config); + + let mock_program_id = Pubkey::new(&[2u8; 32]); + let mock_transaction = create_mock_transaction( + &Keypair::new(), + &Keypair::new(), + &Keypair::new(), + &Keypair::new(), + mock_program_id, + genesis_config.hash(), + ); + let input_check_results = vec![(Ok(()), None)]; + let (retryable_txs, check_results) = + bank.filter_transactions_by_cost(&mut [mock_transaction].iter(), input_check_results); + + // if transactino is booked by cost model, then it will not retry + //* + assert_eq!(0, retryable_txs.len()); + assert_eq!(1, check_results.len()); + assert!(check_results[0].0.is_ok()); + // */ + // if transaction not allowed by cost model, then proper err is logged + /* + assert_eq!( 1, retryable_txs.len() ); + assert_eq!( 0, retryable_txs[0] ); + assert_eq!( 1, check_results.len() ); + assert_eq!( Err(TransactionError::CostExceedsLimit), check_results[0].0 ); + // */ + } } diff --git a/runtime/src/cost_model.rs b/runtime/src/cost_model.rs new file mode 100644 index 00000000000000..e038c0eb5670da --- /dev/null +++ b/runtime/src/cost_model.rs @@ -0,0 +1,458 @@ +//! `cost_model` aiming to limit the size of broadcasting sets, and reducing the number +//! of un-parallelizeble transactions (eg, transactions as same writable key sets). +//! By doing so to improve leader performance. + +use crate::cost_tracker::CostTracker; +use log::*; +use solana_sdk::{ + bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, clock::Slot, message::Message, + pubkey::Pubkey, system_program, transaction::Transaction, +}; +use std::{collections::HashMap, str::FromStr}; + +// TODO revisit these hardcoded numbers, better get from mainnet log +const COST_UNIT: u32 = 1; +const DEFAULT_PROGRAM_COST: u32 = COST_UNIT * 500; +const CHAIN_MAX_COST: u32 = COST_UNIT * 100_000; +const BLOCK_MAX_COST: u32 = COST_UNIT * 100_000_000; + +#[derive(Default, Debug)] +pub struct CostModelStats { + pub total_cost: u32, + pub number_of_accounts: usize, + pub costliest_account: Pubkey, + pub costliest_account_cost: u32, +} + +#[derive(Debug)] +pub struct CostModel { + cost_metrics: HashMap, + cost_tracker: CostTracker, + current_bank_slot: Slot, +} + +macro_rules! costmetrics { + ($( $key: expr => $val: expr ),*) => {{ + let mut hashmap: HashMap< Pubkey, u32 > = HashMap::new(); + $( hashmap.insert( $key, $val); )* + hashmap + }} +} + +impl Default for CostModel { + fn default() -> Self { + CostModel::new() + } +} + +impl CostModel { + pub fn new() -> Self { + Self::new_with_config(CHAIN_MAX_COST, BLOCK_MAX_COST) + } + + // returns total block cost if succeeded in adding; + pub fn try_to_add_transaction(&mut self, transaction: &Transaction) -> Option { + let writable_accounts = &Self::find_writable_keys(transaction.message())[..]; + let transaction_cost = self.find_transaction_cost(&transaction); + + if self + .cost_tracker + .would_exceed_limit(writable_accounts, &transaction_cost) + { + debug!("can not fit transaction {:?}", transaction); + None + } else { + debug!("transaction {:?} added to block", transaction); + self.cost_tracker + .add_transaction(writable_accounts, &transaction_cost); + Some(*self.cost_tracker.package_cost()) + } + } + + pub fn get_stats(&self) -> CostModelStats { + // A temp method to collect bank cost stats + let mut stats = CostModelStats { + total_cost: *self.cost_tracker.package_cost(), + number_of_accounts: self.cost_tracker.account_costs().len(), + costliest_account: Pubkey::default(), + costliest_account_cost: 0, + }; + + for (key, cost) in self.cost_tracker.account_costs().iter() { + if cost > &stats.costliest_account_cost { + stats.costliest_account = *key; + stats.costliest_account_cost = *cost; + } + } + + stats + } + + pub fn total_cost(&self) -> &u32 { + self.cost_tracker.package_cost() + } + + pub fn reset_if_new_bank(&mut self, slot: Slot) { + if slot != self.current_bank_slot { + self.cost_tracker.reset(); + self.current_bank_slot = slot; + } + } + + fn new_with_config(chain_max: u32, block_max: u32) -> Self { + debug!( + "new cost model with chain_max {}, block_max {}", + chain_max, block_max + ); + + // NOTE: message.rs has following lazy_static program ids. Can probably use them to define + // `cost` for each type. + let parse = |s| Pubkey::from_str(s).unwrap(); + Self { + cost_metrics: costmetrics![ + parse("Config1111111111111111111111111111111111111") => COST_UNIT, + parse("Feature111111111111111111111111111111111111") => COST_UNIT, + parse("NativeLoader1111111111111111111111111111111") => COST_UNIT, + parse("Stake11111111111111111111111111111111111111") => COST_UNIT, + parse("StakeConfig11111111111111111111111111111111") => COST_UNIT, + parse("Vote111111111111111111111111111111111111111") => COST_UNIT * 5, + system_program::id() => COST_UNIT, + bpf_loader::id() => COST_UNIT * 1_000, + bpf_loader_deprecated::id() => COST_UNIT * 1_000, + bpf_loader_upgradeable::id() => COST_UNIT * 1_000 + ], + cost_tracker: CostTracker::new(chain_max, block_max), + current_bank_slot: 0, + } + } + + fn find_instruction_cost(&self, program_key: &Pubkey) -> &u32 { + match self.cost_metrics.get(&program_key) { + Some(cost) => cost, + None => { + debug!( + "Program key {:?} does not have assigned cost, using default {}", + program_key, DEFAULT_PROGRAM_COST + ); + &DEFAULT_PROGRAM_COST + } + } + } + + fn find_transaction_cost(&self, transaction: &Transaction) -> u32 { + let mut cost: u32 = 0; + + for instruction in &transaction.message().instructions { + let program_id = + transaction.message().account_keys[instruction.program_id_index as usize]; + let instruction_cost = self.find_instruction_cost(&program_id); + debug!( + "instruction {:?} has cost of {}", + instruction, instruction_cost + ); + cost += instruction_cost; + } + cost + } + + fn find_writable_keys(message: &Message) -> Vec { + let demote_sysvar_write_locks = true; + message + .account_keys + .iter() + .enumerate() + .filter_map(|(i, k)| { + if message.is_writable(i, demote_sysvar_write_locks) { + Some(*k) + } else { + None + } + }) + .collect::>() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + bank::Bank, + genesis_utils::{create_genesis_config, GenesisConfigInfo}, + }; + use solana_sdk::{ + hash::Hash, + instruction::CompiledInstruction, + message::Message, + signature::{Keypair, Signer}, + system_instruction::{self}, + system_transaction, + }; + use std::{ + sync::{Arc, Mutex}, + thread::{self, JoinHandle}, + }; + + fn test_setup() -> (Keypair, Hash) { + solana_logger::setup(); + let GenesisConfigInfo { + genesis_config, + mint_keypair, + .. + } = create_genesis_config(10); + let bank = Arc::new(Bank::new_no_wallclock_throttle(&genesis_config)); + let start_hash = bank.last_blockhash(); + (mint_keypair, start_hash) + } + + #[test] + fn test_cost_model_instruction_cost() { + let testee = CostModel::new(); + + // find cost for known programs + assert_eq!( + COST_UNIT * 5, + *testee.find_instruction_cost( + &Pubkey::from_str("Vote111111111111111111111111111111111111111").unwrap() + ) + ); + assert_eq!( + COST_UNIT * 1_000, + *testee.find_instruction_cost(&bpf_loader::id()) + ); + + // unknown program is assigned with default cost + assert_eq!( + DEFAULT_PROGRAM_COST, + *testee.find_instruction_cost( + &Pubkey::from_str("unknown111111111111111111111111111111111111").unwrap() + ) + ); + } + + #[test] + fn test_cost_model_simple_transaction() { + let (mint_keypair, start_hash) = test_setup(); + + let keypair = Keypair::new(); + let simple_transaction = + system_transaction::transfer(&mint_keypair, &keypair.pubkey(), 2, start_hash); + debug!( + "system_transaction simple_transaction {:?}", + simple_transaction + ); + + // expected cost for one system transfer instructions + let expected_cost = COST_UNIT; + + let testee = CostModel::new(); + assert_eq!( + expected_cost, + testee.find_transaction_cost(&simple_transaction) + ); + } + + #[test] + fn test_cost_model_transaction_many_transfer_instructions() { + let (mint_keypair, start_hash) = test_setup(); + + let key1 = solana_sdk::pubkey::new_rand(); + let key2 = solana_sdk::pubkey::new_rand(); + let instructions = + system_instruction::transfer_many(&mint_keypair.pubkey(), &[(key1, 1), (key2, 1)]); + let message = Message::new(&instructions, Some(&mint_keypair.pubkey())); + let tx = Transaction::new(&[&mint_keypair], message, start_hash); + debug!("many transfer transaction {:?}", tx); + + // expected cost for two system transfer instructions + let expected_cost = COST_UNIT * 2; + + let testee = CostModel::new(); + assert_eq!(expected_cost, testee.find_transaction_cost(&tx)); + } + + #[test] + fn test_cost_model_message_many_different_instructions() { + let (mint_keypair, start_hash) = test_setup(); + + // construct a transaction with multiple random instructions + let key1 = solana_sdk::pubkey::new_rand(); + let key2 = solana_sdk::pubkey::new_rand(); + let prog1 = solana_sdk::pubkey::new_rand(); + let prog2 = solana_sdk::pubkey::new_rand(); + let instructions = vec![ + CompiledInstruction::new(3, &(), vec![0, 1]), + CompiledInstruction::new(4, &(), vec![0, 2]), + ]; + let tx = Transaction::new_with_compiled_instructions( + &[&mint_keypair], + &[key1, key2], + start_hash, + vec![prog1, prog2], + instructions, + ); + debug!("many random transaction {:?}", tx); + + // expected cost for two random/unknown program is + let expected_cost = DEFAULT_PROGRAM_COST * 2; + + let testee = CostModel::new(); + assert_eq!(expected_cost, testee.find_transaction_cost(&tx)); + } + + #[test] + fn test_cost_model_message_get_writable_account() { + let (mint_keypair, start_hash) = test_setup(); + + // construct a transaction with multiple random instructions + let key1 = solana_sdk::pubkey::new_rand(); + let key2 = solana_sdk::pubkey::new_rand(); + let prog1 = solana_sdk::pubkey::new_rand(); + let prog2 = solana_sdk::pubkey::new_rand(); + let instructions = vec![ + CompiledInstruction::new(3, &(), vec![0, 1]), + CompiledInstruction::new(4, &(), vec![0, 2]), + ]; + let tx = Transaction::new_with_compiled_instructions( + &[&mint_keypair], + &[key1, key2], + start_hash, + vec![prog1, prog2], + instructions, + ); + + let writable_keys = CostModel::find_writable_keys(tx.message()); + debug!("transaction has writable keys: {:?}", writable_keys); + + // the mint_key and key1, key2 are all writable + assert_eq!(3, writable_keys.len()); + assert_eq!(mint_keypair.pubkey(), writable_keys[0]); + assert_eq!(key1, writable_keys[1]); + assert_eq!(key2, writable_keys[2]); + } + + #[test] + fn test_cost_model_can_fit_transaction() { + let (mint_keypair, start_hash) = test_setup(); + + // construct a transaction with a random instructions + let mut accounts: Vec = vec![]; + let mut program_ids: Vec = vec![]; + let mut instructions: Vec = vec![]; + + accounts.push(solana_sdk::pubkey::new_rand()); + program_ids.push(solana_sdk::pubkey::new_rand()); + instructions.push(CompiledInstruction::new(2, &(), vec![0, 1])); + let tx = Transaction::new_with_compiled_instructions( + &[&mint_keypair], + &accounts[..], + start_hash, + program_ids, + instructions, + ); + debug!("A random transaction {:?}", tx); + + let mut testee = CostModel::new(); + assert!(testee.try_to_add_transaction(&tx).is_some()); + } + + #[test] + fn test_cost_model_cannot_fit_transaction_on_chain_limit() { + let (mint_keypair, start_hash) = test_setup(); + + // construct a transaction with two random instructions with same signer + let key1 = solana_sdk::pubkey::new_rand(); + let key2 = solana_sdk::pubkey::new_rand(); + let prog1 = solana_sdk::pubkey::new_rand(); + let prog2 = solana_sdk::pubkey::new_rand(); + let instructions = vec![ + CompiledInstruction::new(3, &(), vec![0, 1]), + CompiledInstruction::new(4, &(), vec![0, 2]), + ]; + let tx = Transaction::new_with_compiled_instructions( + &[&mint_keypair], + &[key1, key2], + start_hash, + vec![prog1, prog2], + instructions, + ); + debug!("many random transaction {:?}", tx); + + // build model allows three transaction in total, but chain max is 1 + let mut testee = CostModel::new_with_config(DEFAULT_PROGRAM_COST, DEFAULT_PROGRAM_COST * 3); + assert!(testee.try_to_add_transaction(&tx).is_none()); + } + + #[test] + fn test_cost_model_cannot_fit_transaction_on_block_limit() { + let (_mint_keypair, start_hash) = test_setup(); + + // build model allows one transaction in total + let mut testee = CostModel::new_with_config(DEFAULT_PROGRAM_COST, DEFAULT_PROGRAM_COST); + + { + let signer_account = Keypair::new(); + let tx = Transaction::new_with_compiled_instructions( + &[&signer_account], + &[solana_sdk::pubkey::new_rand()], + start_hash, + vec![solana_sdk::pubkey::new_rand()], + vec![CompiledInstruction::new(2, &(), vec![0, 1])], + ); + debug!("Some random transaction {:?}", tx); + // the first transaction will fit + assert!(testee.try_to_add_transaction(&tx).is_some()); + } + + { + let signer_account = Keypair::new(); + let tx = Transaction::new_with_compiled_instructions( + &[&signer_account], + &[solana_sdk::pubkey::new_rand()], + start_hash, + vec![solana_sdk::pubkey::new_rand()], + vec![CompiledInstruction::new(2, &(), vec![0, 1])], + ); + debug!("Some random transaction {:?}", tx); + // the second transaction will not fit + assert!(testee.try_to_add_transaction(&tx).is_none()); + } + } + + #[test] + fn test_cost_model_can_be_shared_concurrently() { + let (mint_keypair, start_hash) = test_setup(); + let number_threads = 10; + let expected_total_cost = COST_UNIT * number_threads; + + let cost_model = Arc::new(Mutex::new(CostModel::new())); + + let thread_handlers: Vec> = (0..number_threads) + .map(|_| { + // each thread creates its own simple transaction + let simple_transaction = system_transaction::transfer( + &mint_keypair, + &Keypair::new().pubkey(), + 2, + start_hash, + ); + let cost_model = cost_model.clone(); + thread::spawn(move || { + assert!(cost_model + .lock() + .unwrap() + .try_to_add_transaction(&simple_transaction) + .is_some()); + }) + }) + .collect(); + + for th in thread_handlers { + th.join().unwrap(); + } + + assert_eq!( + expected_total_cost, + *cost_model.lock().unwrap().total_cost() + ); + } +} diff --git a/runtime/src/cost_tracker.rs b/runtime/src/cost_tracker.rs new file mode 100644 index 00000000000000..e6ee63a4787c0a --- /dev/null +++ b/runtime/src/cost_tracker.rs @@ -0,0 +1,247 @@ +//! `cost_tracker` keeps tracking tranasction cost per chained accounts as well as for entire block +use solana_sdk::pubkey::Pubkey; +use std::collections::HashMap; + +#[derive(Debug)] +pub struct CostTracker { + chain_max_cost: u32, + package_max_cost: u32, + chained_costs: HashMap, + package_cost: u32, +} + +impl CostTracker { + pub fn new(chain_max: u32, package_max: u32) -> Self { + assert!(chain_max <= package_max); + Self { + chain_max_cost: chain_max, + package_max_cost: package_max, + chained_costs: HashMap::new(), + package_cost: 0, + } + } + + pub fn would_exceed_limit(&self, keys: &[Pubkey], cost: &u32) -> bool { + // check against the total package cost + if self.package_cost + cost > self.package_max_cost { + return true; + } + + // chech if the transaction itself is more costly than the chain_max_cost + if *cost > self.chain_max_cost { + return true; + } + + // check each account against chain_max_cost, + for account_key in keys.iter() { + match self.chained_costs.get(&account_key) { + Some(chained_cost) => { + if chained_cost + cost > self.chain_max_cost { + return true; + } else { + continue; + } + } + None => continue, + } + } + + false + } + + pub fn add_transaction(&mut self, keys: &[Pubkey], cost: &u32) { + for account_key in keys.iter() { + *self.chained_costs.entry(*account_key).or_insert(0) += cost; + } + self.package_cost += cost; + } + + pub fn package_cost(&self) -> &u32 { + &self.package_cost + } + + pub fn account_costs(&self) -> &HashMap { + &self.chained_costs + } + + pub fn reset(&mut self) { + self.chained_costs.clear(); + self.package_cost = 0; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + bank::Bank, + genesis_utils::{create_genesis_config, GenesisConfigInfo}, + }; + use solana_sdk::{ + hash::Hash, + signature::{Keypair, Signer}, + system_transaction, + transaction::Transaction, + }; + use std::{cmp, sync::Arc}; + + fn test_setup() -> (Keypair, Hash) { + solana_logger::setup(); + let GenesisConfigInfo { + genesis_config, + mint_keypair, + .. + } = create_genesis_config(10); + let bank = Arc::new(Bank::new_no_wallclock_throttle(&genesis_config)); + let start_hash = bank.last_blockhash(); + (mint_keypair, start_hash) + } + + fn build_simple_transaction( + mint_keypair: &Keypair, + start_hash: &Hash, + ) -> (Transaction, Vec, u32) { + let keypair = Keypair::new(); + let simple_transaction = + system_transaction::transfer(&mint_keypair, &keypair.pubkey(), 2, *start_hash); + + (simple_transaction, vec![mint_keypair.pubkey()], 5) + } + + #[test] + fn test_cost_tracker_initialization() { + let testee = CostTracker::new(10, 11); + assert_eq!(10, testee.chain_max_cost); + assert_eq!(11, testee.package_max_cost); + assert_eq!(0, testee.chained_costs.len()); + assert_eq!(0, testee.package_cost); + } + + #[test] + fn test_cost_tracker_ok_add_one() { + let (mint_keypair, start_hash) = test_setup(); + let (_tx, keys, cost) = build_simple_transaction(&mint_keypair, &start_hash); + + // build testee to have capacity for one simple transaction + let mut testee = CostTracker::new(cost, cost); + assert_eq!(false, testee.would_exceed_limit(&keys, &cost)); + testee.add_transaction(&keys, &cost); + assert_eq!(cost, testee.package_cost); + } + + #[test] + fn test_cost_tracker_ok_add_two_same_accounts() { + let (mint_keypair, start_hash) = test_setup(); + // build two transactions with same signed account + let (_tx1, keys1, cost1) = build_simple_transaction(&mint_keypair, &start_hash); + let (_tx2, keys2, cost2) = build_simple_transaction(&mint_keypair, &start_hash); + + // build testee to have capacity for two simple transactions, with same accounts + let mut testee = CostTracker::new(cost1 + cost2, cost1 + cost2); + { + assert_eq!(false, testee.would_exceed_limit(&keys1, &cost1)); + testee.add_transaction(&keys1, &cost1); + } + { + assert_eq!(false, testee.would_exceed_limit(&keys2, &cost2)); + testee.add_transaction(&keys2, &cost2); + } + assert_eq!(cost1 + cost2, testee.package_cost); + assert_eq!(1, testee.chained_costs.len()); + } + + #[test] + fn test_cost_tracker_ok_add_two_diff_accounts() { + let (mint_keypair, start_hash) = test_setup(); + // build two transactions with diff accounts + let (_tx1, keys1, cost1) = build_simple_transaction(&mint_keypair, &start_hash); + let second_account = Keypair::new(); + let (_tx2, keys2, cost2) = build_simple_transaction(&second_account, &start_hash); + + // build testee to have capacity for two simple transactions, with same accounts + let mut testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2); + { + assert_eq!(false, testee.would_exceed_limit(&keys1, &cost1)); + testee.add_transaction(&keys1, &cost1); + } + { + assert_eq!(false, testee.would_exceed_limit(&keys2, &cost2)); + testee.add_transaction(&keys2, &cost2); + } + assert_eq!(cost1 + cost2, testee.package_cost); + assert_eq!(2, testee.chained_costs.len()); + } + + #[test] + fn test_cost_tracker_chain_reach_limit() { + let (mint_keypair, start_hash) = test_setup(); + // build two transactions with same signed account + let (_tx1, keys1, cost1) = build_simple_transaction(&mint_keypair, &start_hash); + let (_tx2, keys2, cost2) = build_simple_transaction(&mint_keypair, &start_hash); + + // build testee to have capacity for two simple transactions, but not for same accounts + let mut testee = CostTracker::new(cmp::min(cost1, cost2), cost1 + cost2); + // should have room for first transaction + { + assert_eq!(false, testee.would_exceed_limit(&keys1, &cost1)); + testee.add_transaction(&keys1, &cost1); + } + // but no more sapce on the same chain (same signer account) + { + assert_eq!(true, testee.would_exceed_limit(&keys2, &cost2)); + } + } + + #[test] + fn test_cost_tracker_reach_limit() { + let (mint_keypair, start_hash) = test_setup(); + // build two transactions with diff accounts + let (_tx1, keys1, cost1) = build_simple_transaction(&mint_keypair, &start_hash); + let second_account = Keypair::new(); + let (_tx2, keys2, cost2) = build_simple_transaction(&second_account, &start_hash); + + // build testee to have capacity for each chain, but not enough room for both transactions + let mut testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2 - 1); + // should have room for first transaction + { + assert_eq!(false, testee.would_exceed_limit(&keys1, &cost1)); + testee.add_transaction(&keys1, &cost1); + } + // but no more room for package as whole + { + assert_eq!(true, testee.would_exceed_limit(&keys2, &cost2)); + } + } + + #[test] + fn test_cost_tracker_reset() { + let (mint_keypair, start_hash) = test_setup(); + // build two transactions with same signed account + let (_tx1, keys1, cost1) = build_simple_transaction(&mint_keypair, &start_hash); + let (_tx2, keys2, cost2) = build_simple_transaction(&mint_keypair, &start_hash); + + // build testee to have capacity for two simple transactions, but not for same accounts + let mut testee = CostTracker::new(cmp::min(cost1, cost2), cost1 + cost2); + // should have room for first transaction + { + assert_eq!(false, testee.would_exceed_limit(&keys1, &cost1)); + testee.add_transaction(&keys1, &cost1); + assert_eq!(1, testee.chained_costs.len()); + assert_eq!(cost1, testee.package_cost); + } + // but no more sapce on the same chain (same signer account) + { + assert_eq!(true, testee.would_exceed_limit(&keys2, &cost2)); + } + // reset the tracker + { + testee.reset(); + assert_eq!(0, testee.chained_costs.len()); + assert_eq!(0, testee.package_cost); + } + //now the second transaction can be added + { + assert_eq!(false, testee.would_exceed_limit(&keys2, &cost2)); + } + } +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index bce157325131ed..2ef17dedce01f7 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -16,6 +16,8 @@ pub mod bloom; pub mod builtins; pub mod commitment; pub mod contains; +pub mod cost_model; +pub mod cost_tracker; pub mod epoch_stakes; pub mod genesis_utils; pub mod hardened_unpack; diff --git a/sdk/src/transaction.rs b/sdk/src/transaction.rs index 077e1b21cd0d77..76c5a66ca779b6 100644 --- a/sdk/src/transaction.rs +++ b/sdk/src/transaction.rs @@ -98,6 +98,10 @@ pub enum TransactionError { /// Transaction processing left an account with an outstanding borrowed reference #[error("Transaction processing left an account with an outstanding borrowed reference")] AccountBorrowOutstanding, + + /// Transaction cost exceeds the cost model limit + #[error("Transaction cost exceeds the cost model limit")] + CostExceedsLimit, } pub type Result = result::Result; diff --git a/storage-proto/proto/solana.storage.transaction_by_addr.rs b/storage-proto/proto/solana.storage.transaction_by_addr.rs index 58aae3b2919dae..9398e510a2703a 100644 --- a/storage-proto/proto/solana.storage.transaction_by_addr.rs +++ b/storage-proto/proto/solana.storage.transaction_by_addr.rs @@ -67,6 +67,7 @@ pub enum TransactionErrorType { SanitizeFailure = 14, ClusterMaintenance = 15, AccountBorrowOutstanding = 16, + CostExceedsLimit = 17, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] diff --git a/storage-proto/src/convert.rs b/storage-proto/src/convert.rs index 5f984200877102..13f2d001da5351 100644 --- a/storage-proto/src/convert.rs +++ b/storage-proto/src/convert.rs @@ -530,6 +530,7 @@ impl TryFrom for TransactionError { 14 => TransactionError::SanitizeFailure, 15 => TransactionError::ClusterMaintenance, 16 => TransactionError::AccountBorrowOutstanding, + 17 => TransactionError::CostExceedsLimit, _ => return Err("Invalid TransactionError"), }) } @@ -588,6 +589,9 @@ impl From for tx_by_addr::TransactionError { TransactionError::AccountBorrowOutstanding => { tx_by_addr::TransactionErrorType::AccountBorrowOutstanding } + TransactionError::CostExceedsLimit => { + tx_by_addr::TransactionErrorType::CostExceedsLimit + } } as i32, instruction_error: match transaction_error { TransactionError::InstructionError(index, ref instruction_error) => {