From f947f800cd24d519ad5e4ff2d60952b5acb0e012 Mon Sep 17 00:00:00 2001 From: Julian Meyer Date: Mon, 12 Jan 2026 19:46:55 +0000 Subject: [PATCH] WIP: pull in engine validator --- Cargo.lock | 59 ++ Cargo.toml | 11 + crates/client/engine/Cargo.toml | 51 ++ crates/client/engine/README.md | 51 ++ crates/client/engine/src/lib.rs | 5 + crates/client/engine/src/validator.rs | 213 ++++++ crates/client/node/Cargo.toml | 36 +- crates/client/node/src/handle.rs | 8 +- crates/client/node/src/lib.rs | 3 + crates/client/node/src/node.rs | 791 ++++++++++++++++++++++ crates/client/node/src/runner.rs | 16 +- crates/client/node/src/test_utils/node.rs | 7 +- crates/client/node/src/types.rs | 10 +- 13 files changed, 1230 insertions(+), 31 deletions(-) create mode 100644 crates/client/engine/Cargo.toml create mode 100644 crates/client/engine/README.md create mode 100644 crates/client/engine/src/lib.rs create mode 100644 crates/client/engine/src/validator.rs create mode 100644 crates/client/node/src/node.rs diff --git a/Cargo.lock b/Cargo.lock index 8286332f..003b1f54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1958,6 +1958,49 @@ dependencies = [ "tracing", ] +[[package]] +name = "base-client-engine" +version = "0.2.1" +dependencies = [ + "alloy-consensus", + "alloy-eip7928", + "alloy-eips", + "alloy-evm", + "alloy-primitives 1.5.2", + "alloy-rpc-types", + "alloy-rpc-types-engine", + "derive_more", + "eyre", + "jsonrpsee 0.26.0", + "rayon", + "reth-chain-state", + "reth-chainspec", + "reth-consensus", + "reth-engine-primitives", + "reth-engine-tree", + "reth-errors", + "reth-evm", + "reth-node-api", + "reth-node-builder", + "reth-node-core", + "reth-payload-builder", + "reth-payload-primitives", + "reth-primitives-traits", + "reth-provider", + "reth-revm", + "reth-rpc", + "reth-rpc-api", + "reth-rpc-builder", + "reth-rpc-engine-api", + "reth-rpc-eth-types", + "reth-tokio-util", + "reth-tracing", + "reth-trie", + "reth-trie-parallel", + "revm-primitives", + "tracing", +] + [[package]] name = "base-client-node" version = "0.2.1" @@ -1969,27 +2012,43 @@ dependencies = [ "alloy-rpc-client", "alloy-rpc-types", "alloy-rpc-types-engine", + "alloy-rpc-types-eth", "alloy-signer", + "base-client-engine", "base-primitives", "chrono", "derive_more", "eyre", "futures-util", "jsonrpsee 0.26.0", + "op-alloy-consensus", "op-alloy-network", "op-alloy-rpc-types-engine", "reth", + "reth-chainspec", "reth-db", + "reth-engine-local", + "reth-evm", "reth-ipc", + "reth-node-api", + "reth-node-builder", "reth-node-core", "reth-optimism-chainspec", + "reth-optimism-forks", "reth-optimism-node", + "reth-optimism-payload-builder", "reth-optimism-primitives", "reth-optimism-rpc", + "reth-optimism-storage", + "reth-optimism-txpool", "reth-primitives-traits", "reth-provider", + "reth-rpc-api", "reth-rpc-layer", + "reth-rpc-server-types", "reth-tracing", + "reth-transaction-pool", + "serde", "tokio", "tower 0.5.2", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 6cb5e776..e9c42dc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ base-client-node = { path = "crates/client/node" } base-metering = { path = "crates/client/metering" } base-txpool = { path = "crates/client/txpool" } base-flashblocks = { path = "crates/client/flashblocks" } +base-client-engine = { path = "crates/client/engine" } # reth reth = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } @@ -87,6 +88,11 @@ reth-optimism-cli = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3 reth-optimism-rpc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3", features = [ "client", ] } +reth-consensus = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-engine-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-engine-tree = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-rpc-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-tokio-util = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-optimism-evm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-testing-utils = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-optimism-node = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } @@ -127,6 +133,11 @@ reth-optimism-consensus = { git = "https://github.com/paradigmxyz/reth", tag = " reth-optimism-forks = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-optimism-payload-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } reth-optimism-txpool = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-optimism-storage = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-rpc-server-types = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-trie-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-engine-local = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } +reth-network = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" } # revm revm = { version = "31.0.2", features = [ diff --git a/crates/client/engine/Cargo.toml b/crates/client/engine/Cargo.toml new file mode 100644 index 00000000..29540f91 --- /dev/null +++ b/crates/client/engine/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "base-client-engine" +description = "Engine validator for Base Node" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +alloy-consensus.workspace = true +alloy-eip7928.workspace = true +alloy-eips.workspace = true +alloy-evm.workspace = true +alloy-primitives.workspace = true +alloy-rpc-types.workspace = true +alloy-rpc-types-engine.workspace = true +jsonrpsee.workspace = true +rayon.workspace = true +reth-chain-state.workspace = true +reth-chainspec.workspace = true +reth-consensus.workspace = true +reth-engine-primitives.workspace = true +reth-engine-tree.workspace = true +reth-errors.workspace = true +reth-evm.workspace = true +reth-node-api.workspace = true +reth-node-builder.workspace = true +reth-node-core.workspace = true +reth-payload-builder.workspace = true +reth-payload-primitives.workspace = true +reth-primitives-traits.workspace = true +reth-provider.workspace = true +reth-revm.workspace = true +reth-rpc.workspace = true +reth-rpc-api.workspace = true +reth-rpc-builder.workspace = true +reth-rpc-engine-api.workspace = true +reth-rpc-eth-types.workspace = true +reth-tokio-util.workspace = true +reth-tracing.workspace = true +reth-trie.workspace = true +reth-trie-parallel.workspace = true +revm-primitives.workspace = true +tracing.workspace = true +eyre.workspace = true +derive_more.workspace = true \ No newline at end of file diff --git a/crates/client/engine/README.md b/crates/client/engine/README.md new file mode 100644 index 00000000..b8fa338e --- /dev/null +++ b/crates/client/engine/README.md @@ -0,0 +1,51 @@ +# `base-client-node` + +CI +MIT License + +Primitive types and traits for Base node runner extensions. Provides extension traits and type aliases for building modular node extensions. + +## Overview + +- **`BaseNodeExtension`**: Trait for node builder extensions that can apply additional wiring to the builder. +- **`ConfigurableBaseNodeExtension`**: Trait for extensions that can be constructed from a configuration type. +- **`OpBuilder`**: Type alias for the OP node builder with launch context. +- **`OpProvider`**: Type alias for the blockchain provider instance. + +Configuration types are located in their respective feature crates: +- **`FlashblocksConfig`**: in `base-flashblocks` crate +- **`TxpoolConfig`**: in `base-txpool` crate + +## Usage + +Add the dependency to your `Cargo.toml`: + +```toml +[dependencies] +base-client-node = { git = "https://github.com/base/node-reth" } +``` + +Implement a custom node extension: + +```rust,ignore +use base_client_node::{BaseNodeExtension, ConfigurableBaseNodeExtension, OpBuilder}; +use eyre::Result; + +#[derive(Debug)] +struct MyExtension { + // extension state +} + +impl BaseNodeExtension for MyExtension { + fn apply(self: Box, builder: OpBuilder) -> OpBuilder { + // Apply custom wiring to the builder + builder + } +} + +impl ConfigurableBaseNodeExtension for MyExtension { + fn build(config: &MyConfig) -> Result { + Ok(Self { /* ... */ }) + } +} +``` diff --git a/crates/client/engine/src/lib.rs b/crates/client/engine/src/lib.rs new file mode 100644 index 00000000..1c6f5d9b --- /dev/null +++ b/crates/client/engine/src/lib.rs @@ -0,0 +1,5 @@ +//! Implements custom engine validator that is optimized for validating canonical blocks +//! after flashblock validation. + +pub mod validator; +pub use validator::{BaseEngineValidatorBuilder, BaseEngineValidator}; \ No newline at end of file diff --git a/crates/client/engine/src/validator.rs b/crates/client/engine/src/validator.rs new file mode 100644 index 00000000..f356a32f --- /dev/null +++ b/crates/client/engine/src/validator.rs @@ -0,0 +1,213 @@ +//! Implements custom engine validator that is optimized for validating canonical blocks + +use std::{fmt::Debug, sync::Arc}; + +use reth_chainspec::EthChainSpec; +use reth_consensus::{ConsensusError, FullConsensus}; +use reth_engine_primitives::{ConfigureEngineEvm, InvalidBlockHook, PayloadValidator}; +use reth_engine_tree::tree::{ + BasicEngineValidator, EngineValidator, + error::InsertPayloadError, + payload_validator::{BlockOrPayload, TreeCtx, ValidationOutcome}, +}; +use reth_evm::ConfigureEvm; +use reth_node_api::{ + AddOnsContext, BlockTy, FullNodeComponents, InvalidPayloadAttributesError, NodeTypes, + PayloadTypes, TreeConfig, +}; +use reth_node_builder::{ + invalid_block_hook::InvalidBlockHookExt, + rpc::{EngineValidatorBuilder, PayloadValidatorBuilder}, +}; +use reth_payload_primitives::{BuiltPayload, NewPayloadError}; +use reth_primitives_traits::{NodePrimitives, RecoveredBlock}; +use reth_provider::{ + BlockReader, DatabaseProviderFactory, HashedPostStateProvider, PruneCheckpointReader, + StageCheckpointReader, StateProviderFactory, StateReader, TrieReader, +}; +use tracing::instrument; +/// Basic implementation of [`EngineValidatorBuilder`]. +/// +/// This builder creates a [`BasicEngineValidator`] using the provided payload validator builder. +#[derive(Debug, Clone)] +pub struct BaseEngineValidatorBuilder { + /// The payload validator builder used to create the engine validator. + payload_validator_builder: EV, +} + +impl BaseEngineValidatorBuilder { + /// Creates a new instance with the given payload validator builder. + pub const fn new(payload_validator_builder: EV) -> Self { + Self { payload_validator_builder } + } +} + +impl Default for BaseEngineValidatorBuilder +where + EV: Default, +{ + fn default() -> Self { + Self::new(EV::default()) + } +} + +impl EngineValidatorBuilder for BaseEngineValidatorBuilder +where + Node: FullNodeComponents< + Evm: ConfigureEngineEvm< + <::Payload as PayloadTypes>::ExecutionData, + >, + >, + EV: PayloadValidatorBuilder, + EV::Validator: reth_engine_primitives::PayloadValidator< + ::Payload, + Block = BlockTy, + >, +{ + type EngineValidator = BaseEngineValidator; + + async fn build_tree_validator( + self, + ctx: &AddOnsContext<'_, Node>, + tree_config: TreeConfig, + ) -> eyre::Result { + let validator = self.payload_validator_builder.build(ctx).await?; + let data_dir = ctx.config.datadir.clone().resolve_datadir(ctx.config.chain.chain()); + let invalid_block_hook = ctx.create_invalid_block_hook(&data_dir).await?; + Ok(BaseEngineValidator::new( + ctx.node.provider().clone(), + std::sync::Arc::new(ctx.node.consensus().clone()), + ctx.node.evm_config().clone(), + validator, + tree_config, + invalid_block_hook, + )) + } +} + +/// A helper type that provides reusable payload validation logic for network-specific validators. +/// +/// This type satisfies [`EngineValidator`] and is responsible for executing blocks/payloads. +/// +/// This type contains common validation, execution, and state root computation logic that can be +/// used by network-specific payload validators (e.g., Ethereum, Optimism). It is not meant to be +/// used as a standalone component, but rather as a building block for concrete implementations. +#[derive(derive_more::Debug)] +pub struct BaseEngineValidator +where + Evm: ConfigureEvm, +{ + inner: BasicEngineValidator, +} + +impl BaseEngineValidator +where + N: NodePrimitives, + P: DatabaseProviderFactory< + Provider: BlockReader + TrieReader + StageCheckpointReader + PruneCheckpointReader, + > + BlockReader
+ + StateProviderFactory + + StateReader + + HashedPostStateProvider + + Clone + + 'static, + Evm: ConfigureEvm + 'static, +{ + /// Creates a new `TreePayloadValidator`. + #[allow(clippy::too_many_arguments)] + pub fn new( + provider: P, + consensus: Arc>, + evm_config: Evm, + validator: V, + config: TreeConfig, + invalid_block_hook: Box>, + ) -> Self { + Self { + inner: BasicEngineValidator::new( + provider, + consensus, + evm_config, + validator, + config, + invalid_block_hook, + ), + } + } + + /// Validates a block that has already been converted from a payload. + /// + /// This method performs: + /// - Consensus validation + /// - Block execution + /// - State root computation + /// - Fork detection + #[instrument( + level = "debug", + target = "engine::tree::payload_validator", + skip_all, + fields( + parent = ?input.parent_hash(), + type_name = ?input.type_name(), + ) + )] + pub fn validate_block_with_state>>( + &mut self, + input: BlockOrPayload, + ctx: TreeCtx<'_, N>, + ) -> ValidationOutcome> + where + V: PayloadValidator, + Evm: ConfigureEngineEvm, + { + self.inner.validate_block_with_state(input, ctx) + } +} + +impl EngineValidator for BaseEngineValidator +where + P: DatabaseProviderFactory< + Provider: BlockReader + TrieReader + StageCheckpointReader + PruneCheckpointReader, + > + BlockReader
+ + StateProviderFactory + + StateReader + + HashedPostStateProvider + + Clone + + 'static, + N: NodePrimitives, + V: PayloadValidator, + Evm: ConfigureEngineEvm + 'static, + Types: PayloadTypes>, +{ + fn validate_payload_attributes_against_header( + &self, + attr: &Types::PayloadAttributes, + header: &N::BlockHeader, + ) -> Result<(), InvalidPayloadAttributesError> { + self.inner.validate_payload_attributes_against_header(attr, header) + } + + fn ensure_well_formed_payload( + &self, + payload: Types::ExecutionData, + ) -> Result, NewPayloadError> { + let block = self.inner.ensure_well_formed_payload(payload)?; + Ok(block) + } + + fn validate_payload( + &mut self, + payload: Types::ExecutionData, + ctx: TreeCtx<'_, N>, + ) -> ValidationOutcome { + self.validate_block_with_state(BlockOrPayload::Payload(payload), ctx) + } + + fn validate_block( + &mut self, + block: RecoveredBlock, + ctx: TreeCtx<'_, N>, + ) -> ValidationOutcome { + self.validate_block_with_state(BlockOrPayload::Block(block), ctx) + } +} diff --git a/crates/client/node/Cargo.toml b/crates/client/node/Cargo.toml index 5ac4c765..2fe32238 100644 --- a/crates/client/node/Cargo.toml +++ b/crates/client/node/Cargo.toml @@ -29,39 +29,46 @@ test-utils = [ "dep:op-alloy-rpc-types-engine", "dep:reth-ipc", "dep:reth-node-core", - "dep:reth-optimism-primitives", - "dep:reth-optimism-rpc", "dep:reth-primitives-traits", - "dep:reth-provider", "dep:reth-rpc-layer", - "dep:reth-tracing", - "dep:tokio", "dep:tower", "dep:tracing-subscriber", - "dep:url", "reth-db/test-utils", "reth-optimism-node/test-utils", "reth-primitives-traits?/test-utils", - "reth-provider?/test-utils", + "reth-provider/test-utils", ] [dependencies] # Project base-primitives = { workspace = true, optional = true } +base-client-engine = { workspace = true } # reth reth.workspace = true reth-db.workspace = true reth-optimism-node.workspace = true reth-optimism-chainspec.workspace = true +reth-optimism-storage.workspace = true +reth-optimism-txpool.workspace = true +reth-rpc-api.workspace = true +reth-rpc-server-types.workspace = true +reth-transaction-pool.workspace = true +reth-chainspec.workspace = true +reth-engine-local.workspace = true +reth-evm.workspace = true +reth-node-api.workspace = true +reth-node-builder.workspace = true +reth-optimism-forks.workspace = true +reth-optimism-payload-builder.workspace = true reth-ipc = { workspace = true, optional = true } -reth-tracing = { workspace = true, optional = true } +reth-tracing.workspace = true reth-rpc-layer = { workspace = true, optional = true } -reth-provider = { workspace = true, optional = true } +reth-provider.workspace = true reth-node-core = { workspace = true, optional = true } reth-primitives-traits = { workspace = true, optional = true } -reth-optimism-rpc = { workspace = true, features = ["client"], optional = true } -reth-optimism-primitives = { workspace = true, optional = true } +reth-optimism-rpc = { workspace = true, features = ["client"] } +reth-optimism-primitives.workspace = true # alloy alloy-primitives = { workspace = true, optional = true } @@ -71,14 +78,17 @@ alloy-rpc-types = { workspace = true, optional = true } alloy-rpc-types-engine = { workspace = true, optional = true } alloy-provider = { workspace = true, optional = true } alloy-rpc-client = { workspace = true, optional = true } +alloy-rpc-types-eth.workspace = true alloy-signer = { workspace = true, optional = true } # op-alloy op-alloy-rpc-types-engine = { workspace = true, optional = true } op-alloy-network = { workspace = true, optional = true } +op-alloy-consensus.workspace = true # tokio -tokio = { workspace = true, optional = true } +tokio.workspace = true +serde.workspace = true # rpc jsonrpsee = { workspace = true, optional = true } @@ -89,7 +99,7 @@ futures-util.workspace = true tracing.workspace = true derive_more = { workspace = true, features = ["debug"] } tracing-subscriber = { workspace = true, optional = true } -url = { workspace = true, optional = true } +url.workspace = true chrono = { workspace = true, optional = true } # tower for middleware diff --git a/crates/client/node/src/handle.rs b/crates/client/node/src/handle.rs index 9ffc1ca1..9f19f8c0 100644 --- a/crates/client/node/src/handle.rs +++ b/crates/client/node/src/handle.rs @@ -10,7 +10,7 @@ use derive_more::Debug; use eyre::Result; use futures_util::{FutureExt, future::BoxFuture}; use reth::builder::NodeHandleFor; -use reth_optimism_node::OpNode; +use crate::node::BaseNode; /// Handle to a launched Base node. /// @@ -20,14 +20,14 @@ use reth_optimism_node::OpNode; #[derive(Debug, Default)] pub struct BaseNodeHandle { #[debug(skip)] - build_fut: Option>>>, + build_fut: Option>>>, #[debug(skip)] - handle: Option>>, + handle: Option>>, } impl BaseNodeHandle { pub(crate) fn new( - fut: impl Future>> + Send + 'static, + fut: impl Future>> + Send + 'static, ) -> Self { Self { build_fut: Some(fut.boxed()), handle: None } } diff --git a/crates/client/node/src/lib.rs b/crates/client/node/src/lib.rs index eeeb27f4..a7965379 100644 --- a/crates/client/node/src/lib.rs +++ b/crates/client/node/src/lib.rs @@ -17,3 +17,6 @@ pub use types::{BaseNodeBuilder, OpBuilder, OpProvider}; #[cfg(feature = "test-utils")] pub mod test_utils; + +pub mod node; +pub use node::{BaseNode}; diff --git a/crates/client/node/src/node.rs b/crates/client/node/src/node.rs new file mode 100644 index 00000000..a7af4064 --- /dev/null +++ b/crates/client/node/src/node.rs @@ -0,0 +1,791 @@ +//! Base Node types config. + +use std::{marker::PhantomData, sync::Arc}; + +use base_client_engine::BaseEngineValidatorBuilder; +use reth_chainspec::ChainSpecProvider; +use reth_engine_local::LocalPayloadAttributesBuilder; +use reth_evm::ConfigureEvm; +use reth_node_api::{ + BuildNextEnv, FullNodeComponents, HeaderTy, NodeAddOns, PayloadAttributesBuilder, PayloadTypes, + TxTy, +}; +use reth_node_builder::{ + DebugNode, Node, NodeAdapter, NodeComponentsBuilder, + components::{BasicPayloadServiceBuilder, ComponentsBuilder}, + node::{FullNodeTypes, NodeTypes}, + rpc::{ + EngineApiBuilder, EngineValidatorAddOn, EngineValidatorBuilder, EthApiBuilder, Identity, + PayloadValidatorBuilder, RethRpcAddOns, RethRpcMiddleware, RethRpcServerHandles, RpcAddOns, + RpcContext, RpcHandle, + }, +}; +use reth_optimism_chainspec::OpChainSpec; +use reth_optimism_forks::{OpHardfork, OpHardforks}; +use reth_optimism_node::{ + OpConsensusBuilder, OpEngineApiBuilder, OpEngineTypes, OpEngineValidatorBuilder, + OpExecutorBuilder, OpFullNodeTypes, OpNetworkBuilder, OpNodeComponentBuilder, OpNodeTypes, + args::RollupArgs, + node::{OpPayloadBuilder, OpPoolBuilder}, +}; +use reth_optimism_payload_builder::{ + OpAttributes, OpPayloadPrimitives, + config::{OpDAConfig, OpGasLimitConfig}, +}; +use reth_optimism_primitives::OpPrimitives; +use reth_optimism_rpc::{ + SequencerClient, + eth::{OpEthApiBuilder, ext::OpEthExtApi}, + historical::{HistoricalRpc, HistoricalRpcClient}, + miner::{MinerApiExtServer, OpMinerExtApi}, + witness::OpDebugWitnessApi, +}; +use reth_optimism_storage::OpStorage; +use reth_optimism_txpool::OpPooledTx; +use reth_provider::providers::ProviderFactoryBuilder; +use reth_rpc_api::{ + DebugApiServer, DebugExecutionWitnessApiServer, L2EthApiExtServer, eth::RpcTypes, +}; +use reth_rpc_server_types::RethRpcModule; +use reth_tracing::tracing::{debug, info}; +use reth_transaction_pool::TransactionPool; +use serde::de::DeserializeOwned; +use url::Url; + +/// Type configuration for a regular Base node. +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct BaseNode { + /// Additional Optimism args + pub args: RollupArgs, + /// Data availability configuration for the OP builder. + /// + /// Used to throttle the size of the data availability payloads (configured by the batcher via + /// the `miner_` api). + /// + /// By default no throttling is applied. + pub da_config: OpDAConfig, + /// Gas limit configuration for the OP builder. + /// Used to control the gas limit of the blocks produced by the OP builder.(configured by the + /// batcher via the `miner_` api) + pub gas_limit_config: OpGasLimitConfig, +} + +impl BaseNode { + /// Creates a new instance of the Optimism node type. + pub fn new(args: RollupArgs) -> Self { + Self { + args, + da_config: OpDAConfig::default(), + gas_limit_config: OpGasLimitConfig::default(), + } + } + + /// Configure the data availability configuration for the OP builder. + pub fn with_da_config(mut self, da_config: OpDAConfig) -> Self { + self.da_config = da_config; + self + } + + /// Configure the gas limit configuration for the OP builder. + pub fn with_gas_limit_config(mut self, gas_limit_config: OpGasLimitConfig) -> Self { + self.gas_limit_config = gas_limit_config; + self + } + + /// Returns the components for the given [`RollupArgs`]. + pub fn components(&self) -> OpNodeComponentBuilder + where + Node: FullNodeTypes, + { + let RollupArgs { disable_txpool_gossip, compute_pending_block, discovery_v4, .. } = + self.args; + ComponentsBuilder::default() + .node_types::() + .pool( + OpPoolBuilder::default() + .with_enable_tx_conditional(self.args.enable_tx_conditional) + .with_supervisor( + self.args.supervisor_http.clone(), + self.args.supervisor_safety_level, + ), + ) + .executor(OpExecutorBuilder::default()) + .payload(BasicPayloadServiceBuilder::new( + OpPayloadBuilder::new(compute_pending_block) + .with_da_config(self.da_config.clone()) + .with_gas_limit_config(self.gas_limit_config.clone()), + )) + .network(OpNetworkBuilder::new(disable_txpool_gossip, !discovery_v4)) + .consensus(OpConsensusBuilder::default()) + } + + /// Returns [`OpAddOnsBuilder`] with configured arguments. + pub fn add_ons_builder(&self) -> BaseAddOnsBuilder { + BaseAddOnsBuilder::default() + .with_sequencer(self.args.sequencer.clone()) + .with_sequencer_headers(self.args.sequencer_headers.clone()) + .with_da_config(self.da_config.clone()) + .with_gas_limit_config(self.gas_limit_config.clone()) + .with_enable_tx_conditional(self.args.enable_tx_conditional) + .with_min_suggested_priority_fee(self.args.min_suggested_priority_fee) + .with_historical_rpc(self.args.historical_rpc.clone()) + .with_flashblocks(self.args.flashblocks_url.clone()) + } + + /// Instantiates the [`ProviderFactoryBuilder`] for an opstack node. + /// + /// # Open a Providerfactory in read-only mode from a datadir + /// + /// See also: [`ProviderFactoryBuilder`] and + /// [`ReadOnlyConfig`](reth_provider::providers::ReadOnlyConfig). + /// + /// ```no_run + /// use reth_optimism_chainspec::BASE_MAINNET; + /// use reth_optimism_node::OpNode; + /// + /// let factory = + /// OpNode::provider_factory_builder().open_read_only(BASE_MAINNET.clone(), "datadir").unwrap(); + /// ``` + /// + /// # Open a Providerfactory manually with all required components + /// + /// ```no_run + /// use reth_db::open_db_read_only; + /// use reth_optimism_chainspec::OpChainSpecBuilder; + /// use reth_optimism_node::OpNode; + /// use reth_provider::providers::StaticFileProvider; + /// use std::sync::Arc; + /// + /// let factory = OpNode::provider_factory_builder() + /// .db(Arc::new(open_db_read_only("db", Default::default()).unwrap())) + /// .chainspec(OpChainSpecBuilder::base_mainnet().build().into()) + /// .static_file(StaticFileProvider::read_only("db/static_files", false).unwrap()) + /// .build_provider_factory(); + /// ``` + pub fn provider_factory_builder() -> ProviderFactoryBuilder { + ProviderFactoryBuilder::default() + } +} + +impl Node for BaseNode +where + N: FullNodeTypes, +{ + type ComponentsBuilder = ComponentsBuilder< + N, + OpPoolBuilder, + BasicPayloadServiceBuilder, + OpNetworkBuilder, + OpExecutorBuilder, + OpConsensusBuilder, + >; + + type AddOns = BaseAddOns< + NodeAdapter>::Components>, + OpEthApiBuilder, + OpEngineValidatorBuilder, + OpEngineApiBuilder, + BaseEngineValidatorBuilder, + >; + + fn components_builder(&self) -> Self::ComponentsBuilder { + Self::components(self) + } + + fn add_ons(&self) -> Self::AddOns { + self.add_ons_builder().build() + } +} + +impl DebugNode for BaseNode +where + N: FullNodeComponents, +{ + type RpcBlock = alloy_rpc_types_eth::Block; + + fn rpc_to_primitive_block(rpc_block: Self::RpcBlock) -> reth_node_api::BlockTy { + rpc_block.into_consensus() + } + + fn local_payload_attributes_builder( + chain_spec: &Self::ChainSpec, + ) -> impl PayloadAttributesBuilder<::PayloadAttributes> { + LocalPayloadAttributesBuilder::new(Arc::new(chain_spec.clone())) + } +} + +impl NodeTypes for BaseNode { + type Primitives = OpPrimitives; + type ChainSpec = OpChainSpec; + type Storage = OpStorage; + type Payload = OpEngineTypes; +} + +/// Add-ons w.r.t. optimism. +/// +/// This type provides optimism-specific addons to the node and exposes the RPC server and engine +/// API. +#[derive(Debug)] +pub struct BaseAddOns< + N: FullNodeComponents, + EthB: EthApiBuilder, + PVB, + EB = OpEngineApiBuilder, + EVB = BaseEngineValidatorBuilder, + RpcMiddleware = Identity, +> { + /// Rpc add-ons responsible for launching the RPC servers and instantiating the RPC handlers + /// and eth-api. + pub rpc_add_ons: RpcAddOns, + /// Data availability configuration for the OP builder. + pub da_config: OpDAConfig, + /// Gas limit configuration for the OP builder. + pub gas_limit_config: OpGasLimitConfig, + /// Sequencer client, configured to forward submitted transactions to sequencer of given OP + /// network. + pub sequencer_url: Option, + /// Headers to use for the sequencer client requests. + pub sequencer_headers: Vec, + /// RPC endpoint for historical data. + /// + /// This can be used to forward pre-bedrock rpc requests (op-mainnet). + pub historical_rpc: Option, + /// Enable transaction conditionals. + enable_tx_conditional: bool, + min_suggested_priority_fee: u64, +} + +impl BaseAddOns +where + N: FullNodeComponents, + EthB: EthApiBuilder, +{ + /// Creates a new instance from components. + #[allow(clippy::too_many_arguments)] + pub const fn new( + rpc_add_ons: RpcAddOns, + da_config: OpDAConfig, + gas_limit_config: OpGasLimitConfig, + sequencer_url: Option, + sequencer_headers: Vec, + historical_rpc: Option, + enable_tx_conditional: bool, + min_suggested_priority_fee: u64, + ) -> Self { + Self { + rpc_add_ons, + da_config, + gas_limit_config, + sequencer_url, + sequencer_headers, + historical_rpc, + enable_tx_conditional, + min_suggested_priority_fee, + } + } +} + +impl Default for BaseAddOns +where + N: FullNodeComponents, + OpEthApiBuilder: EthApiBuilder, +{ + fn default() -> Self { + Self::builder().build() + } +} + +impl + BaseAddOns< + N, + OpEthApiBuilder, + OpEngineValidatorBuilder, + OpEngineApiBuilder, + RpcMiddleware, + > +where + N: FullNodeComponents, + OpEthApiBuilder: EthApiBuilder, +{ + /// Build a [`OpAddOns`] using [`OpAddOnsBuilder`]. + pub fn builder() -> BaseAddOnsBuilder { + BaseAddOnsBuilder::default() + } +} + +impl BaseAddOns +where + N: FullNodeComponents, + EthB: EthApiBuilder, +{ + /// Maps the [`reth_node_builder::rpc::EngineApiBuilder`] builder type. + pub fn with_engine_api( + self, + engine_api_builder: T, + ) -> BaseAddOns { + let Self { + rpc_add_ons, + da_config, + gas_limit_config, + sequencer_url, + sequencer_headers, + historical_rpc, + enable_tx_conditional, + min_suggested_priority_fee, + .. + } = self; + BaseAddOns::new( + rpc_add_ons.with_engine_api(engine_api_builder), + da_config, + gas_limit_config, + sequencer_url, + sequencer_headers, + historical_rpc, + enable_tx_conditional, + min_suggested_priority_fee, + ) + } + + /// Maps the [`PayloadValidatorBuilder`] builder type. + pub fn with_payload_validator( + self, + payload_validator_builder: T, + ) -> BaseAddOns { + let Self { + rpc_add_ons, + da_config, + gas_limit_config, + sequencer_url, + sequencer_headers, + enable_tx_conditional, + min_suggested_priority_fee, + historical_rpc, + .. + } = self; + BaseAddOns::new( + rpc_add_ons.with_payload_validator(payload_validator_builder), + da_config, + gas_limit_config, + sequencer_url, + sequencer_headers, + historical_rpc, + enable_tx_conditional, + min_suggested_priority_fee, + ) + } + + /// Sets the RPC middleware stack for processing RPC requests. + /// + /// This method configures a custom middleware stack that will be applied to all RPC requests + /// across HTTP, `WebSocket`, and IPC transports. The middleware is applied to the RPC service + /// layer, allowing you to intercept, modify, or enhance RPC request processing. + /// + /// See also [`RpcAddOns::with_rpc_middleware`]. + pub fn with_rpc_middleware(self, rpc_middleware: T) -> BaseAddOns { + let Self { + rpc_add_ons, + da_config, + gas_limit_config, + sequencer_url, + sequencer_headers, + enable_tx_conditional, + min_suggested_priority_fee, + historical_rpc, + .. + } = self; + BaseAddOns::new( + rpc_add_ons.with_rpc_middleware(rpc_middleware), + da_config, + gas_limit_config, + sequencer_url, + sequencer_headers, + historical_rpc, + enable_tx_conditional, + min_suggested_priority_fee, + ) + } + + /// Sets the hook that is run once the rpc server is started. + pub fn on_rpc_started(mut self, hook: F) -> Self + where + F: FnOnce(RpcContext<'_, N, EthB::EthApi>, RethRpcServerHandles) -> eyre::Result<()> + + Send + + 'static, + { + self.rpc_add_ons = self.rpc_add_ons.on_rpc_started(hook); + self + } + + /// Sets the hook that is run to configure the rpc modules. + pub fn extend_rpc_modules(mut self, hook: F) -> Self + where + F: FnOnce(RpcContext<'_, N, EthB::EthApi>) -> eyre::Result<()> + Send + 'static, + { + self.rpc_add_ons = self.rpc_add_ons.extend_rpc_modules(hook); + self + } +} + +impl NodeAddOns + for BaseAddOns +where + N: FullNodeComponents< + Types: NodeTypes< + ChainSpec: OpHardforks, + Primitives: OpPayloadPrimitives, + Payload: PayloadTypes, + >, + Evm: ConfigureEvm< + NextBlockEnvCtx: BuildNextEnv< + Attrs, + HeaderTy, + ::ChainSpec, + >, + >, + Pool: TransactionPool, + >, + EthB: EthApiBuilder, + PVB: Send, + EB: EngineApiBuilder, + EVB: EngineValidatorBuilder, + RpcMiddleware: RethRpcMiddleware, + Attrs: OpAttributes, RpcPayloadAttributes: DeserializeOwned>, +{ + type Handle = RpcHandle; + + async fn launch_add_ons( + self, + ctx: reth_node_api::AddOnsContext<'_, N>, + ) -> eyre::Result { + let Self { + rpc_add_ons, + da_config, + gas_limit_config, + sequencer_url, + sequencer_headers, + enable_tx_conditional, + historical_rpc, + .. + } = self; + + let maybe_pre_bedrock_historical_rpc = historical_rpc + .and_then(|historical_rpc| { + ctx.node + .provider() + .chain_spec() + .op_fork_activation(OpHardfork::Bedrock) + .block_number() + .filter(|activation| *activation > 0) + .map(|bedrock_block| (historical_rpc, bedrock_block)) + }) + .map(|(historical_rpc, bedrock_block)| -> eyre::Result<_> { + info!(target: "reth::cli", %bedrock_block, ?historical_rpc, "Using historical RPC endpoint pre bedrock"); + let provider = ctx.node.provider().clone(); + let client = HistoricalRpcClient::new(&historical_rpc)?; + let layer = HistoricalRpc::new(provider, client, bedrock_block); + Ok(layer) + }) + .transpose()? + ; + + let rpc_add_ons = rpc_add_ons.option_layer_rpc_middleware(maybe_pre_bedrock_historical_rpc); + + let builder = reth_optimism_payload_builder::OpPayloadBuilder::new( + ctx.node.pool().clone(), + ctx.node.provider().clone(), + ctx.node.evm_config().clone(), + ); + // install additional OP specific rpc methods + let debug_ext = OpDebugWitnessApi::<_, _, _, Attrs>::new( + ctx.node.provider().clone(), + Box::new(ctx.node.task_executor().clone()), + builder, + ); + let miner_ext = OpMinerExtApi::new(da_config, gas_limit_config); + + let sequencer_client = if let Some(url) = sequencer_url { + Some(SequencerClient::new_with_headers(url, sequencer_headers).await?) + } else { + None + }; + + let tx_conditional_ext: OpEthExtApi = OpEthExtApi::new( + sequencer_client, + ctx.node.pool().clone(), + ctx.node.provider().clone(), + ); + + rpc_add_ons + .launch_add_ons_with(ctx, move |container| { + let reth_node_builder::rpc::RpcModuleContainer { modules, auth_module, registry } = + container; + + debug!(target: "reth::cli", "Installing debug payload witness rpc endpoint"); + modules.merge_if_module_configured(RethRpcModule::Debug, debug_ext.into_rpc())?; + + // extend the miner namespace if configured in the regular http server + modules.add_or_replace_if_module_configured( + RethRpcModule::Miner, + miner_ext.clone().into_rpc(), + )?; + + // install the miner extension in the authenticated if configured + if modules.module_config().contains_any(&RethRpcModule::Miner) { + debug!(target: "reth::cli", "Installing miner DA rpc endpoint"); + auth_module.merge_auth_methods(miner_ext.into_rpc())?; + } + + // install the debug namespace in the authenticated if configured + if modules.module_config().contains_any(&RethRpcModule::Debug) { + debug!(target: "reth::cli", "Installing debug rpc endpoint"); + auth_module.merge_auth_methods(registry.debug_api().into_rpc())?; + } + + if enable_tx_conditional { + // extend the eth namespace if configured in the regular http server + modules.merge_if_module_configured( + RethRpcModule::Eth, + tx_conditional_ext.into_rpc(), + )?; + } + + Ok(()) + }) + .await + } +} + +impl RethRpcAddOns + for BaseAddOns +where + N: FullNodeComponents< + Types: NodeTypes< + ChainSpec: OpHardforks, + Primitives: OpPayloadPrimitives, + Payload: PayloadTypes, + >, + Evm: ConfigureEvm< + NextBlockEnvCtx: BuildNextEnv< + Attrs, + HeaderTy, + ::ChainSpec, + >, + >, + >, + <::Pool as TransactionPool>::Transaction: OpPooledTx, + EthB: EthApiBuilder, + PVB: PayloadValidatorBuilder, + EB: EngineApiBuilder, + EVB: EngineValidatorBuilder, + RpcMiddleware: RethRpcMiddleware, + Attrs: OpAttributes, RpcPayloadAttributes: DeserializeOwned>, +{ + type EthApi = EthB::EthApi; + + fn hooks_mut(&mut self) -> &mut reth_node_builder::rpc::RpcHooks { + self.rpc_add_ons.hooks_mut() + } +} + +impl EngineValidatorAddOn + for BaseAddOns +where + N: FullNodeComponents, + EthB: EthApiBuilder, + PVB: Send, + EB: EngineApiBuilder, + EVB: EngineValidatorBuilder, + RpcMiddleware: Send, +{ + type ValidatorBuilder = EVB; + + fn engine_validator_builder(&self) -> Self::ValidatorBuilder { + EngineValidatorAddOn::engine_validator_builder(&self.rpc_add_ons) + } +} + +/// A regular optimism evm and executor builder. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct BaseAddOnsBuilder { + /// Sequencer client, configured to forward submitted transactions to sequencer of given OP + /// network. + sequencer_url: Option, + /// Headers to use for the sequencer client requests. + sequencer_headers: Vec, + /// RPC endpoint for historical data. + historical_rpc: Option, + /// Data availability configuration for the OP builder. + da_config: Option, + /// Gas limit configuration for the OP builder. + gas_limit_config: Option, + /// Enable transaction conditionals. + enable_tx_conditional: bool, + /// Marker for network types. + _nt: PhantomData, + /// Minimum suggested priority fee (tip) + min_suggested_priority_fee: u64, + /// RPC middleware to use + rpc_middleware: RpcMiddleware, + /// Optional tokio runtime to use for the RPC server. + tokio_runtime: Option, + /// A URL pointing to a secure websocket service that streams out flashblocks. + flashblocks_url: Option, +} + +impl Default for BaseAddOnsBuilder { + fn default() -> Self { + Self { + sequencer_url: None, + sequencer_headers: Vec::new(), + historical_rpc: None, + da_config: None, + gas_limit_config: None, + enable_tx_conditional: false, + min_suggested_priority_fee: 1_000_000, + _nt: PhantomData, + rpc_middleware: Identity::new(), + tokio_runtime: None, + flashblocks_url: None, + } + } +} + +impl BaseAddOnsBuilder { + /// With a [`SequencerClient`]. + pub fn with_sequencer(mut self, sequencer_client: Option) -> Self { + self.sequencer_url = sequencer_client; + self + } + + /// With headers to use for the sequencer client requests. + pub fn with_sequencer_headers(mut self, sequencer_headers: Vec) -> Self { + self.sequencer_headers = sequencer_headers; + self + } + + /// Configure the data availability configuration for the OP builder. + pub fn with_da_config(mut self, da_config: OpDAConfig) -> Self { + self.da_config = Some(da_config); + self + } + + /// Configure the gas limit configuration for the OP payload builder. + pub fn with_gas_limit_config(mut self, gas_limit_config: OpGasLimitConfig) -> Self { + self.gas_limit_config = Some(gas_limit_config); + self + } + + /// Configure if transaction conditional should be enabled. + pub const fn with_enable_tx_conditional(mut self, enable_tx_conditional: bool) -> Self { + self.enable_tx_conditional = enable_tx_conditional; + self + } + + /// Configure the minimum priority fee (tip) + pub const fn with_min_suggested_priority_fee(mut self, min: u64) -> Self { + self.min_suggested_priority_fee = min; + self + } + + /// Configures the endpoint for historical RPC forwarding. + pub fn with_historical_rpc(mut self, historical_rpc: Option) -> Self { + self.historical_rpc = historical_rpc; + self + } + + /// Configures a custom tokio runtime for the RPC server. + /// + /// Caution: This runtime must not be created from within asynchronous context. + pub fn with_tokio_runtime(mut self, tokio_runtime: Option) -> Self { + self.tokio_runtime = tokio_runtime; + self + } + + /// Configure the RPC middleware to use + pub fn with_rpc_middleware(self, rpc_middleware: T) -> BaseAddOnsBuilder { + let Self { + sequencer_url, + sequencer_headers, + historical_rpc, + da_config, + gas_limit_config, + enable_tx_conditional, + min_suggested_priority_fee, + tokio_runtime, + _nt, + flashblocks_url, + .. + } = self; + BaseAddOnsBuilder { + sequencer_url, + sequencer_headers, + historical_rpc, + da_config, + gas_limit_config, + enable_tx_conditional, + min_suggested_priority_fee, + _nt, + rpc_middleware, + tokio_runtime, + flashblocks_url, + } + } + + /// With a URL pointing to a flashblocks secure websocket subscription. + pub fn with_flashblocks(mut self, flashblocks_url: Option) -> Self { + self.flashblocks_url = flashblocks_url; + self + } +} + +impl BaseAddOnsBuilder { + /// Builds an instance of [`OpAddOns`]. + pub fn build( + self, + ) -> BaseAddOns, PVB, EB, EVB, RpcMiddleware> + where + N: FullNodeComponents, + OpEthApiBuilder: EthApiBuilder, + PVB: PayloadValidatorBuilder + Default, + EB: Default, + EVB: Default, + { + let Self { + sequencer_url, + sequencer_headers, + da_config, + gas_limit_config, + enable_tx_conditional, + min_suggested_priority_fee, + historical_rpc, + rpc_middleware, + tokio_runtime, + flashblocks_url, + .. + } = self; + + BaseAddOns::new( + RpcAddOns::new( + OpEthApiBuilder::default() + .with_sequencer(sequencer_url.clone()) + .with_sequencer_headers(sequencer_headers.clone()) + .with_min_suggested_priority_fee(min_suggested_priority_fee) + .with_flashblocks(flashblocks_url), + PVB::default(), + EB::default(), + EVB::default(), + rpc_middleware, + ) + .with_tokio_runtime(tokio_runtime), + da_config.unwrap_or_default(), + gas_limit_config.unwrap_or_default(), + sequencer_url, + sequencer_headers, + historical_rpc, + enable_tx_conditional, + min_suggested_priority_fee, + ) + } +} diff --git a/crates/client/node/src/runner.rs b/crates/client/node/src/runner.rs index dcca0009..631b0169 100644 --- a/crates/client/node/src/runner.rs +++ b/crates/client/node/src/runner.rs @@ -2,10 +2,11 @@ use eyre::Result; use reth::{ - builder::{EngineNodeLauncher, Node, NodeHandleFor, TreeConfig}, + builder::{EngineNodeLauncher, NodeHandleFor, TreeConfig}, providers::providers::BlockchainProvider, }; -use reth_optimism_node::{OpNode, args::RollupArgs}; +use crate::node::{BaseNode}; +use reth_optimism_node::args::RollupArgs; use tracing::info; use crate::{BaseNodeBuilder, BaseNodeExtension, BaseNodeHandle, FromExtensionConfig}; @@ -41,15 +42,18 @@ impl BaseNodeRunner { rollup_args: RollupArgs, extensions: Vec>, builder: BaseNodeBuilder, - ) -> Result> { + ) -> Result> { info!(target: "base-runner", "starting custom Base node"); - let op_node = OpNode::new(rollup_args); + let op_node = BaseNode::new(rollup_args); + + + let addons = op_node.add_ons_builder().build(); let builder = builder - .with_types_and_provider::>() + .with_types_and_provider::>() .with_components(op_node.components()) - .with_add_ons(op_node.add_ons()) + .with_add_ons(addons) .on_component_initialized(move |_ctx| Ok(())); let builder = diff --git a/crates/client/node/src/test_utils/node.rs b/crates/client/node/src/test_utils/node.rs index d8ce15c9..3a0498bc 100644 --- a/crates/client/node/src/test_utils/node.rs +++ b/crates/client/node/src/test_utils/node.rs @@ -21,7 +21,8 @@ use reth_node_core::{ dirs::{DataDirPath, MaybePlatformPath}, }; use reth_optimism_chainspec::OpChainSpec; -use reth_optimism_node::{OpNode, args::RollupArgs}; +use reth_optimism_node::{args::RollupArgs}; +use crate::node::BaseNode; use crate::{BaseNodeExtension, OpProvider, test_utils::engine::EngineApi}; @@ -82,7 +83,7 @@ impl LocalNode { RpcServerArgs::default().with_unused_ports().with_http().with_auth_ipc().with_ws(); rpc_args.auth_ipc_path = unique_ipc_path; - let op_node = OpNode::new(RollupArgs::default()); + let op_node = BaseNode::new(RollupArgs::default()); let (db, db_path) = Self::create_test_database()?; @@ -98,7 +99,7 @@ impl LocalNode { let builder = NodeBuilder::new(node_config.clone()) .with_database(db) .with_launch_context(exec.clone()) - .with_types_and_provider::>() + .with_types_and_provider::>() .with_components(op_node.components()) .with_add_ons(op_node.add_ons()) .on_component_initialized(move |_ctx| Ok(())); diff --git a/crates/client/node/src/types.rs b/crates/client/node/src/types.rs index b25f0c46..43c4a389 100644 --- a/crates/client/node/src/types.rs +++ b/crates/client/node/src/types.rs @@ -9,14 +9,14 @@ use reth::{ }; use reth_db::DatabaseEnv; use reth_optimism_chainspec::OpChainSpec; -use reth_optimism_node::OpNode; +use crate::node::BaseNode; -type OpNodeTypes = FullNodeTypesAdapter, OpProvider>; -type OpComponentsBuilder = >::ComponentsBuilder; -type OpAddOns = >::AddOns; +type OpNodeTypes = FullNodeTypesAdapter, OpProvider>; +type OpComponentsBuilder = >::ComponentsBuilder; +type OpAddOns = >::AddOns; /// A [`BlockchainProvider`] instance. -pub type OpProvider = BlockchainProvider>>; +pub type OpProvider = BlockchainProvider>>; /// OP Builder is a [`WithLaunchContext`] reth node builder. pub type OpBuilder =