Skip to main content

iota_core/checkpoints/checkpoint_executor/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5//! CheckpointExecutor is a Node component that executes all checkpoints for the
6//! given epoch. It acts as a Consumer to StateSync
7//! for newly synced checkpoints, taking these checkpoints and
8//! scheduling and monitoring their execution. Its primary goal is to allow
9//! for catching up to the current checkpoint sequence number of the network
10//! as quickly as possible so that a newly joined, or recovering Node can
11//! participate in a timely manner. To that end, CheckpointExecutor attempts
12//! to saturate the CPU with executor tasks (one per checkpoint), each of which
13//! handle scheduling and awaiting checkpoint transaction execution.
14//!
15//! CheckpointExecutor is made recoverable in the event of Node shutdown by way
16//! of a watermark, highest_executed_checkpoint, which is guaranteed to be
17//! updated sequentially in order, despite checkpoints themselves potentially
18//! being executed nonsequentially and in parallel. CheckpointExecutor
19//! parallelizes checkpoints of the same epoch as much as possible.
20//! CheckpointExecutor enforces the invariant that if `run` returns
21//! successfully, we have reached the end of epoch. This allows us to use it as
22//! a signal for reconfig.
23
24use std::{sync::Arc, time::Instant};
25
26use futures::StreamExt;
27use iota_common::{debug_fatal, fatal};
28use iota_config::node::{CheckpointExecutorConfig, RunWithRange};
29use iota_macros::fail_point;
30use iota_sdk_types::{
31    RandomnessRound, TransactionDigest, TransactionEffects, TransactionEffectsDigest,
32    TransactionKind, checkpoint::CheckpointContents,
33};
34use iota_types::{
35    base_types::ExecutionData,
36    effects::TransactionEffectsAPI,
37    executable_transaction::VerifiedExecutableTransaction,
38    full_checkpoint_content::CheckpointData,
39    global_state_hash::GlobalStateHash,
40    messages_checkpoint::{
41        CheckpointContentsExt, CheckpointSequenceNumber, CheckpointSummaryExt,
42        FullCheckpointContents, VerifiedCheckpoint,
43    },
44    transaction::{
45        SenderSignedTransactionAPI, TransactionDataAPI, TransactionKey, VerifiedTransaction,
46    },
47};
48use parking_lot::Mutex;
49use tap::{TapFallible, TapOptional};
50use tracing::{debug, info, instrument};
51
52use crate::{
53    authority::{
54        AuthorityState, authority_per_epoch_store::AuthorityPerEpochStore,
55        backpressure::BackpressureManager,
56    },
57    checkpoint_progress_tracker::CheckpointProgressTracker,
58    checkpoints::CheckpointStore,
59    execution_cache::{ObjectCacheRead, TransactionCacheRead},
60    global_state_hasher::GlobalStateHasher,
61    transaction_manager::TransactionManager,
62};
63
64mod data_ingestion_handler;
65pub mod metrics;
66pub(crate) mod utils;
67
68#[cfg(test)]
69pub(crate) mod tests;
70
71use data_ingestion_handler::{load_checkpoint_data, store_checkpoint_locally};
72use metrics::CheckpointExecutorMetrics;
73use utils::*;
74
75type CheckpointDataSender = Box<dyn Fn(&CheckpointData) + Send + Sync>;
76
77#[derive(PartialEq, Eq, Debug)]
78pub enum StopReason {
79    EpochComplete,
80    RunWithRangeCondition,
81}
82
83pub(crate) struct CheckpointExecutionData {
84    pub checkpoint: VerifiedCheckpoint,
85    pub checkpoint_contents: CheckpointContents,
86    pub tx_digests: Vec<TransactionDigest>,
87    pub fx_digests: Vec<TransactionEffectsDigest>,
88}
89
90pub(crate) struct CheckpointTransactionData {
91    pub transactions: Vec<VerifiedExecutableTransaction>,
92    pub effects: Vec<TransactionEffects>,
93    pub executed_fx_digests: Vec<Option<TransactionEffectsDigest>>,
94}
95
96pub(crate) struct CheckpointExecutionState {
97    pub data: CheckpointExecutionData,
98    state_hash: Option<GlobalStateHash>,
99    full_data: Option<CheckpointData>,
100}
101
102impl CheckpointExecutionState {
103    pub fn new(data: CheckpointExecutionData) -> Self {
104        Self {
105            data,
106            state_hash: None,
107            full_data: None,
108        }
109    }
110
111    pub fn new_with_global_state_hash(
112        data: CheckpointExecutionData,
113        hash: GlobalStateHash,
114    ) -> Self {
115        Self {
116            data,
117            state_hash: Some(hash),
118            full_data: None,
119        }
120    }
121}
122
123macro_rules! finish_stage {
124    ($handle:expr, $stage:ident) => {
125        $handle.finish_stage(PipelineStage::$stage).await;
126    };
127}
128
129pub struct CheckpointExecutor {
130    epoch_store: Arc<AuthorityPerEpochStore>,
131    state: Arc<AuthorityState>,
132    checkpoint_store: Arc<CheckpointStore>,
133    object_cache_reader: Arc<dyn ObjectCacheRead>,
134    transaction_cache_reader: Arc<dyn TransactionCacheRead>,
135    tx_manager: Arc<TransactionManager>,
136    global_state_hasher: Arc<GlobalStateHasher>,
137    backpressure_manager: Arc<BackpressureManager>,
138    config: CheckpointExecutorConfig,
139    metrics: Arc<CheckpointExecutorMetrics>,
140    tps_estimator: Mutex<TPSEstimator>,
141    checkpoint_progress_tracker: Option<Arc<CheckpointProgressTracker>>,
142    data_sender: Option<CheckpointDataSender>,
143}
144
145impl CheckpointExecutor {
146    pub fn new(
147        epoch_store: Arc<AuthorityPerEpochStore>,
148        checkpoint_store: Arc<CheckpointStore>,
149        state: Arc<AuthorityState>,
150        global_state_hasher: Arc<GlobalStateHasher>,
151        backpressure_manager: Arc<BackpressureManager>,
152        config: CheckpointExecutorConfig,
153        metrics: Arc<CheckpointExecutorMetrics>,
154        data_sender: Option<CheckpointDataSender>,
155        checkpoint_progress_tracker: Option<Arc<CheckpointProgressTracker>>,
156    ) -> Self {
157        Self {
158            epoch_store,
159            state: state.clone(),
160            checkpoint_store,
161            object_cache_reader: state.get_object_cache_reader().clone(),
162            transaction_cache_reader: state.get_transaction_cache_reader().clone(),
163            tx_manager: state.transaction_manager().clone(),
164            global_state_hasher,
165            backpressure_manager,
166            config,
167            metrics,
168            tps_estimator: Mutex::new(TPSEstimator::default()),
169            checkpoint_progress_tracker,
170            data_sender,
171        }
172    }
173
174    pub fn new_for_tests(
175        epoch_store: Arc<AuthorityPerEpochStore>,
176        checkpoint_store: Arc<CheckpointStore>,
177        state: Arc<AuthorityState>,
178        global_state_hasher: Arc<GlobalStateHasher>,
179    ) -> Self {
180        Self::new(
181            epoch_store,
182            checkpoint_store.clone(),
183            state,
184            global_state_hasher,
185            BackpressureManager::new_from_checkpoint_store(&checkpoint_store),
186            Default::default(),
187            CheckpointExecutorMetrics::new_for_tests(),
188            None, // No callback for data
189            None, // No progress tracker for tests
190        )
191    }
192
193    // Gets the next checkpoint to schedule for execution. If the epoch is already
194    // completed, returns None.
195    fn get_next_to_schedule(&self) -> Option<CheckpointSequenceNumber> {
196        // Decide the first checkpoint to schedule for execution.
197        // If we haven't executed anything in the past, we schedule checkpoint 0.
198        // Otherwise we schedule the one after highest executed.
199        let highest_executed = self
200            .checkpoint_store
201            .get_highest_executed_checkpoint()
202            .unwrap();
203
204        if let Some(highest_executed) = &highest_executed {
205            if self.epoch_store.epoch() == highest_executed.epoch()
206                && highest_executed.is_last_checkpoint_of_epoch()
207            {
208                // We can arrive at this point if we bump the highest_executed_checkpoint
209                // watermark, and then crash before completing reconfiguration.
210                info!(seq = ?highest_executed.sequence_number, "final checkpoint of epoch has already been executed");
211                return None;
212            }
213        }
214
215        Some(
216            highest_executed
217                .as_ref()
218                .map(|c| c.sequence_number() + 1)
219                .unwrap_or_else(|| {
220                    // TODO this invariant may no longer hold once we introduce snapshots
221                    assert_eq!(self.epoch_store.epoch(), 0);
222                    // we need to execute the genesis checkpoint
223                    0
224                }),
225        )
226    }
227
228    /// Execute all checkpoints for the current epoch, ensuring that the node
229    /// has not forked, and return when finished.
230    /// If `run_with_range` is set, execution will stop early.
231    #[instrument(level = "error", skip_all, fields(epoch = ?self.epoch_store.epoch()))]
232    pub async fn run_epoch(self, run_with_range: Option<RunWithRange>) -> StopReason {
233        let _metrics_scope = iota_metrics::monitored_scope("CheckpointExecutor::run_epoch");
234        info!(?run_with_range, "CheckpointExecutor::run_epoch");
235        debug!(
236            "Checkpoint executor running for epoch {:?}",
237            self.epoch_store.epoch(),
238        );
239
240        // check if we want to run this epoch based on RunWithRange condition value
241        // we want to be inclusive of the defined RunWithRangeEpoch::Epoch
242        // i.e Epoch(N) means we will execute epoch N and stop when reaching N+1
243        if run_with_range.is_some_and(|rwr| rwr.is_epoch_gt(self.epoch_store.epoch())) {
244            info!("RunWithRange condition satisfied at {:?}", run_with_range,);
245            return StopReason::RunWithRangeCondition;
246        };
247
248        self.metrics
249            .checkpoint_exec_epoch
250            .set(self.epoch_store.epoch() as i64);
251
252        let Some(next_to_schedule) = self.get_next_to_schedule() else {
253            return StopReason::EpochComplete;
254        };
255
256        let this = Arc::new(self);
257
258        let concurrency = std::env::var("IOTA_CHECKPOINT_EXECUTION_MAX_CONCURRENCY")
259            .ok()
260            .and_then(|s| s.parse().ok())
261            .unwrap_or(this.config.checkpoint_execution_max_concurrency);
262
263        let pipeline_stages = PipelineStages::new(next_to_schedule, this.metrics.clone());
264
265        let final_checkpoint_executed = stream_synced_checkpoints(
266            this.checkpoint_store.clone(),
267            next_to_schedule,
268            run_with_range.and_then(|rwr| rwr.into_checkpoint_bound()),
269        )
270        // Checkpoint loading and execution is parallelized
271        .map(|checkpoint| {
272            let this = this.clone();
273            let pipeline_handle = pipeline_stages.handle(checkpoint.sequence_number());
274            async move {
275                let pipeline_handle = pipeline_handle.await;
276                tokio::spawn(this.execute_checkpoint(checkpoint, pipeline_handle))
277                    .await
278                    .unwrap()
279            }
280        })
281        .buffered(concurrency)
282        // Take the last value from the stream to determine if we completed the epoch
283        .fold(false, |state, is_final_checkpoint| async move {
284            assert!(!state, "Cannot execute checkpoint after epoch end");
285            is_final_checkpoint
286        })
287        .await;
288
289        if final_checkpoint_executed {
290            StopReason::EpochComplete
291        } else {
292            StopReason::RunWithRangeCondition
293        }
294    }
295}
296
297impl CheckpointExecutor {
298    /// Load all data for a checkpoint, ensure all transactions are executed,
299    /// and check for forks.
300    #[instrument(level = "debug", skip_all, fields(seq = ?checkpoint.sequence_number()))]
301    async fn execute_checkpoint(
302        self: Arc<Self>,
303        checkpoint: VerifiedCheckpoint,
304        mut pipeline_handle: PipelineHandle,
305    ) -> bool /* is final checkpoint */ {
306        debug!("executing checkpoint");
307        let sequence_number = checkpoint.sequence_number;
308
309        checkpoint.report_checkpoint_age(&self.metrics.checkpoint_contents_age);
310        self.backpressure_manager
311            .update_highest_certified_checkpoint(sequence_number);
312
313        if checkpoint.is_last_checkpoint_of_epoch() && sequence_number > 0 {
314            let _wait_for_previous_checkpoints_guard =
315                iota_metrics::monitored_scope("CheckpointExecutor::wait_for_previous_checkpoints");
316
317            info!(
318                "Reached end of epoch checkpoint, waiting for all previous checkpoints to be executed"
319            );
320            self.checkpoint_store
321                .notify_read_executed_checkpoint(sequence_number - 1)
322                .await;
323        }
324
325        let _parallel_step_guard =
326            iota_metrics::monitored_scope("CheckpointExecutor::parallel_step");
327
328        // Note: only `execute_transactions_from_synced_checkpoint` has end-of-epoch
329        // logic.
330        let exec_start = Instant::now();
331        let ckpt_state = if self.state.is_fullnode(&self.epoch_store)
332            || checkpoint.is_last_checkpoint_of_epoch()
333        {
334            self.execute_transactions_from_synced_checkpoint(checkpoint, &mut pipeline_handle)
335                .await
336        } else {
337            self.verify_locally_built_checkpoint(checkpoint, &mut pipeline_handle)
338                .await
339        };
340
341        let tps = self.tps_estimator.lock().update(
342            Instant::now(),
343            ckpt_state.data.checkpoint.network_total_transactions,
344        );
345        self.metrics.checkpoint_exec_sync_tps.set(tps as i64);
346
347        self.backpressure_manager
348            .update_highest_executed_checkpoint(ckpt_state.data.checkpoint.sequence_number());
349
350        let is_final_checkpoint = ckpt_state.data.checkpoint.is_last_checkpoint_of_epoch();
351
352        let seq = ckpt_state.data.checkpoint.sequence_number;
353
354        let batch = self.state.get_cache_commit().build_db_batch(
355            self.epoch_store.epoch(),
356            seq,
357            &ckpt_state.data.tx_digests,
358        );
359
360        finish_stage!(pipeline_handle, BuildDbBatch);
361
362        let mut ckpt_state = tokio::task::spawn_blocking({
363            let this = self.clone();
364            move || {
365                // Commit all transaction effects to disk
366                let cache_commit = this.state.get_cache_commit();
367                debug!(?seq, "committing checkpoint transactions to disk");
368                cache_commit.commit_transaction_outputs(
369                    this.epoch_store.epoch(),
370                    batch,
371                    &ckpt_state.data.tx_digests,
372                );
373                ckpt_state
374            }
375        })
376        .await
377        .unwrap();
378
379        finish_stage!(pipeline_handle, CommitTransactionOutputs);
380
381        self.epoch_store
382            .handle_finalized_checkpoint(&ckpt_state.data.checkpoint, &ckpt_state.data.tx_digests)
383            .expect("cannot fail");
384
385        let randomness_rounds = self.extract_randomness_rounds(
386            &ckpt_state.data.checkpoint,
387            &ckpt_state.data.checkpoint_contents,
388        );
389
390        if self.state.is_fullnode(&self.epoch_store) {
391            let epoch = ckpt_state.data.checkpoint.epoch;
392            // Remove version assignments on fullnodes after checkpoint execution.
393            // On validators, version assignments are removed when consensus output is
394            // committed. We cannot remove here on validators because checkpoint
395            // execution can run ahead of consensus, which would then re-insert
396            // version assignments.
397            self.epoch_store.remove_shared_version_assignments(
398                randomness_rounds
399                    .iter()
400                    .map(|round| TransactionKey::RandomnessRound(epoch, *round)),
401            );
402
403            self.epoch_store.remove_shared_version_assignments(
404                ckpt_state
405                    .data
406                    .tx_digests
407                    .iter()
408                    .copied()
409                    .map(TransactionKey::Digest),
410            );
411        }
412
413        // Once the checkpoint is finalized, we know that any randomness contained in
414        // this checkpoint has been successfully included in a checkpoint
415        // certified by quorum of validators. (RandomnessManager/
416        // RandomnessReporter is only present on validators.)
417        if let Some(randomness_reporter) = self.epoch_store.randomness_reporter() {
418            for round in randomness_rounds {
419                debug!(
420                    ?round,
421                    "notifying RandomnessReporter that randomness update was executed in checkpoint"
422                );
423                randomness_reporter
424                    .notify_randomness_in_checkpoint(round)
425                    .expect("epoch cannot have ended");
426            }
427        }
428
429        finish_stage!(pipeline_handle, FinalizeCheckpoint);
430
431        if let Some(checkpoint_data) = ckpt_state.full_data.take() {
432            self.commit_index_updates(checkpoint_data);
433        }
434
435        finish_stage!(pipeline_handle, UpdateRpcIndex);
436
437        self.global_state_hasher
438            .accumulate_running_root(&self.epoch_store, seq, ckpt_state.state_hash)
439            .expect("Failed to accumulate running root");
440
441        if is_final_checkpoint {
442            self.checkpoint_store
443                .insert_epoch_last_checkpoint(self.epoch_store.epoch(), &ckpt_state.data.checkpoint)
444                .expect("Failed to insert epoch last checkpoint");
445
446            self.global_state_hasher
447                .accumulate_epoch(self.epoch_store.clone(), seq)
448                .expect("Accumulating epoch cannot fail");
449
450            self.checkpoint_store
451                .prune_local_summaries()
452                .tap_err(|e| debug_fatal!("Failed to prune local summaries: {}", e))
453                .ok();
454        }
455
456        fail_point!("crash");
457
458        self.bump_highest_executed_checkpoint(&ckpt_state.data.checkpoint);
459
460        self.broadcast_checkpoint(&ckpt_state.data, ckpt_state.full_data.as_ref());
461
462        // Nudge the pruner now that this checkpoint is executed and available;
463        // pruning of aged-out data runs off the propagation path.
464        self.state
465            .pruner()
466            .nudge(ckpt_state.data.checkpoint.sequence_number());
467
468        finish_stage!(pipeline_handle, BumpHighestExecutedCheckpoint);
469
470        if let Some(tracker) = &self.checkpoint_progress_tracker {
471            tracker.add_execution_time(exec_start.elapsed());
472        }
473
474        // Important: code after the last pipeline stage is finished can run out of
475        // checkpoint order.
476
477        ckpt_state.data.checkpoint.is_last_checkpoint_of_epoch()
478    }
479
480    // On validators, checkpoints have often already been constructed locally, in
481    // which case we can skip many steps of the checkpoint execution process.
482    #[instrument(level = "info", skip_all)]
483    async fn verify_locally_built_checkpoint(
484        &self,
485        checkpoint: VerifiedCheckpoint,
486        pipeline_handle: &mut PipelineHandle,
487    ) -> CheckpointExecutionState {
488        assert!(
489            !checkpoint.is_last_checkpoint_of_epoch(),
490            "only fullnode path has end-of-epoch logic"
491        );
492
493        let sequence_number = checkpoint.sequence_number;
494        let locally_built_checkpoint = self
495            .checkpoint_store
496            .get_locally_computed_checkpoint(sequence_number)
497            .expect("db error");
498
499        let Some(locally_built_checkpoint) = locally_built_checkpoint else {
500            // fall back to tx-by-tx execution path if we are catching up.
501            return self
502                .execute_transactions_from_synced_checkpoint(checkpoint, pipeline_handle)
503                .await;
504        };
505
506        self.metrics.checkpoint_executor_validator_path.inc();
507
508        // Check for fork
509        assert_checkpoint_not_forked(
510            &locally_built_checkpoint,
511            &checkpoint,
512            &self.checkpoint_store,
513        );
514
515        // Checkpoint builder triggers accumulation of the checkpoint, so this is
516        // guaranteed to finish.
517        let state_hash = {
518            let _metrics_scope =
519                iota_metrics::monitored_scope("CheckpointExecutor::notify_read_state_hash");
520            self.epoch_store
521                .notify_read_checkpoint_state_hasher(&[sequence_number])
522                .await
523                .unwrap()
524                .pop()
525                .unwrap()
526        };
527
528        // Checkpoint builder triggers accumulation of the checkpoint, so this is
529        // guaranteed to finish.
530
531        let checkpoint_contents = self
532            .checkpoint_store
533            .get_checkpoint_contents(&checkpoint.contents_digest)
534            .expect("db error")
535            .expect("checkpoint contents not found");
536
537        let (tx_digests, fx_digests): (Vec<_>, Vec<_>) = checkpoint_contents
538            .iter()
539            .map(|digests| (digests.transaction, digests.effects))
540            .unzip();
541
542        pipeline_handle
543            .skip_to(PipelineStage::FinalizeTransactions)
544            .await;
545
546        // Currently this code only runs on validators, where this method call does
547        // nothing. But in the future, fullnodes may follow the consensus dag
548        // and build their own checkpoints.
549        self.insert_finalized_transactions(&tx_digests, sequence_number, checkpoint.timestamp_ms);
550
551        pipeline_handle.skip_to(PipelineStage::BuildDbBatch).await;
552
553        CheckpointExecutionState::new_with_global_state_hash(
554            CheckpointExecutionData {
555                checkpoint,
556                checkpoint_contents,
557                tx_digests,
558                fx_digests,
559            },
560            state_hash,
561        )
562    }
563
564    #[instrument(level = "info", skip_all)]
565    async fn execute_transactions_from_synced_checkpoint(
566        &self,
567        checkpoint: VerifiedCheckpoint,
568        pipeline_handle: &mut PipelineHandle,
569    ) -> CheckpointExecutionState {
570        let sequence_number = checkpoint.sequence_number;
571
572        let (mut ckpt_state, tx_data, unexecuted_tx_digests) = {
573            let _scope = iota_metrics::monitored_scope("CheckpointExecutor::execute_transactions");
574            let (ckpt_state, tx_data) = self.load_checkpoint_transactions(checkpoint);
575            let unexecuted_tx_digests = self.schedule_transaction_execution(&ckpt_state, &tx_data);
576            (ckpt_state, tx_data, unexecuted_tx_digests)
577        };
578
579        finish_stage!(pipeline_handle, ExecuteTransactions);
580
581        {
582            self.transaction_cache_reader
583                .notify_read_executed_effects_digests(
584                    "CheckpointExecutor::notify_read_executed_effects_digests",
585                    &unexecuted_tx_digests,
586                )
587                .await;
588        }
589
590        finish_stage!(pipeline_handle, WaitForTransactions);
591
592        if ckpt_state.data.checkpoint.is_last_checkpoint_of_epoch() {
593            self.execute_change_epoch_tx(&tx_data).await;
594        }
595
596        let _scope = iota_metrics::monitored_scope("CheckpointExecutor::finalize_checkpoint");
597
598        if self.state.is_fullnode(&self.epoch_store) {
599            self.state.congestion_tracker.process_checkpoint_effects(
600                &*self.transaction_cache_reader,
601                &ckpt_state.data.checkpoint,
602                &tx_data.effects,
603            );
604        }
605
606        self.insert_finalized_transactions(
607            &ckpt_state.data.tx_digests,
608            sequence_number,
609            ckpt_state.data.checkpoint.timestamp_ms,
610        );
611
612        // The early versions of the hasher (prior to effectsv2) rely on db
613        // state, so we must wait until all transactions have been executed
614        // before accumulating the checkpoint.
615        ckpt_state.state_hash = Some(
616            self.global_state_hasher
617                .accumulate_checkpoint(&tx_data.effects, sequence_number, &self.epoch_store)
618                .expect("epoch cannot have ended"),
619        );
620
621        finish_stage!(pipeline_handle, FinalizeTransactions);
622
623        ckpt_state.full_data = self.process_checkpoint_data(&ckpt_state.data, &tx_data);
624
625        finish_stage!(pipeline_handle, ProcessCheckpointData);
626
627        ckpt_state
628    }
629
630    fn checkpoint_data_enabled(&self) -> bool {
631        self.state.grpc_indexes_store.is_some()
632            || self.config.data_ingestion_dir.is_some()
633            || self.data_sender.is_some()
634    }
635
636    fn insert_finalized_transactions(
637        &self,
638        tx_digests: &[TransactionDigest],
639        sequence_number: CheckpointSequenceNumber,
640        timestamp_ms: u64,
641    ) {
642        self.epoch_store
643            .insert_finalized_transactions(tx_digests, sequence_number, timestamp_ms)
644            .expect("failed to insert finalized transactions");
645
646        if self.state.is_fullnode(&self.epoch_store) {
647            // TODO remove once we no longer need to support this table for read RPC
648            self.state
649                .get_checkpoint_cache()
650                .insert_finalized_transactions_perpetual_checkpoints(
651                    tx_digests,
652                    self.epoch_store.epoch(),
653                    sequence_number,
654                );
655        }
656    }
657
658    #[instrument(level = "info", skip_all)]
659    fn process_checkpoint_data(
660        &self,
661        ckpt_data: &CheckpointExecutionData,
662        tx_data: &CheckpointTransactionData,
663    ) -> Option<CheckpointData> {
664        let is_checkpoint_data_enabled = self.checkpoint_data_enabled();
665        // Boundaries always need full `CheckpointData` to persist `epoch_info`,
666        // even when no other consumer is configured.
667        let is_last_checkpoint_of_epoch = ckpt_data.checkpoint.is_last_checkpoint_of_epoch();
668        if !is_checkpoint_data_enabled && !is_last_checkpoint_of_epoch {
669            return None;
670        }
671
672        let checkpoint_data = load_checkpoint_data(
673            ckpt_data,
674            tx_data,
675            self.state.get_object_store(),
676            &*self.transaction_cache_reader,
677        )
678        .expect("failed to load checkpoint data");
679
680        // Persist the boundary's `epoch_info` row eagerly. Two properties make
681        // that safe. Boundaries run in epoch order: the boundary waits for every
682        // earlier checkpoint (`notify_read_executed_checkpoint(seq - 1)` in
683        // `execute_checkpoint`) and the stream stops at epoch end, so two
684        // boundaries are never in flight. And `index_epoch_boundary` is
685        // idempotent — a row upsert plus a contiguous +1 watermark guard — so a
686        // crash before the executed watermark advances just re-applies it on the
687        // next run.
688        if is_last_checkpoint_of_epoch {
689            self.checkpoint_store
690                .index_epoch_boundary(&checkpoint_data)
691                .expect("failed to persist epoch info at boundary");
692        }
693
694        if !is_checkpoint_data_enabled {
695            // Data was built solely to seed `epoch_info`; skip the other consumers.
696            return None;
697        }
698
699        // Index the checkpoint. The grpc indexes accumulate non-idempotent state
700        // (owner indexes, live-object sets), so each update must land exactly
701        // once. Indexing runs here out of order (checkpoints execute
702        // concurrently), so the write is only staged now and committed later, in
703        // sequence order, via `commit_update_for_checkpoint` — keeping the grpc
704        // watermark consistent with the executed checkpoint and crash-safe.
705        if let Some(grpc_indexes_store) = &self.state.grpc_indexes_store {
706            grpc_indexes_store.index_checkpoint(&checkpoint_data);
707        }
708
709        if let Some(path) = &self.config.data_ingestion_dir {
710            store_checkpoint_locally(path, &checkpoint_data)
711                .expect("failed to store checkpoint locally");
712        }
713
714        Some(checkpoint_data)
715    }
716
717    // Load all required transaction and effects data for the checkpoint.
718    #[instrument(level = "info", skip_all)]
719    fn load_checkpoint_transactions(
720        &self,
721        checkpoint: VerifiedCheckpoint,
722    ) -> (CheckpointExecutionState, CheckpointTransactionData) {
723        let seq = checkpoint.sequence_number;
724        let epoch = checkpoint.epoch;
725
726        let checkpoint_contents = self
727            .checkpoint_store
728            .get_checkpoint_contents(&checkpoint.contents_digest)
729            .expect("db error")
730            .expect("checkpoint contents not found");
731
732        // attempt to load full checkpoint contents in bulk
733        if let Some(full_contents) = self
734            .checkpoint_store
735            .get_full_checkpoint_contents_by_sequence_number(seq)
736            .tap_some(|_| debug!("loaded full checkpoint contents in bulk for sequence {seq}"))
737        {
738            let num_txns = full_contents.size();
739            let mut tx_digests = Vec::with_capacity(num_txns);
740            let mut transactions = Vec::with_capacity(num_txns);
741            let mut effects = Vec::with_capacity(num_txns);
742            let mut fx_digests = Vec::with_capacity(num_txns);
743
744            full_contents
745                .iter()
746                .zip(checkpoint_contents.iter())
747                .for_each(|(execution_data, digests)| {
748                    let tx_digest = digests.transaction;
749                    let fx_digest = digests.effects;
750                    debug_assert_eq!(tx_digest, *execution_data.transaction.digest());
751                    debug_assert_eq!(fx_digest, execution_data.effects.digest());
752
753                    tx_digests.push(tx_digest);
754                    transactions.push(VerifiedExecutableTransaction::new_from_checkpoint(
755                        VerifiedTransaction::new_unchecked(execution_data.transaction.clone()),
756                        epoch,
757                        seq,
758                    ));
759                    effects.push(execution_data.effects.clone());
760                    fx_digests.push(fx_digest);
761                });
762
763            let executed_fx_digests = self
764                .transaction_cache_reader
765                .multi_get_executed_effects_digests(&tx_digests);
766
767            (
768                CheckpointExecutionState::new(CheckpointExecutionData {
769                    checkpoint,
770                    checkpoint_contents,
771                    tx_digests,
772                    fx_digests,
773                }),
774                CheckpointTransactionData {
775                    transactions,
776                    effects,
777                    executed_fx_digests,
778                },
779            )
780        } else {
781            // load items one-by-one
782
783            let digests = checkpoint_contents.transactions();
784
785            let (tx_digests, fx_digests): (Vec<_>, Vec<_>) =
786                digests.iter().map(|d| (d.transaction, d.effects)).unzip();
787            let verified_transactions: Vec<VerifiedTransaction> = self
788                .transaction_cache_reader
789                .multi_get_transaction_blocks(&tx_digests)
790                .into_iter()
791                .enumerate()
792                .map(|(i, tx)| {
793                    let tx = tx
794                        .unwrap_or_else(|| fatal!("transaction not found for {:?}", tx_digests[i]));
795                    Arc::try_unwrap(tx).unwrap_or_else(|tx| (*tx).clone())
796                })
797                .collect();
798            let effects: Vec<TransactionEffects> = self
799                .transaction_cache_reader
800                .multi_get_effects(&fx_digests)
801                .into_iter()
802                .enumerate()
803                .map(|(i, effect)| {
804                    effect.unwrap_or_else(|| {
805                        fatal!("checkpoint effect not found for {:?}", digests[i])
806                    })
807                })
808                .collect();
809
810            // Reached when the contents were not already cached: a fullnode
811            // syncing from a peer, or a node catching up. Cache the assembled
812            // contents so this node can in turn serve state-sync peers without
813            // reconstruction. (Validators' locally built checkpoints are cached
814            // by the checkpoint builder and take the bulk path above instead.)
815            //
816            // The assembly clones every transaction and effect, so skip it
817            // when the cache wouldn't retain the entry (see `should_cache`).
818            if self
819                .checkpoint_store
820                .should_cache_full_checkpoint_contents(seq)
821            {
822                let execution_data = verified_transactions
823                    .iter()
824                    .zip(effects.iter())
825                    .map(|(tx, fx)| ExecutionData::new(tx.clone().into_inner(), fx.clone()));
826                let full_contents = FullCheckpointContents::from_contents_and_execution_data(
827                    checkpoint_contents.clone(),
828                    execution_data,
829                );
830                self.checkpoint_store.cache_full_checkpoint_contents(
831                    seq,
832                    checkpoint.contents_digest,
833                    full_contents,
834                );
835            }
836
837            let transactions = verified_transactions
838                .into_iter()
839                .map(|tx| VerifiedExecutableTransaction::new_from_checkpoint(tx, epoch, seq))
840                .collect();
841
842            let executed_fx_digests = self
843                .transaction_cache_reader
844                .multi_get_executed_effects_digests(&tx_digests);
845
846            (
847                CheckpointExecutionState::new(CheckpointExecutionData {
848                    checkpoint,
849                    checkpoint_contents,
850                    tx_digests,
851                    fx_digests,
852                }),
853                CheckpointTransactionData {
854                    transactions,
855                    effects,
856                    executed_fx_digests,
857                },
858            )
859        }
860    }
861
862    // Schedule all unexecuted transactions in the checkpoint for execution
863    #[instrument(level = "info", skip_all)]
864    fn schedule_transaction_execution(
865        &self,
866        ckpt_state: &CheckpointExecutionState,
867        tx_data: &CheckpointTransactionData,
868    ) -> Vec<TransactionDigest> {
869        // Find unexecuted transactions and their expected effects digests
870        let (unexecuted_tx_digests, unexecuted_txns, unexecuted_effects): (Vec<_>, Vec<_>, Vec<_>) =
871            itertools::multiunzip(
872                itertools::izip!(
873                    tx_data.transactions.iter(),
874                    ckpt_state.data.tx_digests.iter(),
875                    ckpt_state.data.fx_digests.iter(),
876                    tx_data.effects.iter(),
877                    tx_data.executed_fx_digests.iter()
878                )
879                .filter_map(
880                    |(txn, tx_digest, expected_fx_digest, effects, executed_fx_digest)| {
881                        if let Some(executed_fx_digest) = executed_fx_digest {
882                            assert_not_forked(
883                                &ckpt_state.data.checkpoint,
884                                tx_digest,
885                                expected_fx_digest,
886                                executed_fx_digest,
887                                &*self.transaction_cache_reader,
888                            );
889                            None
890                        } else if txn.transaction().is_end_of_epoch_tx() {
891                            None
892                        } else {
893                            Some((tx_digest, (txn.clone(), *expected_fx_digest), effects))
894                        }
895                    },
896                ),
897            );
898
899        for ((tx, _), effects) in itertools::izip!(unexecuted_txns.iter(), unexecuted_effects) {
900            if tx.contains_shared_object() {
901                self.epoch_store
902                    .acquire_shared_version_assignments_from_effects(
903                        tx,
904                        effects,
905                        &*self.object_cache_reader,
906                    )
907                    .expect("failed to acquire shared version assignments");
908            }
909        }
910
911        // Enqueue unexecuted transactions with their expected effects digests
912        self.tx_manager
913            .enqueue_with_expected_effects_digest(unexecuted_txns, &self.epoch_store);
914
915        unexecuted_tx_digests
916    }
917
918    // Execute the change epoch txn
919    #[instrument(level = "error", skip_all)]
920    async fn execute_change_epoch_tx(&self, tx_data: &CheckpointTransactionData) {
921        let change_epoch_tx = tx_data.transactions.last().unwrap();
922        let change_epoch_fx = tx_data.effects.last().unwrap();
923        assert_eq!(
924            change_epoch_tx.digest(),
925            change_epoch_fx.transaction_digest()
926        );
927        assert!(
928            change_epoch_tx.transaction().is_end_of_epoch_tx(),
929            "final txn must be an end of epoch txn"
930        );
931
932        // Ordinarily we would assert that the change epoch txn has not been executed
933        // yet. However, during crash recovery, it is possible that we already
934        // passed this point and the txn has been executed. You can uncomment
935        // this assert if you are debugging a problem related to reconfig. If
936        // you hit this assert and it is not because of crash-recovery,
937        // it may indicate a bug in the checkpoint executor.
938        //
939        //     if self
940        //         .transaction_cache_reader
941        //         .get_executed_effects(change_epoch_tx.digest())
942        //         .is_some()
943        //     {
944        //         fatal!(
945        //             "end of epoch txn must not have been executed: {:?}",
946        //             change_epoch_tx.digest()
947        //         );
948        //     }
949
950        self.epoch_store
951            .acquire_shared_version_assignments_from_effects(
952                change_epoch_tx,
953                change_epoch_fx,
954                self.object_cache_reader.as_ref(),
955            )
956            .expect("Acquiring shared version assignments for change_epoch tx cannot fail");
957
958        info!(
959            "scheduling change epoch txn with digest: {:?}, expected effects digest: {:?}",
960            change_epoch_tx.digest(),
961            change_epoch_fx.digest()
962        );
963        self.tx_manager.enqueue_with_expected_effects_digest(
964            vec![(change_epoch_tx.clone(), change_epoch_fx.digest())],
965            &self.epoch_store,
966        );
967
968        self.transaction_cache_reader
969            .notify_read_executed_effects_digests(
970                "CheckpointExecutor::notify_read_advance_epoch_tx",
971                &[*change_epoch_tx.digest()],
972            )
973            .await;
974    }
975
976    // Increment the highest executed checkpoint watermark
977    #[instrument(level = "debug", skip_all)]
978    fn bump_highest_executed_checkpoint(&self, checkpoint: &VerifiedCheckpoint) {
979        // Ensure that we are not skipping checkpoints at any point
980        let seq = checkpoint.sequence_number();
981        debug!("Bumping highest_executed_checkpoint watermark to {seq:?}");
982        if let Some(prev_highest) = self
983            .checkpoint_store
984            .get_highest_executed_checkpoint_seq_number()
985            .unwrap()
986        {
987            assert_eq!(prev_highest + 1, seq);
988        } else {
989            assert_eq!(seq, 0);
990        }
991        fail_point!("highest-executed-checkpoint");
992
993        self.checkpoint_store
994            .update_highest_executed_checkpoint(checkpoint)
995            .unwrap();
996        self.metrics.last_executed_checkpoint.set(seq as i64);
997
998        self.metrics
999            .last_executed_checkpoint_timestamp_ms
1000            .set(checkpoint.timestamp_ms as i64);
1001        checkpoint.report_checkpoint_age(&self.metrics.last_executed_checkpoint_age);
1002    }
1003
1004    /// Helper to broadcast checkpoint summary and data if the
1005    /// channels are set.
1006    fn broadcast_checkpoint(
1007        &self,
1008        checkpoint_exec_data: &CheckpointExecutionData,
1009        checkpoint_data: Option<&CheckpointData>,
1010    ) {
1011        if let Some(data_sender) = &self.data_sender {
1012            let checkpoint_data = if let Some(data) = checkpoint_data {
1013                data.clone()
1014            } else {
1015                // Reconstruct checkpoint data if needed (rare case: data_sender configured but
1016                // checkpoint_data_enabled is false)
1017                let (_, tx_data) =
1018                    self.load_checkpoint_transactions(checkpoint_exec_data.checkpoint.clone());
1019                load_checkpoint_data(
1020                    checkpoint_exec_data,
1021                    &tx_data,
1022                    self.state.get_object_store(),
1023                    self.transaction_cache_reader.as_ref(),
1024                )
1025                .expect("Failed to load full CheckpointData")
1026            };
1027            data_sender(&checkpoint_data);
1028        }
1029
1030        debug!(
1031            "[Fullnode] Full CheckpointData is available: seq={}",
1032            checkpoint_exec_data.checkpoint.sequence_number()
1033        );
1034    }
1035
1036    /// If configured, commit the pending index updates for the provided
1037    /// checkpoint
1038    #[instrument(level = "info", skip_all)]
1039    fn commit_index_updates(&self, checkpoint: CheckpointData) {
1040        if let Some(grpc_indexes_store) = &self.state.grpc_indexes_store {
1041            grpc_indexes_store
1042                .commit_update_for_checkpoint(checkpoint.checkpoint_summary.sequence_number)
1043                .expect("failed to update gRPC indexes");
1044        }
1045    }
1046
1047    // Extract randomness rounds from the checkpoint version-specific data (if
1048    // available). Otherwise, extract randomness rounds from the first
1049    // transaction in the checkpoint
1050    #[instrument(level = "debug", skip_all)]
1051    fn extract_randomness_rounds(
1052        &self,
1053        checkpoint: &VerifiedCheckpoint,
1054        checkpoint_contents: &CheckpointContents,
1055    ) -> Vec<RandomnessRound> {
1056        if let Some(version_specific_data) = checkpoint
1057            .parse_version_specific_data(self.epoch_store.protocol_config())
1058            .expect("unable to get version_specific_data")
1059        {
1060            // With version-specific data, randomness rounds are stored in checkpoint
1061            // summary.
1062            version_specific_data.into_v1().randomness_rounds
1063        } else {
1064            // Before version-specific data, checkpoint batching must be disabled. In this
1065            // case, randomness state update tx must be first if it exists,
1066            // because all other transactions in a checkpoint that includes a
1067            // randomness state update are causally dependent on it.
1068            assert_eq!(
1069                0,
1070                self.epoch_store
1071                    .protocol_config()
1072                    .min_checkpoint_interval_ms_as_option()
1073                    .unwrap_or_default(),
1074            );
1075            if let Some(first_digest) = checkpoint_contents.transactions().first() {
1076                let maybe_randomness_tx = self.transaction_cache_reader.get_transaction_block(&first_digest.transaction)
1077                .unwrap_or_else(||
1078                    fatal!(
1079                        "state-sync should have ensured that transaction with digests {first_digest:?} exists for checkpoint: {}",
1080                        checkpoint.sequence_number()
1081                    )
1082                );
1083                if let TransactionKind::RandomnessStateUpdate(rsu) =
1084                    maybe_randomness_tx.data().transaction().kind()
1085                {
1086                    vec![rsu.randomness_round]
1087                } else {
1088                    Vec::new()
1089                }
1090            } else {
1091                Vec::new()
1092            }
1093        }
1094    }
1095}