diff --git a/Cargo.lock b/Cargo.lock index cbcc7ff..b512cc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2788,26 +2788,22 @@ dependencies = [ "alloy-primitives", "alloy-provider", "alloy-rpc-types", - "alloy-rpc-types-beacon", "alloy-rpc-types-eth", - "alloy-serde", "alloy-transport", "async-trait", - "derive_more", "eyre", "futures", + "hilo-providers-alloy", + "hilo-providers-local", "kona-derive", "kona-driver", - "op-alloy-consensus", "op-alloy-genesis", "op-alloy-protocol", "op-alloy-rpc-types-engine", - "parking_lot", "reqwest", "reth-execution-types", "reth-exex", "reth-primitives", - "reth-provider", "serde", "thiserror 2.0.3", "tokio", @@ -2866,11 +2862,10 @@ version = "0.11.0" dependencies = [ "alloy-primitives", "alloy-rpc-types-engine", + "alloy-transport", "ctrlc", "hilo-driver", - "kona-driver", "op-alloy-genesis", - "op-alloy-protocol", "op-alloy-registry", "serde", "serde_json", @@ -2880,6 +2875,52 @@ dependencies = [ "url", ] +[[package]] +name = "hilo-providers-alloy" +version = "0.11.0" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-provider", + "alloy-rlp", + "alloy-rpc-types-beacon", + "alloy-serde", + "alloy-transport", + "async-trait", + "eyre", + "kona-derive", + "lru", + "op-alloy-consensus", + "op-alloy-genesis", + "op-alloy-protocol", + "parking_lot", + "reqwest", + "serde", + "thiserror 2.0.3", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hilo-providers-local" +version = "0.11.0" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "async-trait", + "derive_more", + "kona-derive", + "op-alloy-consensus", + "op-alloy-genesis", + "op-alloy-protocol", + "parking_lot", + "reth-primitives", + "reth-provider", +] + [[package]] name = "hkdf" version = "0.12.4" diff --git a/Cargo.toml b/Cargo.toml index 34a42a0..685e10e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,8 @@ hilo-net = { version = "0.11.0", path = "crates/net", default-features = false } hilo-node = { version = "0.11.0", path = "crates/node", default-features = false } hilo-driver = { version = "0.11.0", path = "crates/driver", default-features = false } hilo-engine = { version = "0.11.0", path = "crates/engine", default-features = false } +hilo-providers-local = { version = "0.11.0", path = "crates/providers-local", default-features = false } +hilo-providers-alloy = { version = "0.11.0", path = "crates/providers-alloy", default-features = false } # Kona kona-derive = { version = "0.1.0", default-features = false } diff --git a/crates/driver/Cargo.toml b/crates/driver/Cargo.toml index 179c8c2..dbc91de 100644 --- a/crates/driver/Cargo.toml +++ b/crates/driver/Cargo.toml @@ -13,18 +13,20 @@ repository.workspace = true rust-version.workspace = true [dependencies] +# Local +hilo-providers-local.workspace = true +hilo-providers-alloy.workspace = true + # Kona kona-derive.workspace = true kona-driver.workspace = true # Alloy alloy-eips.workspace = true -alloy-serde.workspace = true alloy-network.workspace = true alloy-transport.workspace = true alloy-consensus.workspace = true alloy-rpc-types-eth.workspace = true -alloy-rpc-types-beacon.workspace = true alloy-provider = { workspace = true, features = ["ipc", "ws", "reqwest"] } alloy-rpc-types = { workspace = true, features = ["ssz"] } alloy-primitives = { workspace = true, features = ["map"] } @@ -32,27 +34,25 @@ alloy-primitives = { workspace = true, features = ["map"] } # Op Alloy op-alloy-genesis.workspace = true op-alloy-protocol.workspace = true -op-alloy-consensus.workspace = true op-alloy-rpc-types-engine.workspace = true # Reth -reth-provider.workspace = true reth-primitives.workspace = true reth-execution-types.workspace = true reth-exex = { workspace = true, features = ["serde"] } # Misc url.workspace = true -eyre.workspace = true -serde.workspace = true tokio.workspace = true +serde.workspace = true tracing.workspace = true futures.workspace = true thiserror.workspace = true async-trait.workspace = true -parking_lot.workspace = true -reqwest = { workspace = true, features = ["json"] } -derive_more = { workspace = true, features = ["full"] } + +[dev-dependencies] +reqwest.workspace = true +eyre.workspace = true [features] default = [] diff --git a/crates/driver/src/config.rs b/crates/driver/src/config.rs new file mode 100644 index 0000000..229afa4 --- /dev/null +++ b/crates/driver/src/config.rs @@ -0,0 +1,127 @@ +//! Configuration for the Hilo Driver. + +use kona_derive::traits::ChainProvider; +use kona_driver::PipelineCursor; +use op_alloy_genesis::RollupConfig; +use op_alloy_protocol::{BatchValidationProvider, BlockInfo, L2BlockInfo}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use url::Url; + +use hilo_providers_alloy::{ + AlloyChainProvider, AlloyL2ChainProvider, BeaconClient, OnlineBeaconClient, OnlineBlobProvider, + OnlineBlobProviderWithFallback, +}; + +/// An error thrown by a [Config] operation. +#[derive(Debug, thiserror::Error)] +pub enum ConfigError { + /// An error thrown by the beacon client. + #[error("beacon client error: {0}")] + Beacon(String), + /// An L2 chain provider error. + #[error("L2 chain provider error: {0}")] + L2ChainProvider(String), + /// An L1 chain provider error. + #[error("L1 chain provider error: {0}")] + ChainProvider(String), +} + +/// The global node configuration. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Config { + /// The L2 Chain ID. + pub l2_chain_id: u64, + /// The L1 chain RPC URL + pub l1_rpc_url: Url, + /// The base chain beacon client RPC URL + pub l1_beacon_url: Url, + /// An optional blob archiver URL used in the fallback provider. + pub blob_archiver_url: Option, + /// The L2 chain RPC URL + pub l2_rpc_url: Url, + /// The L2 engine API URL + pub l2_engine_url: Url, + /// The rollup config + pub rollup_config: RollupConfig, + /// The hilo-node RPC server + pub rpc_url: Option, + /// The cache size for in-memory providers. + pub cache_size: usize, +} + +impl Config { + /// Construct an [OnlineBlobProviderWithFallback] from the [Config]. + pub async fn blob_provider( + &self, + ) -> Result, ConfigError> + { + let beacon_client = OnlineBeaconClient::new_http(String::from(self.l1_beacon_url.clone())); + let genesis = Some(self.rollup_config.genesis.l1.number); + let slot_interval = beacon_client + .config_spec() + .await + .map_err(|e| ConfigError::Beacon(e.to_string()))? + .data + .seconds_per_slot; + let blob = OnlineBlobProvider::new(beacon_client, genesis, Some(slot_interval)); + Ok(OnlineBlobProviderWithFallback::new(blob, None)) + } + + /// Returns an [AlloyChainProvider] from the configured provider endpoints. + pub fn l1_chain_provider(&self) -> AlloyChainProvider { + AlloyChainProvider::new_http(self.l1_rpc_url.clone()) + } + + /// Returns the L2 provider. + pub fn l2_provider(&self) -> AlloyL2ChainProvider { + AlloyL2ChainProvider::new_http( + self.l2_rpc_url.clone(), + Arc::new(self.rollup_config.clone()), + ) + } + + /// Returns the safe head tip. + /// The chain tip includes the safe L1 block info and the L2 block info. + pub async fn safe_tip(&self) -> Result<(BlockInfo, L2BlockInfo), ConfigError> { + let mut l2_provider = self.l2_provider(); + let latest_block_number = l2_provider + .latest_block_number() + .await + .map_err(|e| ConfigError::L2ChainProvider(e.to_string()))?; + let l2_block_info = l2_provider + .l2_block_info_by_number(latest_block_number) + .await + .map_err(|e| ConfigError::L2ChainProvider(e.to_string()))?; + + let mut l1_provider = self.l1_chain_provider(); + let l1_block_info = l1_provider + .block_info_by_number(l2_block_info.l1_origin.number) + .await + .map_err(|e| ConfigError::ChainProvider(e.to_string()))?; + + Ok((l1_block_info, l2_block_info)) + } + + /// Constructs a [PipelineCursor] from the origin. + pub async fn tip_cursor(&self) -> Result { + // Load the safe head info. + let (origin, safe_head_info) = self.safe_tip().await?; + + // Calculate the channel timeout + let channel_timeout = + self.rollup_config.channel_timeout(safe_head_info.block_info.timestamp); + let mut l1_origin_number = origin.number.saturating_sub(channel_timeout); + if l1_origin_number < self.rollup_config.genesis.l1.number { + l1_origin_number = self.rollup_config.genesis.l1.number; + } + + // Create the pipeline cursor from the origin + let mut l1_provider = self.l1_chain_provider(); + let l1_origin = l1_provider + .block_info_by_number(l1_origin_number) + .await + .map_err(|e| ConfigError::ChainProvider(e.to_string()))?; + Ok(PipelineCursor::new(channel_timeout, l1_origin)) + } +} diff --git a/crates/driver/src/driver.rs b/crates/driver/src/driver.rs index 026fd08..b506178 100644 --- a/crates/driver/src/driver.rs +++ b/crates/driver/src/driver.rs @@ -1,15 +1,13 @@ //! Contains the core `HiloDriver`. -#![allow(unused)] -use crate::{ - Context, HiloDerivationPipeline, HiloExecutor, HiloExecutorConstructor, HiloPipeline, - StandaloneContext, -}; use alloy_transport::TransportResult; -use kona_driver::{Driver, PipelineCursor}; -use op_alloy_genesis::RollupConfig; use std::sync::Arc; -use url::Url; + +use hilo_providers_local::{InMemoryChainProvider, InMemoryL2ChainProvider}; + +use crate::{ + Config, ConfigError, Context, HiloExecutorConstructor, HiloPipeline, StandaloneContext, +}; /// HiloDriver is a wrapper around the `Driver` that /// provides methods of constructing the driver. @@ -17,23 +15,17 @@ use url::Url; pub struct HiloDriver { /// The driver context. pub ctx: C, - /// The rollup config. - cfg: Arc, - /// The driver instance. - pub driver: Driver, + /// The driver config. + pub cfg: Config, + /// A constructor for execution. + pub exec: HiloExecutorConstructor, } impl HiloDriver { /// Creates a new [HiloDriver] with a standalone context. - pub async fn standalone( - cfg: Arc, - l1_rpc_url: Url, - cursor: PipelineCursor, - exec: HiloExecutorConstructor, - pipeline: HiloPipeline, - ) -> TransportResult { - let ctx = StandaloneContext::new(l1_rpc_url).await?; - Ok(Self::new(cfg, ctx, cursor, exec, pipeline)) + pub async fn standalone(cfg: Config, exec: HiloExecutorConstructor) -> TransportResult { + let ctx = StandaloneContext::new(cfg.l1_rpc_url.clone()).await?; + Ok(Self::new(cfg, ctx, exec)) } } @@ -42,19 +34,48 @@ where C: Context, { /// Constructs a new [HiloDriver]. - pub fn new( - cfg: Arc, - ctx: C, - cursor: PipelineCursor, - exec: HiloExecutorConstructor, - pipeline: HiloPipeline, - ) -> Self { - Self { cfg, ctx, driver: Driver::new(cursor, exec, pipeline) } + pub fn new(cfg: Config, ctx: C, exec: HiloExecutorConstructor) -> Self { + Self { cfg, ctx, exec } } - /// Continuously run the [Driver]. - pub async fn start(&mut self) { - todo!("upstream implementation of a continuous driver func") + /// Initializes the pipeline. + pub async fn init_pipeline(&self) -> Result { + let cursor = self.cfg.tip_cursor().await?; + let chain_provider = InMemoryChainProvider::with_capacity(self.cfg.cache_size); + let l2_chain_provider = InMemoryL2ChainProvider::with_capacity(self.cfg.cache_size); + Ok(HiloPipeline::new( + Arc::new(self.cfg.rollup_config.clone()), + cursor.clone(), + self.cfg.blob_provider().await?, + chain_provider.clone(), + l2_chain_provider, + )) + } + + /// Continuously run the [HiloDriver]. + pub async fn start(&mut self) -> Result<(), ConfigError> { + // Step 1: Wait for the L2 origin block to be available + self.wait_for_l2_genesis_l1_block().await; + info!("L1 chain synced to the rollup genesis block"); + + // Step 2: Initialize the rollup pipeline + let _ = self.init_pipeline().await?; + info!("Derivation pipeline initialized"); + + // Step 3: Start the processing loop + // loop { + // // Try to advance the pipeline until there's no more data to process + // if self.step(&mut pipeline).await { + // continue; + // } + // + // // Handle any incoming notifications from the context + // if let Some(notification) = self.ctx.recv_notification().await { + // self.handle_notification(notification, &mut pipeline).await?; + // } + // } + + Ok(()) } /// Wait for the L2 genesis' corresponding L1 block to be available in the L1 chain. @@ -65,7 +86,7 @@ where let tip = new_chain.tip(); self.ctx.send_processed_tip_event(tip); - if tip.number >= self.cfg.genesis.l1.number { + if tip.number >= self.cfg.rollup_config.genesis.l1.number { break; } debug!( diff --git a/crates/driver/src/lib.rs b/crates/driver/src/lib.rs index 3d68b22..9941898 100644 --- a/crates/driver/src/lib.rs +++ b/crates/driver/src/lib.rs @@ -2,44 +2,24 @@ #![doc(issue_tracker_base_url = "https://github.com/anton-rs/hilo/issues/")] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #![cfg_attr(not(test), warn(unused_crate_dependencies))] -// #![cfg_attr(not(test), no_std)] -// extern crate alloc; #[macro_use] extern crate tracing; +mod config; +pub use config::{Config, ConfigError}; + mod executor; pub use executor::{HiloExecutor, HiloExecutorConstructor}; mod driver; pub use driver::HiloDriver; -mod blobs; -pub use blobs::{ - BlobSidecarProvider, OnlineBlobProvider, OnlineBlobProviderBuilder, - OnlineBlobProviderWithFallback, -}; - mod context; pub use context::{Context, StandaloneContext}; -mod beacon_client; -pub use beacon_client::{ - APIConfigResponse, APIGenesisResponse, BeaconClient, OnlineBeaconClient, ReducedConfigData, - ReducedGenesisData, -}; - mod pipeline; pub use pipeline::{ HiloAttributesBuilder, HiloAttributesQueue, HiloDataProvider, HiloDerivationPipeline, HiloPipeline, }; - -mod chain_provider; -pub use chain_provider::{reth_to_alloy_tx, InMemoryChainProvider}; - -mod blob_provider; -pub use blob_provider::{DurableBlobProvider, InnerBlobProvider, LayeredBlobProvider}; - -mod l2_chain_provider; -pub use l2_chain_provider::InMemoryL2ChainProvider; diff --git a/crates/driver/src/pipeline.rs b/crates/driver/src/pipeline.rs index b7dd48b..185dd7d 100644 --- a/crates/driver/src/pipeline.rs +++ b/crates/driver/src/pipeline.rs @@ -19,7 +19,8 @@ use op_alloy_protocol::{BlockInfo, L2BlockInfo}; use op_alloy_rpc_types_engine::OpAttributesWithParent; use std::{boxed::Box, sync::Arc}; -use crate::{DurableBlobProvider, InMemoryChainProvider, InMemoryL2ChainProvider}; +use hilo_providers_alloy::DurableBlobProvider; +use hilo_providers_local::{InMemoryChainProvider, InMemoryL2ChainProvider}; /// Hilo Derivation Pipeline. pub type HiloDerivationPipeline = diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 3c0d31e..5186f0c 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -13,18 +13,15 @@ repository.workspace = true rust-version.workspace = true [dependencies] -# Hilo +# Local hilo-driver.workspace = true -# Kona -kona-driver.workspace = true - # Alloy +alloy-transport.workspace = true alloy-primitives.workspace = true alloy-rpc-types-engine = { workspace = true, features = ["jwt", "serde"] } # op-alloy -op-alloy-protocol.workspace = true op-alloy-genesis = { workspace = true, features = ["serde"] } # Misc diff --git a/crates/node/src/config.rs b/crates/node/src/config.rs index c780665..af8247e 100644 --- a/crates/node/src/config.rs +++ b/crates/node/src/config.rs @@ -6,6 +6,20 @@ use op_alloy_genesis::RollupConfig; use serde::{Deserialize, Serialize}; use url::Url; +/// An error thrown by a [Config] operation. +#[derive(Debug, thiserror::Error)] +pub enum ConfigError { + /// An error thrown by the beacon client. + #[error("beacon client error: {0}")] + Beacon(String), + /// An L2 chain provider error. + #[error("L2 chain provider error: {0}")] + L2ChainProvider(String), + /// An L1 chain provider error. + #[error("L1 chain provider error: {0}")] + ChainProvider(String), +} + /// The global node configuration. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Config { @@ -40,6 +54,22 @@ pub struct Config { pub cache_size: usize, } +impl From for hilo_driver::Config { + fn from(config: Config) -> Self { + hilo_driver::Config { + l2_chain_id: config.l2_chain_id, + l1_rpc_url: config.l1_rpc_url, + l1_beacon_url: config.l1_beacon_url, + blob_archiver_url: config.blob_archiver_url, + l2_rpc_url: config.l2_rpc_url, + l2_engine_url: config.l2_engine_url, + rollup_config: config.rollup_config, + rpc_url: config.rpc_url, + cache_size: config.cache_size, + } + } +} + fn as_hex(v: &JwtSecret, serializer: S) -> Result where S: serde::ser::Serializer, diff --git a/crates/node/src/errors.rs b/crates/node/src/errors.rs new file mode 100644 index 0000000..f78e8bb --- /dev/null +++ b/crates/node/src/errors.rs @@ -0,0 +1,33 @@ +//! Node error types. + +use crate::ConfigError; + +/// A high-level `Node`error. +#[derive(Debug, thiserror::Error)] +pub enum NodeError { + /// An error occurred during standalone initialization. + #[error("standalone initialization failed")] + StandaloneInit, + /// An error from a provider method. + #[error("provider error: {0}")] + Provider(String), + /// An error thrown by a [crate::Config] operation. + #[error("config error: {0}")] + Beacon(#[from] ConfigError), +} + +impl From for NodeError { + fn from(e: alloy_transport::TransportError) -> Self { + Self::Provider(e.to_string()) + } +} + +impl From for NodeError { + fn from(e: hilo_driver::ConfigError) -> Self { + match e { + hilo_driver::ConfigError::Beacon(e) => Self::Beacon(ConfigError::Beacon(e)), + hilo_driver::ConfigError::L2ChainProvider(e) => Self::Provider(e), + hilo_driver::ConfigError::ChainProvider(e) => Self::Provider(e), + } + } +} diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index cb1aef1..3d57a20 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -6,6 +6,9 @@ #[macro_use] extern crate tracing; +mod errors; +pub use errors::NodeError; + mod node; pub use node::Node; @@ -13,4 +16,4 @@ mod sync; pub use sync::SyncMode; mod config; -pub use config::Config; +pub use config::{Config, ConfigError}; diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs index 7656357..12c731f 100644 --- a/crates/node/src/node.rs +++ b/crates/node/src/node.rs @@ -1,24 +1,9 @@ //! Contains the core `Node` runner. -use crate::{Config, SyncMode}; -use hilo_driver::{ - HiloDriver, HiloExecutorConstructor, HiloPipeline, InMemoryChainProvider, - InMemoryL2ChainProvider, OnlineBeaconClient, OnlineBlobProvider, - OnlineBlobProviderWithFallback, -}; -use kona_driver::PipelineCursor; -use op_alloy_protocol::{BlockInfo, L2BlockInfo}; -use std::sync::Arc; +use crate::{Config, NodeError, SyncMode}; +use hilo_driver::{HiloDriver, HiloExecutorConstructor}; use tokio::sync::watch::{channel, Receiver}; -/// A high-level `Node` error. -#[derive(Debug, thiserror::Error)] -pub enum NodeError { - /// An error occurred during standalone initialization. - #[error("standalone initialization failed")] - StandaloneInit, -} - /// The core node runner. #[derive(Debug)] pub struct Node { @@ -101,63 +86,12 @@ impl Node { unimplemented!(); } - /// Construct a blob provider from the config. - pub fn blob_provider( - &self, - ) -> OnlineBlobProviderWithFallback { - let beacon_client = - OnlineBeaconClient::new_http(String::from(self.config.l1_beacon_url.clone())); - let genesis = Some(self.config.rollup_config.genesis.l1.number); - // TODO: fix the slot interval here - let blob = OnlineBlobProvider::new(beacon_client, genesis, Some(10)); - OnlineBlobProviderWithFallback::new(blob, None) - } - /// Creates and starts the [HiloDriver] which handles the derivation sync process. async fn start_driver(&self) -> Result<(), NodeError> { - // TODO: use the proper safe head info. - // This should be pulled in using the checkpoint hash. - let safe_head_info = L2BlockInfo::default(); - // let l1_origin = BlockInfo::default(); - let channel_timeout = - self.config.rollup_config.channel_timeout(safe_head_info.block_info.timestamp); - // let mut l1_origin_number = l1_origin.number.saturating_sub(channel_timeout); - // if l1_origin_number < self.config.rollup_config.genesis.l1.number { - // l1_origin_number = self.config.rollup_config.genesis.l1.number; - // } - - // TODO: pull in the correct origin using the chain provider - // let origin = chain_provider.block_info_by_number(l1_origin_number).await?; - let origin = BlockInfo::default(); - let cursor = PipelineCursor::new(channel_timeout, origin); - - // TODO: pull in chain capacity from config and cli - let chain_provider = InMemoryChainProvider::with_capacity(100); - let l2_chain_provider = InMemoryL2ChainProvider::with_capacity(100); - let pipeline = HiloPipeline::new( - Arc::new(self.config.rollup_config.clone()), - cursor.clone(), - self.blob_provider(), - chain_provider.clone(), - l2_chain_provider, - ); - let executor = HiloExecutorConstructor::new(); - let mut driver = HiloDriver::standalone( - Arc::new(self.config.rollup_config.clone()), - self.config.l1_rpc_url.clone(), - cursor, - executor, - pipeline, - ) - .await - .map_err(|_| NodeError::StandaloneInit)?; - - // Run the derivation pipeline until we are able to produce the output root of the claimed - // L2 block. - // let (number, output_root) = - // driver.advance_to_target(&boot.rollup_config, boot.claimed_l2_block_number).await?; - - driver.start().await; + let cfg = self.config.clone().into(); + let exec = HiloExecutorConstructor::new(); + let mut driver = HiloDriver::standalone(cfg, exec).await?; + driver.start().await?; Ok(()) } diff --git a/crates/providers-alloy/Cargo.toml b/crates/providers-alloy/Cargo.toml new file mode 100644 index 0000000..dbf0f53 --- /dev/null +++ b/crates/providers-alloy/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "hilo-providers-alloy" +description = "Alloy-backed providers for hilo" + +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +# Kona +kona-derive.workspace = true + +# Alloy +alloy-rlp.workspace = true +alloy-serde.workspace = true +alloy-eips.workspace = true +alloy-transport.workspace = true +alloy-consensus.workspace = true +alloy-provider = { workspace = true, features = ["ipc", "ws", "reqwest"] } +alloy-primitives = { workspace = true, features = ["map"] } +alloy-rpc-types-beacon.workspace = true + +# Op Alloy +op-alloy-genesis.workspace = true +op-alloy-protocol.workspace = true +op-alloy-consensus.workspace = true + +# Misc +url.workspace = true +tracing.workspace = true +lru.workspace = true +serde.workspace = true +eyre.workspace = true +thiserror.workspace = true +async-trait.workspace = true +parking_lot.workspace = true +reqwest = { workspace = true, features = ["json"] } + +[dev-dependencies] +tokio.workspace = true diff --git a/crates/providers-alloy/README.md b/crates/providers-alloy/README.md new file mode 100644 index 0000000..b80d28d --- /dev/null +++ b/crates/providers-alloy/README.md @@ -0,0 +1,3 @@ +# `hilo-providers-alloy` + +Alloy-backed providers for hilo. diff --git a/crates/driver/src/beacon_client.rs b/crates/providers-alloy/src/beacon_client.rs similarity index 100% rename from crates/driver/src/beacon_client.rs rename to crates/providers-alloy/src/beacon_client.rs diff --git a/crates/driver/src/blob_provider.rs b/crates/providers-alloy/src/blob_provider.rs similarity index 100% rename from crates/driver/src/blob_provider.rs rename to crates/providers-alloy/src/blob_provider.rs diff --git a/crates/driver/src/blobs.rs b/crates/providers-alloy/src/blobs.rs similarity index 100% rename from crates/driver/src/blobs.rs rename to crates/providers-alloy/src/blobs.rs diff --git a/crates/providers-alloy/src/chain_provider.rs b/crates/providers-alloy/src/chain_provider.rs new file mode 100644 index 0000000..8e733b3 --- /dev/null +++ b/crates/providers-alloy/src/chain_provider.rs @@ -0,0 +1,243 @@ +//! Providers that use alloy provider types on the backend. + +use alloy_consensus::{Block, Header, Receipt, ReceiptWithBloom, TxEnvelope, TxType}; +use alloy_primitives::{Bytes, B256, U64}; +use alloy_provider::{Provider, ReqwestProvider}; +use alloy_rlp::{Buf, Decodable}; +use alloy_transport::{RpcError, TransportErrorKind}; +use async_trait::async_trait; +use kona_derive::{ + errors::{PipelineError, PipelineErrorKind}, + traits::ChainProvider, +}; +use lru::LruCache; +use op_alloy_protocol::BlockInfo; +use std::{boxed::Box, num::NonZeroUsize, vec::Vec}; + +const CACHE_SIZE: usize = 16; + +/// The [AlloyChainProvider] is a concrete implementation of the [ChainProvider] trait, providing +/// data over Ethereum JSON-RPC using an alloy provider as the backend. +/// +/// **Note**: +/// This provider fetches data using the `debug_getRawHeader`, `debug_getRawReceipts`, and +/// `debug_getRawBlock` methods. The RPC must support this namespace. +#[derive(Debug, Clone)] +pub struct AlloyChainProvider { + /// The inner Ethereum JSON-RPC provider. + inner: ReqwestProvider, + /// `header_by_hash` LRU cache. + header_by_hash_cache: LruCache, + /// `block_info_by_number` LRU cache. + block_info_by_number_cache: LruCache, + /// `block_info_by_number` LRU cache. + receipts_by_hash_cache: LruCache>, + /// `block_info_and_transactions_by_hash` LRU cache. + block_info_and_transactions_by_hash_cache: LruCache)>, +} + +impl AlloyChainProvider { + /// Creates a new [AlloyChainProvider] with the given alloy provider. + pub fn new(inner: ReqwestProvider) -> Self { + Self { + inner, + header_by_hash_cache: LruCache::new(NonZeroUsize::new(CACHE_SIZE).unwrap()), + block_info_by_number_cache: LruCache::new(NonZeroUsize::new(CACHE_SIZE).unwrap()), + receipts_by_hash_cache: LruCache::new(NonZeroUsize::new(CACHE_SIZE).unwrap()), + block_info_and_transactions_by_hash_cache: LruCache::new( + NonZeroUsize::new(CACHE_SIZE).unwrap(), + ), + } + } + + /// Creates a new [AlloyChainProvider] from the provided [reqwest::Url]. + pub fn new_http(url: reqwest::Url) -> Self { + let inner = ReqwestProvider::new_http(url); + Self::new(inner) + } + + /// Returns the latest L2 block number. + pub async fn latest_block_number(&mut self) -> Result> { + self.inner.get_block_number().await + } + + /// Returns the chain ID. + pub async fn chain_id(&mut self) -> Result> { + self.inner.get_chain_id().await + } +} + +/// An error for the [AlloyChainProvider]. +#[allow(clippy::enum_variant_names)] +#[derive(Debug, thiserror::Error)] +pub enum AlloyChainProviderError { + /// Failed to fetch the raw header. + #[error("Failed to fetch raw header for hash {0}")] + RawHeaderFetch(B256), + /// Failed to decode the raw header. + #[error("Failed to decode raw header for hash {0}")] + RawHeaderDecoding(B256), + /// Failed to fetch the raw receipts. + #[error("Failed to fetch raw receipts for hash {0}")] + RawReceiptsFetch(B256), + /// Failed to decode the raw receipts. + #[error("Failed to decode raw receipts for hash {0}")] + RawReceiptsDecoding(B256), +} + +impl From for PipelineErrorKind { + fn from(e: AlloyChainProviderError) -> Self { + match e { + AlloyChainProviderError::RawHeaderFetch(_) => PipelineErrorKind::Temporary( + PipelineError::Provider("Failed to fetch raw header".to_string()), + ), + AlloyChainProviderError::RawHeaderDecoding(_) => PipelineErrorKind::Temporary( + PipelineError::Provider("Failed to decode raw header".to_string()), + ), + AlloyChainProviderError::RawReceiptsFetch(_) => PipelineErrorKind::Temporary( + PipelineError::Provider("Failed to fetch raw receipts".to_string()), + ), + AlloyChainProviderError::RawReceiptsDecoding(_) => PipelineErrorKind::Temporary( + PipelineError::Provider("Failed to decode raw receipts".to_string()), + ), + } + } +} + +#[async_trait] +impl ChainProvider for AlloyChainProvider { + type Error = AlloyChainProviderError; + + async fn header_by_hash(&mut self, hash: B256) -> Result { + if let Some(header) = self.header_by_hash_cache.get(&hash) { + return Ok(header.clone()); + } + + let raw_header: Bytes = self + .inner + .raw_request("debug_getRawHeader".into(), [hash]) + .await + .map_err(|_| AlloyChainProviderError::RawHeaderFetch(hash))?; + match Header::decode(&mut raw_header.as_ref()) { + Ok(header) => { + self.header_by_hash_cache.put(hash, header.clone()); + Ok(header) + } + Err(_) => Err(AlloyChainProviderError::RawHeaderDecoding(hash)), + } + } + + async fn block_info_by_number(&mut self, number: u64) -> Result { + if let Some(block_info) = self.block_info_by_number_cache.get(&number) { + return Ok(*block_info); + } + + let raw_header: Bytes = self + .inner + .raw_request("debug_getRawHeader".into(), [U64::from(number)]) + .await + .map_err(|_| AlloyChainProviderError::RawHeaderFetch(B256::default()))?; + let header = match Header::decode(&mut raw_header.as_ref()) { + Ok(h) => h, + Err(_) => { + return Err(AlloyChainProviderError::RawHeaderDecoding(B256::default())); + } + }; + + let block_info = BlockInfo { + hash: header.hash_slow(), + number, + parent_hash: header.parent_hash, + timestamp: header.timestamp, + }; + self.block_info_by_number_cache.put(number, block_info); + Ok(block_info) + } + + async fn receipts_by_hash(&mut self, hash: B256) -> Result, Self::Error> { + if let Some(receipts) = self.receipts_by_hash_cache.get(&hash) { + return Ok(receipts.clone()); + } + + let raw_receipts: Vec = self + .inner + .raw_request("debug_getRawReceipts".into(), [hash]) + .await + .map_err(|_| AlloyChainProviderError::RawReceiptsFetch(hash))?; + + let receipts = raw_receipts + .iter() + .map(|r| { + let r = &mut r.as_ref(); + + // Skip the transaction type byte if it exists + if !r.is_empty() && r[0] <= TxType::Eip4844 as u8 { + r.advance(1); + } + + Ok(ReceiptWithBloom::decode(r) + .map_err(|_| AlloyChainProviderError::RawReceiptsDecoding(hash))? + .receipt) + }) + .collect::, Self::Error>>()?; + self.receipts_by_hash_cache.put(hash, receipts.clone()); + Ok(receipts) + } + + async fn block_info_and_transactions_by_hash( + &mut self, + hash: B256, + ) -> Result<(BlockInfo, Vec), Self::Error> { + if let Some(block_info_and_txs) = self.block_info_and_transactions_by_hash_cache.get(&hash) + { + return Ok(block_info_and_txs.clone()); + } + + let raw_block: Bytes = self + .inner + .raw_request("debug_getRawBlock".into(), [hash]) + .await + .map_err(|_| AlloyChainProviderError::RawHeaderFetch(hash))?; + let block: Block = match Block::decode(&mut raw_block.as_ref()) { + Ok(b) => b, + Err(_) => { + return Err(AlloyChainProviderError::RawHeaderDecoding(hash)); + } + }; + + let block_info = BlockInfo { + hash: block.header.hash_slow(), + number: block.header.number, + parent_hash: block.header.parent_hash, + timestamp: block.header.timestamp, + }; + self.block_info_and_transactions_by_hash_cache + .put(hash, (block_info, block.body.transactions.clone())); + Ok((block_info, block.body.transactions)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn default_provider() -> ReqwestProvider { + ReqwestProvider::new_http("https://docs-demo.quiknode.pro/".try_into().unwrap()) + } + + #[tokio::test] + #[ignore] + async fn test_alloy_chain_provider_latest_block_number() { + let mut provider = AlloyChainProvider::new(default_provider()); + let number = provider.latest_block_number().await.unwrap(); + assert!(number > 0); + } + + #[tokio::test] + #[ignore] + async fn test_alloy_chain_provider_chain_id() { + let mut provider = AlloyChainProvider::new(default_provider()); + let chain_id = provider.chain_id().await.unwrap(); + assert_eq!(chain_id, 1); + } +} diff --git a/crates/providers-alloy/src/l2_chain_provider.rs b/crates/providers-alloy/src/l2_chain_provider.rs new file mode 100644 index 0000000..8c520bb --- /dev/null +++ b/crates/providers-alloy/src/l2_chain_provider.rs @@ -0,0 +1,210 @@ +//! Providers that use alloy provider types on the backend. + +use alloy_primitives::{Bytes, U64}; +use alloy_provider::{Provider, ReqwestProvider}; +use alloy_rlp::Decodable; +use alloy_transport::{RpcError, TransportErrorKind, TransportResult}; +use async_trait::async_trait; +use kona_derive::{ + errors::{PipelineError, PipelineErrorKind}, + traits::L2ChainProvider, +}; +use lru::LruCache; +use op_alloy_consensus::OpBlock; +use op_alloy_genesis::{RollupConfig, SystemConfig}; +use op_alloy_protocol::{to_system_config, BatchValidationProvider, L2BlockInfo}; +use std::{boxed::Box, num::NonZeroUsize, sync::Arc}; + +const CACHE_SIZE: usize = 16; + +/// The [AlloyL2ChainProvider] is a concrete implementation of the [L2ChainProvider] trait, +/// providing data over Ethereum JSON-RPC using an alloy provider as the backend. +/// +/// **Note**: +/// This provider fetches data using the `debug_getRawBlock` method. The RPC must support this +/// namespace. +#[derive(Debug, Clone)] +pub struct AlloyL2ChainProvider { + /// The inner Ethereum JSON-RPC provider. + inner: ReqwestProvider, + /// The rollup configuration. + rollup_config: Arc, + /// `block_by_number` LRU cache. + block_by_number_cache: LruCache, + /// `l2_block_info_by_number` LRU cache. + l2_block_info_by_number_cache: LruCache, + /// `system_config_by_l2_hash` LRU cache. + system_config_by_number_cache: LruCache, +} + +impl AlloyL2ChainProvider { + /// Creates a new [AlloyL2ChainProvider] with the given alloy provider and [RollupConfig]. + pub fn new(inner: ReqwestProvider, rollup_config: Arc) -> Self { + Self { + inner, + rollup_config, + block_by_number_cache: LruCache::new(NonZeroUsize::new(CACHE_SIZE).unwrap()), + l2_block_info_by_number_cache: LruCache::new(NonZeroUsize::new(CACHE_SIZE).unwrap()), + system_config_by_number_cache: LruCache::new(NonZeroUsize::new(CACHE_SIZE).unwrap()), + } + } + + /// Returns the chain ID. + pub async fn chain_id(&mut self) -> Result> { + self.inner.get_chain_id().await + } + + /// Returns the latest L2 block number. + pub async fn latest_block_number(&mut self) -> Result> { + self.inner.get_block_number().await + } + + /// Creates a new [AlloyL2ChainProvider] from the provided [reqwest::Url]. + pub fn new_http(url: reqwest::Url, rollup_config: Arc) -> Self { + let inner = ReqwestProvider::new_http(url); + Self::new(inner, rollup_config) + } +} + +/// An error for the [AlloyL2ChainProvider]. +#[derive(Debug, thiserror::Error)] +pub enum AlloyL2ChainProviderError { + /// Failed to find a block. + #[error("Failed to fetch block {0}")] + BlockNotFound(u64), + /// Failed to construct [L2BlockInfo] from the block and genesis. + #[error("Failed to construct L2BlockInfo from block {0} and genesis")] + L2BlockInfoConstruction(u64), + /// Failed to decode an [OpBlock] from the raw block. + #[error("Failed to decode OpBlock from raw block {0}")] + OpBlockDecode(u64), + /// Failed to convert the block into a [SystemConfig]. + #[error("Failed to convert block {0} into SystemConfig")] + SystemConfigConversion(u64), +} + +impl From for PipelineErrorKind { + fn from(e: AlloyL2ChainProviderError) -> Self { + match e { + AlloyL2ChainProviderError::BlockNotFound(_) => { + PipelineErrorKind::Temporary(PipelineError::Provider("block not found".to_string())) + } + AlloyL2ChainProviderError::L2BlockInfoConstruction(_) => PipelineErrorKind::Temporary( + PipelineError::Provider("l2 block info construction failed".to_string()), + ), + AlloyL2ChainProviderError::OpBlockDecode(_) => PipelineErrorKind::Temporary( + PipelineError::Provider("op block decode failed".to_string()), + ), + AlloyL2ChainProviderError::SystemConfigConversion(_) => PipelineErrorKind::Temporary( + PipelineError::Provider("system config conversion failed".to_string()), + ), + } + } +} + +#[async_trait] +impl BatchValidationProvider for AlloyL2ChainProvider { + type Error = AlloyL2ChainProviderError; + + async fn l2_block_info_by_number(&mut self, number: u64) -> Result { + if let Some(l2_block_info) = self.l2_block_info_by_number_cache.get(&number) { + return Ok(*l2_block_info); + } + + let block = match self.block_by_number(number).await { + Ok(p) => p, + Err(_) => { + return Err(AlloyL2ChainProviderError::BlockNotFound(number)); + } + }; + let l2_block_info = + match L2BlockInfo::from_block_and_genesis(&block, &self.rollup_config.genesis) { + Ok(b) => b, + Err(_) => { + return Err(AlloyL2ChainProviderError::L2BlockInfoConstruction(number)); + } + }; + self.l2_block_info_by_number_cache.put(number, l2_block_info); + Ok(l2_block_info) + } + + async fn block_by_number(&mut self, number: u64) -> Result { + if let Some(block) = self.block_by_number_cache.get(&number) { + return Ok(block.clone()); + } + + let raw_block: TransportResult = + self.inner.raw_request("debug_getRawBlock".into(), [U64::from(number)]).await; + let raw_block: Bytes = match raw_block { + Ok(b) => b, + Err(_) => { + return Err(AlloyL2ChainProviderError::BlockNotFound(number)); + } + }; + let block = match OpBlock::decode(&mut raw_block.as_ref()) { + Ok(b) => b, + Err(_) => { + return Err(AlloyL2ChainProviderError::OpBlockDecode(number)); + } + }; + self.block_by_number_cache.put(number, block.clone()); + Ok(block) + } +} + +#[async_trait] +impl L2ChainProvider for AlloyL2ChainProvider { + type Error = AlloyL2ChainProviderError; + + async fn system_config_by_number( + &mut self, + number: u64, + rollup_config: Arc, + ) -> Result::Error> { + if let Some(system_config) = self.system_config_by_number_cache.get(&number) { + return Ok(*system_config); + } + + let block = match self.block_by_number(number).await { + Ok(e) => e, + Err(_) => { + return Err(AlloyL2ChainProviderError::BlockNotFound(number)); + } + }; + let sys_config = match to_system_config(&block, &rollup_config) { + Ok(s) => s, + Err(_) => { + return Err(AlloyL2ChainProviderError::SystemConfigConversion(number)); + } + }; + self.system_config_by_number_cache.put(number, sys_config); + Ok(sys_config) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + #[ignore] + async fn test_alloy_l2_chain_provider_latest_block_number() { + let mut provider = AlloyL2ChainProvider::new_http( + "https://docs-demo.quiknode.pro/".try_into().unwrap(), + Arc::new(RollupConfig::default()), + ); + let number = provider.latest_block_number().await.unwrap(); + assert!(number > 0); + } + + #[tokio::test] + #[ignore] + async fn test_alloy_l2_chain_provider_chain_id() { + let mut provider = AlloyL2ChainProvider::new_http( + "https://docs-demo.quiknode.pro/".try_into().unwrap(), + Arc::new(RollupConfig::default()), + ); + let chain_id = provider.chain_id().await.unwrap(); + assert_eq!(chain_id, 1); + } +} diff --git a/crates/providers-alloy/src/lib.rs b/crates/providers-alloy/src/lib.rs new file mode 100644 index 0000000..94e6369 --- /dev/null +++ b/crates/providers-alloy/src/lib.rs @@ -0,0 +1,25 @@ +#![doc = include_str!("../README.md")] +#![doc(issue_tracker_base_url = "https://github.com/anton-rs/hilo/issues/")] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +mod beacon_client; +pub use beacon_client::{ + APIConfigResponse, APIGenesisResponse, BeaconClient, OnlineBeaconClient, ReducedConfigData, + ReducedGenesisData, +}; + +mod blob_provider; +pub use blob_provider::{DurableBlobProvider, InnerBlobProvider, LayeredBlobProvider}; + +mod blobs; +pub use blobs::{ + BlobSidecarProvider, OnlineBlobProvider, OnlineBlobProviderBuilder, + OnlineBlobProviderWithFallback, +}; + +mod chain_provider; +pub use chain_provider::AlloyChainProvider; + +mod l2_chain_provider; +pub use l2_chain_provider::AlloyL2ChainProvider; diff --git a/crates/providers-local/Cargo.toml b/crates/providers-local/Cargo.toml new file mode 100644 index 0000000..4db8291 --- /dev/null +++ b/crates/providers-local/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "hilo-providers-local" +description = "Local providers for hilo" + +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +# Kona +kona-derive.workspace = true + +# Alloy +alloy-eips.workspace = true +alloy-consensus.workspace = true +alloy-primitives = { workspace = true, features = ["map"] } + +# Op Alloy +op-alloy-genesis.workspace = true +op-alloy-protocol.workspace = true +op-alloy-consensus.workspace = true + +# Reth +reth-provider.workspace = true +reth-primitives.workspace = true + +# Misc +async-trait.workspace = true +parking_lot.workspace = true +derive_more = { workspace = true, features = ["full"] } diff --git a/crates/providers-local/README.md b/crates/providers-local/README.md new file mode 100644 index 0000000..1450a19 --- /dev/null +++ b/crates/providers-local/README.md @@ -0,0 +1,3 @@ +# `hilo-providers-local` + +Local provider implementations of kona's `ChainProvider` and `L2ChainProvider` traits. diff --git a/crates/driver/src/chain_provider.rs b/crates/providers-local/src/chain_provider.rs similarity index 99% rename from crates/driver/src/chain_provider.rs rename to crates/providers-local/src/chain_provider.rs index c172808..88c4227 100644 --- a/crates/driver/src/chain_provider.rs +++ b/crates/providers-local/src/chain_provider.rs @@ -1,7 +1,7 @@ //! Chain Provider +use alloc::{boxed::Box, collections::vec_deque::VecDeque, string::ToString, sync::Arc, vec::Vec}; use alloy_primitives::{map::HashMap, B256}; -use std::{boxed::Box, collections::vec_deque::VecDeque, string::ToString, sync::Arc, vec::Vec}; use alloy_consensus::{ Header, Receipt, Signed, TxEip1559, TxEip2930, TxEip4844, TxEip4844Variant, TxEnvelope, diff --git a/crates/driver/src/l2_chain_provider.rs b/crates/providers-local/src/l2_chain_provider.rs similarity index 97% rename from crates/driver/src/l2_chain_provider.rs rename to crates/providers-local/src/l2_chain_provider.rs index d7b8e03..f984aa9 100644 --- a/crates/driver/src/l2_chain_provider.rs +++ b/crates/providers-local/src/l2_chain_provider.rs @@ -1,7 +1,7 @@ //! L2 Chain Provider +use alloc::{boxed::Box, collections::vec_deque::VecDeque, string::ToString, sync::Arc, vec::Vec}; use alloy_primitives::{map::HashMap, B256}; -use std::{boxed::Box, collections::vec_deque::VecDeque, string::ToString, sync::Arc, vec::Vec}; use alloy_consensus::{Header, Receipt, TxEnvelope}; use async_trait::async_trait; diff --git a/crates/providers-local/src/lib.rs b/crates/providers-local/src/lib.rs new file mode 100644 index 0000000..7b2b5f5 --- /dev/null +++ b/crates/providers-local/src/lib.rs @@ -0,0 +1,13 @@ +#![doc = include_str!("../README.md")] +#![doc(issue_tracker_base_url = "https://github.com/anton-rs/hilo/issues/")] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr(not(test), no_std)] + +extern crate alloc; + +mod chain_provider; +pub use chain_provider::{reth_to_alloy_tx, InMemoryChainProvider}; + +mod l2_chain_provider; +pub use l2_chain_provider::InMemoryL2ChainProvider;