Skip to content

Commit

Permalink
feat: add intermediate InstructionDetails struct to gather and store …
Browse files Browse the repository at this point in the history
…deterministic ix info from transaction; Add function to sanitize and convert InstructionDetails to ComputeBudgetLimits; Issue #2352
  • Loading branch information
tao-stones committed Aug 1, 2024
1 parent c7192d3 commit 820295f
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 99 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions programs/sbf/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions runtime-transaction/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ edition = { workspace = true }

[dependencies]
log = { workspace = true }
solana-builtins-default-costs = { workspace = true }
solana-compute-budget = { workspace = true }
solana-sdk = { workspace = true }
thiserror = { workspace = true }
Expand Down
199 changes: 199 additions & 0 deletions runtime-transaction/src/instruction_details.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
use {
solana_builtins_default_costs::BUILTIN_INSTRUCTION_COSTS,
solana_compute_budget::compute_budget_limits::*,
solana_sdk::{
borsh1::try_from_slice_unchecked,
compute_budget::{self, ComputeBudgetInstruction},
instruction::{CompiledInstruction, InstructionError},
pubkey::Pubkey,
saturating_add_assign,
transaction::{Result, TransactionError},
},
std::num::NonZeroU32,
};

#[derive(Default, Debug)]
struct ComputeBudgetInstructionDetails {
// compute-budget instruction details:
// the first field in tuple is instruction index, second field is the unsanitized value set by user
requested_compute_unit_limit: Option<(u8, u32)>,
requested_compute_unit_price: Option<(u8, u64)>,
requested_heap_size: Option<(u8, u32)>,
requested_loaded_accounts_data_size_limit: Option<(u8, u32)>,
count_compute_budget_instructions: u32,
}

impl ComputeBudgetInstructionDetails {
pub fn process_instruction<'a>(
&mut self,
index: u8,
program_id: &'a Pubkey,
instruction: &'a CompiledInstruction,
) -> Result<()> {
if compute_budget::check_id(program_id) {
saturating_add_assign!(self.count_compute_budget_instructions, 1);

let invalid_instruction_data_error =
TransactionError::InstructionError(index, InstructionError::InvalidInstructionData);
let duplicate_instruction_error = TransactionError::DuplicateInstruction(index);

match try_from_slice_unchecked(&instruction.data) {
Ok(ComputeBudgetInstruction::RequestHeapFrame(bytes)) => {
if self.requested_heap_size.is_some() {
return Err(duplicate_instruction_error);
}
if Self::sanitize_requested_heap_size(bytes) {
self.requested_heap_size = Some((index, bytes));
} else {
return Err(invalid_instruction_data_error);
}
}
Ok(ComputeBudgetInstruction::SetComputeUnitLimit(compute_unit_limit)) => {
if self.requested_compute_unit_limit.is_some() {
return Err(duplicate_instruction_error);
}
self.requested_compute_unit_limit = Some((index, compute_unit_limit));
}
Ok(ComputeBudgetInstruction::SetComputeUnitPrice(micro_lamports)) => {
if self.requested_compute_unit_price.is_some() {
return Err(duplicate_instruction_error);
}
self.requested_compute_unit_price = Some((index, micro_lamports));
}
Ok(ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit(bytes)) => {
if self.requested_loaded_accounts_data_size_limit.is_some() {
return Err(duplicate_instruction_error);
}
self.requested_loaded_accounts_data_size_limit = Some((index, bytes));
}
_ => return Err(invalid_instruction_data_error),
}
}

Ok(())
}

fn sanitize_requested_heap_size(bytes: u32) -> bool {
(MIN_HEAP_FRAME_BYTES..=MAX_HEAP_FRAME_BYTES).contains(&bytes) && bytes % 1024 == 0
}
}

#[derive(Default, Debug)]
struct BuiltinInstructionDetails {
// builtin instruction details
sum_builtin_compute_units: u32,
count_builtin_instructions: u32,
count_non_builtin_instructions: u32,
}

