Skip to content

Commit

Permalink
Merge pull request #128 from chainbound/feat/sidecar/blob-support
Browse files Browse the repository at this point in the history
Sidecar: preliminary blob validation support
  • Loading branch information
thedevbirb authored Jul 15, 2024
2 parents 96634a7 + 381ff10 commit ec3a42b
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 144 deletions.
8 changes: 5 additions & 3 deletions bolt-sidecar/bin/sidecar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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())));
Expand All @@ -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"
);
Expand Down Expand Up @@ -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}");
};
},
Expand Down
15 changes: 11 additions & 4 deletions bolt-sidecar/src/builder/state_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
128 changes: 54 additions & 74 deletions bolt-sidecar/src/builder/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Address, AccountState> = 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<PooledTransactionsElement> {
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<TransactionSigned> {
pub fn as_signed_transactions(&self) -> Vec<TransactionSigned> {
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()
}

Expand All @@ -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<Address, Vec<&TransactionSigned>> = 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, AccountState> = 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);
});
}
}
Expand All @@ -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() {
Expand Down
15 changes: 15 additions & 0 deletions bolt-sidecar/src/client/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64>) -> TransportResult<u128> {
let block_count = U64::from(1);
let tag = block_number.map_or(BlockNumberOrTag::Latest, BlockNumberOrTag::Number);
let reward_percentiles: Vec<f64> = 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<u64> {
let result: U64 = self.0.request("eth_blockNumber", ()).await?;
Expand Down
11 changes: 7 additions & 4 deletions bolt-sidecar/src/common.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -26,7 +29,7 @@ pub fn calculate_max_basefee(current: u128, block_diff: u64) -> Option<u128> {
}

/// 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();
Expand All @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions bolt-sidecar/src/crypto/bls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: SignableBLS>(
Expand Down
24 changes: 9 additions & 15 deletions bolt-sidecar/src/primitives/commitment.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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.
Expand All @@ -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<TransactionSigned, D::Error>
fn deserialize_tx<'de, D>(deserializer: D) -> Result<PooledTransactionsElement, D::Error>
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<S>(tx: &TransactionSigned, serializer: S) -> Result<S::Ok, S::Error>
fn serialize_tx<S>(tx: &PooledTransactionsElement, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
Expand Down Expand Up @@ -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)
}
Expand Down
Loading

0 comments on commit ec3a42b

Please sign in to comment.