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