impl BuiltinInstructionDetails {
pub fn process_instruction<'a>(
&mut self,
program_id: &'a Pubkey,
_instruction: &'a CompiledInstruction,
) -> Result<()> {
if let Some(builtin_ix_cost) = BUILTIN_INSTRUCTION_COSTS.get(program_id) {
saturating_add_assign!(
self.sum_builtin_compute_units,
u32::try_from(*builtin_ix_cost).unwrap()
);
saturating_add_assign!(self.count_builtin_instructions, 1);
} else {
saturating_add_assign!(self.count_non_builtin_instructions, 1);
}

Ok(())
}
}

/// Information about instructions gathered after scan over transaction;
/// These are "raw" information that suitable for cache and reuse.
#[derive(Default, Debug)]
pub struct InstructionDetails {
compute_budget_instruction_details: ComputeBudgetInstructionDetails,
builtin_instruction_details: BuiltinInstructionDetails,
}

impl InstructionDetails {
pub fn sanitize_and_convert_to_compute_budget_limits(&self) -> Result<ComputeBudgetLimits> {
// Sanitize requested heap size
let updated_heap_bytes = self
.compute_budget_instruction_details
.requested_heap_size
.map_or(MIN_HEAP_FRAME_BYTES, |(_index, requested_heap_size)| {
requested_heap_size
})
.min(MAX_HEAP_FRAME_BYTES);

// Calculate compute unit limit
let compute_unit_limit = self
.compute_budget_instruction_details
.requested_compute_unit_limit
.map_or_else(
|| {
// NOTE: to match current behavior of:
// num_non_compute_budget_instructions * DEFAULT
self.builtin_instruction_details
.count_builtin_instructions
.saturating_add(
self.builtin_instruction_details
.count_non_builtin_instructions,
)
.saturating_sub(
self.compute_budget_instruction_details
.count_compute_budget_instructions,
)
.saturating_mul(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT)
},
|(_index, requested_compute_unit_limit)| requested_compute_unit_limit,
)
.min(MAX_COMPUTE_UNIT_LIMIT);

let compute_unit_price = self
.compute_budget_instruction_details
.requested_compute_unit_price
.map_or(0, |(_index, requested_compute_unit_price)| {
requested_compute_unit_price
});

let loaded_accounts_bytes =
if let Some((_index, requested_loaded_accounts_data_size_limit)) = self
.compute_budget_instruction_details
.requested_loaded_accounts_data_size_limit
{
NonZeroU32::new(requested_loaded_accounts_data_size_limit)
.ok_or(TransactionError::InvalidLoadedAccountsDataSizeLimit)?
} else {
MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES
}
.min(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES);

Ok(ComputeBudgetLimits {
updated_heap_bytes,
compute_unit_limit,
compute_unit_price,
loaded_accounts_bytes,
})
}

pub fn try_from<'a>(
instructions: impl Iterator<Item = (&'a Pubkey, &'a CompiledInstruction)>,
) -> Result<Self> {
let mut compute_budget_instruction_details = ComputeBudgetInstructionDetails::default();
let mut builtin_instruction_details = BuiltinInstructionDetails::default();

for (i, (program_id, instruction)) in instructions.enumerate() {
compute_budget_instruction_details.process_instruction(
i as u8,
program_id,
instruction,
)?;
builtin_instruction_details.process_instruction(program_id, instruction)?;
}

Ok(Self {
compute_budget_instruction_details,
builtin_instruction_details,
})
}
}
110 changes: 11 additions & 99 deletions runtime-transaction/src/instructions_processor.rs
Original file line number Diff line number Diff line change
@@ -1,122 +1,34 @@
use {
crate::instruction_details::*,
solana_compute_budget::compute_budget_limits::*,
solana_sdk::{
borsh1::try_from_slice_unchecked,
compute_budget::{self, ComputeBudgetInstruction},
instruction::{CompiledInstruction, InstructionError},
pubkey::Pubkey,
transaction::TransactionError,
},
std::num::NonZeroU32,
solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey, transaction::Result},
};

