kona_node_service/actors/engine/finalizer.rs
1//! The [`L2Finalizer`].
2
3use kona_engine::{EngineTask, FinalizeTask};
4use kona_protocol::{BlockInfo, OpAttributesWithParent};
5use std::collections::BTreeMap;
6use tokio::sync::watch;
7
8use crate::actors::engine::actor::EngineActorState;
9
10/// An internal type alias for L1 block numbers.
11type L1BlockNumber = u64;
12
13/// An internal type alias for L2 block numbers.
14type L2BlockNumber = u64;
15
16/// The [`L2Finalizer`] is responsible for finalizing L2 blocks derived from finalized L1 blocks.
17/// It maintains a queue of derived L2 blocks that are awaiting finalization, and finalizes them
18/// as new finalized L1 blocks are received.
19#[derive(Debug)]
20pub struct L2Finalizer {
21 /// A channel that receives new finalized L1 blocks intermittently.
22 finalized_l1_block_rx: watch::Receiver<Option<BlockInfo>>,
23 /// A map of `L1 block number -> highest derived L2 block number` within the L1 epoch, used to
24 /// track derived [`OpAttributesWithParent`] awaiting finalization. When a new finalized L1
25 /// block is received, the highest L2 block whose inputs are contained within the finalized
26 /// L1 chain is finalized.
27 awaiting_finalization: BTreeMap<L1BlockNumber, L2BlockNumber>,
28}
29
30impl L2Finalizer {
31 /// Creates a new [`L2Finalizer`] with the given channel receiver for finalized L1 blocks.
32 pub const fn new(finalized_l1_block_rx: watch::Receiver<Option<BlockInfo>>) -> Self {
33 Self { finalized_l1_block_rx, awaiting_finalization: BTreeMap::new() }
34 }
35
36 /// Enqueues a derived [`OpAttributesWithParent`] for finalization. When a new finalized L1
37 /// block is observed that is `>=` the height of [`OpAttributesWithParent::derived_from`], the
38 /// L2 block associated with the payload attributes will be finalized.
39 pub fn enqueue_for_finalization(&mut self, attributes: &OpAttributesWithParent) {
40 self.awaiting_finalization
41 .entry(
42 attributes.derived_from.map(|b| b.number).expect(
43 "Fatal: Cannot enqueue attributes for finalization that weren't derived",
44 ),
45 )
46 .and_modify(|n| *n = (*n).max(attributes.block_number()))
47 .or_insert(attributes.block_number());
48 }
49
50 /// Clears the finalization queue.
51 pub fn clear(&mut self) {
52 self.awaiting_finalization.clear();
53 }
54
55 /// Receives a new finalized L1 block from the channel.
56 pub async fn new_finalized_block(&mut self) -> Result<(), watch::error::RecvError> {
57 self.finalized_l1_block_rx.changed().await
58 }
59
60 /// Attempts to finalize any L2 blocks that the finalizer knows about and are contained within
61 /// the new finalized L1 chain.
62 pub(super) async fn try_finalize_next(&mut self, engine_state: &mut EngineActorState) {
63 // If there is no finalized L1 block available in the watch channel, do nothing.
64 let Some(new_finalized_l1) = *self.finalized_l1_block_rx.borrow() else {
65 return;
66 };
67
68 // Find the highest safe L2 block that is contained within the finalized chain,
69 // that the finalizer is aware of.
70 let highest_safe = self.awaiting_finalization.range(..=new_finalized_l1.number).next_back();
71
72 // If the highest safe block is found, enqueue a finalization task and drain the
73 // queue of all L1 blocks not contained in the finalized L1 chain.
74 if let Some((_, highest_safe_number)) = highest_safe {
75 let task = EngineTask::Finalize(FinalizeTask::new(
76 engine_state.client.clone(),
77 engine_state.rollup.clone(),
78 *highest_safe_number,
79 ));
80 engine_state.engine.enqueue(task);
81
82 self.awaiting_finalization.retain(|&number, _| number > new_finalized_l1.number);
83 }
84 }
85}