1use {
2 crate::{
3 bank::{Bank, TransactionBalancesSet},
4 bank_utils,
5 dependency_tracker::DependencyTracker,
6 prioritization_fee_cache::PrioritizationFeeCache,
7 transaction_balances::compile_collected_balances,
8 transaction_batch::TransactionBatch,
9 vote_sender_types::{ReplayVoteSendType, ReplayVoteSender},
10 },
11 log::{trace, warn},
12 solana_clock::{BankId, Slot},
13 solana_cost_model::{cost_model::CostModel, transaction_cost::TransactionCost},
14 solana_measure::measure::Measure,
15 solana_runtime_transaction::transaction_with_meta::{TransactionWithMeta, writable_accounts},
16 solana_signature::Signature,
17 solana_svm::{
18 transaction_commit_result::{
19 TransactionCommitResult, TransactionCommitResultExtensions as _,
20 },
21 transaction_processor::ExecutionRecordingConfig,
22 },
23 solana_svm_timings::{ExecuteTimingType, ExecuteTimings},
24 solana_svm_transaction::{svm_message::SVMMessage, svm_transaction::SVMTransaction},
25 solana_transaction::sanitized::SanitizedTransaction,
26 solana_transaction_error::{TransactionError, TransactionResult},
27 solana_transaction_status::token_balances::TransactionTokenBalancesSet,
28 std::{borrow::Cow, sync::Arc},
29};
30
31type WorkSequence = u64;
32
33#[derive(Debug)]
34pub struct TransactionStatusBatch {
35 pub slot: Slot,
36 pub bank_id: BankId,
37 pub transactions: Vec<SanitizedTransaction>,
38 pub commit_results: Vec<TransactionCommitResult>,
39 pub balances: TransactionBalancesSet,
40 pub token_balances: TransactionTokenBalancesSet,
41 pub costs: Vec<Option<u64>>,
42 pub transaction_indexes: Vec<usize>,
43}
44
45#[allow(clippy::large_enum_variant)]
46#[derive(Debug)]
47pub enum TransactionStatusMessage {
48 Batch((TransactionStatusBatch, Option<WorkSequence>)),
49 Freeze(Arc<Bank>),
50}
51
52pub struct TransactionBatchWithIndexes<'a, 'b, Tx: SVMMessage> {
53 pub batch: TransactionBatch<'a, 'b, Tx>,
54 pub transaction_indexes: Vec<usize>,
55}
56
57pub fn execute_batch<'a>(
58 batch: &'a TransactionBatchWithIndexes<impl TransactionWithMeta>,
59 bank: &'a Arc<Bank>,
60 transaction_status_sender: Option<&'a TransactionStatusSender>,
61 replay_vote_sender: Option<&'a ReplayVoteSender>,
62 replay_vote_send_type: ReplayVoteSendType,
63 timings: &'a mut ExecuteTimings,
64 log_messages_bytes_limit: Option<usize>,
65 prioritization_fee_cache: Option<&'a PrioritizationFeeCache>,
66) -> TransactionResult<()> {
67 let TransactionBatchWithIndexes {
68 batch,
69 transaction_indexes,
70 } = batch;
71
72 let transaction_indexes = Cow::from(transaction_indexes);
73
74 let pre_commit_callback = |processing_results: &_| -> TransactionResult<()> {
75 get_first_error(batch, processing_results)
77 };
78
79 let (commit_results, balance_collector) = batch
80 .bank()
81 .load_execute_and_commit_transactions_with_pre_commit_callback(
82 batch,
83 ExecutionRecordingConfig::new_single_setting(transaction_status_sender.is_some()),
84 timings,
85 log_messages_bytes_limit,
86 pre_commit_callback,
87 )?;
88
89 let mut check_block_costs_elapsed = Measure::start("check_block_costs");
90
91 let tx_costs = get_transaction_costs(bank, &commit_results, batch.sanitized_transactions());
92 let checked_tx_costs_result =
93 check_block_cost_limits(bank, batch.sanitized_transactions(), &tx_costs);
94
95 check_block_costs_elapsed.stop();
96 timings.saturating_add_in_place(
97 ExecuteTimingType::CheckBlockLimitsUs,
98 check_block_costs_elapsed.as_us(),
99 );
100
101 checked_tx_costs_result?;
102
103 bank_utils::find_and_send_votes(
104 batch.sanitized_transactions(),
105 &commit_results,
106 replay_vote_sender,
107 replay_vote_send_type,
108 );
109
110 if let Some(prioritization_fee_cache) = prioritization_fee_cache {
111 let fee_paying_transactions = commit_results
112 .iter()
113 .zip(batch.sanitized_transactions())
114 .filter_map(|(commit_result, tx)| commit_result.was_fee_paying().then_some(tx));
115 prioritization_fee_cache.update(bank, fee_paying_transactions);
116 }
117 if let Some(transaction_status_sender) = transaction_status_sender {
118 let transactions: Vec<SanitizedTransaction> = batch
119 .sanitized_transactions()
120 .iter()
121 .map(|tx| tx.as_sanitized_transaction().into_owned())
122 .collect();
123
124 debug_assert!(balance_collector.is_some());
131
132 let (balances, token_balances) =
133 compile_collected_balances(balance_collector.unwrap_or_default());
134
135 let tx_costs = tx_costs
139 .into_iter()
140 .map(|tx_cost_option| tx_cost_option.map(|tx_cost| tx_cost.sum()).or(Some(0)))
141 .collect();
142
143 transaction_status_sender.send_transaction_status_batch(
144 bank.slot(),
145 bank.bank_id(),
146 transactions,
147 commit_results,
148 balances,
149 token_balances,
150 tx_costs,
151 transaction_indexes.into_owned(),
152 );
153 }
154
155 Ok(())
156}
157
158fn check_block_cost_limits<Tx: TransactionWithMeta>(
159 bank: &Bank,
160 transactions: &[Tx],
161 tx_costs: &[Option<TransactionCost>],
162) -> TransactionResult<()> {
163 assert_eq!(transactions.len(), tx_costs.len());
164 let mut cost_tracker = bank.write_cost_tracker().unwrap();
165 for (transaction, tx_cost) in transactions.iter().zip(tx_costs) {
166 if let Some(tx_cost) = tx_cost {
167 cost_tracker
168 .try_add(tx_cost, writable_accounts(transaction))
169 .map_err(TransactionError::from)?;
170 }
171 }
172
173 Ok(())
174}
175
176fn get_transaction_costs<Tx: TransactionWithMeta>(
178 bank: &Bank,
179 commit_results: &[TransactionCommitResult],
180 sanitized_transactions: &[Tx],
181) -> Vec<Option<TransactionCost>> {
182 assert_eq!(sanitized_transactions.len(), commit_results.len());
183
184 commit_results
185 .iter()
186 .zip(sanitized_transactions)
187 .map(|(commit_result, tx)| {
188 if let Ok(committed_tx) = commit_result {
189 Some(CostModel::calculate_cost_for_executed_transaction(
190 tx,
191 committed_tx.executed_units,
192 committed_tx.loaded_account_stats.loaded_accounts_data_size,
193 &bank.feature_set,
194 ))
195 } else {
196 None
197 }
198 })
199 .collect()
200}
201
202fn get_first_error<T, Tx: SVMTransaction>(
203 batch: &TransactionBatch<Tx>,
204 commit_results: &[TransactionResult<T>],
205) -> TransactionResult<()> {
206 do_get_first_error(batch, commit_results)
207 .map(|(error, _signature)| error)
208 .unwrap_or(Ok(()))
209}
210
211fn do_get_first_error<T, Tx: SVMTransaction>(
213 batch: &TransactionBatch<Tx>,
214 results: &[TransactionResult<T>],
215) -> Option<(TransactionResult<()>, Signature)> {
216 let mut first_err = None;
217 for (result, transaction) in results.iter().zip(batch.sanitized_transactions()) {
218 if let Err(err) = result {
219 if first_err.is_none() {
220 first_err = Some((Err(err.clone()), *transaction.signature()));
221 }
222 warn!("Unexpected validator error: {err:?}, transaction: {transaction:?}");
223 datapoint_error!(
224 "validator_process_entry_error",
225 (
226 "error",
227 format!("error: {err:?}, transaction: {transaction:?}"),
228 String
229 )
230 );
231 }
232 }
233 first_err
234}
235
236#[derive(Clone, Debug)]
237pub struct TransactionStatusSender {
238 pub sender: crossbeam_channel::Sender<TransactionStatusMessage>,
239 pub dependency_tracker: Option<Arc<DependencyTracker>>,
240}
241
242impl TransactionStatusSender {
243 pub fn send_transaction_status_batch(
244 &self,
245 slot: Slot,
246 bank_id: BankId,
247 transactions: Vec<SanitizedTransaction>,
248 commit_results: Vec<TransactionCommitResult>,
249 balances: TransactionBalancesSet,
250 token_balances: TransactionTokenBalancesSet,
251 costs: Vec<Option<u64>>,
252 transaction_indexes: Vec<usize>,
253 ) {
254 let work_sequence = self
255 .dependency_tracker
256 .as_ref()
257 .map(|dependency_tracker| dependency_tracker.declare_work());
258
259 if let Err(e) = self.sender.send(TransactionStatusMessage::Batch((
260 TransactionStatusBatch {
261 slot,
262 bank_id,
263 transactions,
264 commit_results,
265 balances,
266 token_balances,
267 costs,
268 transaction_indexes,
269 },
270 work_sequence,
271 ))) {
272 trace!("Slot {slot} transaction_status send batch failed: {e:?}");
273 }
274 }
275
276 pub fn send_transaction_status_freeze_message(&self, bank: &Arc<Bank>) {
277 if let Err(e) = self
278 .sender
279 .send(TransactionStatusMessage::Freeze(bank.clone()))
280 {
281 let slot = bank.slot();
282 warn!("Slot {slot} transaction_status send freeze message failed: {e:?}");
283 }
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use {
290 super::*,
291 crate::{
292 genesis_utils::{
293 GenesisConfigInfo, create_genesis_config, create_genesis_config_with_leader,
294 },
295 transaction_batch::OwnedOrBorrowed,
296 },
297 crossbeam_channel::bounded,
298 solana_account::AccountSharedData,
299 solana_compute_budget::compute_budget_limits::MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES,
300 solana_cost_model::{
301 block_cost_limits::{INSTRUCTION_DATA_BYTES_COST, SIGNATURE_COST, WRITE_LOCK_UNITS},
302 cost_tracker::CostTrackerLimits,
303 },
304 solana_hash::Hash,
305 solana_keypair::Keypair,
306 solana_pubkey::Pubkey,
307 solana_runtime_transaction::runtime_transaction::RuntimeTransaction,
308 solana_signer::Signer as _,
309 solana_system_transaction as system_transaction,
310 solana_transaction_error::TransactionError,
311 std::{assert_matches, slice},
312 test_case::test_matrix,
313 };
314
315 #[test]
316 fn test_check_block_cost_limit() {
317 let dummy_leader_pubkey = solana_pubkey::new_rand();
318 let GenesisConfigInfo {
319 genesis_config,
320 mint_keypair,
321 ..
322 } = create_genesis_config_with_leader(500, &dummy_leader_pubkey, 100);
323 let bank = Bank::new_for_tests(&genesis_config);
324
325 let tx =
326 RuntimeTransaction::from_transaction_for_tests(solana_system_transaction::transfer(
327 &mint_keypair,
328 &Pubkey::new_unique(),
329 1,
330 genesis_config.hash(),
331 ));
332 let mut tx_cost = CostModel::calculate_cost(&tx, &bank.feature_set);
333 let actual_execution_cu = 1;
334 let actual_loaded_accounts_data_size = 64 * 1024;
335 tx_cost.programs_execution_cost = actual_execution_cu;
336 tx_cost.loaded_accounts_data_size_cost =
337 CostModel::calculate_loaded_accounts_data_size_cost(
338 actual_loaded_accounts_data_size,
339 &bank.feature_set,
340 );
341 let block_limit = tx_cost.sum();
343 bank.write_cost_tracker()
344 .unwrap()
345 .set_limits(CostTrackerLimits::new(u64::MAX, block_limit, u64::MAX));
346
347 let transactions = std::slice::from_ref(&tx);
348 let tx_costs = [Some(tx_cost)];
349 assert!(check_block_cost_limits(&bank, transactions, &tx_costs).is_ok());
351 assert_eq!(
353 Err(TransactionError::WouldExceedMaxBlockCostLimit),
354 check_block_cost_limits(&bank, transactions, &tx_costs)
355 );
356 assert!(check_block_cost_limits(&bank, transactions, &[None]).is_ok());
358 }
359
360 #[test]
361 fn test_get_first_error() {
362 let GenesisConfigInfo {
363 genesis_config,
364 mint_keypair,
365 ..
366 } = create_genesis_config(1_000_000_000);
367 let (bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config);
368
369 let present_account_key = Keypair::new();
370 let present_account = AccountSharedData::new(1, 10, &Pubkey::default());
371 bank.store_account(&present_account_key.pubkey(), &present_account);
372
373 let already_processed_tx = solana_system_transaction::transfer(
375 &mint_keypair,
376 &solana_pubkey::new_rand(),
377 42,
378 bank.last_blockhash(),
379 );
380 let _ = bank.load_execute_and_commit_transactions(
381 &bank.prepare_batch_for_tests(vec![already_processed_tx.clone()]),
382 ExecutionRecordingConfig::new_single_setting(false),
383 &mut ExecuteTimings::default(),
384 None,
385 );
386 let already_processed_sig = already_processed_tx.signatures[0];
387 let invalid_blockhash_tx = solana_system_transaction::transfer(
388 &mint_keypair,
389 &solana_pubkey::new_rand(),
390 42,
391 Hash::default(),
392 );
393 let txs = vec![already_processed_tx, invalid_blockhash_tx];
394 let batch = bank.prepare_batch_for_tests(txs);
395 let (commit_results, _) = bank.load_execute_and_commit_transactions(
396 &batch,
397 ExecutionRecordingConfig::new_single_setting(false),
398 &mut ExecuteTimings::default(),
399 None,
400 );
401 let (err, signature) = do_get_first_error(&batch, &commit_results).unwrap();
402 assert_eq!(err.unwrap_err(), TransactionError::AlreadyProcessed);
403 assert_eq!(signature, already_processed_sig);
404 }
405
406 enum TxResult {
407 ExecutedWithSuccess,
408 ExecutedWithFailure,
409 NotExecuted,
410 }
411
412 #[test_matrix(
413 [TxResult::ExecutedWithSuccess, TxResult::ExecutedWithFailure, TxResult::NotExecuted]
414 )]
415 fn test_execute_batch_cancels_commit_on_processing_error(tx_result: TxResult) {
416 agave_logger::setup();
417 let dummy_leader_pubkey = solana_pubkey::new_rand();
418 let GenesisConfigInfo {
419 genesis_config,
420 mint_keypair,
421 ..
422 } = create_genesis_config_with_leader(500, &dummy_leader_pubkey, 100);
423 let bank = Bank::new_for_tests(&genesis_config);
424 let (bank, _bank_forks) = bank.wrap_with_bank_forks_for_tests();
425 let bank = Arc::new(bank);
426 let pubkey = solana_pubkey::new_rand();
427 let (tx, expected_tx_result) = match tx_result {
428 TxResult::ExecutedWithSuccess => (
429 RuntimeTransaction::from_transaction_for_tests(
430 solana_system_transaction::transfer(
431 &mint_keypair,
432 &pubkey,
433 1,
434 genesis_config.hash(),
435 ),
436 ),
437 Ok(()),
438 ),
439 TxResult::ExecutedWithFailure => (
440 RuntimeTransaction::from_transaction_for_tests(
441 solana_system_transaction::transfer(
442 &mint_keypair,
443 &pubkey,
444 100000000,
445 genesis_config.hash(),
446 ),
447 ),
448 Ok(()),
449 ),
450 TxResult::NotExecuted => (
451 RuntimeTransaction::from_transaction_for_tests(
452 solana_system_transaction::transfer(&mint_keypair, &pubkey, 1, Hash::default()),
453 ),
454 Err(TransactionError::BlockhashNotFound),
455 ),
456 };
457 let mut batch = TransactionBatch::new(
458 vec![Ok(()); 1],
459 &bank,
460 OwnedOrBorrowed::Borrowed(slice::from_ref(&tx)),
461 );
462 batch.set_needs_unlock(false);
463 let batch = TransactionBatchWithIndexes {
464 batch,
465 transaction_indexes: vec![],
466 };
467 let mut timing = ExecuteTimings::default();
468 let (sender, receiver) = bounded(1024);
469
470 assert_eq!(bank.transaction_count(), 0);
471 assert_eq!(bank.transaction_error_count(), 0);
472
473 let result = execute_batch(
474 &batch,
475 &bank,
476 Some(&TransactionStatusSender {
477 sender,
478 dependency_tracker: None,
479 }),
480 None,
481 ReplayVoteSendType::VerifiedExecuted,
482 &mut timing,
483 None,
484 None,
485 );
486
487 assert_eq!(result, expected_tx_result);
488 if expected_tx_result.is_ok() {
489 assert_eq!(bank.transaction_count(), 1);
490 if matches!(tx_result, TxResult::ExecutedWithFailure) {
491 assert_eq!(bank.transaction_error_count(), 1);
492 } else {
493 assert_eq!(bank.transaction_error_count(), 0);
494 }
495 assert_matches!(
496 receiver.try_recv(),
497 Ok(TransactionStatusMessage::Batch((TransactionStatusBatch{transaction_indexes, ..}, _sequence)))
498 if transaction_indexes.is_empty()
499 );
500 } else {
501 assert_eq!(bank.transaction_count(), 0);
504 assert_matches!(receiver.try_recv(), Err(_));
505 }
506 }
507
508 #[test]
509 fn test_check_noop_cost_units_and_replay() {
510 let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(10_000);
511 let (bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config);
512
513 let tx = system_transaction::transfer(
514 &Keypair::new(),
515 &Pubkey::new_unique(),
516 1,
517 bank.last_blockhash(),
518 );
519
520 let sig = SIGNATURE_COST;
522 let locks = 2 * WRITE_LOCK_UNITS;
523 let data = tx.message.instructions[0].data.len() as u64 / INSTRUCTION_DATA_BYTES_COST;
524
525 let batch = bank.prepare_batch_for_tests(vec![tx.clone()]);
526
527 let compute =
529 CostModel::calculate_cost(&batch.sanitized_transactions()[0], &bank.feature_set)
530 .programs_execution_cost();
531
532 let size = CostModel::calculate_loaded_accounts_data_size_cost(
534 MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(),
535 &bank.feature_set,
536 );
537
538 let (commit_results, _) = bank.load_execute_and_commit_transactions(
539 &batch,
540 ExecutionRecordingConfig::new_single_setting(false),
541 &mut ExecuteTimings::default(),
542 None,
543 );
544
545 let committed = commit_results[0].as_ref().unwrap();
546 assert_eq!(committed.status, Err(TransactionError::AccountNotFound));
547 assert_eq!(committed.executed_units, compute);
548
549 let tx_costs =
550 get_transaction_costs(&bank, &commit_results, batch.sanitized_transactions());
551
552 let noop_cost = tx_costs[0].as_ref().unwrap().sum();
553 assert_eq!(noop_cost, sig + locks + data + compute + size);
554
555 check_block_cost_limits(&bank, batch.sanitized_transactions(), &tx_costs).unwrap();
556 assert_eq!(bank.read_cost_tracker().unwrap().block_cost(), noop_cost);
557
558 drop(batch);
559
560 let (commit_results, _) = bank.load_execute_and_commit_transactions(
561 &bank.prepare_batch_for_tests(vec![tx]),
562 ExecutionRecordingConfig::new_single_setting(false),
563 &mut ExecuteTimings::default(),
564 None,
565 );
566
567 assert_eq!(
569 commit_results,
570 vec![Err(TransactionError::AlreadyProcessed)]
571 );
572 }
573}