/// Processing compute_budget could be part of tx sanitizing, failed to process
/// these instructions will drop the transaction eventually without execution,
/// may as well fail it early.
/// If succeeded, the transaction's specific limits/requests (could be default)
/// are retrieved and returned,
// NOTE - temp adaptor to keep compiler happy for the time being
// all call sites will be updated with this two-step calls, using actual feature-set
// or access runtime-transaction directly
pub fn process_compute_budget_instructions<'a>(
instructions: impl Iterator<Item = (&'a Pubkey, &'a CompiledInstruction)>,
) -> Result<ComputeBudgetLimits, TransactionError> {
let mut num_non_compute_budget_instructions: u32 = 0;
let mut updated_compute_unit_limit = None;
let mut updated_compute_unit_price = None;
let mut requested_heap_size = None;
let mut updated_loaded_accounts_data_size_limit = None;

for (i, (program_id, instruction)) in instructions.enumerate() {
if compute_budget::check_id(program_id) {
let invalid_instruction_data_error = TransactionError::InstructionError(
i as u8,
InstructionError::InvalidInstructionData,
);
let duplicate_instruction_error = TransactionError::DuplicateInstruction(i as u8);

match try_from_slice_unchecked(&instruction.data) {
Ok(ComputeBudgetInstruction::RequestHeapFrame(bytes)) => {
if requested_heap_size.is_some() {
return Err(duplicate_instruction_error);
}
if sanitize_requested_heap_size(bytes) {
requested_heap_size = Some(bytes);
} else {
return Err(invalid_instruction_data_error);
}
}
Ok(ComputeBudgetInstruction::SetComputeUnitLimit(compute_unit_limit)) => {
if updated_compute_unit_limit.is_some() {
return Err(duplicate_instruction_error);
}
updated_compute_unit_limit = Some(compute_unit_limit);
}
Ok(ComputeBudgetInstruction::SetComputeUnitPrice(micro_lamports)) => {
if updated_compute_unit_price.is_some() {
return Err(duplicate_instruction_error);
}
updated_compute_unit_price = Some(micro_lamports);
}
Ok(ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit(bytes)) => {
if updated_loaded_accounts_data_size_limit.is_some() {
return Err(duplicate_instruction_error);
}
updated_loaded_accounts_data_size_limit = Some(
NonZeroU32::new(bytes)
.ok_or(TransactionError::InvalidLoadedAccountsDataSizeLimit)?,
);
}
_ => return Err(invalid_instruction_data_error),
}
} else {
// only include non-request instructions in default max calc
num_non_compute_budget_instructions =
num_non_compute_budget_instructions.saturating_add(1);
}
}

// sanitize limits
let updated_heap_bytes = requested_heap_size
.unwrap_or(MIN_HEAP_FRAME_BYTES) // loader's default heap_size
.min(MAX_HEAP_FRAME_BYTES);

let compute_unit_limit = updated_compute_unit_limit
.unwrap_or_else(|| {
num_non_compute_budget_instructions
.saturating_mul(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT)
})
.min(MAX_COMPUTE_UNIT_LIMIT);

let compute_unit_price = updated_compute_unit_price.unwrap_or(0);

let loaded_accounts_bytes = updated_loaded_accounts_data_size_limit
.unwrap_or(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES)
.min(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES);

Ok(ComputeBudgetLimits {
updated_heap_bytes,
compute_unit_limit,
compute_unit_price,
loaded_accounts_bytes,
})
}

fn sanitize_requested_heap_size(bytes: u32) -> bool {
(MIN_HEAP_FRAME_BYTES..=MAX_HEAP_FRAME_BYTES).contains(&bytes) && bytes % 1024 == 0
) -> Result<ComputeBudgetLimits> {
InstructionDetails::try_from(instructions)?.sanitize_and_convert_to_compute_budget_limits()
}

#[cfg(test)]
mod tests {
use {
super::*,
solana_sdk::{
compute_budget::ComputeBudgetInstruction,
hash::Hash,
instruction::Instruction,
instruction::{Instruction, InstructionError},
message::Message,
pubkey::Pubkey,
signature::Keypair,
signer::Signer,
system_instruction::{self},
transaction::{SanitizedTransaction, Transaction},
transaction::{SanitizedTransaction, Transaction, TransactionError},
},
std::num::NonZeroU32,
};

macro_rules! test {
Expand Down
1 change: 1 addition & 0 deletions runtime-transaction/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![cfg_attr(RUSTC_WITH_SPECIALIZATION, feature(min_specialization))]
#![allow(clippy::arithmetic_side_effects)]

pub mod instruction_details;
pub mod instructions_processor;
pub mod runtime_transaction;
pub mod transaction_meta;

0 comments on commit 820295f

Please sign in to comment.