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