Skip to content
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
257 changes: 236 additions & 21 deletions beacon_node/beacon_chain/src/block_production/gloas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,24 @@ pub struct ExecutionPayloadData<E: types::EthSpec> {
pub blobs_and_proofs: (types::BlobsList<E>, types::KzgProofs<E>),
}

/// The result of a local payload build, used to decide whether to include a builder bid
/// from the gossip cache or fall back to self-build.
pub struct LocalBuildResult<E: EthSpec> {
pub payload_data: ExecutionPayloadData<E>,
/// EL block value (in wei) of the locally-built payload.
pub payload_value: types::Uint256,
/// `true` if the EL signaled `engine_getPayload`'s `shouldOverrideBuilder` flag.
pub should_override_builder: bool,
}

impl<T: BeaconChainTypes> BeaconChain<T> {
pub async fn produce_block_with_verification_gloas(
self: &Arc<Self>,
randao_reveal: Signature,
slot: Slot,
graffiti_settings: GraffitiSettings,
verification: ProduceBlockVerification,
_builder_boost_factor: Option<u64>,
builder_boost_factor: Option<u64>,
) -> Result<BlockProductionResult<T::EthSpec>, BlockProductionError> {
metrics::inc_counter(&metrics::BLOCK_PRODUCTION_REQUESTS);
let _complete_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_TIMES);
Expand Down Expand Up @@ -121,11 +131,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
randao_reveal,
graffiti_settings,
verification,
builder_boost_factor,
)
.await
}

