diff --git a/crates/rbuilder/src/mev_boost/adjustment.rs b/crates/rbuilder/src/mev_boost/adjustment.rs new file mode 100644 index 000000000..e80640fc7 --- /dev/null +++ b/crates/rbuilder/src/mev_boost/adjustment.rs @@ -0,0 +1,42 @@ +use alloy_primitives::{Address, Bytes, B256}; + +/// The type representing UltraSound bid adjustments. +#[derive( + PartialEq, + Eq, + Clone, + Debug, + serde::Serialize, + serde::Deserialize, + ssz_derive::Encode, + ssz_derive::Decode, +)] +pub struct BidAdjustmentData { + /// State root of the payload. + pub state_root: B256, + /// Transactions root of the payload. + pub transactions_root: B256, + /// Receipts root of the payload. + pub receipts_root: B256, + /// The usual builder address that pays the proposer in the last transaction of the block. + /// When we adjust a bid, this transaction is overwritten by a transaction from the collateral + /// account `fee_payer_address`. If we don't adjust the bid, `builder_address` pays the + /// proposer as per usual. + pub builder_address: Address, + /// The state proof for the builder account. + pub builder_proof: Vec, + /// The proposer's fee recipient. + pub fee_recipient_address: Address, + /// The state proof for the fee recipient account. + pub fee_recipient_proof: Vec, + /// The fee payer address that is custodied by the relay. + pub fee_payer_address: Address, + /// The state proof for the fee payer account. + pub fee_payer_proof: Vec, + /// The merkle proof for the last transaction in the block, which will be overwritten with a + /// payment from `fee_payer` to `fee_recipient` if we adjust the bid. + pub placeholder_transaction_proof: Vec, + /// The merkle proof for the receipt of the placeholder transaction. It's required for + /// adjusting payments to contract addresses. + pub placeholder_receipt_proof: Vec, +} diff --git a/crates/rbuilder/src/mev_boost/mod.rs b/crates/rbuilder/src/mev_boost/mod.rs index 88a966c9e..4836c411b 100644 --- a/crates/rbuilder/src/mev_boost/mod.rs +++ b/crates/rbuilder/src/mev_boost/mod.rs @@ -1,3 +1,4 @@ +pub mod adjustment; mod error; pub mod fake_mev_boost_relay; pub mod rpc; @@ -18,7 +19,7 @@ use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; use ssz::Encode; use std::{io::Write, str::FromStr}; -use submission::{SubmitBlockRequest, SubmitBlockRequestNoBlobs, SubmitBlockRequestWithMetadata}; +use submission::{SubmitBlockRequestNoBlobs, SubmitBlockRequestWithMetadata}; use url::Url; pub use error::*; @@ -500,11 +501,7 @@ impl RelayClient { // SSZ vs JSON let (mut body_data, content_type) = if ssz { ( - match &submission_with_metadata.submission { - SubmitBlockRequest::Capella(data) => data.0.as_ssz_bytes(), - SubmitBlockRequest::Deneb(data) => data.0.as_ssz_bytes(), - SubmitBlockRequest::Electra(data) => data.0.as_ssz_bytes(), - }, + submission_with_metadata.submission.as_ssz_bytes(), SSZ_CONTENT_TYPE, ) } else { @@ -694,7 +691,9 @@ mod tests { use submission::{BidMetadata, BidValueMetadata}; use super::{rpc::TestDataGenerator, *}; - use crate::mev_boost::fake_mev_boost_relay::FakeMevBoostRelay; + use crate::mev_boost::{ + fake_mev_boost_relay::FakeMevBoostRelay, submission::SubmitBlockRequest, + }; use std::str::FromStr; diff --git a/crates/rbuilder/src/mev_boost/rpc.rs b/crates/rbuilder/src/mev_boost/rpc.rs index 8d2b1fe81..32eea91b4 100644 --- a/crates/rbuilder/src/mev_boost/rpc.rs +++ b/crates/rbuilder/src/mev_boost/rpc.rs @@ -19,12 +19,15 @@ pub struct TestDataGenerator { impl TestDataGenerator { pub fn create_deneb_submit_block_request(&mut self) -> DenebSubmitBlockRequest { - DenebSubmitBlockRequest(SignedBidSubmissionV3 { - message: self.create_bid_trace(), - execution_payload: self.create_deneb_payload(), - blobs_bundle: self.create_txs_blobs_sidecars(), - signature: self.create_signature(), - }) + DenebSubmitBlockRequest { + submission: SignedBidSubmissionV3 { + message: self.create_bid_trace(), + execution_payload: self.create_deneb_payload(), + blobs_bundle: self.create_txs_blobs_sidecars(), + signature: self.create_signature(), + }, + adjustment_data: None, + } } pub fn create_txs_blobs_sidecars(&mut self) -> BlobsBundleV1 { diff --git a/crates/rbuilder/src/mev_boost/sign_payload.rs b/crates/rbuilder/src/mev_boost/sign_payload.rs index 360f601d1..b4e9bed98 100644 --- a/crates/rbuilder/src/mev_boost/sign_payload.rs +++ b/crates/rbuilder/src/mev_boost/sign_payload.rs @@ -3,13 +3,12 @@ use super::submission::{ SubmitBlockRequest, }; use crate::utils::u256decimal_serde_helper; -use alloy_eips::eip7685::Requests; -use alloy_eips::{eip2718::Encodable2718, eip4844::BlobTransactionSidecar}; +use alloy_eips::{eip2718::Encodable2718, eip4844::BlobTransactionSidecar, eip7685::Requests}; use alloy_primitives::{Address, BlockHash, Bytes, FixedBytes, B256, U256}; -use alloy_rpc_types_beacon::requests::ExecutionRequestsV4; use alloy_rpc_types_beacon::{ events::PayloadAttributesData, relay::{BidTrace, SignedBidSubmissionV2, SignedBidSubmissionV3, SignedBidSubmissionV4}, + requests::ExecutionRequestsV4, BlsPublicKey, }; use alloy_rpc_types_engine::{ @@ -127,6 +126,9 @@ pub fn sign_block_for_relay( pubkey: H384, value: U256, ) -> eyre::Result { + // TODO: add support for bid adjustments + let adjustment_data = None; + let message = BidTrace { slot: attrs.proposal_slot, parent_hash: attrs.parent_block_hash, @@ -185,7 +187,7 @@ pub fn sign_block_for_relay( .unwrap_or_default(), }; - let submit_block_request = if chain_spec.is_cancun_active_at_timestamp(sealed_block.timestamp) { + let request = if chain_spec.is_cancun_active_at_timestamp(sealed_block.timestamp) { let execution_payload = ExecutionPayloadV3 { payload_inner: capella_payload, blob_gas_used: sealed_block @@ -200,31 +202,33 @@ pub fn sign_block_for_relay( let execution_requests = ExecutionRequestsV4::try_from(Requests::new(execution_requests.to_vec()))?; if chain_spec.is_prague_active_at_timestamp(sealed_block.timestamp) { - SubmitBlockRequest::Electra(ElectraSubmitBlockRequest(SignedBidSubmissionV4 { + let submission = SignedBidSubmissionV4 { message, execution_payload, blobs_bundle, signature, execution_requests, - })) + }; + SubmitBlockRequest::electra(ElectraSubmitBlockRequest::new(submission, adjustment_data)) } else { - SubmitBlockRequest::Deneb(DenebSubmitBlockRequest(SignedBidSubmissionV3 { + let submission = SignedBidSubmissionV3 { message, execution_payload, blobs_bundle, signature, - })) + }; + SubmitBlockRequest::deneb(DenebSubmitBlockRequest::new(submission, adjustment_data)) } } else { - let execution_payload = capella_payload; - SubmitBlockRequest::Capella(CapellaSubmitBlockRequest(SignedBidSubmissionV2 { + let submission = SignedBidSubmissionV2 { message, - execution_payload, + execution_payload: capella_payload, signature, - })) + }; + SubmitBlockRequest::capella(CapellaSubmitBlockRequest::new(submission, adjustment_data)) }; - Ok(submit_block_request) + Ok(request) } fn flatten_marshal( diff --git a/crates/rbuilder/src/mev_boost/submission.rs b/crates/rbuilder/src/mev_boost/submission.rs index 0661057e7..10a5f12f5 100644 --- a/crates/rbuilder/src/mev_boost/submission.rs +++ b/crates/rbuilder/src/mev_boost/submission.rs @@ -4,28 +4,16 @@ use alloy_rpc_types_beacon::{ requests::ExecutionRequestsV4, BlsSignature, }; -use alloy_rpc_types_engine::{BlobsBundleV1, ExecutionPayloadV3}; +use alloy_rpc_types_engine::{BlobsBundleV1, ExecutionPayloadV2, ExecutionPayloadV3}; +use derive_more::Deref; use serde::{Deserialize, Serialize}; -use ssz::{Decode, DecodeError, Encode}; +use ssz::DecodeError; +use super::adjustment::BidAdjustmentData; use crate::primitives::OrderId; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ElectraSubmitBlockRequest(pub SignedBidSubmissionV4); - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct DenebSubmitBlockRequest(pub SignedBidSubmissionV3); - -impl DenebSubmitBlockRequest { - pub fn as_ssz_bytes(&self) -> Vec { - self.0.as_ssz_bytes() - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct CapellaSubmitBlockRequest(pub SignedBidSubmissionV2); - -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, ssz_derive::Encode)] +#[ssz(enum_behaviour = "transparent")] #[serde(untagged)] pub enum SubmitBlockRequest { Capella(CapellaSubmitBlockRequest), @@ -34,28 +22,344 @@ pub enum SubmitBlockRequest { } impl SubmitBlockRequest { + #[inline] + pub fn capella(request: CapellaSubmitBlockRequest) -> Self { + Self::Capella(request) + } + + #[inline] + pub fn deneb(request: DenebSubmitBlockRequest) -> Self { + Self::Deneb(request) + } + + #[inline] + pub fn electra(request: ElectraSubmitBlockRequest) -> Self { + Self::Electra(request) + } + pub fn bid_trace(&self) -> &BidTrace { match self { - SubmitBlockRequest::Capella(req) => &req.0.message, - SubmitBlockRequest::Deneb(req) => &req.0.message, - SubmitBlockRequest::Electra(req) => &req.0.message, + SubmitBlockRequest::Capella(req) => &req.message, + SubmitBlockRequest::Deneb(req) => &req.message, + SubmitBlockRequest::Electra(req) => &req.message, } } +} + +impl ssz::Decode for SubmitBlockRequest { + fn is_ssz_fixed_len() -> bool { + false + } - pub fn from_ssz_bytes(bytes: &[u8]) -> Result { - if let Ok(result) = SignedBidSubmissionV4::from_ssz_bytes(bytes) { - return Ok(SubmitBlockRequest::Electra(ElectraSubmitBlockRequest( - result, - ))); + fn from_ssz_bytes(bytes: &[u8]) -> Result { + if let Ok(result) = ElectraSubmitBlockRequest::from_ssz_bytes(bytes) { + return Ok(Self::electra(result)); } - if let Ok(result) = SignedBidSubmissionV3::from_ssz_bytes(bytes) { - return Ok(SubmitBlockRequest::Deneb(DenebSubmitBlockRequest(result))); + if let Ok(result) = DenebSubmitBlockRequest::from_ssz_bytes(bytes) { + return Ok(Self::deneb(result)); + } + + let result = CapellaSubmitBlockRequest::from_ssz_bytes(bytes)?; + Ok(Self::capella(result)) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Deref)] +pub struct ElectraSubmitBlockRequest { + /// Inner bid submission. + #[deref] + #[serde(flatten)] + pub submission: SignedBidSubmissionV4, + /// Bid adjustment data if present. + #[serde(skip_serializing_if = "Option::is_none")] + pub adjustment_data: Option, +} + +impl ElectraSubmitBlockRequest { + /// Create new Electra submit block request. + pub fn new( + submission: SignedBidSubmissionV4, + adjustment_data: Option, + ) -> Self { + Self { + submission, + adjustment_data, } + } +} - let result = SignedBidSubmissionV2::from_ssz_bytes(bytes)?; - Ok(SubmitBlockRequest::Capella(CapellaSubmitBlockRequest( - result, - ))) +impl ssz::Encode for ElectraSubmitBlockRequest { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_append(&self, buf: &mut Vec) { + let mut offset = ::ssz_fixed_len() + + ::ssz_fixed_len() + + ::ssz_fixed_len() + + ::ssz_fixed_len() + + ::ssz_fixed_len(); + if self.adjustment_data.is_some() { + offset += ::ssz_fixed_len(); + } + + let mut encoder = ssz::SszEncoder::container(buf, offset); + + encoder.append(&self.message); + encoder.append(&self.execution_payload); + encoder.append(&self.blobs_bundle); + encoder.append(&self.execution_requests); + encoder.append(&self.signature); + if let Some(adjustment) = &self.adjustment_data { + encoder.append(&adjustment); + } + + encoder.finalize(); + } + + fn ssz_bytes_len(&self) -> usize { + let mut len = ::ssz_bytes_len(&self.message) + + ::ssz_bytes_len(&self.execution_payload) + + ::ssz_bytes_len(&self.blobs_bundle) + + ::ssz_bytes_len(&self.execution_requests) + + ::ssz_bytes_len(&self.signature); + if let Some(adjustment) = &self.adjustment_data { + len += ::ssz_bytes_len(adjustment); + } + len + } +} + +impl ssz::Decode for ElectraSubmitBlockRequest { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + #[derive(ssz_derive::Decode)] + struct ElectraSubmitBlockRequestSszHelper { + message: BidTrace, + execution_payload: ExecutionPayloadV3, + blobs_bundle: BlobsBundleV1, + execution_requests: ExecutionRequestsV4, + signature: BlsSignature, + adjustment_data: BidAdjustmentData, + } + + if let Ok(request) = ElectraSubmitBlockRequestSszHelper::from_ssz_bytes(bytes) { + let ElectraSubmitBlockRequestSszHelper { + message, + execution_payload, + blobs_bundle, + execution_requests, + signature, + adjustment_data, + } = request; + let submission = SignedBidSubmissionV4 { + message, + execution_payload, + blobs_bundle, + execution_requests, + signature, + }; + Ok(Self::new(submission, Some(adjustment_data))) + } else { + let submission = SignedBidSubmissionV4::from_ssz_bytes(bytes)?; + Ok(Self::new(submission, None)) + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Deref)] +pub struct DenebSubmitBlockRequest { + /// Inner bid submission. + #[deref] + #[serde(flatten)] + pub submission: SignedBidSubmissionV3, + /// Bid adjustment data if present. + #[serde(skip_serializing_if = "Option::is_none")] + pub adjustment_data: Option, +} + +impl DenebSubmitBlockRequest { + /// Create new Deneb submit block request. + pub fn new( + submission: SignedBidSubmissionV3, + adjustment_data: Option, + ) -> Self { + Self { + submission, + adjustment_data, + } + } +} + +impl ssz::Encode for DenebSubmitBlockRequest { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_append(&self, buf: &mut Vec) { + let mut offset = ::ssz_fixed_len() + + ::ssz_fixed_len() + + ::ssz_fixed_len() + + ::ssz_fixed_len(); + if self.adjustment_data.is_some() { + offset += ::ssz_fixed_len(); + } + + let mut encoder = ssz::SszEncoder::container(buf, offset); + + encoder.append(&self.message); + encoder.append(&self.execution_payload); + encoder.append(&self.blobs_bundle); + encoder.append(&self.signature); + if let Some(adjustment) = &self.adjustment_data { + encoder.append(&adjustment); + } + + encoder.finalize(); + } + + fn ssz_bytes_len(&self) -> usize { + let mut len = ::ssz_bytes_len(&self.message) + + ::ssz_bytes_len(&self.execution_payload) + + ::ssz_bytes_len(&self.blobs_bundle) + + ::ssz_bytes_len(&self.signature); + if let Some(adjustment) = &self.adjustment_data { + len += ::ssz_bytes_len(adjustment); + } + len + } +} + +impl ssz::Decode for DenebSubmitBlockRequest { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + #[derive(ssz_derive::Decode)] + struct DenebSubmitBlockRequestSszHelper { + message: BidTrace, + execution_payload: ExecutionPayloadV3, + blobs_bundle: BlobsBundleV1, + signature: BlsSignature, + adjustment_data: BidAdjustmentData, + } + + if let Ok(request) = DenebSubmitBlockRequestSszHelper::from_ssz_bytes(bytes) { + let DenebSubmitBlockRequestSszHelper { + message, + execution_payload, + blobs_bundle, + signature, + adjustment_data, + } = request; + let submission = SignedBidSubmissionV3 { + message, + execution_payload, + blobs_bundle, + signature, + }; + Ok(Self::new(submission, Some(adjustment_data))) + } else { + let submission = SignedBidSubmissionV3::from_ssz_bytes(bytes)?; + Ok(Self::new(submission, None)) + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Deref)] +pub struct CapellaSubmitBlockRequest { + /// Inner bid submission. + #[deref] + #[serde(flatten)] + pub submission: SignedBidSubmissionV2, + /// Bid adjustment data if present. + #[serde(skip_serializing_if = "Option::is_none")] + pub adjustment_data: Option, +} + +impl CapellaSubmitBlockRequest { + /// Create new Capella submit block request. + pub fn new( + submission: SignedBidSubmissionV2, + adjustment_data: Option, + ) -> Self { + Self { + submission, + adjustment_data, + } + } +} + +impl ssz::Encode for CapellaSubmitBlockRequest { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_append(&self, buf: &mut Vec) { + let mut offset = ::ssz_fixed_len() + + ::ssz_fixed_len() + + ::ssz_fixed_len(); + if self.adjustment_data.is_some() { + offset += ::ssz_fixed_len(); + } + + let mut encoder = ssz::SszEncoder::container(buf, offset); + encoder.append(&self.message); + encoder.append(&self.execution_payload); + encoder.append(&self.signature); + if let Some(adjustment) = &self.adjustment_data { + encoder.append(&adjustment); + } + + encoder.finalize(); + } + + fn ssz_bytes_len(&self) -> usize { + let mut len = ::ssz_bytes_len(&self.message) + + ::ssz_bytes_len(&self.execution_payload) + + ::ssz_bytes_len(&self.signature); + if let Some(adjustment) = &self.adjustment_data { + len += ::ssz_bytes_len(adjustment); + } + len + } +} + +impl ssz::Decode for CapellaSubmitBlockRequest { + fn is_ssz_fixed_len() -> bool { + false + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + #[derive(ssz_derive::Decode)] + struct CapellaSubmitBlockRequestSszHelper { + message: BidTrace, + execution_payload: ExecutionPayloadV2, + signature: BlsSignature, + adjustment_data: BidAdjustmentData, + } + + if let Ok(request) = CapellaSubmitBlockRequestSszHelper::from_ssz_bytes(bytes) { + let CapellaSubmitBlockRequestSszHelper { + message, + execution_payload, + signature, + adjustment_data, + } = request; + let submission = SignedBidSubmissionV2 { + message, + execution_payload, + signature, + }; + Ok(Self::new(submission, Some(adjustment_data))) + } else { + let submission = SignedBidSubmissionV2::from_ssz_bytes(bytes)?; + Ok(Self::new(submission, None)) + } } } @@ -96,13 +400,16 @@ impl serde::Serialize for SubmitBlockRequestNoBlobs<'_> { execution_payload: &'a ExecutionPayloadV3, blobs_bundle: &'a BlobsBundleV1, signature: &'a BlsSignature, + #[serde(skip_serializing_if = "Option::is_none")] + adjustment_data: &'a Option, } SignedBidSubmissionV3Ref { - message: &v3.0.message, - execution_payload: &v3.0.execution_payload, + message: &v3.message, + execution_payload: &v3.execution_payload, blobs_bundle: &BlobsBundleV1::new([]), // override blobs bundle with empty one - signature: &v3.0.signature, + signature: &v3.signature, + adjustment_data: &v3.adjustment_data, } .serialize(serializer) } @@ -115,14 +422,17 @@ impl serde::Serialize for SubmitBlockRequestNoBlobs<'_> { blobs_bundle: &'a BlobsBundleV1, execution_requests: &'a ExecutionRequestsV4, signature: &'a BlsSignature, + #[serde(skip_serializing_if = "Option::is_none")] + adjustment_data: &'a Option, } SignedBidSubmissionV4Ref { - message: &v4.0.message, - execution_payload: &v4.0.execution_payload, + message: &v4.message, + execution_payload: &v4.execution_payload, blobs_bundle: &BlobsBundleV1::new([]), // override blobs bundle with empty one - signature: &v4.0.signature, - execution_requests: &v4.0.execution_requests, + signature: &v4.signature, + execution_requests: &v4.execution_requests, + adjustment_data: &v4.adjustment_data, } .serialize(serializer) } diff --git a/crates/test-relay/src/relay.rs b/crates/test-relay/src/relay.rs index 42da66c37..34f8eb90a 100644 --- a/crates/test-relay/src/relay.rs +++ b/crates/test-relay/src/relay.rs @@ -20,15 +20,20 @@ use rbuilder::{ primitives::mev_boost::MevBoostRelaySlotInfoProvider, }; use serde::{Deserialize, Serialize}; -use std::time::Instant; -use std::{collections::hash_map::Entry, sync::Arc, time::Duration}; -use std::{io::Read, net::SocketAddr}; +use ssz::Decode as _; +use std::{ + collections::hash_map::Entry, + io::Read, + net::SocketAddr, + sync::Arc, + time::{Duration, Instant}, +}; use time::OffsetDateTime; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, warn}; -use warp::body; use warp::{ + body, http::status::StatusCode, query, reply::{self, Reply},