diff --git a/.github/codecov.yml b/.github/codecov.yml index d9d6e086a..29e4dbf1e 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -24,6 +24,7 @@ ignore: - "**/test_util*" - "**/tests*" - "crates/proof-sdk/proof" + - "crates/providers-alloy" - "crates/mpt/src/noop.rs" # Make comments less noisy diff --git a/Cargo.lock b/Cargo.lock index 23e467941..b246ce128 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -261,10 +261,13 @@ dependencies = [ "alloy-network", "alloy-network-primitives", "alloy-primitives", + "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types-eth", "alloy-transport", "alloy-transport-http", + "alloy-transport-ipc", + "alloy-transport-ws", "async-stream", "async-trait", "auto_impl", @@ -285,6 +288,25 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "alloy-pubsub" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2269fd635f7b505f27c63a3cb293148cd02301efce4c8bdd9ff54fbfc4a20e23" +dependencies = [ + "alloy-json-rpc", + "alloy-primitives", + "alloy-transport", + "bimap", + "futures", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower", + "tracing", +] + [[package]] name = "alloy-rlp" version = "0.3.11" @@ -315,8 +337,11 @@ checksum = "d06a292b37e182e514903ede6e623b9de96420e8109ce300da288a96d88b7e4b" dependencies = [ "alloy-json-rpc", "alloy-primitives", + "alloy-pubsub", "alloy-transport", "alloy-transport-http", + "alloy-transport-ipc", + "alloy-transport-ws", "futures", "pin-project", "reqwest", @@ -544,6 +569,43 @@ dependencies = [ "url", ] +[[package]] +name = "alloy-transport-ipc" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4da44bc9a5155ab599666d26decafcf12204b72a80eeaba7c5e234ee8ac205" +dependencies = [ + "alloy-json-rpc", + "alloy-pubsub", + "alloy-transport", + "bytes", + "futures", + "interprocess", + "pin-project", + "serde_json", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "alloy-transport-ws" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58011745b2f17b334db40df9077d75b181f78360a5bc5c35519e15d4bfce15e2" +dependencies = [ + "alloy-pubsub", + "alloy-transport", + "futures", + "http", + "rustls", + "serde_json", + "tokio", + "tokio-tungstenite", + "tracing", + "ws_stream_wasm", +] + [[package]] name = "alloy-trie" version = "0.7.8" @@ -809,6 +871,17 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version 0.4.1", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -875,6 +948,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + [[package]] name = "bindgen" version = "0.69.5" @@ -1384,6 +1463,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" + [[package]] name = "debugid" version = "0.8.0" @@ -1487,6 +1572,12 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "dunce" version = "1.0.5" @@ -2250,6 +2341,21 @@ dependencies = [ "str_stack", ] +[[package]] +name = "interprocess" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "894148491d817cb36b6f778017b8ac46b17408d522dd90f539d677ea938362eb" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "ipnet" version = "2.10.1" @@ -2479,6 +2585,7 @@ dependencies = [ "kona-preimage", "kona-proof", "kona-proof-interop", + "kona-providers-alloy", "kona-std-fpvm", "maili-genesis", "maili-protocol", @@ -2600,6 +2707,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "kona-providers-alloy" +version = "0.1.0" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-provider", + "alloy-rlp", + "alloy-rpc-types-beacon", + "alloy-serde", + "alloy-transport", + "async-trait", + "kona-derive", + "lru", + "maili-genesis", + "maili-protocol", + "op-alloy-consensus", + "reqwest", + "serde", + "thiserror 2.0.11", + "tokio", +] + [[package]] name = "kona-std-fpvm" version = "0.1.2" @@ -3239,6 +3370,16 @@ dependencies = [ "ucd-trie", ] +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version 0.4.1", +] + [[package]] name = "pin-project" version = "1.1.8" @@ -3563,6 +3704,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.5.8" @@ -3942,6 +4089,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" dependencies = [ "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -4113,6 +4261,12 @@ dependencies = [ "pest", ] +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.217" @@ -4197,6 +4351,17 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.10.8" @@ -4690,6 +4855,22 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots", +] + [[package]] name = "tokio-util" version = "0.7.13" @@ -4810,6 +4991,26 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + [[package]] name = "typenum" version = "1.17.0" @@ -4875,6 +5076,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf16_iter" version = "1.0.5" @@ -5046,6 +5253,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + [[package]] name = "winapi" version = "0.3.9" @@ -5210,6 +5432,25 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "ws_stream_wasm" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7999f5f4217fe3818726b66257a4475f71e74ffd190776ad053fa159e50737f5" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version 0.4.1", + "send_wrapper", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 34e4dc152..0cdd6aa6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "crates/executor", "crates/mpt", "crates/interop", + "crates/providers-alloy", "crates/proof-sdk/*", "bin/*" ] @@ -68,6 +69,7 @@ kona-client = { path = "bin/client", version = "0.1.0", default-features = false kona-mpt = { path = "crates/mpt", version = "0.1.2", default-features = false } kona-derive = { path = "crates/derive", version = "0.2.3", default-features = false } kona-driver = { path = "crates/driver", version = "0.2.3", default-features = false } +kona-providers-alloy = { path = "crates/providers-alloy", version = "0.1.0", default-features = false } kona-executor = { path = "crates/executor", version = "0.2.3", default-features = false } kona-interop = { path = "crates/interop", version = "0.1.1", default-features = false } kona-proof = { path = "crates/proof-sdk/proof", version = "0.2.3", default-features = false } diff --git a/bin/host/Cargo.toml b/bin/host/Cargo.toml index b1d4322e2..1deb38323 100644 --- a/bin/host/Cargo.toml +++ b/bin/host/Cargo.toml @@ -20,6 +20,7 @@ kona-preimage = { workspace = true, features = ["std"] } kona-proof = { workspace = true, features = ["std"] } kona-proof-interop.workspace = true kona-client.workspace = true +kona-providers-alloy.workspace = true # Maili maili-protocol = { workspace = true, features = ["std", "serde"] } diff --git a/bin/host/src/eth/blobs.rs b/bin/host/src/eth/blobs.rs deleted file mode 100644 index b110e8db7..000000000 --- a/bin/host/src/eth/blobs.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! Contains an online implementation of the `BlobProvider` trait. - -use alloy_eips::eip4844::{BlobTransactionSidecarItem, IndexedBlobHash}; -use alloy_rpc_types_beacon::sidecar::{BeaconBlobBundle, BlobData}; -use kona_derive::errors::BlobProviderError; -use maili_protocol::BlockInfo; -use reqwest::Client; - -/// The config spec engine api method. -const SPEC_METHOD: &str = "eth/v1/config/spec"; - -/// The beacon genesis engine api method. -const GENESIS_METHOD: &str = "eth/v1/beacon/genesis"; - -/// The blob sidecars engine api method prefix. -const SIDECARS_METHOD_PREFIX: &str = "eth/v1/beacon/blob_sidecars"; - -/// A reduced genesis data. -#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct ReducedGenesisData { - /// The genesis time. - #[serde(rename = "genesis_time")] - #[serde(with = "alloy_serde::quantity")] - pub genesis_time: u64, -} - -/// An API genesis response. -#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct APIGenesisResponse { - /// The data. - pub data: ReducedGenesisData, -} - -/// A reduced config data. -#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct ReducedConfigData { - /// The seconds per slot. - #[serde(rename = "SECONDS_PER_SLOT")] - #[serde(with = "alloy_serde::quantity")] - pub seconds_per_slot: u64, -} - -/// An API config response. -#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct APIConfigResponse { - /// The data. - pub data: ReducedConfigData, -} - -impl APIConfigResponse { - /// Creates a new API config response. - pub const fn new(seconds_per_slot: u64) -> Self { - Self { data: ReducedConfigData { seconds_per_slot } } - } -} - -impl APIGenesisResponse { - /// Creates a new API genesis response. - pub const fn new(genesis_time: u64) -> Self { - Self { data: ReducedGenesisData { genesis_time } } - } -} - -/// An online provider to fetch blob sidecars. -#[derive(Debug, Clone)] -pub struct OnlineBlobProvider { - /// The base url. - base: String, - /// The inner reqwest client. - inner: Client, - /// The genesis time. - genesis_time: u64, - /// The slot interval. - slot_interval: u64, -} - -impl OnlineBlobProvider { - /// Creates a new instance of the [OnlineBlobProvider]. - /// - /// The `genesis_time` and `slot_interval` arguments are _optional_ and the - /// [OnlineBlobProvider] will attempt to load them dynamically at runtime if they are not - /// provided. - pub async fn new_http(base: String) -> Result { - let inner = Client::new(); - let genesis = inner - .get(format!("{}/{}", base, GENESIS_METHOD)) - .send() - .await - .map_err(|_| BlobProviderError::Backend("Failed to fetch genesis".to_string()))?; - let genesis_time = genesis - .json::() - .await - .map_err(|e| BlobProviderError::Backend(e.to_string()))? - .data - .genesis_time; - let spec = inner - .get(format!("{}/{}", base, SPEC_METHOD)) - .send() - .await - .map_err(|_| BlobProviderError::Backend("Failed to fetch config".to_string()))?; - let slot_interval = spec - .json::() - .await - .map_err(|e| BlobProviderError::Backend(e.to_string()))? - .data - .seconds_per_slot; - Ok(Self { base, inner, genesis_time, slot_interval }) - } - - /// Fetches blob sidecars that were confirmed in the specified L1 block with the given indexed - /// hashes. Order of the returned sidecars is guaranteed to be that of the hashes. Blob data is - /// not checked for validity. - async fn beacon_blob_side_cars( - &self, - slot: u64, - hashes: &[IndexedBlobHash], - ) -> Result, reqwest::Error> { - let raw_response = self - .inner - .get(format!("{}/{}/{}", self.base, SIDECARS_METHOD_PREFIX, slot)) - .send() - .await?; - let raw_response = raw_response.json::().await?; - - // Filter the sidecars by the hashes, in-order. - let mut sidecars = Vec::with_capacity(hashes.len()); - hashes.iter().for_each(|hash| { - if let Some(sidecar) = - raw_response.data.iter().find(|sidecar| sidecar.index == hash.index) - { - sidecars.push(sidecar.clone()); - } - }); - - Ok(sidecars) - } - - /// Fetches blob sidecars for the given slot and blob hashes. - pub async fn fetch_sidecars( - &self, - slot: u64, - hashes: &[IndexedBlobHash], - ) -> Result, BlobProviderError> { - self.beacon_blob_side_cars(slot, hashes) - .await - .map_err(|e| BlobProviderError::Backend(e.to_string())) - } - - /// Computes the slot for the given timestamp. - pub const fn slot( - genesis: u64, - slot_time: u64, - timestamp: u64, - ) -> Result { - if timestamp < genesis { - return Err(BlobProviderError::SlotDerivation); - } - Ok((timestamp - genesis) / slot_time) - } - - /// Fetches blob sidecars for the given block reference and blob hashes. - pub async fn fetch_filtered_sidecars( - &self, - block_ref: &BlockInfo, - blob_hashes: &[IndexedBlobHash], - ) -> Result, BlobProviderError> { - if blob_hashes.is_empty() { - return Ok(Vec::new()); - } - - // Calculate the slot for the given timestamp. - let slot = Self::slot(self.genesis_time, self.slot_interval, block_ref.timestamp)?; - - // Fetch blob sidecars for the slot using the given blob hashes. - let sidecars = self.fetch_sidecars(slot, blob_hashes).await?; - - // Filter blob sidecars that match the indicies in the specified list. - let blob_hash_indicies = blob_hashes.iter().map(|b| b.index).collect::>(); - let filtered = sidecars - .into_iter() - .filter(|s| blob_hash_indicies.contains(&s.index)) - .collect::>(); - - // Validate the correct number of blob sidecars were retrieved. - if blob_hashes.len() != filtered.len() { - return Err(BlobProviderError::SidecarLengthMismatch(blob_hashes.len(), filtered.len())); - } - - Ok(filtered - .into_iter() - .map(|s| BlobTransactionSidecarItem { - index: s.index, - blob: s.blob, - kzg_commitment: s.kzg_commitment, - kzg_proof: s.kzg_proof, - }) - .collect::>()) - } -} diff --git a/bin/host/src/eth/mod.rs b/bin/host/src/eth/mod.rs index 749897f7c..8e94d4537 100644 --- a/bin/host/src/eth/mod.rs +++ b/bin/host/src/eth/mod.rs @@ -5,12 +5,6 @@ use alloy_rpc_client::RpcClient; use alloy_transport_http::Http; use reqwest::Client; -mod blobs; -pub use blobs::{ - APIConfigResponse, APIGenesisResponse, OnlineBlobProvider, ReducedConfigData, - ReducedGenesisData, -}; - mod precompiles; pub(crate) use precompiles::execute; diff --git a/bin/host/src/interop/fetcher.rs b/bin/host/src/interop/fetcher.rs index aae2f933a..8a90a00bc 100644 --- a/bin/host/src/interop/fetcher.rs +++ b/bin/host/src/interop/fetcher.rs @@ -2,7 +2,6 @@ //! preimages from a remote source serving the super-chain (interop) proof mode. use super::InteropHostCli; -use crate::eth::OnlineBlobProvider; use alloy_consensus::{Header, TxEnvelope, EMPTY_ROOT_HASH}; use alloy_eips::{ eip2718::Encodable2718, @@ -24,6 +23,7 @@ use kona_preimage::{ HintRouter, PreimageFetcher, PreimageKey, PreimageKeyType, }; use kona_proof_interop::{Hint, HintType, PreState}; +use kona_providers_alloy::{OnlineBeaconClient, OnlineBlobProvider}; use maili_protocol::BlockInfo; use maili_registry::ROLLUP_CONFIGS; use op_alloy_rpc_types_engine::OpPayloadAttributes; @@ -44,7 +44,7 @@ where /// L1 chain provider. l1_provider: ReqwestProvider, /// The blob provider - blob_provider: OnlineBlobProvider, + blob_provider: OnlineBlobProvider, /// L2 chain providers, keyed by chain ID. l2_providers: HashMap, /// The cached active L2 chain ID. @@ -62,7 +62,7 @@ where cfg: InteropHostCli, kv_store: Arc>, l1_provider: ReqwestProvider, - blob_provider: OnlineBlobProvider, + blob_provider: OnlineBlobProvider, l2_providers: HashMap, ) -> Self { let active_l2_chain_id = cfg.active_l2_chain_id().expect("No active L2 chain ID"); diff --git a/bin/host/src/interop/orchestrator.rs b/bin/host/src/interop/orchestrator.rs index 3e9d0c4d5..7f58c2d76 100644 --- a/bin/host/src/interop/orchestrator.rs +++ b/bin/host/src/interop/orchestrator.rs @@ -1,7 +1,7 @@ //! [SingleChainHostCli]'s [HostOrchestrator] + [DetachedHostOrchestrator] implementations. use super::{InteropFetcher, InteropHostCli, LocalKeyValueStore}; -use crate::eth::{http_provider, OnlineBlobProvider}; +use crate::eth::http_provider; use alloy_provider::{Provider, ReqwestProvider}; use anyhow::{anyhow, Result}; use async_trait::async_trait; @@ -10,6 +10,7 @@ use kona_host::{ SharedKeyValueStore, SplitKeyValueStore, }; use kona_preimage::{HintWriter, NativeChannel, OracleReader}; +use kona_providers_alloy::{OnlineBeaconClient, OnlineBlobProvider}; use std::{collections::HashMap, sync::Arc}; use tokio::sync::RwLock; @@ -19,7 +20,7 @@ pub struct InteropProviders { /// The L1 EL provider. l1_provider: ReqwestProvider, /// The L1 beacon node provider. - blob_provider: OnlineBlobProvider, + blob_provider: OnlineBlobProvider, /// The L2 EL providers, keyed by chain ID. l2_providers: HashMap, } @@ -36,11 +37,10 @@ impl HostOrchestrator for InteropHostCli { let l1_provider = http_provider(self.l1_node_address.as_ref().ok_or(anyhow!("Provider must be set"))?); - let blob_provider = OnlineBlobProvider::new_http( + let blob_provider = OnlineBlobProvider::init(OnlineBeaconClient::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}"))?; + )) + .await; // Resolve all chain IDs to their corresponding providers. let l2_node_addresses = diff --git a/bin/host/src/single/fetcher.rs b/bin/host/src/single/fetcher.rs index 39e5f3ce4..0f5de16f6 100644 --- a/bin/host/src/single/fetcher.rs +++ b/bin/host/src/single/fetcher.rs @@ -1,7 +1,6 @@ //! This module contains the [SingleChainFetcher] struct, which is responsible for fetching //! preimages from a remote source serving the single-chain proof mode. -use crate::eth::OnlineBlobProvider; use alloy_consensus::{Header, TxEnvelope, EMPTY_ROOT_HASH}; use alloy_eips::{ eip2718::Encodable2718, @@ -22,6 +21,7 @@ use kona_preimage::{ HintRouter, PreimageFetcher, PreimageKey, PreimageKeyType, }; use kona_proof::{Hint, HintType}; +use kona_providers_alloy::{OnlineBeaconClient, OnlineBlobProvider}; use maili_protocol::BlockInfo; use op_alloy_rpc_types_engine::OpPayloadAttributes; use std::sync::Arc; @@ -39,7 +39,7 @@ where /// L1 chain provider. l1_provider: ReqwestProvider, /// The blob provider - blob_provider: OnlineBlobProvider, + blob_provider: OnlineBlobProvider, /// L2 chain provider. l2_provider: ReqwestProvider, /// L2 head @@ -56,7 +56,7 @@ where pub fn new( kv_store: Arc>, l1_provider: ReqwestProvider, - blob_provider: OnlineBlobProvider, + blob_provider: OnlineBlobProvider, l2_provider: ReqwestProvider, l2_head: B256, ) -> Self { diff --git a/bin/host/src/single/orchestrator.rs b/bin/host/src/single/orchestrator.rs index 56e1841a5..bb61ae546 100644 --- a/bin/host/src/single/orchestrator.rs +++ b/bin/host/src/single/orchestrator.rs @@ -1,7 +1,7 @@ //! [SingleChainHostCli]'s [HostOrchestrator] + [DetachedHostOrchestrator] implementations. use super::{LocalKeyValueStore, SingleChainFetcher, SingleChainHostCli}; -use crate::eth::{http_provider, OnlineBlobProvider}; +use crate::eth::http_provider; use alloy_provider::ReqwestProvider; use anyhow::{anyhow, Result}; use async_trait::async_trait; @@ -10,6 +10,7 @@ use kona_host::{ SharedKeyValueStore, SplitKeyValueStore, }; use kona_preimage::{HintWriter, NativeChannel, OracleReader}; +use kona_providers_alloy::{OnlineBeaconClient, OnlineBlobProvider}; use std::sync::Arc; use tokio::sync::RwLock; @@ -19,7 +20,7 @@ pub struct SingleChainProviders { /// The L1 EL provider. l1_provider: ReqwestProvider, /// The L1 beacon node provider. - blob_provider: OnlineBlobProvider, + blob_provider: OnlineBlobProvider, /// The L2 EL provider. l2_provider: ReqwestProvider, } @@ -33,13 +34,12 @@ impl HostOrchestrator for SingleChainHostCli { return Ok(None); } - 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 = http_provider(self.l1_node_address.as_ref().ok_or(anyhow!("Provider must be set"))?); + let blob_provider = OnlineBlobProvider::init(OnlineBeaconClient::new_http( + self.l1_beacon_address.clone().ok_or(anyhow!("Beacon API URL must be set"))?, + )) + .await; let l2_provider = http_provider( self.l2_node_address.as_ref().ok_or(anyhow!("L2 node address must be set"))?, ); diff --git a/crates/providers-alloy/Cargo.toml b/crates/providers-alloy/Cargo.toml new file mode 100644 index 000000000..b43d00f79 --- /dev/null +++ b/crates/providers-alloy/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "kona-providers-alloy" +version = "0.1.0" +description = "Alloy-backed providers for hilo" + +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, features = ["kzg"] } +alloy-transport.workspace = true +alloy-consensus.workspace = true +alloy-rpc-types-beacon.workspace = true +alloy-provider = { workspace = true, features = ["ipc", "ws", "reqwest"] } +alloy-primitives = { workspace = true, features = ["map"] } + +# Op Alloy +op-alloy-consensus.workspace = true + +# Maili +maili-genesis.workspace = true +maili-protocol.workspace = true + +# Misc +lru.workspace = true +serde.workspace = true +thiserror.workspace = true +async-trait.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 000000000..7655b4c64 --- /dev/null +++ b/crates/providers-alloy/README.md @@ -0,0 +1,8 @@ +# `kona-providers-alloy` + +CI +Kona MPT +License +Codecov + +Alloy-backed providers for `kona`. diff --git a/crates/providers-alloy/src/beacon_client.rs b/crates/providers-alloy/src/beacon_client.rs new file mode 100644 index 000000000..719e47c5c --- /dev/null +++ b/crates/providers-alloy/src/beacon_client.rs @@ -0,0 +1,149 @@ +//! Contains an online implementation of the `BeaconClient` trait. + +use alloy_eips::eip4844::IndexedBlobHash; +use alloy_rpc_types_beacon::sidecar::{BeaconBlobBundle, BlobData}; +use async_trait::async_trait; +use reqwest::Client; +use std::{ + boxed::Box, + format, + string::{String, ToString}, + vec::Vec, +}; + +/// The config spec engine api method. +const SPEC_METHOD: &str = "eth/v1/config/spec"; + +/// The beacon genesis engine api method. +const GENESIS_METHOD: &str = "eth/v1/beacon/genesis"; + +/// The blob sidecars engine api method prefix. +const SIDECARS_METHOD_PREFIX: &str = "eth/v1/beacon/blob_sidecars"; + +/// A reduced genesis data. +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct ReducedGenesisData { + /// The genesis time. + #[serde(rename = "genesis_time")] + #[serde(with = "alloy_serde::quantity")] + pub genesis_time: u64, +} + +/// An API genesis response. +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct APIGenesisResponse { + /// The data. + pub data: ReducedGenesisData, +} + +/// A reduced config data. +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct ReducedConfigData { + /// The seconds per slot. + #[serde(rename = "SECONDS_PER_SLOT")] + #[serde(with = "alloy_serde::quantity")] + pub seconds_per_slot: u64, +} + +/// An API config response. +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct APIConfigResponse { + /// The data. + pub data: ReducedConfigData, +} + +impl APIConfigResponse { + /// Creates a new API config response. + pub const fn new(seconds_per_slot: u64) -> Self { + Self { data: ReducedConfigData { seconds_per_slot } } + } +} + +impl APIGenesisResponse { + /// Creates a new API genesis response. + pub const fn new(genesis_time: u64) -> Self { + Self { data: ReducedGenesisData { genesis_time } } + } +} + +/// The [BeaconClient] is a thin wrapper around the Beacon API. +#[async_trait] +pub trait BeaconClient { + /// The error type for [BeaconClient] implementations. + type Error: core::fmt::Display + ToString; + + /// Returns the config spec. + async fn config_spec(&self) -> Result; + + /// Returns the beacon genesis. + async fn beacon_genesis(&self) -> Result; + + /// Fetches blob sidecars that were confirmed in the specified L1 block with the given indexed + /// hashes. Order of the returned sidecars is guaranteed to be that of the hashes. Blob data is + /// not checked for validity. + async fn beacon_blob_side_cars( + &self, + slot: u64, + hashes: &[IndexedBlobHash], + ) -> Result, Self::Error>; +} + +/// An online implementation of the [BeaconClient] trait. +#[derive(Debug, Clone)] +pub struct OnlineBeaconClient { + /// The base URL of the beacon API. + base: String, + /// The inner reqwest client. + inner: Client, +} + +impl OnlineBeaconClient { + /// Creates a new [OnlineBeaconClient] from the provided [reqwest::Url]. + pub fn new_http(mut base: String) -> Self { + // If base ends with a slash, remove it + if base.ends_with("/") { + base.remove(base.len() - 1); + } + Self { base, inner: Client::new() } + } +} + +#[async_trait] +impl BeaconClient for OnlineBeaconClient { + type Error = reqwest::Error; + + async fn config_spec(&self) -> Result { + let first = self.inner.get(format!("{}/{}", self.base, SPEC_METHOD)).send().await?; + first.json::().await + } + + async fn beacon_genesis(&self) -> Result { + let first = self.inner.get(format!("{}/{}", self.base, GENESIS_METHOD)).send().await?; + first.json::().await + } + + async fn beacon_blob_side_cars( + &self, + slot: u64, + hashes: &[IndexedBlobHash], + ) -> Result, Self::Error> { + let raw_response = self + .inner + .get(format!("{}/{}/{}", self.base, SIDECARS_METHOD_PREFIX, slot)) + .send() + .await?; + let raw_response = raw_response.json::().await?; + + // Filter the sidecars by the hashes, in-order. + let mut sidecars = Vec::with_capacity(hashes.len()); + hashes.iter().for_each(|hash| { + if let Some(sidecar) = + raw_response.data.iter().find(|sidecar| sidecar.index == hash.index) + { + sidecars.push(sidecar.clone()); + } + }); + + Ok(sidecars) + } +} diff --git a/crates/providers-alloy/src/blobs.rs b/crates/providers-alloy/src/blobs.rs new file mode 100644 index 000000000..c77687dd8 --- /dev/null +++ b/crates/providers-alloy/src/blobs.rs @@ -0,0 +1,176 @@ +//! Contains an online implementation of the `BlobProvider` trait. + +use crate::BeaconClient; +use alloy_eips::eip4844::{Blob, BlobTransactionSidecarItem, IndexedBlobHash}; +use alloy_rpc_types_beacon::sidecar::BlobData; +use async_trait::async_trait; +use kona_derive::{errors::BlobProviderError, traits::BlobProvider}; +use maili_protocol::BlockInfo; +use std::{boxed::Box, string::ToString, vec::Vec}; + +/// An online implementation of the [BlobProvider] trait. +#[derive(Debug, Clone)] +pub struct OnlineBlobProvider { + /// The Beacon API client. + beacon_client: B, + /// Beacon Genesis time used for the time to slot conversion. + pub genesis_time: u64, + /// Slot interval used for the time to slot conversion. + pub slot_interval: u64, +} + +impl OnlineBlobProvider { + /// Creates a new instance of the [OnlineBlobProvider]. + /// + /// The `genesis_time` and `slot_interval` arguments are _optional_ and the + /// [OnlineBlobProvider] will attempt to load them dynamically at runtime if they are not + /// provided. + /// + /// ## Panics + /// Panics if the genesis time or slot interval cannot be loaded from the beacon client. + pub async fn init(beacon_client: B) -> Self { + let genesis_time = beacon_client + .beacon_genesis() + .await + .map(|r| r.data.genesis_time) + .map_err(|e| BlobProviderError::Backend(e.to_string())) + .expect("Failed to load genesis time from beacon client"); + let slot_interval = beacon_client + .config_spec() + .await + .map(|r| r.data.seconds_per_slot) + .map_err(|e| BlobProviderError::Backend(e.to_string())) + .expect("Failed to load slot interval from beacon client"); + Self { beacon_client, genesis_time, slot_interval } + } + + /// Fetches blob sidecars for the given slot and blob hashes. + pub async fn fetch_sidecars( + &self, + slot: u64, + hashes: &[IndexedBlobHash], + ) -> Result, BlobProviderError> { + self.beacon_client + .beacon_blob_side_cars(slot, hashes) + .await + .map_err(|e| BlobProviderError::Backend(e.to_string())) + } + + /// Computes the slot for the given timestamp. + pub const fn slot( + genesis: u64, + slot_time: u64, + timestamp: u64, + ) -> Result { + if timestamp < genesis { + return Err(BlobProviderError::SlotDerivation); + } + Ok((timestamp - genesis) / slot_time) + } + + /// Fetches blob sidecars for the given block reference and blob hashes. + pub async fn fetch_filtered_sidecars( + &self, + block_ref: &BlockInfo, + blob_hashes: &[IndexedBlobHash], + ) -> Result, BlobProviderError> { + if blob_hashes.is_empty() { + return Ok(Vec::new()); + } + + // Calculate the slot for the given timestamp. + let slot = Self::slot(self.genesis_time, self.slot_interval, block_ref.timestamp)?; + + // Fetch blob sidecars for the slot using the given blob hashes. + let sidecars = self.fetch_sidecars(slot, blob_hashes).await?; + + // Filter blob sidecars that match the indicies in the specified list. + let blob_hash_indicies = blob_hashes.iter().map(|b| b.index).collect::>(); + let filtered = sidecars + .into_iter() + .filter(|s| blob_hash_indicies.contains(&s.index)) + .collect::>(); + + // Validate the correct number of blob sidecars were retrieved. + if blob_hashes.len() != filtered.len() { + return Err(BlobProviderError::SidecarLengthMismatch(blob_hashes.len(), filtered.len())); + } + + Ok(filtered + .into_iter() + .map(|bs| BlobTransactionSidecarItem { + index: bs.index, + blob: bs.blob, + kzg_commitment: bs.kzg_commitment, + kzg_proof: bs.kzg_proof, + }) + .collect::>()) + } +} + +#[async_trait] +impl BlobProvider for OnlineBlobProvider +where + B: BeaconClient + Send + Sync, +{ + type Error = BlobProviderError; + + /// Fetches blob sidecars that were confirmed in the specified L1 block with the given indexed + /// hashes. The blobs are validated for their index and hashes using the specified + /// [IndexedBlobHash]. + async fn get_blobs( + &mut self, + block_ref: &BlockInfo, + blob_hashes: &[IndexedBlobHash], + ) -> Result>, Self::Error> { + // Fetch the blob sidecars for the given block reference and blob hashes. + let sidecars = self.fetch_filtered_sidecars(block_ref, blob_hashes).await?; + + // Validate the blob sidecars straight away with the num hashes. + let blobs = sidecars + .into_iter() + .enumerate() + .map(|(i, sidecar)| { + let hash = blob_hashes + .get(i) + .ok_or(BlobProviderError::Backend("Missing blob hash".to_string()))?; + sidecar + .verify_blob(&IndexedBlobHash { hash: hash.hash, index: hash.index }) + .map(|_| sidecar.blob) + .map_err(|e| BlobProviderError::Backend(e.to_string())) + }) + .collect::>, BlobProviderError>>() + .map_err(|e| BlobProviderError::Backend(e.to_string()))?; + Ok(blobs) + } +} + +/// The minimal interface required to fetch sidecars from a remote blob store. +#[async_trait] +pub trait BlobSidecarProvider { + /// Fetches blob sidecars that were confirmed in the specified L1 block with the given indexed + /// hashes. Order of the returned sidecars is guaranteed to be that of the hashes. Blob data is + /// not checked for validity. + /// + /// Consensus specs: + async fn beacon_blob_side_cars( + &self, + slot: u64, + hashes: &[IndexedBlobHash], + ) -> Result, BlobProviderError>; +} + +/// Blanket implementation of the [BlobSidecarProvider] trait for all types that +/// implemend [BeaconClient], which has a superset of the required functionality. +#[async_trait] +impl BlobSidecarProvider for B { + async fn beacon_blob_side_cars( + &self, + slot: u64, + hashes: &[IndexedBlobHash], + ) -> Result, BlobProviderError> { + self.beacon_blob_side_cars(slot, hashes) + .await + .map_err(|e| BlobProviderError::Backend(e.to_string())) + } +} diff --git a/crates/providers-alloy/src/chain_provider.rs b/crates/providers-alloy/src/chain_provider.rs new file mode 100644 index 000000000..50bb75326 --- /dev/null +++ b/crates/providers-alloy/src/chain_provider.rs @@ -0,0 +1,201 @@ +//! 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 maili_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, + /// `receipts_by_hash_cache` 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()), + 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))?; + + let header = Header::decode(&mut raw_header.as_ref()) + .map_err(|_| AlloyChainProviderError::RawHeaderDecoding(hash))?; + self.header_by_hash_cache.put(hash, header.clone()); + + Ok(header) + } + + async fn block_info_by_number(&mut self, number: u64) -> Result { + let raw_header: Bytes = self + .inner + .raw_request("debug_getRawHeader".into(), [U64::from(number)]) + .await + .map_err(|_| AlloyChainProviderError::RawHeaderFetch(B256::default()))?; + let header = Header::decode(&mut raw_header.as_ref()) + .map_err(|_| AlloyChainProviderError::RawHeaderDecoding(B256::default()))?; + + let block_info = BlockInfo { + hash: header.hash_slow(), + number, + parent_hash: header.parent_hash, + timestamp: header.timestamp, + }; + 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::Eip7702 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 = Block::decode(&mut raw_block.as_ref()) + .map_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)) + } +} 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 000000000..b6a270610 --- /dev/null +++ b/crates/providers-alloy/src/l2_chain_provider.rs @@ -0,0 +1,131 @@ +//! 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}; +use async_trait::async_trait; +use kona_derive::{ + errors::{PipelineError, PipelineErrorKind}, + traits::L2ChainProvider, +}; +use maili_genesis::{RollupConfig, SystemConfig}; +use maili_protocol::{to_system_config, BatchValidationProvider, L2BlockInfo}; +use op_alloy_consensus::{OpBlock, OpTxEnvelope}; +use std::sync::Arc; + +/// 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, +} + +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 } + } + + /// 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; + type Transaction = OpTxEnvelope; + + async fn l2_block_info_by_number(&mut self, number: u64) -> Result { + let block = self + .block_by_number(number) + .await + .map_err(|_| AlloyL2ChainProviderError::BlockNotFound(number))?; + L2BlockInfo::from_block_and_genesis(&block, &self.rollup_config.genesis) + .map_err(|_| AlloyL2ChainProviderError::L2BlockInfoConstruction(number)) + } + + async fn block_by_number(&mut self, number: u64) -> Result { + let raw_block: Bytes = self + .inner + .raw_request("debug_getRawBlock".into(), [U64::from(number)]) + .await + .map_err(|_| AlloyL2ChainProviderError::BlockNotFound(number))?; + OpBlock::decode(&mut raw_block.as_ref()) + .map_err(|_| AlloyL2ChainProviderError::OpBlockDecode(number)) + } +} + +#[async_trait] +impl L2ChainProvider for AlloyL2ChainProvider { + type Error = AlloyL2ChainProviderError; + + async fn system_config_by_number( + &mut self, + number: u64, + rollup_config: Arc, + ) -> Result::Error> { + let block = self + .block_by_number(number) + .await + .map_err(|_| AlloyL2ChainProviderError::BlockNotFound(number))?; + to_system_config(&block, &rollup_config) + .map_err(|_| AlloyL2ChainProviderError::SystemConfigConversion(number)) + } +} diff --git a/crates/providers-alloy/src/lib.rs b/crates/providers-alloy/src/lib.rs new file mode 100644 index 000000000..ce30cf6d9 --- /dev/null +++ b/crates/providers-alloy/src/lib.rs @@ -0,0 +1,19 @@ +#![doc = include_str!("../README.md")] +#![doc(issue_tracker_base_url = "https://github.com/op-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 blobs; +pub use blobs::{BlobSidecarProvider, OnlineBlobProvider}; + +mod chain_provider; +pub use chain_provider::AlloyChainProvider; + +mod l2_chain_provider; +pub use l2_chain_provider::AlloyL2ChainProvider; diff --git a/docker/fpvm-prestates/asterisc-repro.dockerfile b/docker/fpvm-prestates/asterisc-repro.dockerfile index 16e49e47b..d46b01d19 100644 --- a/docker/fpvm-prestates/asterisc-repro.dockerfile +++ b/docker/fpvm-prestates/asterisc-repro.dockerfile @@ -44,7 +44,7 @@ RUN git clone https://github.com/op-rs/kona # Build kona-client on the selected tag RUN cd kona && \ git checkout $CLIENT_TAG && \ - cargo build -Zbuild-std=core,alloc --workspace --bin kona --locked --profile release-client-lto --exclude kona-host && \ + cargo build -Zbuild-std=core,alloc --workspace --bin kona --locked --profile release-client-lto --exclude kona-host --exclude kona-providers-alloy && \ mv ./target/riscv64imac-unknown-none-elf/release-client-lto/kona /kona-client-elf ################################################################ diff --git a/justfile b/justfile index a2715d12b..2d643a695 100644 --- a/justfile +++ b/justfile @@ -105,7 +105,7 @@ build-cannon *args='': --platform linux/amd64 \ -v `pwd`/:/workdir \ -w="/workdir" \ - ghcr.io/op-rs/kona/cannon-builder:main cargo build --workspace -Zbuild-std=core,alloc $@ --exclude kona-host + ghcr.io/op-rs/kona/cannon-builder:main cargo build --workspace -Zbuild-std=core,alloc $@ --exclude kona-host --exclude kona-providers-alloy # Build for the `asterisc` target. Any crates that require the stdlib are excluded from the build for this target. build-asterisc *args='': @@ -114,7 +114,7 @@ build-asterisc *args='': --platform linux/amd64 \ -v `pwd`/:/workdir \ -w="/workdir" \ - ghcr.io/op-rs/kona/asterisc-builder:main cargo build --workspace -Zbuild-std=core,alloc $@ --exclude kona-host + ghcr.io/op-rs/kona/asterisc-builder:main cargo build --workspace -Zbuild-std=core,alloc $@ --exclude kona-host --exclude kona-providers-alloy # Clones and checks out the monorepo at the commit present in `.monorepo` monorepo: