diff --git a/Cargo.lock b/Cargo.lock index 38d8219c4..c3ca0ff03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -822,6 +822,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "command-fds" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb11bd1378bf3731b182997b40cefe00aba6a6cc74042c8318c1b271d3badf7" +dependencies = [ + "nix", + "thiserror", +] + [[package]] name = "const-hex" version = "1.11.3" @@ -1540,6 +1550,15 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "kona-client" +version = "0.1.0" +dependencies = [ + "cfg-if", + "kona-common", + "kona-common-proc", +] + [[package]] name = "kona-common" version = "0.0.1" @@ -1600,9 +1619,17 @@ name = "kona-host" version = "0.1.0" dependencies = [ "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-transport-http", "anyhow", "clap", + "command-fds", + "kona-common", + "kona-preimage", + "reqwest", "serde", + "tempfile", "tokio", "tracing", "tracing-subscriber", @@ -1794,6 +1821,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "libc", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index a0ce8870a..3c710d029 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = ["crates/*", "bin/host", "bin/programs/*"] -exclude = ["fpvm-tests/cannon-rs-tests", "bin/programs/optimism"] +exclude = ["fpvm-tests/cannon-rs-tests"] resolver = "2" [workspace.package] diff --git a/bin/host/Cargo.toml b/bin/host/Cargo.toml index 2edbc0a7c..f6f710ac8 100644 --- a/bin/host/Cargo.toml +++ b/bin/host/Cargo.toml @@ -14,7 +14,18 @@ anyhow.workspace = true tracing.workspace = true alloy-primitives = { workspace = true, features = ["serde"] } +# local +kona-common = { path = "../../crates/common", version = "0.0.1" } +kona-preimage = { path = "../../crates/preimage", version = "0.0.1" } + +# external +alloy-provider = { git = "https://github.com/alloy-rs/alloy", rev = "e3f2f07" } +alloy-transport-http = { git = "https://github.com/alloy-rs/alloy", rev = "e3f2f07" } +alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "e3f2f07" } +reqwest = "0.12" tokio = { version = "1.37.0", features = ["full"] } clap = { version = "4.5.4", features = ["derive", "env"] } serde = { version = "1.0.198", features = ["derive"] } tracing-subscriber = "0.3.18" +command-fds = "0.3.0" +tempfile = "3.10" diff --git a/bin/host/src/cli/mod.rs b/bin/host/src/cli/mod.rs index bc9589554..24a306dee 100644 --- a/bin/host/src/cli/mod.rs +++ b/bin/host/src/cli/mod.rs @@ -9,7 +9,7 @@ mod parser; pub(crate) use parser::parse_b256; mod types; -pub(crate) use types::{Network, RpcKind}; +pub(crate) use types::Network; mod tracing_util; pub(crate) use tracing_util::init_tracing_subscriber; @@ -31,7 +31,7 @@ pub struct HostCli { pub data_dir: Option, /// Address of L2 JSON-RPC endpoint to use (eth and debug namespace required). #[clap(long)] - pub l2_node_address: String, + pub l2_node_address: Option, /// Hash of the L1 head block. Derivation stops after this block is processed. #[clap(long, value_parser = parse_b256)] pub l1_head: B256, @@ -52,23 +52,23 @@ pub struct HostCli { pub l2_genesis_path: PathBuf, /// Address of L1 JSON-RPC endpoint to use (eth namespace required) #[clap(long)] - pub l1_node_address: String, + pub l1_node_address: Option, /// Address of the L1 Beacon API endpoint to use. #[clap(long)] - pub l1_beacon_address: String, - /// Trust the L1 RPC, sync faster at risk of malicious/buggy RPC providing bad or inconsistent - /// L1 data - #[clap(long)] - pub l1_trust_rpc: bool, - /// The kind of RPC provider, used to inform optimal transactions receipts fetching, and thus - /// reduce costs. - #[clap(long)] - pub l1_rpc_provider_kind: RpcKind, + pub l1_beacon_address: Option, /// Run the specified client program as a separate process detached from the host. Default is /// to run the client program in the host process. #[clap(long)] pub exec: String, - /// Run in pre-image server mode without executing any client program. + /// Run in pre-image server mode without executing any client program. Defaults to `false`. #[clap(long)] pub server: bool, } + +impl HostCli { + pub fn is_offline(&self) -> bool { + self.l1_node_address.is_none() || + self.l2_node_address.is_none() || + self.l1_beacon_address.is_none() + } +} diff --git a/bin/host/src/cli/types.rs b/bin/host/src/cli/types.rs index f48efb366..aa3b292e6 100644 --- a/bin/host/src/cli/types.rs +++ b/bin/host/src/cli/types.rs @@ -6,11 +6,6 @@ use serde::Serialize; pub enum Network { /// Optimism Mainnet Optimism, -} - -/// Available RPC provider types. -#[derive(Debug, Clone, ValueEnum, Serialize)] -pub enum RpcKind { - /// debug alloy provider - DebugRpc, + /// Optimism Sepolia + OptimismSepolia, } diff --git a/bin/host/src/fetcher/hint.rs b/bin/host/src/fetcher/hint.rs new file mode 100644 index 000000000..54990c4a9 --- /dev/null +++ b/bin/host/src/fetcher/hint.rs @@ -0,0 +1,72 @@ +//! This module contains the [HintType] enum. + +use std::fmt::Display; + +/// The [HintType] enum is used to specify the type of hint that was received. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum HintType { + /// A hint that specifies the block header of a layer 1 block. + L1BlockHeader, + /// A hint that specifies the transactions of a layer 1 block. + L1Transactions, + /// A hint that specifies the state node of a layer 1 block. + L1Receipts, + /// A hint that specifies a blob in the layer 1 beacon chain. + L1Blob, + /// A hint that specifies a precompile call on layer 1. + L1Precompile, + /// A hint that specifies the block header of a layer 2 block. + L2BlockHeader, + /// A hint that specifies the transactions of a layer 2 block. + L2Transactions, + /// A hint that specifies the state node in the L2 state trie. + L2StateNode, + /// A hint that specifies the code of a contract on layer 2. + L2Code, + /// A hint that specifies the output root of a block on layer 2. + L2Output, +} + +impl TryFrom<&str> for HintType { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + match value { + "l1-block-header" => Ok(HintType::L1BlockHeader), + "l1-transactions" => Ok(HintType::L1Transactions), + "l1-receipts" => Ok(HintType::L1Receipts), + "l1-blob" => Ok(HintType::L1Blob), + "l1-precompile" => Ok(HintType::L1Precompile), + "l2-block-header" => Ok(HintType::L2BlockHeader), + "l2-transactions" => Ok(HintType::L2Transactions), + "l2-state-node" => Ok(HintType::L2StateNode), + "l2-code" => Ok(HintType::L2Code), + "l2-output" => Ok(HintType::L2Output), + _ => anyhow::bail!("Invalid hint type: {value}"), + } + } +} + +impl From for &str { + fn from(value: HintType) -> Self { + match value { + HintType::L1BlockHeader => "l1-block-header", + HintType::L1Transactions => "l1-transactions", + HintType::L1Receipts => "l1-receipts", + HintType::L1Blob => "l1-blob", + HintType::L1Precompile => "l1-precompile", + HintType::L2BlockHeader => "l2-block-header", + HintType::L2Transactions => "l2-transactions", + HintType::L2StateNode => "l2-state-node", + HintType::L2Code => "l2-code", + HintType::L2Output => "l2-output", + } + } +} + +impl Display for HintType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s: &str = (*self).into(); + write!(f, "{}", s) + } +} diff --git a/bin/host/src/fetcher/mod.rs b/bin/host/src/fetcher/mod.rs new file mode 100644 index 000000000..3449708f2 --- /dev/null +++ b/bin/host/src/fetcher/mod.rs @@ -0,0 +1,119 @@ +//! This module contains the [Fetcher] struct, which is responsible for fetching preimages from a +//! remote source. + +use crate::{kv::KeyValueStore, util}; +use alloy_primitives::{Bytes, B256}; +use alloy_provider::{Provider, ReqwestProvider}; +use anyhow::{anyhow, Result}; +use kona_preimage::{PreimageKey, PreimageKeyType}; +use std::sync::Arc; +use tokio::sync::RwLock; +use tracing::debug; + +mod hint; +pub use hint::HintType; + +/// The [Fetcher] struct is responsible for fetching preimages from a remote source. +pub struct Fetcher +where + KV: KeyValueStore, +{ + /// Key-value store for preimages. + kv_store: Arc>, + /// L1 chain provider. + l1_provider: ReqwestProvider, + /// L2 chain provider. + /// TODO: OP provider, N = Optimism + #[allow(unused)] + l2_provider: ReqwestProvider, + /// The last hint that was received. [None] if no hint has been received yet. + last_hint: Option, +} + +impl Fetcher +where + KV: KeyValueStore, +{ + /// Create a new [Fetcher] with the given [KeyValueStore]. + pub fn new( + kv_store: Arc>, + l1_provider: ReqwestProvider, + l2_provider: ReqwestProvider, + ) -> Self { + Self { kv_store, l1_provider, l2_provider, last_hint: None } + } + + /// Set the last hint to be received. + pub fn hint(&mut self, hint: &str) { + debug!(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> { + debug!(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).cloned(); + + // Drop the read lock before beginning the 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"); + self.prefetch(hint).await?; + + let kv_lock = self.kv_store.read().await; + preimage = kv_lock.get(key).cloned(); + } + + preimage.ok_or_else(|| anyhow!("Preimage not found.")) + } + + /// Fetch the preimage for the given hint and insert it into the key-value store. + async fn prefetch(&self, hint: &str) -> Result<()> { + let (hint_type, hint_data) = util::parse_hint(hint)?; + debug!(target: "fetcher", "Fetching hint: {hint_type} {hint_data}"); + + match hint_type { + HintType::L1BlockHeader => { + // Validate the hint data length. + if hint_data.len() != 32 { + anyhow::bail!("Invalid hint data length: {}", hint_data.len()); + } + + // Fetch the raw header from the L1 chain provider. + let hash: B256 = hint_data + .as_ref() + .try_into() + .map_err(|e| anyhow!("Failed to convert bytes to B256: {e}"))?; + let raw_header: Bytes = self + .l1_provider + .client() + .request("debug_getRawHeader", [hash]) + .await + .map_err(|e| anyhow!(e))?; + + // Acquire a lock on the key-value store and set the preimage. + let mut kv_lock = self.kv_store.write().await; + kv_lock.set( + PreimageKey::new(*hash, PreimageKeyType::Keccak256).into(), + raw_header.into(), + ); + } + HintType::L1Transactions => todo!(), + HintType::L1Receipts => todo!(), + HintType::L1Blob => todo!(), + HintType::L1Precompile => todo!(), + HintType::L2BlockHeader => todo!(), + HintType::L2Transactions => todo!(), + HintType::L2StateNode => todo!(), + HintType::L2Code => todo!(), + HintType::L2Output => todo!(), + } + + Ok(()) + } +} diff --git a/bin/host/src/kv/mem.rs b/bin/host/src/kv/mem.rs new file mode 100644 index 000000000..d7f2a30b7 --- /dev/null +++ b/bin/host/src/kv/mem.rs @@ -0,0 +1,30 @@ +//! Contains a concrete implementation of the [KeyValueStore] trait that stores data in memory. + +use alloy_primitives::B256; + +use super::KeyValueStore; +use std::collections::HashMap; + +/// A simple, synchronous key-value store that stores data in memory. This is useful for testing and +/// development purposes. +#[derive(Default, Clone, Debug, Eq, PartialEq)] +pub struct MemoryKeyValueStore { + store: HashMap>, +} + +impl MemoryKeyValueStore { + /// Create a new [MemoryKeyValueStore] with an empty store. + pub fn new() -> Self { + Self { store: HashMap::new() } + } +} + +impl KeyValueStore for MemoryKeyValueStore { + fn get(&self, key: B256) -> Option<&Vec> { + self.store.get(&key) + } + + fn set(&mut self, key: B256, value: Vec) { + self.store.insert(key, value); + } +} diff --git a/bin/host/src/kv/mod.rs b/bin/host/src/kv/mod.rs new file mode 100644 index 000000000..babec0ee6 --- /dev/null +++ b/bin/host/src/kv/mod.rs @@ -0,0 +1,15 @@ +//! This module contains the [KeyValueStore] trait and concrete implementations of it. + +use alloy_primitives::B256; + +mod mem; +pub use mem::MemoryKeyValueStore; + +/// Describes the interface of a simple, synchronous key-value store. +pub trait KeyValueStore { + /// Get the value associated with the given key. + fn get(&self, key: B256) -> Option<&Vec>; + + /// Set the value associated with the given key. + fn set(&mut self, key: B256, value: Vec); +} diff --git a/bin/host/src/main.rs b/bin/host/src/main.rs index 39adb427e..2fc587101 100644 --- a/bin/host/src/main.rs +++ b/bin/host/src/main.rs @@ -1,13 +1,127 @@ -use crate::cli::{init_tracing_subscriber, HostCli}; -use anyhow::Result; +use crate::{ + cli::{init_tracing_subscriber, HostCli}, + kv::MemoryKeyValueStore, + server::PreimageServer, +}; +use anyhow::{anyhow, Result}; use clap::Parser; +use command_fds::{CommandFdExt, FdMapping}; +use fetcher::Fetcher; +use kona_common::FileDescriptor; +use kona_preimage::{HintReader, OracleServer, PipeHandle}; +use std::{ + io::{stderr, stdin, stdout}, + os::fd::AsFd, + process::Command, + sync::Arc, +}; +use tokio::sync::RwLock; +use tracing::{error, info}; mod cli; +mod fetcher; +mod kv; +mod server; +mod util; #[tokio::main] async fn main() -> Result<()> { - let HostCli { v: tracing_verbosity, .. } = HostCli::parse(); - let _ = init_tracing_subscriber(tracing_verbosity); - tracing::info!("host telemetry initialized"); + let cfg = HostCli::parse(); + init_tracing_subscriber(cfg.v)?; + + if cfg.server { + start_server(cfg).await?; + } else { + start_server_and_native_client(cfg).await?; + } + + info!("Exiting host program."); + Ok(()) +} + +/// 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. +async fn start_server(cfg: HostCli) -> Result<()> { + let (preimage_pipe, hint_pipe) = ( + PipeHandle::new(FileDescriptor::PreimageRead, FileDescriptor::PreimageWrite), + PipeHandle::new(FileDescriptor::HintRead, FileDescriptor::HintWrite), + ); + let oracle_server = OracleServer::new(preimage_pipe); + let hint_reader = HintReader::new(hint_pipe); + + // TODO: Optional disk store if `cli.data_dir` is set. + let mem_kv_store = Arc::new(RwLock::new(MemoryKeyValueStore::new())); + + let fetcher = (!cfg.is_offline()).then(|| { + let l1_provider = util::http_provider(&cfg.l1_node_address.expect("Provider must be set")); + let l2_provider = util::http_provider(&cfg.l2_node_address.expect("Provider must be set")); + Arc::new(RwLock::new(Fetcher::new(mem_kv_store.clone(), l1_provider, l2_provider))) + }); + + // Start the server and wait for it to complete. + info!("Starting preimage server."); + let server = PreimageServer::new(oracle_server, hint_reader, mem_kv_store, fetcher); + server.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. +async fn start_server_and_native_client(cfg: HostCli) -> Result<()> { + let (preimage_pipe, hint_pipe, mut files) = util::create_native_pipes()?; + let oracle_server = OracleServer::new(preimage_pipe); + let hint_reader = HintReader::new(hint_pipe); + + // TODO: Optional disk store if `cli.data_dir` is set. + let mem_kv_store = Arc::new(RwLock::new(MemoryKeyValueStore::new())); + + let fetcher = (!cfg.is_offline()).then(|| { + let l1_provider = util::http_provider(&cfg.l1_node_address.expect("Provider must be set")); + let l2_provider = util::http_provider(&cfg.l2_node_address.expect("Provider must be set")); + Arc::new(RwLock::new(Fetcher::new(mem_kv_store.clone(), l1_provider, l2_provider))) + }); + + // Create the server and start it. + let server = PreimageServer::new(oracle_server, hint_reader, mem_kv_store, fetcher); + let server_task = tokio::task::spawn(server.start()); + + // Start the client program in a separate child process. + let program_task = tokio::task::spawn(async move { + let mut command = Command::new(cfg.exec); + + // Map the file descriptors to the standard streams and the preimage oracle and hint + // reader's special file descriptors. + command + .fd_mappings(vec![ + FdMapping { parent_fd: stdin().as_fd().try_clone_to_owned().unwrap(), child_fd: 0 }, + FdMapping { + parent_fd: stdout().as_fd().try_clone_to_owned().unwrap(), + child_fd: 1, + }, + FdMapping { + parent_fd: stderr().as_fd().try_clone_to_owned().unwrap(), + child_fd: 2, + }, + FdMapping { parent_fd: files.remove(3).into(), child_fd: 3 }, + FdMapping { parent_fd: files.remove(2).into(), child_fd: 4 }, + FdMapping { parent_fd: files.remove(1).into(), child_fd: 5 }, + FdMapping { parent_fd: files.remove(0).into(), child_fd: 6 }, + ]) + .expect("No errors may occur when mapping file descriptors."); + + Ok(command.status().map_err(|e| anyhow!(e))?.success()) + }); + + info!("Starting preimage server and client program."); + let (server_res, program_res) = + tokio::try_join!(server_task, program_task).map_err(|e| anyhow!(e))?; + server_res?; + if !program_res? { + error!("Client program exited with a non-zero status."); + } + info!("Preimage server and client program have exited."); + Ok(()) } diff --git a/bin/host/src/server.rs b/bin/host/src/server.rs new file mode 100644 index 000000000..d93be8832 --- /dev/null +++ b/bin/host/src/server.rs @@ -0,0 +1,127 @@ +//! This module contains the [PreimageServer] struct and its implementation. + +use crate::{fetcher::Fetcher, kv::KeyValueStore}; +use anyhow::{anyhow, Result}; +use kona_preimage::{HintReaderServer, PreimageKey, PreimageOracleServer}; +use std::{future::Future, pin::Pin, sync::Arc}; +use tokio::sync::RwLock; +use tracing::debug; + +/// The [PreimageServer] is responsible for waiting for incoming preimage requests and +/// serving them to the client. +pub struct PreimageServer +where + P: PreimageOracleServer, + H: HintReaderServer, + KV: KeyValueStore, +{ + /// The oracle server. + oracle_server: P, + /// The hint router. + hint_reader: H, + /// Key-value store for preimages. + 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>>>, +} + +impl PreimageServer +where + P: PreimageOracleServer + Send + Sync + 'static, + H: HintReaderServer + Send + Sync + 'static, + KV: KeyValueStore + Send + Sync + 'static, +{ + /// Create a new [PreimageServer] with the given [PreimageOracleServer], + /// [HintReaderServer], and [KeyValueStore]. Holds onto the file descriptors for the pipes + /// that are created, so that the pipes are not closed until the server is dropped. + pub fn new( + oracle_server: P, + hint_reader: H, + kv_store: Arc>, + fetcher: Option>>>, + ) -> Self { + Self { oracle_server, hint_reader, kv_store, fetcher } + } + + /// Starts the [PreimageServer] and waits for incoming requests. + pub async fn start(self) -> Result<()> { + // Create the futures for the oracle server and hint router. + let server_fut = + Self::start_oracle_server(self.kv_store, self.fetcher.clone(), self.oracle_server); + let hinter_fut = Self::start_hint_router(self.hint_reader, self.fetcher); + + // Spawn tasks for the futures and wait for them to complete. + let server = tokio::task::spawn(server_fut); + let hint_router = tokio::task::spawn(hinter_fut); + tokio::try_join!(server, hint_router).map_err(|e| anyhow!(e))?; + + Ok(()) + } + + /// Starts the oracle server, which waits for incoming preimage requests and serves them to the + /// client. + async fn start_oracle_server( + kv_store: Arc>, + fetcher: Option>>>, + oracle_server: P, + ) { + #[allow(clippy::type_complexity)] + let get_preimage = + |key: PreimageKey| -> Pin>> + Send>> { + if let Some(fetcher) = fetcher.as_ref() { + // If a fetcher is present, use it to fetch the preimage. + Box::pin(async move { + let fetcher = fetcher.read().await; + fetcher.get_preimage(key.into()).await + }) + } else { + // Otherwise, use the key-value store to fetch the preimage when in offline + // mode. + let lock = Arc::clone(&kv_store); + Box::pin(async move { + lock.read() + .await + .get(key.into()) + .ok_or_else(|| anyhow!("Preimage not found")) + .cloned() + }) + } + }; + + loop { + // TODO: More granular error handling. Some errors here are expected, such as the client + // closing the pipe, while others are not and should throw. + if oracle_server.next_preimage_request(get_preimage).await.is_err() { + break; + } + } + } + + /// 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>>>) { + let route_hint = |hint: String| -> Pin> + Send>> { + if let Some(fetcher) = fetcher.as_ref() { + let fetcher = Arc::clone(fetcher); + Box::pin(async move { + fetcher.write().await.hint(&hint); + Ok(()) + }) + } else { + Box::pin(async move { + debug!(target: "preimage_server", "Received hint in offline mode: {}", &hint); + Ok(()) + }) + } + }; + + loop { + // TODO: More granular error handling. Some errors here are expected, such as the client + // closing the pipe, while others are not and should throw. + if hint_reader.next_hint(route_hint).await.is_err() { + break; + } + } + } +} diff --git a/bin/host/src/util.rs b/bin/host/src/util.rs new file mode 100644 index 000000000..9b2459a4c --- /dev/null +++ b/bin/host/src/util.rs @@ -0,0 +1,69 @@ +//! Contains utility functions and helpers for the host program. + +use crate::fetcher::HintType; +use alloy_primitives::{hex, Bytes}; +use alloy_provider::ReqwestProvider; +use alloy_rpc_client::RpcClient; +use alloy_transport_http::Http; +use anyhow::{anyhow, Result}; +use kona_common::FileDescriptor; +use kona_preimage::PipeHandle; +use reqwest::Client; +use std::{fs::File, os::fd::AsRawFd}; +use tempfile::tempfile; + +/// Parses a hint from a string. +/// +/// Hints are of the format ` `, where `` is a string that +/// represents the type of hint, and `` is the data associated with the hint +/// (bytes encoded as hex UTF-8). +pub(crate) fn parse_hint(s: &str) -> Result<(HintType, Bytes)> { + let mut parts = s.split(' ').collect::>(); + + if parts.len() != 2 { + anyhow::bail!("Invalid hint format: {}", s); + } + + let hint_type = HintType::try_from(parts.remove(0))?; + let hint_data = hex::decode(parts.remove(0)).map_err(|e| anyhow!(e))?.into(); + + Ok((hint_type, hint_data)) +} + +/// Creates two temporary files that are connected by a pipe. +pub(crate) fn create_temp_files() -> Result<(File, File)> { + let (read, write) = (tempfile().map_err(|e| anyhow!(e))?, tempfile().map_err(|e| anyhow!(e))?); + Ok((read, write)) +} + +/// Create a pair of pipes for the preimage oracle and hint reader. Also returns the files that are +/// used to create the pipes, which must be kept alive until the pipes are closed. +pub(crate) fn create_native_pipes() -> Result<(PipeHandle, PipeHandle, Vec)> { + let (po_reader, po_writer) = create_temp_files()?; + let (hint_reader, hint_writer) = create_temp_files()?; + let preimage_pipe = PipeHandle::new( + FileDescriptor::Wildcard( + po_reader.as_raw_fd().try_into().map_err(|e| anyhow!("Failed to get raw FD: {e}"))?, + ), + FileDescriptor::Wildcard( + po_writer.as_raw_fd().try_into().map_err(|e| anyhow!("Failed to get raw FD: {e}"))?, + ), + ); + let hint_pipe = PipeHandle::new( + FileDescriptor::Wildcard( + hint_reader.as_raw_fd().try_into().map_err(|e| anyhow!("Failed to get raw FD: {e}"))?, + ), + FileDescriptor::Wildcard( + hint_writer.as_raw_fd().try_into().map_err(|e| anyhow!("Failed to get raw FD: {e}"))?, + ), + ); + + Ok((preimage_pipe, hint_pipe, vec![po_reader, po_writer, hint_reader, hint_writer])) +} + +/// Returns an HTTP provider for the given URL. +pub(crate) fn http_provider(url: &str) -> ReqwestProvider { + let url = url.parse().unwrap(); + let http = Http::::new(url); + ReqwestProvider::new(RpcClient::new(http, true)) +} diff --git a/bin/programs/optimism/.gitkeep b/bin/programs/client/.gitkeep similarity index 100% rename from bin/programs/optimism/.gitkeep rename to bin/programs/client/.gitkeep diff --git a/bin/programs/client/Cargo.toml b/bin/programs/client/Cargo.toml new file mode 100644 index 000000000..f52d1606a --- /dev/null +++ b/bin/programs/client/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "kona-client" +version = "0.1.0" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[dependencies] +cfg-if.workspace = true +kona-common = { path = "../../../crates/common" } +kona-common-proc = { path = "../../../crates/common-proc" } diff --git a/bin/programs/client/README.md b/bin/programs/client/README.md new file mode 100644 index 000000000..96377ce99 --- /dev/null +++ b/bin/programs/client/README.md @@ -0,0 +1,3 @@ +# `kona-client` + +This binary contains the client program for executing the Optimism rollup state transition. diff --git a/bin/programs/client/src/main.rs b/bin/programs/client/src/main.rs new file mode 100644 index 000000000..015eaa605 --- /dev/null +++ b/bin/programs/client/src/main.rs @@ -0,0 +1,12 @@ +#![no_std] +#![cfg_attr(any(target_arch = "mips", target_arch = "riscv64"), no_main)] + +use kona_common::io; +use kona_common_proc::client_entry; + +extern crate alloc; + +#[client_entry(0x77359400)] +fn main() { + io::print("Hello, world!\n"); +} diff --git a/bin/programs/optimism/README.md b/bin/programs/optimism/README.md deleted file mode 100644 index b02ed7f3c..000000000 --- a/bin/programs/optimism/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# `kona-optimism` - -This binary contains the client program for executing the Optimism rollup state transition. - -## Modes - -The `kona-optimism` program supports several different modes, each with a separate purpose: - -| Name | Description | -| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `online` | Directly fetches external data from trusted providers. To be invoked without the `host` program on native hardware. | -| `fault` | Fetches in external data over the wire through the [`PreimageOracle` ABI][preimage-oracle-abi], supported by the `kona-host` program. Can run on native hardware or one of the supported [Fault Proof VM][fpvm] soft-CPUs. | - -[preimage-oracle-abi]: https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-oracle -[fpvm]: https://static.optimism.io/kona/fpp-dev/targets.html diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index 1c7952d68..6a5e43157 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -17,7 +17,7 @@ pub enum FileDescriptor { PreimageRead, /// Write-only. Used to request pre-images. PreimageWrite, - /// Other file descriptor, usually used for testing purposes. + /// Other file descriptor. Wildcard(usize), } diff --git a/crates/preimage/src/hint.rs b/crates/preimage/src/hint.rs index 35a2f07b8..49f871588 100644 --- a/crates/preimage/src/hint.rs +++ b/crates/preimage/src/hint.rs @@ -61,7 +61,7 @@ impl HintReader { #[async_trait::async_trait] impl HintReaderServer for HintReader { - async fn next_hint(&mut self, mut route_hint: F) -> Result<()> + async fn next_hint(&self, mut route_hint: F) -> Result<()> where F: FnMut(String) -> Fut + Send, Fut: Future> + Send, @@ -141,7 +141,7 @@ mod test { const MOCK_DATA: &str = "test-hint 0xfacade"; let sys = client_and_host(); - let (hint_writer, mut hint_reader) = (sys.hint_writer, sys.hint_reader); + let (hint_writer, hint_reader) = (sys.hint_writer, sys.hint_reader); let incoming_hints = Arc::new(Mutex::new(Vec::new())); let client = tokio::task::spawn(async move { hint_writer.write(MOCK_DATA) }); diff --git a/crates/preimage/src/key.rs b/crates/preimage/src/key.rs index c6ebe0d72..b0a05b4d8 100644 --- a/crates/preimage/src/key.rs +++ b/crates/preimage/src/key.rs @@ -91,6 +91,13 @@ impl From for [u8; 32] { } } +impl From for B256 { + fn from(value: PreimageKey) -> Self { + let raw: [u8; 32] = value.into(); + B256::from(raw) + } +} + impl TryFrom<[u8; 32]> for PreimageKey { type Error = anyhow::Error; diff --git a/crates/preimage/src/oracle.rs b/crates/preimage/src/oracle.rs index 2fa6eb3c4..87494d231 100644 --- a/crates/preimage/src/oracle.rs +++ b/crates/preimage/src/oracle.rs @@ -1,5 +1,5 @@ use crate::{PipeHandle, PreimageKey, PreimageOracleClient, PreimageOracleServer}; -use alloc::{boxed::Box, sync::Arc, vec::Vec}; +use alloc::{boxed::Box, vec::Vec}; use anyhow::{bail, Result}; use core::future::Future; use tracing::debug; @@ -88,10 +88,10 @@ impl OracleServer { #[async_trait::async_trait] impl PreimageOracleServer for OracleServer { - async fn next_preimage_request(&mut self, mut get_preimage: F) -> Result<()> + async fn next_preimage_request(&self, mut get_preimage: F) -> Result<()> where F: FnMut(PreimageKey) -> Fut + Send, - Fut: Future>>> + Send, + Fut: Future>> + Send, { // Read the preimage request from the client, and throw early if there isn't is any. let mut buf = [0u8; 32]; @@ -123,6 +123,7 @@ mod test { use super::*; use crate::PreimageKeyType; + use alloc::sync::Arc; use alloy_primitives::keccak256; use core::pin::Pin; use kona_common::FileDescriptor; @@ -174,13 +175,13 @@ mod test { let preimages = { let mut preimages = HashMap::new(); - preimages.insert(key_a, Arc::new(MOCK_DATA_A.to_vec())); - preimages.insert(key_b, Arc::new(MOCK_DATA_B.to_vec())); + preimages.insert(key_a, MOCK_DATA_A.to_vec()); + preimages.insert(key_b, MOCK_DATA_B.to_vec()); Arc::new(Mutex::new(preimages)) }; let sys = client_and_host(); - let (oracle_reader, mut oracle_server) = (sys.oracle_reader, sys.oracle_server); + let (oracle_reader, oracle_server) = (sys.oracle_reader, sys.oracle_server); let client = tokio::task::spawn(async move { let contents_a = oracle_reader.get(key_a).unwrap(); @@ -194,20 +195,19 @@ mod test { }); let host = tokio::task::spawn(async move { #[allow(clippy::type_complexity)] - let get_preimage = move |key: PreimageKey| -> Pin< - Box>>> + Send>, - > { - let preimages = Arc::clone(&preimages); - Box::pin(async move { - // Simulate fetching preimage data - preimages - .lock() - .await - .get(&key) - .ok_or(anyhow::anyhow!("Preimage not available")) - .cloned() - }) - }; + let get_preimage = + move |key: PreimageKey| -> Pin>> + Send>> { + let preimages = Arc::clone(&preimages); + Box::pin(async move { + // Simulate fetching preimage data + preimages + .lock() + .await + .get(&key) + .ok_or(anyhow::anyhow!("Preimage not available")) + .cloned() + }) + }; loop { if oracle_server.next_preimage_request(&get_preimage).await.is_err() { diff --git a/crates/preimage/src/pipe.rs b/crates/preimage/src/pipe.rs index a6dabe208..4fa8ea8ba 100644 --- a/crates/preimage/src/pipe.rs +++ b/crates/preimage/src/pipe.rs @@ -49,4 +49,14 @@ impl PipeHandle { } Ok(written) } + + /// Returns the read handle for the pipe. + pub fn read_handle(&self) -> FileDescriptor { + self.read_handle + } + + /// Returns the write handle for the pipe. + pub fn write_handle(&self) -> FileDescriptor { + self.write_handle + } } diff --git a/crates/preimage/src/traits.rs b/crates/preimage/src/traits.rs index 88f79ef0c..bede547e2 100644 --- a/crates/preimage/src/traits.rs +++ b/crates/preimage/src/traits.rs @@ -1,5 +1,5 @@ use crate::PreimageKey; -use alloc::{boxed::Box, string::String, sync::Arc, vec::Vec}; +use alloc::{boxed::Box, string::String, vec::Vec}; use anyhow::Result; use core::future::Future; @@ -44,10 +44,10 @@ pub trait PreimageOracleServer { /// # Returns /// - `Ok(())` if the data was successfully written into the client pipe. /// - `Err(_)` if the data could not be written to the client. - async fn next_preimage_request(&mut self, get_preimage: F) -> Result<()> + async fn next_preimage_request(&self, get_preimage: F) -> Result<()> where F: FnMut(PreimageKey) -> Fut + Send, - Fut: Future>>> + Send; + Fut: Future>> + Send; } /// A [HintReaderServer] is a high-level interface to read preimage hints from the @@ -60,7 +60,7 @@ pub trait HintReaderServer { /// - `Ok(())` if the hint was received and the client was notified of the host's /// acknowledgement. /// - `Err(_)` if the hint was not received correctly. - async fn next_hint(&mut self, route_hint: F) -> Result<()> + async fn next_hint(&self, route_hint: F) -> Result<()> where F: FnMut(String) -> Fut + Send, Fut: Future> + Send;