From 9bad77c9f99c5856ab31fa74e91d376d00744c84 Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Fri, 9 Aug 2024 11:46:42 +0200 Subject: [PATCH] fix: build local payload with timestamp from beacon clock --- bolt-sidecar/src/builder/mod.rs | 14 +-- bolt-sidecar/src/builder/payload_builder.rs | 109 +++++++++++++------- bolt-sidecar/src/driver.rs | 45 ++++---- 3 files changed, 95 insertions(+), 73 deletions(-) diff --git a/bolt-sidecar/src/builder/mod.rs b/bolt-sidecar/src/builder/mod.rs index 866cdc0ef..437a8ae54 100644 --- a/bolt-sidecar/src/builder/mod.rs +++ b/bolt-sidecar/src/builder/mod.rs @@ -1,4 +1,5 @@ use alloy::primitives::U256; +use beacon_api_client::mainnet::Client as BeaconClient; use blst::min_pk::SecretKey; use ethereum_consensus::{ crypto::{KzgCommitment, PublicKey}, @@ -37,7 +38,7 @@ pub mod payload_builder; #[allow(missing_docs)] pub enum BuilderError { #[error("Failed to parse from integer: {0}")] - Parse(#[from] std::num::ParseIntError), + ParseInt(#[from] std::num::ParseIntError), #[error("Failed to de/serialize JSON: {0}")] Json(#[from] serde_json::Error), #[error("Failed to decode hex: {0}")] @@ -77,10 +78,10 @@ pub struct LocalBuilder { impl LocalBuilder { /// Create a new local builder with the given secret key. - pub fn new(config: &Config) -> Self { + pub fn new(config: &Config, beacon_api_client: BeaconClient, genesis_time: u64) -> Self { Self { payload_and_bid: None, - fallback_builder: FallbackPayloadBuilder::new(config), + fallback_builder: FallbackPayloadBuilder::new(config, beacon_api_client, genesis_time), secret_key: config.builder_private_key.clone(), chain: config.chain.clone(), } @@ -90,6 +91,7 @@ impl LocalBuilder { /// cache the payload in the local builder instance, and make it available pub async fn build_new_local_payload( &mut self, + slot: u64, template: &BlockTemplate, ) -> Result<(), BuilderError> { let transactions = template.as_signed_transactions(); @@ -98,7 +100,7 @@ impl LocalBuilder { // 1. build a fallback payload with the given transactions, on top of // the current head of the chain - let sealed_block = self.fallback_builder.build_fallback_payload(&transactions).await?; + let block = self.fallback_builder.build_fallback_payload(slot, &transactions).await?; // NOTE: we use a big value for the bid to ensure it gets chosen by mev-boost. // the client has no way to actually verify this, and we don't need to trust @@ -108,11 +110,11 @@ impl LocalBuilder { // to ALWAYS prefer PBS blocks. This is a safety measure that doesn't hurt to keep. let value = U256::from(100_000_000_000_000_000_000u128); - let eth_payload = compat::to_consensus_execution_payload(&sealed_block); + let eth_payload = compat::to_consensus_execution_payload(&block); let payload_and_blobs = PayloadAndBlobs { execution_payload: eth_payload, blobs_bundle }; // 2. create a signed builder bid with the sealed block header we just created - let eth_header = compat::to_execution_payload_header(&sealed_block, transactions); + let eth_header = compat::to_execution_payload_header(&block, transactions); // 3. sign the bid with the local builder's BLS key let signed_bid = self.create_signed_builder_bid(value, eth_header, kzg_commitments)?; diff --git a/bolt-sidecar/src/builder/payload_builder.rs b/bolt-sidecar/src/builder/payload_builder.rs index a904605c3..d6a21b168 100644 --- a/bolt-sidecar/src/builder/payload_builder.rs +++ b/bolt-sidecar/src/builder/payload_builder.rs @@ -48,12 +48,13 @@ pub struct FallbackPayloadBuilder { beacon_api_client: BeaconClient, execution_rpc_client: RpcClient, engine_hinter: EngineHinter, - slot_time_in_seconds: u64, + slot_time: u64, + genesis_time: u64, } impl FallbackPayloadBuilder { /// Create a new fallback payload builder - pub fn new(config: &Config) -> Self { + pub fn new(config: &Config, beacon_api_client: BeaconClient, genesis_time: u64) -> Self { let engine_hinter = EngineHinter { client: reqwest::Client::new(), jwt_hex: config.jwt_hex.to_string(), @@ -64,9 +65,10 @@ impl FallbackPayloadBuilder { engine_hinter, extra_data: DEFAULT_EXTRA_DATA.into(), fee_recipient: config.fee_recipient, - beacon_api_client: BeaconClient::new(config.beacon_api_url.clone()), execution_rpc_client: RpcClient::new(config.execution_api_url.clone()), - slot_time_in_seconds: config.chain.slot_time(), + slot_time: config.chain.slot_time(), + genesis_time, + beacon_api_client, } } } @@ -86,7 +88,7 @@ struct Context { transactions_root: B256, withdrawals_root: B256, parent_beacon_block_root: B256, - slot_time_in_seconds: u64, + block_timestamp: u64, } #[derive(Debug, Default)] @@ -103,43 +105,20 @@ impl FallbackPayloadBuilder { /// to provide a valid payload that fulfills the commitments made by Bolt. pub async fn build_fallback_payload( &self, + target_slot: u64, transactions: &[TransactionSigned], ) -> Result { - // TODO: what if the latest block ends up being reorged out? + // We fetch the latest block to get the necessary parent values for the new block. + // For the timestamp, we must use the one expected by the beacon chain instead, to + // prevent edge cases where the proposer before us has missed their slot. let latest_block = self.execution_rpc_client.get_block(None, true).await?; debug!(num = ?latest_block.header.number, "got latest block"); - let withdrawals = self - .beacon_api_client - // Slot: Defaults to the slot after the parent state if not specified. - .get_expected_withdrawals(StateId::Head, None) - .await? - .into_iter() - .map(to_reth_withdrawal) - .collect::>(); - + let withdrawals = self.get_withdrawals_for_slot(target_slot).await?; debug!(amount = ?withdrawals.len(), "got withdrawals"); - // let prev_randao = self - // .beacon_api_client - // .get_randao(StateId::Head, None) - // .await?; - // let prev_randao = B256::from_slice(&prev_randao); - - // NOTE: for some reason, this call fails with an ApiResult deserialization error - // when using the beacon_api_client crate directly, so we use reqwest temporarily. - // this is to be refactored. - let prev_randao = reqwest::Client::new() - .get(self.beacon_api_client.endpoint.join("/eth/v1/beacon/states/head/randao").unwrap()) - .send() - .await - .unwrap() - .json::() - .await - .unwrap(); - let prev_randao = prev_randao.pointer("/data/randao").unwrap().as_str().unwrap(); - let prev_randao = B256::from_hex(prev_randao).unwrap(); - debug!("got prev_randao"); + let prev_randao = self.get_prev_randao().await?; + debug!(randao = ?prev_randao, "got prev_randao"); let parent_beacon_block_root = self.beacon_api_client.get_beacon_block_root(BlockId::Head).await?; @@ -167,6 +146,11 @@ impl FallbackPayloadBuilder { let blob_gas_used = transactions.iter().fold(0, |acc, tx| acc + tx.blob_gas_used().unwrap_or_default()); + // We must calculate the next block timestamp manually rather than rely on the + // previous execution block, to cover the edge case where any previous slots have + // been missed by the proposers immediately before us. + let block_timestamp = self.genesis_time + (target_slot * self.slot_time); + let ctx = Context { base_fee, blob_gas_used, @@ -177,7 +161,7 @@ impl FallbackPayloadBuilder { fee_recipient: self.fee_recipient, transactions_root: proofs::calculate_transaction_root(transactions), withdrawals_root: proofs::calculate_withdrawals_root(&withdrawals), - slot_time_in_seconds: self.slot_time_in_seconds, + block_timestamp, }; let body = BlockBody { @@ -243,6 +227,43 @@ impl FallbackPayloadBuilder { i += 1; } } + + /// Fetch the previous RANDAO value from the beacon chain. + /// + /// NOTE: for some reason, using the ApiResult from `beacon_api_client` doesn't work, so + /// we are making a direct request to the beacon client endpoint. + async fn get_prev_randao(&self) -> Result { + let url = self + .beacon_api_client + .endpoint + .join("/eth/v1/beacon/states/head/randao") + .map_err(|e| BuilderError::Custom(format!("Failed to join URL: {e:?}")))?; + + reqwest::Client::new() + .get(url) + .send() + .await? + .json::() + .await? + .pointer("/data/randao") + .and_then(|value| value.as_str()) + .map(|value| B256::from_hex(value).map_err(BuilderError::Hex)) + .ok_or_else(|| BuilderError::Custom("Failed to fetch prev_randao".to_string()))? + } + + /// Fetch the expected withdrawals for the given slot from the beacon chain. + async fn get_withdrawals_for_slot( + &self, + slot: u64, + ) -> Result, BuilderError> { + Ok(self + .beacon_api_client + .get_expected_withdrawals(StateId::Head, Some(slot)) + .await? + .into_iter() + .map(to_reth_withdrawal) + .collect::>()) + } } /// Engine API hint values that can be fetched from the engine API @@ -370,7 +391,7 @@ fn build_header_with_hints_and_context( number: latest_block.header.number.unwrap_or_default() + 1, gas_limit: latest_block.header.gas_limit as u64, gas_used, - timestamp: latest_block.header.timestamp + context.slot_time_in_seconds, + timestamp: context.block_timestamp, mix_hash: context.prev_randao, nonce: BEACON_NONCE, base_fee_per_gas: Some(context.base_fee), @@ -388,19 +409,21 @@ impl fmt::Debug for FallbackPayloadBuilder { .field("extra_data", &self.extra_data) .field("fee_recipient", &self.fee_recipient) .field("engine_hinter", &self.engine_hinter) - .field("slot_time_in_seconds", &self.slot_time_in_seconds) .finish() } } #[cfg(test)] mod tests { + use std::time::{SystemTime, UNIX_EPOCH}; + use alloy::{ eips::eip2718::Encodable2718, network::{EthereumWallet, TransactionBuilder}, primitives::{hex, Address}, signers::{k256::ecdsa::SigningKey, local::PrivateKeySigner}, }; + use beacon_api_client::mainnet::Client as BeaconClient; use reth_primitives::TransactionSigned; use tracing::warn; @@ -420,7 +443,9 @@ mod tests { let raw_sk = std::env::var("PRIVATE_KEY")?; - let builder = FallbackPayloadBuilder::new(&cfg); + let beacon_client = BeaconClient::new(cfg.beacon_api_url.clone()); + let genesis_time = beacon_client.get_genesis_details().await?.genesis_time; + let builder = FallbackPayloadBuilder::new(&cfg, beacon_client, genesis_time); let sk = SigningKey::from_slice(hex::decode(raw_sk)?.as_slice())?; let signer = PrivateKeySigner::from_signing_key(sk.clone()); @@ -432,7 +457,11 @@ mod tests { let raw_encoded = tx_signed.encoded_2718(); let tx_signed_reth = TransactionSigned::decode_enveloped(&mut raw_encoded.as_slice())?; - let block = builder.build_fallback_payload(&[tx_signed_reth]).await?; + 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?; assert_eq!(block.body.len(), 1); Ok(()) diff --git a/bolt-sidecar/src/driver.rs b/bolt-sidecar/src/driver.rs index ee50c7aa9..a437172bd 100644 --- a/bolt-sidecar/src/driver.rs +++ b/bolt-sidecar/src/driver.rs @@ -87,30 +87,16 @@ impl SidecarDriver eyre::Result { let mevboost_client = MevBoostClient::new(cfg.mevboost_url.clone()); - let execution = ExecutionState::new(fetcher, cfg.limits).await?; - let local_builder = LocalBuilder::new(&cfg); - let beacon_client = BeaconClient::new(cfg.beacon_api_url.clone()); + let execution = ExecutionState::new(fetcher, cfg.limits).await?; - // start the commitments api server - let api_addr = format!("0.0.0.0:{}", cfg.rpc_port); - let (api_events_tx, api_events_rx) = mpsc::channel(1024); - CommitmentsApiServer::new(api_addr).run(api_events_tx).await; - - // TODO: this can be replaced with ethereum_consensus::clock::from_system_time() - // but using beacon node events is easier to work on a custom devnet for now - // (as we don't need to specify genesis time and slot duration) - let head_tracker = HeadTracker::start(beacon_client.clone()); - - // Get the genesis time. let genesis_time = beacon_client.get_genesis_details().await?.genesis_time; + let slot_stream = + clock::from_system_time(genesis_time, cfg.chain.slot_time(), SLOTS_PER_EPOCH) + .into_stream(); - // TODO: head tracker initializes the genesis timestamp with '0' value - // we should add an async fn to fetch the value for safety - // Initialize the consensus clock. - let consensus_clock = - clock::from_system_time(genesis_time, cfg.chain.slot_time(), SLOTS_PER_EPOCH); - let slot_stream = consensus_clock.into_stream(); + let local_builder = LocalBuilder::new(&cfg, beacon_client.clone(), genesis_time); + let head_tracker = HeadTracker::start(beacon_client.clone()); let consensus = ConsensusState::new( beacon_client, @@ -118,13 +104,12 @@ impl SidecarDriver SidecarDriver SidecarDriver SidecarDriver SidecarDriver