// TODO(gloas) need to implement builder boost factor logic
#[instrument(level = "debug", skip_all)]
#[allow(clippy::too_many_arguments)]
pub async fn produce_block_on_state_gloas(
Expand All @@ -138,6 +148,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
randao_reveal: Signature,
graffiti_settings: GraffitiSettings,
verification: ProduceBlockVerification,
builder_boost_factor: Option<u64>,
) -> Result<BlockProductionResult<T::EthSpec>, BlockProductionError> {
// Extract the parent's execution requests from the envelope (if parent was full).
let parent_execution_requests = if parent_payload_status == PayloadStatus::Full {
Expand Down Expand Up @@ -179,10 +190,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {

// Part 2/3 (async)
//
// Produce the execution payload bid.
// TODO(gloas) this is strictly for building local bids
// We'll need to build out trustless/trusted bid paths.
let (execution_payload_bid, state, payload_data) = self
// Produce a local execution payload bid, then select between it and any cached
// gossip-verified builder bid using `builder_boost_factor`.
// TODO(gloas) build out trustless/trusted bid paths.
let (local_signed_bid, state, local_build) = self
.clone()
.produce_execution_payload_bid(
state,
Expand All @@ -194,6 +205,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
)
.await?;

let (execution_payload_bid, payload_data) =
self.select_payload_bid(local_signed_bid, local_build, builder_boost_factor);

// Part 3/3 (blocking)
//
// Complete the block with the execution payload bid.
Expand Down Expand Up @@ -677,16 +691,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
Ok((block, state, consensus_block_value))
}

// TODO(gloas) introduce `ProposerPreferences` so we can build out trustless
// bid building. Right now this only works for local building.
/// Produce an `ExecutionPayloadBid` for some `slot` upon the given `state`.
/// Produce a self-build `ExecutionPayloadBid` for some `slot` upon the given `state`.
/// This function assumes we've already advanced `state`.
///
/// Returns the signed bid, the state, and optionally the payload data needed to construct
/// the `ExecutionPayloadEnvelope` after the beacon block is created.
///
/// For local building, payload data is always returned (`Some`).
/// For trustless building, the builder provides the envelope separately, so `None` is returned.
/// Returns the signed bid, the state, and a `LocalBuildResult` carrying the payload
/// data needed to construct the `ExecutionPayloadEnvelope` after the beacon block is
/// created, plus the EL block value and `should_override_builder` flag used by the
/// caller to compare against any cached p2p builder bid.
#[allow(clippy::type_complexity)]
#[instrument(level = "debug", skip_all)]
pub async fn produce_execution_payload_bid(
Expand All @@ -701,7 +712,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
(
SignedExecutionPayloadBid<T::EthSpec>,
BeaconState<T::EthSpec>,
Option<ExecutionPayloadData<T::EthSpec>>,
LocalBuildResult<T::EthSpec>,
Copy link
Copy Markdown
Member

@jimmygchen jimmygchen Apr 29, 2026

Choose a reason for hiding this comment

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

Doc comments may need update as we no longer return an Option here

),
BlockProductionError,
> {
Expand Down Expand Up @@ -773,10 +784,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {

let BlockProposalContentsGloas {
payload,
payload_value: _,
payload_value,
execution_requests,
blob_kzg_commitments,
blobs_and_proofs,
should_override_builder,
} = block_proposal_contents;

// TODO(gloas) since we are defaulting to local building, execution payment is 0
Expand Down Expand Up @@ -805,19 +817,115 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
blobs_and_proofs,
};

// TODO(gloas) this is only local building
// we'll need to implement builder signature for the trustless path
Ok((
SignedExecutionPayloadBid {
message: bid,
signature: Signature::infinity().map_err(BlockProductionError::BlsError)?,
},
state,
// Local building always returns payload data.
// Trustless building would return None here.
Some(payload_data),
LocalBuildResult {
payload_data,
payload_value,
should_override_builder,
},
))
}

/// Look up the highest gossip-verified bid for the `(slot, parent_block_hash,
/// parent_block_root)` of the local bid, then choose the winner.
fn select_payload_bid(
&self,
local_signed_bid: SignedExecutionPayloadBid<T::EthSpec>,
local_build: LocalBuildResult<T::EthSpec>,
builder_boost_factor: Option<u64>,
) -> (
SignedExecutionPayloadBid<T::EthSpec>,
Option<ExecutionPayloadData<T::EthSpec>>,
) {
let cached_bid = self.gossip_verified_payload_bid_cache.get_highest_bid(
local_signed_bid.message.slot,
local_signed_bid.message.parent_block_hash,
local_signed_bid.message.parent_block_root,
);
select_payload_bid_pure(
local_signed_bid,
local_build,
cached_bid,
builder_boost_factor,
)
}
}

/// Pure local-vs-cached selection logic, factored out for unit testing.
///
/// Selection rule (mirrors the pre-Gloas builder/local race in `execution_layer`):
/// - `boosted_bid = (cached_bid.value / 100) * builder_boost_factor` (raw value when `None`)
/// - if `local_value_wei >= boosted_bid_wei` → keep local
/// - if the EL signaled `should_override_builder` → keep local
/// - otherwise → use the cached builder bid and drop local payload data
/// (the builder is responsible for revealing the envelope).
///
/// `cached_bid.value` is in gwei (`u64`); `payload_value` is in wei (`Uint256`); compared in wei.
pub(crate) fn select_payload_bid_pure<E: EthSpec>(
local_signed_bid: SignedExecutionPayloadBid<E>,
local_build: LocalBuildResult<E>,
cached_bid: Option<Arc<SignedExecutionPayloadBid<E>>>,
builder_boost_factor: Option<u64>,
) -> (
SignedExecutionPayloadBid<E>,
Option<ExecutionPayloadData<E>>,
) {
let LocalBuildResult {
payload_data,
payload_value,
should_override_builder,
} = local_build;

let Some(cached_bid) = cached_bid else {
return (local_signed_bid, Some(payload_data));
};

let slot = local_signed_bid.message.slot;

if should_override_builder {
debug!(
%slot,
cached_bid_value = cached_bid.message.value,
"Using local payload because EL signaled shouldOverrideBuilder"
);
return (local_signed_bid, Some(payload_data));
}

// Convert bid value (gwei) to wei for comparison with `payload_value` (wei).
let bid_value_wei = types::Uint256::from(cached_bid.message.value)
.saturating_mul(types::Uint256::from(1_000_000_000u64));
let boosted_bid_wei = match builder_boost_factor {
Some(factor) => {
(bid_value_wei / types::Uint256::from(100)).saturating_mul(types::Uint256::from(factor))
}
None => bid_value_wei,
};

if payload_value >= boosted_bid_wei {
debug!(
%slot,
%payload_value,
cached_bid_value_gwei = cached_bid.message.value,
?builder_boost_factor,
"Local payload is more profitable than cached builder bid"
);
(local_signed_bid, Some(payload_data))
} else {
debug!(
%slot,
%payload_value,
cached_bid_value_gwei = cached_bid.message.value,
cached_bid_builder_index = cached_bid.message.builder_index,
?builder_boost_factor,
"Including cached builder bid"
);
((*cached_bid).clone(), None)
}
}

/// Gets an execution payload for inclusion in a block.
Expand Down Expand Up @@ -1150,4 +1258,111 @@ mod tests {

assert_eq!(exits.len(), 2);
}

// ---- select_payload_bid_pure ----

const REMOTE_BUILDER: BuilderIndex = 999;

fn gwei(n: u64) -> types::Uint256 {
types::Uint256::from(n).saturating_mul(types::Uint256::from(1_000_000_000u64))
}

fn local_bid() -> SignedExecutionPayloadBid<TestSpec> {
SignedExecutionPayloadBid {
message: ExecutionPayloadBid {
builder_index: BUILDER_INDEX_SELF_BUILD,
..Default::default()
},
signature: Signature::empty(),
}
}

fn cached_bid(value_gwei: u64) -> Arc<SignedExecutionPayloadBid<TestSpec>> {
Arc::new(SignedExecutionPayloadBid {
message: ExecutionPayloadBid {
builder_index: REMOTE_BUILDER,
value: value_gwei,
..Default::default()
},
signature: Signature::empty(),
})
}

fn local_build(payload_gwei: u64, should_override_builder: bool) -> LocalBuildResult<TestSpec> {
LocalBuildResult {
payload_data: ExecutionPayloadData {
payload: types::ExecutionPayloadGloas::default(),
execution_requests: ExecutionRequests::default(),
builder_index: BUILDER_INDEX_SELF_BUILD,
slot: Slot::new(0),
blobs_and_proofs: (VariableList::empty(), VariableList::empty()),
},
payload_value: gwei(payload_gwei),
should_override_builder,
}
}

const LOCAL: BuilderIndex = BUILDER_INDEX_SELF_BUILD;
const REMOTE: BuilderIndex = REMOTE_BUILDER;

/// Run `select_payload_bid_pure` and return `(winning_builder_index, has_payload_data)`.
///
/// Args (positional, mirror `select_payload_bid_pure`):
/// - `local_payload_gwei`: local payload value, in gwei.
/// - `should_override`: EL's `shouldOverrideBuilder` flag.
/// - `cached_gwei`: `Some(g)` ⇒ seed the cache with a bid of `g` gwei.
/// - `boost`: `None` = neutral, `Some(0)` = always local, `Some(>100)` = boost bid.
fn pick(
local_payload_gwei: u64,
should_override: bool,
cached_gwei: Option<u64>,
boost: Option<u64>,
) -> (BuilderIndex, bool) {
let build = local_build(local_payload_gwei, should_override);
let cache = cached_gwei.map(cached_bid);
let (out, data) = select_payload_bid_pure::<TestSpec>(local_bid(), build, cache, boost);
(out.message.builder_index, data.is_some())
}

#[test]
fn select_empty_cache_keeps_local() {
assert_eq!(pick(0, false, None, Some(u64::MAX)), (LOCAL, true));
}

#[test]
fn select_el_override_beats_any_cached_bid() {
// `shouldOverrideBuilder` short-circuits regardless of cache or boost.
assert_eq!(pick(0, true, Some(u64::MAX), Some(u64::MAX)), (LOCAL, true));
}

#[test]
fn select_boost_zero_always_keeps_local() {
// boost=0 deflates the bid to 0 ⇒ local always wins.
assert_eq!(pick(0, false, Some(u64::MAX), Some(0)), (LOCAL, true));
}

#[test]
fn select_neutral_boost_picks_higher_bid() {
// 5 gwei bid > 1 gwei local, neutral compare ⇒ bid.
assert_eq!(pick(1, false, Some(5), None), (REMOTE, false));
}

#[test]
fn select_local_strictly_higher_keeps_local() {
assert_eq!(pick(10, false, Some(5), None), (LOCAL, true));
}

#[test]
fn select_tie_goes_to_local() {
// `>=` ⇒ local wins ties.
assert_eq!(pick(5, false, Some(5), None), (LOCAL, true));
}

#[test]
fn select_boost_factor_amplifies_bid() {
// 5 gwei local vs 3 gwei bid: raw ⇒ local.
assert_eq!(pick(5, false, Some(3), None), (LOCAL, true));
// boost=200 ⇒ bid scaled to 6 gwei ⇒ bid wins.
assert_eq!(pick(5, false, Some(3), Some(200)), (REMOTE, false));
}
}
1 change: 1 addition & 0 deletions beacon_node/beacon_chain/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,7 @@ where
randao_reveal,
graffiti_settings,
ProduceBlockVerification::VerifyRandao,
None,
)
.await
.unwrap();
Expand Down
1 change: 1 addition & 0 deletions beacon_node/beacon_chain/tests/prepare_payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ async fn gloas_block_production_caches_blobs_for_column_publishing() {
randao_reveal,
graffiti_settings,
ProduceBlockVerification::VerifyRandao,
None,
)
.await
.unwrap();
Expand Down
2 changes: 2 additions & 0 deletions beacon_node/execution_layer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ pub struct BlockProposalContentsGloas<E: EthSpec> {
pub blob_kzg_commitments: KzgCommitments<E>,
pub blobs_and_proofs: (BlobsList<E>, KzgProofs<E>),
pub execution_requests: ExecutionRequests<E>,
pub should_override_builder: bool,
}

impl<E: EthSpec> From<GetPayloadResponseGloas<E>> for BlockProposalContentsGloas<E> {
Expand All @@ -215,6 +216,7 @@ impl<E: EthSpec> From<GetPayloadResponseGloas<E>> for BlockProposalContentsGloas
blob_kzg_commitments: response.blobs_bundle.commitments,
blobs_and_proofs: (response.blobs_bundle.blobs, response.blobs_bundle.proofs),
execution_requests: response.requests,
should_override_builder: response.should_override_builder,
}
}
}
Expand Down
Loading