use {
super::{Bank, BankStatusCache},
agave_feature_set::{FeatureSet, raise_cpi_nesting_limit_to_8},
solana_accounts_db::blockhash_queue::BlockhashQueue,
solana_clock::{MAX_PROCESSING_AGE, MAX_TRANSACTION_FORWARDING_DELAY, Slot},
solana_fee::{FeeFeatures, calculate_fee_details},
solana_fee_structure::{FeeBudgetLimits, FeeDetails},
solana_nonce::state::{Data as NonceData, DurableNonce},
solana_nonce_account as nonce_account,
solana_program_runtime::execution_budget::SVMTransactionExecutionAndFeeBudgetLimits,
solana_pubkey::Pubkey,
solana_runtime_transaction::transaction_with_meta::TransactionWithMeta,
solana_svm::{
account_loader::{CheckedTransactionDetails, TransactionCheckResult},
transaction_error_metrics::TransactionErrorMetrics,
},
solana_svm_transaction::svm_message::SVMMessage,
solana_transaction_error::{TransactionError, TransactionResult},
};
impl Bank {
pub fn check_transactions_with_forwarding_delay(
&self,
transactions: &[impl TransactionWithMeta],
filter: &[TransactionResult<()>],
forward_transactions_to_leader_at_slot_offset: u64,
) -> Vec<TransactionCheckResult> {
let mut error_counters = TransactionErrorMetrics::default();
let max_tx_fwd_delay = MAX_TRANSACTION_FORWARDING_DELAY;
self.check_transactions(
transactions,
filter,
(MAX_PROCESSING_AGE)
.saturating_sub(max_tx_fwd_delay)
.saturating_sub(forward_transactions_to_leader_at_slot_offset as usize),
&mut error_counters,
)
}
pub fn check_transactions<Tx: TransactionWithMeta>(
&self,
sanitized_txs: &[impl core::borrow::Borrow<Tx>],
lock_results: &[TransactionResult<()>],
max_age: usize,
error_counters: &mut TransactionErrorMetrics,
) -> Vec<TransactionCheckResult> {
self.check_transactions_with_processed_slots(
sanitized_txs,
lock_results,
max_age,
false,
error_counters,
)
.0
}
pub fn check_transactions_with_processed_slots<Tx: TransactionWithMeta>(
&self,
sanitized_txs: &[impl core::borrow::Borrow<Tx>],
lock_results: &[TransactionResult<()>],
max_age: usize,
collect_processed_slots: bool,
error_counters: &mut TransactionErrorMetrics,
) -> (Vec<TransactionCheckResult>, Option<Vec<Option<Slot>>>) {
let lock_results = self.check_age_and_compute_budget_limits(
sanitized_txs,
lock_results,
max_age,
error_counters,
);
self.check_status_cache(
sanitized_txs,
lock_results,
collect_processed_slots,
error_counters,
)
}
fn check_age_and_compute_budget_limits<Tx: TransactionWithMeta>(
&self,
sanitized_txs: &[impl core::borrow::Borrow<Tx>],
lock_results: &[TransactionResult<()>],
max_age: usize,
error_counters: &mut TransactionErrorMetrics,
) -> Vec<TransactionCheckResult> {
let hash_queue = self.blockhash_queue.read().unwrap();
let last_blockhash = hash_queue.last_hash();
let next_durable_nonce = DurableNonce::from_blockhash(&last_blockhash);
let feature_set: &FeatureSet = &self.feature_set;
let fee_features = FeeFeatures::from(feature_set);
let raise_cpi_limit = feature_set.is_active(&raise_cpi_nesting_limit_to_8::id());
sanitized_txs
.iter()
.zip(lock_results)
.map(|(tx, lock_res)| match lock_res {
Ok(()) => {
let compute_budget_and_limits = tx
.borrow()
.compute_budget_instruction_details()
.sanitize_and_convert_to_compute_budget_limits(feature_set)
.map(|limit| {
let fee_budget = FeeBudgetLimits::from(limit);
let fee_details = calculate_fee_details(
tx.borrow(),
false,
self.fee_structure.lamports_per_signature,
fee_budget.prioritization_fee,
fee_features,
);
if let Some(compute_budget) = self.compute_budget {
compute_budget.get_compute_budget_and_limits(
fee_budget.loaded_accounts_data_size_limit,
fee_details,
)
} else {
limit.get_compute_budget_and_limits(
fee_budget.loaded_accounts_data_size_limit,
fee_details,
raise_cpi_limit,
)
}
})
.inspect_err(|_err| {
error_counters.invalid_compute_budget += 1;
})?;
self.check_transaction_age(
tx.borrow(),
max_age,
&next_durable_nonce,
&hash_queue,
error_counters,
compute_budget_and_limits,
)
}
Err(e) => Err(e.clone()),
})
.collect()
}
fn checked_transactions_details_with_test_override(
nonce_address: Option<Pubkey>,
lamports_per_signature: u64,
mut compute_budget_and_limits: SVMTransactionExecutionAndFeeBudgetLimits,
) -> CheckedTransactionDetails {
if lamports_per_signature == 0 {
compute_budget_and_limits.fee_details = FeeDetails::default();
}
CheckedTransactionDetails::new(nonce_address, compute_budget_and_limits)
}
fn check_transaction_age(
&self,
tx: &impl SVMMessage,
max_age: usize,
next_durable_nonce: &DurableNonce,
hash_queue: &BlockhashQueue,
error_counters: &mut TransactionErrorMetrics,
compute_budget: SVMTransactionExecutionAndFeeBudgetLimits,
) -> TransactionCheckResult {
let recent_blockhash = tx.recent_blockhash();
if let Some(hash_info) = hash_queue.get_hash_info_if_valid(recent_blockhash, max_age) {
Ok(Self::checked_transactions_details_with_test_override(
None,
hash_info.lamports_per_signature(),
compute_budget,
))
} else if let Some((nonce_address, previous_lamports_per_signature)) =
self.check_nonce_transaction_validity(tx, next_durable_nonce)
{
Ok(Self::checked_transactions_details_with_test_override(
Some(nonce_address),
previous_lamports_per_signature,
compute_budget,
))
} else {
error_counters.blockhash_not_found += 1;
Err(TransactionError::BlockhashNotFound)
}
}
pub(super) fn check_nonce_transaction_validity(
&self,
message: &impl SVMMessage,
next_durable_nonce: &DurableNonce,
) -> Option<(Pubkey, u64)> {
let nonce_is_advanceable = message.recent_blockhash() != next_durable_nonce.as_hash();
if !nonce_is_advanceable {
return None;
}
let (nonce_address, nonce_data) = self.load_message_nonce_data(message)?;
let previous_lamports_per_signature = nonce_data.get_lamports_per_signature();
Some((nonce_address, previous_lamports_per_signature))
}
pub(super) fn load_message_nonce_data(
&self,
message: &impl SVMMessage,
) -> Option<(Pubkey, NonceData)> {
let require_static_nonce_account = self
.feature_set
.is_active(&agave_feature_set::require_static_nonce_account::id());
let nonce_address = message.get_durable_nonce(require_static_nonce_account)?;
let nonce_account = self.get_account_with_fixed_root(nonce_address)?;
let nonce_data =
nonce_account::verify_nonce_account(&nonce_account, message.recent_blockhash())?;
Some((*nonce_address, nonce_data))
}
fn check_status_cache<Tx: TransactionWithMeta>(
&self,
sanitized_txs: &[impl core::borrow::Borrow<Tx>],
lock_results: Vec<TransactionCheckResult>,
collect_processed_slots: bool,
error_counters: &mut TransactionErrorMetrics,
) -> (Vec<TransactionCheckResult>, Option<Vec<Option<Slot>>>) {
let mut check_results = Vec::with_capacity(sanitized_txs.len());
let mut processed_slots = if collect_processed_slots {
Some(Vec::with_capacity(sanitized_txs.len()))
} else {
None
};
let rcache = self.status_cache.read().unwrap();
for (sanitized_tx_ref, lock_result) in sanitized_txs.iter().zip(lock_results) {
let sanitized_tx = sanitized_tx_ref.borrow();
let (result, processed_slot) = if lock_result.is_ok() {
if let Some(slot) = self.get_processed_slot(sanitized_tx, &rcache) {
error_counters.already_processed += 1;
(Err(TransactionError::AlreadyProcessed), Some(slot))
} else {
(lock_result, None)
}
} else {
(lock_result, None)
};
check_results.push(result);
if let Some(processed_slots) = processed_slots.as_mut() {
processed_slots.push(processed_slot)
}
}
(check_results, processed_slots)
}
fn get_processed_slot(
&self,
sanitized_tx: &impl TransactionWithMeta,
status_cache: &BankStatusCache,
) -> Option<Slot> {
let key = sanitized_tx.message_hash();
let transaction_blockhash = sanitized_tx.recent_blockhash();
status_cache
.get_status(key, transaction_blockhash, &self.ancestors)
.map(|status| status.0)
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::bank::tests::{
get_nonce_blockhash, get_nonce_data_from_account, new_sanitized_message,
setup_nonce_with_bank,
},
solana_account::state_traits::StateMut,
solana_hash::Hash,
solana_keypair::Keypair,
solana_message::{
Message, MessageHeader, SanitizedMessage, SanitizedVersionedMessage,
SimpleAddressLoader, VersionedMessage,
compiled_instruction::CompiledInstruction,
v0::{self, LoadedAddresses, MessageAddressTableLookup},
},
solana_nonce::{state::State as NonceState, versions::Versions as NonceVersions},
solana_signer::Signer,
solana_system_interface::{
instruction::{self as system_instruction, SystemInstruction},
program as system_program,
},
std::collections::HashSet,
test_case::test_case,
};
#[test]
fn test_check_nonce_transaction_validity_ok() {
const STALE_LAMPORTS_PER_SIGNATURE: u64 = 42;
let (bank, _mint_keypair, custodian_keypair, nonce_keypair, _) = setup_nonce_with_bank(
10_000_000,
|_| {},
5_000_000,
250_000,
None,
FeatureSet::all_enabled(),
)
.unwrap();
let custodian_pubkey = custodian_keypair.pubkey();
let nonce_pubkey = nonce_keypair.pubkey();
let nonce_hash = get_nonce_blockhash(&bank, &nonce_pubkey).unwrap();
let message = new_sanitized_message(Message::new_with_blockhash(
&[
system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey),
system_instruction::transfer(&custodian_pubkey, &nonce_pubkey, 100_000),
],
Some(&custodian_pubkey),
&nonce_hash,
));
let mut nonce_account = bank.get_account(&nonce_pubkey).unwrap();
let nonce_data = get_nonce_data_from_account(&nonce_account).unwrap();
nonce_account
.set_state(&NonceVersions::new(NonceState::new_initialized(
&nonce_data.authority,
nonce_data.durable_nonce,
STALE_LAMPORTS_PER_SIGNATURE,
)))
.unwrap();
bank.store_account(&nonce_pubkey, &nonce_account);
assert_eq!(
bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce()),
Some((nonce_pubkey, STALE_LAMPORTS_PER_SIGNATURE)),
);
}
#[test]
fn test_check_nonce_transaction_validity_not_nonce_fail() {
let (bank, _mint_keypair, custodian_keypair, nonce_keypair, _) = setup_nonce_with_bank(
10_000_000,
|_| {},
5_000_000,
250_000,
None,
FeatureSet::all_enabled(),
)
.unwrap();
let custodian_pubkey = custodian_keypair.pubkey();
let nonce_pubkey = nonce_keypair.pubkey();
let nonce_hash = get_nonce_blockhash(&bank, &nonce_pubkey).unwrap();
let message = new_sanitized_message(Message::new_with_blockhash(
&[
system_instruction::transfer(&custodian_pubkey, &nonce_pubkey, 100_000),
system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey),
],
Some(&custodian_pubkey),
&nonce_hash,
));
assert!(
bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce())
.is_none()
);
}
#[test]
fn test_check_nonce_transaction_validity_missing_ix_pubkey_fail() {
let (bank, _mint_keypair, custodian_keypair, nonce_keypair, _) = setup_nonce_with_bank(
10_000_000,
|_| {},
5_000_000,
250_000,
None,
FeatureSet::all_enabled(),
)
.unwrap();
let custodian_pubkey = custodian_keypair.pubkey();
let nonce_pubkey = nonce_keypair.pubkey();
let nonce_hash = get_nonce_blockhash(&bank, &nonce_pubkey).unwrap();
let mut message = Message::new_with_blockhash(
&[
system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey),
system_instruction::transfer(&custodian_pubkey, &nonce_pubkey, 100_000),
],
Some(&custodian_pubkey),
&nonce_hash,
);
message.instructions[0].accounts.clear();
assert!(
bank.check_nonce_transaction_validity(
&new_sanitized_message(message),
&bank.next_durable_nonce(),
)
.is_none()
);
}
#[test]
fn test_check_nonce_transaction_validity_nonce_acc_does_not_exist_fail() {
let (bank, _mint_keypair, custodian_keypair, nonce_keypair, _) = setup_nonce_with_bank(
10_000_000,
|_| {},
5_000_000,
250_000,
None,
FeatureSet::all_enabled(),
)
.unwrap();
let custodian_pubkey = custodian_keypair.pubkey();
let nonce_pubkey = nonce_keypair.pubkey();
let missing_keypair = Keypair::new();
let missing_pubkey = missing_keypair.pubkey();
let nonce_hash = get_nonce_blockhash(&bank, &nonce_pubkey).unwrap();
let message = new_sanitized_message(Message::new_with_blockhash(
&[
system_instruction::advance_nonce_account(&missing_pubkey, &nonce_pubkey),
system_instruction::transfer(&custodian_pubkey, &nonce_pubkey, 100_000),
],
Some(&custodian_pubkey),
&nonce_hash,
));
assert!(
bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce())
.is_none()
);
}
#[test]
fn test_check_nonce_transaction_validity_bad_tx_hash_fail() {
let (bank, _mint_keypair, custodian_keypair, nonce_keypair, _) = setup_nonce_with_bank(
10_000_000,
|_| {},
5_000_000,
250_000,
None,
FeatureSet::all_enabled(),
)
.unwrap();
let custodian_pubkey = custodian_keypair.pubkey();
let nonce_pubkey = nonce_keypair.pubkey();
let message = new_sanitized_message(Message::new_with_blockhash(
&[
system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey),
system_instruction::transfer(&custodian_pubkey, &nonce_pubkey, 100_000),
],
Some(&custodian_pubkey),
&Hash::default(),
));
assert!(
bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce())
.is_none()
);
}
#[test_case(true; "test_check_nonce_transaction_validity_nonce_is_alt_disallowed")]
#[test_case(false; "test_check_nonce_transaction_validity_nonce_is_alt_allowed")]
fn test_check_nonce_transaction_validity_nonce_is_alt(require_static_nonce_account: bool) {
let feature_set = if require_static_nonce_account {
FeatureSet::all_enabled()
} else {
FeatureSet::default()
};
let nonce_authority = Pubkey::new_unique();
let (bank, _mint_keypair, _custodian_keypair, nonce_keypair, _) = setup_nonce_with_bank(
10_000_000,
|_| {},
5_000_000,
250_000,
Some(nonce_authority),
feature_set,
)
.unwrap();
let nonce_pubkey = nonce_keypair.pubkey();
let nonce_hash = get_nonce_blockhash(&bank, &nonce_pubkey).unwrap();
let loaded_addresses = LoadedAddresses {
writable: vec![nonce_pubkey],
readonly: vec![],
};
let message = SanitizedMessage::try_new(
SanitizedVersionedMessage::try_new(VersionedMessage::V0(v0::Message {
header: MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
},
account_keys: vec![nonce_authority, system_program::id()],
recent_blockhash: nonce_hash,
instructions: vec![CompiledInstruction::new(
1, &SystemInstruction::AdvanceNonceAccount,
vec![
2, 0, ],
)],
address_table_lookups: vec![MessageAddressTableLookup {
account_key: Pubkey::new_unique(),
writable_indexes: (0..loaded_addresses.writable.len())
.map(|x| x as u8)
.collect(),
readonly_indexes: (0..loaded_addresses.readonly.len())
.map(|x| (loaded_addresses.writable.len() + x) as u8)
.collect(),
}],
}))
.unwrap(),
SimpleAddressLoader::Enabled(loaded_addresses),
&HashSet::new(),
)
.unwrap();
assert_eq!(
bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce())
.is_none(),
require_static_nonce_account,
);
}
}