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::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        // We're entering into one of the block-verification methods.
75        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        // There are two cases where balance_collector could be None:
123        // * Balance recording is disabled. If that were the case, there would
124        //   be no TransactionStatusSender, and we would not be in this branch.
125        // * The batch was aborted in its entirety in SVM. In that case, nothing
126        //   would have been committed.
127        // Therefore this should always be true.
128        debug_assert!(balance_collector.is_some());
129
130        let (balances, token_balances) =
131            compile_collected_balances(balance_collector.unwrap_or_default());
132
133        // The length of costs vector needs to be consistent with all other
134        // vectors that are sent over (such as `transactions`). So, replace the
135        // None elements with Some(0)
136        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
169// Get actual transaction execution costs from transaction commit results
170fn 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
204// Includes transaction signature for unit-testing
205fn 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        // set block-limit to be able to just have one transaction
334        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        // The transaction will fit when added the first time
341        assert!(check_block_cost_limits(&bank, &tx_costs).is_ok());
342        // But adding a second time will exceed the block limit
343        assert_eq!(
344            Err(TransactionError::WouldExceedMaxBlockCostLimit),
345            check_block_cost_limits(&bank, &tx_costs)
346        );
347        // Adding another None will noop (even though the block is already full)
348        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        // Create array of two transactions which throw different errors
365        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            // The pre-commit callback surfaced the processing error and
493            // cancelled the commit
494            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        // one signature, two locks, instruction data
512        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        // 3k compute for calling a builtin
519        let compute =
520            CostModel::calculate_cost(&batch.sanitized_transactions()[0], &bank.feature_set)
521                .programs_execution_cost();
522
523        // 64mb default loaded transaction data size limit
524        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        // no-ops are added to StatusCache and cannot be re-executed
559        assert_eq!(
560            commit_results,
561            vec![Err(TransactionError::AlreadyProcessed)]
562        );
563    }
564}