Skip to content

Commit

Permalink
Cap accounts data a transaction can load by its requested limit (back…
Browse files Browse the repository at this point in the history
…port #27840) (#28797)

* Cap accounts data a transaction can load by its requested limit (#27840)

- Add new compute-budget instruction to set transaction-wide accounts data size limit
- Set default accounts data limit to 10MB, and max to 100MB, per transaction;
- Add getters to make changing default and/or max values easier in the future with feature gates;
- added error counter for transactions exceed data size limit

(cherry picked from commit 81dc2e5)

# Conflicts:
#	core/src/transaction_priority_details.rs
#	program-runtime/src/compute_budget.rs
#	programs/bpf/tests/programs.rs
#	runtime/src/accounts.rs
#	runtime/src/bank.rs
#	sdk/src/feature_set.rs

* manual fix backport conflicts

Co-authored-by: Tao Zhu <82401714+taozhu-chicago@users.noreply.github.com>
Co-authored-by: Tao Zhu <tao@solana.com>
  • Loading branch information
3 people authored Nov 17, 2022
1 parent ef5b801 commit 8ad9136
Show file tree
Hide file tree
Showing 15 changed files with 854 additions and 169 deletions.
9 changes: 6 additions & 3 deletions core/src/transaction_priority_details.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use {
solana_program_runtime::compute_budget::ComputeBudget,
solana_program_runtime::compute_budget::{self, ComputeBudget},
solana_sdk::{
instruction::CompiledInstruction,
pubkey::Pubkey,
Expand All @@ -23,8 +23,11 @@ pub trait GetTransactionPriorityDetails {
let prioritization_fee_details = compute_budget
.process_instructions(
instructions,
true, // use default units per instruction
true, // don't reject txs that use set compute unit price ix
true, // use default units per instruction
true, // don't reject txs that use set compute unit price ix
false, //transaction priority doesn't care about accounts data size limit,
compute_budget::LoadedAccountsDataLimitType::V0, // transaction priority doesn't
// care about accounts data size limit.
)
.ok()?;
Some(TransactionPriorityDetails {
Expand Down
20 changes: 18 additions & 2 deletions docs/src/developing/programming-model/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ to.
As the transaction is processed compute units are consumed by its
instruction's programs performing operations such as executing BPF instructions,
calling syscalls, etc... When the transaction consumes its entire budget, or
exceeds a bound such as attempting a call stack that is too deep, the runtime
halts the transaction processing and returns an error.
exceeds a bound such as attempting a call stack that is too deep, or loaded
account data exceeds limit, the runtime halts the transaction processing and
returns an error.

The following operations incur a compute cost:

Expand Down Expand Up @@ -143,6 +144,21 @@ let instruction = ComputeBudgetInstruction::set_compute_unit_limit(300_000);
let instruction = ComputeBudgetInstruction::set_compute_unit_price(1);
```

### Accounts data size limit

A transaction should request the maximum bytes of accounts data it is
allowed to load by including a `SetAccountsDataSizeLimit` instruction, requested
limit is capped by `get_max_loaded_accounts_data_limit()`. If no
`SetAccountsDataSizeLimit` is provided, the transaction is defaulted to
have limit of `get_default_loaded_accounts_data_limit()`.

The `ComputeBudgetInstruction::set_accounts_data_size_limit` function can be used
to create this instruction:

```rust
let instruction = ComputeBudgetInstruction::set_accounts_data_size_limit(100_000);
```

## New Features

As Solana evolves, new features or patches may be introduced that changes the
Expand Down
6 changes: 5 additions & 1 deletion ledger/src/blockstore_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4674,6 +4674,7 @@ pub mod tests {
use {
super::*,
serde::{Deserialize, Serialize},
solana_sdk::compute_budget::ComputeBudgetInstruction,
};

#[derive(Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -4713,7 +4714,10 @@ pub mod tests {
account_metas,
);
Transaction::new_signed_with_payer(
&[instruction],
&[
instruction,
ComputeBudgetInstruction::set_accounts_data_size_limit(u32::MAX),
],
Some(&payer.pubkey()),
&[payer],
recent_blockhash,
Expand Down
131 changes: 131 additions & 0 deletions program-runtime/src/compute_budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,33 @@ pub const DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT: u32 = 200_000;
pub const MAX_COMPUTE_UNIT_LIMIT: u32 = 1_400_000;
const MAX_HEAP_FRAME_BYTES: u32 = 256 * 1024;

/// To change `default` and/or `max` values for `accounts_data_size_limit` in the future,
/// add new enum type here, link to feature gate, and implement the enum in
/// `get_default_loaded_accounts_data_limit()` and/or `get_max_loaded_accounts_data_limit()`.
#[derive(Debug)]
pub enum LoadedAccountsDataLimitType {
V0,
// add future versions here
}

/// Get default value of `ComputeBudget::accounts_data_size_limit` if not set specifically. It
/// sets to 10MB initially, may be changed with feature gate.
const DEFAULT_LOADED_ACCOUNTS_DATA_LIMIT: u32 = 10_000_000;
pub fn get_default_loaded_accounts_data_limit(limit_type: &LoadedAccountsDataLimitType) -> u32 {
match limit_type {
LoadedAccountsDataLimitType::V0 => DEFAULT_LOADED_ACCOUNTS_DATA_LIMIT,
}
}
/// Get max value of `ComputeBudget::accounts_data_size_limit`, it caps value user
/// sets via `ComputeBudgetInstruction::set_compute_unit_limit`. It is set to 100MB
/// initially, can be changed with feature gate.
const MAX_LOADED_ACCOUNTS_DATA_LIMIT: u32 = 100_000_000;
pub fn get_max_loaded_accounts_data_limit(limit_type: &LoadedAccountsDataLimitType) -> u32 {
match limit_type {
LoadedAccountsDataLimitType::V0 => MAX_LOADED_ACCOUNTS_DATA_LIMIT,
}
}

#[cfg(RUSTC_WITH_SPECIALIZATION)]
impl ::solana_frozen_abi::abi_example::AbiExample for ComputeBudget {
fn example() -> Self {
Expand All @@ -28,6 +55,8 @@ pub struct ComputeBudget {
/// allowed to consume. Compute units are consumed by program execution,
/// resources they use, etc...
pub compute_unit_limit: u64,
/// Maximum accounts data size, in bytes, that a transaction is allowed to load
pub accounts_data_size_limit: u64,
/// Number of compute units consumed by a log_u64 call
pub log_64_units: u64,
/// Number of compute units consumed by a create_program_address call
Expand Down Expand Up @@ -94,6 +123,7 @@ impl ComputeBudget {
pub fn new(compute_unit_limit: u64) -> Self {
ComputeBudget {
compute_unit_limit,
accounts_data_size_limit: DEFAULT_LOADED_ACCOUNTS_DATA_LIMIT as u64,
log_64_units: 100,
create_program_address_units: 1500,
invoke_units: 1000,
Expand Down Expand Up @@ -128,11 +158,14 @@ impl ComputeBudget {
instructions: impl Iterator<Item = (&'a Pubkey, &'a CompiledInstruction)>,
default_units_per_instruction: bool,
support_set_compute_unit_price_ix: bool,
cap_transaction_accounts_data_size: bool,
loaded_accounts_data_limit_type: LoadedAccountsDataLimitType,
) -> Result<PrioritizationFeeDetails, TransactionError> {
let mut num_non_compute_budget_instructions: usize = 0;
let mut updated_compute_unit_limit = None;
let mut requested_heap_size = None;
let mut prioritization_fee = None;
let mut updated_accounts_data_size_limit = None;

for (i, (program_id, instruction)) in instructions.enumerate() {
if compute_budget::check_id(program_id) {
Expand Down Expand Up @@ -178,6 +211,14 @@ impl ComputeBudget {
prioritization_fee =
Some(PrioritizationFeeType::ComputeUnitPrice(micro_lamports));
}
Ok(ComputeBudgetInstruction::SetAccountsDataSizeLimit(bytes))
if cap_transaction_accounts_data_size =>
{
if updated_accounts_data_size_limit.is_some() {
return Err(duplicate_instruction_error);
}
updated_accounts_data_size_limit = Some(bytes);
}
_ => return Err(invalid_instruction_data_error),
}
} else if i < 3 {
Expand Down Expand Up @@ -234,6 +275,14 @@ impl ComputeBudget {
.unwrap_or(MAX_COMPUTE_UNIT_LIMIT)
.min(MAX_COMPUTE_UNIT_LIMIT) as u64;

self.accounts_data_size_limit = updated_accounts_data_size_limit
.unwrap_or_else(|| {
get_default_loaded_accounts_data_limit(&loaded_accounts_data_limit_type)
})
.min(get_max_loaded_accounts_data_limit(
&loaded_accounts_data_limit_type,
)) as u64;

Ok(prioritization_fee
.map(|fee_type| PrioritizationFeeDetails::new(fee_type, self.compute_unit_limit))
.unwrap_or_default())
Expand Down Expand Up @@ -279,6 +328,8 @@ mod tests {
tx.message().program_instructions_iter(),
true,
$type_change,
true, /*supports cap transaction accounts data size feature*/
LoadedAccountsDataLimitType::V0,
);
assert_eq!($expected_result, result);
assert_eq!(compute_budget, $expected_budget);
Expand Down Expand Up @@ -602,4 +653,84 @@ mod tests {
ComputeBudget::default()
);
}

#[test]
fn test_process_accounts_data_size_limit_instruction() {
test!(
&[],
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
compute_unit_limit: 0,
..ComputeBudget::default()
}
);
test!(
&[
ComputeBudgetInstruction::set_accounts_data_size_limit(1),
Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]),
],
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64,
accounts_data_size_limit: 1,
..ComputeBudget::default()
}
);
test!(
&[
ComputeBudgetInstruction::set_accounts_data_size_limit(
get_max_loaded_accounts_data_limit(&LoadedAccountsDataLimitType::V0) + 1
),
Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]),
],
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64,
accounts_data_size_limit: get_max_loaded_accounts_data_limit(
&LoadedAccountsDataLimitType::V0
) as u64,
..ComputeBudget::default()
}
);
test!(
&[
Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]),
ComputeBudgetInstruction::set_accounts_data_size_limit(
get_max_loaded_accounts_data_limit(&LoadedAccountsDataLimitType::V0)
),
],
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
compute_unit_limit: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64,
accounts_data_size_limit: get_max_loaded_accounts_data_limit(
&LoadedAccountsDataLimitType::V0
) as u64,
..ComputeBudget::default()
}
);
test!(
&[
Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]),
Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]),
Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]),
ComputeBudgetInstruction::set_accounts_data_size_limit(1),
],
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
compute_unit_limit: 3 * DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64,
accounts_data_size_limit: 1,
..ComputeBudget::default()
}
);

test!(
&[
Instruction::new_with_bincode(Pubkey::new_unique(), &0_u8, vec![]),
ComputeBudgetInstruction::set_accounts_data_size_limit(1),
ComputeBudgetInstruction::set_accounts_data_size_limit(1),
],
Err(TransactionError::DuplicateInstruction(2)),
ComputeBudget::default()
);
}
}
4 changes: 2 additions & 2 deletions programs/bpf-loader-tests/tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub async fn setup_test_context() -> ProgramTestContext {

pub async fn assert_ix_error(
context: &mut ProgramTestContext,
ix: Instruction,
ixs: &[Instruction],
additional_payer_keypair: Option<&Keypair>,
expected_err: InstructionError,
assertion_failed_msg: &str,
Expand All @@ -36,7 +36,7 @@ pub async fn assert_ix_error(
}

let transaction = Transaction::new_signed_with_payer(
&[ix],
ixs,
Some(&fee_payer.pubkey()),
&signers,
recent_blockhash,
Expand Down
Loading

0 comments on commit 8ad9136

Please sign in to comment.