kona_node_service/actors/sequencer/
origin_selector.rs1use alloy_primitives::B256;
4use alloy_provider::{Provider, RootProvider};
5use alloy_transport::{RpcError, TransportErrorKind};
6use async_trait::async_trait;
7use kona_genesis::RollupConfig;
8use kona_protocol::{BlockInfo, L2BlockInfo};
9use std::sync::Arc;
10use tokio::sync::watch;
11
12#[derive(Debug)]
15pub struct L1OriginSelector<P: L1OriginSelectorProvider> {
16 cfg: Arc<RollupConfig>,
18 l1: P,
20 current: Option<BlockInfo>,
22 next: Option<BlockInfo>,
24}
25
26impl<P: L1OriginSelectorProvider> L1OriginSelector<P> {
27 pub const fn new(cfg: Arc<RollupConfig>, l1: P) -> Self {
29 Self { cfg, l1, current: None, next: None }
30 }
31
32 pub const fn current(&self) -> Option<&BlockInfo> {
34 self.current.as_ref()
35 }
36
37 pub const fn next(&self) -> Option<&BlockInfo> {
39 self.next.as_ref()
40 }
41
42 pub async fn next_l1_origin(
50 &mut self,
51 unsafe_head: L2BlockInfo,
52 is_recovery_mode: bool,
53 ) -> Result<BlockInfo, L1OriginSelectorError> {
54 self.select_origins(&unsafe_head, is_recovery_mode).await?;
55
56 if let Some(next) = self.next {
59 if unsafe_head.block_info.timestamp + self.cfg.block_time >= next.timestamp {
60 return Ok(next);
61 }
62 }
63
64 let Some(current) = self.current else {
65 unreachable!("Current L1 origin should always be set by `select_origins`");
66 };
67
68 let max_seq_drift = self.cfg.max_sequencer_drift(current.timestamp);
69 let past_seq_drift = unsafe_head.block_info.timestamp + self.cfg.block_time -
70 current.timestamp >
71 max_seq_drift;
72
73 if !past_seq_drift {
75 return Ok(current);
76 }
77
78 warn!(
79 target: "l1_origin_selector",
80 current_origin_time = current.timestamp,
81 unsafe_head_time = unsafe_head.block_info.timestamp,
82 max_seq_drift,
83 "Next L2 block time is past the sequencer drift"
84 );
85
86 if self
87 .next
88 .map(|n| unsafe_head.block_info.timestamp + self.cfg.block_time < n.timestamp)
89 .unwrap_or(false)
90 {
91 return Ok(current);
94 }
95
96 self.next.ok_or(L1OriginSelectorError::NotEnoughData(current))
97 }
98
99 async fn select_origins(
101 &mut self,
102 unsafe_head: &L2BlockInfo,
103 in_recovery_mode: bool,
104 ) -> Result<(), L1OriginSelectorError> {
105 if in_recovery_mode {
106 self.current = self.l1.get_block_by_hash(unsafe_head.l1_origin.hash).await?;
107 self.next = self.l1.get_block_by_number(unsafe_head.l1_origin.number + 1).await?;
108 return Ok(());
109 }
110
111 if self.current.map(|c| c.hash == unsafe_head.l1_origin.hash).unwrap_or(false) {
112 } else if self.next.map(|n| n.hash == unsafe_head.l1_origin.hash).unwrap_or(false) {
114 self.current = self.next.take();
116 self.next = None;
117 } else {
118 let current = self.l1.get_block_by_hash(unsafe_head.l1_origin.hash).await?;
120
121 self.current = current;
122 self.next = None;
123 }
124
125 self.try_fetch_next_origin().await
126 }
127
128 async fn try_fetch_next_origin(&mut self) -> Result<(), L1OriginSelectorError> {
130 if let Some(current) = self.current.as_ref() {
133 if self.next.is_some() {
135 return Ok(());
136 }
137
138 let next = self.l1.get_block_by_number(current.number + 1).await?;
143 if next.map(|n| n.parent_hash == current.hash).unwrap_or(false) {
144 self.next = next;
145 }
146 }
147
148 Ok(())
149 }
150}
151
152#[derive(Debug, thiserror::Error)]
154pub enum L1OriginSelectorError {
155 #[error(transparent)]
157 Provider(#[from] RpcError<TransportErrorKind>),
158 #[error(
160 "Waiting for more L1 data to be available to select the next L1 origin block. Current L1 origin: {0:?}"
161 )]
162 NotEnoughData(BlockInfo),
163}
164
165#[async_trait]
167pub trait L1OriginSelectorProvider {
168 async fn get_block_by_hash(
170 &self,
171 hash: B256,
172 ) -> Result<Option<BlockInfo>, L1OriginSelectorError>;
173
174 async fn get_block_by_number(
176 &self,
177 number: u64,
178 ) -> Result<Option<BlockInfo>, L1OriginSelectorError>;
179}
180
181#[derive(Debug)]
184pub struct DelayedL1OriginSelectorProvider {
185 inner: RootProvider,
187 l1_head: watch::Receiver<Option<BlockInfo>>,
189 confirmation_depth: u64,
191}
192
193impl DelayedL1OriginSelectorProvider {
194 pub const fn new(
196 inner: RootProvider,
197 l1_head: watch::Receiver<Option<BlockInfo>>,
198 confirmation_depth: u64,
199 ) -> Self {
200 Self { inner, l1_head, confirmation_depth }
201 }
202}
203
204#[async_trait]
205impl L1OriginSelectorProvider for DelayedL1OriginSelectorProvider {
206 async fn get_block_by_hash(
207 &self,
208 hash: B256,
209 ) -> Result<Option<BlockInfo>, L1OriginSelectorError> {
210 Ok(Provider::get_block_by_hash(&self.inner, hash).await?.map(Into::into))
212 }
213
214 async fn get_block_by_number(
215 &self,
216 number: u64,
217 ) -> Result<Option<BlockInfo>, L1OriginSelectorError> {
218 let Some(l1_head) = *self.l1_head.borrow() else {
219 return Ok(Provider::get_block_by_number(&self.inner, number.into())
221 .await?
222 .map(Into::into));
223 };
224
225 if number == 0 ||
226 self.confirmation_depth == 0 ||
227 number + self.confirmation_depth <= l1_head.number
228 {
229 Ok(Provider::get_block_by_number(&self.inner, number.into()).await?.map(Into::into))
230 } else {
231 Ok(None)
232 }
233 }
234}
235
236#[cfg(test)]
237mod test {
238 use super::*;
239 use alloy_eips::NumHash;
240 use rstest::rstest;
241 use std::collections::HashSet;
242
243 #[derive(Default, Debug, Clone)]
245 struct MockOriginSelectorProvider {
246 blocks: HashSet<BlockInfo>,
247 }
248
249 impl MockOriginSelectorProvider {
250 pub(crate) fn with_block(&mut self, block: BlockInfo) {
252 self.blocks.insert(block);
253 }
254 }
255
256 #[async_trait]
257 impl L1OriginSelectorProvider for MockOriginSelectorProvider {
258 async fn get_block_by_hash(
259 &self,
260 hash: B256,
261 ) -> Result<Option<BlockInfo>, L1OriginSelectorError> {
262 Ok(self.blocks.iter().find(|b| b.hash == hash).copied())
263 }
264
265 async fn get_block_by_number(
266 &self,
267 number: u64,
268 ) -> Result<Option<BlockInfo>, L1OriginSelectorError> {
269 Ok(self.blocks.iter().find(|b| b.number == number).copied())
270 }
271 }
272
273 #[tokio::test]
274 #[rstest]
275 #[case::single_epoch(1)]
276 #[case::many_epochs(12)]
277 async fn test_next_l1_origin_several_epochs(#[case] num_epochs: usize) {
278 const L1_SLOT_TIME: u64 = 12;
280 const L2_BLOCK_TIME: u64 = 2;
282
283 let cfg = Arc::new(RollupConfig {
286 block_time: L2_BLOCK_TIME,
287 max_sequencer_drift: 600,
288 ..Default::default()
289 });
290
291 let mut provider = MockOriginSelectorProvider::default();
294 for i in 0..num_epochs + 1 {
295 provider.with_block(BlockInfo {
296 parent_hash: B256::with_last_byte(i.saturating_sub(1) as u8),
297 hash: B256::with_last_byte(i as u8),
298 number: i as u64,
299 timestamp: i as u64 * L1_SLOT_TIME,
300 });
301 }
302
303 let mut selector = L1OriginSelector::new(cfg.clone(), provider);
304
305 for i in 0..(num_epochs as u64 * (L1_SLOT_TIME / cfg.block_time)) {
308 let current_epoch = (i * cfg.block_time) / L1_SLOT_TIME;
309 let unsafe_head = L2BlockInfo {
310 block_info: BlockInfo {
311 hash: B256::ZERO,
312 number: i,
313 timestamp: i * cfg.block_time,
314 ..Default::default()
315 },
316 l1_origin: NumHash {
317 number: current_epoch,
318 hash: B256::with_last_byte(current_epoch as u8),
319 },
320 seq_num: 0,
321 };
322 let next = selector.next_l1_origin(unsafe_head, false).await.unwrap();
323
324 let expected_epoch = ((i + 1) * cfg.block_time) / L1_SLOT_TIME;
327 assert_eq!(next.hash, B256::with_last_byte(expected_epoch as u8));
328 assert_eq!(next.number, expected_epoch);
329 }
330 }
331
332 #[tokio::test]
333 #[rstest]
334 #[case::not_available(false)]
335 #[case::is_available(true)]
336 async fn test_next_l1_origin_next_maybe_available(#[case] next_l1_origin_available: bool) {
337 const L2_BLOCK_TIME: u64 = 2;
339
340 let cfg = Arc::new(RollupConfig {
343 block_time: L2_BLOCK_TIME,
344 max_sequencer_drift: 600,
345 ..Default::default()
346 });
347
348 let mut provider = MockOriginSelectorProvider::default();
350 provider.with_block(BlockInfo {
351 parent_hash: B256::ZERO,
352 hash: B256::ZERO,
353 number: 0,
354 timestamp: 0,
355 });
356
357 if next_l1_origin_available {
358 provider.with_block(BlockInfo {
360 parent_hash: B256::ZERO,
361 hash: B256::with_last_byte(1),
362 number: 1,
363 timestamp: cfg.block_time,
364 });
365 }
366
367 let mut selector = L1OriginSelector::new(cfg.clone(), provider);
368
369 let current_epoch = 0;
370 let unsafe_head = L2BlockInfo {
371 block_info: BlockInfo {
372 hash: B256::ZERO,
373 number: 5,
374 timestamp: 5 * cfg.block_time,
375 ..Default::default()
376 },
377 l1_origin: NumHash {
378 number: current_epoch,
379 hash: B256::with_last_byte(current_epoch as u8),
380 },
381 seq_num: 0,
382 };
383 let next = selector.next_l1_origin(unsafe_head, false).await.unwrap();
384
385 let expected_epoch =
390 if next_l1_origin_available { current_epoch + 1 } else { current_epoch };
391 assert_eq!(next.hash, B256::with_last_byte(expected_epoch as u8));
392 assert_eq!(next.number, expected_epoch);
393 }
394
395 #[tokio::test]
396 #[rstest]
397 #[case::next_not_available(false, false)]
398 #[case::next_available_but_behind(true, false)]
399 #[case::next_available_and_ahead(true, true)]
400 async fn test_next_l1_origin_next_past_seq_drift(
401 #[case] next_available: bool,
402 #[case] next_ahead_of_unsafe: bool,
403 ) {
404 const L2_BLOCK_TIME: u64 = 2;
406
407 let cfg = Arc::new(RollupConfig {
410 block_time: L2_BLOCK_TIME,
411 max_sequencer_drift: 600,
412 ..Default::default()
413 });
414
415 let mut provider = MockOriginSelectorProvider::default();
417 provider.with_block(BlockInfo {
418 parent_hash: B256::ZERO,
419 hash: B256::ZERO,
420 number: 0,
421 timestamp: 0,
422 });
423
424 if next_available {
425 provider.with_block(BlockInfo {
427 parent_hash: B256::ZERO,
428 hash: B256::with_last_byte(1),
429 number: 1,
430 timestamp: if next_ahead_of_unsafe {
431 cfg.max_sequencer_drift + cfg.block_time * 2
432 } else {
433 cfg.block_time
434 },
435 });
436 }
437
438 let mut selector = L1OriginSelector::new(cfg.clone(), provider);
439
440 let current_epoch = 0;
441 let unsafe_head = L2BlockInfo {
442 block_info: BlockInfo { timestamp: cfg.max_sequencer_drift, ..Default::default() },
443 l1_origin: NumHash {
444 number: current_epoch,
445 hash: B256::with_last_byte(current_epoch as u8),
446 },
447 seq_num: 0,
448 };
449
450 if next_available {
451 if next_ahead_of_unsafe {
452 let next = selector.next_l1_origin(unsafe_head, false).await.unwrap();
455 assert_eq!(next.hash, B256::ZERO);
456 assert_eq!(next.number, 0);
457 } else {
458 let next = selector.next_l1_origin(unsafe_head, false).await.unwrap();
461 assert_eq!(next.hash, B256::with_last_byte(1));
462 assert_eq!(next.number, 1);
463 }
464 } else {
465 let next_err = selector.next_l1_origin(unsafe_head, false).await.unwrap_err();
469 assert!(matches!(next_err, L1OriginSelectorError::NotEnoughData(_)));
470 }
471 }
472}