diff --git a/Cargo.lock b/Cargo.lock index 4eee58a7f3..988dd35196 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3424,6 +3424,7 @@ dependencies = [ "reth-network-peers", "reth-node-builder", "reth-op", + "reth-optimism-consensus", "reth-optimism-forks", "reth-payload-builder", "reth-rpc-api", diff --git a/examples/custom-node/Cargo.toml b/examples/custom-node/Cargo.toml index f2aafa1e9a..886f2509fe 100644 --- a/examples/custom-node/Cargo.toml +++ b/examples/custom-node/Cargo.toml @@ -12,7 +12,8 @@ reth-codecs.workspace = true reth-network-peers.workspace = true reth-node-builder.workspace = true reth-optimism-forks.workspace = true -reth-op = { workspace = true, features = ["node"] } +reth-optimism-consensus.workspace = true +reth-op = { workspace = true, features = ["node", "pool"] } reth-payload-builder.workspace = true reth-rpc-api.workspace = true reth-rpc-engine-api.workspace = true diff --git a/examples/custom-node/src/consensus.rs b/examples/custom-node/src/consensus.rs new file mode 100644 index 0000000000..b9a8d2e163 --- /dev/null +++ b/examples/custom-node/src/consensus.rs @@ -0,0 +1,27 @@ +use std::sync::Arc; + +use reth_node_builder::{ + components::ConsensusBuilder, BuilderContext, FullNodeTypes, NodePrimitives, NodeTypes, +}; +use reth_op::DepositReceipt; +use reth_optimism_consensus::OpBeaconConsensus; +use reth_optimism_forks::OpHardforks; + +#[derive(Debug, Default, Clone)] +pub struct CustomConsensusBuilder; + +impl ConsensusBuilder for CustomConsensusBuilder +where + Node: FullNodeTypes< + Types: NodeTypes< + ChainSpec: OpHardforks, + Primitives: NodePrimitives, + >, + >, +{ + type Consensus = Arc::ChainSpec>>; + + async fn build_consensus(self, ctx: &BuilderContext) -> eyre::Result { + Ok(Arc::new(OpBeaconConsensus::new(ctx.chain_spec()))) + } +} diff --git a/examples/custom-node/src/lib.rs b/examples/custom-node/src/lib.rs index 77f6885634..e6a2eab861 100644 --- a/examples/custom-node/src/lib.rs +++ b/examples/custom-node/src/lib.rs @@ -8,20 +8,21 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] use chainspec::CustomChainSpec; +use consensus::CustomConsensusBuilder; use engine::CustomPayloadTypes; +use pool::CustomPoolBuilder; use primitives::CustomNodePrimitives; use reth_ethereum::node::api::{FullNodeTypes, NodeTypes}; use reth_node_builder::{components::ComponentsBuilder, Node, NodeComponentsBuilder}; -use reth_op::node::{ - node::{OpConsensusBuilder, OpPoolBuilder, OpStorage}, - OpNode, -}; +use reth_op::node::{node::OpStorage, OpNode}; pub mod chainspec; +pub mod consensus; pub mod engine; pub mod engine_api; pub mod evm; pub mod network; +pub mod pool; pub mod primitives; #[derive(Debug, Clone)] @@ -45,17 +46,19 @@ where Storage = OpStorage, >, >, - ComponentsBuilder: NodeComponentsBuilder, + ComponentsBuilder: + NodeComponentsBuilder, { - type ComponentsBuilder = ComponentsBuilder; + type ComponentsBuilder = + ComponentsBuilder; type AddOns = (); fn components_builder(&self) -> Self::ComponentsBuilder { ComponentsBuilder::default() .node_types::() - .pool(OpPoolBuilder::default()) - .consensus(OpConsensusBuilder::default()) + .pool(CustomPoolBuilder::default()) + .consensus(CustomConsensusBuilder) } fn add_ons(&self) -> Self::AddOns {} diff --git a/examples/custom-node/src/network.rs b/examples/custom-node/src/network.rs index b77018cf60..38dd12b2e5 100644 --- a/examples/custom-node/src/network.rs +++ b/examples/custom-node/src/network.rs @@ -1,6 +1,9 @@ use crate::{ chainspec::CustomChainSpec, - primitives::{CustomHeader, CustomNodePrimitives}, + primitives::{ + CustomHeader, CustomNodePrimitives, CustomTransactionEnvelope, ExtendedOpTxEnvelope, + ExtendedTxEnvelope, + }, }; use alloy_consensus::{Block, BlockBody}; use eyre::Result; @@ -12,7 +15,7 @@ use reth_ethereum::{ pool::{PoolTransaction, TransactionPool}, }; use reth_node_builder::{components::NetworkBuilder, BuilderContext}; -use reth_op::{OpReceipt, OpTransactionSigned}; +use reth_op::OpReceipt; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] @@ -20,10 +23,10 @@ pub struct CustomNetworkPrimitives; impl NetworkPrimitives for CustomNetworkPrimitives { type BlockHeader = CustomHeader; - type BlockBody = BlockBody; - type Block = Block; - type BroadcastedTransaction = OpTransactionSigned; - type PooledTransaction = OpPooledTransaction; + type BlockBody = BlockBody, CustomHeader>; + type Block = Block, CustomHeader>; + type BroadcastedTransaction = ExtendedOpTxEnvelope; + type PooledTransaction = ExtendedTxEnvelope; type Receipt = OpReceipt; } @@ -77,7 +80,7 @@ where Pool: TransactionPool< Transaction: PoolTransaction< Consensus = TxTy, - Pooled = OpPooledTransaction, + Pooled = ExtendedTxEnvelope, >, > + Unpin + 'static, diff --git a/examples/custom-node/src/pool.rs b/examples/custom-node/src/pool.rs new file mode 100644 index 0000000000..4baedb28c2 --- /dev/null +++ b/examples/custom-node/src/pool.rs @@ -0,0 +1,214 @@ +// use jsonrpsee::tracing::{debug, info}; +use op_alloy_consensus::{interop::SafetyLevel, OpTxEnvelope}; +use reth_chain_state::CanonStateSubscriptions; +use reth_node_builder::{ + components::{PoolBuilder, PoolBuilderConfigOverrides}, + node::{FullNodeTypes, NodeTypes}, + BuilderContext, NodePrimitives, +}; +use reth_op::{ + node::txpool::{ + conditional::MaybeConditionalTransaction, + interop::MaybeInteropTransaction, + supervisor::{SupervisorClient, DEFAULT_SUPERVISOR_URL}, + OpPooledTransaction, OpTransactionPool, OpTransactionValidator, + }, + pool::{ + blobstore::DiskFileBlobStore, CoinbaseTipOrdering, EthPoolTransaction, + TransactionValidationTaskExecutor, + }, +}; +use reth_optimism_forks::OpHardforks; + +use crate::primitives::{CustomTransactionEnvelope, ExtendedTxEnvelope}; + +#[derive(Debug, Clone)] +pub struct CustomPoolBuilder< + T = OpPooledTransaction>, +> { + /// Enforced overrides that are applied to the pool config. + pub pool_config_overrides: PoolBuilderConfigOverrides, + /// Enable transaction conditionals. + pub enable_tx_conditional: bool, + /// Supervisor client url + pub supervisor_http: String, + /// Supervisor safety level + pub supervisor_safety_level: SafetyLevel, + /// Marker for the pooled transaction type. + _pd: core::marker::PhantomData, +} + +impl Default for CustomPoolBuilder { + fn default() -> Self { + Self { + pool_config_overrides: Default::default(), + enable_tx_conditional: false, + supervisor_http: DEFAULT_SUPERVISOR_URL.to_string(), + supervisor_safety_level: SafetyLevel::CrossUnsafe, + _pd: Default::default(), + } + } +} + +impl CustomPoolBuilder { + /// Sets the enable_tx_conditional flag on the pool builder. + pub const fn with_enable_tx_conditional(mut self, enable_tx_conditional: bool) -> Self { + self.enable_tx_conditional = enable_tx_conditional; + self + } + + /// Sets the [PoolBuilderConfigOverrides] on the pool builder. + pub fn with_pool_config_overrides( + mut self, + pool_config_overrides: PoolBuilderConfigOverrides, + ) -> Self { + self.pool_config_overrides = pool_config_overrides; + self + } + + /// Sets the supervisor client + pub fn with_supervisor( + mut self, + supervisor_client: String, + supervisor_safety_level: SafetyLevel, + ) -> Self { + self.supervisor_http = supervisor_client; + self.supervisor_safety_level = supervisor_safety_level; + self + } +} + +impl PoolBuilder for CustomPoolBuilder +where + Node: FullNodeTypes>, + ::Primitives: + NodePrimitives>, + T: EthPoolTransaction> + + MaybeConditionalTransaction + + MaybeInteropTransaction, +{ + type Pool = OpTransactionPool; + + async fn build_pool(self, ctx: &BuilderContext) -> eyre::Result { + let Self { pool_config_overrides, .. } = self; + let data_dir = ctx.config().datadir(); + let blob_store = DiskFileBlobStore::open(data_dir.blobstore(), Default::default())?; + // supervisor used for interop + if ctx.chain_spec().is_interop_active_at_timestamp(ctx.head().timestamp) && + self.supervisor_http == DEFAULT_SUPERVISOR_URL + { + // info!(target: "reth::cli", + // url=%DEFAULT_SUPERVISOR_URL, + // "Default supervisor url is used, consider changing --rollup.supervisor-http." + // ); + } + let supervisor_client = SupervisorClient::builder(self.supervisor_http.clone()) + .minimum_safety(self.supervisor_safety_level) + .build() + .await; + + let validator = TransactionValidationTaskExecutor::eth_builder(ctx.provider().clone()) + .no_eip4844() + .with_head_timestamp(ctx.head().timestamp) + .kzg_settings(ctx.kzg_settings()?) + .set_tx_fee_cap(ctx.config().rpc.rpc_tx_fee_cap) + .with_additional_tasks( + pool_config_overrides + .additional_validation_tasks + .unwrap_or_else(|| ctx.config().txpool.additional_validation_tasks), + ) + .build_with_tasks(ctx.task_executor().clone(), blob_store.clone()) + .map(|validator| { + OpTransactionValidator::new(validator) + // In --dev mode we can't require gas fees because we're unable to decode + // the L1 block info + .require_l1_data_gas_fee(!ctx.config().dev.dev) + .with_supervisor(supervisor_client.clone()) + }); + + let transaction_pool = reth_ethereum::pool::Pool::new( + validator, + CoinbaseTipOrdering::default(), + blob_store, + pool_config_overrides.apply(ctx.pool_config()), + ); + // info!(target: "reth::cli", "Transaction pool initialized";); + + // spawn txpool maintenance tasks + { + let pool = transaction_pool.clone(); + let chain_events = ctx.provider().canonical_state_stream(); + let client = ctx.provider().clone(); + if !ctx.config().txpool.disable_transactions_backup { + // Use configured backup path or default to data dir + let transactions_path = ctx + .config() + .txpool + .transactions_backup_path + .clone() + .unwrap_or_else(|| data_dir.txpool_transactions()); + + let transactions_backup_config = + reth_ethereum::pool::maintain::LocalTransactionBackupConfig::with_local_txs_backup(transactions_path); + + ctx.task_executor().spawn_critical_with_graceful_shutdown_signal( + "local transactions backup task", + |shutdown| { + reth_ethereum::pool::maintain::backup_local_transactions_task( + shutdown, + pool.clone(), + transactions_backup_config, + ) + }, + ); + } + + // spawn the main maintenance task + ctx.task_executor().spawn_critical( + "txpool maintenance task", + reth_ethereum::pool::maintain::maintain_transaction_pool_future( + client, + pool.clone(), + chain_events, + ctx.task_executor().clone(), + reth_ethereum::pool::maintain::MaintainPoolConfig { + max_tx_lifetime: pool.config().max_queued_lifetime, + no_local_exemptions: transaction_pool + .config() + .local_transactions_config + .no_exemptions, + ..Default::default() + }, + ), + ); + // debug!(target: "reth::cli", "Spawned txpool maintenance task"); + + // spawn the Op txpool maintenance task + let chain_events = ctx.provider().canonical_state_stream(); + ctx.task_executor().spawn_critical( + "Op txpool interop maintenance task", + reth_op::node::txpool::maintain::maintain_transaction_pool_interop_future( + pool.clone(), + chain_events, + supervisor_client, + ), + ); + // debug!(target: "reth::cli", "Spawned Op interop txpool maintenance task"); + + if self.enable_tx_conditional { + // spawn the Op txpool maintenance task + let chain_events = ctx.provider().canonical_state_stream(); + ctx.task_executor().spawn_critical( + "Op txpool conditional maintenance task", + reth_op::node::txpool::maintain::maintain_transaction_pool_conditional_future( + pool, + chain_events, + ), + ); + // debug!(target: "reth::cli", "Spawned Op conditional txpool maintenance task"); + } + } + + Ok(transaction_pool) + } +} diff --git a/examples/custom-node/src/primitives/block.rs b/examples/custom-node/src/primitives/block.rs index e14cd6c646..1415c72b13 100644 --- a/examples/custom-node/src/primitives/block.rs +++ b/examples/custom-node/src/primitives/block.rs @@ -1,8 +1,11 @@ use crate::primitives::CustomHeader; -use reth_op::OpTransactionSigned; + +use super::{CustomTransactionEnvelope, ExtendedOpTxEnvelope}; /// The Block type of this node -pub type Block = alloy_consensus::Block; +pub type Block = + alloy_consensus::Block, CustomHeader>; /// The body type of this node -pub type BlockBody = alloy_consensus::BlockBody; +pub type BlockBody = + alloy_consensus::BlockBody, CustomHeader>; diff --git a/examples/custom-node/src/primitives/extended_op_tx_envelope.rs b/examples/custom-node/src/primitives/extended_op_tx_envelope.rs index 92e8cd09f1..2a48027f66 100644 --- a/examples/custom-node/src/primitives/extended_op_tx_envelope.rs +++ b/examples/custom-node/src/primitives/extended_op_tx_envelope.rs @@ -1,13 +1,13 @@ -use alloy_consensus::Transaction; +use alloy_consensus::{error::ValueError, Transaction}; use alloy_eips::{ eip2718::{Eip2718Error, Eip2718Result, IsTyped2718}, eip2930::AccessList, eip7702::SignedAuthorization, Decodable2718, Encodable2718, Typed2718, }; -use alloy_primitives::{bytes::Buf, ChainId, TxHash}; +use alloy_primitives::{bytes::Buf, ChainId, Signature, TxHash}; use alloy_rlp::{BufMut, Decodable, Encodable, Result as RlpResult}; -use op_alloy_consensus::OpTxEnvelope; +use op_alloy_consensus::{OpPooledTransaction, OpTxEnvelope}; use reth_codecs::Compact; use reth_ethereum::primitives::{ serde_bincode_compat::SerdeBincodeCompat, transaction::signed::RecoveryError, InMemorySize, @@ -15,6 +15,8 @@ use reth_ethereum::primitives::{ }; use revm_primitives::{Address, Bytes, TxKind, B256, U256}; +use super::CustomTransactionEnvelope; + macro_rules! delegate { ($self:expr => $tx:ident.$method:ident($($arg:expr),*)) => { match $self { @@ -39,6 +41,50 @@ pub enum ExtendedTxEnvelope { pub type ExtendedOpTxEnvelope = ExtendedTxEnvelope; +impl TryFrom> + for ExtendedTxEnvelope +{ + type Error = OpTxEnvelope; + + fn try_from( + value: ExtendedTxEnvelope, + ) -> Result { + match value { + ExtendedTxEnvelope::BuiltIn(tx) => { + let converted_tx: OpPooledTransaction = tx.clone().try_into().map_err(|_| tx)?; + Ok(ExtendedTxEnvelope::BuiltIn(converted_tx)) + } + ExtendedTxEnvelope::Other(tx) => Ok(ExtendedTxEnvelope::Other(tx)), + } + } +} + +impl From for ExtendedTxEnvelope { + fn from(tx: OpPooledTransaction) -> Self { + ExtendedTxEnvelope::BuiltIn(tx.into()) + } +} + +impl TryFrom> for OpPooledTransaction { + type Error = ValueError; + + fn try_from( + _tx: ExtendedTxEnvelope, + ) -> Result { + match _tx { + ExtendedTxEnvelope::BuiltIn(inner) => inner.try_into(), + ExtendedTxEnvelope::Other(_tx) => Err(ValueError::new( + OpTxEnvelope::Legacy(alloy_consensus::Signed::new_unchecked( + alloy_consensus::TxLegacy::default(), + Signature::decode_rlp_vrs(&mut &[0u8; 65][..], |_| Ok(false)).unwrap(), + B256::default(), + )), + "Cannot convert custom transaction to OpPooledTransaction", + )), + } + } +} + impl Transaction for ExtendedTxEnvelope where B: Transaction, diff --git a/examples/custom-node/src/primitives/mod.rs b/examples/custom-node/src/primitives/mod.rs index 81ed9a84be..bba93b53d3 100644 --- a/examples/custom-node/src/primitives/mod.rs +++ b/examples/custom-node/src/primitives/mod.rs @@ -15,7 +15,7 @@ pub mod tx_custom; pub use tx_custom::*; use reth_ethereum::primitives::NodePrimitives; -use reth_op::{OpReceipt, OpTransactionSigned}; +use reth_op::OpReceipt; #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct CustomNodePrimitives; @@ -24,6 +24,6 @@ impl NodePrimitives for CustomNodePrimitives { type Block = Block; type BlockHeader = CustomHeader; type BlockBody = BlockBody; - type SignedTx = OpTransactionSigned; + type SignedTx = ExtendedOpTxEnvelope; type Receipt = OpReceipt; } diff --git a/examples/custom-node/src/primitives/tx.rs b/examples/custom-node/src/primitives/tx.rs index c94d67f3e7..f64451a3ee 100644 --- a/examples/custom-node/src/primitives/tx.rs +++ b/examples/custom-node/src/primitives/tx.rs @@ -204,18 +204,18 @@ impl reth_codecs::alloy::transaction::Envelope for CustomTransactionEnvelope { } } -// impl Compact for CustomTransactionEnvelope { -// fn to_compact(&self, buf: &mut B) -> usize -// where -// B: alloy_rlp::bytes::BufMut + AsMut<[u8]>, -// { -// self.inner.tx().to_compact(buf) -// } -// -// fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) { -// let (signature, rest) = Signature::from_compact(buf, len); -// let (inner, buf) = ::from_compact(rest, len); -// let signed = Signed::new_unhashed(inner, signature); -// (CustomTransactionEnvelope { inner: signed }, buf) -// } -// } +impl Compact for CustomTransactionEnvelope { + fn to_compact(&self, buf: &mut B) -> usize + where + B: alloy_rlp::bytes::BufMut + AsMut<[u8]>, + { + self.inner.tx().to_compact(buf) + } + + fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) { + let (signature, rest) = Signature::from_compact(buf, len); + let (inner, buf) = ::from_compact(rest, len); + let signed = Signed::new_unhashed(inner, signature); + (CustomTransactionEnvelope { inner: signed }, buf) + } +}