diff --git a/bin/lumen/src/attributes.rs b/bin/lumen/src/attributes.rs new file mode 100644 index 0000000..89ee665 --- /dev/null +++ b/bin/lumen/src/attributes.rs @@ -0,0 +1,113 @@ +use alloy_eips::{eip4895::Withdrawals, Decodable2718}; +use alloy_primitives::{Address, Bytes, B256}; +use alloy_rpc_types::{ + engine::{PayloadAttributes as EthPayloadAttributes, PayloadId}, + Withdrawal, +}; +use reth_engine_local::payload::UnsupportedLocalAttributes; +use reth_ethereum::{ + node::api::payload::{PayloadAttributes, PayloadBuilderAttributes}, + TransactionSigned, +}; +use reth_payload_builder::EthPayloadBuilderAttributes; +use serde::{Deserialize, Serialize}; + +use crate::error::RollkitEngineError; + +/// Rollkit payload attributes that support passing transactions via Engine API +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct RollkitEnginePayloadAttributes { + /// Standard Ethereum payload attributes + #[serde(flatten)] + pub inner: EthPayloadAttributes, + /// Transactions to be included in the payload (passed via Engine API) + pub transactions: Option>, + /// Optional gas limit for the payload + #[serde(rename = "gasLimit")] + pub gas_limit: Option, +} + +impl UnsupportedLocalAttributes for RollkitEnginePayloadAttributes {} + +impl PayloadAttributes for RollkitEnginePayloadAttributes { + fn timestamp(&self) -> u64 { + self.inner.timestamp() + } + + fn withdrawals(&self) -> Option<&Vec> { + self.inner.withdrawals() + } + + fn parent_beacon_block_root(&self) -> Option { + self.inner.parent_beacon_block_root() + } +} + +/// Rollkit payload builder attributes +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RollkitEnginePayloadBuilderAttributes { + /// Ethereum payload builder attributes + pub ethereum_attributes: EthPayloadBuilderAttributes, + /// Decoded transactions from the Engine API + pub transactions: Vec, + /// Gas limit for the payload + pub gas_limit: Option, +} + +impl PayloadBuilderAttributes for RollkitEnginePayloadBuilderAttributes { + type RpcPayloadAttributes = RollkitEnginePayloadAttributes; + type Error = RollkitEngineError; + + fn try_new( + parent: B256, + attributes: RollkitEnginePayloadAttributes, + _version: u8, + ) -> Result { + let ethereum_attributes = EthPayloadBuilderAttributes::new(parent, attributes.inner); + + // Decode transactions from bytes if provided + let transactions = attributes + .transactions + .unwrap_or_default() + .into_iter() + .map(|tx_bytes| { + TransactionSigned::network_decode(&mut tx_bytes.as_ref()) + .map_err(|e| RollkitEngineError::InvalidTransactionData(e.to_string())) + }) + .collect::, _>>()?; + + Ok(Self { + ethereum_attributes, + transactions, + gas_limit: attributes.gas_limit, + }) + } + + fn payload_id(&self) -> PayloadId { + self.ethereum_attributes.id + } + + fn parent(&self) -> B256 { + self.ethereum_attributes.parent + } + + fn timestamp(&self) -> u64 { + self.ethereum_attributes.timestamp + } + + fn parent_beacon_block_root(&self) -> Option { + self.ethereum_attributes.parent_beacon_block_root + } + + fn suggested_fee_recipient(&self) -> Address { + self.ethereum_attributes.suggested_fee_recipient + } + + fn prev_randao(&self) -> B256 { + self.ethereum_attributes.prev_randao + } + + fn withdrawals(&self) -> &Withdrawals { + &self.ethereum_attributes.withdrawals + } +} diff --git a/bin/lumen/src/builder.rs b/bin/lumen/src/builder.rs new file mode 100644 index 0000000..c94d8e7 --- /dev/null +++ b/bin/lumen/src/builder.rs @@ -0,0 +1,232 @@ +use alloy_primitives::U256; +use clap::Parser; +use lumen_node::{RollkitPayloadBuilder, RollkitPayloadBuilderConfig}; +use lumen_rollkit::RollkitPayloadAttributes; +use reth_basic_payload_builder::{ + BuildArguments, BuildOutcome, HeaderForPayload, PayloadBuilder, PayloadConfig, +}; +use reth_ethereum::{ + chainspec::{ChainSpec, ChainSpecProvider}, + node::{ + api::{payload::PayloadBuilderAttributes, FullNodeTypes, NodeTypes}, + builder::{components::PayloadBuilderBuilder, BuilderContext}, + EthEvmConfig, + }, + pool::{PoolTransaction, TransactionPool}, + primitives::Header, + TransactionSigned, +}; +use reth_payload_builder::{EthBuiltPayload, PayloadBuilderError}; +use reth_provider::HeaderProvider; +use reth_revm::cached::CachedReads; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tracing::info; + +use crate::{attributes::RollkitEnginePayloadBuilderAttributes, RollkitEngineTypes}; + +/// Rollkit-specific command line arguments +#[derive(Debug, Clone, Parser, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct RollkitArgs { + /// Enable Rollkit mode for the node (enabled by default) + #[arg( + long = "rollkit.enable", + default_value = "true", + help = "Enable Rollkit integration for transaction processing via Engine API" + )] + pub enable_rollkit: bool, +} + +/// Rollkit payload service builder that integrates with the rollkit payload builder +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct RollkitPayloadBuilderBuilder { + config: RollkitPayloadBuilderConfig, +} + +impl RollkitPayloadBuilderBuilder { + /// Create a new builder with rollkit args + pub fn new(_args: &RollkitArgs) -> Self { + let config = RollkitPayloadBuilderConfig { + max_transactions: 1000, + min_gas_price: 1_000_000_000, // 1 Gwei + }; + info!("Created Rollkit payload builder with config: {:?}", config); + Self { config } + } +} + +impl Default for RollkitPayloadBuilderBuilder { + fn default() -> Self { + Self::new(&RollkitArgs::default()) + } +} + +/// The rollkit engine payload builder that integrates with the rollkit payload builder +#[derive(Debug, Clone)] +pub struct RollkitEnginePayloadBuilder +where + Pool: Clone, + Client: Clone, +{ + pub(crate) rollkit_builder: Arc>, + #[allow(dead_code)] + pub(crate) pool: Pool, + #[allow(dead_code)] + pub(crate) config: RollkitPayloadBuilderConfig, +} + +impl PayloadBuilderBuilder for RollkitPayloadBuilderBuilder +where + Node: FullNodeTypes< + Types: NodeTypes< + Payload = RollkitEngineTypes, + ChainSpec = ChainSpec, + Primitives = reth_ethereum::EthPrimitives, + >, + >, + Pool: TransactionPool> + + Unpin + + 'static, +{ + type PayloadBuilder = RollkitEnginePayloadBuilder; + + async fn build_payload_builder( + self, + ctx: &BuilderContext, + pool: Pool, + evm_config: EthEvmConfig, + ) -> eyre::Result { + let rollkit_builder = Arc::new(RollkitPayloadBuilder::new( + Arc::new(ctx.provider().clone()), + evm_config, + )); + + Ok(RollkitEnginePayloadBuilder { + rollkit_builder, + pool, + config: self.config, + }) + } +} + +impl PayloadBuilder for RollkitEnginePayloadBuilder +where + Client: reth_ethereum::provider::StateProviderFactory + + ChainSpecProvider + + HeaderProvider
+ + Clone + + Send + + Sync + + 'static, + Pool: TransactionPool>, +{ + type Attributes = RollkitEnginePayloadBuilderAttributes; + type BuiltPayload = EthBuiltPayload; + + fn try_build( + &self, + args: BuildArguments, + ) -> Result, PayloadBuilderError> { + let BuildArguments { + cached_reads: _, + config, + cancel: _, + best_payload, + } = args; + let PayloadConfig { + parent_header, + attributes, + } = config; + + info!( + "Rollkit engine payload builder: building payload with {} transactions", + attributes.transactions.len() + ); + + // Convert Engine API attributes to Rollkit payload attributes + let rollkit_attrs = RollkitPayloadAttributes::new( + attributes.transactions.clone(), + attributes.gas_limit, + attributes.timestamp(), + attributes.prev_randao(), + attributes.suggested_fee_recipient(), + attributes.parent(), + parent_header.number + 1, + ); + + // Build the payload using the rollkit payload builder - use spawn_blocking for async work + let rollkit_builder = self.rollkit_builder.clone(); + let sealed_block = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(rollkit_builder.build_payload(rollkit_attrs)) + }) + .map_err(PayloadBuilderError::other)?; + + info!( + "Rollkit engine payload builder: built block with {} transactions, gas used: {}", + sealed_block.transaction_count(), + sealed_block.gas_used + ); + + // Convert to EthBuiltPayload + let gas_used = sealed_block.gas_used; + let built_payload = EthBuiltPayload::new( + attributes.payload_id(), // Use the proper payload ID from attributes + Arc::new(sealed_block), + U256::from(gas_used), // Block gas used + None, // No blob sidecar for rollkit + ); + + if let Some(best) = best_payload { + if built_payload.fees() <= best.fees() { + return Ok(BuildOutcome::Aborted { + fees: built_payload.fees(), + cached_reads: CachedReads::default(), + }); + } + } + + Ok(BuildOutcome::Better { + payload: built_payload, + cached_reads: CachedReads::default(), + }) + } + + fn build_empty_payload( + &self, + config: PayloadConfig>, + ) -> Result { + let PayloadConfig { + parent_header, + attributes, + } = config; + + info!("Rollkit engine payload builder: building empty payload"); + + // Create empty rollkit attributes (no transactions) + let rollkit_attrs = RollkitPayloadAttributes::new( + vec![], + attributes.gas_limit, + attributes.timestamp(), + attributes.prev_randao(), + attributes.suggested_fee_recipient(), + attributes.parent(), + parent_header.number + 1, + ); + + // Build empty payload - use spawn_blocking for async work + let rollkit_builder = self.rollkit_builder.clone(); + let sealed_block = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(rollkit_builder.build_payload(rollkit_attrs)) + }) + .map_err(PayloadBuilderError::other)?; + + let gas_used = sealed_block.gas_used; + Ok(EthBuiltPayload::new( + attributes.payload_id(), + Arc::new(sealed_block), + U256::from(gas_used), + None, + )) + } +} diff --git a/bin/lumen/src/error.rs b/bin/lumen/src/error.rs new file mode 100644 index 0000000..dc51212 --- /dev/null +++ b/bin/lumen/src/error.rs @@ -0,0 +1,13 @@ +use lumen_rollkit::PayloadAttributesError; +use thiserror::Error; + +/// Custom error type used in payload attributes validation +#[derive(Debug, Error)] +pub enum RollkitEngineError { + #[error("Invalid transaction data: {0}")] + InvalidTransactionData(String), + #[error("Gas limit exceeded")] + GasLimitExceeded, + #[error("Rollkit payload attributes error: {0}")] + PayloadAttributes(#[from] PayloadAttributesError), +} diff --git a/bin/lumen/src/main.rs b/bin/lumen/src/main.rs index 31f3199..8f2caf4 100644 --- a/bin/lumen/src/main.rs +++ b/bin/lumen/src/main.rs @@ -5,182 +5,48 @@ #![allow(missing_docs, rustdoc::missing_crate_level_docs)] -use alloy_eips::{eip2718::Decodable2718, eip4895::Withdrawals}; -use alloy_primitives::{Address, Bytes, B256, U256}; -use alloy_rpc_types::{ - engine::{ - ExecutionData, ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, - ExecutionPayloadEnvelopeV4, ExecutionPayloadEnvelopeV5, ExecutionPayloadV1, - PayloadAttributes as EthPayloadAttributes, PayloadId, - }, - Withdrawal, +pub mod attributes; +pub mod builder; +pub mod error; +pub mod validator; + +use alloy_rpc_types::engine::{ + ExecutionData, ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, + ExecutionPayloadEnvelopeV4, ExecutionPayloadEnvelopeV5, ExecutionPayloadV1, }; use clap::Parser; -use lumen_node::{RollkitPayloadBuilder, RollkitPayloadBuilderConfig}; -use lumen_rollkit::{PayloadAttributesError, RollkitPayloadAttributes}; -use reth_basic_payload_builder::{ - BuildArguments, BuildOutcome, HeaderForPayload, PayloadBuilder, PayloadConfig, -}; -use reth_engine_local::payload::UnsupportedLocalAttributes; use reth_ethereum::{ - chainspec::{ChainSpec, ChainSpecProvider}, + chainspec::ChainSpec, node::{ - api::{ - payload::{EngineApiMessageVersion, EngineObjectValidationError, PayloadOrAttributes}, - validate_version_specific_fields, AddOnsContext, EngineTypes, EngineValidator, - FullNodeComponents, FullNodeTypes, InvalidPayloadAttributesError, NewPayloadError, - NodeTypes, PayloadAttributes, PayloadBuilderAttributes, PayloadTypes, PayloadValidator, - }, + api::{EngineTypes, FullNodeTypes, NodeTypes, PayloadTypes}, builder::{ - components::{BasicPayloadServiceBuilder, ComponentsBuilder, PayloadBuilderBuilder}, - rpc::{EngineValidatorBuilder, RpcAddOns}, - BuilderContext, Node, NodeAdapter, NodeComponentsBuilder, + components::{BasicPayloadServiceBuilder, ComponentsBuilder}, + rpc::RpcAddOns, + Node, NodeAdapter, NodeComponentsBuilder, }, node::{ EthereumConsensusBuilder, EthereumExecutorBuilder, EthereumNetworkBuilder, EthereumPoolBuilder, }, - EthEvmConfig, EthereumEthApiBuilder, + EthereumEthApiBuilder, }, - pool::{PoolTransaction, TransactionPool}, - primitives::{Header, RecoveredBlock, SealedBlock}, - TransactionSigned, + primitives::SealedBlock, }; use reth_ethereum_cli::{chainspec::EthereumChainSpecParser, Cli}; -use reth_ethereum_payload_builder::EthereumExecutionPayloadValidator; -use reth_payload_builder::{EthBuiltPayload, EthPayloadBuilderAttributes, PayloadBuilderError}; -use reth_provider::HeaderProvider; -use reth_revm::cached::CachedReads; +use reth_payload_builder::EthBuiltPayload; use reth_trie_db::MerklePatriciaTrie; use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use thiserror::Error; use tracing::info; +use crate::{ + attributes::{RollkitEnginePayloadAttributes, RollkitEnginePayloadBuilderAttributes}, + builder::{RollkitArgs, RollkitPayloadBuilderBuilder}, + validator::RollkitEngineValidatorBuilder, +}; + #[global_allocator] static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator(); -/// Rollkit-specific command line arguments -#[derive(Debug, Clone, Parser, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct RollkitArgs { - /// Enable Rollkit mode for the node (enabled by default) - #[arg( - long = "rollkit.enable", - default_value = "true", - help = "Enable Rollkit integration for transaction processing via Engine API" - )] - pub enable_rollkit: bool, -} - -/// Rollkit payload attributes that support passing transactions via Engine API -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct RollkitEnginePayloadAttributes { - /// Standard Ethereum payload attributes - #[serde(flatten)] - pub inner: EthPayloadAttributes, - /// Transactions to be included in the payload (passed via Engine API) - pub transactions: Option>, - /// Optional gas limit for the payload - #[serde(rename = "gasLimit")] - pub gas_limit: Option, -} - -impl UnsupportedLocalAttributes for RollkitEnginePayloadAttributes {} - -/// Custom error type used in payload attributes validation -#[derive(Debug, Error)] -pub enum RollkitEngineError { - #[error("Invalid transaction data: {0}")] - InvalidTransactionData(String), - #[error("Gas limit exceeded")] - GasLimitExceeded, - #[error("Rollkit payload attributes error: {0}")] - PayloadAttributes(#[from] PayloadAttributesError), -} - -impl PayloadAttributes for RollkitEnginePayloadAttributes { - fn timestamp(&self) -> u64 { - self.inner.timestamp() - } - - fn withdrawals(&self) -> Option<&Vec> { - self.inner.withdrawals() - } - - fn parent_beacon_block_root(&self) -> Option { - self.inner.parent_beacon_block_root() - } -} - -/// Rollkit payload builder attributes -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RollkitEnginePayloadBuilderAttributes { - /// Ethereum payload builder attributes - pub ethereum_attributes: EthPayloadBuilderAttributes, - /// Decoded transactions from the Engine API - pub transactions: Vec, - /// Gas limit for the payload - pub gas_limit: Option, -} - -impl PayloadBuilderAttributes for RollkitEnginePayloadBuilderAttributes { - type RpcPayloadAttributes = RollkitEnginePayloadAttributes; - type Error = RollkitEngineError; - - fn try_new( - parent: B256, - attributes: RollkitEnginePayloadAttributes, - _version: u8, - ) -> Result { - let ethereum_attributes = EthPayloadBuilderAttributes::new(parent, attributes.inner); - - // Decode transactions from bytes if provided - let mut transactions = Vec::new(); - if let Some(tx_bytes_vec) = attributes.transactions { - for tx_bytes in tx_bytes_vec { - // Decode the transaction from RLP bytes (as sent by Go client) - let tx = TransactionSigned::network_decode(&mut tx_bytes.as_ref()) - .map_err(|e| RollkitEngineError::InvalidTransactionData(e.to_string()))?; - transactions.push(tx); - } - } - - Ok(Self { - ethereum_attributes, - transactions, - gas_limit: attributes.gas_limit, - }) - } - - fn payload_id(&self) -> PayloadId { - self.ethereum_attributes.id - } - - fn parent(&self) -> B256 { - self.ethereum_attributes.parent - } - - fn timestamp(&self) -> u64 { - self.ethereum_attributes.timestamp - } - - fn parent_beacon_block_root(&self) -> Option { - self.ethereum_attributes.parent_beacon_block_root - } - - fn suggested_fee_recipient(&self) -> Address { - self.ethereum_attributes.suggested_fee_recipient - } - - fn prev_randao(&self) -> B256 { - self.ethereum_attributes.prev_randao - } - - fn withdrawals(&self) -> &Withdrawals { - &self.ethereum_attributes.withdrawals - } -} - /// Rollkit engine types - uses custom payload attributes that support transactions #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[non_exhaustive] @@ -214,139 +80,6 @@ impl EngineTypes for RollkitEngineTypes { type ExecutionPayloadEnvelopeV5 = ExecutionPayloadEnvelopeV5; } -/// Rollkit engine validator that handles custom payload validation -#[derive(Debug, Clone)] -pub struct RollkitEngineValidator { - inner: EthereumExecutionPayloadValidator, -} - -impl RollkitEngineValidator { - /// Instantiates a new validator. - pub const fn new(chain_spec: Arc) -> Self { - Self { - inner: EthereumExecutionPayloadValidator::new(chain_spec), - } - } - - /// Returns the chain spec used by the validator. - #[inline] - fn chain_spec(&self) -> &ChainSpec { - self.inner.chain_spec().as_ref() - } -} - -impl PayloadValidator for RollkitEngineValidator { - type Block = reth_ethereum::Block; - type ExecutionData = ExecutionData; - - fn ensure_well_formed_payload( - &self, - payload: ExecutionData, - ) -> Result, NewPayloadError> { - info!("Rollkit engine validator: validating payload"); - - // Use inner validator but with custom rollkit handling - match self.inner.ensure_well_formed_payload(payload.clone()) { - Ok(sealed_block) => { - info!("Rollkit engine validator: payload validation succeeded"); - sealed_block - .try_recover() - .map_err(|e| NewPayloadError::Other(e.into())) - } - Err(err) => { - // Log the error for debugging - tracing::debug!("Rollkit payload validation error: {:?}", err); - - // Check if this is a block hash mismatch error - bypass it for rollkit - if matches!(err, alloy_rpc_types::engine::PayloadError::BlockHash { .. }) { - info!("Rollkit engine validator: bypassing block hash mismatch for rollkit"); - // For rollkit, we trust the payload builder - just parse the block without hash validation - use reth_primitives_traits::Block; - let ExecutionData { payload, sidecar } = payload; - let sealed_block = payload.try_into_block_with_sidecar(&sidecar)?.seal_slow(); - sealed_block - .try_recover() - .map_err(|e| NewPayloadError::Other(e.into())) - } else { - // For other errors, re-throw them - Err(NewPayloadError::Eth(err)) - } - } - } - } -} - -impl EngineValidator for RollkitEngineValidator -where - T: PayloadTypes< - PayloadAttributes = RollkitEnginePayloadAttributes, - ExecutionData = ExecutionData, - >, -{ - fn validate_version_specific_fields( - &self, - version: EngineApiMessageVersion, - payload_or_attrs: PayloadOrAttributes<'_, Self::ExecutionData, T::PayloadAttributes>, - ) -> Result<(), EngineObjectValidationError> { - validate_version_specific_fields(self.chain_spec(), version, payload_or_attrs) - } - - fn ensure_well_formed_attributes( - &self, - version: EngineApiMessageVersion, - attributes: &T::PayloadAttributes, - ) -> Result<(), EngineObjectValidationError> { - validate_version_specific_fields( - self.chain_spec(), - version, - PayloadOrAttributes::::PayloadAttributes( - attributes, - ), - )?; - - // Validate rollkit-specific attributes - if let Some(ref transactions) = attributes.transactions { - info!( - "Rollkit engine validator: validating {} transactions", - transactions.len() - ); - } - - Ok(()) - } - - fn validate_payload_attributes_against_header( - &self, - _attr: &::PayloadAttributes, - _header: &::Header, - ) -> Result<(), InvalidPayloadAttributesError> { - // Skip default timestamp validation for rollkit - Ok(()) - } -} - -/// Rollkit engine validator builder -#[derive(Debug, Default, Clone, Copy)] -#[non_exhaustive] -pub struct RollkitEngineValidatorBuilder; - -impl EngineValidatorBuilder for RollkitEngineValidatorBuilder -where - N: FullNodeComponents< - Types: NodeTypes< - Payload = RollkitEngineTypes, - ChainSpec = ChainSpec, - Primitives = reth_ethereum::EthPrimitives, - >, - >, -{ - type Validator = RollkitEngineValidator; - - async fn build(self, ctx: &AddOnsContext<'_, N>) -> eyre::Result { - Ok(RollkitEngineValidator::new(ctx.config.chain.clone())) - } -} - /// Rollkit node type #[derive(Debug, Clone, Default)] #[non_exhaustive] @@ -413,200 +146,6 @@ where } } -/// Rollkit payload service builder that integrates with the rollkit payload builder -#[derive(Debug, Clone)] -#[non_exhaustive] -pub struct RollkitPayloadBuilderBuilder { - config: RollkitPayloadBuilderConfig, -} - -impl RollkitPayloadBuilderBuilder { - /// Create a new builder with rollkit args - pub fn new(_args: &RollkitArgs) -> Self { - let config = RollkitPayloadBuilderConfig { - max_transactions: 1000, - min_gas_price: 1_000_000_000, // 1 Gwei - }; - info!("Created Rollkit payload builder with config: {:?}", config); - Self { config } - } -} - -impl Default for RollkitPayloadBuilderBuilder { - fn default() -> Self { - Self::new(&RollkitArgs::default()) - } -} - -/// The rollkit engine payload builder that integrates with the rollkit payload builder -#[derive(Debug, Clone)] -pub struct RollkitEnginePayloadBuilder -where - Pool: Clone, - Client: Clone, -{ - rollkit_builder: Arc>, - #[allow(dead_code)] - pool: Pool, - #[allow(dead_code)] - config: RollkitPayloadBuilderConfig, -} - -impl PayloadBuilder for RollkitEnginePayloadBuilder -where - Client: reth_ethereum::provider::StateProviderFactory - + ChainSpecProvider - + HeaderProvider
- + Clone - + Send - + Sync - + 'static, - Pool: TransactionPool>, -{ - type Attributes = RollkitEnginePayloadBuilderAttributes; - type BuiltPayload = EthBuiltPayload; - - fn try_build( - &self, - args: BuildArguments, - ) -> Result, PayloadBuilderError> { - let BuildArguments { - cached_reads: _, - config, - cancel: _, - best_payload, - } = args; - let PayloadConfig { - parent_header, - attributes, - } = config; - - info!( - "Rollkit engine payload builder: building payload with {} transactions", - attributes.transactions.len() - ); - - // Convert Engine API attributes to Rollkit payload attributes - let rollkit_attrs = RollkitPayloadAttributes::new( - attributes.transactions.clone(), - attributes.gas_limit, - attributes.timestamp(), - attributes.prev_randao(), - attributes.suggested_fee_recipient(), - attributes.parent(), - parent_header.number + 1, - ); - - // Build the payload using the rollkit payload builder - use spawn_blocking for async work - let rollkit_builder = self.rollkit_builder.clone(); - let sealed_block = tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(rollkit_builder.build_payload(rollkit_attrs)) - }) - .map_err(PayloadBuilderError::other)?; - - info!( - "Rollkit engine payload builder: built block with {} transactions, gas used: {}", - sealed_block.transaction_count(), - sealed_block.gas_used - ); - - // Convert to EthBuiltPayload - let gas_used = sealed_block.gas_used; - let built_payload = EthBuiltPayload::new( - attributes.payload_id(), // Use the proper payload ID from attributes - Arc::new(sealed_block), - U256::from(gas_used), // Block gas used - None, // No blob sidecar for rollkit - ); - - if let Some(best) = best_payload { - if built_payload.fees() <= best.fees() { - return Ok(BuildOutcome::Aborted { - fees: built_payload.fees(), - cached_reads: CachedReads::default(), - }); - } - } - - Ok(BuildOutcome::Better { - payload: built_payload, - cached_reads: CachedReads::default(), - }) - } - - fn build_empty_payload( - &self, - config: PayloadConfig>, - ) -> Result { - let PayloadConfig { - parent_header, - attributes, - } = config; - - info!("Rollkit engine payload builder: building empty payload"); - - // Create empty rollkit attributes (no transactions) - let rollkit_attrs = RollkitPayloadAttributes::new( - vec![], - attributes.gas_limit, - attributes.timestamp(), - attributes.prev_randao(), - attributes.suggested_fee_recipient(), - attributes.parent(), - parent_header.number + 1, - ); - - // Build empty payload - use spawn_blocking for async work - let rollkit_builder = self.rollkit_builder.clone(); - let sealed_block = tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(rollkit_builder.build_payload(rollkit_attrs)) - }) - .map_err(PayloadBuilderError::other)?; - - let gas_used = sealed_block.gas_used; - Ok(EthBuiltPayload::new( - attributes.payload_id(), //TODO: make sure this works - Arc::new(sealed_block), - U256::from(gas_used), - None, - )) - } -} - -impl PayloadBuilderBuilder for RollkitPayloadBuilderBuilder -where - Node: FullNodeTypes< - Types: NodeTypes< - Payload = RollkitEngineTypes, - ChainSpec = ChainSpec, - Primitives = reth_ethereum::EthPrimitives, - >, - >, - Pool: TransactionPool> - + Unpin - + 'static, -{ - type PayloadBuilder = RollkitEnginePayloadBuilder; - - async fn build_payload_builder( - self, - ctx: &BuilderContext, - pool: Pool, - evm_config: EthEvmConfig, - ) -> eyre::Result { - let rollkit_builder = Arc::new(RollkitPayloadBuilder::new( - Arc::new(ctx.provider().clone()), - evm_config, - )); - - Ok(RollkitEnginePayloadBuilder { - rollkit_builder, - pool, - config: self.config, - }) - } -} - fn main() { info!("=== ROLLKIT-RETH NODE STARTING ==="); diff --git a/bin/lumen/src/validator.rs b/bin/lumen/src/validator.rs new file mode 100644 index 0000000..8eb4f9f --- /dev/null +++ b/bin/lumen/src/validator.rs @@ -0,0 +1,155 @@ +#![allow(missing_docs, rustdoc::missing_crate_level_docs)] + +use alloy_rpc_types::engine::ExecutionData; + +use reth_ethereum::{ + chainspec::ChainSpec, + node::{ + api::{ + payload::{EngineApiMessageVersion, EngineObjectValidationError, PayloadOrAttributes}, + validate_version_specific_fields, AddOnsContext, EngineValidator, FullNodeComponents, + InvalidPayloadAttributesError, NewPayloadError, NodeTypes, PayloadTypes, + PayloadValidator, + }, + builder::rpc::EngineValidatorBuilder, + }, + primitives::RecoveredBlock, +}; +use reth_ethereum_payload_builder::EthereumExecutionPayloadValidator; +use std::sync::Arc; +use tracing::info; + +use crate::{attributes::RollkitEnginePayloadAttributes, RollkitEngineTypes}; + +/// Rollkit engine validator that handles custom payload validation +#[derive(Debug, Clone)] +pub struct RollkitEngineValidator { + inner: EthereumExecutionPayloadValidator, +} + +impl RollkitEngineValidator { + /// Instantiates a new validator. + pub const fn new(chain_spec: Arc) -> Self { + Self { + inner: EthereumExecutionPayloadValidator::new(chain_spec), + } + } + + /// Returns the chain spec used by the validator. + #[inline] + fn chain_spec(&self) -> &ChainSpec { + self.inner.chain_spec().as_ref() + } +} + +impl PayloadValidator for RollkitEngineValidator { + type Block = reth_ethereum::Block; + type ExecutionData = ExecutionData; + + fn ensure_well_formed_payload( + &self, + payload: ExecutionData, + ) -> Result, NewPayloadError> { + info!("Rollkit engine validator: validating payload"); + + // Use inner validator but with custom rollkit handling + match self.inner.ensure_well_formed_payload(payload.clone()) { + Ok(sealed_block) => { + info!("Rollkit engine validator: payload validation succeeded"); + sealed_block + .try_recover() + .map_err(|e| NewPayloadError::Other(e.into())) + } + Err(err) => { + // Log the error for debugging + tracing::debug!("Rollkit payload validation error: {:?}", err); + + // Check if this is a block hash mismatch error - bypass it for rollkit + if matches!(err, alloy_rpc_types::engine::PayloadError::BlockHash { .. }) { + info!("Rollkit engine validator: bypassing block hash mismatch for rollkit"); + // For rollkit, we trust the payload builder - just parse the block without hash validation + use reth_primitives_traits::Block; + let ExecutionData { payload, sidecar } = payload; + let sealed_block = payload.try_into_block_with_sidecar(&sidecar)?.seal_slow(); + sealed_block + .try_recover() + .map_err(|e| NewPayloadError::Other(e.into())) + } else { + // For other errors, re-throw them + Err(NewPayloadError::Eth(err)) + } + } + } + } +} + +impl EngineValidator for RollkitEngineValidator +where + T: PayloadTypes< + PayloadAttributes = RollkitEnginePayloadAttributes, + ExecutionData = ExecutionData, + >, +{ + fn validate_version_specific_fields( + &self, + version: EngineApiMessageVersion, + payload_or_attrs: PayloadOrAttributes<'_, Self::ExecutionData, T::PayloadAttributes>, + ) -> Result<(), EngineObjectValidationError> { + validate_version_specific_fields(self.chain_spec(), version, payload_or_attrs) + } + + fn ensure_well_formed_attributes( + &self, + version: EngineApiMessageVersion, + attributes: &T::PayloadAttributes, + ) -> Result<(), EngineObjectValidationError> { + validate_version_specific_fields( + self.chain_spec(), + version, + PayloadOrAttributes::::PayloadAttributes( + attributes, + ), + )?; + + // Validate rollkit-specific attributes + if let Some(ref transactions) = attributes.transactions { + info!( + "Rollkit engine validator: validating {} transactions", + transactions.len() + ); + } + + Ok(()) + } + + fn validate_payload_attributes_against_header( + &self, + _attr: &::PayloadAttributes, + _header: &::Header, + ) -> Result<(), InvalidPayloadAttributesError> { + // Skip default timestamp validation for rollkit + Ok(()) + } +} + +/// Rollkit engine validator builder +#[derive(Debug, Default, Clone, Copy)] +#[non_exhaustive] +pub struct RollkitEngineValidatorBuilder; + +impl EngineValidatorBuilder for RollkitEngineValidatorBuilder +where + N: FullNodeComponents< + Types: NodeTypes< + Payload = RollkitEngineTypes, + ChainSpec = ChainSpec, + Primitives = reth_ethereum::EthPrimitives, + >, + >, +{ + type Validator = RollkitEngineValidator; + + async fn build(self, ctx: &AddOnsContext<'_, N>) -> eyre::Result { + Ok(RollkitEngineValidator::new(ctx.config.chain.clone())) + } +}