diff --git a/bolt-sidecar/bin/sidecar.rs b/bolt-sidecar/bin/sidecar.rs index 941d5782..1a4222a7 100644 --- a/bolt-sidecar/bin/sidecar.rs +++ b/bolt-sidecar/bin/sidecar.rs @@ -67,6 +67,7 @@ async fn main() -> eyre::Result<()> { loop { tokio::select! { Some(ApiEvent { request, response_tx }) = api_events_rx.recv() => { + let start = std::time::Instant::now(); tracing::info!("Received commitment request: {:?}", request); let validator_index = match consensus_state.validate_request(&request) { @@ -79,10 +80,10 @@ async fn main() -> eyre::Result<()> { }; let sender = match execution_state - .check_commitment_validity(&request) + .validate_commitment_request(&request) .await { - Ok(sender) => { sender }, + Ok(sender) => sender, Err(e) => { tracing::error!("Failed to commit request: {:?}", e); let _ = response_tx.send(Err(ApiError::Custom(e.to_string()))); @@ -93,6 +94,7 @@ async fn main() -> eyre::Result<()> { // TODO: match when we have more request types let CommitmentRequest::Inclusion(request) = request; tracing::info!( + elapsed = ?start.elapsed(), tx_hash = %request.tx.hash(), "Validation against execution state passed" ); @@ -146,7 +148,7 @@ async fn main() -> eyre::Result<()> { } - if let Err(e) = local_builder.build_new_local_payload(template.transactions()).await { + if let Err(e) = local_builder.build_new_local_payload(template.as_signed_transactions()).await { tracing::error!(err = ?e, "CRITICAL: Error while building local payload at slot deadline for {slot}"); }; }, diff --git a/bolt-sidecar/src/builder/state_root.rs b/bolt-sidecar/src/builder/state_root.rs index 73aa92f3..d2d1b23e 100644 --- a/bolt-sidecar/src/builder/state_root.rs +++ b/bolt-sidecar/src/builder/state_root.rs @@ -10,17 +10,24 @@ mod tests { use partial_mpt::StateTrie; use reqwest::Url; - use crate::{builder::CallTraceManager, client::rpc::RpcClient}; + use crate::{ + builder::CallTraceManager, client::rpc::RpcClient, test_util::try_get_execution_api_url, + }; + #[ignore] #[tokio::test] async fn test_trace_call() -> eyre::Result<()> { dotenvy::dotenv().ok(); - tracing_subscriber::fmt::init(); + let _ = tracing_subscriber::fmt::try_init(); + + let Some(rpc_url) = try_get_execution_api_url().await else { + tracing::warn!("EL_RPC not reachable, skipping test"); + return Ok(()); + }; tracing::info!("Starting test_trace_call"); - let rpc_url = std::env::var("RPC_URL").expect("RPC_URL must be set"); - let rpc_url = Url::parse(&rpc_url).unwrap(); + let rpc_url = Url::parse(rpc_url).unwrap(); let client = RpcClient::new(rpc_url.clone()); let (call_trace_manager, call_trace_handler) = CallTraceManager::new(rpc_url); diff --git a/bolt-sidecar/src/builder/template.rs b/bolt-sidecar/src/builder/template.rs index 356ddd8d..4c12701f 100644 --- a/bolt-sidecar/src/builder/template.rs +++ b/bolt-sidecar/src/builder/template.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use alloy_primitives::{Address, U256}; -use reth_primitives::{TransactionSigned, TxType}; +use reth_primitives::{PooledTransactionsElement, TransactionSigned}; use crate::{ common::max_transaction_cost, @@ -37,45 +37,27 @@ impl BlockTemplate { &self.state_diff } - /// Adds a transaction to the block template and updates the state diff. - pub fn add_constraints(&mut self, signed_constraints: SignedConstraints) { - let mut address_to_state_diffs: HashMap = HashMap::new(); - signed_constraints.message.constraints.iter().for_each(|c| { - address_to_state_diffs - .entry(c.sender) - .and_modify(|state| { - state.balance = state - .balance - .saturating_add(max_transaction_cost(&c.tx_decoded)); - state.transaction_count += 1; - }) - .or_insert(AccountState { - balance: max_transaction_cost(&c.tx_decoded), - transaction_count: 1, - }); - }); - - // Now update intermediate state - address_to_state_diffs.iter().for_each(|(address, diff)| { - self.state_diff - .diffs - .entry(*address) - .and_modify(|(nonce, balance)| { - *nonce += diff.transaction_count; - *balance += diff.balance; - }) - .or_insert((diff.transaction_count, diff.balance)); - }); - - self.signed_constraints_list.push(signed_constraints); + /// Returns the cloned list of transactions from the constraints. + #[inline] + pub fn transactions(&self) -> Vec { + self.signed_constraints_list + .iter() + .flat_map(|sc| sc.message.constraints.iter().map(|c| c.transaction.clone())) + .collect() } - /// Returns all a clone of all transactions from the signed constraints list + /// Converts the list of signed constraints into a list of signed transactions. Use this when building + /// a local execution payload. #[inline] - pub fn transactions(&self) -> Vec { + pub fn as_signed_transactions(&self) -> Vec { self.signed_constraints_list .iter() - .flat_map(|sc| sc.message.constraints.iter().map(|c| c.tx_decoded.clone())) + .flat_map(|sc| { + sc.message + .constraints + .iter() + .map(|c| c.transaction.clone().into_transaction()) + }) .collect() } @@ -90,48 +72,47 @@ impl BlockTemplate { /// Returns the blob count of the block template. #[inline] pub fn blob_count(&self) -> usize { - self.signed_constraints_list.iter().fold(0, |acc, sc| { - acc + sc - .message - .constraints - .iter() - .filter(|c| c.tx_decoded.tx_type() == TxType::Eip4844) - .count() + self.signed_constraints_list.iter().fold(0, |mut acc, sc| { + acc += sc.message.constraints.iter().fold(0, |acc, c| { + acc + c + .transaction + .as_eip4844() + .map(|tx| tx.blob_versioned_hashes.len()) + .unwrap_or(0) + }); + + acc }) } + /// Adds a list of constraints to the block template and updates the state diff. + pub fn add_constraints(&mut self, constraints: SignedConstraints) { + for constraint in constraints.message.constraints.iter() { + let max_cost = max_transaction_cost(&constraint.transaction); + self.state_diff + .diffs + .entry(constraint.sender) + .and_modify(|(nonce, balance)| { + *nonce += 1; + *balance += max_cost; + }) + .or_insert((1, max_cost)); + } + + self.signed_constraints_list.push(constraints); + } + /// Remove all signed constraints at the specified index and updates the state diff fn remove_constraints_at_index(&mut self, index: usize) { - let sc = self.signed_constraints_list.remove(index); - let mut address_to_txs: HashMap> = HashMap::new(); - sc.message.constraints.iter().for_each(|c| { - address_to_txs - .entry(c.sender) - .and_modify(|txs| txs.push(&c.tx_decoded)) - .or_insert(vec![&c.tx_decoded]); - }); - - // Collect the diff for each address and every transaction - let address_to_diff: HashMap = address_to_txs - .iter() - .map(|(address, txs)| { - let mut state = AccountState::default(); - for tx in txs { - state.balance = state.balance.saturating_add(max_transaction_cost(tx)); - state.transaction_count = state.transaction_count.saturating_sub(1); - } - (*address, state) - }) - .collect(); + let constraints = self.signed_constraints_list.remove(index); - // Update intermediate state - for (address, diff) in address_to_diff.iter() { + for constraint in constraints.message.constraints.iter() { self.state_diff .diffs - .entry(*address) + .entry(constraint.sender) .and_modify(|(nonce, balance)| { - *nonce = nonce.saturating_sub(diff.transaction_count); - *balance += diff.balance; + *nonce = nonce.saturating_sub(1); + *balance -= max_transaction_cost(&constraint.transaction); }); } } @@ -157,22 +138,21 @@ impl BlockTemplate { .iter() .flat_map(|c| c.1.clone()) .fold((U256::ZERO, u64::MAX), |mut acc, c| { - let nonce = c.tx_decoded.nonce(); + let nonce = c.transaction.nonce(); if nonce < acc.1 { acc.1 = nonce; } - acc.0 += max_transaction_cost(&c.tx_decoded); + acc.0 += max_transaction_cost(&c.transaction); acc }); if state.balance < max_total_cost || state.transaction_count > min_nonce { - // NOTE: We drop all the signed constraints containing such pre-confirmations - // since at least one of them has been invalidated. + // Remove invalidated constraints due to balance / nonce of chain state tracing::warn!( %address, - "Removing all signed constraints which contain such address pre-confirmations due to conflict with account state", + "Removing invalidated constraints for address" ); - indexes = constraints_with_address.iter().map(|c| c.0).collect(); + indexes = constraints_with_address.iter().map(|(i, _)| *i).collect(); } for index in indexes.into_iter().rev() { diff --git a/bolt-sidecar/src/client/rpc.rs b/bolt-sidecar/src/client/rpc.rs index ec796167..67c51014 100644 --- a/bolt-sidecar/src/client/rpc.rs +++ b/bolt-sidecar/src/client/rpc.rs @@ -45,6 +45,21 @@ impl RpcClient { Ok(fee_history.latest_block_base_fee().unwrap()) } + /// Get the blob basefee of the latest block. + /// + /// Reference: https://github.com/ethereum/execution-apis/blob/main/src/eth/fee_market.yaml + pub async fn get_blob_basefee(&self, block_number: Option) -> TransportResult { + let block_count = U64::from(1); + let tag = block_number.map_or(BlockNumberOrTag::Latest, BlockNumberOrTag::Number); + let reward_percentiles: Vec = vec![]; + let fee_history: FeeHistory = self + .0 + .request("eth_feeHistory", (block_count, tag, &reward_percentiles)) + .await?; + + Ok(fee_history.latest_block_blob_base_fee().unwrap_or(0)) + } + /// Get the latest block number pub async fn get_head(&self) -> TransportResult { let result: U64 = self.0.request("eth_blockNumber", ()).await?; diff --git a/bolt-sidecar/src/common.rs b/bolt-sidecar/src/common.rs index 4e429bcc..8caa8b7c 100644 --- a/bolt-sidecar/src/common.rs +++ b/bolt-sidecar/src/common.rs @@ -1,7 +1,10 @@ use alloy_primitives::U256; -use reth_primitives::TransactionSigned; +use reth_primitives::PooledTransactionsElement; -use crate::{primitives::AccountState, state::ValidationError}; +use crate::{ + primitives::{AccountState, TransactionExt}, + state::ValidationError, +}; /// Calculates the max_basefee `slot_diff` blocks in the future given a current basefee (in gwei). /// Returns None if an overflow would occur. @@ -26,7 +29,7 @@ pub fn calculate_max_basefee(current: u128, block_diff: u64) -> Option { } /// Calculates the max transaction cost (gas + value) in wei. -pub fn max_transaction_cost(transaction: &TransactionSigned) -> U256 { +pub fn max_transaction_cost(transaction: &PooledTransactionsElement) -> U256 { let gas_limit = transaction.gas_limit() as u128; let fee_cap = transaction.max_fee_per_gas(); @@ -40,7 +43,7 @@ pub fn max_transaction_cost(transaction: &TransactionSigned) -> U256 { /// 2. The balance of the account must be higher than the transaction's max cost. pub fn validate_transaction( account_state: &AccountState, - transaction: &TransactionSigned, + transaction: &PooledTransactionsElement, ) -> Result<(), ValidationError> { // Check if the nonce is correct (should be the same as the transaction count) if transaction.nonce() < account_state.transaction_count { diff --git a/bolt-sidecar/src/crypto/bls.rs b/bolt-sidecar/src/crypto/bls.rs index 122690d7..e0625713 100644 --- a/bolt-sidecar/src/crypto/bls.rs +++ b/bolt-sidecar/src/crypto/bls.rs @@ -62,6 +62,13 @@ impl Signer { Self { key } } + /// Create a signer with a random BLS key. + pub fn random() -> Self { + Self { + key: random_bls_secret(), + } + } + /// Verify the signature of the object with the given public key. #[allow(dead_code)] pub fn verify( diff --git a/bolt-sidecar/src/primitives/commitment.rs b/bolt-sidecar/src/primitives/commitment.rs index c220ad37..416e6188 100644 --- a/bolt-sidecar/src/primitives/commitment.rs +++ b/bolt-sidecar/src/primitives/commitment.rs @@ -1,8 +1,8 @@ +use serde::{de, Deserialize, Deserializer, Serialize}; use std::str::FromStr; use alloy_primitives::{keccak256, Signature, B256}; -use reth_primitives::TransactionSigned; -use serde::{de, Deserialize, Deserializer, Serialize}; +use reth_primitives::PooledTransactionsElement; /// Commitment requests sent by users or RPC proxies to the sidecar. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -18,11 +18,8 @@ pub struct InclusionRequest { /// The consensus slot number at which the transaction should be included. pub slot: u64, /// The transaction to be included. - #[serde( - deserialize_with = "deserialize_tx_signed", - serialize_with = "serialize_tx_signed" - )] - pub tx: TransactionSigned, + #[serde(deserialize_with = "deserialize_tx", serialize_with = "serialize_tx")] + pub tx: PooledTransactionsElement, /// The signature over the "slot" and "tx" fields by the user. /// A valid signature is the only proof that the user actually requested /// this specific commitment to be included at the given slot. @@ -37,23 +34,20 @@ impl InclusionRequest { /// Validates the transaction fee against a minimum basefee. /// Returns true if the fee is greater than or equal to the min, false otherwise. pub fn validate_basefee(&self, min: u128) -> bool { - if self.tx.max_fee_per_gas() < min { - return false; - } - true + self.tx.max_fee_per_gas() >= min } } -fn deserialize_tx_signed<'de, D>(deserializer: D) -> Result +fn deserialize_tx<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; let data = hex::decode(s.trim_start_matches("0x")).map_err(de::Error::custom)?; - TransactionSigned::decode_enveloped(&mut data.as_slice()).map_err(de::Error::custom) + PooledTransactionsElement::decode_enveloped(&mut data.as_slice()).map_err(de::Error::custom) } -fn serialize_tx_signed(tx: &TransactionSigned, serializer: S) -> Result +fn serialize_tx(tx: &PooledTransactionsElement, serializer: S) -> Result where S: serde::Serializer, { @@ -88,7 +82,7 @@ impl InclusionRequest { pub fn digest(&self) -> B256 { let mut data = Vec::new(); data.extend_from_slice(&self.slot.to_le_bytes()); - data.extend_from_slice(self.tx.hash.as_slice()); + data.extend_from_slice(self.tx.hash().as_slice()); keccak256(&data) } diff --git a/bolt-sidecar/src/primitives/constraint.rs b/bolt-sidecar/src/primitives/constraint.rs index 2ad6cbc6..5543c891 100644 --- a/bolt-sidecar/src/primitives/constraint.rs +++ b/bolt-sidecar/src/primitives/constraint.rs @@ -1,5 +1,5 @@ use alloy_primitives::{keccak256, Address}; -use reth_primitives::TransactionSigned; +use reth_primitives::PooledTransactionsElement; use secp256k1::Message; use serde::{Deserialize, Serialize}; @@ -57,7 +57,7 @@ impl ConstraintsMessage { request: InclusionRequest, sender: Address, ) -> Self { - let constraints = vec![Constraint::from_inclusion_request(request, None, sender)]; + let constraints = vec![Constraint::from_transaction(request.tx, None, sender)]; Self { validator_index, slot, @@ -85,32 +85,25 @@ impl SignableBLS for ConstraintsMessage { /// A general constraint on block building. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Constraint { - /// The raw transaction that needs to be included in the block - pub tx: String, /// The optional index at which the transaction needs to be included in the block pub index: Option, - /// The decoded transaction for internal use - #[serde(skip)] - pub(crate) tx_decoded: TransactionSigned, + /// The transaction to be included in the block + pub(crate) transaction: PooledTransactionsElement, /// The ec-recovered address of the transaction sender for internal use #[serde(skip)] pub(crate) sender: Address, } impl Constraint { - /// Builds a constraint from an inclusion request and an optional index - pub fn from_inclusion_request( - req: InclusionRequest, + /// Builds a constraint from a transaction, with an optional index + pub fn from_transaction( + transaction: PooledTransactionsElement, index: Option, sender: Address, ) -> Self { - let mut encoded_tx = Vec::new(); - req.tx.encode_enveloped(&mut encoded_tx); - Self { - tx: format!("0x{}", hex::encode(encoded_tx)), + transaction, index, - tx_decoded: req.tx, sender, } } @@ -119,7 +112,7 @@ impl Constraint { /// TODO: remove if we go with SSZ pub fn as_bytes(&self) -> Vec { let mut data = Vec::new(); - data.extend_from_slice(self.tx.as_bytes()); + self.transaction.encode_enveloped(&mut data); data.extend_from_slice(&self.index.unwrap_or(0).to_le_bytes()); data } diff --git a/bolt-sidecar/src/primitives/mod.rs b/bolt-sidecar/src/primitives/mod.rs index 51e191eb..35579926 100644 --- a/bolt-sidecar/src/primitives/mod.rs +++ b/bolt-sidecar/src/primitives/mod.rs @@ -17,6 +17,7 @@ use ethereum_consensus::{ types::mainnet::ExecutionPayload, Fork, }; +use reth_primitives::{PooledTransactionsElement, TxType}; use tokio::sync::{mpsc, oneshot}; /// Commitment types, received by users wishing to receive preconfirmations. @@ -219,3 +220,40 @@ impl ChainHead { self.block.load(std::sync::atomic::Ordering::SeqCst) } } + +/// Trait that exposes additional information on transaction types that don't already do it +/// by themselves (e.g. [`PooledTransactionsElement`]). +pub trait TransactionExt { + fn gas_limit(&self) -> u64; + fn value(&self) -> U256; + fn tx_type(&self) -> TxType; +} + +impl TransactionExt for PooledTransactionsElement { + fn gas_limit(&self) -> u64 { + match self { + PooledTransactionsElement::Legacy { transaction, .. } => transaction.gas_limit, + PooledTransactionsElement::Eip2930 { transaction, .. } => transaction.gas_limit, + PooledTransactionsElement::Eip1559 { transaction, .. } => transaction.gas_limit, + PooledTransactionsElement::BlobTransaction(blob_tx) => blob_tx.transaction.gas_limit, + } + } + + fn value(&self) -> U256 { + match self { + PooledTransactionsElement::Legacy { transaction, .. } => transaction.value, + PooledTransactionsElement::Eip2930 { transaction, .. } => transaction.value, + PooledTransactionsElement::Eip1559 { transaction, .. } => transaction.value, + PooledTransactionsElement::BlobTransaction(blob_tx) => blob_tx.transaction.value, + } + } + + fn tx_type(&self) -> TxType { + match self { + PooledTransactionsElement::Legacy { .. } => TxType::Legacy, + PooledTransactionsElement::Eip2930 { .. } => TxType::Eip2930, + PooledTransactionsElement::Eip1559 { .. } => TxType::Eip1559, + PooledTransactionsElement::BlobTransaction(_) => TxType::Eip4844, + } + } +} diff --git a/bolt-sidecar/src/state/execution.rs b/bolt-sidecar/src/state/execution.rs index 58391c1f..c3c4eb1b 100644 --- a/bolt-sidecar/src/state/execution.rs +++ b/bolt-sidecar/src/state/execution.rs @@ -1,7 +1,9 @@ use alloy_eips::eip4844::MAX_BLOBS_PER_BLOCK; use alloy_primitives::{Address, SignatureError}; use alloy_transport::TransportError; -use reth_primitives::transaction::TxType; +use reth_primitives::{ + revm_primitives::EnvKzgSettings, BlobTransactionValidationError, PooledTransactionsElement, +}; use std::{collections::HashMap, num::NonZero}; use thiserror::Error; @@ -17,8 +19,14 @@ use super::fetcher::StateFetcher; #[derive(Debug, Error)] pub enum ValidationError { /// The transaction fee is too low to cover the maximum base fee. - #[error("Transaction fee is too low, need {0} gwei to cover the maximum base fee")] + #[error("Transaction fee is too low, need {0} gwei to cover the maximum basefee")] BaseFeeTooLow(u128), + /// The transaction blob fee is too low to cover the maximum blob base fee. + #[error("Transaction blob fee is too low, need {0} gwei to cover the maximum blob basefee")] + BlobBaseFeeTooLow(u128), + /// The transaction blob is invalid. + #[error(transparent)] + BlobValidation(#[from] BlobTransactionValidationError), /// The transaction nonce is too low. #[error("Transaction nonce too low")] NonceTooLow, @@ -66,12 +74,12 @@ impl ValidationError { pub struct ExecutionState { /// The latest block number. block_number: u64, - /// The latest slot number. slot: u64, - - /// The base fee at the head block. + /// The basefee at the head block. basefee: u128, + /// The blob basefee at the head block. + blob_basefee: u128, /// The cached account states. This should never be read directly. /// These only contain the canonical account states at the head block, /// not the intermediate states. @@ -84,6 +92,9 @@ pub struct ExecutionState { max_commitments_per_slot: NonZero, + /// The KZG settings for validating blobs. + kzg_settings: EnvKzgSettings, + /// The state fetcher client. client: C, } @@ -95,12 +106,18 @@ impl ExecutionState { client: C, max_commitments_per_slot: NonZero, ) -> Result { + let (basefee, blob_basefee) = + tokio::try_join!(client.get_basefee(None), client.get_blob_basefee(None))?; + Ok(Self { - basefee: client.get_basefee(None).await?, + basefee, + blob_basefee, block_number: client.get_head().await?, slot: 0, account_states: HashMap::new(), block_templates: HashMap::new(), + // Load the default KZG settings + kzg_settings: EnvKzgSettings::default(), max_commitments_per_slot, client, }) @@ -121,10 +138,12 @@ impl ExecutionState { /// timing or proposer slot targets. /// /// If the commitment is invalid because of nonce, basefee or balance errors, it will return an error. - /// If the commitment is valid, it will be added to the block template and its account state + /// If the commitment is valid, its account state /// will be cached. If this is succesful, any callers can be sure that the commitment is valid /// and SHOULD sign it and respond to the requester. - pub async fn check_commitment_validity( + /// + /// TODO: should also validate everything in https://github.com/paradigmxyz/reth/blob/9aa44e1a90b262c472b14cd4df53264c649befc2/crates/transaction-pool/src/validate/eth.rs#L153 + pub async fn validate_commitment_request( &mut self, request: &CommitmentRequest, ) -> Result { @@ -181,14 +200,27 @@ impl ExecutionState { } // Check EIP-4844-specific limits - if req.tx.tx_type() == TxType::Eip4844 { + if let Some(transaction) = req.tx.as_eip4844() { if let Some(template) = self.block_templates.get(&req.slot) { if template.blob_count() >= MAX_BLOBS_PER_BLOCK { return Err(ValidationError::Eip4844Limit); } } - // TODO: check max_fee_per_blob_gas against the blob_base_fee + let PooledTransactionsElement::BlobTransaction(ref blob_transaction) = req.tx else { + unreachable!("EIP-4844 transaction should be a blob transaction") + }; + + // Calculate max possible increase in blob basefee + let max_blob_basefee = calculate_max_basefee(self.blob_basefee, slot_diff) + .ok_or(reject_internal("Overflow calculating max blob basefee"))?; + + if blob_transaction.transaction.max_fee_per_blob_gas < max_blob_basefee { + return Err(ValidationError::BlobBaseFeeTooLow(max_blob_basefee)); + } + + // Validate blob against KZG settings + transaction.validate_blob(&blob_transaction.sidecar, self.kzg_settings.get())?; } Ok(sender) @@ -223,6 +255,9 @@ impl ExecutionState { self.apply_state_update(update); + // Remove any block templates that are no longer valid + self.block_templates.remove(&slot); + Ok(()) } @@ -290,6 +325,7 @@ impl ExecutionState { pub struct StateUpdate { pub account_states: HashMap, pub min_basefee: u128, + pub min_blob_basefee: u128, pub block_number: u64, } diff --git a/bolt-sidecar/src/state/fetcher.rs b/bolt-sidecar/src/state/fetcher.rs index 58957978..049aab94 100644 --- a/bolt-sidecar/src/state/fetcher.rs +++ b/bolt-sidecar/src/state/fetcher.rs @@ -33,6 +33,8 @@ pub trait StateFetcher { async fn get_basefee(&self, block_number: Option) -> Result; + async fn get_blob_basefee(&self, block_number: Option) -> Result; + async fn get_account_state( &self, address: &Address, @@ -103,12 +105,14 @@ impl StateFetcher for StateClient { batch.send().await?; let basefee = self.client.get_basefee(None); + let blob_basefee = self.client.get_blob_basefee(None); // Collect the results - let (nonce_vec, balance_vec, basefee) = tokio::join!( + let (nonce_vec, balance_vec, basefee, blob_basefee) = tokio::join!( nonce_futs.collect::>(), balance_futs.collect::>(), basefee, + blob_basefee, ); // Insert the results @@ -143,6 +147,7 @@ impl StateFetcher for StateClient { Ok(StateUpdate { account_states, min_basefee: basefee?, + min_blob_basefee: blob_basefee?, block_number, }) } @@ -155,6 +160,10 @@ impl StateFetcher for StateClient { self.client.get_basefee(block_number).await } + async fn get_blob_basefee(&self, block_number: Option) -> Result { + self.client.get_blob_basefee(block_number).await + } + async fn get_account_state( &self, address: &Address, diff --git a/bolt-sidecar/src/state/mod.rs b/bolt-sidecar/src/state/mod.rs index 83cc2119..d8a127cf 100644 --- a/bolt-sidecar/src/state/mod.rs +++ b/bolt-sidecar/src/state/mod.rs @@ -80,11 +80,12 @@ mod tests { use execution::{ExecutionState, ValidationError}; use fetcher::StateClient; use reqwest::Url; - use reth_primitives::TransactionSigned; + use reth_primitives::PooledTransactionsElement; use tracing_subscriber::fmt; use crate::{ - primitives::{CommitmentRequest, InclusionRequest}, + crypto::{bls::Signer, SignableBLS, SignerBLS}, + primitives::{CommitmentRequest, ConstraintsMessage, InclusionRequest, SignedConstraints}, test_util::{default_test_transaction, launch_anvil}, }; @@ -131,7 +132,7 @@ mod tests { // Trick to parse into the TransactionSigned type let tx_signed_bytes = signed.encoded_2718(); let tx_signed = - TransactionSigned::decode_enveloped(&mut tx_signed_bytes.as_slice()).unwrap(); + PooledTransactionsElement::decode_enveloped(&mut tx_signed_bytes.as_slice()).unwrap(); let request = CommitmentRequest::Inclusion(InclusionRequest { slot: 10, @@ -139,7 +140,7 @@ mod tests { signature: sig, }); - assert!(state.check_commitment_validity(&request).await.is_ok()); + assert!(state.validate_commitment_request(&request).await.is_ok()); } #[tokio::test] @@ -167,7 +168,7 @@ mod tests { // Trick to parse into the TransactionSigned type let tx_signed_bytes = signed.encoded_2718(); let tx_signed = - TransactionSigned::decode_enveloped(&mut tx_signed_bytes.as_slice()).unwrap(); + PooledTransactionsElement::decode_enveloped(&mut tx_signed_bytes.as_slice()).unwrap(); let request = CommitmentRequest::Inclusion(InclusionRequest { slot: 10, @@ -176,7 +177,7 @@ mod tests { }); assert!(matches!( - state.check_commitment_validity(&request).await, + state.validate_commitment_request(&request).await, Err(ValidationError::NonceTooHigh) )); } @@ -207,7 +208,7 @@ mod tests { // Trick to parse into the TransactionSigned type let tx_signed_bytes = signed.encoded_2718(); let tx_signed = - TransactionSigned::decode_enveloped(&mut tx_signed_bytes.as_slice()).unwrap(); + PooledTransactionsElement::decode_enveloped(&mut tx_signed_bytes.as_slice()).unwrap(); let request = CommitmentRequest::Inclusion(InclusionRequest { slot: 10, @@ -216,7 +217,7 @@ mod tests { }); assert!(matches!( - state.check_commitment_validity(&request).await, + state.validate_commitment_request(&request).await, Err(ValidationError::InsufficientBalance) )); } @@ -248,7 +249,7 @@ mod tests { // Trick to parse into the TransactionSigned type let tx_signed_bytes = signed.encoded_2718(); let tx_signed = - TransactionSigned::decode_enveloped(&mut tx_signed_bytes.as_slice()).unwrap(); + PooledTransactionsElement::decode_enveloped(&mut tx_signed_bytes.as_slice()).unwrap(); let request = CommitmentRequest::Inclusion(InclusionRequest { slot: 10, @@ -257,7 +258,7 @@ mod tests { }); assert!(matches!( - state.check_commitment_validity(&request).await, + state.validate_commitment_request(&request).await, Err(ValidationError::BaseFeeTooLow(_)) )); } @@ -266,6 +267,8 @@ mod tests { async fn test_invalidate_inclusion_request() { let _ = fmt::try_init(); + let target_slot = 10; + let anvil = launch_anvil(); let client = StateClient::new(Url::parse(&anvil.endpoint()).unwrap()); @@ -284,19 +287,37 @@ mod tests { let signer: EthereumWallet = wallet.into(); let signed = tx.build(&signer).await.unwrap(); + let bls_signer = Signer::random(); + // Trick to parse into the TransactionSigned type let tx_signed_bytes = signed.encoded_2718(); let tx_signed = - TransactionSigned::decode_enveloped(&mut tx_signed_bytes.as_slice()).unwrap(); + PooledTransactionsElement::decode_enveloped(&mut tx_signed_bytes.as_slice()).unwrap(); - let request = CommitmentRequest::Inclusion(InclusionRequest { - slot: 10, + let inclusion_request = InclusionRequest { + slot: target_slot, tx: tx_signed, signature: sig, - }); + }; + + let request = CommitmentRequest::Inclusion(inclusion_request.clone()); - assert!(state.check_commitment_validity(&request).await.is_ok()); - assert!(state.block_templates().get(&10).unwrap().transactions_len() == 1); + assert!(state.validate_commitment_request(&request).await.is_ok()); + + let message = ConstraintsMessage::build(0, target_slot, inclusion_request, sender); + let signature = bls_signer.sign(&message.digest()).unwrap().to_string(); + let signed_constraints = SignedConstraints { message, signature }; + + state.add_constraint(target_slot, signed_constraints.clone()); + + assert!( + state + .block_templates() + .get(&target_slot) + .unwrap() + .transactions_len() + == 1 + ); let provider = ProviderBuilder::new().on_http(anvil.endpoint_url()); @@ -314,7 +335,73 @@ mod tests { .await .unwrap(); - let transactions_len = state.block_templates().get(&10).unwrap().transactions_len(); + let transactions_len = state + .block_templates() + .get(&target_slot) + .unwrap() + .transactions_len(); assert!(transactions_len == 0); } + + #[tokio::test] + async fn test_invalidate_stale_template() { + let _ = fmt::try_init(); + + let target_slot = 10; + + let anvil = launch_anvil(); + let client = StateClient::new(Url::parse(&anvil.endpoint()).unwrap()); + + let mut state = ExecutionState::new(client, NonZero::new(1024).expect("valid non-zero")) + .await + .unwrap(); + + let wallet: PrivateKeySigner = anvil.keys()[0].clone().into(); + + let sender = anvil.addresses()[0]; + + let tx = default_test_transaction(sender, None); + + let sig = wallet.sign_message_sync(&hex!("abcd")).unwrap(); + + let signer: EthereumWallet = wallet.into(); + let signed = tx.build(&signer).await.unwrap(); + + let bls_signer = Signer::random(); + + // Trick to parse into the TransactionSigned type + let tx_signed_bytes = signed.encoded_2718(); + let tx_signed = + PooledTransactionsElement::decode_enveloped(&mut tx_signed_bytes.as_slice()).unwrap(); + + let inclusion_request = InclusionRequest { + slot: target_slot, + tx: tx_signed, + signature: sig, + }; + + let request = CommitmentRequest::Inclusion(inclusion_request.clone()); + + assert!(state.validate_commitment_request(&request).await.is_ok()); + + let message = ConstraintsMessage::build(0, target_slot, inclusion_request, sender); + let signature = bls_signer.sign(&message.digest()).unwrap().to_string(); + let signed_constraints = SignedConstraints { message, signature }; + + state.add_constraint(target_slot, signed_constraints.clone()); + + assert!( + state + .block_templates() + .get(&target_slot) + .unwrap() + .transactions_len() + == 1 + ); + + // Update the head, which should invalidate the transaction due to a nonce conflict + state.update_head(None, target_slot).await.unwrap(); + + assert!(state.block_templates().get(&target_slot).is_none()); + } }