Skip to content

Commit 90ec3c4

Browse files
authored
Merge pull request #600 from chainbound/feat/nethermind-payload-building
feat: build valid fallback payload with Nethermind
2 parents 28a17b0 + 60ffbee commit 90ec3c4

File tree

7 files changed

+62
-44
lines changed

7 files changed

+62
-44
lines changed

bolt-sidecar/src/builder/fallback/engine_hinter.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ use crate::{
2020

2121
use super::engine_hints::parse_hint_from_engine_response;
2222

23+
/// The maximum number of hint iterations to try before giving up.
24+
const MAX_HINT_ITERATIONS: u64 = 20;
25+
2326
/// The [EngineHinter] is responsible for gathering "hints" from the
2427
/// engine API error responses to complete the sealed block.
2528
///
@@ -58,7 +61,6 @@ impl EngineHinter {
5861

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

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

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

8790
iteration += 1;
88-
if iteration >= max_iterations {
89-
return Err(BuilderError::ExceededMaxHintIterations(max_iterations));
91+
if iteration >= MAX_HINT_ITERATIONS {
92+
return Err(BuilderError::ExceededMaxHintIterations(MAX_HINT_ITERATIONS));
9093
}
9194
}
9295
}
@@ -121,7 +124,8 @@ impl EngineHinter {
121124
// Parse the hint from the engine API response, based on the EL client code
122125
let Some(hint) = parse_hint_from_engine_response(ctx.el_client_code, &validation_error)?
123126
else {
124-
return Err(BuilderError::FailedToParseHintsFromEngine);
127+
let el_name = ctx.el_client_code.client_name().to_string();
128+
return Err(BuilderError::FailedToParseHintsFromEngine(el_name));
125129
};
126130

127131
Ok(hint)

bolt-sidecar/src/builder/fallback/engine_hints/geth.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
use alloy::primitives::{Bloom, B256};
22
use hex::FromHex;
3+
use lazy_static::lazy_static;
34
use regex::Regex;
45

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

8+
lazy_static! {
9+
/// Capture either the "local" or "got" value from the error message
10+
static ref REGEX: Regex = Regex::new(r"(?:local:|got) ([0-9a-zA-Z]+)").expect("valid regex");
11+
}
12+
713
/// Parse a hinted value from the engine response.
814
/// An example error message from the engine API looks like this:
915
///
@@ -22,10 +28,7 @@ use crate::builder::{fallback::engine_hinter::EngineApiHint, BuilderError};
2228
/// - [ValidateState](<https://github.com/ethereum/go-ethereum/blob/9298d2db884c4e3f9474880e3dcfd080ef9eacfa/core/block_validator.go#L122-L151>)
2329
/// - [Blockhash Mismatch](<https://github.com/ethereum/go-ethereum/blob/9298d2db884c4e3f9474880e3dcfd080ef9eacfa/beacon/engine/types.go#L253-L256>)
2430
pub fn parse_geth_engine_error_hint(error: &str) -> Result<Option<EngineApiHint>, BuilderError> {
25-
// Capture either the "local" or "got" value from the error message
26-
let re = Regex::new(r"(?:local:|got) ([0-9a-zA-Z]+)").expect("valid regex");
27-
28-
let raw_hint_value = match re.captures(error).and_then(|cap| cap.get(1)) {
31+
let raw_hint_value = match REGEX.captures(error).and_then(|cap| cap.get(1)) {
2932
Some(matched) => matched.as_str().to_string(),
3033
None => return Ok(None),
3134
};

bolt-sidecar/src/builder/fallback/engine_hints/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ pub fn parse_hint_from_engine_response(
2121
) -> Result<Option<EngineApiHint>, BuilderError> {
2222
match client {
2323
ClientCode::GE => geth::parse_geth_engine_error_hint(error),
24-
// TODO: Add Nethermind engine hints parsing
25-
// ClientCode::NM => nethermind::parse_nethermind_engine_error_hint(error),
24+
ClientCode::NM => nethermind::parse_nethermind_engine_error_hint(error),
25+
2626
_ => {
2727
error!("Unsupported fallback execution client: {}", client.client_name());
2828
Err(BuilderError::UnsupportedEngineClient(client))
Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
use tracing::warn;
1+
use alloy::primitives::{Bloom, B256};
2+
use hex::FromHex;
3+
use lazy_static::lazy_static;
4+
use regex::Regex;
25

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

8+
lazy_static! {
9+
/// Capture the "got" value from the error message
10+
static ref REGEX: Regex = Regex::new(r"got ([0-9a-zA-Z]+)").expect("valid regex");
11+
}
12+
513
/// Parse a hinted value from the engine response.
614
/// An example error message from the engine API looks like this:
715
///
@@ -11,17 +19,30 @@ use crate::builder::{fallback::engine_hinter::EngineApiHint, BuilderError};
1119
/// "id": 1,
1220
/// "error": {
1321
/// "code":-32000,
14-
/// "message": "local: blockhash mismatch: got 0x... expected 0x..."
22+
/// "message": "HeaderGasUsedMismatch: Gas used in header does not match calculated. Expected 0, got 21000"
1523
/// }
1624
/// }
1725
/// ```
18-
// TODO: implement hints parsing
19-
// TODO: remove dead_code attribute
20-
#[allow(dead_code)]
2126
pub fn parse_nethermind_engine_error_hint(
2227
error: &str,
2328
) -> Result<Option<EngineApiHint>, BuilderError> {
24-
warn!(%error, "Nethermind engine error hint parsing is not implemented");
29+
let raw_hint_value = match REGEX.captures(error).and_then(|cap| cap.get(1)) {
30+
Some(matched) => matched.as_str().to_string(),
31+
None => return Ok(None),
32+
};
33+
34+
// Match the hint value to the corresponding hint type based on other parts of the error message
35+
if error.contains("InvalidHeaderHash") {
36+
return Ok(Some(EngineApiHint::BlockHash(B256::from_hex(raw_hint_value)?)));
37+
} else if error.contains("HeaderGasUsedMismatch") {
38+
return Ok(Some(EngineApiHint::GasUsed(raw_hint_value.parse()?)));
39+
} else if error.contains("InvalidStateRoot") {
40+
return Ok(Some(EngineApiHint::StateRoot(B256::from_hex(raw_hint_value)?)));
41+
} else if error.contains("InvalidReceiptsRoot") {
42+
return Ok(Some(EngineApiHint::ReceiptsRoot(B256::from_hex(raw_hint_value)?)));
43+
} else if error.contains("InvalidLogsBloom") {
44+
return Ok(Some(EngineApiHint::LogsBloom(Bloom::from_hex(&raw_hint_value)?)));
45+
};
2546

2647
Ok(None)
2748
}

bolt-sidecar/src/builder/fallback/payload_builder.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,8 @@ impl FallbackPayloadBuilder {
144144
mod tests {
145145
use std::time::{SystemTime, UNIX_EPOCH};
146146

147-
use alloy::consensus::constants;
148147
use alloy::{
149-
consensus::proofs,
148+
consensus::{constants, proofs},
150149
eips::eip2718::{Decodable2718, Encodable2718},
151150
network::{EthereumWallet, TransactionBuilder},
152151
primitives::{hex, Address},
@@ -195,9 +194,9 @@ mod tests {
195194
let raw_encoded = tx_signed.encoded_2718();
196195
let tx_signed_reth = TransactionSigned::decode_2718(&mut raw_encoded.as_slice())?;
197196

198-
let slot = genesis_time
199-
+ (SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() / cfg.chain.slot_time())
200-
+ 1;
197+
let slot = genesis_time +
198+
(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() / cfg.chain.slot_time()) +
199+
1;
201200

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

bolt-sidecar/src/builder/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ pub enum BuilderError {
6666
InvalidTransactions(String),
6767
#[error("Got an unexpected response from engine_newPayload query: {0}")]
6868
UnexpectedPayloadStatus(PayloadStatusEnum),
69-
#[error("Failed to parse any hints from engine API validation error")]
70-
FailedToParseHintsFromEngine,
69+
#[error("Failed to parse any hints from engine API validation error (client: {0})")]
70+
FailedToParseHintsFromEngine(String),
7171
#[error("Unsupported engine hint: {0}")]
7272
UnsupportedEngineHint(String),
7373
#[error("Unsupported engine client: {0}")]

bolt-sidecar/src/chain_io/manager.rs

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::time::Duration;
2+
13
use alloy::{
24
contract::Error,
35
primitives::Address,
@@ -9,15 +11,14 @@ use ethereum_consensus::primitives::BlsPublicKey;
911
use eyre::{bail, Context};
1012
use reqwest::{Client, Url};
1113
use serde::Serialize;
12-
1314
use tracing::{debug, warn};
15+
1416
use BoltManagerContract::{
1517
BoltManagerContractErrors, BoltManagerContractInstance, ProposerStatus, ValidatorDoesNotExist,
1618
};
1719

18-
use crate::config::chain::Chain;
19-
2020
use super::utils::{self, CompressedHash};
21+
use crate::config::chain::Chain;
2122

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

111-
bail!(
112-
generate_bolt_manager_error(decoded_error, commitment_signer_pubkey,)
113-
);
105+
bail!(generate_bolt_manager_error(decoded_error, commitment_signer_pubkey));
114106
}
115107
}
116108
};
@@ -226,8 +218,7 @@ sol! {
226218
#[cfg(test)]
227219
mod tests {
228220
use ::hex::FromHex;
229-
use alloy::hex;
230-
use alloy::primitives::Address;
221+
use alloy::{hex, primitives::Address};
231222
use alloy_node_bindings::Anvil;
232223
use ethereum_consensus::primitives::BlsPublicKey;
233224
use reqwest::Url;
@@ -268,8 +259,8 @@ mod tests {
268259
.as_ref()).expect("valid bls public key")];
269260
let res = manager.verify_validator_pubkeys(keys.clone(), commitment_signer_pubkey).await;
270261
assert!(
271-
res.unwrap_err().to_string()
272-
== generate_operator_keys_mismatch_error(
262+
res.unwrap_err().to_string() ==
263+
generate_operator_keys_mismatch_error(
273264
pubkey_hash(&keys[0]),
274265
commitment_signer_pubkey,
275266
operator
@@ -317,8 +308,8 @@ mod tests {
317308
let result = manager.verify_validator_pubkeys(keys.clone(), commitment_signer_pubkey).await;
318309

319310
assert!(
320-
result.unwrap_err().to_string()
321-
== generate_operator_keys_mismatch_error(
311+
result.unwrap_err().to_string() ==
312+
generate_operator_keys_mismatch_error(
322313
pubkey_hash(&keys[0]),
323314
commitment_signer_pubkey,
324315
operator

0 commit comments

Comments
 (0)