diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2e13c192fd7f..3695e01b3989 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -195,7 +195,7 @@ jobs: --exclude reth-e2e-test-utils --exclude reth-ethereum-payload-builder --exclude reth-exex-test-utils \ --exclude reth-node-ethereum --exclude reth-scroll-cli --exclude reth-scroll-evm \ --exclude reth-scroll-node --exclude scroll-reth --exclude reth-scroll-rpc --exclude reth-scroll-trie \ - --exclude reth-scroll-engine-primitives + --exclude reth-scroll-engine-primitives --exclude scroll-alloy-provider book: name: book diff --git a/Cargo.lock b/Cargo.lock index 7a4251a4d8b4..9a6c89e838d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -786,7 +786,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e1509599021330a31c4a6816b655e34bf67acb1cc03c564e09fd8754ff6c5de" dependencies = [ "alloy-json-rpc", + "alloy-rpc-types-engine", "alloy-transport", + "http-body-util", + "hyper", + "hyper-util", + "jsonwebtoken", "reqwest", "serde_json", "tower 0.5.2", @@ -10337,6 +10342,44 @@ dependencies = [ "scroll-alloy-rpc-types", ] +[[package]] +name = "scroll-alloy-provider" +version = "1.1.5" +dependencies = [ + "alloy-json-rpc", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types-engine", + "alloy-transport", + "alloy-transport-http", + "async-trait", + "derive_more", + "eyre", + "futures-util", + "http-body-util", + "reqwest", + "reth-engine-primitives", + "reth-payload-builder", + "reth-primitives", + "reth-primitives-traits", + "reth-provider", + "reth-rpc-builder", + "reth-rpc-engine-api", + "reth-rpc-types-compat", + "reth-scroll-chainspec", + "reth-scroll-engine-primitives", + "reth-scroll-node", + "reth-scroll-payload", + "reth-tasks", + "reth-tracing", + "reth-transaction-pool", + "scroll-alloy-network", + "scroll-alloy-rpc-types-engine", + "tokio", + "tower 0.4.13", +] + [[package]] name = "scroll-alloy-rpc-types" version = "1.1.5" diff --git a/Cargo.toml b/Cargo.toml index 1157589aee19..3f5196bf54b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,6 +100,7 @@ members = [ "crates/rpc/rpc/", "crates/scroll/alloy/consensus", "crates/scroll/alloy/network", + "crates/scroll/alloy/provider", "crates/scroll/alloy/rpc-types", "crates/scroll/alloy/rpc-types-engine", "crates/scroll/bin/scroll-reth", @@ -487,6 +488,7 @@ alloy-transport-ws = { version = "0.9.2", default-features = false } # scroll scroll-alloy-consensus = { path = "crates/scroll/alloy/consensus" } +scroll-alloy-provider = { path = "crates/scroll/alloy/provider" } scroll-alloy-rpc-types = { path = "crates/scroll/alloy/rpc-types" } scroll-alloy-rpc-types-engine = { path = "crates/scroll/alloy/rpc-types-engine" } scroll-alloy-network = { path = "crates/scroll/alloy/network" } diff --git a/crates/scroll/alloy/provider/Cargo.toml b/crates/scroll/alloy/provider/Cargo.toml new file mode 100644 index 000000000000..75738a39778a --- /dev/null +++ b/crates/scroll/alloy/provider/Cargo.toml @@ -0,0 +1,74 @@ +[package] +name = "scroll-alloy-provider" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[lints] +workspace = true + +[dependencies] +# alloy +alloy-json-rpc.workspace = true +alloy-provider.workspace = true +alloy-primitives.workspace = true +alloy-rpc-types-engine = { workspace = true, features = ["serde"] } +alloy-rpc-client.workspace = true +alloy-transport.workspace = true +alloy-transport-http = { workspace = true, features = ["jwt-auth"] } + +# scroll +scroll-alloy-network.workspace = true +scroll-alloy-rpc-types-engine = { workspace = true, features = ["serde"] } + +# misc +async-trait.workspace = true +derive_more.workspace = true +eyre.workspace = true +http-body-util.workspace = true +reqwest.workspace = true +tower.workspace = true + +[dev-dependencies] +reth-payload-builder = { workspace = true, features = ["test-utils"] } +reth-engine-primitives.workspace = true +reth-primitives.workspace = true +reth-primitives-traits.workspace = true +reth-provider = { workspace = true, features = ["test-utils"] } +reth-rpc-builder.workspace = true +reth-rpc-engine-api.workspace = true +reth-rpc-types-compat.workspace = true +reth-scroll-engine-primitives.workspace = true +reth-scroll-node.workspace = true +reth-scroll-payload.workspace = true +reth-scroll-chainspec.workspace = true +reth-tasks.workspace = true +reth-tracing.workspace = true +reth-transaction-pool.workspace = true + +tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } +futures-util.workspace = true + +[features] +default = ["std"] +std = [ + "alloy-primitives/std", + "alloy-rpc-types-engine/std", + "scroll-alloy-rpc-types-engine/std", + "derive_more/std", + "reth-primitives/std", + "reth-primitives-traits/std", +] +scroll = [ + "reth-scroll-node/scroll", + "reth-scroll-engine-primitives/scroll" +] +optimism = [ + "reth-provider/optimism", + "reth-scroll-engine-primitives/optimism", + "reth-scroll-node/optimism" +] diff --git a/crates/scroll/alloy/provider/src/engine/mod.rs b/crates/scroll/alloy/provider/src/engine/mod.rs new file mode 100644 index 000000000000..0af647b8c99d --- /dev/null +++ b/crates/scroll/alloy/provider/src/engine/mod.rs @@ -0,0 +1,146 @@ +mod provider; +pub use provider::ScrollAuthEngineApiProvider; + +use alloy_primitives::{BlockHash, U64}; +use alloy_provider::{Network, Provider}; +use alloy_rpc_types_engine::{ + ClientVersionV1, ExecutionPayloadBodiesV1, ExecutionPayloadV1, ForkchoiceState, + ForkchoiceUpdated, PayloadId, PayloadStatus, +}; +use alloy_transport::{Transport, TransportResult}; +use scroll_alloy_rpc_types_engine::ScrollPayloadAttributes; + +/// Engine API trait for Scroll. Only exposes versions of the API that are supported. +/// Note: +/// > The provider should use a JWT authentication layer. +#[async_trait::async_trait] +pub trait ScrollEngineApi { + /// See also + /// Caution: This should not accept the `withdrawals` field + async fn new_payload_v1(&self, payload: ExecutionPayloadV1) -> TransportResult; + + /// See also + /// Caution: This should not accept the `withdrawals` field in the payload attributes. + /// + /// Modifications: + /// - Adds the below fields to the `payload_attributes`: + /// - transactions: an optional list of transactions to include at the start of the block. + /// - no_tx_pool: a boolean which signals whether pool transactions need to be included in + /// the payload building task. + async fn fork_choice_updated_v1( + &self, + fork_choice_state: ForkchoiceState, + payload_attributes: Option, + ) -> TransportResult; + + /// See also + /// + /// Returns the most recent version of the payload that is available in the corresponding + /// payload build process at the time of receiving this call. + /// + /// Caution: This should not return the `withdrawals` field + /// + /// Note: + /// > Provider software MAY stop the corresponding build process after serving this call. + async fn get_payload_v1(&self, payload_id: PayloadId) -> TransportResult; + + /// See also + async fn get_payload_bodies_by_hash_v1( + &self, + block_hashes: Vec, + ) -> TransportResult; + + /// See also + /// + /// Returns the execution payload bodies by the range starting at `start`, containing `count` + /// blocks. + /// + /// WARNING: This method is associated with the BeaconBlocksByRange message in the consensus + /// layer p2p specification, meaning the input should be treated as untrusted or potentially + /// adversarial. + /// + /// Implementers should take care when acting on the input to this method, specifically + /// ensuring that the range is limited properly, and that the range boundaries are computed + /// correctly and without panics. + async fn get_payload_bodies_by_range_v1( + &self, + start: U64, + count: U64, + ) -> TransportResult; + + /// This function will return the ClientVersionV1 object. + /// See also: + /// make fmt + /// + /// + /// - When connected to a single execution client, the consensus client **MUST** receive an + /// array with a single `ClientVersionV1` object. + /// - When connected to multiple execution clients via a multiplexer, the multiplexer **MUST** + /// concatenate the responses from each execution client into a single, + /// flat array before returning the response to the consensus client. + async fn get_client_version_v1( + &self, + client_version: ClientVersionV1, + ) -> TransportResult>; + + /// See also + async fn exchange_capabilities( + &self, + capabilities: Vec, + ) -> TransportResult>; +} + +#[async_trait::async_trait] +impl ScrollEngineApi for P +where + N: Network, + T: Transport + Clone, + P: Provider, +{ + async fn new_payload_v1(&self, payload: ExecutionPayloadV1) -> TransportResult { + self.client().request("engine_newPayloadV1", (payload,)).await + } + + async fn fork_choice_updated_v1( + &self, + fork_choice_state: ForkchoiceState, + payload_attributes: Option, + ) -> TransportResult { + self.client() + .request("engine_forkchoiceUpdatedV1", (fork_choice_state, payload_attributes)) + .await + } + + async fn get_payload_v1(&self, payload_id: PayloadId) -> TransportResult { + self.client().request("engine_getPayloadV1", (payload_id,)).await + } + + async fn get_payload_bodies_by_hash_v1( + &self, + block_hashes: Vec, + ) -> TransportResult { + self.client().request("engine_getPayloadBodiesByHashV1", (block_hashes,)).await + } + + async fn get_payload_bodies_by_range_v1( + &self, + start: U64, + count: U64, + ) -> TransportResult { + self.client().request("engine_getPayloadBodiesByRangeV1", (start, count)).await + } + + async fn get_client_version_v1( + &self, + client_version: ClientVersionV1, + ) -> TransportResult> { + self.client().request("engine_getClientVersionV1", (client_version,)).await + } + + async fn exchange_capabilities( + &self, + capabilities: Vec, + ) -> TransportResult> { + self.client().request("engine_exchangeCapabilities", (capabilities,)).await + } +} diff --git a/crates/scroll/alloy/provider/src/engine/provider.rs b/crates/scroll/alloy/provider/src/engine/provider.rs new file mode 100644 index 000000000000..c63796194283 --- /dev/null +++ b/crates/scroll/alloy/provider/src/engine/provider.rs @@ -0,0 +1,135 @@ +use alloy_primitives::bytes::Bytes; +use alloy_provider::{Network, RootProvider}; +use alloy_rpc_client::RpcClient; +use alloy_rpc_types_engine::JwtSecret; +use alloy_transport::utils::guess_local_url; +use alloy_transport_http::{ + hyper_util, + hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}, + AuthLayer, AuthService, Http, HyperClient, +}; +use derive_more::Deref; +use http_body_util::Full; +use reqwest::Url; +use scroll_alloy_network::Scroll; + +/// A JWT authenticated Http hyper client. +type JwtAuthHttpClient> = + Http>>>; + +/// An authenticated [`alloy_provider::Provider`] to the [`super::ScrollEngineApi`]. +#[derive(Debug, Clone, Deref)] +pub struct ScrollAuthEngineApiProvider { + auth_provider: RootProvider, +} + +impl ScrollAuthEngineApiProvider { + /// Returns a new [`ScrollAuthEngineApiProvider`], authenticated for interfacing with the Engine + /// API server at the provided URL using the passed JWT secret. + pub fn new(jwt_secret: JwtSecret, url: Url) -> Self { + let auth_layer = AuthLayer::new(jwt_secret); + let hyper_client = hyper_util::client::legacy::Client::builder(TokioExecutor::new()) + .build_http::>(); + + let service = tower::ServiceBuilder::new().layer(auth_layer).service(hyper_client); + let transport = HyperClient::, _>::with_service(service); + + let is_url_local = guess_local_url(&url); + let http = Http::with_client(transport, url); + let client = RpcClient::new(http, is_url_local); + + let provider = RootProvider::new(client); + Self { auth_provider: provider } + } +} + +#[cfg(all(test, feature = "scroll", not(feature = "optimism")))] +mod tests { + use super::*; + use crate::engine::ScrollEngineApi; + use alloy_primitives::U64; + use alloy_rpc_types_engine::{ClientCode, ClientVersionV1, ForkchoiceState, PayloadId}; + use reth_engine_primitives::{BeaconConsensusEngineHandle, PayloadTypes}; + use reth_payload_builder::{PayloadBuilderHandle, PayloadBuilderService}; + use reth_primitives::{Block, TransactionSigned}; + use reth_primitives_traits::block::Block as _; + use reth_provider::{test_utils::NoopProvider, CanonStateNotification}; + use reth_rpc_builder::auth::{AuthRpcModule, AuthServerConfig, AuthServerHandle}; + use reth_rpc_engine_api::{capabilities::EngineCapabilities, EngineApi}; + use reth_rpc_types_compat::engine::block_to_payload_v1; + use reth_scroll_chainspec::SCROLL_MAINNET; + use reth_scroll_engine_primitives::{ + ScrollBuiltPayload, ScrollEngineTypes, ScrollPayloadBuilderAttributes, + }; + use reth_scroll_node::ScrollEngineValidator; + use reth_scroll_payload::NoopPayloadJobGenerator; + use reth_tasks::TokioTaskExecutor; + use reth_transaction_pool::noop::NoopTransactionPool; + use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; + use tokio::sync::mpsc::unbounded_channel; + + fn spawn_test_payload_service() -> PayloadBuilderHandle + where + T: PayloadTypes< + PayloadBuilderAttributes = ScrollPayloadBuilderAttributes, + BuiltPayload = ScrollBuiltPayload, + > + 'static, + { + let (service, handle) = PayloadBuilderService::< + NoopPayloadJobGenerator, + futures_util::stream::Empty, + T, + >::new(Default::default(), futures_util::stream::empty()); + tokio::spawn(service); + handle + } + + async fn launch_auth(jwt_secret: JwtSecret) -> AuthServerHandle { + let config = AuthServerConfig::builder(jwt_secret) + .socket_addr(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0))) + .build(); + let (tx, _rx) = unbounded_channel(); + let beacon_engine_handle = BeaconConsensusEngineHandle::::new(tx); + let client = ClientVersionV1 { + code: ClientCode::RH, + name: "Reth".to_string(), + version: "v0.2.0-beta.5".to_string(), + commit: "defa64b2".to_string(), + }; + + let engine_api = EngineApi::new( + NoopProvider::default(), + SCROLL_MAINNET.clone(), + beacon_engine_handle, + spawn_test_payload_service().into(), + NoopTransactionPool::default(), + Box::::default(), + client, + EngineCapabilities::default(), + ScrollEngineValidator::new(SCROLL_MAINNET.clone()), + ); + let module = AuthRpcModule::new(engine_api); + module.start_server(config).await.unwrap() + } + + #[allow(unused_must_use)] + #[tokio::test(flavor = "multi_thread")] + async fn test_engine_api_provider() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let secret = JwtSecret::random(); + let handle = launch_auth(secret).await; + let url = handle.http_url().parse()?; + let provider = ScrollAuthEngineApiProvider::new(secret, url); + + let block = Block::::default().seal_slow(); + provider.new_payload_v1(block_to_payload_v1(block.clone())).await; + provider.fork_choice_updated_v1(ForkchoiceState::default(), None).await; + provider.get_payload_v1(PayloadId::new([0, 0, 0, 0, 0, 0, 0, 0])).await; + provider.get_payload_bodies_by_hash_v1(vec![]).await; + provider.get_payload_bodies_by_range_v1(U64::ZERO, U64::from(1u64)).await; + provider.exchange_capabilities(vec![]).await; + + Ok(()) + } +} diff --git a/crates/scroll/alloy/provider/src/lib.rs b/crates/scroll/alloy/provider/src/lib.rs new file mode 100644 index 000000000000..8c3dad3d0e55 --- /dev/null +++ b/crates/scroll/alloy/provider/src/lib.rs @@ -0,0 +1,4 @@ +//! Providers implementations fitted to Scroll needs. + +mod engine; +pub use engine::{ScrollAuthEngineApiProvider, ScrollEngineApi}; diff --git a/crates/scroll/node/src/builder/engine.rs b/crates/scroll/node/src/builder/engine.rs index 8d1a78513695..f0d5c5f5a692 100644 --- a/crates/scroll/node/src/builder/engine.rs +++ b/crates/scroll/node/src/builder/engine.rs @@ -47,6 +47,13 @@ pub struct ScrollEngineValidator { chainspec: Arc, } +impl ScrollEngineValidator { + /// Returns a new [`ScrollEngineValidator`]. + pub const fn new(chainspec: Arc) -> Self { + Self { chainspec } + } +} + impl EngineValidator for ScrollEngineValidator where Types: EngineTypes,