Skip to content

Commit c3ec1a2

Browse files
authored
L1 liveness probe (#479)
* introduce L1 liveness probe * add L1 liveness mod
1 parent b88371e commit c3ec1a2

File tree

8 files changed

+110
-1
lines changed

8 files changed

+110
-1
lines changed

crates/node/src/args.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,8 @@ impl ScrollRollupNodeConfig {
392392
l1_block_startup_info,
393393
node_config,
394394
self.l1_provider_args.logs_query_block_range,
395+
self.l1_provider_args.liveness_threshold,
396+
self.l1_provider_args.liveness_check_interval,
395397
)
396398
.await,
397399
),
@@ -668,7 +670,7 @@ impl RollupNodeNetworkArgs {
668670
}
669671

670672
/// The arguments for the L1 provider.
671-
#[derive(Debug, Default, Clone, clap::Args)]
673+
#[derive(Debug, Clone, clap::Args)]
672674
pub struct L1ProviderArgs {
673675
/// The URL for the L1 RPC.
674676
#[arg(long = "l1.url", id = "l1_url", value_name = "L1_URL")]
@@ -688,6 +690,28 @@ pub struct L1ProviderArgs {
688690
/// The maximum number of items to be stored in the cache layer.
689691
#[arg(long = "l1.cache-max-items", id = "l1_cache_max_items", value_name = "L1_CACHE_MAX_ITEMS", default_value_t = constants::L1_PROVIDER_CACHE_MAX_ITEMS)]
690692
pub cache_max_items: u32,
693+
/// The L1 liveness threshold in seconds. If no new L1 block is received within this duration,
694+
/// an error is logged.
695+
#[arg(long = "l1.liveness-threshold", id = "l1_liveness_threshold", value_name = "L1_LIVENESS_THRESHOLD", default_value_t = constants::L1_LIVENESS_THRESHOLD)]
696+
pub liveness_threshold: u64,
697+
/// The interval in seconds at which to check L1 liveness.
698+
#[arg(long = "l1.liveness-check-interval", id = "l1_liveness_check_interval", value_name = "L1_LIVENESS_CHECK_INTERVAL", default_value_t = constants::L1_LIVENESS_CHECK_INTERVAL)]
699+
pub liveness_check_interval: u64,
700+
}
701+
702+
impl Default for L1ProviderArgs {
703+
fn default() -> Self {
704+
Self {
705+
url: None,
706+
compute_units_per_second: constants::PROVIDER_COMPUTE_UNITS_PER_SECOND,
707+
max_retries: constants::L1_PROVIDER_MAX_RETRIES,
708+
initial_backoff: constants::L1_PROVIDER_INITIAL_BACKOFF,
709+
logs_query_block_range: constants::LOGS_QUERY_BLOCK_RANGE,
710+
cache_max_items: constants::L1_PROVIDER_CACHE_MAX_ITEMS,
711+
liveness_threshold: constants::L1_LIVENESS_THRESHOLD,
712+
liveness_check_interval: constants::L1_LIVENESS_CHECK_INTERVAL,
713+
}
714+
}
691715
}
692716

693717
/// The arguments for the Beacon provider.

crates/node/src/constants.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ pub(crate) const L1_PROVIDER_INITIAL_BACKOFF: u64 = 100;
1111
/// The maximum number of items to store in L1 provider's cache layer.
1212
pub(crate) const L1_PROVIDER_CACHE_MAX_ITEMS: u32 = 100;
1313

14+
/// The default L1 liveness threshold in seconds.
15+
pub(crate) const L1_LIVENESS_THRESHOLD: u64 = 60;
16+
17+
/// The default L1 liveness check interval in seconds.
18+
pub(crate) const L1_LIVENESS_CHECK_INTERVAL: u64 = 12;
19+
1420
/// The block range used to fetch L1 logs.
1521
pub(crate) const LOGS_QUERY_BLOCK_RANGE: u64 = 500;
1622

crates/node/tests/sync.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ async fn test_should_consolidate_to_block_15k() -> eyre::Result<()> {
5555
initial_backoff: 100,
5656
logs_query_block_range: 500,
5757
cache_max_items: 100,
58+
..Default::default()
5859
},
5960
engine_driver_args: EngineDriverArgs { sync_at_startup: false },
6061
sequencer_args: SequencerArgs {

crates/watcher/src/lib.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ pub use error::{EthRequestError, FilterLogError, L1WatcherError};
66
mod handle;
77
pub use handle::{L1WatcherCommand, L1WatcherHandle};
88

9+
mod liveness;
10+
use liveness::LivenessProbe;
11+
912
mod metrics;
1013
pub use metrics::WatcherMetrics;
1114

@@ -97,6 +100,8 @@ pub struct L1Watcher<EP> {
97100
is_synced: bool,
98101
/// The log query block range.
99102
log_query_block_range: u64,
103+
/// The L1 liveness probe.
104+
liveness_probe: LivenessProbe,
100105
}
101106

102107
/// The L1 notification type yielded by the [`L1Watcher`].
@@ -206,6 +211,8 @@ where
206211
l1_block_startup_info: L1BlockStartupInfo,
207212
config: Arc<NodeConfig>,
208213
log_query_block_range: u64,
214+
liveness_threshold: u64,
215+
liveness_check_interval: u64,
209216
) -> L1WatcherHandle {
210217
tracing::trace!(target: "scroll::watcher", ?l1_block_startup_info, ?config, "spawning L1 watcher");
211218

@@ -271,6 +278,7 @@ where
271278
metrics: WatcherMetrics::default(),
272279
is_synced: false,
273280
log_query_block_range,
281+
liveness_probe: LivenessProbe::new(liveness_threshold, liveness_check_interval),
274282
};
275283

276284
// notify at spawn.
@@ -304,6 +312,11 @@ where
304312
}
305313
}
306314

