close
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
refactor: update optimistic v3 adjustments to use adjustment data v3
  • Loading branch information
yeguclu committed Jan 16, 2026
commit d4576e080877ed06647dd23693c0aefadbcb0e45
18 changes: 12 additions & 6 deletions crates/rbuilder-primitives/src/mev_boost/adjustment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ pub struct BidAdjustmentDataV1 {
ssz_derive::Encode,
ssz_derive::Decode,
)]
pub struct BidAdjustmentDataV2 {
pub struct BidAdjustmentDataV3 {
/// Transactions root of the payload.
pub el_transactions_root: B256,
/// Withdrawals root of the payload.
Expand All @@ -80,9 +80,12 @@ pub struct BidAdjustmentDataV2 {
pub cl_placeholder_transaction_proof: Vec<B256>,
/// The merkle proof for the receipt of the placeholder transaction. It's required for
/// adjusting payments to contract addresses.
pub placeholder_receipt_proof: Vec<Bytes>,
pub el_placeholder_receipt_proof: Vec<Bytes>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Field naming change

The field was renamed from placeholder_receipt_proof to el_placeholder_receipt_proof. While this naming is more consistent with the EL/CL prefix convention used elsewhere in V3, ensure this matches the exact field name expected by the relay API. The UltraSound docs should be the source of truth for field names.

The el_ prefix makes sense as this is an execution layer proof (vs consensus layer), similar to el_transactions_root and cl_placeholder_transaction_proof.

/// New in V2: Logs bloom accrued until but not including the last (payment) transaction.
pub pre_payment_logs_bloom: Bloom,
/// Gas used by the placeholder (payout) transaction. Required for V3 to relax the
/// gas_limit == gas_used requirement.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

New V3 field - verify relay compatibility

The placeholder_gas_used field is the key addition for V3 format. This is correctly documented as relaxing the gas_limit == gas_used requirement.

Please verify:

  1. This field name matches exactly what the relay expects
  2. The relay version you're targeting supports this field (ensure relay is updated to accept V3 format if not already)

pub placeholder_gas_used: u64,
}

/// Common bid adjustment information that can be used for creating bid adjustment data.
Expand All @@ -106,6 +109,8 @@ pub struct BidAdjustmentData {
pub placeholder_receipt_proof: Vec<Bytes>,
/// New in V2: Logs bloom accrued until but not including the last (payment) transaction.
pub pre_payment_logs_bloom: Bloom,
/// Gas used by the placeholder (payout) transaction.
pub placeholder_gas_used: u64,
/// State proofs.
pub state_proofs: BidAdjustmentStateProofs,
}
Expand All @@ -128,9 +133,9 @@ impl BidAdjustmentData {
}
}

/// Convert bid adjustment data into [`BidAdjustmentDataV2`].
pub fn into_v2(self) -> BidAdjustmentDataV2 {
BidAdjustmentDataV2 {
/// Convert bid adjustment data into [`BidAdjustmentDataV3`].
pub fn into_v3(self) -> BidAdjustmentDataV3 {
BidAdjustmentDataV3 {
el_transactions_root: self.el_transactions_root,
el_withdrawals_root: self.el_withdrawals_root,
builder_address: self.state_proofs.builder_address,
Expand All @@ -141,8 +146,9 @@ impl BidAdjustmentData {
fee_payer_proof: self.state_proofs.fee_payer_proof,
el_placeholder_transaction_proof: self.el_placeholder_transaction_proof,
cl_placeholder_transaction_proof: self.cl_placeholder_transaction_proof,
placeholder_receipt_proof: self.placeholder_receipt_proof,
el_placeholder_receipt_proof: self.placeholder_receipt_proof,
pre_payment_logs_bloom: self.pre_payment_logs_bloom,
placeholder_gas_used: self.placeholder_gas_used,
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions crates/rbuilder-primitives/src/mev_boost/submit_header.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::mev_boost::{
adjustment::BidAdjustmentDataV2,
adjustment::BidAdjustmentDataV3,
ssz_roots::{calculate_transactions_root_ssz, calculate_withdrawals_root_ssz},
BidMetadata,
};
Expand Down Expand Up @@ -84,8 +84,8 @@ pub struct HeaderSubmissionElectra {
pub execution_requests: ExecutionRequestsV4,
/// Blob KZG commitments.
pub commitments: Vec<alloy_consensus::Bytes48>,
/// Bid adjustment data V2.
pub adjustment_data: Option<BidAdjustmentDataV2>,
/// Bid adjustment data V3.
pub adjustment_data: Option<BidAdjustmentDataV3>,
}

impl ssz::Encode for HeaderSubmissionElectra {
Expand All @@ -99,7 +99,7 @@ impl ssz::Encode for HeaderSubmissionElectra {
+ <ExecutionRequestsV4 as ssz::Encode>::ssz_fixed_len()
+ <Vec<alloy_consensus::Bytes48> as ssz::Encode>::ssz_fixed_len();
if self.adjustment_data.is_some() {
offset += <BidAdjustmentDataV2 as ssz::Encode>::ssz_fixed_len();
offset += <BidAdjustmentDataV3 as ssz::Encode>::ssz_fixed_len();
}

let mut encoder = ssz::SszEncoder::container(buf, offset);
Expand All @@ -123,7 +123,7 @@ impl ssz::Encode for HeaderSubmissionElectra {
+ <ExecutionRequestsV4 as ssz::Encode>::ssz_bytes_len(&self.execution_requests)
+ <Vec<alloy_consensus::Bytes48> as ssz::Encode>::ssz_bytes_len(&self.commitments);
if let Some(adjustment) = &self.adjustment_data {
len += <BidAdjustmentDataV2 as ssz::Encode>::ssz_bytes_len(adjustment);
len += <BidAdjustmentDataV3 as ssz::Encode>::ssz_bytes_len(adjustment);
}
len
}
Expand All @@ -141,7 +141,7 @@ impl ssz::Decode for HeaderSubmissionElectra {
execution_payload_header: ExecutionPayloadHeaderElectra,
execution_requests: ExecutionRequestsV4,
commitments: Vec<alloy_consensus::Bytes48>,
adjustment_data: BidAdjustmentDataV2,
adjustment_data: BidAdjustmentDataV3,
}

#[derive(ssz_derive::Decode)]
Expand Down
6 changes: 6 additions & 0 deletions crates/rbuilder/src/building/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,11 @@ impl<Tracer: SimulationTracer, PartialBlockExecutionTracerType: PartialBlockExec
&local_ctx.tx_ssz_leaf_root_cache,
)
});
let placeholder_gas_used = self
.executed_tx_infos
.last()
.map(|tx_info| tx_info.space_used.gas)
.expect("payout transaction must exist");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Potential panic in edge cases

This expect() is now evaluated unconditionally, whereas previously the bid adjustment data was constructed lazily inside the iterator. If executed_tx_infos is ever empty (e.g., in testing or edge cases), this will panic.

Consider deferring this calculation to only when bid_adjustment_state_proofs is non-empty, or using unwrap_or(0) if a default is acceptable:

Suggested change
.expect("payout transaction must exist");
.expect("payout transaction must exist");

Alternatively, you could compute this lazily:

let placeholder_gas_used = LazyCell::new(|| {
    self.executed_tx_infos
        .last()
        .map(|tx_info| tx_info.space_used.gas)
        .expect("payout transaction must exist")
});

This preserves the original behavior where the expect would only be evaluated when bid adjustments are actually created.

let bid_adjustments = bid_adjustment_state_proofs
.into_iter()
.map(|(fee_payer, state_proofs)| {
Expand All @@ -1155,6 +1160,7 @@ impl<Tracer: SimulationTracer, PartialBlockExecutionTracerType: PartialBlockExec
cl_placeholder_transaction_proof: cl_placeholder_transaction_proof.clone(),
placeholder_receipt_proof: placeholder_receipt_proof.clone(),
pre_payment_logs_bloom,
placeholder_gas_used,
state_proofs,
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,8 @@ fn create_optimistic_v3_request(
maybe_adjustment_data: Option<&BidAdjustmentData>,
adjustment_data_required: bool,
) -> eyre::Result<SubmitHeaderRequest> {
let maybe_adjustment_data_v2 = maybe_adjustment_data.map(|d| d.clone().into_v2());
if maybe_adjustment_data_v2.is_none() && adjustment_data_required {
let maybe_adjustment_data_v3 = maybe_adjustment_data.map(|d| d.clone().into_v3());
if maybe_adjustment_data_v3.is_none() && adjustment_data_required {
eyre::bail!("adjustment data is required")
}

Expand All @@ -347,7 +347,7 @@ fn create_optimistic_v3_request(
),
execution_requests: request.execution_requests.clone(),
commitments: request.blobs_bundle.commitments.clone(),
adjustment_data: maybe_adjustment_data_v2,
adjustment_data: maybe_adjustment_data_v3,
}),
signature: request.signature,
};
Expand All @@ -368,7 +368,7 @@ fn create_optimistic_v3_request(
),
execution_requests: request.execution_requests.clone(),
commitments: request.blobs_bundle.commitments.clone(),
adjustment_data: maybe_adjustment_data_v2,
adjustment_data: maybe_adjustment_data_v3,
}),
signature: request.signature,
};
Expand Down