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());
+ }
}