Skip to content
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
113 changes: 113 additions & 0 deletions bin/lumen/src/attributes.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<Bytes>>,
/// Optional gas limit for the payload
#[serde(rename = "gasLimit")]
pub gas_limit: Option<u64>,
}

impl UnsupportedLocalAttributes for RollkitEnginePayloadAttributes {}

impl PayloadAttributes for RollkitEnginePayloadAttributes {
fn timestamp(&self) -> u64 {
self.inner.timestamp()
}

fn withdrawals(&self) -> Option<&Vec<Withdrawal>> {
self.inner.withdrawals()
}

fn parent_beacon_block_root(&self) -> Option<B256> {
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<TransactionSigned>,
/// Gas limit for the payload
pub gas_limit: Option<u64>,
}

impl PayloadBuilderAttributes for RollkitEnginePayloadBuilderAttributes {
type RpcPayloadAttributes = RollkitEnginePayloadAttributes;
type Error = RollkitEngineError;

fn try_new(
parent: B256,
attributes: RollkitEnginePayloadAttributes,
_version: u8,
) -> Result<Self, Self::Error> {
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::<Result<Vec<_>, _>>()?;

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<B256> {
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
}
}
232 changes: 232 additions & 0 deletions bin/lumen/src/builder.rs
Original file line number Diff line number Diff line change
@@ -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<Pool, Client>
where
Pool: Clone,
Client: Clone,
{
pub(crate) rollkit_builder: Arc<RollkitPayloadBuilder<Client>>,
#[allow(dead_code)]
pub(crate) pool: Pool,
#[allow(dead_code)]
pub(crate) config: RollkitPayloadBuilderConfig,
Comment on lines +73 to +76
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The pool and config fields in RollkitEnginePayloadBuilder are marked with #[allow(dead_code)] and appear to be unused. If these fields are not intended for future use, consider removing them to simplify the struct and its associated code.

}

impl<Node, Pool> PayloadBuilderBuilder<Node, Pool, EthEvmConfig> for RollkitPayloadBuilderBuilder
where
Node: FullNodeTypes<
Types: NodeTypes<
Payload = RollkitEngineTypes,
ChainSpec = ChainSpec,
Primitives = reth_ethereum::EthPrimitives,
>,
>,
Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>
+ Unpin
+ 'static,
{
type PayloadBuilder = RollkitEnginePayloadBuilder<Pool, Node::Provider>;

async fn build_payload_builder(
self,
ctx: &BuilderContext<Node>,
pool: Pool,
evm_config: EthEvmConfig,
) -> eyre::Result<Self::PayloadBuilder> {
let rollkit_builder = Arc::new(RollkitPayloadBuilder::new(
Arc::new(ctx.provider().clone()),
evm_config,
));

Ok(RollkitEnginePayloadBuilder {
rollkit_builder,
pool,
config: self.config,
})
}
}

impl<Pool, Client> PayloadBuilder for RollkitEnginePayloadBuilder<Pool, Client>
where
Client: reth_ethereum::provider::StateProviderFactory
+ ChainSpecProvider<ChainSpec = ChainSpec>
+ HeaderProvider<Header = Header>
+ Clone
+ Send
+ Sync
+ 'static,
Pool: TransactionPool<Transaction: PoolTransaction<Consensus = TransactionSigned>>,
{
type Attributes = RollkitEnginePayloadBuilderAttributes;
type BuiltPayload = EthBuiltPayload;

fn try_build(
&self,
args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
) -> Result<BuildOutcome<Self::BuiltPayload>, 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<Self::Attributes, HeaderForPayload<Self::BuiltPayload>>,
) -> Result<Self::BuiltPayload, PayloadBuilderError> {
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,
))
}
}
13 changes: 13 additions & 0 deletions bin/lumen/src/error.rs
Original file line number Diff line number Diff line change
@@ -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),
}
Loading