diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 1de5083f6f..37639047fb 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -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( .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( + state: &BeaconState, + builder_index: BuilderIndex, + builder: &Builder, + bid_amount: u64, + spec: &ChainSpec, +) -> Result { + 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>( + state: &mut BeaconState, + 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(()) +} diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 5c1db9d732..53178a7a64 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -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 for BlockProcessingError { } } +impl From for BlockProcessingError { + fn from(reason: ExecutionPayloadBidInvalid) -> Self { + Self::ExecutionPayloadBidInvalid { reason } + } +} + impl From> for BlockProcessingError { fn from(e: BlockOperationError) -> 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. diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index 0e936007ee..0cc591ba4c 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -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 = std::result::Result; @@ -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 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( state: &BeaconState, 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( + state: &BeaconState, + builder_index: BuilderIndex, +) -> Option> +where + E: EthSpec, +{ + state + .builders() + .ok()? + .get(builder_index as usize) + .and_then(|b| { + let pk: Option = 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>( state: &'a BeaconState, @@ -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, + get_builder_pubkey: F, + signed_execution_payload_bid: &'a SignedExecutionPayloadBid, + spec: &'a ChainSpec, +) -> Result>> +where + E: EthSpec, + F: Fn(BuilderIndex) -> Option>, +{ + 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, diff --git a/consensus/types/src/builder/builder.rs b/consensus/types/src/builder/builder.rs index 2bd50f42cc..7d494da3ee 100644 --- a/consensus/types/src/builder/builder.rs +++ b/consensus/types/src/builder/builder.rs @@ -1,5 +1,5 @@ use crate::test_utils::TestRandom; -use crate::{Address, Epoch, ForkName}; +use crate::{Address, ChainSpec, Epoch, ForkName}; use bls::PublicKeyBytes; use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; @@ -24,3 +24,12 @@ pub struct Builder { pub deposit_epoch: Epoch, pub withdrawable_epoch: Epoch, } + +impl Builder { + /// Check if a builder is active in a state with `finalized_epoch`. + /// + /// This implements `is_active_builder` from the spec. + pub fn is_active_at_finalized_epoch(&self, finalized_epoch: Epoch, spec: &ChainSpec) -> bool { + self.deposit_epoch < finalized_epoch && self.withdrawable_epoch == spec.far_future_epoch + } +} diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 1745908c40..6838b588eb 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -9,7 +9,7 @@ use fixed_bytes::FixedBytesExtended; use int_to_bytes::{int_to_bytes4, int_to_bytes8}; use metastruct::{NumFields, metastruct}; use milhouse::{List, Vector}; -use safe_arith::{ArithError, SafeArith}; +use safe_arith::{ArithError, SafeArith, SafeArithIter}; use serde::{Deserialize, Deserializer, Serialize}; use ssz::{Decode, DecodeError, Encode, ssz_encode}; use ssz_derive::{Decode, Encode}; @@ -218,6 +218,7 @@ pub enum BeaconStateError { envelope_epoch: Epoch, }, InvalidIndicesCount, + InvalidBuilderPendingPaymentsIndex(usize), InvalidExecutionPayloadAvailabilityIndex(usize), } @@ -2749,6 +2750,30 @@ impl BeaconState { Ok(pending_balance) } + pub fn get_pending_balance_to_withdraw_for_builder( + &self, + builder_index: BuilderIndex, + ) -> Result { + let pending_withdrawals_total = self + .builder_pending_withdrawals()? + .iter() + .filter_map(|withdrawal| { + (withdrawal.builder_index == builder_index).then_some(withdrawal.amount) + }) + .safe_sum()?; + let pending_payments_total = self + .builder_pending_payments()? + .iter() + .filter_map(|payment| { + (payment.withdrawal.builder_index == builder_index) + .then_some(payment.withdrawal.amount) + }) + .safe_sum()?; + pending_withdrawals_total + .safe_add(pending_payments_total) + .map_err(Into::into) + } + // ******* Electra mutators ******* pub fn queue_excess_active_balance( diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 8e5bd24d24..628ee83936 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -48,8 +48,6 @@ excluded_paths = [ "tests/.*/eip7732", "tests/.*/eip7805", # TODO(gloas): remove these ignores as more Gloas operations are implemented - "tests/.*/gloas/operations/block_header/.*", - "tests/.*/gloas/operations/execution_payload_bid/.*", "tests/.*/gloas/operations/payload_attestation/.*", # TODO(EIP-7732): remove these ignores as Gloas consensus is implemented "tests/.*/gloas/epoch_processing/.*", diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index ef998a94ba..2f08727045 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -16,7 +16,7 @@ use state_processing::{ per_block_processing::{ VerifyBlockRoot, VerifySignatures, errors::BlockProcessingError, - process_block_header, process_execution_payload, + process_block_header, process_execution_payload, process_execution_payload_bid, process_operations::{ altair_deneb, base, gloas, process_attester_slashings, process_bls_to_execution_changes, process_deposits, process_exits, @@ -51,6 +51,12 @@ pub struct WithdrawalsPayload { payload: Option>, } +/// Newtype for testing execution payload bids. +#[derive(Debug, Clone, Deserialize)] +pub struct ExecutionPayloadBidBlock { + block: BeaconBlock, +} + #[derive(Debug, Clone)] pub struct Operations> { metadata: Metadata, @@ -459,6 +465,37 @@ impl Operation for SignedExecutionPayloadEnvelope { } } +impl Operation for ExecutionPayloadBidBlock { + type Error = BlockProcessingError; + + fn handler_name() -> String { + "execution_payload_bid".into() + } + + fn filename() -> String { + "block.ssz_snappy".into() + } + + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name.gloas_enabled() + } + + fn decode(path: &Path, _fork_name: ForkName, spec: &ChainSpec) -> Result { + ssz_decode_file_with(path, |bytes| BeaconBlock::from_ssz_bytes(bytes, spec)) + .map(|block| ExecutionPayloadBidBlock { block }) + } + + fn apply_to( + &self, + state: &mut BeaconState, + spec: &ChainSpec, + _: &Operations, + ) -> Result<(), BlockProcessingError> { + process_execution_payload_bid(state, self.block.to_ref(), VerifySignatures::True, spec)?; + Ok(()) + } +} + impl Operation for WithdrawalsPayload { type Error = BlockProcessingError; diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 9d11252edb..5a43642c88 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -1139,11 +1139,13 @@ impl> Handler for OperationsHandler && (!fork_name.gloas_enabled() || self.handler_name() == "attestation" || self.handler_name() == "attester_slashing" + || self.handler_name() == "block_header" || self.handler_name() == "bls_to_execution_change" || self.handler_name() == "consolidation_request" || self.handler_name() == "deposit_request" || self.handler_name() == "deposit" || self.handler_name() == "execution_payload" + || self.handler_name() == "execution_payload_bid" || self.handler_name() == "proposer_slashing" || self.handler_name() == "sync_aggregate" || self.handler_name() == "withdrawal_request" diff --git a/testing/ef_tests/src/lib.rs b/testing/ef_tests/src/lib.rs index 8ec4860cab..49bea7d85f 100644 --- a/testing/ef_tests/src/lib.rs +++ b/testing/ef_tests/src/lib.rs @@ -1,11 +1,11 @@ pub use case_result::CaseResult; -pub use cases::WithdrawalsPayload; pub use cases::{ - Case, EffectiveBalanceUpdates, Eth1DataReset, FeatureName, HistoricalRootsUpdate, - HistoricalSummariesUpdate, InactivityUpdates, JustificationAndFinalization, - ParticipationFlagUpdates, ParticipationRecordUpdates, PendingBalanceDeposits, - PendingConsolidations, ProposerLookahead, RandaoMixesReset, RegistryUpdates, - RewardsAndPenalties, Slashings, SlashingsReset, SyncCommitteeUpdates, + Case, EffectiveBalanceUpdates, Eth1DataReset, ExecutionPayloadBidBlock, FeatureName, + HistoricalRootsUpdate, HistoricalSummariesUpdate, InactivityUpdates, + JustificationAndFinalization, ParticipationFlagUpdates, ParticipationRecordUpdates, + PendingBalanceDeposits, PendingConsolidations, ProposerLookahead, RandaoMixesReset, + RegistryUpdates, RewardsAndPenalties, Slashings, SlashingsReset, SyncCommitteeUpdates, + WithdrawalsPayload, }; pub use decode::log_file_access; pub use error::Error; diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 8a53a61929..332f077984 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -93,6 +93,12 @@ fn operations_execution_payload_envelope() { OperationsHandler::>::default().run(); } +#[test] +fn operations_execution_payload_bid() { + OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); +} + #[test] fn operations_withdrawals() { OperationsHandler::>::default().run();