use crate::{NodeActor, actors::CancellableContext};
use alloy_eips::{BlockId, BlockNumberOrTag};
use alloy_primitives::{Address, B256};
use alloy_provider::{Provider, RootProvider};
use alloy_rpc_client::PollerBuilder;
use alloy_rpc_types_eth::{Block, Log};
use alloy_transport::TransportError;
use async_stream::stream;
use async_trait::async_trait;
use futures::{Stream, StreamExt};
use kona_genesis::{RollupConfig, SystemConfigLog, SystemConfigUpdate, UnsafeBlockSignerUpdate};
use kona_protocol::BlockInfo;
use kona_rpc::{L1State, L1WatcherQueries};
use std::{sync::Arc, time::Duration};
use thiserror::Error;
use tokio::{
select,
sync::{
mpsc::{self, error::SendError},
watch,
},
task::JoinHandle,
};
use tokio_util::sync::{CancellationToken, WaitForCancellationFuture};
#[derive(Debug)]
pub struct L1WatcherRpc {
state: L1WatcherRpcState,
pub inbound_queries: tokio::sync::mpsc::Receiver<L1WatcherQueries>,
}
#[derive(Debug)]
pub struct L1WatcherRpcState {
pub rollup: Arc<RollupConfig>,
pub l1_provider: RootProvider,
}
impl L1WatcherRpcState {
async fn fetch_logs(&self, block_hash: B256) -> Result<Vec<Log>, L1WatcherRpcError<BlockInfo>> {
let logs = self
.l1_provider
.get_logs(&alloy_rpc_types_eth::Filter::new().select(block_hash))
.await?;
Ok(logs)
}
fn start_query_processor(
&self,
mut inbound_queries: tokio::sync::mpsc::Receiver<L1WatcherQueries>,
head_updates_recv: watch::Receiver<Option<BlockInfo>>,
) -> JoinHandle<()> {
let l1_provider = self.l1_provider.clone();
let rollup_config = self.rollup.clone();
tokio::spawn(async move {
while let Some(query) = inbound_queries.recv().await {
match query {
L1WatcherQueries::Config(sender) => {
if let Err(e) = sender.send((*rollup_config).clone()) {
warn!(target: "l1_watcher", error = ?e, "Failed to send L1 config to the query sender");
}
}
L1WatcherQueries::L1State(sender) => {
let current_l1 = *head_updates_recv.borrow();
let head_l1 = match l1_provider.get_block(BlockId::latest()).await {
Ok(block) => block,
Err(e) => {
warn!(target: "l1_watcher", error = ?e, "failed to query l1 provider for latest head block");
None
}}.map(|block| block.into_consensus().into());
let finalized_l1 = match l1_provider.get_block(BlockId::finalized()).await {
Ok(block) => block,
Err(e) => {
warn!(target: "l1_watcher", error = ?e, "failed to query l1 provider for latest finalized block");
None
}}.map(|block| block.into_consensus().into());
let safe_l1 = match l1_provider.get_block(BlockId::safe()).await {
Ok(block) => block,
Err(e) => {
warn!(target: "l1_watcher", error = ?e, "failed to query l1 provider for latest safe block");
None
}}.map(|block| block.into_consensus().into());
if let Err(e) = sender.send(L1State {
current_l1,
current_l1_finalized: finalized_l1,
head_l1,
safe_l1,
finalized_l1,
}) {
warn!(target: "l1_watcher", error = ?e, "Failed to send L1 state to the query sender");
}
}
}
}
error!(target: "l1_watcher", "L1 watcher query channel closed unexpectedly, exiting query processor task.");
})
}
}
#[derive(Debug)]
pub struct L1WatcherRpcInboundChannels {
pub inbound_queries: tokio::sync::mpsc::Sender<L1WatcherQueries>,
}
#[derive(Debug)]
pub struct L1WatcherRpcContext {
pub latest_head: watch::Sender<Option<BlockInfo>>,
pub latest_finalized: watch::Sender<Option<BlockInfo>>,
pub block_signer_sender: mpsc::Sender<Address>,
pub cancellation: CancellationToken,
}
impl CancellableContext for L1WatcherRpcContext {
fn cancelled(&self) -> WaitForCancellationFuture<'_> {
self.cancellation.cancelled()
}
}
impl L1WatcherRpc {
pub fn new(config: L1WatcherRpcState) -> (L1WatcherRpcInboundChannels, Self) {
let (l1_watcher_queries_sender, l1_watcher_queries_recv) = mpsc::channel(1024);
let actor = Self { state: config, inbound_queries: l1_watcher_queries_recv };
(L1WatcherRpcInboundChannels { inbound_queries: l1_watcher_queries_sender }, actor)
}
}
#[async_trait]
impl NodeActor for L1WatcherRpc {
type Error = L1WatcherRpcError<BlockInfo>;
type InboundData = L1WatcherRpcInboundChannels;
type OutboundData = L1WatcherRpcContext;
type Builder = L1WatcherRpcState;
fn build(config: Self::Builder) -> (Self::InboundData, Self) {
Self::new(config)
}
async fn start(
mut self,
L1WatcherRpcContext { latest_head, latest_finalized, block_signer_sender, cancellation }: Self::OutboundData,
) -> Result<(), Self::Error> {
let mut head_stream = BlockStream::new(
&self.state.l1_provider,
BlockNumberOrTag::Latest,
Duration::from_secs(13),
)
.into_stream();
let mut finalized_stream = BlockStream::new(
&self.state.l1_provider,
BlockNumberOrTag::Finalized,
Duration::from_secs(60),
)
.into_stream();
let inbound_query_processor =
self.state.start_query_processor(self.inbound_queries, latest_head.subscribe());
loop {
select! {
_ = cancellation.cancelled() => {
info!(
target: "l1_watcher",
"Received shutdown signal. Exiting L1 watcher task."
);
inbound_query_processor.abort();
return Ok(());
},
new_head = head_stream.next() => match new_head {
None => {
return Err(L1WatcherRpcError::StreamEnded);
}
Some(head_block_info) => {
latest_head.send_replace(Some(head_block_info));
let logs = self.state.fetch_logs(head_block_info.hash).await?;
let ecotone_active = self.state.rollup.is_ecotone_active(head_block_info.timestamp);
for log in logs {
if log.address() != self.state.rollup.l1_system_config_address {
continue; }
let sys_cfg_log = SystemConfigLog::new(log.into(), ecotone_active);
if let Ok(SystemConfigUpdate::UnsafeBlockSigner(UnsafeBlockSignerUpdate { unsafe_block_signer })) = sys_cfg_log.build() {
info!(
target: "l1_watcher",
"Unsafe block signer update: {unsafe_block_signer}"
);
if let Err(e) = block_signer_sender.send(unsafe_block_signer).await {
error!(
target: "l1_watcher",
"Error sending unsafe block signer update: {e}"
);
}
}
}
},
},
new_finalized = finalized_stream.next() => match new_finalized {
None => {
return Err(L1WatcherRpcError::StreamEnded);
}
Some(finalized_block_info) => {
latest_finalized.send_replace(Some(finalized_block_info));
}
}
}
}
}
}
struct BlockStream<'a> {
l1_provider: &'a RootProvider,
tag: BlockNumberOrTag,
poll_interval: Duration,
}
impl<'a> BlockStream<'a> {
fn new(l1_provider: &'a RootProvider, tag: BlockNumberOrTag, poll_interval: Duration) -> Self {
if matches!(tag, BlockNumberOrTag::Number(_)) {
panic!("Invalid BlockNumberOrTag variant - Must be a tag");
}
Self { l1_provider, tag, poll_interval }
}
fn into_stream(self) -> impl Stream<Item = BlockInfo> + Unpin {
let mut poll_stream = PollerBuilder::<_, Block>::new(
self.l1_provider.weak_client(),
"eth_getBlockByNumber",
(self.tag, false),
)
.with_poll_interval(self.poll_interval)
.into_stream();
Box::pin(stream! {
let mut last_block = None;
while let Some(next) = poll_stream.next().await {
let info: BlockInfo = next.into_consensus().into();
if last_block.map(|b| b != info).unwrap_or(true) {
last_block = Some(info);
yield info;
}
}
})
}
}
#[derive(Error, Debug)]
pub enum L1WatcherRpcError<T> {
#[error("Error sending the head update event: {0}")]
SendError(#[from] SendError<T>),
#[error("Transport error: {0}")]
Transport(#[from] TransportError),
#[error("L1 block not found: {0}")]
L1BlockNotFound(BlockId),
#[error("Stream ended unexpectedly")]
StreamEnded,
}