Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### FEATURES

- Scripts can now generate setup for more than 4 nodes ([#136](https://github.com/informalsystems/emerald/pull/136))
- Height replay mechanism automatically recovers when Reth is behind Emerald's stored height after a crash, eliminating the need for `--engine.persistence-threshold=0`

### FIXES

Expand Down
148 changes: 145 additions & 3 deletions app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,106 @@ pub async fn initialize_state_from_genesis(state: &mut State, engine: &Engine) -
Ok(())
}

/// Replay blocks from Emerald's store to the execution client (Reth).
/// This is needed when Reth is behind Emerald's stored height after a crash.
async fn replay_heights_to_engine(
state: &State,
engine: &Engine,
start_height: Height,
end_height: Height,
emerald_config: &EmeraldConfig,
) -> eyre::Result<()> {
info!(
"🔄 Replaying heights {} to {} to execution client",
start_height, end_height
);

for height in start_height.as_u64()..=end_height.as_u64() {
let height = Height::new(height);

// Get the certificate and header from store
let (_certificate, header_bytes) = state
.store
.get_certificate_and_header(height)
.await?
.ok_or_eyre(format!("Missing certificate or header for height {height}"))?;

// Deserialize the execution payload
let execution_payload = ExecutionPayloadV3::from_ssz_bytes(&header_bytes).map_err(|e| {
eyre!(
"Failed to deserialize execution payload at height {}: {:?}",
height,
e
)
})?;

debug!(
"🔄 Replaying block at height {} with hash {:?}",
height, execution_payload.payload_inner.payload_inner.block_hash
);

// Extract versioned hashes from blob transactions
let block: Block = execution_payload.clone().try_into_block().map_err(|e| {
eyre!(
"Failed to convert execution payload to block at height {}: {}",
height,
e
)
})?;
let versioned_hashes: Vec<BlockHash> =
block.body.blob_versioned_hashes_iter().copied().collect();

// Submit the block to Reth
let payload_status = engine
.notify_new_block_with_retry(
execution_payload.clone(),
versioned_hashes,
&emerald_config.retry_config,
)
.await?;

// Verify the block was accepted
match payload_status.status {
PayloadStatusEnum::Valid => {
debug!("✅ Block at height {} replayed successfully", height);
}
PayloadStatusEnum::Invalid { validation_error } => {
return Err(eyre::eyre!(
"Block replay failed at height {}: {}",
height,
validation_error
));
}
PayloadStatusEnum::Accepted => {
// ACCEPTED is no instant finality and there is a possibility of a fork.
return Err(eyre::eyre!(
"Block replay failed at height {}: execution client returned ACCEPTED status, which is not supported during replay",
height
));
}
PayloadStatusEnum::Syncing => {
return Err(eyre::eyre!(
"Block replay failed at height {}: execution client still syncing",
height
));
}
}

// Update forkchoice to this block
engine
.set_latest_forkchoice_state(
execution_payload.payload_inner.payload_inner.block_hash,
&emerald_config.retry_config,
)
.await?;

debug!("🎯 Forkchoice updated to height {}", height);
}

info!("✅ Successfully replayed all heights to execution client");
Ok(())
}

pub async fn initialize_state_from_existing_block(
state: &mut State,
engine: &Engine,
Expand All @@ -61,6 +161,39 @@ pub async fn initialize_state_from_existing_block(
.await
.ok_or_eyre("we have not atomically stored the last block, database corrupted")?;

// Check if Reth is behind Emerald's stored height
let reth_latest_height = engine.get_latest_block_number().await?;

match reth_latest_height {
Some(reth_height) if reth_height < start_height.as_u64() => {
// Reth is behind - we need to replay blocks
warn!(
"⚠️ Execution client is at height {} but Emerald has blocks up to height {}. Starting height replay.",
reth_height, start_height
);

// Replay from Reth's next height to Emerald's stored height
let replay_start = Height::new(reth_height + 1);
replay_heights_to_engine(state, engine, replay_start, start_height, emerald_config)
.await?;

info!("✅ Height replay completed successfully");
}
Some(reth_height) => {
debug!(
"Execution client at height {} is aligned with or ahead of Emerald's stored height {}",
reth_height, start_height
);
}
None => {
// No blocks in Reth yet (genesis case) - this shouldn't happen here
// but handle it gracefully
warn!("⚠️ Execution client has no blocks, replaying from genesis");
replay_heights_to_engine(state, engine, Height::new(1), start_height, emerald_config)
.await?;
}
}

let payload_status = engine
.send_forkchoice_updated(
latest_block_candidate_from_store.block_hash,
Expand All @@ -81,13 +214,20 @@ pub async fn initialize_state_from_existing_block(
// requisite data for the validation is missing
debug!("Payload is valid");
debug!("latest block {:?}", state.latest_block);

// Read the validator set at the stored block - this is the validator set
// that will be active for the NEXT height (where consensus will start)
let block_validator_set = read_validators_from_contract(
engine.eth.url().as_ref(),
&latest_block_candidate_from_store.block_hash,
)
.await?;
debug!("🌈 Got block validator set: {:?}", block_validator_set);
state.set_validator_set(start_height, block_validator_set);

// Consensus will start at the next height, so we set the validator set for that height
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@jmalicevic I also fixed the code here. The validator set from the latest accepted block was stored for the wrong height (current instead of next), causing the code to fail later since the validator set for the next height is requested.

let next_height = start_height.increment();
debug!("🌈 Got validator set: {:?} for height {}", block_validator_set, next_height);
state.set_validator_set(next_height, block_validator_set);

Ok(())
}
PayloadStatusEnum::Invalid { validation_error } => Err(eyre::eyre!(validation_error)),
Expand Down Expand Up @@ -182,7 +322,9 @@ pub async fn run(
start_height,
state
.get_validator_set(start_height)
.ok_or_eyre("Validator set not found for start height {start_height}")?
.ok_or_eyre(format!(
"Validator set not found for start height {start_height}"
))?
.clone(),
))
.is_err()
Expand Down
7 changes: 0 additions & 7 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ services:
- "--discovery.port=31303"
- "--port=31303"
- "--nat=extip:127.0.0.1"
- "--engine.persistence-threshold=0"
# - "--builder.gaslimit=3600000000" # default * 100
# - "--builder.interval=10ms"
# - "--builder.deadline=1" # The deadline in seconds for when the payload builder job should resolve
Expand Down Expand Up @@ -87,7 +86,6 @@ services:
- "--discovery.port=32303"
- "--port=32303"
- "--nat=extip:127.0.0.1"
- "--engine.persistence-threshold=0"
# - "--builder.gaslimit=3600000000" # default * 100
# - "--builder.interval=10ms"
# - "--builder.deadline=1" # The deadline in seconds for when the payload builder job should resolve
Expand Down Expand Up @@ -127,7 +125,6 @@ services:
- "--discovery.port=33303"
- "--port=33303"
- "--nat=extip:127.0.0.1"
- "--engine.persistence-threshold=0"
# - "--builder.gaslimit=3600000000" # default * 100
# - "--builder.interval=10ms"
# - "--builder.deadline=1" # The deadline in seconds for when the payload builder job should resolve
Expand Down Expand Up @@ -167,7 +164,6 @@ services:
- "--discovery.port=34303"
- "--port=34303"
- "--nat=extip:127.0.0.1"
- "--engine.persistence-threshold=0"
# - "--builder.gaslimit=3600000000" # default * 100
# - "--builder.interval=10ms"
# - "--builder.deadline=1" # The deadline in seconds for when the payload builder job should resolve
Expand Down Expand Up @@ -207,7 +203,6 @@ services:
- "--discovery.port=35303"
- "--port=35303"
- "--nat=extip:127.0.0.1"
- "--engine.persistence-threshold=0"
# - "--builder.gaslimit=3600000000" # default * 100
# - "--builder.interval=10ms"
# - "--builder.deadline=1" # The deadline in seconds for when the payload builder job should resolve
Expand Down Expand Up @@ -247,7 +242,6 @@ services:
- "--discovery.port=36303"
- "--port=36303"
- "--nat=extip:127.0.0.1"
- "--engine.persistence-threshold=0"
# - "--builder.gaslimit=3600000000" # default * 100
# - "--builder.interval=10ms"
# - "--builder.deadline=1" # The deadline in seconds for when the payload builder job should resolve
Expand Down Expand Up @@ -287,7 +281,6 @@ services:
- "--discovery.port=37303"
- "--port=37303"
- "--nat=extip:127.0.0.1"
- "--engine.persistence-threshold=0"
# - "--builder.gaslimit=3600000000" # default * 100
# - "--builder.interval=10ms"
# - "--builder.deadline=1" # The deadline in seconds for when the payload builder job should resolve
Expand Down
9 changes: 9 additions & 0 deletions engine/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,15 @@ impl Engine {
}
}

/// Get the latest block number from the execution client.
/// Returns None if the client has no blocks (genesis case).
pub async fn get_latest_block_number(&self) -> eyre::Result<Option<u64>> {
debug!("🟠 get_latest_block_number");

let block = self.eth.get_block_by_number("latest").await?;
Ok(block.map(|b| b.block_number))
}

/// Returns the duration since the unix epoch.
fn _timestamp_now(&self) -> u64 {
SystemTime::now()
Expand Down