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

Commit

Permalink
Limit loaded data per transaction to a fixed cap (#29743)
Browse files Browse the repository at this point in the history
  • Loading branch information
tao-stones authored Feb 1, 2023
1 parent 9780cd1 commit a5af546
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 1 deletion.
113 changes: 113 additions & 0 deletions runtime/src/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ use {
State as NonceState,
},
pubkey::Pubkey,
saturating_add_assign,
signature::Signature,
slot_hashes::SlotHashes,
system_program,
Expand All @@ -56,6 +57,7 @@ use {
std::{
cmp::Reverse,
collections::{hash_map, BinaryHeap, HashMap, HashSet},
num::NonZeroUsize,
ops::RangeBounds,
path::PathBuf,
sync::{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
);
}
}
}
2 changes: 1 addition & 1 deletion runtime/src/bank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions runtime/src/transaction_error_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
),
);
}
}
5 changes: 5 additions & 0 deletions sdk/src/feature_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> = [
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions sdk/src/transaction/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions storage-proto/proto/transaction_by_addr.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions storage-proto/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
})
}
Expand Down Expand Up @@ -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) => {
Expand Down

0 comments on commit a5af546

Please sign in to comment.