Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sidecar): build valid fallback payload #89

Merged
merged 1 commit into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to verify proofs again imo. We can assume mev-boost & relay have already done this. If mev-boost verification fails it will just return no bid

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, just thought it would be low cost, plus as a reminder for #24 which is still open


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