diff --git a/Cargo.lock b/Cargo.lock index 564235a92af..79f730845be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10476,11 +10476,16 @@ name = "reth-scroll-rpc" version = "1.5.0" dependencies = [ "alloy-consensus", + "alloy-json-rpc", "alloy-primitives", + "alloy-rpc-client", "alloy-rpc-types-eth", + "alloy-transport", + "alloy-transport-http", "eyre", "jsonrpsee-types", "parking_lot", + "reqwest", "reth-chainspec", "reth-evm", "reth-network-api", @@ -10506,6 +10511,7 @@ dependencies = [ "scroll-alloy-rpc-types", "thiserror 2.0.12", "tokio", + "tracing", ] [[package]] diff --git a/crates/rpc/rpc-eth-api/src/helpers/trace.rs b/crates/rpc/rpc-eth-api/src/helpers/trace.rs index 31085bdc08f..92f2b4fe4cf 100644 --- a/crates/rpc/rpc-eth-api/src/helpers/trace.rs +++ b/crates/rpc/rpc-eth-api/src/helpers/trace.rs @@ -327,11 +327,13 @@ pub trait Trace: // prepare transactions, we do everything upfront to reduce time spent with open // state - let max_transactions = - highest_index.map_or(block.body().transaction_count(), |highest| { + let max_transactions = highest_index.map_or_else( + || block.body().transaction_count(), + |highest| { // we need + 1 because the index is 0-based highest as usize + 1 - }); + }, + ); let mut idx = 0; diff --git a/crates/scroll/node/src/builder/pool.rs b/crates/scroll/node/src/builder/pool.rs index bc8034ceb62..8152a5fe5e7 100644 --- a/crates/scroll/node/src/builder/pool.rs +++ b/crates/scroll/node/src/builder/pool.rs @@ -65,6 +65,9 @@ where .no_eip4844() .with_head_timestamp(ctx.head().timestamp) .kzg_settings(ctx.kzg_settings()?) + .with_local_transactions_config( + pool_config_overrides.clone().apply(ctx.pool_config()).local_transactions_config, + ) .with_additional_tasks( pool_config_overrides .additional_validation_tasks diff --git a/crates/scroll/rpc/Cargo.toml b/crates/scroll/rpc/Cargo.toml index 3abfd428dc3..a7203c92fb5 100644 --- a/crates/scroll/rpc/Cargo.toml +++ b/crates/scroll/rpc/Cargo.toml @@ -43,6 +43,16 @@ alloy-primitives.workspace = true alloy-rpc-types-eth.workspace = true alloy-consensus.workspace = true revm.workspace = true +alloy-transport.workspace = true +alloy-json-rpc.workspace = true +alloy-rpc-client.workspace = true +alloy-transport-http.workspace = true + +# reqwest +reqwest = { workspace = true, default-features = false, features = ["rustls-tls-native-roots"] } + +# tracing +tracing.workspace = true # async parking_lot.workspace = true diff --git a/crates/scroll/rpc/src/error.rs b/crates/scroll/rpc/src/error.rs index db577f7b7b8..5471ccf1c8c 100644 --- a/crates/scroll/rpc/src/error.rs +++ b/crates/scroll/rpc/src/error.rs @@ -1,6 +1,9 @@ //! RPC errors specific to Scroll. +use alloy_json_rpc::ErrorPayload; use alloy_rpc_types_eth::BlockError; +use alloy_transport::{RpcError, TransportErrorKind}; +use jsonrpsee_types::error::INTERNAL_ERROR_CODE; use reth_evm::execute::ProviderError; use reth_rpc_convert::transaction::EthTxEnvError; use reth_rpc_eth_api::{AsEthApiError, TransactionConversionError}; @@ -13,12 +16,16 @@ pub enum ScrollEthApiError { /// L1 ethereum error. #[error(transparent)] Eth(#[from] EthApiError), + /// Sequencer client error. + #[error(transparent)] + Sequencer(#[from] SequencerClientError), } impl AsEthApiError for ScrollEthApiError { fn as_err(&self) -> Option<&EthApiError> { match self { Self::Eth(err) => Some(err), + _ => None, } } } @@ -27,6 +34,7 @@ impl From for jsonrpsee_types::error::ErrorObject<'static> { fn from(err: ScrollEthApiError) -> Self { match err { ScrollEthApiError::Eth(err) => err.into(), + ScrollEthApiError::Sequencer(err) => err.into(), } } } @@ -69,3 +77,31 @@ impl From for ScrollEthApiError { Self::Eth(EthApiError::from(value)) } } + +/// Error type when interacting with the Sequencer +#[derive(Debug, thiserror::Error)] +pub enum SequencerClientError { + /// Wrapper around an [`RpcError`]. + #[error(transparent)] + HttpError(#[from] RpcError), + /// Thrown when serializing transaction to forward to sequencer + #[error("invalid sequencer transaction")] + InvalidSequencerTransaction, +} + +impl From for jsonrpsee_types::error::ErrorObject<'static> { + fn from(err: SequencerClientError) -> Self { + match err { + SequencerClientError::HttpError(RpcError::ErrorResp(ErrorPayload { + code, + message, + data, + })) => jsonrpsee_types::error::ErrorObject::owned(code as i32, message, data), + err => jsonrpsee_types::error::ErrorObject::owned( + INTERNAL_ERROR_CODE, + err.to_string(), + None::, + ), + } + } +} diff --git a/crates/scroll/rpc/src/eth/mod.rs b/crates/scroll/rpc/src/eth/mod.rs index 1eae675040c..e78754e2167 100644 --- a/crates/scroll/rpc/src/eth/mod.rs +++ b/crates/scroll/rpc/src/eth/mod.rs @@ -1,6 +1,7 @@ //! Scroll-Reth `eth_` endpoint implementation. use alloy_primitives::U256; +use eyre::WrapErr; use reth_chainspec::{EthChainSpec, EthereumHardforks}; use reth_evm::ConfigureEvm; use reth_network_api::NetworkInfo; @@ -40,6 +41,8 @@ mod pending_block; pub mod receipt; pub mod transaction; +use crate::SequencerClient; + /// Adapter for [`EthApiInner`], which holds all the data required to serve core `eth_` API. pub type EthApiNodeBackend = EthApiInner< ::Provider, @@ -73,8 +76,8 @@ pub struct ScrollEthApi { impl ScrollEthApi { /// Creates a new [`ScrollEthApi`]. - pub fn new(eth_api: EthApiNodeBackend) -> Self { - let inner = Arc::new(ScrollEthApiInner { eth_api }); + pub fn new(eth_api: EthApiNodeBackend, sequencer_client: Option) -> Self { + let inner = Arc::new(ScrollEthApiInner { eth_api, sequencer_client }); Self { inner: inner.clone(), _nt: PhantomData, @@ -98,6 +101,11 @@ where self.inner.eth_api() } + /// Returns the configured sequencer client, if any. + pub fn sequencer_client(&self) -> Option<&SequencerClient> { + self.inner.sequencer_client() + } + /// Return a builder for the [`ScrollEthApi`]. pub const fn builder() -> ScrollEthApiBuilder { ScrollEthApiBuilder::new() @@ -307,6 +315,9 @@ impl fmt::Debug for ScrollEthApi { pub struct ScrollEthApiInner { /// Gateway to node's core components. pub eth_api: EthApiNodeBackend, + /// Sequencer client, configured to forward submitted transactions to sequencer of given Scroll + /// network. + sequencer_client: Option, } impl ScrollEthApiInner { @@ -314,16 +325,31 @@ impl ScrollEthApiInner { const fn eth_api(&self) -> &EthApiNodeBackend { &self.eth_api } + + /// Returns the configured sequencer client, if any. + const fn sequencer_client(&self) -> Option<&SequencerClient> { + self.sequencer_client.as_ref() + } } /// A type that knows how to build a [`ScrollEthApi`]. #[derive(Debug, Default)] -pub struct ScrollEthApiBuilder {} +pub struct ScrollEthApiBuilder { + /// Sequencer client, configured to forward submitted transactions to sequencer of given Scroll + /// network. + sequencer_url: Option, +} impl ScrollEthApiBuilder { /// Creates a [`ScrollEthApiBuilder`] instance. pub const fn new() -> Self { - Self {} + Self { sequencer_url: None } + } + + /// With a [`SequencerClient`]. + pub fn with_sequencer(mut self, sequencer_url: Option) -> Self { + self.sequencer_url = sequencer_url; + self } } @@ -335,6 +361,7 @@ where type EthApi = ScrollEthApi; async fn build_eth_api(self, ctx: EthApiCtx<'_, N>) -> eyre::Result { + let Self { sequencer_url } = self; let eth_api = reth_rpc::EthApiBuilder::new( ctx.components.provider().clone(), ctx.components.pool().clone(), @@ -350,6 +377,16 @@ where .proof_permits(ctx.config.proof_permits) .build_inner(); - Ok(ScrollEthApi::new(eth_api)) + let sequencer_client = if let Some(url) = sequencer_url { + Some( + SequencerClient::new(&url) + .await + .wrap_err_with(|| "Failed to init sequencer client with: {url}")?, + ) + } else { + None + }; + + Ok(ScrollEthApi::new(eth_api, sequencer_client)) } } diff --git a/crates/scroll/rpc/src/eth/transaction.rs b/crates/scroll/rpc/src/eth/transaction.rs index 3eaac929e22..7a749f891da 100644 --- a/crates/scroll/rpc/src/eth/transaction.rs +++ b/crates/scroll/rpc/src/eth/transaction.rs @@ -2,7 +2,7 @@ use crate::{ eth::{ScrollEthApiInner, ScrollNodeCore}, - ScrollEthApi, + ScrollEthApi, ScrollEthApiError, SequencerClient, }; use alloy_consensus::transaction::TransactionInfo; use alloy_primitives::{Bytes, B256}; @@ -11,10 +11,10 @@ use reth_node_api::FullNodeComponents; use reth_provider::{ BlockReader, BlockReaderIdExt, ProviderTx, ReceiptProvider, TransactionsProvider, }; -use reth_rpc_convert::try_into_scroll_tx_info; use reth_rpc_eth_api::{ helpers::{EthSigner, EthTransactions, LoadTransaction, SpawnBlocking}, - FromEthApiError, FullEthApiTypes, RpcNodeCore, RpcNodeCoreExt, TxInfoMapper, + try_into_scroll_tx_info, EthApiTypes, FromEthApiError, FullEthApiTypes, RpcNodeCore, + RpcNodeCoreExt, TxInfoMapper, }; use reth_rpc_eth_types::utils::recover_raw_transaction; use reth_scroll_primitives::ScrollReceipt; @@ -27,7 +27,7 @@ use std::{ impl EthTransactions for ScrollEthApi where - Self: LoadTransaction, + Self: LoadTransaction + EthApiTypes, N: ScrollNodeCore>>, { fn signers(&self) -> &parking_lot::RwLock>>>> { @@ -41,6 +41,33 @@ where let recovered = recover_raw_transaction(&tx)?; let pool_transaction = ::Transaction::from_pooled(recovered); + // On scroll, transactions are forwarded directly to the sequencer to be included in + // blocks that it builds. + if let Some(client) = self.raw_tx_forwarder().as_ref() { + tracing::debug!(target: "scroll::rpc::eth", hash = %pool_transaction.hash(), "forwarding raw transaction to sequencer"); + + // Retain tx in local tx pool before forwarding to sequencer rpc, for local RPC usage. + let hash = self + .pool() + .add_transaction(TransactionOrigin::Local, pool_transaction.clone()) + .await + .map_err(Self::Error::from_eth_err)?; + + tracing::debug!(target: "scroll::rpc::eth", %hash, "successfully added transaction to local tx pool"); + + // Forward to remote sequencer RPC. + match client.forward_raw_transaction(&tx).await { + Ok(sequencer_hash) => { + tracing::debug!(target: "scroll::rpc::eth", local_hash=%hash, sequencer_hash=%sequencer_hash, "successfully forwarded transaction to sequencer"); + } + Err(err) => { + tracing::warn!(target: "scroll::rpc::eth", %err, %hash, "failed to forward transaction to sequencer, but transaction is in local pool"); + } + } + + return Ok(hash); + } + // submit the transaction to the pool with a `Local` origin let hash = self .pool() @@ -60,6 +87,16 @@ where { } +impl ScrollEthApi +where + N: ScrollNodeCore, +{ + /// Returns the [`SequencerClient`] if one is set. + pub fn raw_tx_forwarder(&self) -> Option { + self.inner.sequencer_client.clone() + } +} + /// Scroll implementation of [`TxInfoMapper`]. /// /// Receipt is fetched to extract the `l1_fee` for all transactions but L1 messages. diff --git a/crates/scroll/rpc/src/lib.rs b/crates/scroll/rpc/src/lib.rs index a3058ffee02..76ecfbec7b6 100644 --- a/crates/scroll/rpc/src/lib.rs +++ b/crates/scroll/rpc/src/lib.rs @@ -10,6 +10,8 @@ pub mod error; pub mod eth; +pub mod sequencer; -pub use error::ScrollEthApiError; +pub use error::{ScrollEthApiError, SequencerClientError}; pub use eth::{ScrollEthApi, ScrollReceiptBuilder}; +pub use sequencer::SequencerClient; diff --git a/crates/scroll/rpc/src/sequencer.rs b/crates/scroll/rpc/src/sequencer.rs new file mode 100644 index 00000000000..637525460d4 --- /dev/null +++ b/crates/scroll/rpc/src/sequencer.rs @@ -0,0 +1,201 @@ +//! Helpers for scroll specific RPC implementations. + +use crate::SequencerClientError; +use alloy_json_rpc::{RpcRecv, RpcSend}; +use alloy_primitives::{hex, B256}; +use alloy_rpc_client::{BuiltInConnectionString, ClientBuilder, RpcClient as Client}; +use alloy_transport_http::Http; +use std::{str::FromStr, sync::Arc}; +use thiserror::Error; +use tracing::warn; + +/// Sequencer client error +#[derive(Error, Debug)] +pub enum Error { + /// Invalid scheme + #[error("Invalid scheme of sequencer url: {0}")] + InvalidScheme(String), + /// Invalid url + #[error("Invalid sequencer url: {0}")] + InvalidUrl(String), + /// Establishing a connection to the sequencer endpoint resulted in an error. + #[error("Failed to connect to sequencer: {0}")] + TransportError( + #[from] + #[source] + alloy_transport::TransportError, + ), + /// Reqwest failed to init client + #[error("Failed to init reqwest client for sequencer: {0}")] + ReqwestError( + #[from] + #[source] + reqwest::Error, + ), +} + +/// A client to interact with a Sequencer +#[derive(Debug, Clone)] +pub struct SequencerClient { + inner: Arc, +} + +impl SequencerClient { + /// Creates a new [`SequencerClient`] for the given URL. + /// + /// If the URL is a websocket endpoint we connect a websocket instance. + pub async fn new(sequencer_endpoint: impl Into) -> Result { + let sequencer_endpoint = sequencer_endpoint.into(); + let endpoint = BuiltInConnectionString::from_str(&sequencer_endpoint)?; + if let BuiltInConnectionString::Http(url) = endpoint { + let client = reqwest::Client::builder() + // we force use tls to prevent native issues + .use_rustls_tls() + .build()?; + Self::with_http_client(url, client) + } else { + let client = ClientBuilder::default().connect_with(endpoint).await?; + let inner = SequencerClientInner { sequencer_endpoint, client }; + Ok(Self { inner: Arc::new(inner) }) + } + } + + /// Creates a new [`SequencerClient`] with http transport with the given http client. + pub fn with_http_client( + sequencer_endpoint: impl Into, + client: reqwest::Client, + ) -> Result { + let sequencer_endpoint: String = sequencer_endpoint.into(); + let url = sequencer_endpoint + .parse() + .map_err(|_| Error::InvalidUrl(sequencer_endpoint.clone()))?; + + let http_client = Http::with_client(client, url); + let is_local = http_client.guess_local(); + let client = ClientBuilder::default().transport(http_client, is_local); + + let inner = SequencerClientInner { sequencer_endpoint, client }; + Ok(Self { inner: Arc::new(inner) }) + } + + /// Returns the network of the client + pub fn endpoint(&self) -> &str { + &self.inner.sequencer_endpoint + } + + /// Returns the client + pub fn client(&self) -> &Client { + &self.inner.client + } + + /// Sends a [`alloy_rpc_client::RpcCall`] request to the sequencer endpoint. + async fn send_rpc_call( + &self, + method: &str, + params: Params, + ) -> Result { + let resp = + self.client().request::(method.to_string(), params).await.inspect_err( + |err| { + warn!( + target: "scroll::rpc::sequencer", + %err, + "HTTP request to sequencer failed", + ); + }, + )?; + Ok(resp) + } + + /// Forwards a transaction to the sequencer endpoint. + pub async fn forward_raw_transaction(&self, tx: &[u8]) -> Result { + let rlp_hex = hex::encode_prefixed(tx); + let tx_hash = + self.send_rpc_call("eth_sendRawTransaction", (rlp_hex,)).await.inspect_err(|err| { + warn!( + target: "scroll::rpc::eth", + %err, + "Failed to forward transaction to sequencer", + ); + })?; + + Ok(tx_hash) + } +} + +#[derive(Debug)] +struct SequencerClientInner { + /// The endpoint of the sequencer + sequencer_endpoint: String, + /// The client + client: Client, +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::U64; + + #[tokio::test] + async fn test_http_body_str() { + let client = SequencerClient::new("http://localhost:8545").await.unwrap(); + + let request = client + .client() + .make_request("eth_getBlockByNumber", (U64::from(10),)) + .serialize() + .unwrap() + .take_request(); + let body = request.get(); + + assert_eq!( + body, + r#"{"method":"eth_getBlockByNumber","params":["0xa"],"id":0,"jsonrpc":"2.0"}"# + ); + + let request = client + .client() + .make_request("eth_sendRawTransaction", format!("0x{}", hex::encode("abcd"))) + .serialize() + .unwrap() + .take_request(); + let body = request.get(); + + assert_eq!( + body, + r#"{"method":"eth_sendRawTransaction","params":"0x61626364","id":1,"jsonrpc":"2.0"}"# + ); + } + + #[tokio::test] + #[ignore = "Start if WS is reachable at ws://localhost:8546"] + async fn test_ws_body_str() { + let client = SequencerClient::new("ws://localhost:8546").await.unwrap(); + + let request = client + .client() + .make_request("eth_getBlockByNumber", (U64::from(10),)) + .serialize() + .unwrap() + .take_request(); + let body = request.get(); + + assert_eq!( + body, + r#"{"method":"eth_getBlockByNumber","params":["0xa"],"id":0,"jsonrpc":"2.0"}"# + ); + + let request = client + .client() + .make_request("eth_sendRawTransaction", format!("0x{}", hex::encode("abcd"))) + .serialize() + .unwrap() + .take_request(); + let body = request.get(); + + assert_eq!( + body, + r#"{"method":"eth_sendRawTransaction","params":"0x61626364","id":1,"jsonrpc":"2.0"}"# + ); + } +} diff --git a/crates/transaction-pool/src/test_utils/mock.rs b/crates/transaction-pool/src/test_utils/mock.rs index 9ddde67ba59..afa69b1f95a 100644 --- a/crates/transaction-pool/src/test_utils/mock.rs +++ b/crates/transaction-pool/src/test_utils/mock.rs @@ -799,21 +799,24 @@ impl alloy_consensus::Transaction for MockTransaction { } fn effective_gas_price(&self, base_fee: Option) -> u128 { - base_fee.map_or(self.max_fee_per_gas(), |base_fee| { - // if the tip is greater than the max priority fee per gas, set it to the max - // priority fee per gas + base fee - let tip = self.max_fee_per_gas().saturating_sub(base_fee as u128); - if let Some(max_tip) = self.max_priority_fee_per_gas() { - if tip > max_tip { - max_tip + base_fee as u128 + base_fee.map_or_else( + || self.max_fee_per_gas(), + |base_fee| { + // if the tip is greater than the max priority fee per gas, set it to the max + // priority fee per gas + base fee + let tip = self.max_fee_per_gas().saturating_sub(base_fee as u128); + if let Some(max_tip) = self.max_priority_fee_per_gas() { + if tip > max_tip { + max_tip + base_fee as u128 + } else { + // otherwise return the max fee per gas + self.max_fee_per_gas() + } } else { - // otherwise return the max fee per gas self.max_fee_per_gas() } - } else { - self.max_fee_per_gas() - } - }) + }, + ) } fn is_dynamic_fee(&self) -> bool {