kona_engine/state/
core.rs

1//! The internal state of the engine controller.
2
3use crate::Metrics;
4use alloy_rpc_types_engine::ForkchoiceState;
5use kona_protocol::L2BlockInfo;
6use serde::{Deserialize, Serialize};
7
8/// The synchronization state of the execution layer across different safety levels.
9///
10/// Tracks block progression through various stages of verification and finalization,
11/// from initial unsafe blocks received via P2P to fully finalized blocks derived from
12/// finalized L1 data. Each level represents increasing confidence in the block's validity.
13///
14/// # Safety Levels
15///
16/// The state tracks blocks at different safety levels, listed from least to most safe:
17///
18/// 1. **Unsafe** - Most recent blocks from P2P network (unverified)
19/// 2. **Cross-unsafe** - Unsafe blocks with cross-layer verification
20/// 3. **Local-safe** - Derived from L1 data, completed span-batch
21/// 4. **Safe** - Cross-verified with safe L1 dependencies
22/// 5. **Finalized** - Derived from finalized L1 data only
23///
24/// See the [OP Stack specifications](https://specs.optimism.io) for detailed safety definitions.
25#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
26pub struct EngineSyncState {
27    /// Most recent block found on the P2P network (lowest safety level).
28    unsafe_head: L2BlockInfo,
29    /// Cross-verified unsafe head (equal to unsafe_head pre-interop).
30    cross_unsafe_head: L2BlockInfo,
31    /// Derived from L1 data as a completed span-batch, but not yet cross-verified.
32    local_safe_head: L2BlockInfo,
33    /// Derived from L1 data and cross-verified to have safe L1 dependencies.
34    safe_head: L2BlockInfo,
35    /// Derived from finalized L1 data with only finalized dependencies (highest safety level).
36    finalized_head: L2BlockInfo,
37}
38
39impl EngineSyncState {
40    /// Returns the current unsafe head.
41    pub const fn unsafe_head(&self) -> L2BlockInfo {
42        self.unsafe_head
43    }
44
45    /// Returns the current cross-verified unsafe head.
46    pub const fn cross_unsafe_head(&self) -> L2BlockInfo {
47        self.cross_unsafe_head
48    }
49
50    /// Returns the current local safe head.
51    pub const fn local_safe_head(&self) -> L2BlockInfo {
52        self.local_safe_head
53    }
54
55    /// Returns the current safe head.
56    pub const fn safe_head(&self) -> L2BlockInfo {
57        self.safe_head
58    }
59
60    /// Returns the current finalized head.
61    pub const fn finalized_head(&self) -> L2BlockInfo {
62        self.finalized_head
63    }
64
65    /// Creates a `ForkchoiceState`
66    ///
67    /// - `head_block` = `unsafe_head`
68    /// - `safe_block` = `safe_head`
69    /// - `finalized_block` = `finalized_head`
70    ///
71    /// If the block info is not yet available, the default values are used.
72    pub const fn create_forkchoice_state(&self) -> ForkchoiceState {
73        ForkchoiceState {
74            head_block_hash: self.unsafe_head.hash(),
75            safe_block_hash: self.safe_head.hash(),
76            finalized_block_hash: self.finalized_head.hash(),
77        }
78    }
79
80    /// Applies the update to the provided sync state, using the current state values if the update
81    /// is not specified. Returns the new sync state.
82    pub fn apply_update(self, sync_state_update: EngineSyncStateUpdate) -> Self {
83        if let Some(unsafe_head) = sync_state_update.unsafe_head {
84            Self::update_block_label_metric(
85                Metrics::UNSAFE_BLOCK_LABEL,
86                unsafe_head.block_info.number,
87            );
88        }
89        if let Some(cross_unsafe_head) = sync_state_update.cross_unsafe_head {
90            Self::update_block_label_metric(
91                Metrics::CROSS_UNSAFE_BLOCK_LABEL,
92                cross_unsafe_head.block_info.number,
93            );
94        }
95        if let Some(local_safe_head) = sync_state_update.local_safe_head {
96            Self::update_block_label_metric(
97                Metrics::LOCAL_SAFE_BLOCK_LABEL,
98                local_safe_head.block_info.number,
99            );
100        }
101        if let Some(safe_head) = sync_state_update.safe_head {
102            Self::update_block_label_metric(Metrics::SAFE_BLOCK_LABEL, safe_head.block_info.number);
103        }
104        if let Some(finalized_head) = sync_state_update.finalized_head {
105            Self::update_block_label_metric(
106                Metrics::FINALIZED_BLOCK_LABEL,
107                finalized_head.block_info.number,
108            );
109        }
110
111        Self {
112            unsafe_head: sync_state_update.unsafe_head.unwrap_or(self.unsafe_head),
113            cross_unsafe_head: sync_state_update
114                .cross_unsafe_head
115                .unwrap_or(self.cross_unsafe_head),
116            local_safe_head: sync_state_update.local_safe_head.unwrap_or(self.local_safe_head),
117            safe_head: sync_state_update.safe_head.unwrap_or(self.safe_head),
118            finalized_head: sync_state_update.finalized_head.unwrap_or(self.finalized_head),
119        }
120    }
121
122    /// Updates a block label metric, keyed by the label.
123    #[inline]
124    fn update_block_label_metric(label: &'static str, number: u64) {
125        kona_macros::set!(gauge, Metrics::BLOCK_LABELS, "label", label, number as f64);
126    }
127}
128
129/// Specifies how to update the sync state of the engine.
130#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub struct EngineSyncStateUpdate {
132    /// Most recent block found on the p2p network
133    pub unsafe_head: Option<L2BlockInfo>,
134    /// Cross-verified unsafe head, always equal to the unsafe head pre-interop
135    pub cross_unsafe_head: Option<L2BlockInfo>,
136    /// Derived from L1, and known to be a completed span-batch,
137    /// but not cross-verified yet.
138    pub local_safe_head: Option<L2BlockInfo>,
139    /// Derived from L1 and cross-verified to have cross-safe dependencies.
140    pub safe_head: Option<L2BlockInfo>,
141    /// Derived from finalized L1 data,
142    /// and cross-verified to only have finalized dependencies.
143    pub finalized_head: Option<L2BlockInfo>,
144}
145
146/// The chain state viewed by the engine controller.
147#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
148pub struct EngineState {
149    /// The sync state of the engine.
150    pub sync_state: EngineSyncState,
151
152    /// Whether or not the EL has finished syncing.
153    pub el_sync_finished: bool,
154
155    /// Track when the rollup node changes the forkchoice to restore previous
156    /// known unsafe chain. e.g. Unsafe Reorg caused by Invalid span batch.
157    /// This update does not retry except engine returns non-input error
158    /// because engine may forgot backupUnsafeHead or backupUnsafeHead is not part
159    /// of the chain.
160    pub need_fcu_call_backup_unsafe_reorg: bool,
161}
162
163impl EngineState {
164    /// Returns if consolidation is needed.
165    ///
166    /// [Consolidation] is only performed by a rollup node when the unsafe head
167    /// is ahead of the safe head. When the two are equal, consolidation isn't
168    /// required and the [`crate::BuildTask`] can be used to build the block.
169    ///
170    /// [Consolidation]: https://specs.optimism.io/protocol/derivation.html#l1-consolidation-payload-attributes-matching
171    pub fn needs_consolidation(&self) -> bool {
172        self.sync_state.safe_head() != self.sync_state.unsafe_head()
173    }
174}
175
176#[cfg(test)]
177mod test {
178    use super::*;
179    use crate::Metrics;
180    use kona_protocol::BlockInfo;
181    use metrics_exporter_prometheus::PrometheusBuilder;
182    use rstest::rstest;
183
184    impl EngineState {
185        /// Set the unsafe head.
186        pub fn set_unsafe_head(&mut self, unsafe_head: L2BlockInfo) {
187            self.sync_state.apply_update(EngineSyncStateUpdate {
188                unsafe_head: Some(unsafe_head),
189                ..Default::default()
190            });
191        }
192
193        /// Set the cross-verified unsafe head.
194        pub fn set_cross_unsafe_head(&mut self, cross_unsafe_head: L2BlockInfo) {
195            self.sync_state.apply_update(EngineSyncStateUpdate {
196                cross_unsafe_head: Some(cross_unsafe_head),
197                ..Default::default()
198            });
199        }
200
201        /// Set the local safe head.
202        pub fn set_local_safe_head(&mut self, local_safe_head: L2BlockInfo) {
203            self.sync_state.apply_update(EngineSyncStateUpdate {
204                local_safe_head: Some(local_safe_head),
205                ..Default::default()
206            });
207        }
208
209        /// Set the safe head.
210        pub fn set_safe_head(&mut self, safe_head: L2BlockInfo) {
211            self.sync_state.apply_update(EngineSyncStateUpdate {
212                safe_head: Some(safe_head),
213                ..Default::default()
214            });
215        }
216
217        /// Set the finalized head.
218        pub fn set_finalized_head(&mut self, finalized_head: L2BlockInfo) {
219            self.sync_state.apply_update(EngineSyncStateUpdate {
220                finalized_head: Some(finalized_head),
221                ..Default::default()
222            });
223        }
224    }
225
226    #[rstest]
227    #[case::set_unsafe(EngineState::set_unsafe_head, Metrics::UNSAFE_BLOCK_LABEL, 1)]
228    #[case::set_cross_unsafe(
229        EngineState::set_cross_unsafe_head,
230        Metrics::CROSS_UNSAFE_BLOCK_LABEL,
231        2
232    )]
233    #[case::set_local_safe(EngineState::set_local_safe_head, Metrics::LOCAL_SAFE_BLOCK_LABEL, 3)]
234    #[case::set_safe_head(EngineState::set_safe_head, Metrics::SAFE_BLOCK_LABEL, 4)]
235    #[case::set_finalized_head(EngineState::set_finalized_head, Metrics::FINALIZED_BLOCK_LABEL, 5)]
236    #[cfg(feature = "metrics")]
237    fn test_chain_label_metrics(
238        #[case] set_fn: impl Fn(&mut EngineState, L2BlockInfo),
239        #[case] label_name: &str,
240        #[case] number: u64,
241    ) {
242        let handle = PrometheusBuilder::new().install_recorder().unwrap();
243        crate::Metrics::init();
244
245        let mut state = EngineState::default();
246        set_fn(
247            &mut state,
248            L2BlockInfo {
249                block_info: BlockInfo { number, ..Default::default() },
250                ..Default::default()
251            },
252        );
253
254        assert!(handle.render().contains(
255            format!("kona_node_block_labels{{label=\"{label_name}\"}} {number}").as_str()
256        ));
257    }
258}