Limit loaded data per transaction to a fixed cap (#29743)

This commit is contained in:
Tao Zhu 2023-01-31 22:51:35 -06:00 committed by GitHub
parent 9780cd10c4
commit a5af54669a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 138 additions and 1 deletions

View File

@ -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<NonZeroUsize> {
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<NonZeroUsize>,
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)
);
}
}
}

View File

@ -278,7 +278,7 @@ impl RentDebits {
}
pub type BankStatusCache = StatusCache<Result<()>>;
#[frozen_abi(digest = "A7T7XohiSoo8FGoCPTsaXAYYugXTkoYnBjQAdBgYHH85")]
#[frozen_abi(digest = "AwRx17Bgesvvu9NZVwAoqofq7MU52gkwvjLvjVQpW64S")]
pub type BankSlotDelta = SlotDelta<Result<()>>;
// Eager rent collection repeats in cyclic manner.

View File

@ -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
),
);
}
}

View File

@ -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<Pubkey, &'static str> = [
@ -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()

View File

@ -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<SanitizeError> for TransactionError {

View File

@ -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 {

View File

@ -801,6 +801,7 @@ impl TryFrom<tx_by_addr::TransactionError> for TransactionError {
27 => TransactionError::InvalidRentPayingAccount,
28 => TransactionError::WouldExceedMaxVoteCostLimit,
29 => TransactionError::WouldExceedAccountDataTotalLimit,
32 => TransactionError::MaxLoadedAccountsDataSizeExceeded,
_ => return Err("Invalid TransactionError"),
})
}
@ -904,6 +905,9 @@ impl From<TransactionError> 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) => {