315+
// Check L1 liveness if due.
316+
if self.liveness_probe.is_due() {
317+
self.liveness_probe.check(self.unfinalized_blocks.last());
318+
}
319+
307320
// step the watcher.
308321
if let Err(L1WatcherError::SendError(_)) = self
309322
.step()
@@ -923,6 +936,7 @@ mod tests {
923936
metrics: WatcherMetrics::default(),
924937
is_synced: false,
925938
log_query_block_range: LOG_QUERY_BLOCK_RANGE,
939+
liveness_probe: LivenessProbe::new(60, 12),
926940
},
927941
handle,
928942
)

crates/watcher/src/liveness.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use super::Header;
2+
use std::time::Instant;
3+
4+
/// A probe that checks L1 liveness by monitoring block timestamps.
5+
#[derive(Debug)]
6+
pub(crate) struct LivenessProbe {
7+
/// The threshold in seconds after which to log an error if no new block is received.
8+
threshold: u64,
9+
/// The interval in seconds at which to perform the liveness check.
10+
check_interval: u64,
11+
/// The last time a liveness check was performed.
12+
last_check: Instant,
13+
}
14+
15+
impl LivenessProbe {
16+
/// Creates a new liveness probe.
17+
pub(crate) fn new(threshold: u64, check_interval: u64) -> Self {
18+
Self { threshold, check_interval, last_check: Instant::now() }
19+
}
20+
21+
/// Returns true if a liveness check is due based on the configured interval.
22+
pub(crate) fn is_due(&self) -> bool {
23+
self.last_check.elapsed().as_secs() >= self.check_interval
24+
}
25+
26+
/// Checks L1 liveness based on the latest block header.
27+
/// Logs an error if no new block has been received within the threshold.
28+
pub(crate) fn check(&mut self, latest_block: Option<&Header>) {
29+
self.last_check = Instant::now();
30+
31+
if let Some(block) = latest_block {
32+
let now = std::time::SystemTime::now()
33+
.duration_since(std::time::UNIX_EPOCH)
34+
.expect("time went backwards")
35+
.as_secs();
36+
37+
let elapsed = now.saturating_sub(block.timestamp);
38+
if elapsed > self.threshold {
39+
tracing::error!(
40+
target: "scroll::watcher",
41+
latest_block_number = block.number,
42+
latest_block_timestamp = block.timestamp,
43+
elapsed_secs = elapsed,
44+
threshold_secs = self.threshold,
45+
"L1 liveness check failed: no new L1 block received"
46+
);
47+
}
48+
}
49+
}
50+
}

