Skip to main content

solana_runtime/
transaction_execution.rs

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        // We're entering into one of the block-verification methods.
76        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        // There are two cases where balance_collector could be None:
125        // * Balance recording is disabled. If that were the case, there would
126        //   be no TransactionStatusSender, and we would not be in this branch.
127        // * The batch was aborted in its entirety in SVM. In that case, nothing
128        //   would have been committed.
129        // Therefore this should always be true.
130        debug_assert!(balance_collector.is_some());
131
132        let (balances, token_balances) =
133            compile_collected_balances(balance_collector.unwrap_or_default());
134
135        // The length of costs vector needs to be consistent with all other
136        // vectors that are sent over (such as `transactions`). So, replace the
137        // None elements with Some(0)
138        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
176// Get actual transaction execution costs from transaction commit results
177fn 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
211// Includes transaction signature for unit-testing
212fn 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        // set block-limit to be able to just have one transaction
342        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        // The transaction will fit when added the first time
350        assert!(check_block_cost_limits(&bank, transactions, &tx_costs).is_ok());
351        // But adding a second time will exceed the block limit
352        assert_eq!(
353            Err(TransactionError::WouldExceedMaxBlockCostLimit),
354            check_block_cost_limits(&bank, transactions, &tx_costs)
355        );
356        // Adding another None will noop (even though the block is already full)
357        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        // Create array of two transactions which throw different errors
374        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            // The pre-commit callback surfaced the processing error and
502            // cancelled the commit
503            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        // one signature, two locks, instruction data
521        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        // 3k compute for calling a builtin
528        let compute =
529            CostModel::calculate_cost(&batch.sanitized_transactions()[0], &bank.feature_set)
530                .programs_execution_cost();
531
532        // 64mb default loaded transaction data size limit
533        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        // no-ops are added to StatusCache and cannot be re-executed
568        assert_eq!(
569            commit_results,
570            vec![Err(TransactionError::AlreadyProcessed)]
571        );
572    }
573}