use super::{
DelayedL1OriginSelectorProvider, L1OriginSelector, L1OriginSelectorError, SequencerConfig,
};
use crate::{CancellableContext, NodeActor, actors::sequencer::conductor::ConductorClient};
use alloy_provider::RootProvider;
use async_trait::async_trait;
use kona_derive::{AttributesBuilder, PipelineErrorKind, StatefulAttributesBuilder};
use kona_genesis::RollupConfig;
use kona_protocol::{BlockInfo, L2BlockInfo, OpAttributesWithParent};
use kona_providers_alloy::{AlloyChainProvider, AlloyL2ChainProvider};
use kona_rpc::SequencerAdminQuery;
use op_alloy_network::Optimism;
use op_alloy_rpc_types_engine::OpExecutionPayloadEnvelope;
use std::{
sync::Arc,
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use tokio::{
select,
sync::{mpsc, watch},
};
use tokio_util::sync::{CancellationToken, WaitForCancellationFuture};
#[derive(Debug)]
pub struct SequencerActor<AB: AttributesBuilderConfig> {
pub builder: AB,
pub unsafe_head_rx: watch::Receiver<L2BlockInfo>,
pub admin_query_rx: mpsc::Receiver<SequencerAdminQuery>,
}
#[derive(Debug)]
pub(super) struct SequencerActorState<AB: AttributesBuilder> {
pub cfg: Arc<RollupConfig>,
pub builder: AB,
pub origin_selector: L1OriginSelector<DelayedL1OriginSelectorProvider>,
pub build_ticker: tokio::time::Interval,
pub conductor: Option<ConductorClient>,
pub is_active: bool,
pub is_recovery_mode: bool,
}
pub trait AttributesBuilderConfig {
type AB: AttributesBuilder;
fn build(self) -> Self::AB;
}
impl SequencerActorState<StatefulAttributesBuilder<AlloyChainProvider, AlloyL2ChainProvider>> {
fn new(
seq_builder: SequencerBuilder,
l1_head_watcher: watch::Receiver<Option<BlockInfo>>,
) -> Self {
let SequencerConfig {
sequencer_stopped,
sequencer_recovery_mode,
conductor_rpc_url,
l1_conf_delay,
} = seq_builder.seq_cfg.clone();
let cfg = seq_builder.rollup_cfg.clone();
let l1_provider = DelayedL1OriginSelectorProvider::new(
seq_builder.l1_provider.clone(),
l1_head_watcher,
l1_conf_delay,
);
let conductor = conductor_rpc_url.map(ConductorClient::new_http);
let builder = seq_builder.build();
let build_ticker = tokio::time::interval(Duration::from_secs(cfg.block_time));
let origin_selector = L1OriginSelector::new(cfg.clone(), l1_provider);
Self {
cfg,
builder,
origin_selector,
build_ticker,
conductor,
is_active: !sequencer_stopped,
is_recovery_mode: sequencer_recovery_mode,
}
}
}
const DERIVATION_PROVIDER_CACHE_SIZE: usize = 1024;
#[derive(Debug)]
pub struct SequencerBuilder {
pub seq_cfg: SequencerConfig,
pub rollup_cfg: Arc<RollupConfig>,
pub l1_provider: RootProvider,
pub l2_provider: RootProvider<Optimism>,
}
impl AttributesBuilderConfig for SequencerBuilder {
type AB = StatefulAttributesBuilder<AlloyChainProvider, AlloyL2ChainProvider>;
fn build(self) -> Self::AB {
let l1_derivation_provider =
AlloyChainProvider::new(self.l1_provider.clone(), DERIVATION_PROVIDER_CACHE_SIZE);
let l2_derivation_provider = AlloyL2ChainProvider::new(
self.l2_provider.clone(),
self.rollup_cfg.clone(),
DERIVATION_PROVIDER_CACHE_SIZE,
);
StatefulAttributesBuilder::new(
self.rollup_cfg,
l2_derivation_provider,
l1_derivation_provider,
)
}
}
#[derive(Debug)]
pub struct SequencerInboundData {
pub unsafe_head_tx: watch::Sender<L2BlockInfo>,
pub admin_query_tx: mpsc::Sender<SequencerAdminQuery>,
}
#[derive(Debug)]
pub struct SequencerContext {
pub cancellation: CancellationToken,
pub l1_head_rx: watch::Receiver<Option<BlockInfo>>,
pub reset_request_tx: mpsc::Sender<()>,
pub build_request_tx:
mpsc::Sender<(OpAttributesWithParent, mpsc::Sender<OpExecutionPayloadEnvelope>)>,
pub gossip_payload_tx: mpsc::Sender<OpExecutionPayloadEnvelope>,
}
impl CancellableContext for SequencerContext {
fn cancelled(&self) -> WaitForCancellationFuture<'_> {
self.cancellation.cancelled()
}
}
#[derive(Debug, thiserror::Error)]
pub enum SequencerActorError {
#[error(transparent)]
AttributesBuilder(#[from] PipelineErrorKind),
#[error(transparent)]
L1OriginSelector(#[from] L1OriginSelectorError),
#[error("Channel closed unexpectedly")]
ChannelClosed,
}
impl<AB: AttributesBuilderConfig> SequencerActor<AB> {
pub fn new(state: AB) -> (SequencerInboundData, Self) {
let (unsafe_head_tx, unsafe_head_rx) = watch::channel(L2BlockInfo::default());
let (admin_query_tx, admin_query_rx) = mpsc::channel(1024);
let actor = Self { builder: state, unsafe_head_rx, admin_query_rx };
(SequencerInboundData { unsafe_head_tx, admin_query_tx }, actor)
}
}
impl<AB: AttributesBuilder> SequencerActorState<AB> {
async fn build_block(
&mut self,
ctx: &mut SequencerContext,
unsafe_head_rx: &mut watch::Receiver<L2BlockInfo>,
in_recovery_mode: bool,
) -> Result<(), SequencerActorError> {
let unsafe_head = *unsafe_head_rx.borrow();
let l1_origin = match self
.origin_selector
.next_l1_origin(unsafe_head, self.is_recovery_mode)
.await
{
Ok(l1_origin) => l1_origin,
Err(err) => {
warn!(
target: "sequencer",
?err,
"Temporary error occurred while selecting next L1 origin. Re-attempting on next tick."
);
return Ok(());
}
};
if unsafe_head.l1_origin.hash != l1_origin.parent_hash &&
unsafe_head.l1_origin.hash != l1_origin.hash
{
warn!(
target: "sequencer",
l1_origin = ?l1_origin,
unsafe_head_hash = %unsafe_head.l1_origin.hash,
unsafe_head_l1_origin = ?unsafe_head.l1_origin,
"Cannot build new L2 block on inconsistent L1 origin, resetting engine"
);
if let Err(err) = ctx.reset_request_tx.send(()).await {
error!(target: "sequencer", ?err, "Failed to reset engine");
ctx.cancellation.cancel();
return Err(SequencerActorError::ChannelClosed);
}
return Ok(());
}
info!(
target: "sequencer",
parent_num = unsafe_head.block_info.number,
l1_origin_num = l1_origin.number,
"Started sequencing new block"
);
let _attributes_build_start = Instant::now();
let mut attributes =
match self.builder.prepare_payload_attributes(unsafe_head, l1_origin.id()).await {
Ok(attrs) => attrs,
Err(PipelineErrorKind::Temporary(_)) => {
return Ok(());
}
Err(PipelineErrorKind::Reset(_)) => {
if let Err(err) = ctx.reset_request_tx.send(()).await {
error!(target: "sequencer", ?err, "Failed to reset engine");
ctx.cancellation.cancel();
return Err(SequencerActorError::ChannelClosed);
}
warn!(
target: "sequencer",
"Resetting engine due to pipeline error while preparing payload attributes"
);
return Ok(());
}
Err(err @ PipelineErrorKind::Critical(_)) => {
error!(target: "sequencer", ?err, "Failed to prepare payload attributes");
ctx.cancellation.cancel();
return Err(err.into());
}
};
if in_recovery_mode {
attributes.no_tx_pool = Some(true);
}
attributes.no_tx_pool = (attributes.payload_attributes.timestamp >
l1_origin.timestamp + self.cfg.max_sequencer_drift(l1_origin.timestamp))
.then_some(true);
if self.cfg.is_first_ecotone_block(attributes.payload_attributes.timestamp) {
info!(target: "sequencer", "Sequencing ecotone upgrade block");
attributes.no_tx_pool = Some(true);
}
if self.cfg.is_first_fjord_block(attributes.payload_attributes.timestamp) {
info!(target: "sequencer", "Sequencing fjord upgrade block");
attributes.no_tx_pool = Some(true);
}
if self.cfg.is_first_granite_block(attributes.payload_attributes.timestamp) {
info!(target: "sequencer", "Sequencing granite upgrade block");
attributes.no_tx_pool = Some(true);
}
if self.cfg.is_first_holocene_block(attributes.payload_attributes.timestamp) {
info!(target: "sequencer", "Sequencing holocene upgrade block");
attributes.no_tx_pool = Some(true);
}
if self.cfg.is_first_isthmus_block(attributes.payload_attributes.timestamp) {
info!(target: "sequencer", "Sequencing isthmus upgrade block");
attributes.no_tx_pool = Some(true);
}
if self.cfg.is_first_interop_block(attributes.payload_attributes.timestamp) {
info!(target: "sequencer", "Sequencing interop upgrade block");
attributes.no_tx_pool = Some(true);
}
let attrs_with_parent = OpAttributesWithParent::new(attributes, unsafe_head, None, false);
kona_macros::set!(
gauge,
crate::Metrics::SEQUENCER_ATTRIBUTES_BUILDER_DURATION,
_attributes_build_start.elapsed()
);
let (payload_tx, payload_rx) = mpsc::channel(1);
let _build_request_start = Instant::now();
if let Err(err) = ctx.build_request_tx.send((attrs_with_parent, payload_tx)).await {
error!(target: "sequencer", ?err, "Failed to send built attributes to engine");
ctx.cancellation.cancel();
return Err(SequencerActorError::ChannelClosed);
}
let payload = self.try_wait_for_payload(ctx, payload_rx).await?;
kona_macros::set!(
gauge,
crate::Metrics::SEQUENCER_BLOCK_BUILDING_JOB_DURATION,
_build_request_start.elapsed()
);
if let Some(conductor) = &self.conductor {
let _conductor_commitment_start = Instant::now();
if let Err(err) = conductor.commit_unsafe_payload(&payload).await {
error!(target: "sequencer", ?err, "Failed to commit unsafe payload to conductor");
}
kona_macros::set!(
gauge,
crate::Metrics::SEQUENCER_CONDUCTOR_COMMITMENT_DURATION,
_conductor_commitment_start.elapsed()
);
}
let now =
SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs();
let then = payload.execution_payload.timestamp() + self.cfg.block_time;
if then.saturating_sub(now) <= self.cfg.block_time {
warn!(
target: "sequencer",
"Next block timestamp is more than a block time away from now, building immediately"
);
self.build_ticker.reset_immediately();
}
self.schedule_gossip(ctx, payload).await
}
async fn try_wait_for_payload(
&mut self,
ctx: &mut SequencerContext,
mut payload_rx: mpsc::Receiver<OpExecutionPayloadEnvelope>,
) -> Result<OpExecutionPayloadEnvelope, SequencerActorError> {
payload_rx.recv().await.ok_or_else(|| {
error!(target: "sequencer", "Failed to receive built payload");
ctx.cancellation.cancel();
SequencerActorError::ChannelClosed
})
}
async fn schedule_gossip(
&mut self,
ctx: &mut SequencerContext,
payload: OpExecutionPayloadEnvelope,
) -> Result<(), SequencerActorError> {
if let Err(err) = ctx.gossip_payload_tx.send(payload).await {
error!(target: "sequencer", ?err, "Failed to send payload to be signed and gossipped");
ctx.cancellation.cancel();
return Err(SequencerActorError::ChannelClosed);
}
Ok(())
}
async fn schedule_initial_reset(
&mut self,
ctx: &mut SequencerContext,
unsafe_head_rx: &mut watch::Receiver<L2BlockInfo>,
) -> Result<(), SequencerActorError> {
if let Err(err) = ctx.reset_request_tx.send(()).await {
error!(target: "sequencer", ?err, "Failed to send reset request to engine");
ctx.cancellation.cancel();
return Err(SequencerActorError::ChannelClosed);
}
if unsafe_head_rx.changed().await.is_err() {
error!(target: "sequencer", "Failed to receive unsafe head update after reset request");
ctx.cancellation.cancel();
return Err(SequencerActorError::ChannelClosed);
}
Ok(())
}
#[cfg(feature = "metrics")]
fn update_metrics(&self) {
let state_flags: [(&str, String); 2] = [
("active", self.is_active.to_string()),
("recovery", self.is_recovery_mode.to_string()),
];
let gauge = metrics::gauge!(crate::Metrics::SEQUENCER_STATE, &state_flags);
gauge.set(1);
}
}
#[async_trait]
impl NodeActor for SequencerActor<SequencerBuilder> {
type Error = SequencerActorError;
type OutboundData = SequencerContext;
type Builder = SequencerBuilder;
type InboundData = SequencerInboundData;
fn build(config: Self::Builder) -> (Self::InboundData, Self) {
Self::new(config)
}
async fn start(mut self, mut ctx: Self::OutboundData) -> Result<(), Self::Error> {
let mut state = SequencerActorState::new(self.builder, ctx.l1_head_rx.clone());
#[cfg(feature = "metrics")]
state.update_metrics();
state.schedule_initial_reset(&mut ctx, &mut self.unsafe_head_rx).await?;
loop {
select! {
biased;
_ = ctx.cancellation.cancelled() => {
info!(
target: "sequencer",
"Received shutdown signal. Exiting sequencer task."
);
return Ok(());
}
Some(admin_query) = self.admin_query_rx.recv(), if !self.admin_query_rx.is_closed() => {
let is_sequencer_active = state.is_active;
if let Err(e) = state.handle_admin_query(admin_query, &mut self.unsafe_head_rx).await {
error!(target: "sequencer", err = ?e, "Failed to handle admin query");
}
if is_sequencer_active != state.is_active {
state.build_ticker.reset_immediately();
}
#[cfg(feature = "metrics")]
state.update_metrics();
}
_ = state.build_ticker.tick(), if state.is_active => {
state.build_block(&mut ctx, &mut self.unsafe_head_rx, state.is_recovery_mode).await?;
}
}
}
}
}