Skip to content

Commit

Permalink
Merge pull request #89 from chainbound/feat/create-fallback-block
Browse files Browse the repository at this point in the history
feat(sidecar): build valid fallback payload
  • Loading branch information
mempirate authored Jun 24, 2024
2 parents c97e519 + 9983438 commit ca1b391
Show file tree
Hide file tree
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
Expand Up @@ -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" }
Expand All @@ -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" }
Expand Down
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},
Expand All @@ -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;

Expand All @@ -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;
}
}
});

Expand Down Expand Up @@ -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);
}

Expand Down
6 changes: 4 additions & 2 deletions bolt-sidecar/src/api/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(_) => {
Expand Down Expand Up @@ -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))
Expand Down
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()
}
Loading

0 comments on commit ca1b391

Please sign in to comment.