Signals
Understanding signals first require a more in-depth review of the result returned by stepping on the derivation pipeline.
The StepResult
As briefly outlined in the intro, stepping on the derivation
pipeline returns a StepResult
. Step results provide a
an extensible way for pipeline stages to signal different results to the
pipeline driver. The variants of StepResult
and what they
signal include the following.
StepResult::PreparedAttributes
- signals that payload attributes are ready to be be consumed by the pipeline driver.StepResult::AdvancedOrigin
- signals that the pipeline has derived all payload attributes for the given L1 block, and the origin of the pipeline was advanced to the next canonical L1 block.StepResult::OriginAdvanceErr(_)
- The driver failed to advance the origin of pipeline.StepResult::StepFailed(_)
- The step failed.
No action is needed when the prepared attributes step result is received.
The pipeline driver may chose to consume the payload attributes how it
wishes. Likewise, StepResult::AdvancedOrigin
simply notifies the driver
that the pipeline advanced its origin - the driver may continue stepping
on the pipeline. Now, it becomes more involved with the remaining two
variants of StepResult
.
When either StepResult::OriginAdvanceErr(_)
or StepResult::StepFailed(_)
are received, the pipeline driver needs to introspect the error within these
variants. Depending on the PipelineErrorKind
, the driver may
need to send a "signal" down through the pipeline.
The next section goes over pipeline signals by looking at the variants of
the PipelineErrorKind
and the driver's response.
PipelineErrorKind
There are three variants of the PipelineErrorKind
, each
groups the inner error based on severity (or how they should be handled).
PipelineErrorKind::Temporary
- This is an error that's expected, and is temporary. For example, not all channel data has been posted to L1 so the pipeline doesn't have enough data yet to continue deriving payload attributes.PipelineErrorKind::Critical
- This is an unexpected error that breaks the derivation pipeline. It should cause the driver to error since this is behavior that is breaking the derivation of payload attributes.PipelineErrorKind::Reset
- When this is received, it effectively requests that the driver perform some action on the pipeline. Kona uses message passing so the driver can send aSignal
down the pipeline with whatever action that needs to be performed. By allowing both the driver and individual pipeline stages to define their own behaviour around signals, they become very extensible. More on this in a later section.
The Signal
Type
Continuing from the PipelineErrorKind
, when the driver
receives a PipelineErrorKind::Reset
, it needs to send a signal down
through the pipeline.
Prior to the Holocene hardfork, the pipeline only needed to be reset
when the reset pipeline error was received. Holocene activation rules
changed this to require Holocene-specific activation logic internal to
the pipeline stages. The way kona's driver handles this activation is
by sending a new ActivationSignal
if the PipelineErrorKind::Reset
type is a ResetError::HoloceneActivation
. Otherwise, it will send the
ResetSignal
.
The last of the three Signal
variants is the FlushChannel
signal. Similar to ActivationSignal
, the flush channel signal is logic
introduced post-Holocene. When the driver fails to execute payload
attributes and Holocene is active, a FlushChannel
signal needs to
forwards invalidate the associated batch and channel, and the block
is replaced with a deposit-only block.
Extending the Signal Type
To extend the Signal
type, all that is needed is to introduce
a new variant to the Signal
enum.
Once the variant is added, the segments where signals are handled need to
be updated. Anywhere the SignalReceiver
trait is
implemented, handling needs to be updated for the new signal variant. Most
notably, this is on the top-level DerivationPipeline
type, as well
as all the pipeline stages.
An Example
Let's create a new Signal
variant that updates the RollupConfig
in the L1Traversal
stage. Let's call it SetConfig
.
The signal
type would look like the following with this new
variant.
#![allow(unused)] fn main() { /// A signal to send to the pipeline. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[allow(clippy::large_enum_variant)] pub enum Signal { /// Reset the pipeline. Reset(ResetSignal), /// Hardfork Activation. Activation(ActivationSignal), /// Flush the currently active channel. FlushChannel, /// Updates the rollup config in the L1Traversal stage. UpdateConfig(ConfigUpdateSignal), } /// A signal that updates the `RollupConfig`. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct ConfigUpdateSignal(Arc<RollupConfig>); }
Next, all handling of the Signal
type needs to be updated for
the new UpdateConfig
variant. For the sake of this example, we'll just
focus on updating the L1Traversal
stage.
#![allow(unused)] fn main() { #[async_trait] impl<F: ChainProvider + Send> SignalReceiver for L1Traversal<F> { async fn signal(&mut self, signal: Signal) -> PipelineResult<()> { match signal { Signal::Reset(ResetSignal { l1_origin, system_config, .. }) | Signal::Activation(ActivationSignal { l1_origin, system_config, .. }) => { self.block = Some(l1_origin); self.done = false; self.system_config = system_config.expect("System config must be provided."); } Signal::UpdateConfig(inner) => { self.rollup_config = Arc::clone(&inner.0); } _ => {} } Ok(()) } } }