crates/watcher/tests/indexing.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ async fn test_should_not_index_latest_block_multiple_times() -> eyre::Result<()>
1919
const CHAIN_LEN: usize = 200;
2020
const HALF_CHAIN_LEN: usize = 100;
2121
const LOGS_QUERY_BLOCK_RANGE: u64 = 500;
22+
const L1_LIVENESS_THRESHOLD: u64 = 60;
23+
const L1_LIVENESS_CHECK_INTERVAL: u64 = 12;
2224

2325
// Given
2426
let (finalized, latest, headers) = chain(CHAIN_LEN);
@@ -64,6 +66,8 @@ async fn test_should_not_index_latest_block_multiple_times() -> eyre::Result<()>
6466
L1BlockStartupInfo::None,
6567
Arc::new(config),
6668
LOGS_QUERY_BLOCK_RANGE,
69+
L1_LIVENESS_THRESHOLD,
70+
L1_LIVENESS_CHECK_INTERVAL,
6771
)
6872
.await;
6973
let mut prev_block_info = Default::default();

crates/watcher/tests/logs.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ async fn test_should_not_miss_logs_on_reorg() -> eyre::Result<()> {
2121
const CHAIN_LEN: usize = 200;
2222
const HALF_CHAIN_LEN: usize = CHAIN_LEN / 2;
2323
const LOGS_QUERY_BLOCK_RANGE: u64 = 500;
24+
const L1_LIVENESS_THRESHOLD: u64 = 60;
25+
const L1_LIVENESS_CHECK_INTERVAL: u64 = 12;
2426

2527
// Given
2628
let (finalized, _, headers) = chain(CHAIN_LEN);
@@ -69,6 +71,8 @@ async fn test_should_not_miss_logs_on_reorg() -> eyre::Result<()> {
6971
L1BlockStartupInfo::None,
7072
Arc::new(config),
7173
LOGS_QUERY_BLOCK_RANGE,
74+
L1_LIVENESS_THRESHOLD,
75+
L1_LIVENESS_CHECK_INTERVAL,
7276
)
7377
.await;
7478
let mut received_logs = Vec::new();

crates/watcher/tests/reorg.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use rollup_node_watcher::{
1111
random, test_utils::provider::MockProvider, Block, L1Notification, L1Watcher,
1212
};
1313
const LOGS_QUERY_BLOCK_RANGE: u64 = 500;
14+
const L1_LIVENESS_THRESHOLD: u64 = 60;
15+
const L1_LIVENESS_CHECK_INTERVAL: u64 = 12;
1416

1517
// Generate a set blocks that will be fed to the l1 watcher.
1618
// Every fork_cycle blocks, generates a small reorg.
@@ -77,6 +79,8 @@ async fn test_should_detect_reorg() -> eyre::Result<()> {
7779
L1BlockStartupInfo::None,
7880
Arc::new(config),
7981
LOGS_QUERY_BLOCK_RANGE,
82+
L1_LIVENESS_THRESHOLD,
83+
L1_LIVENESS_CHECK_INTERVAL,
8084
)
8185
.await;
8286

@@ -184,6 +188,8 @@ async fn test_should_fetch_gap_in_unfinalized_blocks() -> eyre::Result<()> {
184188
L1BlockStartupInfo::None,
185189
Arc::new(config),
186190
LOGS_QUERY_BLOCK_RANGE,
191+
L1_LIVENESS_THRESHOLD,
192+
L1_LIVENESS_CHECK_INTERVAL,
187193
)
188194
.await;
189195

0 commit comments

Comments
 (0)