Reject blocks for costs above the max block cost (#18994)

* added realtime cost checking logic to reject block that would exceed max limit:
- defines max limits at block_cost_limits.rs
- right after each bath's execution, accumulate its cost and check again
  limit, return error if limit is exceeded

* update abi that changed due to adding additional TransactionError

* To avoid counting stats mltiple times, only accumulate execute-timing when a bank is completed

* gate it by a feature

* move cost const def into block_cost_limits.rs

* redefine the cost for signature and account access, removed signer part as it is not well defined for now

* check if per_program_timings of execute_timings before sending
This commit is contained in:
Tao Zhu
2021-08-12 10:48:47 -05:00
committed by GitHub
parent 9d8594a046
commit 414d904959
10 changed files with 159 additions and 47 deletions

View File

@@ -9,30 +9,10 @@
//!
use crate::execute_cost_table::ExecuteCostTable;
use log::*;
use solana_ledger::block_cost_limits::*;
use solana_sdk::{pubkey::Pubkey, sanitized_transaction::SanitizedTransaction};
use std::collections::HashMap;
// 07-27-2021, compute_unit to microsecond conversion ratio collected from mainnet-beta
// differs between instructions. Some bpf instruction has much higher CU/US ratio
// (eg 7vxeyaXGLqcp66fFShqUdHxdacp4k4kwUpRSSeoZLCZ4 has average ratio 135), others
// have lower ratio (eg 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin has an average ratio 14).
// With this, I am guestimating the flat_fee for sigver and account read/write
// as following. This can be adjusted when needed.
const SIGVER_COST: u64 = 1;
const NON_SIGNED_READONLY_ACCOUNT_ACCESS_COST: u64 = 1;
const NON_SIGNED_WRITABLE_ACCOUNT_ACCESS_COST: u64 = 2;
const SIGNED_READONLY_ACCOUNT_ACCESS_COST: u64 =
SIGVER_COST + NON_SIGNED_READONLY_ACCOUNT_ACCESS_COST;
const SIGNED_WRITABLE_ACCOUNT_ACCESS_COST: u64 =
SIGVER_COST + NON_SIGNED_WRITABLE_ACCOUNT_ACCESS_COST;
// 07-27-2021, cost model limit is set to "worst case scenario", which is the
// max compute unit it can execute. From mainnet-beta, the max CU of instruction
// is 3753, round up to 4_000. Say we allows max 50_000 instruction per writable i
// account, and 1_000_000 instruction per block. It comes to following limits:
pub const ACCOUNT_MAX_COST: u64 = 200_000_000;
pub const BLOCK_MAX_COST: u64 = 4_000_000_000;
const MAX_WRITABLE_ACCOUNTS: usize = 256;
#[derive(Debug, Clone)]
@@ -88,7 +68,7 @@ pub struct CostModel {
impl Default for CostModel {
fn default() -> Self {
CostModel::new(ACCOUNT_MAX_COST, BLOCK_MAX_COST)
CostModel::new(account_cost_max(), block_cost_max())
}
}
@@ -142,21 +122,13 @@ impl CostModel {
// calculate account access cost
let message = transaction.message();
message.account_keys.iter().enumerate().for_each(|(i, k)| {
let is_signer = message.is_signer(i);
let is_writable = message.is_writable(i);
if is_signer && is_writable {
if is_writable {
self.transaction_cost.writable_accounts.push(*k);
self.transaction_cost.account_access_cost += SIGNED_WRITABLE_ACCOUNT_ACCESS_COST;
} else if is_signer && !is_writable {
self.transaction_cost.account_access_cost += SIGNED_READONLY_ACCOUNT_ACCESS_COST;
} else if !is_signer && is_writable {
self.transaction_cost.writable_accounts.push(*k);
self.transaction_cost.account_access_cost +=
NON_SIGNED_WRITABLE_ACCOUNT_ACCESS_COST;
self.transaction_cost.account_access_cost += account_write_cost();
} else {
self.transaction_cost.account_access_cost +=
NON_SIGNED_READONLY_ACCOUNT_ACCESS_COST;
self.transaction_cost.account_access_cost += account_read_cost();
}
});
debug!(
@@ -418,9 +390,8 @@ mod tests {
.try_into()
.unwrap();
let expected_account_cost = SIGNED_WRITABLE_ACCOUNT_ACCESS_COST
+ NON_SIGNED_WRITABLE_ACCOUNT_ACCESS_COST
+ NON_SIGNED_READONLY_ACCOUNT_ACCESS_COST;
let expected_account_cost =
account_write_cost() + account_write_cost() + account_read_cost();
let expected_execution_cost = 8;
let mut cost_model = CostModel::default();
@@ -475,9 +446,8 @@ mod tests {
);
let number_threads = 10;
let expected_account_cost = SIGNED_WRITABLE_ACCOUNT_ACCESS_COST
+ NON_SIGNED_WRITABLE_ACCOUNT_ACCESS_COST * 2
+ NON_SIGNED_READONLY_ACCOUNT_ACCESS_COST * 2;
let expected_account_cost =
account_write_cost() + account_write_cost() * 2 + account_read_cost() * 2;
let cost1 = 100;
let cost2 = 200;
// execution cost can be either 2 * Default (before write) or cost1+cost2 (after write)

View File

@@ -1931,7 +1931,6 @@ impl ReplayStage {
replay_vote_sender,
verify_recyclers,
);
execute_timings.accumulate(&bank_progress.replay_stats.execute_timings);
match replay_result {
Ok(replay_tx_count) => tx_count += replay_tx_count,
Err(err) => {
@@ -1957,6 +1956,12 @@ impl ReplayStage {
}
assert_eq!(*bank_slot, bank.slot());
if bank.is_complete() {
execute_timings.accumulate(&bank_progress.replay_stats.execute_timings);
debug!("bank {} is completed replay from blockstore, contribute to update cost with {:?}",
bank.slot(),
bank_progress.replay_stats.execute_timings
);
bank_progress.replay_stats.report_stats(
bank.slot(),
bank_progress.replay_progress.num_entries,
@@ -2031,9 +2036,11 @@ impl ReplayStage {
}
// send accumulated excute-timings to cost_update_service
cost_update_sender
.send(execute_timings)
.unwrap_or_else(|err| warn!("cost_update_sender failed: {:?}", err));
if !execute_timings.details.per_program_timings.is_empty() {
cost_update_sender
.send(execute_timings)
.unwrap_or_else(|err| warn!("cost_update_sender failed: {:?}", err));
}
inc_new_counter_info!("replay_stage-replay_transactions", tx_count);
did_complete_bank