diff --git a/.monorepo b/.monorepo index a17988b8f..288f730d7 100644 --- a/.monorepo +++ b/.monorepo @@ -1 +1 @@ -e52030dba4d6130874f80e80900961c342c8c074 +3cc36be2ef1ff1797fd85a30754551949b29dbc1 diff --git a/bin/client/justfile b/bin/client/justfile index 4964c3aa9..ea613179f 100644 --- a/bin/client/justfile +++ b/bin/client/justfile @@ -49,6 +49,7 @@ run-client-asterisc block_number l1_rpc l1_beacon_rpc l2_rpc rollup_node_rpc ver --input $STATE_PATH \ -- \ $HOST_BIN_PATH \ + single \ --l1-head $L1_HEAD \ --agreed-l2-head-hash $AGREED_L2_HEAD_HASH \ --claimed-l2-output-root $CLAIMED_L2_OUTPUT_ROOT \ @@ -96,6 +97,7 @@ run-client-native block_number l1_rpc l1_beacon_rpc l2_rpc rollup_node_rpc rollu echo "Running host program with native client program..." cargo r --bin kona-host --release -- \ + single \ --l1-head $L1_HEAD \ --agreed-l2-head-hash $AGREED_L2_HEAD_HASH \ --claimed-l2-output-root $CLAIMED_L2_OUTPUT_ROOT \ @@ -125,6 +127,7 @@ run-client-native-offline block_number l2_claim l2_output_root l2_head l1_head l echo "Running host program with native client program..." cargo r --bin kona-host --release -- \ + single \ --l1-head $L1_HEAD \ --agreed-l2-head-hash $AGREED_L2_HEAD_HASH \ --claimed-l2-output-root $CLAIMED_L2_OUTPUT_ROOT \ @@ -169,6 +172,7 @@ run-client-asterisc-offline block_number l2_claim l2_output_root l2_head l1_head --input $STATE_PATH \ -- \ $HOST_BIN_PATH \ + single \ --l1-head $L1_HEAD \ --agreed-l2-head-hash $AGREED_L2_HEAD_HASH \ --claimed-l2-output-root $CLAIMED_L2_OUTPUT_ROOT \ diff --git a/bin/host/src/cli/mod.rs b/bin/host/src/cli/mod.rs index 4acdab56b..2ee7a49cd 100644 --- a/bin/host/src/cli/mod.rs +++ b/bin/host/src/cli/mod.rs @@ -1,26 +1,11 @@ //! This module contains all CLI-specific code for the host binary. -use crate::{ - blobs::OnlineBlobProvider, - kv::{ - DiskKeyValueStore, LocalKeyValueStore, MemoryKeyValueStore, SharedKeyValueStore, - SplitKeyValueStore, - }, -}; -use alloy_primitives::B256; -use alloy_provider::ReqwestProvider; -use alloy_rpc_client::RpcClient; -use alloy_transport_http::Http; -use anyhow::{anyhow, Result}; +use crate::single::SingleChainHostCli; use clap::{ builder::styling::{AnsiColor, Color, Style}, - ArgAction, Parser, + ArgAction, Parser, Subcommand, }; -use op_alloy_genesis::RollupConfig; -use reqwest::Client; use serde::Serialize; -use std::{path::PathBuf, sync::Arc}; -use tokio::sync::RwLock; mod parser; pub(crate) use parser::parse_b256; @@ -37,164 +22,22 @@ primary thread. "; /// The host binary CLI application arguments. -#[derive(Default, Parser, Serialize, Clone, Debug)] +#[derive(Parser, Serialize, Clone, Debug)] #[command(about = ABOUT, version, styles = cli_styles())] pub struct HostCli { /// Verbosity level (0-2) #[arg(long, short, action = ArgAction::Count)] pub v: u8, - /// Hash of the L1 head block. Derivation stops after this block is processed. - #[clap(long, value_parser = parse_b256, env)] - pub l1_head: B256, - /// Hash of the agreed upon safe L2 block committed to by `--agreed-l2-output-root`. - #[clap(long, visible_alias = "l2-head", value_parser = parse_b256, env)] - pub agreed_l2_head_hash: B256, - /// Agreed safe L2 Output Root to start derivation from. - #[clap(long, visible_alias = "l2-output-root", value_parser = parse_b256, env)] - pub agreed_l2_output_root: B256, - /// Claimed L2 output root at block # `--claimed-l2-block-number` to validate. - #[clap(long, visible_alias = "l2-claim", value_parser = parse_b256, env)] - pub claimed_l2_output_root: B256, - /// Number of the L2 block that the claimed output root commits to. - #[clap(long, visible_alias = "l2-block-number", env)] - pub claimed_l2_block_number: u64, - /// Address of L2 JSON-RPC endpoint to use (eth and debug namespace required). - #[clap( - long, - visible_alias = "l2", - requires = "l1_node_address", - requires = "l1_beacon_address", - env - )] - pub l2_node_address: Option, - /// Address of L1 JSON-RPC endpoint to use (eth and debug namespace required) - #[clap( - long, - visible_alias = "l1", - requires = "l2_node_address", - requires = "l1_beacon_address", - env - )] - pub l1_node_address: Option, - /// Address of the L1 Beacon API endpoint to use. - #[clap( - long, - visible_alias = "beacon", - requires = "l1_node_address", - requires = "l2_node_address", - env - )] - pub l1_beacon_address: Option, - /// The Data Directory for preimage data storage. Optional if running in online mode, - /// required if running in offline mode. - #[clap( - long, - visible_alias = "db", - required_unless_present_all = ["l2_node_address", "l1_node_address", "l1_beacon_address"], - env - )] - pub data_dir: Option, - /// Run the client program natively. - #[clap(long, conflicts_with = "server", required_unless_present = "server")] - pub native: bool, - /// Run in pre-image server mode without executing any client program. If not provided, the - /// host will run the client program in the host process. - #[clap(long, conflicts_with = "native", required_unless_present = "native")] - pub server: bool, - /// The L2 chain ID of a supported chain. If provided, the host will look for the corresponding - /// rollup config in the superchain registry. - #[clap( - long, - conflicts_with = "rollup_config_path", - required_unless_present = "rollup_config_path", - env - )] - pub l2_chain_id: Option, - /// Path to rollup config. If provided, the host will use this config instead of attempting to - /// look up the config in the superchain registry. - #[clap( - long, - alias = "rollup-cfg", - conflicts_with = "l2_chain_id", - required_unless_present = "l2_chain_id", - env - )] - pub rollup_config_path: Option, + /// Host mode + #[clap(subcommand)] + pub mode: HostMode, } -impl HostCli { - /// Returns `true` if the host is running in offline mode. - pub const fn is_offline(&self) -> bool { - self.l1_node_address.is_none() && - self.l2_node_address.is_none() && - self.l1_beacon_address.is_none() - } - - /// Returns an HTTP provider for the given URL. - fn http_provider(url: &str) -> ReqwestProvider { - let url = url.parse().unwrap(); - let http = Http::::new(url); - ReqwestProvider::new(RpcClient::new(http, true)) - } - - /// Creates the providers associated with the [HostCli] configuration. - /// - /// ## Returns - /// - A [ReqwestProvider] for the L1 node. - /// - An [OnlineBlobProvider] for the L1 beacon node. - /// - A [ReqwestProvider] for the L2 node. - pub async fn create_providers( - &self, - ) -> Result<(ReqwestProvider, OnlineBlobProvider, ReqwestProvider)> { - let blob_provider = OnlineBlobProvider::new_http( - self.l1_beacon_address.clone().ok_or(anyhow!("Beacon API URL must be set"))?, - ) - .await - .map_err(|e| anyhow!("Failed to load blob provider configuration: {e}"))?; - let l1_provider = Self::http_provider( - self.l1_node_address.as_ref().ok_or(anyhow!("Provider must be set"))?, - ); - let l2_provider = Self::http_provider( - self.l2_node_address.as_ref().ok_or(anyhow!("L2 node address must be set"))?, - ); - - Ok((l1_provider, blob_provider, l2_provider)) - } - - /// Parses the CLI arguments and returns a new instance of a [SharedKeyValueStore], as it is - /// configured to be created. - pub fn construct_kv_store(&self) -> SharedKeyValueStore { - let local_kv_store = LocalKeyValueStore::new(self.clone()); - - let kv_store: SharedKeyValueStore = if let Some(ref data_dir) = self.data_dir { - let disk_kv_store = DiskKeyValueStore::new(data_dir.clone()); - let split_kv_store = SplitKeyValueStore::new(local_kv_store, disk_kv_store); - Arc::new(RwLock::new(split_kv_store)) - } else { - let mem_kv_store = MemoryKeyValueStore::new(); - let split_kv_store = SplitKeyValueStore::new(local_kv_store, mem_kv_store); - Arc::new(RwLock::new(split_kv_store)) - }; - - kv_store - } - - /// Reads the [RollupConfig] from the file system and returns it as a string. - pub fn read_rollup_config(&self) -> Result { - let path = self.rollup_config_path.as_ref().ok_or_else(|| { - anyhow::anyhow!( - "No rollup config path provided. Please provide a path to the rollup config." - ) - })?; - - // Read the serialized config from the file system. - let ser_config = std::fs::read_to_string(path) - .map_err(|e| anyhow!("Error reading RollupConfig file: {e}"))?; - - // Deserialize the config and return it. - serde_json::from_str(&ser_config) - .map_err(|e| anyhow!("Error deserializing RollupConfig: {e}")) - } +/// Operation modes for the host binary. +#[derive(Subcommand, Serialize, Clone, Debug)] +pub enum HostMode { + /// Run the host in single-chain mode. + Single(SingleChainHostCli), } /// Styles for the CLI application. @@ -208,69 +51,3 @@ const fn cli_styles() -> clap::builder::Styles { .valid(Style::new().bold().underline().fg_color(Some(Color::Ansi(AnsiColor::Green)))) .placeholder(Style::new().fg_color(Some(Color::Ansi(AnsiColor::White)))) } - -#[cfg(test)] -mod test { - use crate::HostCli; - use alloy_primitives::B256; - use clap::Parser; - - #[test] - fn test_flags() { - let zero_hash_str = &B256::ZERO.to_string(); - let default_flags = [ - "host", - "--l1-head", - zero_hash_str, - "--l2-head", - zero_hash_str, - "--l2-output-root", - zero_hash_str, - "--l2-claim", - zero_hash_str, - "--l2-block-number", - "0", - ]; - - let cases = [ - // valid - (["--server", "--l2-chain-id", "0", "--data-dir", "dummy"].as_slice(), true), - (["--server", "--rollup-config-path", "dummy", "--data-dir", "dummy"].as_slice(), true), - (["--native", "--l2-chain-id", "0", "--data-dir", "dummy"].as_slice(), true), - (["--native", "--rollup-config-path", "dummy", "--data-dir", "dummy"].as_slice(), true), - ( - [ - "--l1-node-address", - "dummy", - "--l2-node-address", - "dummy", - "--l1-beacon-address", - "dummy", - "--server", - "--l2-chain-id", - "0", - ] - .as_slice(), - true, - ), - // invalid - (["--server", "--native", "--l2-chain-id", "0"].as_slice(), false), - (["--l2-chain-id", "0", "--rollup-config-path", "dummy", "--server"].as_slice(), false), - (["--server"].as_slice(), false), - (["--native"].as_slice(), false), - (["--rollup-config-path", "dummy"].as_slice(), false), - (["--l2-chain-id", "0"].as_slice(), false), - (["--l1-node-address", "dummy", "--server", "--l2-chain-id", "0"].as_slice(), false), - (["--l2-node-address", "dummy", "--server", "--l2-chain-id", "0"].as_slice(), false), - (["--l1-beacon-address", "dummy", "--server", "--l2-chain-id", "0"].as_slice(), false), - ([].as_slice(), false), - ]; - - for (args_ext, valid) in cases.into_iter() { - let args = default_flags.iter().chain(args_ext.iter()).cloned().collect::>(); - - let parsed = HostCli::try_parse_from(args); - assert_eq!(parsed.is_ok(), valid); - } - } -} diff --git a/bin/host/src/blobs.rs b/bin/host/src/eth/blobs.rs similarity index 100% rename from bin/host/src/blobs.rs rename to bin/host/src/eth/blobs.rs diff --git a/bin/host/src/eth/mod.rs b/bin/host/src/eth/mod.rs new file mode 100644 index 000000000..ddbb40fc7 --- /dev/null +++ b/bin/host/src/eth/mod.rs @@ -0,0 +1,10 @@ +//! Ethereum utilities for the host binary. + +mod blobs; +pub use blobs::{ + APIConfigResponse, APIGenesisResponse, OnlineBlobProvider, ReducedConfigData, + ReducedGenesisData, +}; + +mod precompiles; +pub(crate) use precompiles::execute; diff --git a/bin/host/src/fetcher/precompiles.rs b/bin/host/src/eth/precompiles.rs similarity index 100% rename from bin/host/src/fetcher/precompiles.rs rename to bin/host/src/eth/precompiles.rs diff --git a/bin/host/src/fetcher.rs b/bin/host/src/fetcher.rs new file mode 100644 index 000000000..4e5498cba --- /dev/null +++ b/bin/host/src/fetcher.rs @@ -0,0 +1,11 @@ +//! Fetcher trait definition. + +use kona_preimage::{HintRouter, PreimageFetcher}; + +/// The Fetcher trait is used to define the interface for fetching data from the preimage oracle, +/// by [PreimageKey], and routing hints. +/// +/// [PreimageKey]: kona_preimage::PreimageKey +pub trait Fetcher: PreimageFetcher + HintRouter {} + +impl Fetcher for T where T: PreimageFetcher + HintRouter {} diff --git a/bin/host/src/kv/mod.rs b/bin/host/src/kv/mod.rs index 78ef8c4fa..65f41bce3 100644 --- a/bin/host/src/kv/mod.rs +++ b/bin/host/src/kv/mod.rs @@ -14,9 +14,6 @@ pub use disk::DiskKeyValueStore; mod split; pub use split::SplitKeyValueStore; -mod local; -pub use local::LocalKeyValueStore; - /// A type alias for a shared key-value store. pub type SharedKeyValueStore = Arc>; diff --git a/bin/host/src/lib.rs b/bin/host/src/lib.rs index 560e020b1..e48cd5dd3 100644 --- a/bin/host/src/lib.rs +++ b/bin/host/src/lib.rs @@ -2,121 +2,13 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #![cfg_attr(not(test), warn(unused_crate_dependencies))] -pub mod blobs; pub mod cli; pub use cli::{init_tracing_subscriber, HostCli}; +pub mod single; + +pub mod eth; pub mod fetcher; pub mod kv; pub mod preimage; pub mod server; - -use anyhow::Result; -use fetcher::Fetcher; -use kona_preimage::{ - BidirectionalChannel, HintReader, HintWriter, NativeChannel, OracleReader, OracleServer, -}; -use kona_std_fpvm::{FileChannel, FileDescriptor}; -use kv::KeyValueStore; -use server::PreimageServer; -use std::sync::Arc; -use tokio::{sync::RwLock, task}; -use tracing::info; - -/// Starts the [PreimageServer] in the primary thread. In this mode, the host program has been -/// invoked by the Fault Proof VM and the client program is running in the parent process. -pub async fn start_server(cfg: HostCli) -> Result<()> { - let (preimage_chan, hint_chan) = ( - FileChannel::new(FileDescriptor::PreimageRead, FileDescriptor::PreimageWrite), - FileChannel::new(FileDescriptor::HintRead, FileDescriptor::HintWrite), - ); - let oracle_server = OracleServer::new(preimage_chan); - let hint_reader = HintReader::new(hint_chan); - let kv_store = cfg.construct_kv_store(); - let fetcher = if !cfg.is_offline() { - let (l1_provider, blob_provider, l2_provider) = cfg.create_providers().await?; - Some(Arc::new(RwLock::new(Fetcher::new( - kv_store.clone(), - l1_provider, - blob_provider, - l2_provider, - cfg.agreed_l2_head_hash, - )))) - } else { - None - }; - - // Start the server and wait for it to complete. - info!("Starting preimage server."); - PreimageServer::new(oracle_server, hint_reader, kv_store, fetcher).start().await?; - info!("Preimage server has exited."); - - Ok(()) -} - -/// Starts the [PreimageServer] and the client program in separate threads. The client program is -/// ran natively in this mode. -/// -/// ## Takes -/// - `cfg`: The host configuration. -/// -/// ## Returns -/// - `Ok(exit_code)` if the client program exits successfully. -/// - `Err(_)` if the client program failed to execute, was killed by a signal, or the host program -/// exited first. -pub async fn start_server_and_native_client(cfg: HostCli) -> Result { - let hint_chan = BidirectionalChannel::new()?; - let preimage_chan = BidirectionalChannel::new()?; - let kv_store = cfg.construct_kv_store(); - let fetcher = if !cfg.is_offline() { - let (l1_provider, blob_provider, l2_provider) = cfg.create_providers().await?; - Some(Arc::new(RwLock::new(Fetcher::new( - kv_store.clone(), - l1_provider, - blob_provider, - l2_provider, - cfg.agreed_l2_head_hash, - )))) - } else { - None - }; - - // Create the server and start it. - let server_task = task::spawn(start_native_preimage_server( - kv_store, - fetcher, - hint_chan.host, - preimage_chan.host, - )); - - // Start the client program in a separate child process. - let program_task = task::spawn(kona_client::single::run( - OracleReader::new(preimage_chan.client), - HintWriter::new(hint_chan.client), - None, - )); - - // Execute both tasks and wait for them to complete. - info!("Starting preimage server and client program."); - let (_, client_result) = tokio::try_join!(server_task, program_task,)?; - info!(target: "kona_host", "Preimage server and client program have joined."); - - Ok(client_result.is_err() as i32) -} - -/// Starts the preimage server in a separate thread. The client program is ran natively in this -/// mode. -pub async fn start_native_preimage_server( - kv_store: Arc>, - fetcher: Option>>>, - hint_chan: NativeChannel, - preimage_chan: NativeChannel, -) -> Result<()> -where - KV: KeyValueStore + Send + Sync + ?Sized + 'static, -{ - let hint_reader = HintReader::new(hint_chan); - let oracle_server = OracleServer::new(preimage_chan); - - PreimageServer::new(oracle_server, hint_reader, kv_store, fetcher).start().await -} diff --git a/bin/host/src/main.rs b/bin/host/src/main.rs index d6992589f..caf42cb8c 100644 --- a/bin/host/src/main.rs +++ b/bin/host/src/main.rs @@ -2,27 +2,18 @@ use anyhow::Result; use clap::Parser; -use kona_host::{init_tracing_subscriber, start_server, start_server_and_native_client, HostCli}; -use tracing::{error, info}; +use kona_host::{cli::HostMode, init_tracing_subscriber, HostCli}; +use tracing::info; #[tokio::main(flavor = "multi_thread")] async fn main() -> Result<()> { let cfg = HostCli::parse(); init_tracing_subscriber(cfg.v)?; - if cfg.server { - start_server(cfg).await?; - } else { - let status = match start_server_and_native_client(cfg).await { - Ok(status) => status, - Err(e) => { - error!(target: "kona_host", "Exited with an error: {:?}", e); - panic!("{e}"); - } - }; - - // Bubble up the exit status of the client program. - std::process::exit(status as i32); + match cfg.mode { + HostMode::Single(cfg) => { + cfg.run().await?; + } } info!("Exiting host program."); diff --git a/bin/host/src/preimage.rs b/bin/host/src/preimage.rs index d184016f2..7da932326 100644 --- a/bin/host/src/preimage.rs +++ b/bin/host/src/preimage.rs @@ -11,33 +11,30 @@ use tokio::sync::RwLock; /// A [Fetcher]-backed implementation of the [PreimageFetcher] trait. #[derive(Debug)] -pub struct OnlinePreimageFetcher +pub struct OnlinePreimageFetcher where - KV: KeyValueStore + ?Sized, + F: Fetcher, { - inner: Arc>>, + inner: Arc>, } #[async_trait] -impl PreimageFetcher for OnlinePreimageFetcher +impl PreimageFetcher for OnlinePreimageFetcher where - KV: KeyValueStore + Send + Sync + ?Sized, + F: Fetcher + Send + Sync, { async fn get_preimage(&self, key: PreimageKey) -> PreimageOracleResult> { let fetcher = self.inner.read().await; - fetcher - .get_preimage(key.into()) - .await - .map_err(|e| PreimageOracleError::Other(e.to_string())) + fetcher.get_preimage(key).await.map_err(|e| PreimageOracleError::Other(e.to_string())) } } -impl OnlinePreimageFetcher +impl OnlinePreimageFetcher where - KV: KeyValueStore + ?Sized, + F: Fetcher, { /// Create a new [OnlinePreimageFetcher] from the given [Fetcher]. - pub const fn new(fetcher: Arc>>) -> Self { + pub const fn new(fetcher: Arc>) -> Self { Self { inner: fetcher } } } @@ -74,31 +71,30 @@ where /// A [Fetcher]-backed implementation of the [HintRouter] trait. #[derive(Debug)] -pub struct OnlineHintRouter +pub struct OnlineHintRouter where - KV: KeyValueStore + ?Sized, + F: Fetcher, { - inner: Arc>>, + inner: Arc>, } #[async_trait] -impl HintRouter for OnlineHintRouter +impl HintRouter for OnlineHintRouter where - KV: KeyValueStore + Send + Sync + ?Sized, + F: Fetcher + Send + Sync, { async fn route_hint(&self, hint: String) -> PreimageOracleResult<()> { - let mut fetcher = self.inner.write().await; - fetcher.hint(&hint); - Ok(()) + let fetcher = self.inner.write().await; + fetcher.route_hint(hint).await } } -impl OnlineHintRouter +impl OnlineHintRouter where - KV: KeyValueStore + ?Sized, + F: Fetcher, { /// Create a new [OnlineHintRouter] from the given [Fetcher]. - pub const fn new(fetcher: Arc>>) -> Self { + pub const fn new(fetcher: Arc>) -> Self { Self { inner: fetcher } } } diff --git a/bin/host/src/server.rs b/bin/host/src/server.rs index 5d857676b..089d2714c 100644 --- a/bin/host/src/server.rs +++ b/bin/host/src/server.rs @@ -19,11 +19,12 @@ use tracing::{error, info}; /// The [PreimageServer] is responsible for waiting for incoming preimage requests and /// serving them to the client. #[derive(Debug)] -pub struct PreimageServer +pub struct PreimageServer where P: PreimageOracleServer, H: HintReaderServer, KV: KeyValueStore + ?Sized, + F: Fetcher, { /// The oracle server. oracle_server: P, @@ -33,14 +34,15 @@ where kv_store: Arc>, /// The fetcher for fetching preimages from a remote source. If [None], the server will only /// serve preimages that are already in the key-value store. - fetcher: Option>>>, + fetcher: Option>>, } -impl PreimageServer +impl PreimageServer where P: PreimageOracleServer + Send + Sync + 'static, H: HintReaderServer + Send + Sync + 'static, KV: KeyValueStore + Send + Sync + ?Sized + 'static, + F: Fetcher + Send + Sync + 'static, { /// Create a new [PreimageServer] with the given [PreimageOracleServer], /// [HintReaderServer], and [KeyValueStore]. Holds onto the file descriptors for the pipes @@ -49,7 +51,7 @@ where oracle_server: P, hint_reader: H, kv_store: Arc>, - fetcher: Option>>>, + fetcher: Option>>, ) -> Self { Self { oracle_server, hint_reader, kv_store, fetcher } } @@ -75,7 +77,7 @@ where /// client. async fn start_oracle_server( kv_store: Arc>, - fetcher: Option>>>, + fetcher: Option>>, oracle_server: P, ) -> Result<()> { #[inline(always)] @@ -106,10 +108,7 @@ where /// Starts the hint router, which waits for incoming hints and routes them to the appropriate /// handler. - async fn start_hint_router( - hint_reader: H, - fetcher: Option>>>, - ) -> Result<()> { + async fn start_hint_router(hint_reader: H, fetcher: Option>>) -> Result<()> { #[inline(always)] async fn do_loop(router: &R, server: &H) -> Result<()> where diff --git a/bin/host/src/single/cli.rs b/bin/host/src/single/cli.rs new file mode 100644 index 000000000..a6909226e --- /dev/null +++ b/bin/host/src/single/cli.rs @@ -0,0 +1,279 @@ +//! This module contains all CLI-specific code for the host binary. + +use super::{start_server, start_server_and_native_client, LocalKeyValueStore}; +use crate::{ + cli::parse_b256, + eth::OnlineBlobProvider, + kv::{DiskKeyValueStore, MemoryKeyValueStore, SharedKeyValueStore, SplitKeyValueStore}, +}; +use alloy_primitives::B256; +use alloy_provider::ReqwestProvider; +use alloy_rpc_client::RpcClient; +use alloy_transport_http::Http; +use anyhow::{anyhow, Result}; +use clap::{ + builder::styling::{AnsiColor, Color, Style}, + Parser, +}; +use op_alloy_genesis::RollupConfig; +use reqwest::Client; +use serde::Serialize; +use std::{path::PathBuf, sync::Arc}; +use tokio::sync::RwLock; +use tracing::error; + +/// The host binary CLI application arguments. +#[derive(Default, Parser, Serialize, Clone, Debug)] +#[command(styles = cli_styles())] +pub struct SingleChainHostCli { + /// Hash of the L1 head block. Derivation stops after this block is processed. + #[clap(long, value_parser = parse_b256, env)] + pub l1_head: B256, + /// Hash of the agreed upon safe L2 block committed to by `--agreed-l2-output-root`. + #[clap(long, visible_alias = "l2-head", value_parser = parse_b256, env)] + pub agreed_l2_head_hash: B256, + /// Agreed safe L2 Output Root to start derivation from. + #[clap(long, visible_alias = "l2-output-root", value_parser = parse_b256, env)] + pub agreed_l2_output_root: B256, + /// Claimed L2 output root at block # `--claimed-l2-block-number` to validate. + #[clap(long, visible_alias = "l2-claim", value_parser = parse_b256, env)] + pub claimed_l2_output_root: B256, + /// Number of the L2 block that the claimed output root commits to. + #[clap(long, visible_alias = "l2-block-number", env)] + pub claimed_l2_block_number: u64, + /// Address of L2 JSON-RPC endpoint to use (eth and debug namespace required). + #[clap( + long, + visible_alias = "l2", + requires = "l1_node_address", + requires = "l1_beacon_address", + env + )] + pub l2_node_address: Option, + /// Address of L1 JSON-RPC endpoint to use (eth and debug namespace required) + #[clap( + long, + visible_alias = "l1", + requires = "l2_node_address", + requires = "l1_beacon_address", + env + )] + pub l1_node_address: Option, + /// Address of the L1 Beacon API endpoint to use. + #[clap( + long, + visible_alias = "beacon", + requires = "l1_node_address", + requires = "l2_node_address", + env + )] + pub l1_beacon_address: Option, + /// The Data Directory for preimage data storage. Optional if running in online mode, + /// required if running in offline mode. + #[clap( + long, + visible_alias = "db", + required_unless_present_all = ["l2_node_address", "l1_node_address", "l1_beacon_address"], + env + )] + pub data_dir: Option, + /// Run the client program natively. + #[clap(long, conflicts_with = "server", required_unless_present = "server")] + pub native: bool, + /// Run in pre-image server mode without executing any client program. If not provided, the + /// host will run the client program in the host process. + #[clap(long, conflicts_with = "native", required_unless_present = "native")] + pub server: bool, + /// The L2 chain ID of a supported chain. If provided, the host will look for the corresponding + /// rollup config in the superchain registry. + #[clap( + long, + conflicts_with = "rollup_config_path", + required_unless_present = "rollup_config_path", + env + )] + pub l2_chain_id: Option, + /// Path to rollup config. If provided, the host will use this config instead of attempting to + /// look up the config in the superchain registry. + #[clap( + long, + alias = "rollup-cfg", + conflicts_with = "l2_chain_id", + required_unless_present = "l2_chain_id", + env + )] + pub rollup_config_path: Option, +} + +impl SingleChainHostCli { + /// Runs the host binary in single-chain mode. + pub async fn run(self) -> Result<()> { + if self.server { + start_server(self).await?; + } else { + let status = match start_server_and_native_client(self).await { + Ok(status) => status, + Err(e) => { + error!(target: "kona_host", "Exited with an error: {:?}", e); + panic!("{e}"); + } + }; + + // Bubble up the exit status of the client program. + std::process::exit(status as i32); + } + + Ok(()) + } + + /// Returns `true` if the host is running in offline mode. + pub const fn is_offline(&self) -> bool { + self.l1_node_address.is_none() && + self.l2_node_address.is_none() && + self.l1_beacon_address.is_none() + } + + /// Returns an HTTP provider for the given URL. + fn http_provider(url: &str) -> ReqwestProvider { + let url = url.parse().unwrap(); + let http = Http::::new(url); + ReqwestProvider::new(RpcClient::new(http, true)) + } + + /// Creates the providers associated with the [SingleChainHostCli] configuration. + /// + /// ## Returns + /// - A [ReqwestProvider] for the L1 node. + /// - An [OnlineBlobProvider] for the L1 beacon node. + /// - A [ReqwestProvider] for the L2 node. + pub async fn create_providers( + &self, + ) -> Result<(ReqwestProvider, OnlineBlobProvider, ReqwestProvider)> { + let blob_provider = OnlineBlobProvider::new_http( + self.l1_beacon_address.clone().ok_or(anyhow!("Beacon API URL must be set"))?, + ) + .await + .map_err(|e| anyhow!("Failed to load blob provider configuration: {e}"))?; + let l1_provider = Self::http_provider( + self.l1_node_address.as_ref().ok_or(anyhow!("Provider must be set"))?, + ); + let l2_provider = Self::http_provider( + self.l2_node_address.as_ref().ok_or(anyhow!("L2 node address must be set"))?, + ); + + Ok((l1_provider, blob_provider, l2_provider)) + } + + /// Parses the CLI arguments and returns a new instance of a [SharedKeyValueStore], as it is + /// configured to be created. + pub fn construct_kv_store(&self) -> SharedKeyValueStore { + let local_kv_store = LocalKeyValueStore::new(self.clone()); + + let kv_store: SharedKeyValueStore = if let Some(ref data_dir) = self.data_dir { + let disk_kv_store = DiskKeyValueStore::new(data_dir.clone()); + let split_kv_store = SplitKeyValueStore::new(local_kv_store, disk_kv_store); + Arc::new(RwLock::new(split_kv_store)) + } else { + let mem_kv_store = MemoryKeyValueStore::new(); + let split_kv_store = SplitKeyValueStore::new(local_kv_store, mem_kv_store); + Arc::new(RwLock::new(split_kv_store)) + }; + + kv_store + } + + /// Reads the [RollupConfig] from the file system and returns it as a string. + pub fn read_rollup_config(&self) -> Result { + let path = self.rollup_config_path.as_ref().ok_or_else(|| { + anyhow::anyhow!( + "No rollup config path provided. Please provide a path to the rollup config." + ) + })?; + + // Read the serialized config from the file system. + let ser_config = std::fs::read_to_string(path) + .map_err(|e| anyhow!("Error reading RollupConfig file: {e}"))?; + + // Deserialize the config and return it. + serde_json::from_str(&ser_config) + .map_err(|e| anyhow!("Error deserializing RollupConfig: {e}")) + } +} + +/// Styles for the CLI application. +const fn cli_styles() -> clap::builder::Styles { + clap::builder::Styles::styled() + .usage(Style::new().bold().underline().fg_color(Some(Color::Ansi(AnsiColor::Yellow)))) + .header(Style::new().bold().underline().fg_color(Some(Color::Ansi(AnsiColor::Yellow)))) + .literal(Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green)))) + .invalid(Style::new().bold().fg_color(Some(Color::Ansi(AnsiColor::Red)))) + .error(Style::new().bold().fg_color(Some(Color::Ansi(AnsiColor::Red)))) + .valid(Style::new().bold().underline().fg_color(Some(Color::Ansi(AnsiColor::Green)))) + .placeholder(Style::new().fg_color(Some(Color::Ansi(AnsiColor::White)))) +} + +#[cfg(test)] +mod test { + use crate::single::SingleChainHostCli; + use alloy_primitives::B256; + use clap::Parser; + + #[test] + fn test_flags() { + let zero_hash_str = &B256::ZERO.to_string(); + let default_flags = [ + "single", + "--l1-head", + zero_hash_str, + "--l2-head", + zero_hash_str, + "--l2-output-root", + zero_hash_str, + "--l2-claim", + zero_hash_str, + "--l2-block-number", + "0", + ]; + + let cases = [ + // valid + (["--server", "--l2-chain-id", "0", "--data-dir", "dummy"].as_slice(), true), + (["--server", "--rollup-config-path", "dummy", "--data-dir", "dummy"].as_slice(), true), + (["--native", "--l2-chain-id", "0", "--data-dir", "dummy"].as_slice(), true), + (["--native", "--rollup-config-path", "dummy", "--data-dir", "dummy"].as_slice(), true), + ( + [ + "--l1-node-address", + "dummy", + "--l2-node-address", + "dummy", + "--l1-beacon-address", + "dummy", + "--server", + "--l2-chain-id", + "0", + ] + .as_slice(), + true, + ), + // invalid + (["--server", "--native", "--l2-chain-id", "0"].as_slice(), false), + (["--l2-chain-id", "0", "--rollup-config-path", "dummy", "--server"].as_slice(), false), + (["--server"].as_slice(), false), + (["--native"].as_slice(), false), + (["--rollup-config-path", "dummy"].as_slice(), false), + (["--l2-chain-id", "0"].as_slice(), false), + (["--l1-node-address", "dummy", "--server", "--l2-chain-id", "0"].as_slice(), false), + (["--l2-node-address", "dummy", "--server", "--l2-chain-id", "0"].as_slice(), false), + (["--l1-beacon-address", "dummy", "--server", "--l2-chain-id", "0"].as_slice(), false), + ([].as_slice(), false), + ]; + + for (args_ext, valid) in cases.into_iter() { + let args = default_flags.iter().chain(args_ext.iter()).cloned().collect::>(); + + let parsed = SingleChainHostCli::try_parse_from(args); + assert_eq!(parsed.is_ok(), valid); + } + } +} diff --git a/bin/host/src/fetcher/mod.rs b/bin/host/src/single/fetcher.rs similarity index 90% rename from bin/host/src/fetcher/mod.rs rename to bin/host/src/single/fetcher.rs index 23294234e..98b51c88d 100644 --- a/bin/host/src/fetcher/mod.rs +++ b/bin/host/src/single/fetcher.rs @@ -1,7 +1,7 @@ -//! This module contains the [Fetcher] struct, which is responsible for fetching preimages from a -//! remote source. +//! This module contains the [SingleChainFetcher] struct, which is responsible for fetching +//! preimages from a remote source serving the single-chain proof mode. -use crate::{blobs::OnlineBlobProvider, kv::KeyValueStore}; +use crate::{eth::OnlineBlobProvider, kv::KeyValueStore}; use alloy_consensus::{Header, TxEnvelope, EMPTY_ROOT_HASH}; use alloy_eips::{ eip2718::Encodable2718, @@ -16,7 +16,10 @@ use alloy_rpc_types::{ Transaction, }; use anyhow::{anyhow, Result}; -use kona_preimage::{PreimageKey, PreimageKeyType}; +use kona_preimage::{ + errors::{PreimageOracleError, PreimageOracleResult}, + HintRouter, PreimageFetcher, PreimageKey, PreimageKeyType, +}; use kona_proof::{Hint, HintType}; use maili_protocol::BlockInfo; use op_alloy_rpc_types_engine::OpPayloadAttributes; @@ -24,11 +27,9 @@ use std::sync::Arc; use tokio::sync::RwLock; use tracing::{error, trace, warn}; -mod precompiles; - -/// The [Fetcher] struct is responsible for fetching preimages from a remote source. +/// The [SingleChainFetcher] struct is responsible for fetching preimages from a remote source. #[derive(Debug)] -pub struct Fetcher +pub struct SingleChainFetcher where KV: KeyValueStore + ?Sized, { @@ -43,56 +44,29 @@ where /// L2 head l2_head: B256, /// The last hint that was received. [None] if no hint has been received yet. - last_hint: Option, + last_hint: Arc>>, } -impl Fetcher +impl SingleChainFetcher where KV: KeyValueStore + ?Sized, { - /// Create a new [Fetcher] with the given [KeyValueStore]. - pub const fn new( + /// Create a new [SingleChainFetcher] with the given [KeyValueStore]. + pub fn new( kv_store: Arc>, l1_provider: ReqwestProvider, blob_provider: OnlineBlobProvider, l2_provider: ReqwestProvider, l2_head: B256, ) -> Self { - Self { kv_store, l1_provider, blob_provider, l2_provider, l2_head, last_hint: None } - } - - /// Set the last hint to be received. - pub fn hint(&mut self, hint: &str) { - trace!(target: "fetcher", "Received hint: {hint}"); - self.last_hint = Some(hint.to_string()); - } - - /// Get the preimage for the given key. - pub async fn get_preimage(&self, key: B256) -> Result> { - trace!(target: "fetcher", "Pre-image requested. Key: {key}"); - - // Acquire a read lock on the key-value store. - let kv_lock = self.kv_store.read().await; - let mut preimage = kv_lock.get(key); - - // Drop the read lock before beginning the retry loop. - drop(kv_lock); - - // Use a loop to keep retrying the prefetch as long as the key is not found - while preimage.is_none() && self.last_hint.is_some() { - let hint = self.last_hint.as_ref().expect("Cannot be None"); - - if let Err(e) = self.prefetch(hint).await { - error!(target: "fetcher", "Failed to prefetch hint: {e}"); - warn!(target: "fetcher", "Retrying hint fetch: {hint}"); - continue; - } - - let kv_lock = self.kv_store.read().await; - preimage = kv_lock.get(key); + Self { + kv_store, + l1_provider, + blob_provider, + l2_provider, + l2_head, + last_hint: Arc::new(RwLock::new(None)), } - - preimage.ok_or_else(|| anyhow!("Preimage not found.")) } /// Fetch the preimage for the given hint and insert it into the key-value store. @@ -252,16 +226,15 @@ where let precompile_input = hint_data[20..].to_vec(); let input_hash = keccak256(hint_data.as_ref()); - let result = precompiles::execute(precompile_address, precompile_input) - .map_or_else( - |_| vec![0u8; 1], - |raw_res| { - let mut res = Vec::with_capacity(1 + raw_res.len()); - res.push(0x01); - res.extend_from_slice(&raw_res); - res - }, - ); + let result = crate::eth::execute(precompile_address, precompile_input).map_or_else( + |_| vec![0u8; 1], + |raw_res| { + let mut res = Vec::with_capacity(1 + raw_res.len()); + res.push(0x01); + res.extend_from_slice(&raw_res); + res + }, + ); // Acquire a lock on the key-value store and set the preimages. let mut kv_lock = self.kv_store.write().await; @@ -594,3 +567,54 @@ where Ok(()) } } + +#[async_trait::async_trait] +impl HintRouter for SingleChainFetcher +where + KV: KeyValueStore + Send + Sync + ?Sized, +{ + /// Set the last hint to be received. + async fn route_hint(&self, hint: String) -> PreimageOracleResult<()> { + trace!(target: "fetcher", "Received hint: {hint}"); + let mut hint_lock = self.last_hint.write().await; + hint_lock.replace(hint); + Ok(()) + } +} + +#[async_trait::async_trait] +impl PreimageFetcher for SingleChainFetcher +where + KV: KeyValueStore + Send + Sync + ?Sized, +{ + /// Get the preimage for the given key. + async fn get_preimage(&self, key: PreimageKey) -> PreimageOracleResult> { + trace!(target: "fetcher", "Pre-image requested. Key: {key}"); + + // Acquire a read lock on the key-value store. + let kv_lock = self.kv_store.read().await; + let mut preimage = kv_lock.get(key.into()); + + // Drop the read lock before beginning the retry loop. + drop(kv_lock); + + // Use a loop to keep retrying the prefetch as long as the key is not found + while preimage.is_none() { + match self.last_hint.read().await.as_ref() { + None => continue, + Some(hint) => { + if let Err(e) = self.prefetch(hint).await { + error!(target: "fetcher", "Failed to prefetch hint: {e}"); + warn!(target: "fetcher", "Retrying hint fetch: {hint}"); + continue; + } + + let kv_lock = self.kv_store.read().await; + preimage = kv_lock.get(key.into()); + } + } + } + + preimage.ok_or(PreimageOracleError::KeyNotFound) + } +} diff --git a/bin/host/src/kv/local.rs b/bin/host/src/single/local_kv.rs similarity index 83% rename from bin/host/src/kv/local.rs rename to bin/host/src/single/local_kv.rs index ace48fbef..d1b0de05e 100644 --- a/bin/host/src/kv/local.rs +++ b/bin/host/src/single/local_kv.rs @@ -1,7 +1,8 @@ -//! Contains a concrete implementation of the [KeyValueStore] trait that stores data on disk. +//! Contains a concrete implementation of the [KeyValueStore] trait that stores data on disk, +//! using the [SingleChainHostCli] config. -use super::KeyValueStore; -use crate::cli::HostCli; +use super::SingleChainHostCli; +use crate::kv::KeyValueStore; use alloy_primitives::B256; use anyhow::Result; use kona_preimage::PreimageKey; @@ -13,15 +14,15 @@ use kona_proof::boot::{ /// The default chain ID to use if none is provided. const DEFAULT_CHAIN_ID: u64 = 0xbeefbabe; -/// A simple, synchronous key-value store that returns data from a [HostCli] config. +/// A simple, synchronous key-value store that returns data from a [SingleChainHostCli] config. #[derive(Debug)] pub struct LocalKeyValueStore { - cfg: HostCli, + cfg: SingleChainHostCli, } impl LocalKeyValueStore { - /// Create a new [LocalKeyValueStore] with the given [HostCli] config. - pub const fn new(cfg: HostCli) -> Self { + /// Create a new [LocalKeyValueStore] with the given [SingleChainHostCli] config. + pub const fn new(cfg: SingleChainHostCli) -> Self { Self { cfg } } } diff --git a/bin/host/src/single/mod.rs b/bin/host/src/single/mod.rs new file mode 100644 index 000000000..5022472ca --- /dev/null +++ b/bin/host/src/single/mod.rs @@ -0,0 +1,119 @@ +//! This module contains the single-chain mode CLI for the host binary. + +use anyhow::Result; +use kona_preimage::{ + BidirectionalChannel, HintReader, HintWriter, NativeChannel, OracleReader, OracleServer, +}; +use kona_std_fpvm::{FileChannel, FileDescriptor}; +use std::sync::Arc; +use tokio::{sync::RwLock, task}; +use tracing::info; + +mod cli; +pub use cli::SingleChainHostCli; + +mod local_kv; +pub use local_kv::LocalKeyValueStore; + +mod fetcher; +pub use fetcher::SingleChainFetcher; + +use crate::{kv::KeyValueStore, server::PreimageServer}; + +/// Starts the [PreimageServer] in the primary thread. In this mode, the host program has been +/// invoked by the Fault Proof VM and the client program is running in the parent process. +pub async fn start_server(cfg: SingleChainHostCli) -> Result<()> { + let (preimage_chan, hint_chan) = ( + FileChannel::new(FileDescriptor::PreimageRead, FileDescriptor::PreimageWrite), + FileChannel::new(FileDescriptor::HintRead, FileDescriptor::HintWrite), + ); + let oracle_server = OracleServer::new(preimage_chan); + let hint_reader = HintReader::new(hint_chan); + let kv_store = cfg.construct_kv_store(); + let fetcher = if !cfg.is_offline() { + let (l1_provider, blob_provider, l2_provider) = cfg.create_providers().await?; + Some(Arc::new(RwLock::new(SingleChainFetcher::new( + kv_store.clone(), + l1_provider, + blob_provider, + l2_provider, + cfg.agreed_l2_head_hash, + )))) + } else { + None + }; + + // Start the server and wait for it to complete. + info!("Starting preimage server."); + PreimageServer::new(oracle_server, hint_reader, kv_store, fetcher).start().await?; + info!("Preimage server has exited."); + + Ok(()) +} + +/// Starts the [PreimageServer] and the client program in separate threads. The client program is +/// ran natively in this mode. +/// +/// ## Takes +/// - `cfg`: The host configuration. +/// +/// ## Returns +/// - `Ok(exit_code)` if the client program exits successfully. +/// - `Err(_)` if the client program failed to execute, was killed by a signal, or the host program +/// exited first. +pub async fn start_server_and_native_client(cfg: SingleChainHostCli) -> Result { + let hint_chan = BidirectionalChannel::new()?; + let preimage_chan = BidirectionalChannel::new()?; + let kv_store = cfg.construct_kv_store(); + let fetcher = if !cfg.is_offline() { + let (l1_provider, blob_provider, l2_provider) = cfg.create_providers().await?; + Some(Arc::new(RwLock::new(SingleChainFetcher::new( + kv_store.clone(), + l1_provider, + blob_provider, + l2_provider, + cfg.agreed_l2_head_hash, + )))) + } else { + None + }; + + // Create the server and start it. + let server_task = task::spawn(start_native_preimage_server( + kv_store, + fetcher, + hint_chan.host, + preimage_chan.host, + )); + + // Start the client program in a separate child process. + let program_task = task::spawn(kona_client::single::run( + OracleReader::new(preimage_chan.client), + HintWriter::new(hint_chan.client), + None, + )); + + // Execute both tasks and wait for them to complete. + info!("Starting preimage server and client program."); + let (_, client_result) = tokio::try_join!(server_task, program_task,)?; + info!(target: "kona_host", "Preimage server and client program have joined."); + + Ok(client_result.is_err() as i32) +} + +/// Starts the preimage server in a separate thread. The client program is ran natively in this +/// mode. +pub async fn start_native_preimage_server( + kv_store: Arc>, + fetcher: Option>>>, + hint_chan: NativeChannel, + preimage_chan: NativeChannel, +) -> Result<()> +where + KV: KeyValueStore + Send + Sync + ?Sized + 'static, +{ + let hint_reader = HintReader::new(hint_chan); + let oracle_server = OracleServer::new(preimage_chan); + + PreimageServer::new(oracle_server, hint_reader, kv_store, fetcher).start().await +} diff --git a/justfile b/justfile index b8449b43d..2c6ab7cb5 100644 --- a/justfile +++ b/justfile @@ -30,7 +30,7 @@ action-tests test_name='Test_ProgramAction' *args='': if [ ! -d "monorepo/.devnet" ]; then echo "Building devnet allocs for the monorepo" - (cd monorepo && make devnet-allocs) + (cd monorepo/packages/contracts-bedrock && forge build) fi echo "Building host program for the native target"