diff --git a/Cargo.lock b/Cargo.lock index 84eb31e..c6918c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2671,11 +2671,8 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" name = "hera" version = "0.1.0" dependencies = [ - "anyhow", "clap", "eyre", - "kona-derive", - "kona-providers", "reth", "reth-exex", "reth-node-api", @@ -4222,6 +4219,19 @@ dependencies = [ "serde", ] +[[package]] +name = "op-rs" +version = "0.1.0" +dependencies = [ + "clap", + "eyre", + "reth", + "reth-node-ethereum", + "rollup", + "superchain-registry", + "tracing", +] + [[package]] name = "opaque-debug" version = "0.3.1" @@ -7363,6 +7373,21 @@ dependencies = [ "chrono", ] +[[package]] +name = "rollup" +version = "0.0.0" +dependencies = [ + "async-trait", + "clap", + "eyre", + "reth-exex", + "reth-node-api", + "superchain-registry", + "tokio", + "tracing", + "url", +] + [[package]] name = "route-recognizer" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 90c9c14..8d4c1eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ categories = ["cryptography", "cryptography::cryptocurrencies"] [workspace] members = [ "bin/hera", + "bin/op-rs", + "crates/rollup", "crates/kona-providers", "crates/ser", ] diff --git a/bin/hera/Cargo.toml b/bin/hera/Cargo.toml index 65c0bfc..9828caa 100644 --- a/bin/hera/Cargo.toml +++ b/bin/hera/Cargo.toml @@ -11,9 +11,6 @@ keywords.workspace = true categories.workspace = true [dependencies] -# Local Dependencies -kona-providers = { path = "../../crates/kona-providers" } - # Workspace eyre.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } @@ -28,10 +25,6 @@ reth-node-ethereum.workspace = true # OP Stack Dependencies superchain-registry = { workspace = true, default-features = false } -kona-derive = { workspace = true, features = ["online", "serde"] } - -# Needed for compatibility with Kona's ChainProvider trait -anyhow = { version = "1.0.86", default-features = false } # Misc url = "2.5.2" diff --git a/bin/hera/src/hera.rs b/bin/hera/src/hera.rs deleted file mode 100644 index 7828c21..0000000 --- a/bin/hera/src/hera.rs +++ /dev/null @@ -1,175 +0,0 @@ -//! Module for the Hera CLI and its subcommands. - -use clap::{Args, Parser, Subcommand}; -use eyre::{bail, Result}; -use reth::cli::Cli; -use reth_exex::{ExExContext, ExExEvent}; -use reth_node_api::FullNodeComponents; -use reth_node_ethereum::EthereumNode; -use std::{path::PathBuf, sync::Arc}; -use superchain_registry::{RollupConfig, ROLLUP_CONFIGS}; -use tracing::{debug, info}; -use url::Url; - -/// The top-level Hera CLI Command -#[derive(Debug, Parser)] -#[command(author, about = "Hera", long_about = None)] -pub struct HeraCli { - /// Hera's subcommands - #[command(subcommand)] - pub subcmd: HeraSubCmd, -} - -impl HeraCli { - /// Runs the Hera CLI - pub fn run(self) -> Result<()> { - match self.subcmd { - HeraSubCmd::ExEx(cli) => cli.run(|builder, args| async move { - let Some(cfg) = ROLLUP_CONFIGS.get(&args.l2_chain_id).cloned().map(Arc::new) else { - bail!("Rollup configuration not found for L2 chain ID: {}", args.l2_chain_id); - }; - - let node = EthereumNode::default(); - let hera = move |ctx| async { Ok(HeraExEx::new(ctx, args, cfg).await.start()) }; - let handle = builder.node(node).install_exex(crate::EXEX_ID, hera).launch().await?; - handle.wait_for_node_exit().await - }), - HeraSubCmd::Bin => unimplemented!(), - } - } -} - -/// The Hera subcommands -#[derive(Debug, Subcommand)] -pub enum HeraSubCmd { - /// The Execution Extension - #[clap(name = "exex")] - ExEx(Cli), - /// A standalone rollup node binary. - #[clap(name = "bin")] - Bin, -} - -/// The default L2 chain ID to use. This corresponds to OP Mainnet. -pub const DEFAULT_L2_CHAIN_ID: u64 = 10; - -/// The default L2 RPC URL to use. -pub const DEFAULT_L2_RPC_URL: &str = "https://optimism.llamarpc.com/"; - -/// The default L1 Beacon Client RPC URL to use. -pub const DEFAULT_L1_BEACON_CLIENT_URL: &str = "http://localhost:5052/"; - -/// The Hera Execution Extension CLI Arguments. -#[derive(Debug, Clone, Args)] -pub(crate) struct HeraArgsExt { - /// Chain ID of the L2 network - #[clap(long = "hera.l2-chain-id", default_value_t = DEFAULT_L2_CHAIN_ID)] - pub l2_chain_id: u64, - - /// RPC URL of an L2 execution client - #[clap(long = "hera.l2-rpc-url", default_value = DEFAULT_L2_RPC_URL)] - pub l2_rpc_url: Url, - - /// URL of an L1 beacon client to fetch blobs - #[clap(long = "hera.l1-beacon-client-url", default_value = DEFAULT_L1_BEACON_CLIENT_URL)] - pub l1_beacon_client_url: Url, - - /// URL of the blob archiver to fetch blobs that are expired on - /// the beacon client but still needed for processing. - /// - /// Blob archivers need to implement the `blob_sidecars` API: - /// - #[clap(long = "hera.l1-blob-archiver-url")] - pub l1_blob_archiver_url: Option, - - /// The payload validation mode to use. - /// - /// - Trusted: rely on a trusted synced L2 execution client. Validation happens by fetching the - /// same block and comparing the results. - /// - Engine API: use a local or remote engine API of an L2 execution client. Validation - /// happens by sending the `new_payload` to the API and expecting a VALID response. - #[clap( - long = "hera.validation-mode", - default_value = "trusted", - requires_ifs([("engine-api", "l2-engine-api-url"), ("engine-api", "l2-engine-jwt-secret")]), - )] - pub validation_mode: ValidationMode, - - /// If the mode is "engine api", we also need an URL for the engine API endpoint of - /// the execution client to validate the payload. - #[clap(long = "hera.l2-engine-api-url")] - pub l2_engine_api_url: Option, - - /// If the mode is "engine api", we also need a JWT secret for the auth-rpc. - /// This MUST be a valid path to a file containing the hex-encoded JWT secret. - #[clap(long = "hera.l2-engine-jwt-secret")] - pub l2_engine_jwt_secret: Option, -} - -#[derive(Debug, Clone)] -pub(crate) enum ValidationMode { - Trusted, - EngineApi, -} - -impl std::str::FromStr for ValidationMode { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "trusted" => Ok(ValidationMode::Trusted), - "engine-api" => Ok(ValidationMode::EngineApi), - _ => Err(format!("Invalid validation mode: {}", s)), - } - } -} - -/// The Hera Execution Extension. -#[derive(Debug)] -#[allow(unused)] -pub(crate) struct HeraExEx { - /// The rollup configuration - cfg: Arc, - /// The context of the Execution Extension - ctx: ExExContext, -} - -#[allow(unused)] -impl HeraExEx { - /// Creates a new instance of the Hera Execution Extension. - pub async fn new(ctx: ExExContext, args: HeraArgsExt, cfg: Arc) -> Self { - Self { ctx, cfg } - } - - /// Wait for the L2 genesis L1 block (aka "origin block") to be available in the L1 chain. - async fn wait_for_l2_genesis_l1_block(&mut self) -> Result<()> { - loop { - if let Some(notification) = self.ctx.notifications.recv().await { - if let Some(committed_chain) = notification.committed_chain() { - let tip = committed_chain.tip().block.header().number; - // TODO: commit the chain to a local buffered provider - // self.chain_provider.commit_chain(committed_chain); - - if let Err(err) = self.ctx.events.send(ExExEvent::FinishedHeight(tip)) { - bail!("Critical: Failed to send ExEx event: {:?}", err); - } - - if tip >= self.cfg.genesis.l1.number { - break Ok(()); - } else { - debug!("Chain not yet synced to rollup genesis. L1 block number: {}", tip); - } - } - } - } - } - - /// Starts the Hera Execution Extension loop. - pub async fn start(mut self) -> Result<()> { - // Step 1: Wait for the L2 origin block to be available - self.wait_for_l2_genesis_l1_block().await?; - info!("Chain synced to rollup genesis"); - - todo!("init pipeline and start processing events"); - } -} diff --git a/bin/hera/src/main.rs b/bin/hera/src/main.rs index 27f0c04..8a96f9a 100644 --- a/bin/hera/src/main.rs +++ b/bin/hera/src/main.rs @@ -1,16 +1,11 @@ +//! Hera OP Stack Rollup node + #![doc = include_str!("../README.md")] #![doc(issue_tracker_base_url = "https://github.com/paradigmxyz/op-rs/issues/")] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -use clap::Parser; use eyre::Result; -mod hera; -use hera::HeraCli; - -/// The identifier of the Hera Execution Extension. -const EXEX_ID: &str = "hera"; - fn main() -> Result<()> { - HeraCli::parse().run() + unimplemented!() } diff --git a/bin/op-rs/Cargo.toml b/bin/op-rs/Cargo.toml new file mode 100644 index 0000000..37c02bd --- /dev/null +++ b/bin/op-rs/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "op-rs" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +# Local Dependencies +rollup = { path = "../../crates/rollup" } + +# Workspace +eyre.workspace = true +tracing.workspace = true +clap.workspace = true + +# Reth Dependencies +reth.workspace = true +reth-node-ethereum.workspace = true + +# OP Stack Dependencies +superchain-registry = { workspace = true, default-features = false } diff --git a/bin/op-rs/README.md b/bin/op-rs/README.md new file mode 100644 index 0000000..5dc06cf --- /dev/null +++ b/bin/op-rs/README.md @@ -0,0 +1 @@ +# `op-rs` binary diff --git a/bin/op-rs/src/main.rs b/bin/op-rs/src/main.rs new file mode 100644 index 0000000..455ff0b --- /dev/null +++ b/bin/op-rs/src/main.rs @@ -0,0 +1,56 @@ +//! Runner for OP-RS execution extensions + +#![doc = include_str!("../README.md")] +#![doc(issue_tracker_base_url = "https://github.com/paradigmxyz/op-rs/issues/")] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +use std::sync::Arc; + +use clap::Parser; +use eyre::{bail, Result}; +use reth::cli::Cli; +use reth_node_ethereum::EthereumNode; +use rollup::{Driver, HeraArgsExt, HERA_EXEX_ID}; +use superchain_registry::ROLLUP_CONFIGS; +use tracing::{info, warn}; + +/// The Reth CLI arguments with optional Hera Execution Extension support. +#[derive(Debug, Clone, Parser)] +pub(crate) struct RethArgsExt { + /// Whether to install the Hera Execution Extension. + /// + /// Additional Hera-specific flags will be parsed if this flag is set. + #[clap(long, default_value_t = false)] + pub hera: bool, + /// The Hera Execution Extension configuration. + /// + /// This is only used if the `hera` flag is set. + #[clap(flatten)] + pub hera_config: Option, +} + +fn main() -> Result<()> { + Cli::::parse().run(|builder, args| async move { + if args.hera { + info!("Running Reth with the Hera Execution Extension"); + let Some(hera_args) = args.hera_config else { + bail!("Hera Execution Extension configuration is required when the `hera` flag is set"); + }; + + let Some(cfg) = ROLLUP_CONFIGS.get(&hera_args.l2_chain_id).cloned().map(Arc::new) else { + bail!("Rollup configuration not found for L2 chain ID: {}", hera_args.l2_chain_id); + }; + + let node = EthereumNode::default(); + let hera = move |ctx| async { Ok(Driver::new(ctx, hera_args, cfg).await.start()) }; + let handle = builder.node(node).install_exex(HERA_EXEX_ID, hera).launch().await?; + handle.wait_for_node_exit().await + } else { + warn!("Running Reth without the Hera Execution Extension"); + let node = EthereumNode::default(); + let handle = builder.node(node).launch().await?; + handle.wait_for_node_exit().await + } + }) +} diff --git a/crates/kona-providers/src/chain_provider.rs b/crates/kona-providers/src/chain_provider.rs index 0d22993..337a1da 100644 --- a/crates/kona-providers/src/chain_provider.rs +++ b/crates/kona-providers/src/chain_provider.rs @@ -1,6 +1,7 @@ //! Chain Provider use alloc::{collections::vec_deque::VecDeque, sync::Arc}; +use alloy_rlp::Decodable; use hashbrown::HashMap; use alloy::{ @@ -9,6 +10,7 @@ use alloy::{ TxLegacy, }, primitives::B256, + signers::Signature, }; use async_trait::async_trait; use kona_derive::traits::ChainProvider; @@ -188,11 +190,7 @@ impl InMemoryChainProviderInner { .flat_map(|tx| { let mut buf = Vec::new(); tx.signature.encode(&mut buf); - use alloy_rlp::Decodable; - let sig = match alloy::primitives::Signature::decode(&mut buf.as_slice()) { - Ok(s) => s, - Err(_) => return None, - }; + let sig = Signature::decode(&mut buf.as_slice()).ok()?; let new = match &tx.transaction { Transaction::Legacy(l) => { let legacy_tx = TxLegacy { diff --git a/crates/rollup/Cargo.toml b/crates/rollup/Cargo.toml new file mode 100644 index 0000000..33f8fbe --- /dev/null +++ b/crates/rollup/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "rollup" +version = "0.0.0" +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +# Workspace +eyre.workspace = true +tracing.workspace = true +clap.workspace = true +async-trait.workspace = true +tokio.workspace = true + +# Reth Dependencies +reth-exex.workspace = true +reth-node-api.workspace = true + +# OP Stack Dependencies +superchain-registry = { workspace = true, default-features = false } + +# Misc +url = "2.5.2" diff --git a/crates/rollup/src/cli.rs b/crates/rollup/src/cli.rs new file mode 100644 index 0000000..d468dc9 --- /dev/null +++ b/crates/rollup/src/cli.rs @@ -0,0 +1,93 @@ +//! Module for the Hera Execution Extension CLI arguments. + +use std::path::PathBuf; + +use clap::Args; +use url::Url; + +/// The default L2 chain ID to use. This corresponds to OP Mainnet. +pub const DEFAULT_L2_CHAIN_ID: u64 = 10; + +/// The default L2 RPC URL to use. +pub const DEFAULT_L2_RPC_URL: &str = "https://optimism.llamarpc.com/"; + +/// The default L1 Beacon Client RPC URL to use. +pub const DEFAULT_L1_BEACON_CLIENT_URL: &str = "http://localhost:5052/"; + +/// The Hera Execution Extension CLI Arguments. +#[derive(Debug, Clone, Args)] +pub struct HeraArgsExt { + /// Chain ID of the L2 network + #[clap(long = "hera.l2-chain-id", default_value_t = DEFAULT_L2_CHAIN_ID)] + pub l2_chain_id: u64, + + /// RPC URL of an L2 execution client + #[clap(long = "hera.l2-rpc-url", default_value = DEFAULT_L2_RPC_URL)] + pub l2_rpc_url: Url, + + /// URL of an L1 beacon client to fetch blobs + #[clap(long = "hera.l1-beacon-client-url", default_value = DEFAULT_L1_BEACON_CLIENT_URL)] + pub l1_beacon_client_url: Url, + + /// URL of the blob archiver to fetch blobs that are expired on + /// the beacon client but still needed for processing. + /// + /// Blob archivers need to implement the `blob_sidecars` API: + /// + #[clap(long = "hera.l1-blob-archiver-url")] + pub l1_blob_archiver_url: Option, + + /// The payload validation mode to use. + /// + /// - Trusted: rely on a trusted synced L2 execution client. Validation happens by fetching the + /// same block and comparing the results. + /// - Engine API: use a local or remote engine API of an L2 execution client. Validation + /// happens by sending the `new_payload` to the API and expecting a VALID response. + #[clap( + long = "hera.validation-mode", + default_value = "trusted", + requires_ifs([("engine-api", "l2-engine-api-url"), ("engine-api", "l2-engine-jwt-secret")]), + )] + pub validation_mode: ValidationMode, + + /// If the mode is "engine api", we also need an URL for the engine API endpoint of + /// the execution client to validate the payload. + #[clap(long = "hera.l2-engine-api-url")] + pub l2_engine_api_url: Option, + + /// If the mode is "engine api", we also need a JWT secret for the auth-rpc. + /// This MUST be a valid path to a file containing the hex-encoded JWT secret. + #[clap(long = "hera.l2-engine-jwt-secret")] + pub l2_engine_jwt_secret: Option, +} + +/// The payload validation mode. +/// +/// Every newly derived payload needs to be validated against a local +/// execution of all transactions included inside it. This can be done +/// in two ways: +/// +/// - Trusted: rely on a trusted synced L2 execution client. Validation happens by fetching the same +/// block and comparing the results. +/// - Engine API: use the authenticated engine API of an L2 execution client. Validation happens by +/// sending the `new_payload` to the API and expecting a VALID response. This method can also be +/// used to verify unsafe payloads from the sequencer. +#[derive(Debug, Clone)] +pub enum ValidationMode { + /// Use a trusted synced L2 execution client. + Trusted, + /// Use the authenticated engine API of an L2 execution client. + EngineApi, +} + +impl std::str::FromStr for ValidationMode { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "trusted" => Ok(ValidationMode::Trusted), + "engine-api" => Ok(ValidationMode::EngineApi), + _ => Err(format!("Invalid validation mode: {}", s)), + } + } +} diff --git a/crates/rollup/src/driver.rs b/crates/rollup/src/driver.rs new file mode 100644 index 0000000..1870c2e --- /dev/null +++ b/crates/rollup/src/driver.rs @@ -0,0 +1,80 @@ +//! Rollup Node Driver + +use std::sync::Arc; + +use async_trait::async_trait; +use eyre::{bail, Result}; +use reth_exex::{ExExContext, ExExEvent, ExExNotification}; +use reth_node_api::FullNodeComponents; +use superchain_registry::RollupConfig; +use tokio::sync::mpsc::error::SendError; +use tracing::{debug, info}; + +use crate::cli::HeraArgsExt; + +#[async_trait] +pub trait DriverContext { + async fn recv_notification(&mut self) -> Option; + + fn send_event(&mut self, event: ExExEvent) -> Result<(), SendError>; +} + +#[async_trait] +impl DriverContext for ExExContext { + async fn recv_notification(&mut self) -> Option { + self.notifications.recv().await + } + + fn send_event(&mut self, event: ExExEvent) -> Result<(), SendError> { + self.events.send(event) + } +} + +/// The Rollup Driver entrypoint. +#[derive(Debug)] +pub struct Driver { + /// The rollup configuration + cfg: Arc, + /// The context of the node + ctx: DC, +} + +#[allow(unused)] +impl Driver { + /// Creates a new instance of the Hera Execution Extension. + pub async fn new(ctx: DC, args: HeraArgsExt, cfg: Arc) -> Self { + Self { ctx, cfg } + } + + /// Wait for the L2 genesis L1 block (aka "origin block") to be available in the L1 chain. + async fn wait_for_l2_genesis_l1_block(&mut self) -> Result<()> { + loop { + if let Some(notification) = self.ctx.recv_notification().await { + if let Some(committed_chain) = notification.committed_chain() { + let tip = committed_chain.tip().block.header().number; + // TODO: commit the chain to a local buffered provider + // self.chain_provider.commit_chain(committed_chain); + + if let Err(err) = self.ctx.send_event(ExExEvent::FinishedHeight(tip)) { + bail!("Critical: Failed to send ExEx event: {:?}", err); + } + + if tip >= self.cfg.genesis.l1.number { + break Ok(()); + } else { + debug!("Chain not yet synced to rollup genesis. L1 block number: {}", tip); + } + } + } + } + } + + /// Starts the Hera Execution Extension loop. + pub async fn start(mut self) -> Result<()> { + // Step 1: Wait for the L2 origin block to be available + self.wait_for_l2_genesis_l1_block().await?; + info!("Chain synced to rollup genesis"); + + todo!("init pipeline and start processing events"); + } +} diff --git a/crates/rollup/src/lib.rs b/crates/rollup/src/lib.rs new file mode 100644 index 0000000..15105a5 --- /dev/null +++ b/crates/rollup/src/lib.rs @@ -0,0 +1,14 @@ +//! Rollup Node + +#![doc(issue_tracker_base_url = "https://github.com/paradigmxyz/op-rs/issues/")] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +mod driver; +pub use driver::Driver; + +mod cli; +pub use cli::HeraArgsExt; + +/// The identifier of the Hera Execution Extension. +pub const HERA_EXEX_ID: &str = "hera";