Skip to content

Commit 9f748bc

Browse files
tac0turtleclaude
andcommitted
feat: implement custom consensus for Rollkit to allow same timestamps
This implements a custom consensus wrapper that allows multiple blocks to have the same timestamp, which is required for Rollkit's operation. The key changes: - Add RollkitConsensus that wraps EthBeaconConsensus - Override validate_header_against_parent to allow header.timestamp >= parent.timestamp - Integrate with node builder using RollkitConsensusBuilder - Add comprehensive unit tests for timestamp validation This addresses issue #15 where Rollkit needs to submit multiple blocks with the same timestamp. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 561077e commit 9f748bc

9 files changed

Lines changed: 364 additions & 7 deletions

File tree

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,12 @@ reth-rpc-engine-api = { git = "https://github.com/paradigmxyz/reth.git", version
6767
reth_ethereum_primitives = { git = "https://github.com/paradigmxyz/reth.git", version = "1.5.0" }
6868

6969

70-
# Test dependencies
70+
# Consensus dependencies
7171
reth-consensus = { git = "https://github.com/paradigmxyz/reth.git", version = "1.5.0", default-features = false }
72+
reth-consensus-common = { git = "https://github.com/paradigmxyz/reth.git", version = "1.5.0", default-features = false }
73+
reth-ethereum-consensus = { git = "https://github.com/paradigmxyz/reth.git", version = "1.5.0", default-features = false }
74+
75+
# Test dependencies
7276
reth-testing-utils = { git = "https://github.com/paradigmxyz/reth.git", version = "1.5.0", default-features = false }
7377
reth-db = { git = "https://github.com/paradigmxyz/reth.git", version = "1.5.0", default-features = false }
7478
reth-tasks = { git = "https://github.com/paradigmxyz/reth.git", version = "1.5.0", default-features = false }

bin/lumen/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ reth-cli-util.workspace = true
2323
reth-ethereum-cli.workspace = true
2424
reth-ethereum = { workspace = true, features = ["node", "cli", "pool"] }
2525
reth-node-builder.workspace = true
26+
reth-node-api.workspace = true
2627
reth-chainspec.workspace = true
2728
reth-primitives-traits.workspace = true
2829
reth-engine-local.workspace = true
@@ -32,6 +33,8 @@ reth-payload-builder.workspace = true
3233
reth-revm.workspace = true
3334
reth-provider.workspace = true
3435
reth-trie-db.workspace = true
36+
reth-consensus.workspace = true
37+
reth-ethereum-primitives.workspace = true
3538

3639
# Alloy dependencies
3740
alloy-network.workspace = true

bin/lumen/src/consensus_builder.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//! Rollkit consensus builder implementation
2+
3+
use lumen_rollkit::consensus::RollkitConsensus;
4+
use reth_chainspec::ChainSpec;
5+
use reth_consensus::{ConsensusError, FullConsensus};
6+
use reth_ethereum::node::builder::{components::ConsensusBuilder, BuilderContext};
7+
use reth_ethereum_primitives::EthPrimitives;
8+
use reth_node_api::{FullNodeTypes, NodeTypes};
9+
use std::sync::Arc;
10+
11+
/// Builder for `RollkitConsensus` that implements the `ConsensusBuilder` trait
12+
#[derive(Debug, Default, Clone)]
13+
pub struct RollkitConsensusBuilder;
14+
15+
impl RollkitConsensusBuilder {
16+
/// Create a new instance
17+
pub const fn new() -> Self {
18+
Self
19+
}
20+
}
21+
22+
impl<Node> ConsensusBuilder<Node> for RollkitConsensusBuilder
23+
where
24+
Node: FullNodeTypes,
25+
Node::Types: NodeTypes<ChainSpec = ChainSpec, Primitives = EthPrimitives>,
26+
{
27+
type Consensus = Arc<dyn FullConsensus<EthPrimitives, Error = ConsensusError>>;
28+
29+
async fn build_consensus(self, ctx: &BuilderContext<Node>) -> eyre::Result<Self::Consensus> {
30+
Ok(Arc::new(RollkitConsensus::new(ctx.chain_spec())) as Self::Consensus)
31+
}
32+
}

bin/lumen/src/main.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
pub mod attributes;
99
pub mod builder;
10+
pub mod consensus_builder;
1011
pub mod error;
1112
pub mod validator;
1213

@@ -28,10 +29,7 @@ use reth_ethereum::{
2829
rpc::RpcAddOns,
2930
Node, NodeAdapter, NodeComponentsBuilder,
3031
},
31-
node::{
32-
EthereumConsensusBuilder, EthereumExecutorBuilder, EthereumNetworkBuilder,
33-
EthereumPoolBuilder,
34-
},
32+
node::{EthereumExecutorBuilder, EthereumNetworkBuilder, EthereumPoolBuilder},
3533
EthereumEthApiBuilder,
3634
},
3735
primitives::SealedBlock,
@@ -45,6 +43,7 @@ use tracing::info;
4543
use crate::{
4644
attributes::{RollkitEnginePayloadAttributes, RollkitEnginePayloadBuilderAttributes},
4745
builder::{RollkitArgs, RollkitPayloadBuilderBuilder},
46+
consensus_builder::RollkitConsensusBuilder,
4847
validator::RollkitEngineValidatorBuilder,
4948
};
5049

