Skip to content

Commit

Permalink
chore: rebase
Browse files Browse the repository at this point in the history
merklefruit committed Jun 22, 2024
1 parent c97e519 commit 9983438
Showing 15 changed files with 954 additions and 293 deletions.
693 changes: 654 additions & 39 deletions bolt-sidecar/Cargo.lock

Large diffs are not rendered by default.

20 changes: 8 additions & 12 deletions bolt-sidecar/Cargo.toml
Original file line number Diff line number Diff line change
@@ -8,23 +8,17 @@ default-run = "bolt-sidecar"
# core
clap = { version = "4.5.4", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
axum = { version = "0.7", features = ["macros"] }
warp = "0.3.7"
futures = "0.3"
axum = { version = "0.7", features = ["macros"] }

# crypto
blst = "0.3.12"
secp256k1 = { version = "0.29.0", features = ["rand"] }

# alloy
alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", features = [
"reqwest",
"ws",
"pubsub",
] }
alloy-provider = { git = "https://github.com/alloy-rs/alloy", features = [
"ws",
] }
alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", features = ["reqwest", "ws", "pubsub"] }
alloy-provider = { git = "https://github.com/alloy-rs/alloy", features = ["ws"] }
alloy-signer = { git = "https://github.com/alloy-rs/alloy" }
alloy-signer-local = { git = "https://github.com/alloy-rs/alloy" }
alloy-transport = { git = "https://github.com/alloy-rs/alloy" }
@@ -34,14 +28,16 @@ alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy" }
alloy-pubsub = { git = "https://github.com/alloy-rs/alloy" }
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy" }
alloy-rpc-types-beacon = { git = "https://github.com/alloy-rs/alloy" }
alloy-consensus = { git = "https://github.com/alloy-rs/alloy", features = [
"k256",
] }
alloy-consensus = { git = "https://github.com/alloy-rs/alloy", features = ["k256"] }
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy" }
alloy-primitives = { version = "0.7.1", features = ["rand"] }
alloy-network = { git = "https://github.com/alloy-rs/alloy" }
alloy-rlp = "0.3"

# reth
reth-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "71c404d" }
# reth-provider = { git = "https://github.com/paradigmxyz/reth", rev = "71c404d" }

reqwest = "0.12"
ethereum-consensus = { git = "https://github.com/ralexstokes/ethereum-consensus", rev = "cf3c404" }
beacon-api-client = { git = "https://github.com/ralexstokes/ethereum-consensus", rev = "cf3c404" }
44 changes: 29 additions & 15 deletions bolt-sidecar/bin/sidecar.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Duration;

