Skip to content

feat: support directly forward transactions to sequencer #265

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jul 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions crates/rpc/rpc-eth-api/src/helpers/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions crates/scroll/node/src/builder/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions crates/scroll/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions crates/scroll/rpc/src/error.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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,
}
}
}
Expand All @@ -27,6 +34,7 @@ impl From<ScrollEthApiError> for jsonrpsee_types::error::ErrorObject<'static> {
fn from(err: ScrollEthApiError) -> Self {
match err {
ScrollEthApiError::Eth(err) => err.into(),
ScrollEthApiError::Sequencer(err) => err.into(),
}
}
}
Expand Down Expand Up @@ -69,3 +77,31 @@ impl From<ProviderError> 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<TransportErrorKind>`].
#[error(transparent)]
HttpError(#[from] RpcError<TransportErrorKind>),
/// Thrown when serializing transaction to forward to sequencer
#[error("invalid sequencer transaction")]
InvalidSequencerTransaction,
}

impl From<SequencerClientError> 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::<String>,
),
}
}
}
47 changes: 42 additions & 5 deletions crates/scroll/rpc/src/eth/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<N> = EthApiInner<
<N as RpcNodeCore>::Provider,
Expand Down Expand Up @@ -73,8 +76,8 @@ pub struct ScrollEthApi<N: ScrollNodeCore, NetworkT = Scroll> {

impl<N: ScrollNodeCore, NetworkT> ScrollEthApi<N, NetworkT> {
/// Creates a new [`ScrollEthApi`].
pub fn new(eth_api: EthApiNodeBackend<N>) -> Self {
let inner = Arc::new(ScrollEthApiInner { eth_api });
pub fn new(eth_api: EthApiNodeBackend<N>, sequencer_client: Option<SequencerClient>) -> Self {
let inner = Arc::new(ScrollEthApiInner { eth_api, sequencer_client });
Self {
inner: inner.clone(),
_nt: PhantomData,
Expand All @@ -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()
Expand Down Expand Up @@ -307,23 +315,41 @@ impl<N: ScrollNodeCore, NetworkT> fmt::Debug for ScrollEthApi<N, NetworkT> {
pub struct ScrollEthApiInner<N: ScrollNodeCore> {
/// Gateway to node's core components.
pub eth_api: EthApiNodeBackend<N>,
/// Sequencer client, configured to forward submitted transactions to sequencer of given Scroll
/// network.
sequencer_client: Option<SequencerClient>,
}

impl<N: ScrollNodeCore> ScrollEthApiInner<N> {
/// Returns a reference to the [`EthApiNodeBackend`].
const fn eth_api(&self) -> &EthApiNodeBackend<N> {
&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<String>,
}

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<String>) -> Self {
self.sequencer_url = sequencer_url;
self
}
}

Expand All @@ -335,6 +361,7 @@ where
type EthApi = ScrollEthApi<N>;

async fn build_eth_api(self, ctx: EthApiCtx<'_, N>) -> eyre::Result<Self::EthApi> {
let Self { sequencer_url } = self;
let eth_api = reth_rpc::EthApiBuilder::new(
ctx.components.provider().clone(),
ctx.components.pool().clone(),
Expand All @@ -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))
}
}
45 changes: 41 additions & 4 deletions crates/scroll/rpc/src/eth/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::{
eth::{ScrollEthApiInner, ScrollNodeCore},
ScrollEthApi,
ScrollEthApi, ScrollEthApiError, SequencerClient,
};
use alloy_consensus::transaction::TransactionInfo;
use alloy_primitives::{Bytes, B256};
Expand All @@ -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;
Expand All @@ -27,7 +27,7 @@ use std::{

impl<N> EthTransactions for ScrollEthApi<N>
where
Self: LoadTransaction<Provider: BlockReaderIdExt>,
Self: LoadTransaction<Provider: BlockReaderIdExt> + EthApiTypes<Error = ScrollEthApiError>,
N: ScrollNodeCore<Provider: BlockReader<Transaction = ProviderTx<Self::Provider>>>,
{
fn signers(&self) -> &parking_lot::RwLock<Vec<Box<dyn EthSigner<ProviderTx<Self::Provider>>>>> {
Expand All @@ -41,6 +41,33 @@ where
let recovered = recover_raw_transaction(&tx)?;
let pool_transaction = <Self::Pool as TransactionPool>::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()
Expand All @@ -60,6 +87,16 @@ where
{
}

impl<N> ScrollEthApi<N>
where
N: ScrollNodeCore,
{
/// Returns the [`SequencerClient`] if one is set.
pub fn raw_tx_forwarder(&self) -> Option<SequencerClient> {
self.inner.sequencer_client.clone()
}
}

/// Scroll implementation of [`TxInfoMapper`].
///
/// Receipt is fetched to extract the `l1_fee` for all transactions but L1 messages.
Expand Down
4 changes: 3 additions & 1 deletion crates/scroll/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading