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