Skip to content

Commit

Permalink
Merge pull request #600 from chainbound/feat/nethermind-payload-building
Browse files Browse the repository at this point in the history
feat: build valid fallback payload with Nethermind
  • Loading branch information
merklefruit authored Dec 19, 2024
2 parents 28a17b0 + 60ffbee commit 90ec3c4
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 44 deletions.
12 changes: 8 additions & 4 deletions bolt-sidecar/src/builder/fallback/engine_hinter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ use crate::{

use super::engine_hints::parse_hint_from_engine_response;

/// The maximum number of hint iterations to try before giving up.
const MAX_HINT_ITERATIONS: u64 = 20;

/// The [EngineHinter] is responsible for gathering "hints" from the
/// engine API error responses to complete the sealed block.
///
Expand Down Expand Up @@ -58,7 +61,6 @@ impl EngineHinter {

// Loop until we get a valid payload from the engine API. On each iteration,
// we build a new block header with the hints from the context and fetch the next hint.
let max_iterations = 20;
let mut iteration = 0;
loop {
debug!(%iteration, "Fetching hint from engine API");
Expand All @@ -76,6 +78,7 @@ impl EngineHinter {

// attempt to fetch the next hint from the engine API payload response
let hint = self.next_hint(exec_payload, &ctx).await?;
debug!(?hint, "Received hint from engine API");

if matches!(hint, EngineApiHint::ValidPayload) {
return Ok(sealed_block);
Expand All @@ -85,8 +88,8 @@ impl EngineHinter {
ctx.hints.populate_new(hint);

iteration += 1;
if iteration >= max_iterations {
return Err(BuilderError::ExceededMaxHintIterations(max_iterations));
if iteration >= MAX_HINT_ITERATIONS {
return Err(BuilderError::ExceededMaxHintIterations(MAX_HINT_ITERATIONS));
}
}
}
Expand Down Expand Up @@ -121,7 +124,8 @@ impl EngineHinter {
// Parse the hint from the engine API response, based on the EL client code
let Some(hint) = parse_hint_from_engine_response(ctx.el_client_code, &validation_error)?
else {
return Err(BuilderError::FailedToParseHintsFromEngine);
let el_name = ctx.el_client_code.client_name().to_string();
return Err(BuilderError::FailedToParseHintsFromEngine(el_name));
};

Ok(hint)
Expand Down
11 changes: 7 additions & 4 deletions bolt-sidecar/src/builder/fallback/engine_hints/geth.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
use alloy::primitives::{Bloom, B256};
use hex::FromHex;
use lazy_static::lazy_static;
use regex::Regex;

use crate::builder::{fallback::engine_hinter::EngineApiHint, BuilderError};

lazy_static! {
/// Capture either the "local" or "got" value from the error message
static ref REGEX: Regex = Regex::new(r"(?:local:|got) ([0-9a-zA-Z]+)").expect("valid regex");
}

/// Parse a hinted value from the engine response.
/// An example error message from the engine API looks like this:
///
Expand All @@ -22,10 +28,7 @@ use crate::builder::{fallback::engine_hinter::EngineApiHint, BuilderError};
/// - [ValidateState](<https://github.com/ethereum/go-ethereum/blob/9298d2db884c4e3f9474880e3dcfd080ef9eacfa/core/block_validator.go#L122-L151>)
/// - [Blockhash Mismatch](<https://github.com/ethereum/go-ethereum/blob/9298d2db884c4e3f9474880e3dcfd080ef9eacfa/beacon/engine/types.go#L253-L256>)
pub fn parse_geth_engine_error_hint(error: &str) -> Result<Option<EngineApiHint>, BuilderError> {
// Capture either the "local" or "got" value from the error message
let re = Regex::new(r"(?:local:|got) ([0-9a-zA-Z]+)").expect("valid regex");

let raw_hint_value = match re.captures(error).and_then(|cap| cap.get(1)) {
let raw_hint_value = match REGEX.captures(error).and_then(|cap| cap.get(1)) {
Some(matched) => matched.as_str().to_string(),
None => return Ok(None),
};
Expand Down
4 changes: 2 additions & 2 deletions bolt-sidecar/src/builder/fallback/engine_hints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ pub fn parse_hint_from_engine_response(
) -> Result<Option<EngineApiHint>, BuilderError> {
match client {
ClientCode::GE => geth::parse_geth_engine_error_hint(error),
// TODO: Add Nethermind engine hints parsing
// ClientCode::NM => nethermind::parse_nethermind_engine_error_hint(error),
ClientCode::NM => nethermind::parse_nethermind_engine_error_hint(error),

_ => {
error!("Unsupported fallback execution client: {}", client.client_name());
Err(BuilderError::UnsupportedEngineClient(client))
Expand Down
33 changes: 27 additions & 6 deletions bolt-sidecar/src/builder/fallback/engine_hints/nethermind.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
use tracing::warn;
use alloy::primitives::{Bloom, B256};
use hex::FromHex;
use lazy_static::lazy_static;
use regex::Regex;

use crate::builder::{fallback::engine_hinter::EngineApiHint, BuilderError};

lazy_static! {
/// Capture the "got" value from the error message
static ref REGEX: Regex = Regex::new(r"got ([0-9a-zA-Z]+)").expect("valid regex");
}

/// Parse a hinted value from the engine response.
/// An example error message from the engine API looks like this:
///
Expand All @@ -11,17 +19,30 @@ use crate::builder::{fallback::engine_hinter::EngineApiHint, BuilderError};
/// "id": 1,
/// "error": {
/// "code":-32000,
/// "message": "local: blockhash mismatch: got 0x... expected 0x..."
/// "message": "HeaderGasUsedMismatch: Gas used in header does not match calculated. Expected 0, got 21000"
/// }
/// }
/// ```
// TODO: implement hints parsing
// TODO: remove dead_code attribute
#[allow(dead_code)]
pub fn parse_nethermind_engine_error_hint(
error: &str,
) -> Result<Option<EngineApiHint>, BuilderError> {
warn!(%error, "Nethermind engine error hint parsing is not implemented");
let raw_hint_value = match REGEX.captures(error).and_then(|cap| cap.get(1)) {
Some(matched) => matched.as_str().to_string(),
None => return Ok(None),
};

// Match the hint value to the corresponding hint type based on other parts of the error message
if error.contains("InvalidHeaderHash") {
return Ok(Some(EngineApiHint::BlockHash(B256::from_hex(raw_hint_value)?)));
} else if error.contains("HeaderGasUsedMismatch") {
return Ok(Some(EngineApiHint::GasUsed(raw_hint_value.parse()?)));
} else if error.contains("InvalidStateRoot") {
return Ok(Some(EngineApiHint::StateRoot(B256::from_hex(raw_hint_value)?)));
} else if error.contains("InvalidReceiptsRoot") {
return Ok(Some(EngineApiHint::ReceiptsRoot(B256::from_hex(raw_hint_value)?)));
} else if error.contains("InvalidLogsBloom") {
return Ok(Some(EngineApiHint::LogsBloom(Bloom::from_hex(&raw_hint_value)?)));
};

Ok(None)
}
9 changes: 4 additions & 5 deletions bolt-sidecar/src/builder/fallback/payload_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,8 @@ impl FallbackPayloadBuilder {
mod tests {
use std::time::{SystemTime, UNIX_EPOCH};

use alloy::consensus::constants;
use alloy::{
consensus::proofs,
consensus::{constants, proofs},
eips::eip2718::{Decodable2718, Encodable2718},
network::{EthereumWallet, TransactionBuilder},
primitives::{hex, Address},
Expand Down Expand Up @@ -195,9 +194,9 @@ mod tests {
let raw_encoded = tx_signed.encoded_2718();
let tx_signed_reth = TransactionSigned::decode_2718(&mut raw_encoded.as_slice())?;

let slot = genesis_time
+ (SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() / cfg.chain.slot_time())
+ 1;
let slot = genesis_time +
(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() / cfg.chain.slot_time()) +
1;

let block = builder.build_fallback_payload(slot, &[tx_signed_reth]).await?;

Expand Down
4 changes: 2 additions & 2 deletions bolt-sidecar/src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ pub enum BuilderError {
InvalidTransactions(String),
#[error("Got an unexpected response from engine_newPayload query: {0}")]
UnexpectedPayloadStatus(PayloadStatusEnum),
#[error("Failed to parse any hints from engine API validation error")]
FailedToParseHintsFromEngine,
#[error("Failed to parse any hints from engine API validation error (client: {0})")]
FailedToParseHintsFromEngine(String),
#[error("Unsupported engine hint: {0}")]
UnsupportedEngineHint(String),
#[error("Unsupported engine client: {0}")]
Expand Down
33 changes: 12 additions & 21 deletions bolt-sidecar/src/chain_io/manager.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Duration;

use alloy::{
contract::Error,
primitives::Address,
Expand All @@ -9,15 +11,14 @@ use ethereum_consensus::primitives::BlsPublicKey;
use eyre::{bail, Context};
use reqwest::{Client, Url};
use serde::Serialize;

use tracing::{debug, warn};

use BoltManagerContract::{
BoltManagerContractErrors, BoltManagerContractInstance, ProposerStatus, ValidatorDoesNotExist,
};

use crate::config::chain::Chain;

use super::utils::{self, CompressedHash};
use crate::config::chain::Chain;

/// Maximum number of keys to fetch from the EL node in a single query.
const MAX_CHUNK_SIZE: usize = 100;
Expand Down Expand Up @@ -86,15 +87,8 @@ impl BoltManager {
// `retry_with_backoff_if` is not used here because we need to check
// that the error is retryable.
if transport_err.to_string().contains("error sending request for url") {
warn!(
"Retryable transport error when connecting to EL node: {}",
transport_err
);
// Crude increasing backoff
tokio::time::sleep(std::time::Duration::from_millis(
100 * retries as u64,
))
.await;
warn!("Transport error when connecting to EL node: {}", transport_err);
tokio::time::sleep(Duration::from_millis(100 * retries as u64)).await;
continue;
}
warn!(
Expand All @@ -108,9 +102,7 @@ impl BoltManager {
let decoded_error = utils::try_parse_contract_error(err)
.wrap_err("Failed to fetch proposer statuses from EL client")?;

bail!(
generate_bolt_manager_error(decoded_error, commitment_signer_pubkey,)
);
bail!(generate_bolt_manager_error(decoded_error, commitment_signer_pubkey));
}
}
};
Expand Down Expand Up @@ -226,8 +218,7 @@ sol! {
#[cfg(test)]
mod tests {
use ::hex::FromHex;
use alloy::hex;
use alloy::primitives::Address;
use alloy::{hex, primitives::Address};
use alloy_node_bindings::Anvil;
use ethereum_consensus::primitives::BlsPublicKey;
use reqwest::Url;
Expand Down Expand Up @@ -268,8 +259,8 @@ mod tests {
.as_ref()).expect("valid bls public key")];
let res = manager.verify_validator_pubkeys(keys.clone(), commitment_signer_pubkey).await;
assert!(
res.unwrap_err().to_string()
== generate_operator_keys_mismatch_error(
res.unwrap_err().to_string() ==
generate_operator_keys_mismatch_error(
pubkey_hash(&keys[0]),
commitment_signer_pubkey,
operator
Expand Down Expand Up @@ -317,8 +308,8 @@ mod tests {
let result = manager.verify_validator_pubkeys(keys.clone(), commitment_signer_pubkey).await;

assert!(
result.unwrap_err().to_string()
== generate_operator_keys_mismatch_error(
result.unwrap_err().to_string() ==
generate_operator_keys_mismatch_error(
pubkey_hash(&keys[0]),
commitment_signer_pubkey,
operator
Expand Down

0 comments on commit 90ec3c4

Please sign in to comment.