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