Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: build valid fallback payload with Nethermind #600

Merged
merged 1 commit into from
Dec 19, 2024
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
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
Loading