diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index 750a6fd9f..42f5993e2 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -46,6 +46,7 @@ use { State as NonceState, }, pubkey::Pubkey, + saturating_add_assign, signature::Signature, slot_hashes::SlotHashes, system_program, @@ -56,6 +57,7 @@ use { std::{ cmp::Reverse, collections::{hash_map, BinaryHeap, HashMap, HashSet}, + num::NonZeroUsize, ops::RangeBounds, path::PathBuf, sync::{ @@ -237,6 +239,45 @@ impl Accounts { }) } + /// If feature `cap_transaction_accounts_data_size` is active, total accounts data a + /// transaction can load is limited to 64MiB to not break anyone in Mainnet-beta today. + /// (It will be set by compute_budget instruction in the future to more reasonable level). + fn get_requested_loaded_accounts_data_size_limit( + feature_set: &FeatureSet, + ) -> Option { + feature_set + .is_active(&feature_set::cap_transaction_accounts_data_size::id()) + .then(|| { + const REQUESTED_LOADED_ACCOUNTS_DATA_SIZE: usize = 64 * 1024 * 1024; + NonZeroUsize::new(REQUESTED_LOADED_ACCOUNTS_DATA_SIZE) + .expect("requested loaded accounts data size is greater than 0") + }) + } + + /// Accumulate loaded account data size into `accumulated_accounts_data_size`. + /// Returns TransactionErr::MaxLoadedAccountsDataSizeExceeded if + /// `requested_loaded_accounts_data_size_limit` is specified and + /// `accumulated_accounts_data_size` exceeds it. + fn accumulate_and_check_loaded_account_data_size( + accumulated_loaded_accounts_data_size: &mut usize, + account_data_size: usize, + requested_loaded_accounts_data_size_limit: Option, + error_counters: &mut TransactionErrorMetrics, + ) -> Result<()> { + if let Some(requested_loaded_accounts_data_size) = requested_loaded_accounts_data_size_limit + { + saturating_add_assign!(*accumulated_loaded_accounts_data_size, account_data_size); + if *accumulated_loaded_accounts_data_size > requested_loaded_accounts_data_size.get() { + error_counters.max_loaded_accounts_data_size_exceeded += 1; + Err(TransactionError::MaxLoadedAccountsDataSizeExceeded) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + fn load_transaction_accounts( &self, ancestors: &Ancestors, @@ -264,6 +305,11 @@ impl Accounts { let set_exempt_rent_epoch_max = feature_set.is_active(&solana_sdk::feature_set::set_exempt_rent_epoch_max::id()); + + let requested_loaded_accounts_data_size_limit = + Self::get_requested_loaded_accounts_data_size_limit(feature_set); + let mut accumulated_accounts_data_size: usize = 0; + let mut accounts = account_keys .iter() .enumerate() @@ -312,6 +358,12 @@ impl Accounts { (default_account, 0) }) }; + Self::accumulate_and_check_loaded_account_data_size( + &mut accumulated_accounts_data_size, + account.data().len(), + requested_loaded_accounts_data_size_limit, + error_counters, + )?; if !validated_fee_payer && message.is_non_loader_key(i) { if i != 0 { @@ -347,6 +399,12 @@ impl Accounts { .accounts_db .load_with_fixed_root(ancestors, &programdata_address) { + Self::accumulate_and_check_loaded_account_data_size( + &mut accumulated_accounts_data_size, + programdata_account.data().len(), + requested_loaded_accounts_data_size_limit, + error_counters, + )?; account_dep_index = Some(account_keys.len().saturating_add(account_deps.len()) as IndexOfAccount); @@ -435,6 +493,12 @@ impl Accounts { if let Some((program_account, _)) = self.accounts_db.load_with_fixed_root(ancestors, owner_id) { + Self::accumulate_and_check_loaded_account_data_size( + &mut accumulated_accounts_data_size, + program_account.data().len(), + requested_loaded_accounts_data_size_limit, + error_counters, + )?; accounts.push((*owner_id, program_account)); } else { error_counters.account_not_found += 1; @@ -3735,4 +3799,53 @@ mod tests { )); } } + + #[test] + fn test_accumulate_and_check_loaded_account_data_size() { + let mut error_counter = TransactionErrorMetrics::default(); + + // assert check is OK if data limit is not enabled + { + let mut accumulated_data_size: usize = 0; + let data_size = usize::MAX; + let requested_data_size_limit = None; + + assert!(Accounts::accumulate_and_check_loaded_account_data_size( + &mut accumulated_data_size, + data_size, + requested_data_size_limit, + &mut error_counter + ) + .is_ok()); + } + + // assert check will fail with correct error if loaded data exceeds limit + { + let mut accumulated_data_size: usize = 0; + let data_size: usize = 123; + let requested_data_size_limit = NonZeroUsize::new(data_size); + + // OK - loaded data size is up to limit + assert!(Accounts::accumulate_and_check_loaded_account_data_size( + &mut accumulated_data_size, + data_size, + requested_data_size_limit, + &mut error_counter + ) + .is_ok()); + assert_eq!(data_size, accumulated_data_size); + + // fail - loading more data that would exceed limit + let another_byte: usize = 1; + assert_eq!( + Accounts::accumulate_and_check_loaded_account_data_size( + &mut accumulated_data_size, + another_byte, + requested_data_size_limit, + &mut error_counter + ), + Err(TransactionError::MaxLoadedAccountsDataSizeExceeded) + ); + } + } } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index e3fa8f2b1..915e6560d 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -278,7 +278,7 @@ impl RentDebits { } pub type BankStatusCache = StatusCache>; -#[frozen_abi(digest = "A7T7XohiSoo8FGoCPTsaXAYYugXTkoYnBjQAdBgYHH85")] +#[frozen_abi(digest = "AwRx17Bgesvvu9NZVwAoqofq7MU52gkwvjLvjVQpW64S")] pub type BankSlotDelta = SlotDelta>; // Eager rent collection repeats in cyclic manner. diff --git a/runtime/src/transaction_error_metrics.rs b/runtime/src/transaction_error_metrics.rs index baa25a073..ef947ebd7 100644 --- a/runtime/src/transaction_error_metrics.rs +++ b/runtime/src/transaction_error_metrics.rs @@ -23,6 +23,7 @@ pub struct TransactionErrorMetrics { pub would_exceed_max_account_cost_limit: usize, pub would_exceed_max_vote_cost_limit: usize, pub would_exceed_account_data_block_limit: usize, + pub max_loaded_accounts_data_size_exceeded: usize, } impl TransactionErrorMetrics { @@ -76,6 +77,10 @@ impl TransactionErrorMetrics { self.would_exceed_account_data_block_limit, other.would_exceed_account_data_block_limit ); + saturating_add_assign!( + self.max_loaded_accounts_data_size_exceeded, + other.max_loaded_accounts_data_size_exceeded + ); } pub fn report(&self, id: u32, slot: Slot) { @@ -152,6 +157,11 @@ impl TransactionErrorMetrics { self.would_exceed_account_data_block_limit as i64, i64 ), + ( + "max_loaded_accounts_data_size_exceeded", + self.max_loaded_accounts_data_size_exceeded as i64, + i64 + ), ); } } diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index 30ba1ed1c..4e70ed882 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -594,6 +594,10 @@ pub mod disable_builtin_loader_ownership_chains { solana_sdk::declare_id!("4UDcAfQ6EcA6bdcadkeHpkarkhZGJ7Bpq7wTAiRMjkoi"); } +pub mod cap_transaction_accounts_data_size { + solana_sdk::declare_id!("DdLwVYuvDz26JohmgSbA7mjpJFgX5zP2dkp8qsF2C33V"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -737,6 +741,7 @@ lazy_static! { (update_hashes_per_tick::id(), "Update desired hashes per tick on epoch boundary"), (enable_big_mod_exp_syscall::id(), "add big_mod_exp syscall #28503"), (disable_builtin_loader_ownership_chains::id(), "disable builtin loader ownership chains #29956"), + (cap_transaction_accounts_data_size::id(), "cap transaction accounts data size up to a limit #27839"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/sdk/src/transaction/error.rs b/sdk/src/transaction/error.rs index 47d5856ce..32e4af7e8 100644 --- a/sdk/src/transaction/error.rs +++ b/sdk/src/transaction/error.rs @@ -149,6 +149,10 @@ pub enum TransactionError { "Transaction results in an account ({account_index}) with insufficient funds for rent" )] InsufficientFundsForRent { account_index: u8 }, + + /// Transaction exceeded max loaded accounts data size cap + #[error("Transaction exceeded max loaded accounts data size cap")] + MaxLoadedAccountsDataSizeExceeded, } impl From for TransactionError { diff --git a/storage-proto/proto/transaction_by_addr.proto b/storage-proto/proto/transaction_by_addr.proto index 6a1869fc2..6c82129af 100644 --- a/storage-proto/proto/transaction_by_addr.proto +++ b/storage-proto/proto/transaction_by_addr.proto @@ -57,6 +57,7 @@ enum TransactionErrorType { WOULD_EXCEED_ACCOUNT_DATA_TOTAL_LIMIT = 29; DUPLICATE_INSTRUCTION = 30; INSUFFICIENT_FUNDS_FOR_RENT = 31; + MAX_LOADED_ACCOUNTS_DATA_SIZE_EXCEEDED = 32; } message InstructionError { diff --git a/storage-proto/src/convert.rs b/storage-proto/src/convert.rs index ce800cfe4..9e4926b19 100644 --- a/storage-proto/src/convert.rs +++ b/storage-proto/src/convert.rs @@ -801,6 +801,7 @@ impl TryFrom for TransactionError { 27 => TransactionError::InvalidRentPayingAccount, 28 => TransactionError::WouldExceedMaxVoteCostLimit, 29 => TransactionError::WouldExceedAccountDataTotalLimit, + 32 => TransactionError::MaxLoadedAccountsDataSizeExceeded, _ => return Err("Invalid TransactionError"), }) } @@ -904,6 +905,9 @@ impl From for tx_by_addr::TransactionError { TransactionError::InsufficientFundsForRent { .. } => { tx_by_addr::TransactionErrorType::InsufficientFundsForRent } + TransactionError::MaxLoadedAccountsDataSizeExceeded => { + tx_by_addr::TransactionErrorType::MaxLoadedAccountsDataSizeExceeded + } } as i32, instruction_error: match transaction_error { TransactionError::InstructionError(index, ref instruction_error) => {