@@ -127,7 +126,7 @@ where
127126
BasicPayloadServiceBuilder<RollkitPayloadBuilderBuilder>,
128127
EthereumNetworkBuilder,
129128
EthereumExecutorBuilder,
130-
EthereumConsensusBuilder,
129+
RollkitConsensusBuilder,
131130
>;
132131
type AddOns = RollkitNodeAddOns<
133132
NodeAdapter<N, <Self::ComponentsBuilder as NodeComponentsBuilder<N>>::Components>,
@@ -142,7 +141,7 @@ where
142141
RollkitPayloadBuilderBuilder::new(&self.args),
143142
))
144143
.network(EthereumNetworkBuilder::default())
145-
.consensus(EthereumConsensusBuilder::default())
144+
.consensus(RollkitConsensusBuilder)
146145
}
147146

148147
fn add_ons(&self) -> Self::AddOns {

crates/rollkit/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ reth-primitives.workspace = true
1515
reth-primitives-traits.workspace = true
1616
reth-engine-primitives.workspace = true
1717
reth-transaction-pool.workspace = true
18+
reth-consensus.workspace = true
19+
reth-consensus-common.workspace = true
20+
reth-ethereum-consensus.workspace = true
21+
reth-chainspec.workspace = true
22+
reth-node-api.workspace = true
23+
reth-ethereum.workspace = true
24+
reth-ethereum-primitives.workspace = true
25+
reth-execution-types.workspace = true
1826

1927
# Alloy dependencies
2028
alloy-rpc-types-engine.workspace = true
@@ -31,6 +39,7 @@ async-trait.workspace = true
3139
jsonrpsee = { workspace = true, features = ["server", "macros"] }
3240
jsonrpsee-core.workspace = true
3341
jsonrpsee-proc-macros.workspace = true
42+
eyre.workspace = true
3443

3544
[dev-dependencies]
3645
serde_json.workspace = true

crates/rollkit/src/consensus.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//! Rollkit custom consensus implementation that allows same timestamps across blocks.
2+
3+
use reth_chainspec::ChainSpec;
4+
use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator};
5+
use reth_consensus_common::validation::validate_body_against_header;
6+
use reth_ethereum_consensus::EthBeaconConsensus;
7+
use reth_ethereum_primitives::{Block, BlockBody, EthPrimitives, Receipt};
8+
use reth_execution_types::BlockExecutionResult;
9+
use reth_primitives::{GotExpected, GotExpectedBoxed, RecoveredBlock, SealedBlock, SealedHeader};
10+
use std::sync::Arc;
11+
12+
/// Rollkit consensus implementation that allows blocks with the same timestamp.
13+
///
14+
/// This consensus implementation wraps the standard Ethereum beacon consensus
15+
/// but modifies the timestamp validation to allow multiple blocks to have the
16+
/// same timestamp, which is required for Rollkit's operation.
17+
#[derive(Debug, Clone)]
18+
pub struct RollkitConsensus {
19+
/// Inner Ethereum beacon consensus for standard validation
20+
inner: EthBeaconConsensus<ChainSpec>,
21+
}
22+
23+
impl RollkitConsensus {
24+
/// Create a new Rollkit consensus instance
25+
pub const fn new(chain_spec: Arc<ChainSpec>) -> Self {
26+
let inner = EthBeaconConsensus::new(chain_spec);
27+
Self { inner }
28+
}
29+
}
30+
31+
impl HeaderValidator for RollkitConsensus {
32+
fn validate_header(&self, header: &SealedHeader) -> Result<(), ConsensusError> {
33+
// Use inner consensus for basic header validation
34+
self.inner.validate_header(header)
35+
}
36+
37+
fn validate_header_against_parent(
38+
&self,
39+
header: &SealedHeader,
40+
parent: &SealedHeader,
41+
) -> Result<(), ConsensusError> {
42+
// Custom validation that allows same timestamps
43+
// This is the key difference from standard Ethereum consensus
44+
45+
// First validate parent hash and number
46+
if header.parent_hash != parent.hash() {
47+
return Err(ConsensusError::ParentHashMismatch(GotExpectedBoxed(
48+
Box::new(GotExpected {
49+
got: header.parent_hash,
50+
expected: parent.hash(),
51+
}),
52+
)));
53+
}
54+
55+
if header.number != parent.number + 1 {
56+
return Err(ConsensusError::ParentBlockNumberMismatch {
57+
parent_block_number: parent.number,
58+
block_number: header.number,
59+
});
60+
}
61+
62+
// ROLLKIT MODIFICATION: Allow same timestamp
63+
// Standard Ethereum requires: header.timestamp > parent.timestamp
64+
// Rollkit allows: header.timestamp >= parent.timestamp
65+
if header.timestamp < parent.timestamp {
66+
return Err(ConsensusError::TimestampIsInPast {
67+
parent_timestamp: parent.timestamp,
68+
timestamp: header.timestamp,
69+
});
70+
}
71+
// NOTE: We explicitly do NOT check for header.timestamp == parent.timestamp
72+
// as an error, which is the main change for Rollkit
73+
74+
// For all other validations, delegate to the inner consensus
75+
// but skip it when timestamps are equal since the inner consensus
76+
// would reject this case
77+
if header.timestamp == parent.timestamp {
78+
// Timestamps are equal, which we explicitly allow for Rollkit
79+
// Skip the inner consensus validation that would reject this
80+
Ok(())
81+
} else {
82+
// Timestamps are different, so we can safely delegate to inner consensus
83+
self.inner.validate_header_against_parent(header, parent)
84+
}
85+
}
86+
}
87+
88+
impl Consensus<Block> for RollkitConsensus {
89+
type Error = ConsensusError;
90+
91+
fn validate_body_against_header(
92+
&self,
93+
body: &BlockBody,
94+
header: &SealedHeader,
95+
) -> Result<(), Self::Error> {
96+
validate_body_against_header(body, header.header())
97+
}
98+
99+
fn validate_block_pre_execution(&self, block: &SealedBlock) -> Result<(), Self::Error> {
100+
// Use inner consensus for pre-execution validation
101+
self.inner.validate_block_pre_execution(block)
102+
}
103+
}
104+
105+
impl FullConsensus<EthPrimitives> for RollkitConsensus {
106+
fn validate_block_post_execution(
107+
&self,
108+
block: &RecoveredBlock<Block>,
109+
result: &BlockExecutionResult<Receipt>,
110+
) -> Result<(), ConsensusError> {
111+
<EthBeaconConsensus<ChainSpec> as FullConsensus<EthPrimitives>>::validate_block_post_execution(&self.inner, block, result)
112+
}
113+
}
114+
115+
/// Builder for `RollkitConsensus`
116+
#[derive(Debug, Default, Clone)]
117+
#[non_exhaustive]
118+
pub struct RollkitConsensusBuilder;
119+
120+
impl RollkitConsensusBuilder {
121+
/// Create a new `RollkitConsensusBuilder`
122+
pub const fn new() -> Self {
123+
Self
124+
}
125+
}
126+
127+
// Instead of implementing the generic ConsensusBuilder trait,
128+
// we'll use a direct approach that works with the node builder
129+
impl RollkitConsensusBuilder {
130+
/// Build the consensus implementation
131+
pub fn build(chain_spec: Arc<ChainSpec>) -> Arc<RollkitConsensus> {
132+
Arc::new(RollkitConsensus::new(chain_spec))
133+
}
134+
}

crates/rollkit/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! This crate provides Rollkit-specific functionality including:
44
//! - Custom payload attributes for Rollkit
55
//! - Rollkit-specific types and traits
6+
//! - Custom consensus implementation
67
78
/// Rollkit-specific types and related definitions.
89
pub mod types;
@@ -13,9 +14,13 @@ pub mod config;
1314
/// RPC modules for Rollkit functionality.
1415
pub mod rpc;
1516

17+
/// Custom consensus implementation for Rollkit.
18+
pub mod consensus;
19+
1620
#[cfg(test)]
1721
mod tests;
1822

1923
// Re-export public types
2024
pub use config::{RollkitConfig, DEFAULT_MAX_TXPOOL_BYTES};
25+
pub use consensus::{RollkitConsensus, RollkitConsensusBuilder};
2126
pub use types::{PayloadAttributesError, RollkitPayloadAttributes};

0 commit comments

Comments
 (0)