Gloas payload bid consensus (#8801)

- [x] Consensus changes for execution payload bids
- [x] EF tests for bids (and `block_header` -- no changes required).


Co-Authored-By: Michael Sproul <michael@sigmaprime.io>
This commit is contained in:
Michael Sproul
2026-02-12 15:26:23 +11:00
committed by GitHub
parent 711971f269
commit b8072c5b77
10 changed files with 358 additions and 18 deletions

View File

@@ -1,12 +1,17 @@
use crate::consensus_context::ConsensusContext;
use errors::{BlockOperationError, BlockProcessingError, HeaderInvalid};
use errors::{
BlockOperationError, BlockProcessingError, ExecutionPayloadBidInvalid, HeaderInvalid,
};
use rayon::prelude::*;
use safe_arith::{ArithError, SafeArith};
use signature_sets::{block_proposal_signature_set, get_pubkey_from_state, randao_signature_set};
use signature_sets::{
block_proposal_signature_set, execution_payload_bid_signature_set,
get_builder_pubkey_from_state, get_pubkey_from_state, randao_signature_set,
};
use std::borrow::Cow;
use tree_hash::TreeHash;
use typenum::Unsigned;
use types::*;
use types::{consts::gloas::BUILDER_INDEX_SELF_BUILD, *};
pub use self::verify_attester_slashing::{
get_slashable_indices, get_slashable_indices_modular, verify_attester_slashing,
@@ -522,3 +527,162 @@ pub fn compute_timestamp_at_slot<E: EthSpec>(
.safe_mul(spec.get_slot_duration().as_secs())
.and_then(|since_genesis| state.genesis_time().safe_add(since_genesis))
}
pub fn can_builder_cover_bid<E: EthSpec>(
state: &BeaconState<E>,
builder_index: BuilderIndex,
builder: &Builder,
bid_amount: u64,
spec: &ChainSpec,
) -> Result<bool, BlockProcessingError> {
let builder_balance = builder.balance;
let pending_withdrawals_amount =
state.get_pending_balance_to_withdraw_for_builder(builder_index)?;
let min_balance = spec
.min_deposit_amount
.safe_add(pending_withdrawals_amount)?;
if builder_balance < min_balance {
Ok(false)
} else {
Ok(builder_balance.safe_sub(min_balance)? >= bid_amount)
}
}
pub fn process_execution_payload_bid<E: EthSpec, Payload: AbstractExecPayload<E>>(
state: &mut BeaconState<E>,
block: BeaconBlockRef<'_, E, Payload>,
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
// Verify the bid signature
let signed_bid = block.body().signed_execution_payload_bid()?;
let bid = &signed_bid.message;
let amount = bid.value;
let builder_index = bid.builder_index;
// For self-builds, amount must be zero regardless of withdrawal credential prefix
if builder_index == BUILDER_INDEX_SELF_BUILD {
block_verify!(
amount == 0,
ExecutionPayloadBidInvalid::SelfBuildNonZeroAmount.into()
);
block_verify!(
signed_bid.signature.is_infinity(),
ExecutionPayloadBidInvalid::BadSignature.into()
);
} else {
let builder = state.get_builder(builder_index)?;
// Verify that the builder is active
block_verify!(
builder.is_active_at_finalized_epoch(state.finalized_checkpoint().epoch, spec),
ExecutionPayloadBidInvalid::BuilderNotActive(builder_index).into()
);
// Verify that the builder has funds to cover the bid
block_verify!(
can_builder_cover_bid(state, builder_index, builder, amount, spec)?,
ExecutionPayloadBidInvalid::InsufficientBalance {
builder_index,
builder_balance: builder.balance,
bid_value: amount,
}
.into()
);
if verify_signatures.is_true() {
block_verify!(
// We know this is NOT a self-build, so there MUST be a signature set (func does not
// return None).
execution_payload_bid_signature_set(
state,
|i| get_builder_pubkey_from_state(state, i),
signed_bid,
spec
)?
.ok_or(ExecutionPayloadBidInvalid::BadSignature)?
.verify(),
ExecutionPayloadBidInvalid::BadSignature.into()
);
}
}
// Verify commitments are under limit
let max_blobs_per_block = spec.max_blobs_per_block(state.current_epoch()) as usize;
block_verify!(
bid.blob_kzg_commitments.len() <= max_blobs_per_block,
ExecutionPayloadBidInvalid::ExcessBlobCommitments {
max: max_blobs_per_block,
bid: bid.blob_kzg_commitments.len(),
}
.into()
);
// Verify that the bid is for the current slot
block_verify!(
bid.slot == block.slot(),
ExecutionPayloadBidInvalid::SlotMismatch {
bid_slot: bid.slot,
block_slot: block.slot(),
}
.into()
);
// Verify that the bid is for the right parent block
let latest_block_hash = state.latest_block_hash()?;
block_verify!(
bid.parent_block_hash == *latest_block_hash,
ExecutionPayloadBidInvalid::ParentBlockHashMismatch {
state_block_hash: *latest_block_hash,
bid_parent_hash: bid.parent_block_hash,
}
.into()
);
block_verify!(
bid.parent_block_root == block.parent_root(),
ExecutionPayloadBidInvalid::ParentBlockRootMismatch {
block_parent_root: block.parent_root(),
bid_parent_root: bid.parent_block_root,
}
.into()
);
let expected_randao = *state.get_randao_mix(state.current_epoch())?;
block_verify!(
bid.prev_randao == expected_randao,
ExecutionPayloadBidInvalid::PrevRandaoMismatch {
expected: expected_randao,
bid: bid.prev_randao,
}
.into()
);
// Record the pending payment if there is some payment
if amount > 0 {
let pending_payment = BuilderPendingPayment {
weight: 0,
withdrawal: BuilderPendingWithdrawal {
fee_recipient: bid.fee_recipient,
amount,
builder_index,
},
};
let payment_index = E::SlotsPerEpoch::to_usize()
.safe_add(bid.slot.as_usize().safe_rem(E::SlotsPerEpoch::to_usize())?)?;
*state
.builder_pending_payments_mut()?
.get_mut(payment_index)
.ok_or(BlockProcessingError::BeaconStateError(
BeaconStateError::InvalidBuilderPendingPaymentsIndex(payment_index),
))? = pending_payment;
}
// Cache the execution bid
*state.latest_execution_payload_bid_mut()? = bid.clone();
Ok(())
}

View File

@@ -99,6 +99,9 @@ pub enum BlockProcessingError {
IncorrectExpectedWithdrawalsVariant,
MissingLastWithdrawal,
PendingAttestationInElectra,
ExecutionPayloadBidInvalid {
reason: ExecutionPayloadBidInvalid,
},
/// Builder payment index out of bounds (Gloas)
BuilderPaymentIndexOutOfBounds(usize),
}
@@ -157,6 +160,12 @@ impl From<milhouse::Error> for BlockProcessingError {
}
}
impl From<ExecutionPayloadBidInvalid> for BlockProcessingError {
fn from(reason: ExecutionPayloadBidInvalid) -> Self {
Self::ExecutionPayloadBidInvalid { reason }
}
}
impl From<BlockOperationError<HeaderInvalid>> for BlockProcessingError {
fn from(e: BlockOperationError<HeaderInvalid>) -> BlockProcessingError {
match e {
@@ -452,6 +461,38 @@ pub enum ExitInvalid {
PendingWithdrawalInQueue(u64),
}
#[derive(Debug, PartialEq, Clone)]
pub enum ExecutionPayloadBidInvalid {
/// The validator set a non-zero amount for a self-build.
SelfBuildNonZeroAmount,
/// The signature is invalid.
BadSignature,
/// The builder is not active.
BuilderNotActive(u64),
/// The builder has insufficient balance to cover the bid
InsufficientBalance {
builder_index: u64,
builder_balance: u64,
bid_value: u64,
},
/// Bid slot doesn't match block slot
SlotMismatch { bid_slot: Slot, block_slot: Slot },
/// The bid's parent block hash doesn't match the state's latest block hash
ParentBlockHashMismatch {
state_block_hash: ExecutionBlockHash,
bid_parent_hash: ExecutionBlockHash,
},
/// The bid's parent block root doesn't match the block's parent root
ParentBlockRootMismatch {
block_parent_root: Hash256,
bid_parent_root: Hash256,
},
/// The bid's prev randao doesn't match the state.
PrevRandaoMismatch { expected: Hash256, bid: Hash256 },
/// The bid contains more than the maximum number of kzg blob commitments.
ExcessBlobCommitments { max: usize, bid: usize },
}
#[derive(Debug, PartialEq, Clone)]
pub enum BlsExecutionChangeInvalid {
/// The specified validator is not in the state's validator registry.

View File

@@ -9,11 +9,12 @@ use tree_hash::TreeHash;
use typenum::Unsigned;
use types::{
AbstractExecPayload, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError,
ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork,
BuilderIndex, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork,
IndexedAttestation, IndexedAttestationRef, ProposerSlashing, SignedAggregateAndProof,
SignedBeaconBlock, SignedBeaconBlockHeader, SignedBlsToExecutionChange,
SignedContributionAndProof, SignedRoot, SignedVoluntaryExit, SigningData, Slot, SyncAggregate,
SyncAggregatorSelectionData,
SignedContributionAndProof, SignedExecutionPayloadBid, SignedRoot, SignedVoluntaryExit,
SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData,
consts::gloas::BUILDER_INDEX_SELF_BUILD,
};
pub type Result<T> = std::result::Result<T, Error>;
@@ -28,6 +29,9 @@ pub enum Error {
/// Attempted to find the public key of a validator that does not exist. You cannot distinguish
/// between an error and an invalid block in this case.
ValidatorUnknown(u64),
/// Attempted to find the public key of a builder that does not exist. You cannot distinguish
/// between an error and an invalid block in this case.
BuilderUnknown(BuilderIndex),
/// Attempted to find the public key of a validator that does not exist. You cannot distinguish
/// between an error and an invalid block in this case.
ValidatorPubkeyUnknown(PublicKeyBytes),
@@ -53,7 +57,7 @@ impl From<BeaconStateError> for Error {
}
}
/// Helper function to get a public key from a `state`.
/// Helper function to get a validator public key from a `state`.
pub fn get_pubkey_from_state<E>(
state: &BeaconState<E>,
validator_index: usize,
@@ -71,6 +75,25 @@ where
.map(Cow::Owned)
}
/// Helper function to get a builder public key from a `state`.
pub fn get_builder_pubkey_from_state<E>(
state: &BeaconState<E>,
builder_index: BuilderIndex,
) -> Option<Cow<'_, PublicKey>>
where
E: EthSpec,
{
state
.builders()
.ok()?
.get(builder_index as usize)
.and_then(|b| {
let pk: Option<PublicKey> = b.pubkey.decompress().ok();
pk
})
.map(Cow::Owned)
}
/// A signature set that is valid if a block was signed by the expected block producer.
pub fn block_proposal_signature_set<'a, E, F, Payload: AbstractExecPayload<E>>(
state: &'a BeaconState<E>,
@@ -332,6 +355,41 @@ where
Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message))
}
pub fn execution_payload_bid_signature_set<'a, E, F>(
state: &'a BeaconState<E>,
get_builder_pubkey: F,
signed_execution_payload_bid: &'a SignedExecutionPayloadBid<E>,
spec: &'a ChainSpec,
) -> Result<Option<SignatureSet<'a>>>
where
E: EthSpec,
F: Fn(BuilderIndex) -> Option<Cow<'a, PublicKey>>,
{
let execution_payload_bid = &signed_execution_payload_bid.message;
let builder_index = execution_payload_bid.builder_index;
if builder_index == BUILDER_INDEX_SELF_BUILD {
// No signatures to verify in case of a self-build, but consensus code MUST check that
// the signature is the point at infinity.
// See `process_execution_payload_bid`.
return Ok(None);
}
let domain = spec.get_domain(
state.current_epoch(),
Domain::BeaconBuilder,
&state.fork(),
state.genesis_validators_root(),
);
let pubkey = get_builder_pubkey(builder_index).ok_or(Error::BuilderUnknown(builder_index))?;
let message = execution_payload_bid.signing_root(domain);
Ok(Some(SignatureSet::single_pubkey(
&signed_execution_payload_bid.signature,
pubkey,
message,
)))
}
/// Returns the signature set for the given `attester_slashing` and corresponding `pubkeys`.
pub fn attester_slashing_signature_sets<'a, E, F>(
state: &'a BeaconState<E>,