use bolt_sidecar::{
crypto::{
bls::{from_bls_signature_to_consensus_signature, Signer, SignerBLS},
@@ -6,18 +8,17 @@ use bolt_sidecar::{
json_rpc::{api::ApiError, start_server},
primitives::{
BatchedSignedConstraints, ChainHead, CommitmentRequest, ConstraintsMessage,
LocalPayloadFetcher, NoopPayloadFetcher, SignedConstraints,
LocalPayloadFetcher, SignedConstraints,
},
spec::ConstraintsApi,
start_builder_proxy,
state::{
fetcher::{StateClient, StateFetcher},
ExecutionState,
},
BuilderProxyConfig, Config, MevBoostClient, Opts,
BuilderProxyConfig, Config, MevBoostClient,
};

use clap::Parser;
use tokio::sync::mpsc;
use tracing::info;

@@ -27,31 +28,34 @@ async fn main() -> eyre::Result<()> {

info!("Starting sidecar");

let opts = Opts::parse();
let config = Config::try_from(opts)?;
let config = Config::parse_from_cli()?;

let (api_events, mut api_events_rx) = mpsc::channel(1024);

// TODO: support external signers
let signer = Signer::new(config.private_key.clone().unwrap());

let state_client = StateClient::new(&config.execution_api, 8);
let mevboost_client = MevBoostClient::new(&config.mevboost_url);

let head = state_client.get_head().await?;

let mut execution_state = ExecutionState::new(state_client, ChainHead::new(0, head)).await?;

let mevboost_client = MevBoostClient::new(config.mevboost_url.clone());

let shutdown_tx = start_server(config, api_events).await?;

let builder_proxy_config = BuilderProxyConfig::default();

let (payload_tx, mut payload_rx) = mpsc::channel(1);
let _payload_fetcher = LocalPayloadFetcher::new(payload_tx);

let _builder_proxy = tokio::spawn(async move {
if let Err(e) = start_builder_proxy(NoopPayloadFetcher, builder_proxy_config).await {
tracing::error!("Builder proxy failed: {:?}", e);
let payload_fetcher = LocalPayloadFetcher::new(payload_tx);

tokio::spawn(async move {
loop {
if let Err(e) =
start_builder_proxy(payload_fetcher.clone(), builder_proxy_config.clone()).await
{
tracing::error!("Builder API proxy failed: {:?}", e);
tokio::time::sleep(Duration::from_secs(5)).await;
}
}
});

@@ -94,8 +98,18 @@ async fn main() -> eyre::Result<()> {
}
Some(request) = payload_rx.recv() => {
tracing::info!("Received payload request: {:?}", request);
let _response = execution_state.get_block_template(request.slot);
// TODO: extract payload & bid
let Some(response) = execution_state.get_block_template(request.slot) else {
tracing::warn!("No block template found for slot {} when requested", request.slot);
let _ = request.response.send(None);
continue;
};

// For fallback block building, we need to turn a block template into an actual SignedBuilderBid.
// This will also require building the full ExecutionPayload that we want the proposer to commit to.
// Once we have that, we need to send it as response to the validator via the pending get_header RPC call.
// The validator will then call get_payload with the corresponding SignedBlindedBeaconBlock. We then need to
// respond with the full ExecutionPayload inside the BeaconBlock (+ blobs if any).

let _ = request.response.send(None);
}

6 changes: 4 additions & 2 deletions bolt-sidecar/src/api/builder.rs
Original file line number Diff line number Diff line change
@@ -113,6 +113,8 @@ impl<T: ConstraintsApi, P: PayloadFetcher + Send + Sync> BuilderProxyServer<T, P
{
Ok(Ok(header)) => {
tracing::debug!(elapsed = ?start.elapsed(), "Returning signed builder bid: {:?}", header);
// TODO: verify proofs here. If they are invalid, we should fall back to locally built block

Ok(Json(header.bid))
}
Ok(Err(_)) | Err(_) => {
@@ -210,14 +212,14 @@ impl Default for BuilderProxyConfig {
pub async fn start_builder_proxy<P: PayloadFetcher + Send + Sync + 'static>(
payload_fetcher: P,
config: BuilderProxyConfig,
) -> Result<(), Box<dyn std::error::Error>> {
) -> Result<(), eyre::Error> {
tracing::info!(
port = config.port,
target = config.mev_boost_url,
"Starting builder proxy..."
);

let mev_boost = MevBoostClient::new(config.mev_boost_url);
let mev_boost = MevBoostClient::new(&config.mev_boost_url);
let server = Arc::new(BuilderProxyServer::new(mev_boost, payload_fetcher));
let router = Router::new()
.route("/", get(index))
4 changes: 4 additions & 0 deletions bolt-sidecar/src/builder/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod template;
pub use template::BlockTemplate;

pub mod payload_builder;
200 changes: 200 additions & 0 deletions bolt-sidecar/src/builder/payload_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
#![allow(missing_docs)]
#![allow(unused)]

use alloy_consensus::TxEnvelope;
use alloy_primitives::{Address, Bytes, B256, U256};
use ethereum_consensus::{
capella::spec,
crypto::bls::{PublicKey as BlsPublicKey, SecretKey as BlsSecretKey},
deneb::mainnet::ExecutionPayloadHeader,
ssz::prelude::{ssz_rs, ByteList, ByteVector, List},
types::mainnet::ExecutionPayload,
};
use reth_primitives::{
constants::BEACON_NONCE, proofs, BlockBody, Bloom, Header, SealedBlock, SealedHeader,
TransactionSigned, EMPTY_OMMER_ROOT_HASH,
};

use crate::primitives::{BuilderBid, Slot};

#[derive(Debug, thiserror::Error)]
pub enum PayloadBuilderError {
#[error("Failed to build payload: {0}")]
Custom(String),
}

#[derive(Debug)]
pub struct FallbackPayloadBuilder<SRP>
where
SRP: StateRootProvider,
{
state_root_provider: SRP,

fee_recipient: Address,

// keypair used for signing the payload
private_key: BlsSecretKey,
public_key: BlsPublicKey,
}

/// Minimal execution context required to build a valid payload on the target slot.
#[derive(Debug)]
pub struct ExecutionContext {
head_slot_number: Slot,
parent_hash: B256,
transactions: Vec<TransactionSigned>,
block: NextBlockInfo,
}

#[derive(Debug)]
pub struct NextBlockInfo {
number: u64,
timestamp: u64,
prev_randao: B256,
base_fee: u64,
extra_data: Bytes,
gas_limit: u64,
}

/// Provider that is able to compute the state root over a set of state diffs.
/// TODO: how do we avoid full access to the state DB here?
pub trait StateRootProvider {
fn get_state_root(&self) -> Result<B256, PayloadBuilderError>;
}

impl<SRP> FallbackPayloadBuilder<SRP>
where
SRP: StateRootProvider,
{
/// Build a minimal payload to be used as a fallback
pub async fn build_fallback_payload(
&self,
context: ExecutionContext,
) -> Result<BuilderBid, PayloadBuilderError> {
// TODO: actually get the state root (needs to count post-state diffs)
let state_root = self.state_root_provider.get_state_root()?;
let transactions_root = proofs::calculate_transaction_root(&context.transactions);

// TODO: fill all of these with correct values
let withdrawals_root = Some(B256::default());
let receipts_root = B256::default();
let logs_bloom = Bloom::default();
let gas_used = 0;
let parent_beacon_root = B256::default();
let value = U256::ZERO;

let header = Header {
parent_hash: context.parent_hash,
ommers_hash: EMPTY_OMMER_ROOT_HASH,
beneficiary: self.fee_recipient,
state_root,
transactions_root,
receipts_root,
withdrawals_root,
logs_bloom,
difficulty: U256::ZERO,
number: context.block.number,
gas_limit: context.block.gas_limit,
gas_used,
timestamp: context.block.timestamp,
mix_hash: context.block.prev_randao,
nonce: BEACON_NONCE,
base_fee_per_gas: Some(context.block.base_fee),
blob_gas_used: None,
excess_blob_gas: None,
parent_beacon_block_root: Some(parent_beacon_root),
extra_data: context.block.extra_data,
};

let body = BlockBody {
transactions: context.transactions,
ommers: Vec::new(),
withdrawals: None,
};

let sealed_block = SealedBlock::new(header.seal_slow(), body);
let submission = BuilderBid {
header: to_execution_payload_header(&sealed_block.header),
blob_kzg_commitments: List::default(),
public_key: self.public_key.clone(),
value,
};

Ok(submission)
}
}

pub(crate) fn to_execution_payload_header(value: &SealedHeader) -> ExecutionPayloadHeader {
ExecutionPayloadHeader {
parent_hash: to_bytes32(value.parent_hash),
fee_recipient: to_bytes20(value.beneficiary),
state_root: to_bytes32(value.state_root),
receipts_root: to_bytes32(value.receipts_root),
logs_bloom: to_byte_vector(value.logs_bloom),
prev_randao: to_bytes32(value.mix_hash),
block_number: value.number,
gas_limit: value.gas_limit,
gas_used: value.gas_used,
timestamp: value.timestamp,
extra_data: ByteList::try_from(value.extra_data.as_ref()).unwrap(),
base_fee_per_gas: ssz_rs::U256::from(value.base_fee_per_gas.unwrap_or_default()),
block_hash: to_bytes32(value.hash()),
transactions_root: value.transactions_root,
withdrawals_root: value.withdrawals_root.unwrap_or_default(),
blob_gas_used: value.blob_gas_used.unwrap_or_default(),
excess_blob_gas: value.excess_blob_gas.unwrap_or_default(),
}
}

pub(crate) fn to_execution_payload(value: &SealedBlock) -> ExecutionPayload {
let hash = value.hash();
let header = &value.header;
let transactions = &value.body;
let withdrawals = &value.withdrawals;
let transactions = transactions
.iter()
.map(|t| spec::Transaction::try_from(t.envelope_encoded().as_ref()).unwrap())
.collect::<Vec<_>>();
let withdrawals = withdrawals
.as_ref()
.unwrap()
.iter()
.map(|w| spec::Withdrawal {
index: w.index as usize,
validator_index: w.validator_index as usize,
address: to_bytes20(w.address),
amount: w.amount,
})
.collect::<Vec<_>>();

let payload = spec::ExecutionPayload {
parent_hash: to_bytes32(header.parent_hash),
fee_recipient: to_bytes20(header.beneficiary),
state_root: to_bytes32(header.state_root),
receipts_root: to_bytes32(header.receipts_root),
logs_bloom: to_byte_vector(header.logs_bloom),
prev_randao: to_bytes32(header.mix_hash),
block_number: header.number,
gas_limit: header.gas_limit,
gas_used: header.gas_used,
timestamp: header.timestamp,
extra_data: ByteList::try_from(header.extra_data.as_ref()).unwrap(),
base_fee_per_gas: ssz_rs::U256::from(header.base_fee_per_gas.unwrap_or_default()),
block_hash: to_bytes32(hash),
transactions: TryFrom::try_from(transactions).unwrap(),
withdrawals: TryFrom::try_from(withdrawals).unwrap(),
};
ExecutionPayload::Capella(payload)
}

fn to_bytes32(value: B256) -> spec::Bytes32 {
spec::Bytes32::try_from(value.as_ref()).unwrap()
}

fn to_bytes20(value: Address) -> spec::ExecutionAddress {
spec::ExecutionAddress::try_from(value.as_ref()).unwrap()
}

fn to_byte_vector(value: Bloom) -> ByteVector<256> {
ByteVector::<256>::try_from(value.as_ref()).unwrap()
}
Original file line number Diff line number Diff line change
@@ -116,21 +116,6 @@ impl BlockTemplate {
}
}

/// StateDiff tracks the intermediate changes to the state according to the block template.
#[derive(Debug, Default)]
pub struct StateDiff {
diffs: HashMap<Address, (u64, U256)>,
}

impl StateDiff {
/// Returns a tuple of the nonce and balance diff for the given address.
/// The nonce diff should be added to the current nonce, the balance diff should be subtracted from
/// the current balance.
pub fn get_diff(&self, address: &Address) -> Option<(u64, U256)> {
self.diffs.get(address).copied()
}
}

impl TryFrom<BlockTemplate> for PayloadAndBid {
type Error = Box<dyn std::error::Error>;

@@ -144,6 +129,28 @@ impl TryFrom<BlockTemplate> for PayloadAndBid {
},
signature: todo!(),
};

Ok(PayloadAndBid {
payload: todo!(),
bid,
})
}
}

/// StateDiff tracks the intermediate changes to the state according to the block template.
#[derive(Debug, Default)]
pub struct StateDiff {
/// Map of diffs per address. Each diff is a tuple of the nonce and balance diff
/// that should be applied to the current state.
diffs: HashMap<Address, (u64, U256)>,
}

impl StateDiff {
/// Returns a tuple of the nonce and balance diff for the given address.
/// The nonce diff should be added to the current nonce, the balance diff should be subtracted from
/// the current balance.
pub fn get_diff(&self, address: &Address) -> Option<(u64, U256)> {
self.diffs.get(address).copied()
}
}

2 changes: 1 addition & 1 deletion bolt-sidecar/src/client/mevboost.rs
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@ pub struct MevBoostClient {

impl MevBoostClient {
/// Creates a new MEV-Boost client with the given URL.
pub fn new(url: String) -> Self {
pub fn new(url: &str) -> Self {
Self {
url: url.trim_end_matches('/').to_string(),
client: reqwest::ClientBuilder::new()
4 changes: 2 additions & 2 deletions bolt-sidecar/src/client/rpc.rs
Original file line number Diff line number Diff line change
@@ -16,8 +16,8 @@ use reqwest::{Client, Url};

use crate::primitives::AccountState;

/// An HTTP-based JSON-RPC client that supports batching. Implements all methods that are relevant
/// to Bolt state.
/// An HTTP-based JSON-RPC client that supports batching.
/// Implements all methods that are relevant to Bolt state.
#[derive(Clone, Debug)]
pub struct RpcClient(alloy::RpcClient<Http<Client>>);

8 changes: 8 additions & 0 deletions bolt-sidecar/src/config.rs
Original file line number Diff line number Diff line change
@@ -73,6 +73,14 @@ impl Default for Config {
}
}

impl Config {
/// Parse the command-line options and return a new `Config` instance
pub fn parse_from_cli() -> eyre::Result<Self> {
let opts = Opts::parse();
Self::try_from(opts)
}
}

impl TryFrom<Opts> for Config {
type Error = eyre::Report;

2 changes: 1 addition & 1 deletion bolt-sidecar/src/lib.rs
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ mod common;
/// Functionality for building local block templates that can
/// be used as a fallback for proposers. It's also used to keep
/// any intermediary state that is needed to simulate EVM execution
mod template;
pub mod builder;

/// Configuration and command-line argument parsing
mod config;
40 changes: 14 additions & 26 deletions bolt-sidecar/src/primitives/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// TODO: add docs
#![allow(missing_docs)]

use std::sync::{atomic::AtomicU64, Arc};

use alloy_primitives::{Bytes, TxHash, U256};
use alloy_primitives::U256;
use ethereum_consensus::{
capella,
crypto::{KzgCommitment, PublicKey as BlsPublicKey, Signature as BlsSignature},
@@ -32,23 +35,6 @@ pub use transaction::TxInfo;
/// An alias for a Beacon Chain slot number
pub type Slot = u64;

/// An enum representing all possible (signed) commitments.
#[derive(Debug)]
pub enum Commitment {
/// Inclusion commitment, accepted and signed by the proposer
/// through this sidecar's signer.
Inclusion(InclusionCommitment),
}

#[derive(Debug)]
pub struct InclusionCommitment {
pub slot: Slot,
pub tx_hash: TxHash,
pub raw_tx: Bytes,
// TODO:
pub signature: Bytes,
}

/// Minimal account state needed for commitment validation.
#[derive(Debug, Clone, Copy)]
pub struct AccountState {
@@ -79,14 +65,6 @@ pub struct SignedBuilderBidWithProofs {
pub proofs: List<ConstraintProof, 300>,
}

#[derive(Debug, Default, Clone, SimpleSerialize, serde::Serialize, serde::Deserialize)]
pub struct MerkleMultiProof {
// We use List here for SSZ, TODO: choose max
transaction_hashes: List<Hash32, 300>,
generalized_indexes: List<u64, 300>,
merkle_hashes: List<Hash32, 1000>,
}

#[derive(Debug, Default, Clone, SimpleSerialize, serde::Serialize, serde::Deserialize)]
pub struct ConstraintProof {
#[serde(rename = "txHash")]
@@ -102,6 +80,14 @@ pub struct MerkleProof {
hashes: List<Hash32, 1000>,
}

#[derive(Debug, Default, Clone, SimpleSerialize, serde::Serialize, serde::Deserialize)]
pub struct MerkleMultiProof {
// We use List here for SSZ, TODO: choose max
transaction_hashes: List<Hash32, 300>,
generalized_indexes: List<u64, 300>,
merkle_hashes: List<Hash32, 1000>,
}

#[derive(Debug)]
pub struct FetchPayloadRequest {
pub slot: u64,
@@ -114,6 +100,7 @@ pub struct PayloadAndBid {
pub payload: GetPayloadResponse,
}

#[derive(Debug, Clone)]
pub struct LocalPayloadFetcher {
tx: mpsc::Sender<FetchPayloadRequest>,
}
@@ -142,6 +129,7 @@ pub trait PayloadFetcher {
async fn fetch_payload(&self, slot: u64) -> Option<PayloadAndBid>;
}

#[derive(Debug)]
pub struct NoopPayloadFetcher;

#[async_trait::async_trait]
2 changes: 1 addition & 1 deletion bolt-sidecar/src/state/execution.rs
Original file line number Diff line number Diff line change
@@ -5,9 +5,9 @@ use std::collections::HashMap;
use thiserror::Error;

use crate::{
builder::BlockTemplate,
common::{calculate_max_basefee, validate_transaction},
primitives::{AccountState, ChainHead, CommitmentRequest, Slot},
template::BlockTemplate,
};

use super::{fetcher::StateFetcher, StateError};
7 changes: 6 additions & 1 deletion bolt-sidecar/src/state/fetcher.rs
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ const MAX_RETRIES: u32 = 8;
/// The retry backoff in milliseconds.
const RETRY_BACKOFF_MS: u64 = 200;

/// A trait for fetching state updates.
pub trait StateFetcher {
async fn get_state_update(
&self,
@@ -23,21 +24,25 @@ pub trait StateFetcher {
) -> Result<StateUpdate, TransportError>;

async fn get_head(&self) -> Result<u64, TransportError>;

async fn get_basefee(&self, block_number: Option<u64>) -> Result<u128, TransportError>;

async fn get_account_state(
&self,
address: &Address,
block_number: Option<u64>,
) -> Result<AccountState, TransportError>;
}

#[derive(Clone)]
/// A basic state fetcher that uses an RPC client to fetch state updates.
#[derive(Clone, Debug)]
pub struct StateClient {
client: RpcClient,
retry_backoff: Duration,
}

impl StateClient {
/// Create a new `StateClient` with the given URL and maximum retries.
pub fn new(url: &str, max_retries: u32) -> Self {
let client = RpcClient::new(url);
Self {
178 changes: 0 additions & 178 deletions bolt-sidecar/src/types/mod.rs

This file was deleted.

0 comments on commit 9983438

Please sign in to comment.