Skip to content

Commit f4a7694

Browse files
committed
feat(chain): implement genesis block initialization and validation
- Added functionality in ChainActor to initialize the genesis block if it doesn't exist during startup, ensuring all nodes share the same starting state for consensus. - Introduced a new module for genesis block creation, which queries the execution layer for block #0 and wraps it in a ConsensusBlock. - Enhanced validation logic to ensure that block #1 references the genesis block correctly, preventing issues with parent hash mismatches. - Updated handlers to query the genesis block from storage when no chain head is found, improving block production reliability.
1 parent b42e72c commit f4a7694

File tree

8 files changed

+491
-6
lines changed

8 files changed

+491
-6
lines changed

app/src/actors_v2/chain/actor.rs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,12 +517,91 @@ impl ChainActor {
517517
impl Actor for ChainActor {
518518
type Context = Context<Self>;
519519

520-
fn started(&mut self, _ctx: &mut Context<Self>) {
520+
fn started(&mut self, ctx: &mut Context<Self>) {
521521
info!(
522522
"ChainActor V2 started - is_validator: {}",
523523
self.config.is_validator
524524
);
525525
self.record_activity();
526+
527+
// Initialize genesis block if it doesn't exist
528+
// This is critical for consensus - all nodes must share the same genesis
529+
let storage = self.storage_actor.clone();
530+
let engine = self.engine_actor.clone();
531+
532+
// Construct ChainSpec from state (Aura has authorities and slot_duration)
533+
let chain_spec = crate::spec::ChainSpec {
534+
slot_duration: self.state.aura.slot_duration,
535+
authorities: self.state.aura.authorities.clone(),
536+
federation: self.state.federation.clone(),
537+
federation_bitcoin_pubkeys: Vec::new(), // Not needed for genesis
538+
bits: self.state.retarget_params.pow_limit,
539+
chain_id: self.config.chain_id,
540+
max_blocks_without_pow: self.state.max_blocks_without_pow,
541+
bitcoin_start_height: 0, // Not relevant for genesis
542+
retarget_params: self.state.retarget_params.clone(),
543+
is_validator: self.state.is_validator,
544+
execution_timeout_length: 8, // Default value
545+
required_btc_txn_confirmations: 6, // Default value
546+
};
547+
548+
ctx.spawn(
549+
async move {
550+
// Check if genesis already exists
551+
if let (Some(storage_actor), Some(engine_actor)) = (storage, engine) {
552+
match super::genesis::genesis_exists(&storage_actor).await {
553+
Ok(true) => {
554+
info!("Genesis block already exists in storage");
555+
}
556+
Ok(false) => {
557+
info!("Genesis block not found - creating from execution layer");
558+
559+
// Create genesis block from execution layer
560+
match super::genesis::create_genesis_block(&engine_actor, chain_spec)
561+
.await
562+
{
563+
Ok(genesis) => {
564+
let genesis_hash = genesis.canonical_root();
565+
info!(
566+
consensus_hash = %genesis_hash,
567+
block_number = genesis.message.execution_payload.block_number,
568+
"Genesis block created successfully"
569+
);
570+
571+
// Store genesis block
572+
let store_msg =
573+
crate::actors_v2::storage::messages::StoreBlockMessage {
574+
block: genesis.clone(),
575+
canonical: true, // Genesis is always canonical
576+
correlation_id: Some(Uuid::new_v4()),
577+
};
578+
579+
if let Err(e) = storage_actor.send(store_msg).await {
580+
error!(
581+
error = ?e,
582+
"Failed to send genesis block to storage actor"
583+
);
584+
} else {
585+
info!("Genesis block stored successfully");
586+
}
587+
}
588+
Err(e) => {
589+
error!(error = ?e, "Failed to create genesis block");
590+
// Don't panic - this is a recoverable error
591+
// Node can sync genesis from peers if needed
592+
}
593+
}
594+
}
595+
Err(e) => {
596+
warn!(error = ?e, "Failed to check for genesis block existence");
597+
}
598+
}
599+
} else {
600+
warn!("Storage or Engine actor not set - skipping genesis initialization");
601+
}
602+
}
603+
.into_actor(self),
604+
);
526605
}
527606

528607
fn stopped(&mut self, _ctx: &mut Context<Self>) {

app/src/actors_v2/chain/genesis.rs

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
//! Genesis block creation for V2 consensus layer
2+
//!
3+
//! This module handles creating the genesis block (height 0) by querying
4+
//! the execution layer for block #0 and wrapping it in a ConsensusBlock.
5+
//!
6+
//! The genesis block serves as the common foundation that all validator nodes
7+
//! share, ensuring consensus starts from the same state.
8+
9+
use crate::actors_v2::chain::ChainError;
10+
use crate::actors_v2::engine::EngineActor;
11+
use crate::block::SignedConsensusBlock;
12+
use crate::spec::ChainSpec;
13+
use actix::Addr;
14+
use lighthouse_wrapper::types::MainnetEthSpec;
15+
use tracing::{debug, info};
16+
17+
/// Create genesis block by querying execution layer for block #0
18+
///
19+
/// This function:
20+
/// 1. Queries the execution layer (Reth/Geth) for block #0
21+
/// 2. Wraps the execution payload in a ConsensusBlock structure
22+
/// 3. Returns a genesis block ready for storage
23+
///
24+
/// # Genesis Block Properties
25+
/// - Height: 0
26+
/// - Slot: 0
27+
/// - Parent hash: 0x0000...0000 (genesis has no parent)
28+
/// - Execution payload: Retrieved from execution layer block #0
29+
/// - Signature: Empty (genesis is not signed by any authority)
30+
///
31+
/// # Determinism
32+
/// All nodes using the same genesis.json will produce identical genesis blocks
33+
/// because the execution layer's block #0 is deterministically generated from
34+
/// the genesis.json configuration.
35+
///
36+
/// # Arguments
37+
/// * `engine_actor` - Address of the EngineActor for querying execution layer
38+
/// * `chain_spec` - Chain specification (authorities, slot duration, etc.)
39+
///
40+
/// # Returns
41+
/// - `Ok(SignedConsensusBlock)` - Genesis block ready for storage
42+
/// - `Err(ChainError)` - If execution layer query fails
43+
///
44+
/// # Errors
45+
/// Returns `ChainError::Engine` if:
46+
/// - Cannot communicate with EngineActor
47+
/// - Execution layer doesn't have block #0
48+
/// - Execution payload is invalid
49+
///
50+
pub async fn create_genesis_block(
51+
engine_actor: &Addr<EngineActor>,
52+
chain_spec: ChainSpec,
53+
) -> Result<SignedConsensusBlock<MainnetEthSpec>, ChainError> {
54+
info!("Creating genesis block from execution layer");
55+
56+
// Query execution layer for block #0
57+
// We use the GetPayloadByTag message which accepts "0x0" or "earliest"
58+
let get_genesis_msg = crate::actors_v2::engine::messages::EngineMessage::GetPayloadByTag {
59+
block_tag: "0x0".to_string(), // Query block #0 (genesis)
60+
correlation_id: Some(uuid::Uuid::new_v4()),
61+
};
62+
63+
debug!("Querying execution layer for block #0");
64+
65+
let execution_payload = match engine_actor.send(get_genesis_msg).await {
66+
Ok(Ok(crate::actors_v2::engine::messages::EngineResponse::PayloadByTag { payload })) => {
67+
// Extract the Capella payload
68+
match payload {
69+
lighthouse_wrapper::types::ExecutionPayload::Capella(capella_payload) => {
70+
info!(
71+
block_number = capella_payload.block_number,
72+
block_hash = %capella_payload.block_hash,
73+
"Retrieved execution layer block #0"
74+
);
75+
capella_payload
76+
}
77+
_ => {
78+
return Err(ChainError::Engine(
79+
"Expected Capella execution payload for genesis".to_string(),
80+
));
81+
}
82+
}
83+
}
84+
Ok(Ok(_)) => {
85+
return Err(ChainError::Engine(
86+
"Unexpected response type from EngineActor".to_string(),
87+
));
88+
}
89+
Ok(Err(e)) => {
90+
return Err(ChainError::Engine(format!(
91+
"Execution layer failed to provide block #0: {}",
92+
e
93+
)));
94+
}
95+
Err(e) => {
96+
return Err(ChainError::NetworkError(format!(
97+
"Failed to communicate with EngineActor: {}",
98+
e
99+
)));
100+
}
101+
};
102+
103+
// Validate that we actually got block #0
104+
if execution_payload.block_number != 0 {
105+
return Err(ChainError::InvalidBlock(format!(
106+
"Expected block #0 from execution layer, got block #{}",
107+
execution_payload.block_number
108+
)));
109+
}
110+
111+
// Wrap execution payload in a ConsensusBlock
112+
let genesis = SignedConsensusBlock::genesis(chain_spec, execution_payload);
113+
114+
let genesis_hash = genesis.canonical_root();
115+
let genesis_exec_hash = genesis.message.execution_payload.block_hash;
116+
117+
info!(
118+
consensus_hash = %genesis_hash,
119+
execution_hash = %genesis_exec_hash,
120+
"Genesis block created successfully"
121+
);
122+
123+
Ok(genesis)
124+
}
125+
126+
/// Check if genesis block exists in storage
127+
///
128+
/// Helper function to determine if genesis has already been initialized.
129+
/// Used during ChainActor startup to decide whether to create genesis.
130+
///
131+
/// # Arguments
132+
/// * `storage_actor` - Address of the StorageActor
133+
///
134+
/// # Returns
135+
/// - `Ok(true)` - Genesis block exists in storage
136+
/// - `Ok(false)` - Genesis block does not exist
137+
/// - `Err(ChainError)` - Communication or query error
138+
///
139+
pub async fn genesis_exists(
140+
storage_actor: &Addr<crate::actors_v2::storage::StorageActor>,
141+
) -> Result<bool, ChainError> {
142+
let get_genesis_msg = crate::actors_v2::storage::messages::GetBlockByHeightMessage {
143+
height: 0,
144+
correlation_id: Some(uuid::Uuid::new_v4()),
145+
};
146+
147+
match storage_actor.send(get_genesis_msg).await {
148+
Ok(Ok(Some(_))) => Ok(true),
149+
Ok(Ok(None)) => Ok(false),
150+
Ok(Err(e)) => Err(ChainError::Storage(format!(
151+
"Failed to query genesis from storage: {}",
152+
e
153+
))),
154+
Err(e) => Err(ChainError::NetworkError(format!(
155+
"Failed to communicate with StorageActor: {}",
156+
e
157+
))),
158+
}
159+
}
160+
161+
#[cfg(test)]
162+
mod tests {
163+
use super::*;
164+
use crate::aura::Authority;
165+
use lighthouse_wrapper::bls::Keypair;
166+
167+
#[test]
168+
fn test_genesis_has_zero_height() {
169+
use crate::block::ConsensusBlock;
170+
171+
let block = ConsensusBlock::default();
172+
let keypair = Keypair::random();
173+
let authority = Authority {
174+
signer: keypair.clone(),
175+
index: 0,
176+
};
177+
178+
// Create a signed block with default values
179+
let signed_block = block.sign_block(&authority);
180+
181+
// Default ConsensusBlock should have height 0
182+
assert_eq!(
183+
signed_block.message.execution_payload.block_number, 0,
184+
"Default block should have height 0"
185+
);
186+
}
187+
188+
#[test]
189+
fn test_genesis_has_zero_parent_hash() {
190+
use crate::block::ConsensusBlock;
191+
use ethereum_types::H256;
192+
193+
let block = ConsensusBlock::default();
194+
let keypair = Keypair::random();
195+
let authority = Authority {
196+
signer: keypair.clone(),
197+
index: 0,
198+
};
199+
200+
let signed_block = block.sign_block(&authority);
201+
202+
// Genesis parent hash should be zero
203+
assert_eq!(
204+
signed_block.message.parent_hash,
205+
H256::zero(),
206+
"Genesis block should have zero parent hash"
207+
);
208+
}
209+
}

app/src/actors_v2/chain/handlers.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,57 @@ impl Handler<ChainMessage> for ChainActor {
137137
(head_ref.execution_hash, head_ref.hash)
138138
}
139139
Ok(None) => {
140-
info!(correlation_id = %correlation_id, "No chain head found - producing genesis block (parent_hash will be None for Engine)");
141-
(lighthouse_wrapper::types::ExecutionBlockHash::zero(), lighthouse_wrapper::types::Hash256::zero())
140+
info!(correlation_id = %correlation_id, "No chain head found - querying genesis for parent hashes");
141+
142+
// Query genesis block (height 0) to get proper parent hashes
143+
let get_genesis_msg = crate::actors_v2::storage::messages::GetBlockByHeightMessage {
144+
height: 0,
145+
correlation_id: Some(correlation_id),
146+
};
147+
148+
match storage_actor.send(get_genesis_msg).await {
149+
Ok(Ok(Some(genesis))) => {
150+
let genesis_hash = genesis.canonical_root();
151+
let genesis_exec_hash = genesis.message.execution_payload.block_hash;
152+
153+
info!(
154+
correlation_id = %correlation_id,
155+
genesis_consensus_hash = %genesis_hash,
156+
genesis_execution_hash = %genesis_exec_hash,
157+
"Using genesis block as parent for block #1"
158+
);
159+
160+
(genesis_exec_hash, genesis_hash)
161+
}
162+
Ok(Ok(None)) => {
163+
error!(
164+
correlation_id = %correlation_id,
165+
"Genesis block not found in storage - cannot produce blocks"
166+
);
167+
return Err(ChainError::InvalidState(
168+
"Cannot produce blocks without genesis - wait for ChainActor genesis initialization".to_string()
169+
));
170+
}
171+
Ok(Err(e)) => {
172+
error!(
173+
correlation_id = %correlation_id,
174+
error = ?e,
175+
"Failed to query genesis from storage"
176+
);
177+
return Err(ChainError::Storage(e.to_string()));
178+
}
179+
Err(e) => {
180+
error!(
181+
correlation_id = %correlation_id,
182+
error = ?e,
183+
"Communication error querying genesis"
184+
);
185+
return Err(ChainError::NetworkError(format!(
186+
"Genesis query communication failed: {}",
187+
e
188+
)));
189+
}
190+
}
142191
}
143192
Err(e) => {
144193
error!(correlation_id = %correlation_id, error = ?e, "Failed to get chain head");

app/src/actors_v2/chain/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub mod withdrawals;
2828
// Phase 4 production hardening modules
2929
pub mod auxpow;
3030
pub mod fork_choice;
31+
pub mod genesis;
3132
pub mod monitoring;
3233
pub mod recovery;
3334
pub mod reorganization;

0 commit comments

Comments
 (0)