Skip to main content

sui_core/checkpoints/checkpoint_executor/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! CheckpointExecutor is a Node component that executes all checkpoints for the
5//! given epoch. It acts as a Consumer to StateSync
6//! for newly synced checkpoints, taking these checkpoints and
7//! scheduling and monitoring their execution. Its primary goal is to allow
8//! for catching up to the current checkpoint sequence number of the network
9//! as quickly as possible so that a newly joined, or recovering Node can
10//! participate in a timely manner. To that end, CheckpointExecutor attempts
11//! to saturate the CPU with executor tasks (one per checkpoint), each of which
12//! handle scheduling and awaiting checkpoint transaction execution.
13//!
14//! CheckpointExecutor is made recoverable in the event of Node shutdown by way of a watermark,
15//! highest_executed_checkpoint, which is guaranteed to be updated sequentially in order,
16//! despite checkpoints themselves potentially being executed nonsequentially and in parallel.
17//! CheckpointExecutor parallelizes checkpoints of the same epoch as much as possible.
18//! CheckpointExecutor enforces the invariant that if `run` returns successfully, we have reached the
19//! end of epoch. This allows us to use it as a signal for reconfig.
20
21use futures::{StreamExt, TryStreamExt};
22use mysten_common::{ZipDebugEqIteratorExt, debug_fatal, fatal, izip_debug_eq};
23use parking_lot::Mutex;
24use std::{sync::Arc, time::Instant};
25use sui_types::base_types::SequenceNumber;
26use sui_types::crypto::RandomnessRound;
27use sui_types::messages_checkpoint::{CheckpointContents, CheckpointSequenceNumber};
28use sui_types::transaction::{TransactionDataAPI, TransactionKind};
29use sui_types::{
30    SUI_ACCUMULATOR_ROOT_OBJECT_ID,
31    node_role::{FullNodeSyncMode, NodeRole},
32};
33use tokio::task::JoinError;
34
35use sui_config::node::{CheckpointExecutorConfig, RunWithRange};
36use sui_macros::fail_point;
37use sui_types::effects::{TransactionEffects, TransactionEffectsAPI};
38use sui_types::executable_transaction::VerifiedExecutableTransaction;
39use sui_types::execution_status::{ExecutionErrorKind, ExecutionFailure, ExecutionStatus};
40use sui_types::full_checkpoint_content::Checkpoint;
41use sui_types::global_state_hash::GlobalStateHash;
42use sui_types::message_envelope::Message;
43use sui_types::{
44    base_types::{TransactionDigest, TransactionEffectsDigest},
45    messages_checkpoint::VerifiedCheckpoint,
46    transaction::VerifiedTransaction,
47};
48use tap::{TapFallible, TapOptional};
49use tracing::{debug, info, instrument};
50
51use crate::authority::authority_per_epoch_store::AuthorityPerEpochStore;
52use crate::authority::backpressure::BackpressureManager;
53use crate::authority::{AuthorityState, ExecutionEnv, ExpectedEffectsDigest};
54use crate::execution_scheduler::ExecutionScheduler;
55use crate::execution_scheduler::execution_scheduler_impl::BarrierDependencyBuilder;
56use crate::global_state_hasher::GlobalStateHasher;
57use crate::{
58    checkpoints::CheckpointStore,
59    execution_cache::{ObjectCacheRead, TransactionCacheRead},
60};
61
62mod data_ingestion_handler;
63pub mod metrics;
64pub(crate) mod utils;
65
66use data_ingestion_handler::{load_checkpoint, store_checkpoint_locally};
67use metrics::CheckpointExecutorMetrics;
68use utils::*;
69
70const CHECKPOINT_PROGRESS_LOG_COUNT_INTERVAL: u64 = 5000;
71
72#[derive(PartialEq, Eq, Debug)]
73pub enum StopReason {
74    EpochComplete,
75    RunWithRangeCondition,
76    /// The tokio runtime is shutting down and cancelled checkpoint execution work.
77    RuntimeShutdown,
78}
79
80pub(crate) struct CheckpointExecutionData {
81    pub checkpoint: VerifiedCheckpoint,
82    pub checkpoint_contents: CheckpointContents,
83    pub tx_digests: Vec<TransactionDigest>,
84    pub fx_digests: Vec<TransactionEffectsDigest>,
85}
86
87pub(crate) struct CheckpointTransactionData {
88    pub transactions: Vec<VerifiedExecutableTransaction>,
89    pub effects: Vec<TransactionEffects>,
90    pub executed_fx_digests: Vec<Option<TransactionEffectsDigest>>,
91    /// The accumulator versions for the transactions in the checkpoint.
92    /// None only if accumulator is not enabled (either all Some, or all None).
93    /// This information is needed for object balance withdraw processing.
94    /// The vector should be 1:1 with the transactions in the checkpoint.
95    pub accumulator_versions: Vec<Option<SequenceNumber>>,
96}
97
98impl CheckpointTransactionData {
99    pub fn new(
100        transactions: Vec<VerifiedExecutableTransaction>,
101        effects: Vec<TransactionEffects>,
102        executed_fx_digests: Vec<Option<TransactionEffectsDigest>>,
103    ) -> Self {
104        assert_eq!(transactions.len(), effects.len());
105        assert_eq!(transactions.len(), executed_fx_digests.len());
106        let mut accumulator_versions = vec![None; transactions.len()];
107        let mut next_update_index = 0;
108        for (idx, efx) in effects.iter().enumerate() {
109            // Only barrier settlement transactions mutate the accumulator root object.
110            // This filtering detects whether this transaction is a barrier settlement transaction.
111            // And if so we get the old version of the accumulator root object.
112            // Transactions prior to the barrier settlement transaction reads this accumulator version.
113            let acc_version = efx.object_changes().into_iter().find_map(|change| {
114                if change.id == SUI_ACCUMULATOR_ROOT_OBJECT_ID {
115                    change.input_version
116                } else {
117                    None
118                }
119            });
120            if let Some(acc_version) = acc_version {
121                // Set version for transactions between [next_update_index, idx] inclusive.
122                for slot in accumulator_versions
123                    .iter_mut()
124                    .take(idx + 1)
125                    .skip(next_update_index)
126                {
127                    *slot = Some(acc_version);
128                }
129                next_update_index = idx + 1;
130            }
131        }
132        // Either accumulator is not enabled, then next_update_index == 0;
133        // or the last transaction is the barrier settlement transaction, and next_update_index == transactions.len();
134        // or the last transaction is the end of epoch transaction, and next_update_index == transactions.len() - 1.
135        assert!(
136            next_update_index == 0
137                || next_update_index == transactions.len()
138                || (next_update_index == transactions.len() - 1
139                    && transactions
140                        .last()
141                        .unwrap()
142                        .transaction_data()
143                        .is_end_of_epoch_tx())
144        );
145        Self {
146            transactions,
147            effects,
148            executed_fx_digests,
149            accumulator_versions,
150        }
151    }
152}
153pub(crate) struct CheckpointExecutionState {
154    pub data: CheckpointExecutionData,
155
156    state_hasher: Option<GlobalStateHash>,
157    full_data: Option<Checkpoint>,
158}
159
160impl CheckpointExecutionState {
161    pub fn new(data: CheckpointExecutionData) -> Self {
162        Self {
163            data,
164            state_hasher: None,
165            full_data: None,
166        }
167    }
168
169    pub fn new_with_global_state_hasher(
170        data: CheckpointExecutionData,
171        hasher: GlobalStateHash,
172    ) -> Self {
173        Self {
174            data,
175            state_hasher: Some(hasher),
176            full_data: None,
177        }
178    }
179}
180
181macro_rules! finish_stage {
182    ($handle:expr, $stage:ident) => {
183        $handle.finish_stage(PipelineStage::$stage).await;
184    };
185}
186
187pub struct CheckpointExecutor {
188    epoch_store: Arc<AuthorityPerEpochStore>,
189    state: Arc<AuthorityState>,
190    // TODO: We should use RocksDbStore in the executor
191    // to consolidate DB accesses.
192    checkpoint_store: Arc<CheckpointStore>,
193    object_cache_reader: Arc<dyn ObjectCacheRead>,
194    transaction_cache_reader: Arc<dyn TransactionCacheRead>,
195    execution_scheduler: Arc<ExecutionScheduler>,
196    global_state_hasher: Arc<GlobalStateHasher>,
197    backpressure_manager: Arc<BackpressureManager>,
198    config: CheckpointExecutorConfig,
199    metrics: Arc<CheckpointExecutorMetrics>,
200    tps_estimator: Mutex<TPSEstimator>,
201    subscription_service_checkpoint_sender: Option<tokio::sync::broadcast::Sender<Arc<Checkpoint>>>,
202}
203
204impl CheckpointExecutor {
205    pub fn new(
206        epoch_store: Arc<AuthorityPerEpochStore>,
207        checkpoint_store: Arc<CheckpointStore>,
208        state: Arc<AuthorityState>,
209        global_state_hasher: Arc<GlobalStateHasher>,
210        backpressure_manager: Arc<BackpressureManager>,
211        config: CheckpointExecutorConfig,
212        metrics: Arc<CheckpointExecutorMetrics>,
213        subscription_service_checkpoint_sender: Option<
214            tokio::sync::broadcast::Sender<Arc<Checkpoint>>,
215        >,
216    ) -> Self {
217        Self {
218            epoch_store,
219            state: state.clone(),
220            checkpoint_store,
221            object_cache_reader: state.get_object_cache_reader().clone(),
222            transaction_cache_reader: state.get_transaction_cache_reader().clone(),
223            execution_scheduler: state.execution_scheduler().clone(),
224            global_state_hasher,
225            backpressure_manager,
226            config,
227            metrics,
228            tps_estimator: Mutex::new(TPSEstimator::default()),
229            subscription_service_checkpoint_sender,
230        }
231    }
232
233    pub fn new_for_tests(
234        epoch_store: Arc<AuthorityPerEpochStore>,
235        checkpoint_store: Arc<CheckpointStore>,
236        state: Arc<AuthorityState>,
237        state_hasher: Arc<GlobalStateHasher>,
238    ) -> Self {
239        Self::new(
240            epoch_store,
241            checkpoint_store,
242            state,
243            state_hasher,
244            BackpressureManager::new_for_tests(),
245            Default::default(),
246            CheckpointExecutorMetrics::new_for_tests(),
247            None,
248        )
249    }
250
251    // Gets the next checkpoint to schedule for execution. If the epoch is already
252    // completed, returns None.
253    fn get_next_to_schedule(&self) -> Option<CheckpointSequenceNumber> {
254        // Decide the first checkpoint to schedule for execution.
255        // If we haven't executed anything in the past, we schedule checkpoint 0.
256        // Otherwise we schedule the one after highest executed.
257        let highest_executed = self
258            .checkpoint_store
259            .get_highest_executed_checkpoint()
260            .unwrap();
261
262        if let Some(highest_executed) = &highest_executed
263            && self.epoch_store.epoch() == highest_executed.epoch()
264            && highest_executed.is_last_checkpoint_of_epoch()
265        {
266            // We can arrive at this point if we bump the highest_executed_checkpoint watermark, and then
267            // crash before completing reconfiguration.
268            info!(seq = ?highest_executed.sequence_number, "final checkpoint of epoch has already been executed");
269            return None;
270        }
271
272        Some(
273            highest_executed
274                .as_ref()
275                .map(|c| c.sequence_number() + 1)
276                .unwrap_or_else(|| {
277                    // TODO this invariant may no longer hold once we introduce snapshots
278                    assert_eq!(self.epoch_store.epoch(), 0);
279                    // we need to execute the genesis checkpoint
280                    0
281                }),
282        )
283    }
284
285    /// Execute all checkpoints for the current epoch, ensuring that the node has not
286    /// forked, and return when finished.
287    /// If `run_with_range` is set, execution will stop early.
288    #[instrument(level = "error", skip_all, fields(epoch = ?self.epoch_store.epoch()))]
289    pub async fn run_epoch(self, run_with_range: Option<RunWithRange>) -> StopReason {
290        let _metrics_scope = mysten_metrics::monitored_scope("CheckpointExecutor::run_epoch");
291        info!(?run_with_range, "CheckpointExecutor::run_epoch");
292        debug!(
293            "Checkpoint executor running for epoch {:?}",
294            self.epoch_store.epoch(),
295        );
296
297        // check if we want to run this epoch based on RunWithRange condition value
298        // we want to be inclusive of the defined RunWithRangeEpoch::Epoch
299        // i.e Epoch(N) means we will execute epoch N and stop when reaching N+1
300        if run_with_range.is_some_and(|rwr| rwr.is_epoch_gt(self.epoch_store.epoch())) {
301            info!("RunWithRange condition satisfied at {:?}", run_with_range,);
302            return StopReason::RunWithRangeCondition;
303        };
304
305        self.metrics
306            .checkpoint_exec_epoch
307            .set(self.epoch_store.epoch() as i64);
308
309        let Some(next_to_schedule) = self.get_next_to_schedule() else {
310            return StopReason::EpochComplete;
311        };
312
313        let this = Arc::new(self);
314
315        let concurrency = std::env::var("SUI_CHECKPOINT_EXECUTION_MAX_CONCURRENCY")
316            .ok()
317            .and_then(|s| s.parse().ok())
318            .unwrap_or(this.config.checkpoint_execution_max_concurrency);
319
320        let pipeline_stages = PipelineStages::new(next_to_schedule, this.metrics.clone());
321
322        let final_checkpoint_executed = stream_synced_checkpoints(
323            this.checkpoint_store.clone(),
324            next_to_schedule,
325            run_with_range.and_then(|rwr| rwr.into_checkpoint_bound()),
326        )
327        // Checkpoint loading and execution is parallelized
328        .map(|checkpoint| {
329            let this = this.clone();
330            let pipeline_handle = pipeline_stages.handle(*checkpoint.sequence_number());
331            async move {
332                let pipeline_handle = pipeline_handle.await;
333                tokio::spawn(this.execute_checkpoint(checkpoint, pipeline_handle))
334                    .await
335                    .and_then(|result| result)
336            }
337        })
338        .buffered(concurrency)
339        // Take the last value from the stream to determine if we completed the epoch
340        .try_fold(false, |state, is_final_checkpoint| async move {
341            assert!(
342                !state,
343                "fold can't be called again after the final checkpoint"
344            );
345            Ok(is_final_checkpoint)
346        })
347        .await;
348
349        match final_checkpoint_executed {
350            Ok(true) => StopReason::EpochComplete,
351            Ok(false) => StopReason::RunWithRangeCondition,
352            Err(e) if e.is_panic() => std::panic::resume_unwind(e.into_panic()),
353            // Tasks are only cancelled when the runtime shuts down.
354            Err(e) => {
355                info!("Checkpoint execution cancelled by runtime shutdown: {e}");
356                StopReason::RuntimeShutdown
357            }
358        }
359    }
360}
361
362impl CheckpointExecutor {
363    /// Load all data for a checkpoint, ensure all transactions are executed, and check for forks.
364    #[instrument(level = "info", skip_all, fields(seq = ?checkpoint.sequence_number()))]
365    async fn execute_checkpoint(
366        self: Arc<Self>,
367        checkpoint: VerifiedCheckpoint,
368        mut pipeline_handle: PipelineHandle,
369    ) -> Result<bool /* is final checkpoint */, JoinError> {
370        info!("executing checkpoint");
371        let sequence_number = checkpoint.sequence_number;
372
373        checkpoint.report_checkpoint_age(
374            &self.metrics.checkpoint_contents_age,
375            &self.metrics.checkpoint_contents_age_ms,
376        );
377        self.backpressure_manager
378            .update_highest_certified_checkpoint(sequence_number);
379
380        if checkpoint.is_last_checkpoint_of_epoch() && sequence_number > 0 {
381            let _wait_for_previous_checkpoints_guard = mysten_metrics::monitored_scope(
382                "CheckpointExecutor::wait_for_previous_checkpoints",
383            );
384
385            info!(
386                "Reached end of epoch checkpoint, waiting for all previous checkpoints to be executed"
387            );
388            self.checkpoint_store
389                .notify_read_executed_checkpoint(sequence_number - 1)
390                .await;
391        }
392
393        let _parallel_step_guard =
394            mysten_metrics::monitored_scope("CheckpointExecutor::parallel_step");
395
396        // Note: only `execute_transactions_from_synced_checkpoint` has end-of-epoch logic.
397        // For nodes that are not running consensus (Full nodes with checkpoint state sync only), always
398        // execute transactions via synced checkpoints. The rest of nodes should attempt only to verify the locally
399        // build checkpoints as those should (potentially) be already executed.
400        let ckpt_state = if !self.epoch_store.node_role().runs_consensus()
401            || checkpoint.is_last_checkpoint_of_epoch()
402        {
403            self.execute_transactions_from_synced_checkpoint(checkpoint, &mut pipeline_handle)
404                .await
405        } else {
406            self.verify_locally_built_checkpoint(checkpoint, &mut pipeline_handle)
407                .await
408        };
409
410        let tps = self.tps_estimator.lock().update(
411            Instant::now(),
412            ckpt_state.data.checkpoint.network_total_transactions,
413        );
414        self.metrics.checkpoint_exec_sync_tps.set(tps as i64);
415
416        self.backpressure_manager
417            .update_highest_executed_checkpoint(*ckpt_state.data.checkpoint.sequence_number());
418
419        let is_final_checkpoint = ckpt_state.data.checkpoint.is_last_checkpoint_of_epoch();
420
421        let seq = ckpt_state.data.checkpoint.sequence_number;
422
423        let mut batch = self
424            .state
425            .get_cache_commit()
426            .build_db_batch(self.epoch_store.epoch(), &ckpt_state.data.tx_digests);
427
428        // Stamp the highest-committed-checkpoint watermark into the same batch
429        // as the outputs, so it lands atomically with the object writes. This
430        // gives consumers that read the live object set directly (the embedded
431        // rpc-store restore) a watermark that never lags the durable objects,
432        // unlike the separately-bumped `highest_executed` watermark below.
433        self.state
434            .get_cache_commit()
435            .set_highest_committed_checkpoint_in_batch(&mut batch, seq);
436
437        finish_stage!(pipeline_handle, BuildDbBatch);
438
439        // commit_accumulator_versions can only be called after the checkpoint is fully executed.
440        // This is the earliest point where we can guarantee that no transactions will be reading
441        // the unsettled object withdraws for the committed accumulator versions.
442        let committed_accumulator_versions = batch
443            .0
444            .iter()
445            .filter_map(|outputs| {
446                outputs.effects.object_changes().into_iter().find_map(|o| {
447                    if o.id == SUI_ACCUMULATOR_ROOT_OBJECT_ID {
448                        o.input_version
449                    } else {
450                        None
451                    }
452                })
453            })
454            .collect::<Vec<_>>();
455        self.state
456            .unsettled_object_withdrawals
457            .commit_accumulator_versions(committed_accumulator_versions);
458
459        let mut ckpt_state = tokio::task::spawn_blocking({
460            let this = self.clone();
461            move || {
462                // Commit all transaction effects to disk
463                let cache_commit = this.state.get_cache_commit();
464                debug!(?seq, "committing checkpoint transactions to disk");
465                cache_commit.commit_transaction_outputs(
466                    this.epoch_store.epoch(),
467                    batch,
468                    &ckpt_state.data.tx_digests,
469                );
470                ckpt_state
471            }
472        })
473        .await?;
474
475        finish_stage!(pipeline_handle, CommitTransactionOutputs);
476
477        self.epoch_store
478            .handle_finalized_checkpoint(&ckpt_state.data.checkpoint, &ckpt_state.data.tx_digests)
479            .expect("cannot fail");
480
481        let randomness_rounds = self.extract_randomness_rounds(
482            &ckpt_state.data.checkpoint,
483            &ckpt_state.data.checkpoint_contents,
484        );
485
486        // Once the checkpoint is finalized, we know that any randomness contained in this checkpoint has
487        // been successfully included in a checkpoint certified by quorum of validators.
488        // (RandomnessManager/RandomnessReporter is only present on validators.)
489        if let Some(randomness_reporter) = self.epoch_store.randomness_reporter() {
490            for round in randomness_rounds {
491                debug!(
492                    ?round,
493                    "notifying RandomnessReporter that randomness update was executed in checkpoint"
494                );
495                randomness_reporter
496                    .notify_randomness_in_checkpoint(round)
497                    .expect("epoch cannot have ended");
498            }
499        }
500
501        finish_stage!(pipeline_handle, FinalizeCheckpoint);
502
503        if let Some(checkpoint_data) = ckpt_state.full_data.take() {
504            self.enqueue_to_subscription_service(checkpoint_data);
505        }
506
507        finish_stage!(pipeline_handle, UpdateRpcIndex);
508
509        self.global_state_hasher
510            .accumulate_running_root(&self.epoch_store, seq, ckpt_state.state_hasher)
511            .expect("Failed to accumulate running root");
512
513        if is_final_checkpoint {
514            self.checkpoint_store
515                .insert_epoch_last_checkpoint(self.epoch_store.epoch(), &ckpt_state.data.checkpoint)
516                .expect("Failed to insert epoch last checkpoint");
517
518            self.global_state_hasher
519                .accumulate_epoch(self.epoch_store.clone(), seq)
520                .expect("Accumulating epoch cannot fail");
521
522            self.checkpoint_store
523                .prune_local_summaries()
524                .tap_err(|e| debug_fatal!("Failed to prune local summaries: {}", e))
525                .ok();
526        }
527
528        fail_point!("crash");
529
530        self.bump_highest_executed_checkpoint(&ckpt_state.data.checkpoint);
531
532        finish_stage!(pipeline_handle, BumpHighestExecutedCheckpoint);
533
534        // Important: code after the last pipeline stage is finished can run out of checkpoint order.
535
536        Ok(ckpt_state.data.checkpoint.is_last_checkpoint_of_epoch())
537    }
538
539    // On validators, checkpoints have often already been constructed locally, in which
540    // case we can skip many steps of the checkpoint execution process.
541    // If the node is a validator, then the checkpoint execution state will not contain the full data.
542    // If the node is a full node that has consensus state sync enabled, then the full data will be populated as they are required
543    // by downstream components.
544    #[instrument(level = "info", skip_all)]
545    async fn verify_locally_built_checkpoint(
546        &self,
547        checkpoint: VerifiedCheckpoint,
548        pipeline_handle: &mut PipelineHandle,
549    ) -> CheckpointExecutionState {
550        assert!(
551            !checkpoint.is_last_checkpoint_of_epoch(),
552            "only fullnode path has end-of-epoch logic"
553        );
554
555        let sequence_number = checkpoint.sequence_number;
556        let locally_built_checkpoint = self
557            .checkpoint_store
558            .get_locally_computed_checkpoint(sequence_number)
559            .expect("db error");
560
561        let Some(locally_built_checkpoint) = locally_built_checkpoint else {
562            // fall back to tx-by-tx execution path if we are catching up.
563            self.metrics
564                .checkpoint_executor_validator_sync_fallback_path
565                .inc();
566            return self
567                .execute_transactions_from_synced_checkpoint(checkpoint, pipeline_handle)
568                .await;
569        };
570
571        self.metrics.checkpoint_executor_validator_path.inc();
572
573        // Check for fork
574        assert_checkpoint_not_forked(
575            &locally_built_checkpoint,
576            &checkpoint,
577            &self.checkpoint_store,
578        );
579
580        // Checkpoint builder triggers accumulation of the checkpoint, so this is guaranteed to finish.
581        let state_hasher = {
582            let _metrics_scope =
583                mysten_metrics::monitored_scope("CheckpointExecutor::notify_read_state_hasher");
584            self.epoch_store
585                .notify_read_checkpoint_state_hasher(&[sequence_number])
586                .await
587                .pop()
588                .unwrap()
589        };
590
591        // Observer fullnodes have already executed these transactions through consensus,
592        // but still need the fullnode side effects that the synced path normally performs.
593        if matches!(
594            self.epoch_store.node_role(),
595            NodeRole::FullNode(FullNodeSyncMode::ConsensusObserver)
596        ) {
597            pipeline_handle
598                .skip_to(PipelineStage::FinalizeTransactions)
599                .await;
600
601            let (state, tx_data) =
602                self.load_checkpoint_transactions(checkpoint, Some(state_hasher));
603
604            return self
605                .finalize_executed_checkpoint_transactions(state, &tx_data, pipeline_handle)
606                .await;
607        }
608
609        let checkpoint_contents = self
610            .checkpoint_store
611            .get_checkpoint_contents(&checkpoint.content_digest)
612            .expect("db error")
613            .expect("checkpoint contents not found");
614
615        let (tx_digests, fx_digests): (Vec<_>, Vec<_>) = checkpoint_contents
616            .iter()
617            .map(|digests| (digests.transaction, digests.effects))
618            .unzip();
619
620        pipeline_handle
621            .skip_to(PipelineStage::FinalizeTransactions)
622            .await;
623
624        self.insert_finalized_transactions(&tx_digests, sequence_number);
625
626        pipeline_handle.skip_to(PipelineStage::BuildDbBatch).await;
627
628        let ckpt_data = CheckpointExecutionData {
629            checkpoint,
630            checkpoint_contents,
631            tx_digests,
632            fx_digests,
633        };
634        CheckpointExecutionState::new_with_global_state_hasher(ckpt_data, state_hasher)
635    }
636
637    #[instrument(level = "info", skip_all)]
638    async fn execute_transactions_from_synced_checkpoint(
639        &self,
640        checkpoint: VerifiedCheckpoint,
641        pipeline_handle: &mut PipelineHandle,
642    ) -> CheckpointExecutionState {
643        let (ckpt_state, tx_data, unexecuted_tx_digests) = {
644            let _scope =
645                mysten_metrics::monitored_scope("CheckpointExecutor::execute_transactions");
646            let (ckpt_state, tx_data) = self.load_checkpoint_transactions(checkpoint, None);
647            let unexecuted_tx_digests = self.schedule_transaction_execution(&ckpt_state, &tx_data);
648            (ckpt_state, tx_data, unexecuted_tx_digests)
649        };
650
651        finish_stage!(pipeline_handle, ExecuteTransactions);
652
653        {
654            self.transaction_cache_reader
655                .notify_read_executed_effects_digests(
656                    "CheckpointExecutor::notify_read_executed_effects_digests",
657                    &unexecuted_tx_digests,
658                )
659                .await;
660        }
661
662        finish_stage!(pipeline_handle, WaitForTransactions);
663
664        if ckpt_state.data.checkpoint.is_last_checkpoint_of_epoch() {
665            self.execute_change_epoch_tx(&tx_data, ckpt_state.data.checkpoint.sequence_number)
666                .await;
667        }
668
669        self.finalize_executed_checkpoint_transactions(ckpt_state, &tx_data, pipeline_handle)
670            .await
671    }
672
673    async fn finalize_executed_checkpoint_transactions(
674        &self,
675        mut ckpt_state: CheckpointExecutionState,
676        tx_data: &CheckpointTransactionData,
677        pipeline_handle: &mut PipelineHandle,
678    ) -> CheckpointExecutionState {
679        let sequence_number = ckpt_state.data.checkpoint.sequence_number;
680
681        let _scope = mysten_metrics::monitored_scope("CheckpointExecutor::finalize_checkpoint");
682
683        if self.state.is_fullnode(&self.epoch_store) {
684            self.state.congestion_tracker.process_checkpoint_effects(
685                &*self.transaction_cache_reader,
686                &ckpt_state.data.checkpoint,
687                &tx_data.effects,
688            );
689        }
690
691        self.insert_finalized_transactions(&ckpt_state.data.tx_digests, sequence_number);
692
693        // The early versions of the hasher (prior to effectsv2) rely on db
694        // state, so we must wait until all transactions have been executed
695        // before accumulating the checkpoint.
696        if ckpt_state.state_hasher.is_none() {
697            ckpt_state.state_hasher = Some(self.global_state_hasher.accumulate_checkpoint(
698                &tx_data.effects,
699                sequence_number,
700                &self.epoch_store,
701            ));
702        }
703
704        finish_stage!(pipeline_handle, FinalizeTransactions);
705
706        ckpt_state.full_data = self.process_checkpoint_data(&ckpt_state.data, tx_data);
707
708        finish_stage!(pipeline_handle, ProcessCheckpointData);
709
710        ckpt_state
711    }
712
713    fn checkpoint_data_enabled(&self) -> bool {
714        self.subscription_service_checkpoint_sender.is_some()
715            || self.config.data_ingestion_dir.is_some()
716    }
717
718    fn insert_finalized_transactions(
719        &self,
720        tx_digests: &[TransactionDigest],
721        sequence_number: CheckpointSequenceNumber,
722    ) {
723        self.epoch_store
724            .insert_finalized_transactions(tx_digests, sequence_number)
725            .expect("failed to insert finalized transactions");
726
727        if self.state.is_fullnode(&self.epoch_store) {
728            // TODO remove once we no longer need to support this table for read RPC
729            self.state
730                .get_checkpoint_cache()
731                .deprecated_insert_finalized_transactions(
732                    tx_digests,
733                    self.epoch_store.epoch(),
734                    sequence_number,
735                );
736        }
737    }
738
739    #[instrument(level = "info", skip_all)]
740    fn process_checkpoint_data(
741        &self,
742        ckpt_data: &CheckpointExecutionData,
743        tx_data: &CheckpointTransactionData,
744    ) -> Option<Checkpoint> {
745        if !self.checkpoint_data_enabled() {
746            return None;
747        }
748
749        let checkpoint = load_checkpoint(
750            ckpt_data,
751            tx_data,
752            self.state.get_object_store(),
753            &*self.transaction_cache_reader,
754        )
755        .expect("failed to load checkpoint data");
756
757        if let Some(path) = &self.config.data_ingestion_dir {
758            store_checkpoint_locally(path, &checkpoint)
759                .expect("failed to store checkpoint locally");
760        }
761
762        Some(checkpoint)
763    }
764
765    // Load all required transaction and effects data for the checkpoint.
766    // When `state_hasher` is provided the returned `CheckpointExecutionState`
767    // carries the hasher (used by the verify-locally-built-checkpoint path).
768    #[instrument(level = "info", skip_all)]
769    fn load_checkpoint_transactions(
770        &self,
771        checkpoint: VerifiedCheckpoint,
772        state_hasher: Option<GlobalStateHash>,
773    ) -> (CheckpointExecutionState, CheckpointTransactionData) {
774        let seq = checkpoint.sequence_number;
775        let epoch = checkpoint.epoch;
776
777        let checkpoint_contents = self
778            .checkpoint_store
779            .get_checkpoint_contents(&checkpoint.content_digest)
780            .expect("db error")
781            .expect("checkpoint contents not found");
782
783        let checkpoint_state = |data| match state_hasher {
784            Some(hasher) => CheckpointExecutionState::new_with_global_state_hasher(data, hasher),
785            None => CheckpointExecutionState::new(data),
786        };
787
788        // attempt to load full checkpoint contents in bulk
789        // Tolerate db error in case of data corruption.
790        // We will fall back to loading items one-by-one below in case of error.
791        if let Some(full_contents) = self
792            .checkpoint_store
793            .get_full_checkpoint_contents_by_sequence_number(seq)
794            .tap_err(|e| debug_fatal!("Failed to get checkpoint contents from store: {e}"))
795            .ok()
796            .flatten()
797            .tap_some(|_| debug!("loaded full checkpoint contents in bulk for sequence {seq}"))
798        {
799            let num_txns = full_contents.size();
800            let mut tx_digests = Vec::with_capacity(num_txns);
801            let mut transactions = Vec::with_capacity(num_txns);
802            let mut effects = Vec::with_capacity(num_txns);
803            let mut fx_digests = Vec::with_capacity(num_txns);
804
805            full_contents
806                .into_iter()
807                .zip_debug_eq(checkpoint_contents.iter())
808                .for_each(|(execution_data, digests)| {
809                    let tx_digest = digests.transaction;
810                    let fx_digest = digests.effects;
811                    debug_assert_eq!(tx_digest, *execution_data.transaction.digest());
812                    debug_assert_eq!(fx_digest, execution_data.effects.digest());
813
814                    tx_digests.push(tx_digest);
815                    transactions.push(VerifiedExecutableTransaction::new_from_checkpoint(
816                        VerifiedTransaction::new_unchecked(execution_data.transaction),
817                        epoch,
818                        seq,
819                    ));
820                    effects.push(execution_data.effects);
821                    fx_digests.push(fx_digest);
822                });
823
824            let executed_fx_digests = self
825                .transaction_cache_reader
826                .multi_get_executed_effects_digests(&tx_digests);
827
828            (
829                checkpoint_state(CheckpointExecutionData {
830                    checkpoint,
831                    checkpoint_contents,
832                    tx_digests,
833                    fx_digests,
834                }),
835                CheckpointTransactionData::new(transactions, effects, executed_fx_digests),
836            )
837        } else {
838            // load items one-by-one
839            // TODO: If we used RocksDbStore in the executor instead,
840            // all the logic below could be removed.
841
842            let digests = checkpoint_contents.inner();
843
844            let (tx_digests, fx_digests): (Vec<_>, Vec<_>) = digests
845                .digests_iter()
846                .map(|d| (d.transaction, d.effects))
847                .unzip();
848            let transactions = self
849                .transaction_cache_reader
850                .multi_get_transaction_blocks(&tx_digests)
851                .into_iter()
852                .enumerate()
853                .map(|(i, tx)| {
854                    let tx = tx
855                        .unwrap_or_else(|| fatal!("transaction not found for {:?}", tx_digests[i]));
856                    let tx = Arc::try_unwrap(tx).unwrap_or_else(|tx| (*tx).clone());
857                    VerifiedExecutableTransaction::new_from_checkpoint(tx, epoch, seq)
858                })
859                .collect();
860            let effects = self
861                .transaction_cache_reader
862                .multi_get_effects(&fx_digests)
863                .into_iter()
864                .enumerate()
865                .map(|(i, effect)| {
866                    effect.unwrap_or_else(|| {
867                        fatal!("checkpoint effect not found for {:?}", digests[i])
868                    })
869                })
870                .collect();
871
872            let executed_fx_digests = self
873                .transaction_cache_reader
874                .multi_get_executed_effects_digests(&tx_digests);
875
876            (
877                checkpoint_state(CheckpointExecutionData {
878                    checkpoint,
879                    checkpoint_contents,
880                    tx_digests,
881                    fx_digests,
882                }),
883                CheckpointTransactionData::new(transactions, effects, executed_fx_digests),
884            )
885        }
886    }
887
888    // Schedule all unexecuted transactions in the checkpoint for execution
889    #[instrument(level = "info", skip_all)]
890    fn schedule_transaction_execution(
891        &self,
892        ckpt_state: &CheckpointExecutionState,
893        tx_data: &CheckpointTransactionData,
894    ) -> Vec<TransactionDigest> {
895        let mut barrier_deps_builder = BarrierDependencyBuilder::new();
896
897        // Find unexecuted transactions and their expected effects digests
898        let (unexecuted_tx_digests, unexecuted_txns): (Vec<_>, Vec<_>) = itertools::multiunzip(
899            izip_debug_eq!(
900                tx_data.transactions.iter(),
901                ckpt_state.data.tx_digests.iter(),
902                ckpt_state.data.fx_digests.iter(),
903                tx_data.effects.iter(),
904                tx_data.executed_fx_digests.iter(),
905                tx_data.accumulator_versions.iter()
906            )
907            .filter_map(
908                |(
909                    txn,
910                    tx_digest,
911                    expected_fx_digest,
912                    effects,
913                    executed_fx_digest,
914                    accumulator_version,
915                )| {
916                    let barrier_deps =
917                        barrier_deps_builder.process_tx(*tx_digest, txn.transaction_data());
918
919                    if let Some(executed_fx_digest) = executed_fx_digest {
920                        assert_not_forked(
921                            &ckpt_state.data.checkpoint,
922                            tx_digest,
923                            expected_fx_digest,
924                            executed_fx_digest,
925                            &*self.transaction_cache_reader,
926                        );
927                        None
928                    } else if txn.transaction_data().is_end_of_epoch_tx() {
929                        None
930                    } else {
931                        let assigned_versions = self
932                            .epoch_store
933                            .acquire_shared_version_assignments_from_effects(
934                                txn,
935                                effects,
936                                *accumulator_version,
937                                &*self.object_cache_reader,
938                            );
939
940                        let mut env = ExecutionEnv::new()
941                            .with_assigned_versions(assigned_versions)
942                            .with_expected_effects(ExpectedEffectsDigest::Certified {
943                                digest: *expected_fx_digest,
944                                checkpoint_seq: ckpt_state.data.checkpoint.sequence_number,
945                            })
946                            .with_barrier_dependencies(barrier_deps);
947
948                        // Check if the expected effects indicate insufficient balance
949                        if let &ExecutionStatus::Failure(ExecutionFailure {
950                            error: ExecutionErrorKind::InsufficientFundsForWithdraw,
951                            ..
952                        }) = effects.status()
953                        {
954                            env = env.with_insufficient_funds();
955                        }
956
957                        Some((tx_digest, (txn.clone(), env)))
958                    }
959                },
960            ),
961        );
962
963        // When a randomness update arrives via checkpoint rather than being constructed
964        // locally from the round's signature, nothing else resolves its transaction key,
965        // and the keyed placeholder enqueued by consensus would wait forever.
966        for (txn, _) in &unexecuted_txns {
967            if let Some(key) = txn.non_digest_key() {
968                self.epoch_store.insert_tx_key(key, *txn.digest());
969            }
970        }
971
972        // Enqueue unexecuted transactions with their expected effects digests
973        self.execution_scheduler
974            .enqueue_transactions(unexecuted_txns, &self.epoch_store);
975
976        unexecuted_tx_digests
977    }
978
979    // Execute the change epoch txn
980    #[instrument(level = "error", skip_all)]
981    async fn execute_change_epoch_tx(
982        &self,
983        tx_data: &CheckpointTransactionData,
984        checkpoint_seq: CheckpointSequenceNumber,
985    ) {
986        let change_epoch_tx = tx_data.transactions.last().unwrap();
987        let change_epoch_fx = tx_data.effects.last().unwrap();
988        assert_eq!(
989            change_epoch_tx.digest(),
990            change_epoch_fx.transaction_digest()
991        );
992        assert!(
993            change_epoch_tx.transaction_data().is_end_of_epoch_tx(),
994            "final txn must be an end of epoch txn"
995        );
996
997        // Ordinarily we would assert that the change epoch txn has not been executed yet.
998        // However, during crash recovery, it is possible that we already passed this point and
999        // the txn has been executed. You can uncomment this assert if you are debugging a problem
1000        // related to reconfig. If you hit this assert and it is not because of crash-recovery,
1001        // it may indicate a bug in the checkpoint executor.
1002        //
1003        //     if self
1004        //         .transaction_cache_reader
1005        //         .get_executed_effects(change_epoch_tx.digest())
1006        //         .is_some()
1007        //     {
1008        //         fatal!(
1009        //             "end of epoch txn must not have been executed: {:?}",
1010        //             change_epoch_tx.digest()
1011        //         );
1012        //     }
1013
1014        let assigned_versions = self
1015            .epoch_store
1016            .acquire_shared_version_assignments_from_effects(
1017                change_epoch_tx,
1018                change_epoch_fx,
1019                None,
1020                self.object_cache_reader.as_ref(),
1021            );
1022
1023        info!(
1024            "scheduling change epoch txn with digest: {:?}, expected effects digest: {:?}, assigned versions: {:?}",
1025            change_epoch_tx.digest(),
1026            change_epoch_fx.digest(),
1027            assigned_versions
1028        );
1029        self.execution_scheduler.enqueue_transactions(
1030            vec![(
1031                change_epoch_tx.clone(),
1032                ExecutionEnv::new()
1033                    .with_assigned_versions(assigned_versions)
1034                    .with_expected_effects(ExpectedEffectsDigest::Certified {
1035                        digest: change_epoch_fx.digest(),
1036                        checkpoint_seq,
1037                    }),
1038            )],
1039            &self.epoch_store,
1040        );
1041
1042        self.transaction_cache_reader
1043            .notify_read_executed_effects_digests(
1044                "CheckpointExecutor::notify_read_advance_epoch_tx",
1045                &[*change_epoch_tx.digest()],
1046            )
1047            .await;
1048    }
1049
1050    // Increment the highest executed checkpoint watermark and prune old full-checkpoint contents
1051    #[instrument(level = "debug", skip_all)]
1052    fn bump_highest_executed_checkpoint(&self, checkpoint: &VerifiedCheckpoint) {
1053        // Ensure that we are not skipping checkpoints at any point
1054        let seq = *checkpoint.sequence_number();
1055        debug!("Bumping highest_executed_checkpoint watermark to {seq:?}");
1056        if let Some(prev_highest) = self
1057            .checkpoint_store
1058            .get_highest_executed_checkpoint_seq_number()
1059            .unwrap()
1060        {
1061            assert_eq!(prev_highest + 1, seq);
1062        } else {
1063            assert_eq!(seq, 0);
1064        }
1065        if seq.is_multiple_of(CHECKPOINT_PROGRESS_LOG_COUNT_INTERVAL) {
1066            info!("Finished syncing and executing checkpoint {}", seq);
1067        }
1068
1069        fail_point!("highest-executed-checkpoint");
1070
1071        // We store a fixed number of additional FullCheckpointContents after execution is complete
1072        // for use in state sync.
1073        const NUM_SAVED_FULL_CHECKPOINT_CONTENTS: u64 = 5_000;
1074        if seq >= NUM_SAVED_FULL_CHECKPOINT_CONTENTS {
1075            let prune_seq = seq - NUM_SAVED_FULL_CHECKPOINT_CONTENTS;
1076            if let Some(prune_checkpoint) = self
1077                .checkpoint_store
1078                .get_checkpoint_by_sequence_number(prune_seq)
1079                .expect("Failed to fetch checkpoint")
1080            {
1081                self.checkpoint_store
1082                    .delete_full_checkpoint_contents(prune_seq)
1083                    .expect("Failed to delete full checkpoint contents");
1084                self.checkpoint_store
1085                    .delete_contents_digest_sequence_number_mapping(
1086                        &prune_checkpoint.content_digest,
1087                    )
1088                    .expect("Failed to delete contents digest -> sequence number mapping");
1089            } else {
1090                // If this is directly after a snapshot restore with skiplisting,
1091                // this is expected for the first `NUM_SAVED_FULL_CHECKPOINT_CONTENTS`
1092                // checkpoints.
1093                debug!(
1094                    "Failed to fetch checkpoint with sequence number {:?}",
1095                    prune_seq
1096                );
1097            }
1098        }
1099
1100        self.checkpoint_store
1101            .update_highest_executed_checkpoint(checkpoint)
1102            .unwrap();
1103        self.metrics.last_executed_checkpoint.set(seq as i64);
1104
1105        self.metrics
1106            .last_executed_checkpoint_timestamp_ms
1107            .set(checkpoint.timestamp_ms as i64);
1108        checkpoint.report_checkpoint_age(
1109            &self.metrics.last_executed_checkpoint_age,
1110            &self.metrics.last_executed_checkpoint_age_ms,
1111        );
1112    }
1113
1114    /// Publish the checkpoint to the broadcast stream so its downstream
1115    /// consumers (the RPC subscription service and the embedded rpc-store
1116    /// indexer) can pick it up.
1117    #[instrument(level = "info", skip_all)]
1118    fn enqueue_to_subscription_service(&self, checkpoint: Checkpoint) {
1119        // Best-effort, non-blocking publish to the broadcast stream. A send
1120        // error just means there are no live subscribers right now, which is
1121        // fine: subscribers (the RPC subscription service and the embedded
1122        // rpc-store indexer) recover any checkpoints they miss by fetching from
1123        // the local stores.
1124        if let Some(sender) = &self.subscription_service_checkpoint_sender {
1125            let _ = sender.send(Arc::new(checkpoint));
1126        }
1127    }
1128
1129    // Extract randomness rounds from the checkpoint version-specific data (if available).
1130    // Otherwise, extract randomness rounds from the first transaction in the checkpoint
1131    #[instrument(level = "debug", skip_all)]
1132    fn extract_randomness_rounds(
1133        &self,
1134        checkpoint: &VerifiedCheckpoint,
1135        checkpoint_contents: &CheckpointContents,
1136    ) -> Vec<RandomnessRound> {
1137        if let Some(version_specific_data) = checkpoint
1138            .version_specific_data(self.epoch_store.protocol_config())
1139            .expect("unable to get version_specific_data")
1140        {
1141            // With version-specific data, randomness rounds are stored in checkpoint summary.
1142            version_specific_data.into_v1().randomness_rounds
1143        } else {
1144            // Before version-specific data, checkpoint batching must be disabled. In this case,
1145            // randomness state update tx must be first if it exists, because all other
1146            // transactions in a checkpoint that includes a randomness state update are causally
1147            // dependent on it.
1148            assert_eq!(
1149                0,
1150                self.epoch_store
1151                    .protocol_config()
1152                    .min_checkpoint_interval_ms_as_option()
1153                    .unwrap_or_default(),
1154            );
1155            if let Some(first_digest) = checkpoint_contents.inner().first_digests() {
1156                let maybe_randomness_tx = self.transaction_cache_reader.get_transaction_block(&first_digest.transaction)
1157                .unwrap_or_else(||
1158                    fatal!(
1159                        "state-sync should have ensured that transaction with digests {first_digest:?} exists for checkpoint: {}",
1160                        checkpoint.sequence_number()
1161                    )
1162                );
1163                if let TransactionKind::RandomnessStateUpdate(rsu) =
1164                    maybe_randomness_tx.data().transaction_data().kind()
1165                {
1166                    vec![rsu.randomness_round]
1167                } else {
1168                    Vec::new()
1169                }
1170            } else {
1171                Vec::new()
1172            }
1173        }
1174    }
1175}