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