Stop loading program accounts if program exists in cache (#30703)
* Stop loading program accounts if program exists in cache * load accounts for upgradeable programs * revert loader change to conditionally use program data account * load instruction accounts * generate TransactionExecutorCache from loaded programs * cleanup account_found_and_dep_index variable * address review comments * handle tombstones in loader * unify tombstone constructor * handle multiple tombstones
This commit is contained in:
parent
e7887cfb06
commit
aebc191c38
|
@ -1,5 +1,5 @@
|
||||||
use {
|
use {
|
||||||
crate::loaded_programs::LoadedProgram,
|
crate::loaded_programs::{LoadedProgram, LoadedProgramType},
|
||||||
log::*,
|
log::*,
|
||||||
rand::Rng,
|
rand::Rng,
|
||||||
solana_sdk::{pubkey::Pubkey, saturating_add_assign, slot_history::Slot, stake_history::Epoch},
|
solana_sdk::{pubkey::Pubkey, saturating_add_assign, slot_history::Slot, stake_history::Epoch},
|
||||||
|
@ -42,8 +42,13 @@ impl TransactionExecutorCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_tombstone(&mut self, key: Pubkey, slot: Slot) {
|
pub fn set_tombstone(&mut self, key: Pubkey, slot: Slot) {
|
||||||
self.visible
|
self.visible.insert(
|
||||||
.insert(key, Arc::new(LoadedProgram::new_tombstone(slot)));
|
key,
|
||||||
|
Arc::new(LoadedProgram::new_tombstone(
|
||||||
|
slot,
|
||||||
|
LoadedProgramType::Closed,
|
||||||
|
)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(
|
pub fn set(
|
||||||
|
@ -58,7 +63,13 @@ impl TransactionExecutorCache {
|
||||||
if delay_visibility_of_program_deployment {
|
if delay_visibility_of_program_deployment {
|
||||||
// Place a tombstone in the cache so that
|
// Place a tombstone in the cache so that
|
||||||
// we don't load the new version from the database as it should remain invisible
|
// we don't load the new version from the database as it should remain invisible
|
||||||
self.set_tombstone(key, current_slot);
|
self.visible.insert(
|
||||||
|
key,
|
||||||
|
Arc::new(LoadedProgram::new_tombstone(
|
||||||
|
current_slot,
|
||||||
|
LoadedProgramType::DelayVisibility,
|
||||||
|
)),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
self.visible.insert(key, executor.clone());
|
self.visible.insert(key, executor.clone());
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ use {
|
||||||
solana_rbpf::vm::ContextObject,
|
solana_rbpf::vm::ContextObject,
|
||||||
solana_sdk::{
|
solana_sdk::{
|
||||||
account::{AccountSharedData, ReadableAccount},
|
account::{AccountSharedData, ReadableAccount},
|
||||||
bpf_loader_upgradeable::{self, UpgradeableLoaderState},
|
|
||||||
feature_set::{
|
feature_set::{
|
||||||
enable_early_verification_of_account_modifications, native_programs_consume_cu,
|
enable_early_verification_of_account_modifications, native_programs_consume_cu,
|
||||||
FeatureSet,
|
FeatureSet,
|
||||||
|
@ -624,37 +623,11 @@ impl<'a> InvokeContext<'a> {
|
||||||
ic_msg!(self, "Account {} is not executable", callee_program_id);
|
ic_msg!(self, "Account {} is not executable", callee_program_id);
|
||||||
return Err(InstructionError::AccountNotExecutable);
|
return Err(InstructionError::AccountNotExecutable);
|
||||||
}
|
}
|
||||||
let mut program_indices = vec![];
|
|
||||||
if borrowed_program_account.get_owner() == &bpf_loader_upgradeable::id() {
|
|
||||||
if let UpgradeableLoaderState::Program {
|
|
||||||
programdata_address,
|
|
||||||
} = borrowed_program_account.get_state()?
|
|
||||||
{
|
|
||||||
if let Some(programdata_account_index) = self
|
|
||||||
.transaction_context
|
|
||||||
.find_index_of_program_account(&programdata_address)
|
|
||||||
{
|
|
||||||
program_indices.push(programdata_account_index);
|
|
||||||
} else {
|
|
||||||
ic_msg!(
|
|
||||||
self,
|
|
||||||
"Unknown upgradeable programdata account {}",
|
|
||||||
programdata_address,
|
|
||||||
);
|
|
||||||
return Err(InstructionError::MissingAccount);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ic_msg!(
|
|
||||||
self,
|
|
||||||
"Invalid upgradeable program account {}",
|
|
||||||
callee_program_id,
|
|
||||||
);
|
|
||||||
return Err(InstructionError::MissingAccount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
program_indices.push(borrowed_program_account.get_index_in_transaction());
|
|
||||||
|
|
||||||
Ok((instruction_accounts, program_indices))
|
Ok((
|
||||||
|
instruction_accounts,
|
||||||
|
vec![borrowed_program_account.get_index_in_transaction()],
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes an instruction and returns how many compute units were used
|
/// Processes an instruction and returns how many compute units were used
|
||||||
|
|
|
@ -58,7 +58,9 @@ pub trait WorkingSlot {
|
||||||
pub enum LoadedProgramType {
|
pub enum LoadedProgramType {
|
||||||
/// Tombstone for undeployed, closed or unloadable programs
|
/// Tombstone for undeployed, closed or unloadable programs
|
||||||
#[default]
|
#[default]
|
||||||
Invalid,
|
FailedVerification,
|
||||||
|
Closed,
|
||||||
|
DelayVisibility,
|
||||||
LegacyV0(VerifiedExecutable<RequisiteVerifier, InvokeContext<'static>>),
|
LegacyV0(VerifiedExecutable<RequisiteVerifier, InvokeContext<'static>>),
|
||||||
LegacyV1(VerifiedExecutable<RequisiteVerifier, InvokeContext<'static>>),
|
LegacyV1(VerifiedExecutable<RequisiteVerifier, InvokeContext<'static>>),
|
||||||
Typed(VerifiedExecutable<RequisiteVerifier, InvokeContext<'static>>),
|
Typed(VerifiedExecutable<RequisiteVerifier, InvokeContext<'static>>),
|
||||||
|
@ -68,7 +70,11 @@ pub enum LoadedProgramType {
|
||||||
impl Debug for LoadedProgramType {
|
impl Debug for LoadedProgramType {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
LoadedProgramType::Invalid => write!(f, "LoadedProgramType::Invalid"),
|
LoadedProgramType::FailedVerification => {
|
||||||
|
write!(f, "LoadedProgramType::FailedVerification")
|
||||||
|
}
|
||||||
|
LoadedProgramType::Closed => write!(f, "LoadedProgramType::Closed"),
|
||||||
|
LoadedProgramType::DelayVisibility => write!(f, "LoadedProgramType::DelayVisibility"),
|
||||||
LoadedProgramType::LegacyV0(_) => write!(f, "LoadedProgramType::LegacyV0"),
|
LoadedProgramType::LegacyV0(_) => write!(f, "LoadedProgramType::LegacyV0"),
|
||||||
LoadedProgramType::LegacyV1(_) => write!(f, "LoadedProgramType::LegacyV1"),
|
LoadedProgramType::LegacyV1(_) => write!(f, "LoadedProgramType::LegacyV1"),
|
||||||
LoadedProgramType::Typed(_) => write!(f, "LoadedProgramType::Typed"),
|
LoadedProgramType::Typed(_) => write!(f, "LoadedProgramType::Typed"),
|
||||||
|
@ -190,18 +196,25 @@ impl LoadedProgram {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_tombstone(slot: Slot) -> Self {
|
pub fn new_tombstone(slot: Slot, reason: LoadedProgramType) -> Self {
|
||||||
Self {
|
let tombstone = Self {
|
||||||
program: LoadedProgramType::Invalid,
|
program: reason,
|
||||||
account_size: 0,
|
account_size: 0,
|
||||||
deployment_slot: slot,
|
deployment_slot: slot,
|
||||||
effective_slot: slot,
|
effective_slot: slot,
|
||||||
usage_counter: AtomicU64::default(),
|
usage_counter: AtomicU64::default(),
|
||||||
}
|
};
|
||||||
|
debug_assert!(tombstone.is_tombstone());
|
||||||
|
tombstone
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_tombstone(&self) -> bool {
|
pub fn is_tombstone(&self) -> bool {
|
||||||
matches!(self.program, LoadedProgramType::Invalid)
|
matches!(
|
||||||
|
self.program,
|
||||||
|
LoadedProgramType::FailedVerification
|
||||||
|
| LoadedProgramType::Closed
|
||||||
|
| LoadedProgramType::DelayVisibility
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +279,15 @@ impl LoadedPrograms {
|
||||||
let existing = second_level
|
let existing = second_level
|
||||||
.get(index)
|
.get(index)
|
||||||
.expect("Missing entry, even though position was found");
|
.expect("Missing entry, even though position was found");
|
||||||
assert!(
|
if existing.is_tombstone()
|
||||||
|
&& entry.is_tombstone()
|
||||||
|
&& existing.deployment_slot == entry.deployment_slot
|
||||||
|
{
|
||||||
|
// If there's already a tombstone for the program at the given slot, let's return
|
||||||
|
// the existing entry instead of adding another.
|
||||||
|
return existing.clone();
|
||||||
|
}
|
||||||
|
debug_assert!(
|
||||||
existing.deployment_slot != entry.deployment_slot
|
existing.deployment_slot != entry.deployment_slot
|
||||||
|| existing.effective_slot != entry.effective_slot
|
|| existing.effective_slot != entry.effective_slot
|
||||||
);
|
);
|
||||||
|
@ -395,7 +416,13 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_tombstone(cache: &mut LoadedPrograms, key: Pubkey, slot: Slot) -> Arc<LoadedProgram> {
|
fn set_tombstone(cache: &mut LoadedPrograms, key: Pubkey, slot: Slot) -> Arc<LoadedProgram> {
|
||||||
cache.assign_program(key, Arc::new(LoadedProgram::new_tombstone(slot)))
|
cache.assign_program(
|
||||||
|
key,
|
||||||
|
Arc::new(LoadedProgram::new_tombstone(
|
||||||
|
slot,
|
||||||
|
LoadedProgramType::FailedVerification,
|
||||||
|
)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -637,14 +664,17 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_tombstone() {
|
fn test_tombstone() {
|
||||||
let tombstone = LoadedProgram::new_tombstone(0);
|
let tombstone = LoadedProgram::new_tombstone(0, LoadedProgramType::FailedVerification);
|
||||||
assert!(matches!(tombstone.program, LoadedProgramType::Invalid));
|
assert!(matches!(
|
||||||
|
tombstone.program,
|
||||||
|
LoadedProgramType::FailedVerification
|
||||||
|
));
|
||||||
assert!(tombstone.is_tombstone());
|
assert!(tombstone.is_tombstone());
|
||||||
assert_eq!(tombstone.deployment_slot, 0);
|
assert_eq!(tombstone.deployment_slot, 0);
|
||||||
assert_eq!(tombstone.effective_slot, 0);
|
assert_eq!(tombstone.effective_slot, 0);
|
||||||
|
|
||||||
let tombstone = LoadedProgram::new_tombstone(100);
|
let tombstone = LoadedProgram::new_tombstone(100, LoadedProgramType::Closed);
|
||||||
assert!(matches!(tombstone.program, LoadedProgramType::Invalid));
|
assert!(matches!(tombstone.program, LoadedProgramType::Closed));
|
||||||
assert!(tombstone.is_tombstone());
|
assert!(tombstone.is_tombstone());
|
||||||
assert_eq!(tombstone.deployment_slot, 100);
|
assert_eq!(tombstone.deployment_slot, 100);
|
||||||
assert_eq!(tombstone.effective_slot, 100);
|
assert_eq!(tombstone.effective_slot, 100);
|
||||||
|
@ -835,7 +865,7 @@ mod tests {
|
||||||
usage_counter: AtomicU64,
|
usage_counter: AtomicU64,
|
||||||
) -> Arc<LoadedProgram> {
|
) -> Arc<LoadedProgram> {
|
||||||
Arc::new(LoadedProgram {
|
Arc::new(LoadedProgram {
|
||||||
program: LoadedProgramType::Invalid,
|
program: LoadedProgramType::FailedVerification,
|
||||||
account_size: 0,
|
account_size: 0,
|
||||||
deployment_slot,
|
deployment_slot,
|
||||||
effective_slot,
|
effective_slot,
|
||||||
|
|
|
@ -135,7 +135,7 @@ pub fn load_program_from_bytes(
|
||||||
Ok(loaded_program)
|
Ok(loaded_program)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_programdata_offset_and_depoyment_offset(
|
fn get_programdata_offset_and_deployment_offset(
|
||||||
log_collector: &Option<Rc<RefCell<LogCollector>>>,
|
log_collector: &Option<Rc<RefCell<LogCollector>>>,
|
||||||
program: &BorrowedAccount,
|
program: &BorrowedAccount,
|
||||||
programdata: &BorrowedAccount,
|
programdata: &BorrowedAccount,
|
||||||
|
@ -181,14 +181,10 @@ pub fn load_program_from_account(
|
||||||
return Err(InstructionError::IncorrectProgramId);
|
return Err(InstructionError::IncorrectProgramId);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (programdata_offset, deployment_slot) =
|
|
||||||
get_programdata_offset_and_depoyment_offset(&log_collector, program, programdata)?;
|
|
||||||
|
|
||||||
if let Some(ref tx_executor_cache) = tx_executor_cache {
|
if let Some(ref tx_executor_cache) = tx_executor_cache {
|
||||||
if let Some(loaded_program) = tx_executor_cache.get(program.get_key()) {
|
if let Some(loaded_program) = tx_executor_cache.get(program.get_key()) {
|
||||||
if loaded_program.is_tombstone() {
|
if loaded_program.is_tombstone() {
|
||||||
// We cached that the Executor does not exist, abort
|
// We cached that the Executor does not exist, abort
|
||||||
// This case can only happen once delay_visibility_of_program_deployment is active.
|
|
||||||
return Err(InstructionError::InvalidAccountData);
|
return Err(InstructionError::InvalidAccountData);
|
||||||
}
|
}
|
||||||
// Executor exists and is cached, use it
|
// Executor exists and is cached, use it
|
||||||
|
@ -196,6 +192,9 @@ pub fn load_program_from_account(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (programdata_offset, deployment_slot) =
|
||||||
|
get_programdata_offset_and_deployment_offset(&log_collector, program, programdata)?;
|
||||||
|
|
||||||
let programdata_size = if programdata_offset != 0 {
|
let programdata_size = if programdata_offset != 0 {
|
||||||
programdata.get_data().len()
|
programdata.get_data().len()
|
||||||
} else {
|
} else {
|
||||||
|
@ -556,14 +555,16 @@ fn process_instruction_common(
|
||||||
ic_logger_msg!(log_collector, "Program is not executable");
|
ic_logger_msg!(log_collector, "Program is not executable");
|
||||||
return Err(InstructionError::IncorrectProgramId);
|
return Err(InstructionError::IncorrectProgramId);
|
||||||
}
|
}
|
||||||
|
|
||||||
let programdata_account = if bpf_loader_upgradeable::check_id(program_account.get_owner()) {
|
let programdata_account = if bpf_loader_upgradeable::check_id(program_account.get_owner()) {
|
||||||
let programdata_account = instruction_context.try_borrow_program_account(
|
instruction_context
|
||||||
transaction_context,
|
.try_borrow_program_account(
|
||||||
instruction_context
|
transaction_context,
|
||||||
.get_number_of_program_accounts()
|
instruction_context
|
||||||
.saturating_sub(2),
|
.get_number_of_program_accounts()
|
||||||
)?;
|
.saturating_sub(2),
|
||||||
Some(programdata_account)
|
)
|
||||||
|
.ok()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
@ -591,7 +592,9 @@ fn process_instruction_common(
|
||||||
|
|
||||||
executor.usage_counter.fetch_add(1, Ordering::Relaxed);
|
executor.usage_counter.fetch_add(1, Ordering::Relaxed);
|
||||||
match &executor.program {
|
match &executor.program {
|
||||||
LoadedProgramType::Invalid => Err(InstructionError::InvalidAccountData),
|
LoadedProgramType::FailedVerification
|
||||||
|
| LoadedProgramType::Closed
|
||||||
|
| LoadedProgramType::DelayVisibility => Err(InstructionError::InvalidAccountData),
|
||||||
LoadedProgramType::LegacyV0(executable) => execute(executable, invoke_context),
|
LoadedProgramType::LegacyV0(executable) => execute(executable, invoke_context),
|
||||||
LoadedProgramType::LegacyV1(executable) => execute(executable, invoke_context),
|
LoadedProgramType::LegacyV1(executable) => execute(executable, invoke_context),
|
||||||
_ => Err(InstructionError::IncorrectProgramId),
|
_ => Err(InstructionError::IncorrectProgramId),
|
||||||
|
|
|
@ -590,7 +590,9 @@ pub fn process_instruction(invoke_context: &mut InvokeContext) -> Result<(), Ins
|
||||||
drop(program);
|
drop(program);
|
||||||
loaded_program.usage_counter.fetch_add(1, Ordering::Relaxed);
|
loaded_program.usage_counter.fetch_add(1, Ordering::Relaxed);
|
||||||
match &loaded_program.program {
|
match &loaded_program.program {
|
||||||
LoadedProgramType::Invalid => Err(InstructionError::InvalidAccountData),
|
LoadedProgramType::FailedVerification
|
||||||
|
| LoadedProgramType::Closed
|
||||||
|
| LoadedProgramType::DelayVisibility => Err(InstructionError::InvalidAccountData),
|
||||||
LoadedProgramType::Typed(executable) => execute(invoke_context, executable),
|
LoadedProgramType::Typed(executable) => execute(invoke_context, executable),
|
||||||
_ => Err(InstructionError::IncorrectProgramId),
|
_ => Err(InstructionError::IncorrectProgramId),
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,16 +24,21 @@ use {
|
||||||
transaction_error_metrics::TransactionErrorMetrics,
|
transaction_error_metrics::TransactionErrorMetrics,
|
||||||
},
|
},
|
||||||
dashmap::DashMap,
|
dashmap::DashMap,
|
||||||
|
itertools::Itertools,
|
||||||
log::*,
|
log::*,
|
||||||
solana_address_lookup_table_program::{error::AddressLookupError, state::AddressLookupTable},
|
solana_address_lookup_table_program::{error::AddressLookupError, state::AddressLookupTable},
|
||||||
solana_program_runtime::compute_budget::{self, ComputeBudget},
|
solana_program_runtime::{
|
||||||
|
compute_budget::{self, ComputeBudget},
|
||||||
|
loaded_programs::{LoadedProgram, LoadedProgramType},
|
||||||
|
},
|
||||||
solana_sdk::{
|
solana_sdk::{
|
||||||
account::{Account, AccountSharedData, ReadableAccount, WritableAccount},
|
account::{Account, AccountSharedData, ReadableAccount, WritableAccount},
|
||||||
account_utils::StateMut,
|
account_utils::StateMut,
|
||||||
bpf_loader_upgradeable::{self, UpgradeableLoaderState},
|
bpf_loader_upgradeable,
|
||||||
clock::{BankId, Slot},
|
clock::{BankId, Slot},
|
||||||
feature_set::{
|
feature_set::{
|
||||||
self, add_set_tx_loaded_accounts_data_size_instruction, enable_request_heap_frame_ix,
|
self, add_set_tx_loaded_accounts_data_size_instruction,
|
||||||
|
delay_visibility_of_program_deployment, enable_request_heap_frame_ix,
|
||||||
include_loaded_accounts_data_size_in_fee_calculation,
|
include_loaded_accounts_data_size_in_fee_calculation,
|
||||||
remove_congestion_multiplier_from_fee_calculation, remove_deprecated_request_unit_ix,
|
remove_congestion_multiplier_from_fee_calculation, remove_deprecated_request_unit_ix,
|
||||||
use_default_units_in_fee_calculation, FeatureSet,
|
use_default_units_in_fee_calculation, FeatureSet,
|
||||||
|
@ -287,6 +292,38 @@ impl Accounts {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn account_shared_data_from_program(
|
||||||
|
key: &Pubkey,
|
||||||
|
feature_set: &FeatureSet,
|
||||||
|
program: &LoadedProgram,
|
||||||
|
program_accounts: &HashMap<Pubkey, &Pubkey>,
|
||||||
|
) -> Result<AccountSharedData> {
|
||||||
|
// Check for tombstone
|
||||||
|
// Ignoring the tombstone here for now. The loader will catch this condition and return
|
||||||
|
// error.
|
||||||
|
let _ignore = match &program.program {
|
||||||
|
LoadedProgramType::FailedVerification | LoadedProgramType::Closed => {
|
||||||
|
Err(TransactionError::InvalidProgramForExecution)
|
||||||
|
}
|
||||||
|
LoadedProgramType::DelayVisibility => {
|
||||||
|
debug_assert!(feature_set.is_active(&delay_visibility_of_program_deployment::id()));
|
||||||
|
Err(TransactionError::InvalidProgramForExecution)
|
||||||
|
}
|
||||||
|
_ => Ok(()),
|
||||||
|
};
|
||||||
|
// It's an executable program account. The program is already loaded in the cache.
|
||||||
|
// So the account data is not needed. Return a dummy AccountSharedData with meta
|
||||||
|
// information.
|
||||||
|
let mut program_account = AccountSharedData::default();
|
||||||
|
let program_owner = program_accounts
|
||||||
|
.get(key)
|
||||||
|
.ok_or(TransactionError::AccountNotFound)?;
|
||||||
|
program_account.set_owner(**program_owner);
|
||||||
|
program_account.set_executable(true);
|
||||||
|
Ok(program_account)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn load_transaction_accounts(
|
fn load_transaction_accounts(
|
||||||
&self,
|
&self,
|
||||||
ancestors: &Ancestors,
|
ancestors: &Ancestors,
|
||||||
|
@ -296,6 +333,8 @@ impl Accounts {
|
||||||
rent_collector: &RentCollector,
|
rent_collector: &RentCollector,
|
||||||
feature_set: &FeatureSet,
|
feature_set: &FeatureSet,
|
||||||
account_overrides: Option<&AccountOverrides>,
|
account_overrides: Option<&AccountOverrides>,
|
||||||
|
program_accounts: &HashMap<Pubkey, &Pubkey>,
|
||||||
|
loaded_programs: &HashMap<Pubkey, Arc<LoadedProgram>>,
|
||||||
) -> Result<LoadedTransaction> {
|
) -> Result<LoadedTransaction> {
|
||||||
// NOTE: this check will never fail because `tx` is sanitized
|
// NOTE: this check will never fail because `tx` is sanitized
|
||||||
if tx.signatures().is_empty() && fee != 0 {
|
if tx.signatures().is_empty() && fee != 0 {
|
||||||
|
@ -308,7 +347,7 @@ impl Accounts {
|
||||||
let mut tx_rent: TransactionRent = 0;
|
let mut tx_rent: TransactionRent = 0;
|
||||||
let message = tx.message();
|
let message = tx.message();
|
||||||
let account_keys = message.account_keys();
|
let account_keys = message.account_keys();
|
||||||
let mut account_found_and_dep_index = Vec::with_capacity(account_keys.len());
|
let mut accounts_found = Vec::with_capacity(account_keys.len());
|
||||||
let mut account_deps = Vec::with_capacity(account_keys.len());
|
let mut account_deps = Vec::with_capacity(account_keys.len());
|
||||||
let mut rent_debits = RentDebits::default();
|
let mut rent_debits = RentDebits::default();
|
||||||
|
|
||||||
|
@ -319,12 +358,18 @@ impl Accounts {
|
||||||
Self::get_requested_loaded_accounts_data_size_limit(tx, feature_set)?;
|
Self::get_requested_loaded_accounts_data_size_limit(tx, feature_set)?;
|
||||||
let mut accumulated_accounts_data_size: usize = 0;
|
let mut accumulated_accounts_data_size: usize = 0;
|
||||||
|
|
||||||
|
let instruction_accounts = message
|
||||||
|
.instructions()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|instruction| &instruction.accounts)
|
||||||
|
.unique()
|
||||||
|
.collect::<Vec<&u8>>();
|
||||||
|
|
||||||
let mut accounts = account_keys
|
let mut accounts = account_keys
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, key)| {
|
.map(|(i, key)| {
|
||||||
let mut account_found = true;
|
let mut account_found = true;
|
||||||
let mut account_dep_index = None;
|
|
||||||
#[allow(clippy::collapsible_else_if)]
|
#[allow(clippy::collapsible_else_if)]
|
||||||
let account = if solana_sdk::sysvar::instructions::check_id(key) {
|
let account = if solana_sdk::sysvar::instructions::check_id(key) {
|
||||||
Self::construct_instructions_account(
|
Self::construct_instructions_account(
|
||||||
|
@ -333,10 +378,30 @@ impl Accounts {
|
||||||
.is_active(&feature_set::instructions_sysvar_owned_by_sysvar::id()),
|
.is_active(&feature_set::instructions_sysvar_owned_by_sysvar::id()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let (mut account, rent) = if let Some(account_override) =
|
let instruction_account = u8::try_from(i)
|
||||||
|
.map(|i| instruction_accounts.contains(&&i))
|
||||||
|
.unwrap_or(false);
|
||||||
|
let (account_size, mut account, rent) = if let Some(account_override) =
|
||||||
account_overrides.and_then(|overrides| overrides.get(key))
|
account_overrides.and_then(|overrides| overrides.get(key))
|
||||||
{
|
{
|
||||||
(account_override.clone(), 0)
|
(account_override.data().len(), account_override.clone(), 0)
|
||||||
|
} else if let Some(program) = (!instruction_account && !message.is_writable(i))
|
||||||
|
.then_some(())
|
||||||
|
.and_then(|_| loaded_programs.get(key))
|
||||||
|
{
|
||||||
|
// This condition block does special handling for accounts that are passed
|
||||||
|
// as instruction account to any of the instructions in the transaction.
|
||||||
|
// It's been noticed that some programs are reading other program accounts
|
||||||
|
// (that are passed to the program as instruction accounts). So such accounts
|
||||||
|
// are needed to be loaded even though corresponding compiled program may
|
||||||
|
// already be present in the cache.
|
||||||
|
Self::account_shared_data_from_program(
|
||||||
|
key,
|
||||||
|
feature_set,
|
||||||
|
program,
|
||||||
|
program_accounts,
|
||||||
|
)
|
||||||
|
.map(|program_account| (program.account_size, program_account, 0))?
|
||||||
} else {
|
} else {
|
||||||
self.accounts_db
|
self.accounts_db
|
||||||
.load_with_fixed_root(ancestors, key)
|
.load_with_fixed_root(ancestors, key)
|
||||||
|
@ -350,9 +415,9 @@ impl Accounts {
|
||||||
set_exempt_rent_epoch_max,
|
set_exempt_rent_epoch_max,
|
||||||
)
|
)
|
||||||
.rent_amount;
|
.rent_amount;
|
||||||
(account, rent_due)
|
(account.data().len(), account, rent_due)
|
||||||
} else {
|
} else {
|
||||||
(account, 0)
|
(account.data().len(), account, 0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
|
@ -364,12 +429,12 @@ impl Accounts {
|
||||||
// with this field already set would allow us to skip rent collection for these accounts.
|
// with this field already set would allow us to skip rent collection for these accounts.
|
||||||
default_account.set_rent_epoch(u64::MAX);
|
default_account.set_rent_epoch(u64::MAX);
|
||||||
}
|
}
|
||||||
(default_account, 0)
|
(default_account.data().len(), default_account, 0)
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
Self::accumulate_and_check_loaded_account_data_size(
|
Self::accumulate_and_check_loaded_account_data_size(
|
||||||
&mut accumulated_accounts_data_size,
|
&mut accumulated_accounts_data_size,
|
||||||
account.data().len(),
|
account_size,
|
||||||
requested_loaded_accounts_data_size_limit,
|
requested_loaded_accounts_data_size_limit,
|
||||||
error_counters,
|
error_counters,
|
||||||
)?;
|
)?;
|
||||||
|
@ -397,36 +462,6 @@ impl Accounts {
|
||||||
error_counters.invalid_writable_account += 1;
|
error_counters.invalid_writable_account += 1;
|
||||||
return Err(TransactionError::InvalidWritableAccount);
|
return Err(TransactionError::InvalidWritableAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
if account.executable() {
|
|
||||||
// The upgradeable loader requires the derived ProgramData account
|
|
||||||
if let Ok(UpgradeableLoaderState::Program {
|
|
||||||
programdata_address,
|
|
||||||
}) = account.state()
|
|
||||||
{
|
|
||||||
if let Some((programdata_account, _)) = self
|
|
||||||
.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);
|
|
||||||
account_deps.push((programdata_address, programdata_account));
|
|
||||||
} else {
|
|
||||||
error_counters.account_not_found += 1;
|
|
||||||
return Err(TransactionError::ProgramAccountNotFound);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error_counters.invalid_program_for_execution += 1;
|
|
||||||
return Err(TransactionError::InvalidProgramForExecution);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if account.executable() && message.is_writable(i) {
|
} else if account.executable() && message.is_writable(i) {
|
||||||
error_counters.invalid_writable_account += 1;
|
error_counters.invalid_writable_account += 1;
|
||||||
return Err(TransactionError::InvalidWritableAccount);
|
return Err(TransactionError::InvalidWritableAccount);
|
||||||
|
@ -438,7 +473,7 @@ impl Accounts {
|
||||||
account
|
account
|
||||||
};
|
};
|
||||||
|
|
||||||
account_found_and_dep_index.push((account_found, account_dep_index));
|
accounts_found.push(account_found);
|
||||||
Ok((*key, account))
|
Ok((*key, account))
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>>>()?;
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
@ -468,9 +503,7 @@ impl Accounts {
|
||||||
let (program_id, program_account) = accounts
|
let (program_id, program_account) = accounts
|
||||||
.get(program_index)
|
.get(program_index)
|
||||||
.ok_or(TransactionError::ProgramAccountNotFound)?;
|
.ok_or(TransactionError::ProgramAccountNotFound)?;
|
||||||
let (account_found, account_dep_index) = account_found_and_dep_index
|
let account_found = accounts_found.get(program_index).unwrap_or(&true);
|
||||||
.get(program_index)
|
|
||||||
.unwrap_or(&(true, None));
|
|
||||||
if native_loader::check_id(program_id) {
|
if native_loader::check_id(program_id) {
|
||||||
return Ok(account_indices);
|
return Ok(account_indices);
|
||||||
}
|
}
|
||||||
|
@ -483,9 +516,6 @@ impl Accounts {
|
||||||
return Err(TransactionError::InvalidProgramForExecution);
|
return Err(TransactionError::InvalidProgramForExecution);
|
||||||
}
|
}
|
||||||
account_indices.insert(0, program_index as IndexOfAccount);
|
account_indices.insert(0, program_index as IndexOfAccount);
|
||||||
if let Some(account_index) = account_dep_index {
|
|
||||||
account_indices.insert(0, *account_index);
|
|
||||||
}
|
|
||||||
let owner_id = program_account.owner();
|
let owner_id = program_account.owner();
|
||||||
if native_loader::check_id(owner_id) {
|
if native_loader::check_id(owner_id) {
|
||||||
return Ok(account_indices);
|
return Ok(account_indices);
|
||||||
|
@ -641,6 +671,8 @@ impl Accounts {
|
||||||
feature_set: &FeatureSet,
|
feature_set: &FeatureSet,
|
||||||
fee_structure: &FeeStructure,
|
fee_structure: &FeeStructure,
|
||||||
account_overrides: Option<&AccountOverrides>,
|
account_overrides: Option<&AccountOverrides>,
|
||||||
|
program_accounts: &HashMap<Pubkey, &Pubkey>,
|
||||||
|
loaded_programs: &HashMap<Pubkey, Arc<LoadedProgram>>,
|
||||||
) -> Vec<TransactionLoadResult> {
|
) -> Vec<TransactionLoadResult> {
|
||||||
txs.iter()
|
txs.iter()
|
||||||
.zip(lock_results)
|
.zip(lock_results)
|
||||||
|
@ -676,6 +708,8 @@ impl Accounts {
|
||||||
rent_collector,
|
rent_collector,
|
||||||
feature_set,
|
feature_set,
|
||||||
account_overrides,
|
account_overrides,
|
||||||
|
program_accounts,
|
||||||
|
loaded_programs,
|
||||||
) {
|
) {
|
||||||
Ok(loaded_transaction) => loaded_transaction,
|
Ok(loaded_transaction) => loaded_transaction,
|
||||||
Err(e) => return (Err(e), None),
|
Err(e) => return (Err(e), None),
|
||||||
|
@ -1416,6 +1450,7 @@ mod tests {
|
||||||
solana_program_runtime::executor_cache::TransactionExecutorCache,
|
solana_program_runtime::executor_cache::TransactionExecutorCache,
|
||||||
solana_sdk::{
|
solana_sdk::{
|
||||||
account::{AccountSharedData, WritableAccount},
|
account::{AccountSharedData, WritableAccount},
|
||||||
|
bpf_loader_upgradeable::UpgradeableLoaderState,
|
||||||
epoch_schedule::EpochSchedule,
|
epoch_schedule::EpochSchedule,
|
||||||
genesis_config::ClusterType,
|
genesis_config::ClusterType,
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
|
@ -1500,6 +1535,8 @@ mod tests {
|
||||||
feature_set,
|
feature_set,
|
||||||
fee_structure,
|
fee_structure,
|
||||||
None,
|
None,
|
||||||
|
&HashMap::new(),
|
||||||
|
&HashMap::new(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2542,10 +2579,6 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.accounts[result.program_indices[0][1] as usize],
|
result.accounts[result.program_indices[0][1] as usize],
|
||||||
accounts[4]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
result.accounts[result.program_indices[0][2] as usize],
|
|
||||||
accounts[3]
|
accounts[3]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3272,6 +3305,8 @@ mod tests {
|
||||||
&FeatureSet::all_enabled(),
|
&FeatureSet::all_enabled(),
|
||||||
&FeeStructure::default(),
|
&FeeStructure::default(),
|
||||||
account_overrides,
|
account_overrides,
|
||||||
|
&HashMap::new(),
|
||||||
|
&HashMap::new(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,9 @@ use {
|
||||||
compute_budget::{self, ComputeBudget},
|
compute_budget::{self, ComputeBudget},
|
||||||
executor_cache::{BankExecutorCache, TransactionExecutorCache, MAX_CACHED_EXECUTORS},
|
executor_cache::{BankExecutorCache, TransactionExecutorCache, MAX_CACHED_EXECUTORS},
|
||||||
invoke_context::{BuiltinProgram, ProcessInstructionWithContext},
|
invoke_context::{BuiltinProgram, ProcessInstructionWithContext},
|
||||||
loaded_programs::{LoadedProgram, LoadedProgramEntry, LoadedPrograms, WorkingSlot},
|
loaded_programs::{
|
||||||
|
LoadedProgram, LoadedProgramEntry, LoadedProgramType, LoadedPrograms, WorkingSlot,
|
||||||
|
},
|
||||||
log_collector::LogCollector,
|
log_collector::LogCollector,
|
||||||
sysvar_cache::SysvarCache,
|
sysvar_cache::SysvarCache,
|
||||||
timings::{ExecuteTimingType, ExecuteTimings},
|
timings::{ExecuteTimingType, ExecuteTimings},
|
||||||
|
@ -4043,6 +4045,7 @@ impl Bank {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get any cached executors needed by the transaction
|
/// Get any cached executors needed by the transaction
|
||||||
|
#[cfg(test)]
|
||||||
fn get_tx_executor_cache(
|
fn get_tx_executor_cache(
|
||||||
&self,
|
&self,
|
||||||
accounts: &[TransactionAccount],
|
accounts: &[TransactionAccount],
|
||||||
|
@ -4197,15 +4200,8 @@ impl Bank {
|
||||||
timings: &mut ExecuteTimings,
|
timings: &mut ExecuteTimings,
|
||||||
error_counters: &mut TransactionErrorMetrics,
|
error_counters: &mut TransactionErrorMetrics,
|
||||||
log_messages_bytes_limit: Option<usize>,
|
log_messages_bytes_limit: Option<usize>,
|
||||||
|
tx_executor_cache: Rc<RefCell<TransactionExecutorCache>>,
|
||||||
) -> TransactionExecutionResult {
|
) -> TransactionExecutionResult {
|
||||||
let mut get_tx_executor_cache_time = Measure::start("get_tx_executor_cache_time");
|
|
||||||
let tx_executor_cache = self.get_tx_executor_cache(&loaded_transaction.accounts);
|
|
||||||
get_tx_executor_cache_time.stop();
|
|
||||||
saturating_add_assign!(
|
|
||||||
timings.execute_accessories.get_executors_us,
|
|
||||||
get_tx_executor_cache_time.as_us()
|
|
||||||
);
|
|
||||||
|
|
||||||
let prev_accounts_data_len = self.load_accounts_data_size();
|
let prev_accounts_data_len = self.load_accounts_data_size();
|
||||||
let transaction_accounts = std::mem::take(&mut loaded_transaction.accounts);
|
let transaction_accounts = std::mem::take(&mut loaded_transaction.accounts);
|
||||||
let mut transaction_context = TransactionContext::new(
|
let mut transaction_context = TransactionContext::new(
|
||||||
|
@ -4439,11 +4435,13 @@ impl Bank {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Create a tombstone for the program in the cache
|
// Create a tombstone for the program in the cache
|
||||||
debug!("Failed to load program {}, error {:?}", pubkey, e);
|
debug!("Failed to load program {}, error {:?}", pubkey, e);
|
||||||
let tombstone = self
|
let tombstone = self.loaded_programs_cache.write().unwrap().assign_program(
|
||||||
.loaded_programs_cache
|
*pubkey,
|
||||||
.write()
|
Arc::new(LoadedProgram::new_tombstone(
|
||||||
.unwrap()
|
self.slot,
|
||||||
.assign_program(*pubkey, Arc::new(LoadedProgram::new_tombstone(self.slot)));
|
LoadedProgramType::FailedVerification,
|
||||||
|
)),
|
||||||
|
);
|
||||||
loaded_programs_for_txs.insert(*pubkey, tombstone);
|
loaded_programs_for_txs.insert(*pubkey, tombstone);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -4451,11 +4449,14 @@ impl Bank {
|
||||||
(program_accounts_map, loaded_programs_for_txs)
|
(program_accounts_map, loaded_programs_for_txs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replenish_executor_cache(
|
fn replenish_executor_cache<'a>(
|
||||||
&self,
|
&self,
|
||||||
program_owners: &[&Pubkey],
|
program_owners: &[&'a Pubkey],
|
||||||
sanitized_txs: &[SanitizedTransaction],
|
sanitized_txs: &[SanitizedTransaction],
|
||||||
check_results: &mut [TransactionCheckResult],
|
check_results: &mut [TransactionCheckResult],
|
||||||
|
) -> (
|
||||||
|
HashMap<Pubkey, &'a Pubkey>,
|
||||||
|
HashMap<Pubkey, Arc<LoadedProgram>>,
|
||||||
) {
|
) {
|
||||||
let mut filter_programs_time = Measure::start("filter_programs_accounts");
|
let mut filter_programs_time = Measure::start("filter_programs_accounts");
|
||||||
let program_accounts_map = self.rc.accounts.filter_executable_program_accounts(
|
let program_accounts_map = self.rc.accounts.filter_executable_program_accounts(
|
||||||
|
@ -4467,6 +4468,7 @@ impl Bank {
|
||||||
);
|
);
|
||||||
filter_programs_time.stop();
|
filter_programs_time.stop();
|
||||||
|
|
||||||
|
let mut loaded_programs_for_txs = HashMap::new();
|
||||||
let mut filter_missing_programs_time = Measure::start("filter_missing_programs_accounts");
|
let mut filter_missing_programs_time = Measure::start("filter_missing_programs_accounts");
|
||||||
let missing_executors = program_accounts_map
|
let missing_executors = program_accounts_map
|
||||||
.keys()
|
.keys()
|
||||||
|
@ -4475,6 +4477,10 @@ impl Bank {
|
||||||
.read()
|
.read()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get(key)
|
.get(key)
|
||||||
|
.map(|program| {
|
||||||
|
loaded_programs_for_txs.insert(*key, program.clone());
|
||||||
|
program
|
||||||
|
})
|
||||||
.is_none()
|
.is_none()
|
||||||
.then_some(key)
|
.then_some(key)
|
||||||
})
|
})
|
||||||
|
@ -4484,15 +4490,27 @@ impl Bank {
|
||||||
let executors = missing_executors
|
let executors = missing_executors
|
||||||
.iter()
|
.iter()
|
||||||
.map(|pubkey| match self.load_program(pubkey) {
|
.map(|pubkey| match self.load_program(pubkey) {
|
||||||
Ok(program) => (**pubkey, program),
|
Ok(program) => {
|
||||||
|
loaded_programs_for_txs.insert(**pubkey, program.clone());
|
||||||
|
(**pubkey, program)
|
||||||
|
}
|
||||||
// Create a tombstone for the programs that failed to load
|
// Create a tombstone for the programs that failed to load
|
||||||
Err(_) => (**pubkey, Arc::new(LoadedProgram::new_tombstone(self.slot))),
|
Err(_) => {
|
||||||
|
let tombstone = Arc::new(LoadedProgram::new_tombstone(
|
||||||
|
self.slot,
|
||||||
|
LoadedProgramType::FailedVerification,
|
||||||
|
));
|
||||||
|
loaded_programs_for_txs.insert(**pubkey, tombstone.clone());
|
||||||
|
(**pubkey, tombstone)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// avoid locking the cache if there are no new executors
|
// avoid locking the cache if there are no new executors
|
||||||
if executors.len() > 0 {
|
if executors.len() > 0 {
|
||||||
self.executor_cache.write().unwrap().put(executors);
|
self.executor_cache.write().unwrap().put(executors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(program_accounts_map, loaded_programs_for_txs)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
|
@ -4561,7 +4579,6 @@ impl Bank {
|
||||||
bpf_loader_upgradeable::id(),
|
bpf_loader_upgradeable::id(),
|
||||||
bpf_loader::id(),
|
bpf_loader::id(),
|
||||||
bpf_loader_deprecated::id(),
|
bpf_loader_deprecated::id(),
|
||||||
native_loader::id(),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let program_owners_refs: Vec<&Pubkey> = program_owners.iter().collect();
|
let program_owners_refs: Vec<&Pubkey> = program_owners.iter().collect();
|
||||||
|
@ -4574,7 +4591,12 @@ impl Bank {
|
||||||
&check_results,
|
&check_results,
|
||||||
);
|
);
|
||||||
*/
|
*/
|
||||||
self.replenish_executor_cache(&program_owners_refs, sanitized_txs, &mut check_results);
|
let (executable_programs_in_tx_batch, loaded_programs_map) =
|
||||||
|
self.replenish_executor_cache(&program_owners_refs, sanitized_txs, &mut check_results);
|
||||||
|
|
||||||
|
let tx_executor_cache = Rc::new(RefCell::new(TransactionExecutorCache::new(
|
||||||
|
loaded_programs_map.clone().into_iter(),
|
||||||
|
)));
|
||||||
|
|
||||||
let mut load_time = Measure::start("accounts_load");
|
let mut load_time = Measure::start("accounts_load");
|
||||||
let mut loaded_transactions = self.rc.accounts.load_accounts(
|
let mut loaded_transactions = self.rc.accounts.load_accounts(
|
||||||
|
@ -4587,6 +4609,8 @@ impl Bank {
|
||||||
&self.feature_set,
|
&self.feature_set,
|
||||||
&self.fee_structure,
|
&self.fee_structure,
|
||||||
account_overrides,
|
account_overrides,
|
||||||
|
&executable_programs_in_tx_batch,
|
||||||
|
&loaded_programs_map,
|
||||||
);
|
);
|
||||||
load_time.stop();
|
load_time.stop();
|
||||||
|
|
||||||
|
@ -4643,6 +4667,7 @@ impl Bank {
|
||||||
timings,
|
timings,
|
||||||
&mut error_counters,
|
&mut error_counters,
|
||||||
log_messages_bytes_limit,
|
log_messages_bytes_limit,
|
||||||
|
tx_executor_cache.clone(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -11740,6 +11740,8 @@ fn test_rent_state_list_len() {
|
||||||
&bank.feature_set,
|
&bank.feature_set,
|
||||||
&FeeStructure::default(),
|
&FeeStructure::default(),
|
||||||
None,
|
None,
|
||||||
|
&HashMap::new(),
|
||||||
|
&HashMap::new(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let compute_budget = bank.runtime_config.compute_budget.unwrap_or_else(|| {
|
let compute_budget = bank.runtime_config.compute_budget.unwrap_or_else(|| {
|
||||||
|
|
Loading…
Reference in New Issue