diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index e3de8d73245..8a13f0ff39a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4794,7 +4794,7 @@ impl BeaconChain { let proposal_epoch = proposal_slot.epoch(T::EthSpec::slots_per_epoch()); if head_state.current_epoch() == proposal_epoch { return get_expected_withdrawals(&unadvanced_state, &self.spec) - .map(|(withdrawals, _)| withdrawals) + .map(|(withdrawals, _, _, _, _)| withdrawals) .map_err(Error::PrepareProposerFailed); } @@ -4812,7 +4812,7 @@ impl BeaconChain { &self.spec, )?; get_expected_withdrawals(&advanced_state, &self.spec) - .map(|(withdrawals, _)| withdrawals) + .map(|(withdrawals, _, _, _, _)| withdrawals) .map_err(Error::PrepareProposerFailed) } diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index ba0621ae720..f2e9896b9dd 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -1364,6 +1364,7 @@ async fn proposer_shuffling_root_consistency_at_fork_boundary() { } #[tokio::test] +#[allow(clippy::large_stack_frames)] async fn proposer_shuffling_changing_with_lookahead() { let initial_blocks = E::slots_per_epoch() * 4 - 1; @@ -1412,7 +1413,7 @@ async fn proposer_shuffling_changing_with_lookahead() { let consolidation_request: ConsolidationRequest = ConsolidationRequest { source_address: validator_to_topup - .get_execution_withdrawal_address(spec) + .get_execution_withdrawal_address(spec, ForkName::Fulu) .unwrap(), source_pubkey: validator_to_topup.pubkey, target_pubkey: validator_to_topup.pubkey, @@ -1491,7 +1492,7 @@ async fn proposer_shuffling_changing_with_lookahead() { let validator = current_epoch_state .get_validator(validator_to_topup_index) .unwrap(); - assert!(validator.has_compounding_withdrawal_credential(spec)); + assert!(validator.has_compounding_withdrawal_credential(spec, ForkName::Fulu)); assert_eq!(validator.effective_balance, 95_000_000_000); // The shuffling for the current epoch from `prev_epoch_state` should match the shuffling diff --git a/beacon_node/http_api/src/builder_states.rs b/beacon_node/http_api/src/builder_states.rs index 7c05dd00d26..aa9ae956547 100644 --- a/beacon_node/http_api/src/builder_states.rs +++ b/beacon_node/http_api/src/builder_states.rs @@ -32,7 +32,7 @@ pub fn get_next_withdrawals( } match get_expected_withdrawals(&state, &chain.spec) { - Ok((withdrawals, _)) => Ok(withdrawals), + Ok((withdrawals, _, _, _, _)) => Ok(withdrawals), Err(e) => Err(warp_utils::reject::custom_server_error(format!( "failed to get expected withdrawal: {:?}", e diff --git a/beacon_node/operation_pool/src/bls_to_execution_changes.rs b/beacon_node/operation_pool/src/bls_to_execution_changes.rs index 485f21b5c8b..4458b2f70da 100644 --- a/beacon_node/operation_pool/src/bls_to_execution_changes.rs +++ b/beacon_node/operation_pool/src/bls_to_execution_changes.rs @@ -113,16 +113,18 @@ impl BlsToExecutionChanges { .validators() .get(validator_index as usize) .is_none_or(|validator| { - let prune = validator.has_execution_withdrawal_credential(spec) - && head_block - .message() - .body() - .bls_to_execution_changes() - .map_or(true, |recent_changes| { - !recent_changes - .iter() - .any(|c| c.message.validator_index == validator_index) - }); + let prune = validator.has_execution_withdrawal_credential( + spec, + head_state.fork_name_unchecked(), + ) && head_block + .message() + .body() + .bls_to_execution_changes() + .map_or(true, |recent_changes| { + !recent_changes + .iter() + .any(|c| c.message.validator_index == validator_index) + }); if prune { validator_indices_pruned.push(validator_index); } diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 81423d6abd4..71f54e9d2d2 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -582,7 +582,12 @@ impl OperationPool { address_change.signature_is_still_valid(&state.fork()) && state .get_validator(address_change.as_inner().message.validator_index as usize) - .is_ok_and(|validator| !validator.has_execution_withdrawal_credential(spec)) + .is_ok_and(|validator| { + !validator.has_execution_withdrawal_credential( + spec, + state.fork_name_unchecked(), + ) + }) }, |address_change| address_change.as_inner().clone(), E::MaxBlsToExecutionChanges::to_usize(), diff --git a/beacon_node/store/src/consensus_context.rs b/beacon_node/store/src/consensus_context.rs index 281106d9aaa..9b492d68871 100644 --- a/beacon_node/store/src/consensus_context.rs +++ b/beacon_node/store/src/consensus_context.rs @@ -35,6 +35,8 @@ impl OnDiskConsensusContext { proposer_index, current_block_root, indexed_attestations, + indexed_payload_attestations: _, + // TODO(EIP-7732): add indexed_payload_attestations to the on-disk format. } = ctxt; OnDiskConsensusContext { slot, diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml index d4df1582f32..5df6370abe2 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml @@ -22,13 +22,13 @@ TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615 # Genesis # --------------------------------------------------------------- -# `2**14` (= 16,384) +# 2**14 (= 16,384) validators MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 16384 # Dec 1, 2020, 12pm UTC MIN_GENESIS_TIME: 1606824000 -# Mainnet initial fork version, recommend altering for testnets +# Initial fork version for mainnet GENESIS_FORK_VERSION: 0x00000000 -# 604800 seconds (7 days) +# 7 * 24 * 3,600 (= 604,800) seconds, 7 days GENESIS_DELAY: 604800 # Forking @@ -39,25 +39,34 @@ GENESIS_DELAY: 604800 # Altair ALTAIR_FORK_VERSION: 0x01000000 -ALTAIR_FORK_EPOCH: 74240 # Oct 27, 2021, 10:56:23am UTC +ALTAIR_FORK_EPOCH: 74240 # Oct 27, 2021, 10:56:23am UTC # Bellatrix BELLATRIX_FORK_VERSION: 0x02000000 -BELLATRIX_FORK_EPOCH: 144896 # Sept 6, 2022, 11:34:47am UTC +BELLATRIX_FORK_EPOCH: 144896 # Sept 6, 2022, 11:34:47am UTC # Capella CAPELLA_FORK_VERSION: 0x03000000 -CAPELLA_FORK_EPOCH: 194048 # April 12, 2023, 10:27:35pm UTC +CAPELLA_FORK_EPOCH: 194048 # April 12, 2023, 10:27:35pm UTC # Deneb DENEB_FORK_VERSION: 0x04000000 -DENEB_FORK_EPOCH: 269568 # March 13, 2024, 01:55:35pm UTC +DENEB_FORK_EPOCH: 269568 # March 13, 2024, 01:55:35pm UTC # Electra ELECTRA_FORK_VERSION: 0x05000000 -ELECTRA_FORK_EPOCH: 364032 # May 7, 2025, 10:05:11am UTC +ELECTRA_FORK_EPOCH: 364032 # May 7, 2025, 10:05:11am UTC # Fulu FULU_FORK_VERSION: 0x06000000 -FULU_FORK_EPOCH: 411392 # December 3, 2025, 09:49:11pm UTC +FULU_FORK_EPOCH: 411392 # December 3, 2025, 09:49:11pm UTC # Gloas -GLOAS_FORK_VERSION: 0x07000000 +GLOAS_FORK_VERSION: 0x07000000 # temporary stub GLOAS_FORK_EPOCH: 18446744073709551615 +# EIP7441 +EIP7441_FORK_VERSION: 0x08000000 # temporary stub +EIP7441_FORK_EPOCH: 18446744073709551615 +# EIP7805 +EIP7805_FORK_VERSION: 0x0a000000 # temporary stub +EIP7805_FORK_EPOCH: 18446744073709551615 +# EIP7928 +EIP7928_FORK_VERSION: 0x0b000000 # temporary stub +EIP7928_FORK_EPOCH: 18446744073709551615 # Time parameters # --------------------------------------------------------------- @@ -86,6 +95,28 @@ SYNC_MESSAGE_DUE_BPS: 3333 # 6667 basis points, ~67% of SLOT_DURATION_MS CONTRIBUTION_DUE_BPS: 6667 +# Gloas +# 2**12 (= 4,096) epochs +MIN_BUILDER_WITHDRAWABILITY_DELAY: 4096 +# 2500 basis points, 25% of SLOT_DURATION_MS +ATTESTATION_DUE_BPS_GLOAS: 2500 +# 5000 basis points, 50% of SLOT_DURATION_MS +AGGREGATE_DUE_BPS_GLOAS: 5000 +# 2500 basis points, 25% of SLOT_DURATION_MS +SYNC_MESSAGE_DUE_BPS_GLOAS: 2500 +# 5000 basis points, 50% of SLOT_DURATION_MS +CONTRIBUTION_DUE_BPS_GLOAS: 5000 +# 7500 basis points, 75% of SLOT_DURATION_MS +PAYLOAD_ATTESTATION_DUE_BPS: 7500 + +# EIP7805 +# 7500 basis points, 75% of SLOT_DURATION_MS +VIEW_FREEZE_CUTOFF_BPS: 7500 +# 6667 basis points, ~67% of SLOT_DURATION_MS +INCLUSION_LIST_SUBMISSION_DUE_BPS: 6667 +# 9167 basis points, ~92% of SLOT_DURATION_MS +PROPOSER_INCLUSION_LIST_CUTOFF_BPS: 9167 + # Validator cycle # --------------------------------------------------------------- # 2**2 (= 4) @@ -137,10 +168,6 @@ MAX_REQUEST_BLOCKS: 1024 EPOCHS_PER_SUBNET_SUBSCRIPTION: 256 # MIN_VALIDATOR_WITHDRAWABILITY_DELAY + CHURN_LIMIT_QUOTIENT // 2 (= 33,024) epochs MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 -# 5s -TTFB_TIMEOUT: 5 -# 10s -RESP_TIMEOUT: 10 # 2**5 (= 32) slots ATTESTATION_PROPAGATION_SLOT_RANGE: 32 # 500ms @@ -154,9 +181,7 @@ ATTESTATION_SUBNET_COUNT: 64 # 0 bits ATTESTATION_SUBNET_EXTRA_BITS: 0 # ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS (= 6 + 0) bits -# computed at runtime ATTESTATION_SUBNET_PREFIX_BITS: 6 -ATTESTATION_SUBNET_SHUFFLING_PREFIX_BITS: 3 # Deneb # 2**7 (= 128) blocks @@ -196,6 +221,22 @@ BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000 # 2**12 (= 4,096) epochs MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 +# Gloas +# 2**7 (= 128) payloads +MAX_REQUEST_PAYLOADS: 128 + +# EIP7441 +# 2**8 (= 256) epochs +EPOCHS_PER_SHUFFLING_PHASE: 256 +# 2**1 (= 2) epochs +PROPOSER_SELECTION_GAP: 2 + +# EIP7805 +# 2**4 (= 16) inclusion lists +MAX_REQUEST_INCLUSION_LIST: 16 +# 2**13 (= 8,192) bytes +MAX_BYTES_PER_INCLUSION_LIST: 8192 + # Blob Scheduling # --------------------------------------------------------------- @@ -204,5 +245,3 @@ BLOB_SCHEDULE: MAX_BLOBS_PER_BLOCK: 15 - EPOCH: 419072 # January 7, 2026, 01:01:11am UTC MAX_BLOBS_PER_BLOCK: 21 - -# Gloas diff --git a/consensus/state_processing/src/common/get_attestation_participation.rs b/consensus/state_processing/src/common/get_attestation_participation.rs index 71bf6329f11..b7e877c5940 100644 --- a/consensus/state_processing/src/common/get_attestation_participation.rs +++ b/consensus/state_processing/src/common/get_attestation_participation.rs @@ -1,4 +1,5 @@ use integer_sqrt::IntegerSquareRoot; +use safe_arith::SafeArith; use smallvec::SmallVec; use types::{AttestationData, BeaconState, ChainSpec, EthSpec}; use types::{ @@ -22,18 +23,47 @@ pub fn get_attestation_participation_flag_indices( inclusion_delay: u64, spec: &ChainSpec, ) -> Result, Error> { + // Matching source let justified_checkpoint = if data.target.epoch == state.current_epoch() { state.current_justified_checkpoint() } else { state.previous_justified_checkpoint() }; - - // Matching roots. let is_matching_source = data.source == justified_checkpoint; - let is_matching_target = is_matching_source - && data.target.root == *state.get_block_root_at_epoch(data.target.epoch)?; - let is_matching_head = - is_matching_target && data.beacon_block_root == *state.get_block_root(data.slot)?; + + // Matching target + let target_root = *state.get_block_root_at_epoch(data.target.epoch)?; + let target_root_matches = data.target.root == target_root; + let is_matching_target = is_matching_source && target_root_matches; + + // Matching head + let head_root = *state.get_block_root(data.slot)?; + let head_root_matches = data.beacon_block_root == head_root; + + let is_matching_head = if state.fork_name_unchecked().gloas_enabled() { + let payload_matches = if state.is_attestation_same_slot(data)? { + // For same-slot attestations, data.index must be 0 + if data.index != 0 { + return Err(Error::BadOverloadedDataIndex(data.index)); + } + true + } else { + // For non same-slot attestations, check execution payload availability + let slot_index = data + .slot + .as_usize() + .safe_rem(E::slots_per_historical_root())?; + let payload_index = state + .execution_payload_availability()? + .get(slot_index) + .map(|avail| if avail { 1 } else { 0 } as u64) + .map_err(|_| Error::InvalidExecutionPayloadAvailabilityIndex(slot_index))?; + data.index == payload_index + }; + is_matching_target && head_root_matches && payload_matches + } else { + is_matching_target && head_root_matches + }; if !is_matching_source { return Err(Error::IncorrectAttestationSource); diff --git a/consensus/state_processing/src/common/get_payload_attesting_indices.rs b/consensus/state_processing/src/common/get_payload_attesting_indices.rs new file mode 100644 index 00000000000..47f58025ca3 --- /dev/null +++ b/consensus/state_processing/src/common/get_payload_attesting_indices.rs @@ -0,0 +1,45 @@ +use crate::per_block_processing::errors::{ + BlockOperationError, PayloadAttestationInvalid as Invalid, +}; +use ssz_types::VariableList; +use typenum::Unsigned; +use types::*; + +pub fn get_indexed_payload_attestation( + state: &BeaconState, + slot: Slot, + payload_attestation: &PayloadAttestation, + spec: &ChainSpec, +) -> Result, BlockOperationError> { + let attesting_indices = get_payload_attesting_indices(state, slot, payload_attestation, spec)?; + + Ok(IndexedPayloadAttestation { + attesting_indices: VariableList::new(attesting_indices)?, + data: payload_attestation.data.clone(), + signature: payload_attestation.signature.clone(), + }) +} + +pub fn get_payload_attesting_indices( + state: &BeaconState, + slot: Slot, + payload_attestation: &PayloadAttestation, + spec: &ChainSpec, +) -> Result, BeaconStateError> { + let ptc = state.get_ptc(slot, spec)?; + + let bitlist = &payload_attestation.aggregation_bits; + if bitlist.len() != E::PTCSize::to_usize() { + return Err(BeaconStateError::InvalidBitfield); + } + + let mut attesting_indices = Vec::::new(); + for (i, index) in ptc.into_iter().enumerate() { + if let Ok(true) = bitlist.get(i) { + attesting_indices.push(index as u64); + } + } + attesting_indices.sort_unstable(); + + Ok(attesting_indices) +} diff --git a/consensus/state_processing/src/common/mod.rs b/consensus/state_processing/src/common/mod.rs index 0287748fd04..e550a6c48b1 100644 --- a/consensus/state_processing/src/common/mod.rs +++ b/consensus/state_processing/src/common/mod.rs @@ -1,6 +1,7 @@ mod deposit_data_tree; mod get_attestation_participation; mod get_attesting_indices; +mod get_payload_attesting_indices; mod initiate_validator_exit; mod slash_validator; @@ -13,6 +14,9 @@ pub use get_attestation_participation::get_attestation_participation_flag_indice pub use get_attesting_indices::{ attesting_indices_base, attesting_indices_electra, get_attesting_indices_from_state, }; +pub use get_payload_attesting_indices::{ + get_indexed_payload_attestation, get_payload_attesting_indices, +}; pub use initiate_validator_exit::initiate_validator_exit; pub use slash_validator::slash_validator; diff --git a/consensus/state_processing/src/consensus_context.rs b/consensus/state_processing/src/consensus_context.rs index 07d554e3037..a7af510f716 100644 --- a/consensus/state_processing/src/consensus_context.rs +++ b/consensus/state_processing/src/consensus_context.rs @@ -1,11 +1,16 @@ use crate::EpochCacheError; -use crate::common::{attesting_indices_base, attesting_indices_electra}; -use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError}; +use crate::common::{ + attesting_indices_base, attesting_indices_electra, get_indexed_payload_attestation, +}; +use crate::per_block_processing::errors::{ + AttestationInvalid, BlockOperationError, PayloadAttestationInvalid, +}; use std::collections::{HashMap, hash_map::Entry}; use tree_hash::TreeHash; use types::{ AbstractExecPayload, AttestationRef, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, - Hash256, IndexedAttestation, IndexedAttestationRef, SignedBeaconBlock, Slot, + Hash256, IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, + PayloadAttestation, SignedBeaconBlock, Slot, }; #[derive(Debug, PartialEq, Clone)] @@ -22,6 +27,8 @@ pub struct ConsensusContext { pub current_block_root: Option, /// Cache of indexed attestations constructed during block processing. pub indexed_attestations: HashMap>, + /// Cache of indexed payload attestations constructed during block processing. + pub indexed_payload_attestations: HashMap>, } #[derive(Debug, PartialEq, Clone)] @@ -55,6 +62,7 @@ impl ConsensusContext { proposer_index: None, current_block_root: None, indexed_attestations: HashMap::new(), + indexed_payload_attestations: HashMap::new(), } } @@ -177,6 +185,25 @@ impl ConsensusContext { .map(|indexed_attestation| (*indexed_attestation).to_ref()) } + pub fn get_indexed_payload_attestation<'a>( + &'a mut self, + state: &BeaconState, + slot: Slot, + payload_attestation: &'a PayloadAttestation, + spec: &ChainSpec, + ) -> Result<&'a IndexedPayloadAttestation, BlockOperationError> + { + let key = payload_attestation.tree_hash_root(); + match self.indexed_payload_attestations.entry(key) { + Entry::Occupied(occupied) => Ok(occupied.into_mut()), + Entry::Vacant(vacant) => { + let indexed_payload_attestation = + get_indexed_payload_attestation(state, slot, payload_attestation, spec)?; + Ok(vacant.insert(indexed_payload_attestation)) + } + } + } + pub fn num_cached_indexed_attestations(&self) -> usize { self.indexed_attestations.len() } diff --git a/consensus/state_processing/src/envelope_processing.rs b/consensus/state_processing/src/envelope_processing.rs new file mode 100644 index 00000000000..d0f5d5a999f --- /dev/null +++ b/consensus/state_processing/src/envelope_processing.rs @@ -0,0 +1,273 @@ +use crate::BlockProcessingError; +use crate::VerifySignatures; +use crate::per_block_processing::compute_timestamp_at_slot; +use crate::per_block_processing::process_operations::{ + process_consolidation_requests, process_deposit_requests, process_withdrawal_requests, +}; +use safe_arith::{ArithError, SafeArith}; +use tree_hash::TreeHash; +use types::{ + BeaconState, BeaconStateError, BuilderPendingPayment, ChainSpec, EthSpec, ExecutionBlockHash, + Hash256, SignedExecutionPayloadEnvelope, Slot, +}; + +// TODO(EIP-7732): don't use this redefinition.. +macro_rules! envelope_verify { + ($condition: expr, $result: expr) => { + if !$condition { + return Err($result); + } + }; +} + +#[derive(Debug, Clone)] +pub enum EnvelopeProcessingError { + /// Bad Signature + BadSignature, + BeaconStateError(BeaconStateError), + BlockProcessingError(BlockProcessingError), + ArithError(ArithError), + /// Envelope doesn't match latest beacon block header + LatestBlockHeaderMismatch { + envelope_root: Hash256, + block_header_root: Hash256, + }, + /// Envelope doesn't match latest beacon block slot + SlotMismatch { + envelope_slot: Slot, + parent_state_slot: Slot, + }, + /// The withdrawals root doesn't match the state's latest withdrawals root + WithdrawalsRootMismatch { + state: Hash256, + envelope: Hash256, + }, + // The gas limit doesn't match the committed bid + GasLimitMismatch { + committed_bid: u64, + envelope: u64, + }, + // The block hash doesn't match the committed bid + BlockHashMismatch { + committed_bid: ExecutionBlockHash, + envelope: ExecutionBlockHash, + }, + // The parent hash doesn't match the previous execution payload + ParentHashMismatch { + state: ExecutionBlockHash, + envelope: ExecutionBlockHash, + }, + /// The blob KZG commitments root doesn't match the committed bid + BlobKzgCommitmentsRootMismatch { + committed_bid: Hash256, + envelope: Hash256, + }, + // The previous randao didn't match the payload + PrevRandaoMismatch { + state: Hash256, + envelope: Hash256, + }, + // The timestamp didn't match the payload + TimestampMismatch { + state: u64, + envelope: u64, + }, + // Blob committments exceeded the maximum + BlobLimitExceeded { + max: usize, + envelope: usize, + }, + // Invalid state root + InvalidStateRoot { + state: Hash256, + envelope: Hash256, + }, + // BitFieldError + BitFieldError(ssz::BitfieldError), + // Some kind of error calculating the builder payment index + BuilderPaymentIndexOutOfBounds(usize), +} + +impl From for EnvelopeProcessingError { + fn from(e: BeaconStateError) -> Self { + EnvelopeProcessingError::BeaconStateError(e) + } +} + +impl From for EnvelopeProcessingError { + fn from(e: BlockProcessingError) -> Self { + EnvelopeProcessingError::BlockProcessingError(e) + } +} + +impl From for EnvelopeProcessingError { + fn from(e: ArithError) -> Self { + EnvelopeProcessingError::ArithError(e) + } +} + +/// Processes a `SignedExecutionPayloadEnvelope` +/// +/// This function does all the state modifications inside `process_execution_payload()` +pub fn envelope_processing( + state: &mut BeaconState, + parent_state_root: Option, + signed_envelope: &SignedExecutionPayloadEnvelope, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), EnvelopeProcessingError> { + if verify_signatures.is_true() { + // Verify Signed Envelope Signature + // TODO(EIP-7732): there is probably a more efficient way to do this.. + if !signed_envelope.verify_signature_with_state(state, spec)? { + return Err(EnvelopeProcessingError::BadSignature); + } + } + + let envelope = &signed_envelope.message; + let payload = &envelope.payload; + let execution_requests = &envelope.execution_requests; + + // Cache latest block header state root + if state.latest_block_header().state_root == Hash256::default() { + let previous_state_root = parent_state_root + .map(Ok) + .unwrap_or_else(|| state.canonical_root())?; + state.latest_block_header_mut().state_root = previous_state_root; + } + + // Verify consistency with the beacon block + envelope_verify!( + envelope.beacon_block_root == state.latest_block_header().tree_hash_root(), + EnvelopeProcessingError::LatestBlockHeaderMismatch { + envelope_root: envelope.beacon_block_root, + block_header_root: state.latest_block_header().tree_hash_root(), + } + ); + envelope_verify!( + envelope.slot == state.slot(), + EnvelopeProcessingError::SlotMismatch { + envelope_slot: envelope.slot, + parent_state_slot: state.slot(), + } + ); + + // Verify consistency with the committed bid + let committed_bid = state.latest_execution_payload_bid()?; + // builder index match already verified + if committed_bid.blob_kzg_commitments_root != envelope.blob_kzg_commitments.tree_hash_root() { + return Err(EnvelopeProcessingError::BlobKzgCommitmentsRootMismatch { + committed_bid: committed_bid.blob_kzg_commitments_root, + envelope: envelope.blob_kzg_commitments.tree_hash_root(), + }); + }; + + // Verify the gas limit + envelope_verify!( + payload.gas_limit == committed_bid.gas_limit, + EnvelopeProcessingError::GasLimitMismatch { + committed_bid: committed_bid.gas_limit, + envelope: payload.gas_limit, + } + ); + + // Verify the block hash + envelope_verify!( + committed_bid.block_hash == payload.block_hash, + EnvelopeProcessingError::BlockHashMismatch { + committed_bid: committed_bid.block_hash, + envelope: payload.block_hash, + } + ); + + // Verify consistency of the parent hash with respect to the previous execution payload + envelope_verify!( + payload.parent_hash == *state.latest_block_hash()?, + EnvelopeProcessingError::ParentHashMismatch { + state: *state.latest_block_hash()?, + envelope: payload.parent_hash, + } + ); + + // Verify prev_randao + envelope_verify!( + payload.prev_randao == *state.get_randao_mix(state.current_epoch())?, + EnvelopeProcessingError::PrevRandaoMismatch { + state: *state.get_randao_mix(state.current_epoch())?, + envelope: payload.prev_randao, + } + ); + + // Verify the timestamp + let state_timestamp = compute_timestamp_at_slot(state, state.slot(), spec)?; + envelope_verify!( + payload.timestamp == state_timestamp, + EnvelopeProcessingError::TimestampMismatch { + state: state_timestamp, + envelope: payload.timestamp, + } + ); + + // Verify the commitments are under limit + let max_blobs = spec.max_blobs_per_block(state.current_epoch()) as usize; + envelope_verify!( + envelope.blob_kzg_commitments.len() <= max_blobs, + EnvelopeProcessingError::BlobLimitExceeded { + max: max_blobs, + envelope: envelope.blob_kzg_commitments.len(), + } + ); + + // process electra operations + process_deposit_requests(state, &execution_requests.deposits, spec)?; + process_withdrawal_requests(state, &execution_requests.withdrawals, spec)?; + process_consolidation_requests(state, &execution_requests.consolidations, spec)?; + + // queue the builder payment + let payment_index = E::slots_per_epoch() + .safe_add(state.slot().as_u64().safe_rem(E::slots_per_epoch())?)? + as usize; + let payment = state + .builder_pending_payments()? + .get(payment_index) + .ok_or(EnvelopeProcessingError::BuilderPaymentIndexOutOfBounds( + payment_index, + ))? + .clone(); + let amount = payment.withdrawal.amount; + if amount > 0 { + let _exit_queue_epoch = state.compute_exit_epoch_and_update_churn(amount, spec)?; + state + .builder_pending_withdrawals_mut()? + .push(payment.withdrawal) + .map_err(|e| EnvelopeProcessingError::BeaconStateError(e.into()))?; + } + *state + .builder_pending_payments_mut()? + .get_mut(payment_index) + .ok_or(EnvelopeProcessingError::BuilderPaymentIndexOutOfBounds( + payment_index, + ))? = BuilderPendingPayment::default(); + + // cache the execution payload hash + let availability_index = state + .slot() + .safe_rem(E::slots_per_historical_root() as u64)? + .as_usize(); + state + .execution_payload_availability_mut()? + .set(availability_index, true) + .map_err(EnvelopeProcessingError::BitFieldError)?; + *state.latest_block_hash_mut()? = payload.block_hash; + + // verify the state root + envelope_verify!( + envelope.state_root == state.canonical_root()?, + EnvelopeProcessingError::InvalidStateRoot { + state: state.canonical_root()?, + envelope: envelope.state_root, + } + ); + + Ok(()) +} diff --git a/consensus/state_processing/src/lib.rs b/consensus/state_processing/src/lib.rs index 9b2696c6d59..e37c5265799 100644 --- a/consensus/state_processing/src/lib.rs +++ b/consensus/state_processing/src/lib.rs @@ -20,6 +20,7 @@ pub mod all_caches; pub mod block_replayer; pub mod common; pub mod consensus_context; +pub mod envelope_processing; pub mod epoch_cache; pub mod genesis; pub mod per_block_processing; diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index cd1c1b9849d..8aeb383ef76 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -1,11 +1,16 @@ +use self::errors::ExecutionPayloadBidInvalid; use crate::consensus_context::ConsensusContext; use errors::{BlockOperationError, BlockProcessingError, HeaderInvalid}; use rayon::prelude::*; use safe_arith::{ArithError, SafeArith, SafeArithIter}; -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_pubkey_from_state, + randao_signature_set, +}; use std::borrow::Cow; use tree_hash::TreeHash; use typenum::Unsigned; +use types::consts::gloas::BUILDER_INDEX_FLAG; use types::*; pub use self::verify_attester_slashing::{ @@ -15,6 +20,7 @@ pub use self::verify_proposer_slashing::verify_proposer_slashing; pub use altair::sync_committee::process_sync_aggregate; pub use block_signature_verifier::{BlockSignatureVerifier, ParallelSignatureSets}; pub use is_valid_indexed_attestation::is_valid_indexed_attestation; +pub use is_valid_indexed_payload_attestation::is_valid_indexed_payload_attestation; pub use process_operations::process_operations; pub use verify_attestation::{ verify_attestation_for_block_inclusion, verify_attestation_for_state, @@ -30,7 +36,9 @@ pub mod block_signature_verifier; pub mod deneb; pub mod errors; mod is_valid_indexed_attestation; +mod is_valid_indexed_payload_attestation; pub mod process_operations; +pub mod process_withdrawals; pub mod signature_sets; pub mod tests; mod verify_attestation; @@ -38,9 +46,9 @@ mod verify_attester_slashing; mod verify_bls_to_execution_change; mod verify_deposit; mod verify_exit; +mod verify_payload_attestation; mod verify_proposer_slashing; -use crate::common::decrease_balance; use crate::common::update_progressive_balances_cache::{ initialize_progressive_balances_cache, update_progressive_balances_metrics, }; @@ -172,14 +180,19 @@ pub fn per_block_processing>( // previous block. if is_execution_enabled(state, block.body()) { let body = block.body(); - // TODO(EIP-7732): build out process_withdrawals variant for gloas - process_withdrawals::(state, body.execution_payload()?, spec)?; - process_execution_payload::(state, body, spec)?; + if state.fork_name_unchecked().gloas_enabled() { + process_withdrawals::gloas::process_withdrawals::(state, spec)?; + process_execution_payload_bid(state, block, verify_signatures, spec)?; + } else { + process_withdrawals::capella::process_withdrawals::( + state, + body.execution_payload()?, + spec, + )?; + process_execution_payload::(state, body, spec)?; + } } - // TODO(EIP-7732): build out process_execution_bid - // process_execution_bid(state, block, verify_signatures, spec)?; - process_randao(state, block, verify_randao, ctxt, spec)?; process_eth1_data(state, block.body().eth1_data())?; process_operations(state, block.body(), verify_signatures, ctxt, spec)?; @@ -514,189 +527,472 @@ pub fn compute_timestamp_at_slot( .and_then(|since_genesis| state.genesis_time().safe_add(since_genesis)) } -/// Compute the next batch of withdrawals which should be included in a block. -/// -/// https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-get_expected_withdrawals -pub fn get_expected_withdrawals( +pub fn convert_builder_index_to_validator_index(builder_index: BuilderIndex) -> u64 { + builder_index | BUILDER_INDEX_FLAG +} + +pub fn convert_validator_index_to_builder_index(validator_index: u64) -> BuilderIndex { + validator_index & !BUILDER_INDEX_FLAG +} + +pub fn get_balance_after_withdrawals( + state: &BeaconState, + validator_index: u64, + withdrawals: &[Withdrawal], +) -> Result { + let withdrawn = withdrawals + .iter() + .filter(|withdrawal| withdrawal.validator_index == validator_index) + .map(|withdrawal| withdrawal.amount) + .safe_sum()?; + state + .get_balance(validator_index as usize)? + .safe_sub(withdrawn) + .map_err(Into::into) +} + +fn is_eligible_for_partial_withdrawals( + validator: &Validator, + balance: u64, + spec: &ChainSpec, +) -> bool { + let has_sufficient_effective_balance = + validator.effective_balance >= spec.min_activation_balance; + let has_excess_balance = balance > spec.min_activation_balance; + validator.exit_epoch == spec.far_future_epoch + && has_sufficient_effective_balance + && has_excess_balance +} + +pub fn get_builder_withdrawals( + state: &BeaconState, + withdrawal_index: &mut u64, + withdrawals: &mut Vec, +) -> Result, BlockProcessingError> { + let Ok(builder_pending_withdrawals) = state.builder_pending_withdrawals() else { + // Pre-Gloas, nothing to do. + return Ok(None); + }; + + let withdrawals_limit = E::max_withdrawals_per_payload(); + + let mut processed_count = 0; + for withdrawal in builder_pending_withdrawals { + let has_reached_limit = withdrawals.len() == withdrawals_limit; + + if has_reached_limit { + break; + } + + let builder_index = withdrawal.builder_index; + + withdrawals.push(Withdrawal { + index: *withdrawal_index, + validator_index: convert_builder_index_to_validator_index(builder_index), + address: withdrawal.fee_recipient, + amount: withdrawal.amount, + }); + withdrawal_index.safe_add_assign(1)?; + processed_count.safe_add_assign(1)?; + } + Ok(Some(processed_count)) +} + +pub fn get_pending_partial_withdrawals( state: &BeaconState, + withdrawal_index: &mut u64, + withdrawals: &mut Vec, spec: &ChainSpec, -) -> Result<(Withdrawals, Option), BlockProcessingError> { +) -> Result, BlockProcessingError> { + let Ok(pending_partial_withdrawals) = state.pending_partial_withdrawals() else { + // Pre-Electra nothing to do. + return Ok(None); + }; let epoch = state.current_epoch(); - let mut withdrawal_index = state.next_withdrawal_index()?; - let mut validator_index = state.next_withdrawal_validator_index()?; - let mut withdrawals = Vec::::with_capacity(E::max_withdrawals_per_payload()); - let fork_name = state.fork_name_unchecked(); - // [New in Electra:EIP7251] - // Consume pending partial withdrawals - let processed_partial_withdrawals_count = - if let Ok(pending_partial_withdrawals) = state.pending_partial_withdrawals() { - let mut processed_partial_withdrawals_count = 0; - for withdrawal in pending_partial_withdrawals { - if withdrawal.withdrawable_epoch > epoch - || withdrawals.len() == spec.max_pending_partials_per_withdrawals_sweep as usize - { - break; - } + let withdrawals_limit = std::cmp::min( + withdrawals + .len() + .safe_add(spec.max_pending_partials_per_withdrawals_sweep as usize)?, + E::max_withdrawals_per_payload(), + ); - let validator = state.get_validator(withdrawal.validator_index as usize)?; - - let has_sufficient_effective_balance = - validator.effective_balance >= spec.min_activation_balance; - let total_withdrawn = withdrawals - .iter() - .filter_map(|w| { - (w.validator_index == withdrawal.validator_index).then_some(w.amount) - }) - .safe_sum()?; - let balance = state - .get_balance(withdrawal.validator_index as usize)? - .safe_sub(total_withdrawn)?; - let has_excess_balance = balance > spec.min_activation_balance; - - if validator.exit_epoch == spec.far_future_epoch - && has_sufficient_effective_balance - && has_excess_balance - { - let withdrawable_balance = std::cmp::min( - balance.safe_sub(spec.min_activation_balance)?, - withdrawal.amount, - ); - withdrawals.push(Withdrawal { - index: withdrawal_index, - validator_index: withdrawal.validator_index, - address: validator - .get_execution_withdrawal_address(spec) - .ok_or(BeaconStateError::NonExecutionAddressWithdrawalCredential)?, - amount: withdrawable_balance, - }); - withdrawal_index.safe_add_assign(1)?; - } - processed_partial_withdrawals_count.safe_add_assign(1)?; - } - Some(processed_partial_withdrawals_count) - } else { - None - }; + block_verify!( + withdrawals.len() <= withdrawals_limit, + BlockProcessingError::WithdrawalsLimitExceeded { + limit: withdrawals_limit, + prior_withdrawals: withdrawals.len() + } + ); + + let mut processed_count = 0; + for withdrawal in pending_partial_withdrawals { + let is_withdrawable = withdrawal.withdrawable_epoch <= epoch; + let has_reached_limit = withdrawals.len() >= withdrawals_limit; + + if !is_withdrawable || has_reached_limit { + break; + } + + let validator_index = withdrawal.validator_index; + let validator = state.get_validator(validator_index as usize)?; + let balance = get_balance_after_withdrawals(state, validator_index, withdrawals)?; + + if is_eligible_for_partial_withdrawals(validator, balance, spec) { + let withdrawal_amount = std::cmp::min( + balance.safe_sub(spec.min_activation_balance)?, + withdrawal.amount, + ); + withdrawals.push(Withdrawal { + index: *withdrawal_index, + validator_index, + address: validator + .get_execution_withdrawal_address(spec, state.fork_name_unchecked()) + .ok_or(BeaconStateError::NonExecutionAddressWithdrawalCredential)?, + amount: withdrawal_amount, + }); + withdrawal_index.safe_add_assign(1)?; + } + processed_count.safe_add_assign(1)?; + } + + Ok(Some(processed_count)) +} + +/// Get withdrawals from the builders sweep. +/// +/// This function iterates through builders starting from `next_withdrawal_builder_index` +/// and adds withdrawals for builders whose withdrawable_epoch has been reached and have balance. +/// +/// https://ethereum.github.io/consensus-specs/specs/gloas/beacon-chain/#new-get_builders_sweep_withdrawals +pub fn get_builders_sweep_withdrawals( + state: &BeaconState, + withdrawal_index: &mut u64, + withdrawals: &mut Vec, +) -> Result, BlockProcessingError> { + let Ok(builders) = state.builders() else { + // Pre-Gloas, nothing to do. + return Ok(None); + }; + + if builders.is_empty() { + return Ok(Some(0)); + } + + let epoch = state.current_epoch(); + let builders_limit = std::cmp::min(builders.len(), E::max_builders_per_withdrawals_sweep()); + // Reserve one slot for validator sweep withdrawals + let withdrawals_limit = E::max_withdrawals_per_payload().saturating_sub(1); + + block_verify!( + withdrawals.len() <= withdrawals_limit, + BlockProcessingError::WithdrawalsLimitExceeded { + limit: withdrawals_limit, + prior_withdrawals: withdrawals.len() + } + ); + + let mut processed_count: u64 = 0; + let mut builder_index = state.next_withdrawal_builder_index()?; + + for _ in 0..builders_limit { + if withdrawals.len() >= withdrawals_limit { + break; + } + + let builder = builders + .get(builder_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(builder_index))?; + + if builder.withdrawable_epoch <= epoch && builder.balance > 0 { + withdrawals.push(Withdrawal { + index: *withdrawal_index, + validator_index: convert_builder_index_to_validator_index(builder_index), + address: builder.execution_address, + amount: builder.balance, + }); + withdrawal_index.safe_add_assign(1)?; + } + + builder_index = builder_index.safe_add(1)?.safe_rem(builders.len() as u64)?; + processed_count.safe_add_assign(1)?; + } + + Ok(Some(processed_count)) +} - let bound = std::cmp::min( +/// Get withdrawals from the validator sweep. +/// +/// This function iterates through validators starting from `next_withdrawal_validator_index` +/// and adds full or partial withdrawals for eligible validators. +/// +/// https://ethereum.github.io/consensus-specs/specs/capella/beacon-chain/#new-get_validators_sweep_withdrawals +pub fn get_validators_sweep_withdrawals( + state: &BeaconState, + withdrawal_index: &mut u64, + withdrawals: &mut Vec, + spec: &ChainSpec, +) -> Result { + let epoch = state.current_epoch(); + let fork_name = state.fork_name_unchecked(); + let mut validator_index = state.next_withdrawal_validator_index()?; + let validators_limit = std::cmp::min( state.validators().len() as u64, spec.max_validators_per_withdrawals_sweep, ); - for _ in 0..bound { + let withdrawals_limit = E::max_withdrawals_per_payload(); + + // There must be at least one space reserved for validator sweep withdrawals + block_verify!( + withdrawals.len() < withdrawals_limit, + BlockProcessingError::WithdrawalsLimitExceeded { + limit: withdrawals_limit, + prior_withdrawals: withdrawals.len() + } + ); + + let mut processed_count: u64 = 0; + + for _ in 0..validators_limit { + if withdrawals.len() >= withdrawals_limit { + break; + } + let validator = state.get_validator(validator_index as usize)?; - let partially_withdrawn_balance = withdrawals - .iter() - .filter_map(|withdrawal| { - (withdrawal.validator_index == validator_index).then_some(withdrawal.amount) - }) - .safe_sum()?; - let balance = state - .balances() - .get(validator_index as usize) - .ok_or(BeaconStateError::BalancesOutOfBounds( - validator_index as usize, - ))? - .safe_sub(partially_withdrawn_balance)?; + let balance = get_balance_after_withdrawals(state, validator_index, withdrawals)?; + if validator.is_fully_withdrawable_validator(balance, epoch, spec, fork_name) { withdrawals.push(Withdrawal { - index: withdrawal_index, + index: *withdrawal_index, validator_index, address: validator - .get_execution_withdrawal_address(spec) + .get_execution_withdrawal_address(spec, fork_name) .ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?, amount: balance, }); withdrawal_index.safe_add_assign(1)?; } else if validator.is_partially_withdrawable_validator(balance, spec, fork_name) { withdrawals.push(Withdrawal { - index: withdrawal_index, + index: *withdrawal_index, validator_index, address: validator - .get_execution_withdrawal_address(spec) + .get_execution_withdrawal_address(spec, fork_name) .ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?, amount: balance.safe_sub(validator.get_max_effective_balance(spec, fork_name))?, }); withdrawal_index.safe_add_assign(1)?; } - if withdrawals.len() == E::max_withdrawals_per_payload() { - break; - } + validator_index = validator_index .safe_add(1)? .safe_rem(state.validators().len() as u64)?; + processed_count.safe_add_assign(1)?; } + Ok(processed_count) +} + +/// Compute the next batch of withdrawals which should be included in a block. +/// +/// https://ethereum.github.io/consensus-specs/specs/gloas/beacon-chain/#modified-get_expected_withdrawals +#[allow(clippy::type_complexity)] +pub fn get_expected_withdrawals( + state: &BeaconState, + spec: &ChainSpec, +) -> Result< + ( + Withdrawals, + Option, + Option, + Option, + u64, + ), + BlockProcessingError, +> { + let mut withdrawal_index = state.next_withdrawal_index()?; + let mut withdrawals = Vec::::with_capacity(E::max_withdrawals_per_payload()); + + // [New in Gloas:EIP7732] + // Get builder withdrawals + let processed_builder_withdrawals_count = + get_builder_withdrawals(state, &mut withdrawal_index, &mut withdrawals)?; + + // [New in Electra:EIP7251] + // Get partial withdrawals. + let processed_partial_withdrawals_count = + get_pending_partial_withdrawals(state, &mut withdrawal_index, &mut withdrawals, spec)?; + + // [New in Gloas:EIP7732] + // Get builders sweep withdrawals + let processed_builders_sweep_count = + get_builders_sweep_withdrawals(state, &mut withdrawal_index, &mut withdrawals)?; + + // Get validators sweep withdrawals + let processed_validators_sweep_count = + get_validators_sweep_withdrawals(state, &mut withdrawal_index, &mut withdrawals, spec)?; + Ok(( withdrawals .try_into() .map_err(BlockProcessingError::SszTypesError)?, + processed_builder_withdrawals_count, processed_partial_withdrawals_count, + processed_builders_sweep_count, + processed_validators_sweep_count, )) } -/// Apply withdrawals to the state. -/// TODO(EIP-7732): abstract this out and create gloas variant -pub fn process_withdrawals>( +pub fn process_execution_payload_bid>( state: &mut BeaconState, - payload: Payload::Ref<'_>, + block: BeaconBlockRef<'_, E, Payload>, + verify_signatures: VerifySignatures, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - if state.fork_name_unchecked().capella_enabled() { - let (expected_withdrawals, processed_partial_withdrawals_count) = - get_expected_withdrawals(state, spec)?; - let expected_root = expected_withdrawals.tree_hash_root(); - let withdrawals_root = payload.withdrawals_root()?; - - if expected_root != withdrawals_root { - return Err(BlockProcessingError::WithdrawalsRootMismatch { - expected: expected_root, - found: withdrawals_root, - }); + // 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; + let builder = state.get_validator(builder_index as usize)?; + + // For self-builds, amount must be zero regardless of withdrawal credential prefix + if builder_index == block.proposer_index() { + block_verify!(amount == 0, ExecutionPayloadBidInvalid::BadAmount.into()); + // TODO(EIP-7732): check with team if we should use ExecutionPayloadBidInvalid::BadSignature or a new error variant for this, like BadSelfBuildSignature + block_verify!( + signed_bid.signature.is_infinity(), + ExecutionPayloadBidInvalid::BadSignature.into() + ); + } else { + // Non-self builds require builder withdrawal credential + block_verify!( + builder.has_builder_withdrawal_credential(spec), + ExecutionPayloadBidInvalid::BadWithdrawalCredentials.into() + ); + if verify_signatures.is_true() { + block_verify!( + execution_payload_bid_signature_set( + state, + |i| get_pubkey_from_state(state, i), + signed_bid, + spec + )? + .verify(), + ExecutionPayloadBidInvalid::BadSignature.into() + ); } + } - for withdrawal in expected_withdrawals.iter() { - decrease_balance( - state, - withdrawal.validator_index as usize, - withdrawal.amount, - )?; - } + // Verify builder is active and not slashed + block_verify!( + builder.is_active_at(state.current_epoch()), + ExecutionPayloadBidInvalid::BuilderNotActive(builder_index).into() + ); + block_verify!( + !builder.slashed, + ExecutionPayloadBidInvalid::BuilderSlashed(builder_index).into() + ); - // Update pending partial withdrawals [New in Electra:EIP7251] - if let Some(processed_partial_withdrawals_count) = processed_partial_withdrawals_count { - state - .pending_partial_withdrawals_mut()? - .pop_front(processed_partial_withdrawals_count)?; - } + // Only perform payment related checks if amount > 0 + if amount > 0 { + // Check that the builder has funds to cover the bid + let pending_payments = state + .builder_pending_payments()? + .iter() + .filter_map(|payment| { + if payment.withdrawal.builder_index == builder_index { + Some(payment.withdrawal.amount) + } else { + None + } + }) + .safe_sum()?; - // Update the next withdrawal index if this block contained withdrawals - if let Some(latest_withdrawal) = expected_withdrawals.last() { - *state.next_withdrawal_index_mut()? = latest_withdrawal.index.safe_add(1)?; - - // Update the next validator index to start the next withdrawal sweep - if expected_withdrawals.len() == E::max_withdrawals_per_payload() { - // Next sweep starts after the latest withdrawal's validator index - let next_validator_index = latest_withdrawal - .validator_index - .safe_add(1)? - .safe_rem(state.validators().len() as u64)?; - *state.next_withdrawal_validator_index_mut()? = next_validator_index; + let pending_withdrawals = state + .builder_pending_withdrawals()? + .iter() + .filter_map(|withdrawal| { + if withdrawal.builder_index == builder_index { + Some(withdrawal.amount) + } else { + None + } + }) + .safe_sum()?; + + let builder_balance = state.get_balance(builder_index as usize)?; + + block_verify!( + builder_balance + >= amount + .safe_add(pending_payments)? + .safe_add(pending_withdrawals)? + .safe_add(spec.min_activation_balance)?, + ExecutionPayloadBidInvalid::InsufficientBalance { + builder_index, + builder_balance, + bid_value: amount, } + .into() + ); + } + + // Verify that the bid is for the current slot + block_verify!( + bid.slot == block.slot(), + ExecutionPayloadBidInvalid::SlotMismatch { + state_slot: block.slot(), + bid_slot: bid.slot, } + .into() + ); - // Advance sweep by the max length of the sweep if there was not a full set of withdrawals - if expected_withdrawals.len() != E::max_withdrawals_per_payload() { - let next_validator_index = state - .next_withdrawal_validator_index()? - .safe_add(spec.max_validators_per_withdrawals_sweep)? - .safe_rem(state.validators().len() as u64)?; - *state.next_withdrawal_validator_index_mut()? = next_validator_index; + // 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() + ); - Ok(()) - } else { - // these shouldn't even be encountered but they're here for completeness - Ok(()) + 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() + ); + + // 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::slots_per_epoch() + .safe_add(bid.slot.as_u64().safe_rem(E::slots_per_epoch())?)?) + as usize; + + *state + .builder_pending_payments_mut()? + .get_mut(payment_index) + .ok_or(BlockProcessingError::BeaconStateError( + BeaconStateError::BuilderPendingPaymentsIndexNotSupported(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 ff7c0204e24..50bae68076a 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -41,6 +41,10 @@ pub enum BlockProcessingError { index: usize, reason: AttestationInvalid, }, + PayloadAttestationInvalid { + index: usize, + reason: PayloadAttestationInvalid, + }, DepositInvalid { index: usize, reason: DepositInvalid, @@ -90,7 +94,17 @@ pub enum BlockProcessingError { found: Hash256, }, WithdrawalCredentialsInvalid, + /// This should be unreachable unless there's a logical flaw in the spec for withdrawals. + WithdrawalsLimitExceeded { + limit: usize, + prior_withdrawals: usize, + }, PendingAttestationInElectra, + ExecutionPayloadBidInvalid { + reason: ExecutionPayloadBidInvalid, + }, + /// Builder payment index out of bounds (Gloas) + BuilderPaymentIndexOutOfBounds(usize), } impl From for BlockProcessingError { @@ -147,6 +161,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 { @@ -200,7 +220,8 @@ impl_into_block_processing_error_with_index!( AttestationInvalid, DepositInvalid, ExitInvalid, - BlsExecutionChangeInvalid + BlsExecutionChangeInvalid, + PayloadAttestationInvalid ); pub type HeaderValidationError = BlockOperationError; @@ -331,7 +352,10 @@ pub enum AttestationInvalid { attestation: Slot, }, /// Attestation slot is too far in the past to be included in a block. - IncludedTooLate { state: Slot, attestation: Slot }, + IncludedTooLate { + state: Slot, + attestation: Slot, + }, /// Attestation target epoch does not match attestation slot. TargetEpochSlotMismatch { target_epoch: Epoch, @@ -364,6 +388,7 @@ pub enum AttestationInvalid { BadSignature, /// The indexed attestation created from this attestation was found to be invalid. BadIndexedAttestation(IndexedAttestationInvalid), + BadOverloadedDataIndex, } impl From> @@ -401,6 +426,58 @@ pub enum IndexedAttestationInvalid { SignatureSetError(SignatureSetError), } +#[derive(Debug, PartialEq, Clone)] +pub enum PayloadAttestationInvalid { + /// Block root does not match the parent beacon block root. + BlockRootMismatch { + expected: Hash256, + found: Hash256, + }, + /// The attestation slot is not the previous slot. + SlotMismatch { + expected: Slot, + found: Slot, + }, + BadIndexedPayloadAttestation(IndexedPayloadAttestationInvalid), +} + +impl From> + for BlockOperationError +{ + fn from(e: BlockOperationError) -> Self { + match e { + BlockOperationError::Invalid(e) => BlockOperationError::invalid( + PayloadAttestationInvalid::BadIndexedPayloadAttestation(e), + ), + BlockOperationError::BeaconStateError(e) => BlockOperationError::BeaconStateError(e), + BlockOperationError::SignatureSetError(e) => BlockOperationError::SignatureSetError(e), + BlockOperationError::SszTypesError(e) => BlockOperationError::SszTypesError(e), + BlockOperationError::BitfieldError(e) => BlockOperationError::BitfieldError(e), + BlockOperationError::ConsensusContext(e) => BlockOperationError::ConsensusContext(e), + BlockOperationError::ArithError(e) => BlockOperationError::ArithError(e), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum IndexedPayloadAttestationInvalid { + /// The number of indices is 0. + IndicesEmpty, + /// The validator indices were not in increasing order. + /// + /// The error occurred between the given `index` and `index + 1` + BadValidatorIndicesOrdering(usize), + /// The validator index is unknown. One cannot slash one who does not exist. + UnknownValidator(u64), + /// The indexed attestation aggregate signature was not valid. + BadSignature, + /// There was an error whilst attempting to get a set of signatures. The signatures may have + /// been invalid or an internal error occurred. + SignatureSetError(SignatureSetError), + /// Invalid Payload Status + PayloadStatusInvalid, +} + #[derive(Debug, PartialEq, Clone)] pub enum DepositInvalid { /// The signature (proof-of-possession) does not match the given pubkey. @@ -440,6 +517,38 @@ pub enum ExitInvalid { PendingWithdrawalInQueue(u64), } +#[derive(Debug, PartialEq, Clone)] +pub enum ExecutionPayloadBidInvalid { + /// The builder sent a 0 amount + BadAmount, + /// The signature is invalid. + BadSignature, + /// The builder's withdrawal credential is invalid + BadWithdrawalCredentials, + /// The builder is not an active validator. + BuilderNotActive(u64), + /// The builder is slashed + BuilderSlashed(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 state slot + SlotMismatch { state_slot: Slot, bid_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, + }, +} + #[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/is_valid_indexed_payload_attestation.rs b/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs new file mode 100644 index 00000000000..45ccdf35e22 --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs @@ -0,0 +1,50 @@ +use super::errors::{BlockOperationError, IndexedPayloadAttestationInvalid as Invalid}; +use super::signature_sets::{get_pubkey_from_state, indexed_payload_attestation_signature_set}; +use crate::VerifySignatures; +use itertools::Itertools; +use types::*; + +fn error(reason: Invalid) -> BlockOperationError { + BlockOperationError::invalid(reason) +} + +pub fn is_valid_indexed_payload_attestation( + state: &BeaconState, + indexed_payload_attestation: &IndexedPayloadAttestation, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockOperationError> { + // Verify indices are non-empty and sorted (duplicates allowed) + let indices = &indexed_payload_attestation.attesting_indices; + verify!(!indices.is_empty(), Invalid::IndicesEmpty); + let check_sorted = |list: &[u64]| -> Result<(), BlockOperationError> { + list.iter() + .tuple_windows() + .enumerate() + .try_for_each(|(i, (x, y))| { + if x <= y { + Ok(()) + } else { + Err(error(Invalid::BadValidatorIndicesOrdering(i))) + } + })?; + Ok(()) + }; + check_sorted(indices)?; + + if verify_signatures.is_true() { + verify!( + indexed_payload_attestation_signature_set( + state, + |i| get_pubkey_from_state(state, i), + &indexed_payload_attestation.signature, + indexed_payload_attestation, + spec + )? + .verify(), + Invalid::BadSignature + ); + } + + Ok(()) +} diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 8afeeb685bc..4d5553b5cf3 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -1,10 +1,11 @@ use super::*; -use crate::VerifySignatures; use crate::common::{ get_attestation_participation_flag_indices, increase_balance, initiate_validator_exit, slash_validator, }; use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; +use crate::per_block_processing::verify_payload_attestation::verify_payload_attestation; +use safe_arith::SafeArith; use ssz_types::FixedVector; use typenum::U33; use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; @@ -38,7 +39,15 @@ pub fn process_operations>( process_bls_to_execution_changes(state, bls_to_execution_changes, verify_signatures, spec)?; } - if state.fork_name_unchecked().electra_enabled() { + if state.fork_name_unchecked().gloas_enabled() { + process_payload_attestations( + state, + block_body.payload_attestations()?.iter(), + verify_signatures, + ctxt, + spec, + )?; + } else if state.fork_name_unchecked().electra_enabled() { state.update_pubkey_cache()?; process_deposit_requests(state, &block_body.execution_requests()?.deposits, spec)?; process_withdrawal_requests(state, &block_body.execution_requests()?.withdrawals, spec)?; @@ -212,6 +221,149 @@ pub mod altair_deneb { } } +// TODO(EIP-7732): add test cases to `consensus/state_processing/src/per_block_processing/tests.rs` to handle gloas. +// The tests will require being able to build gloas blocks, which currently fails due to errors as mentioned here. +// https://github.com/sigp/lighthouse/pull/8273 +pub mod gloas { + use super::*; + use crate::common::update_progressive_balances_cache::update_progressive_balances_on_attestation; + + pub fn process_attestations<'a, E: EthSpec, I>( + state: &mut BeaconState, + attestations: I, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> + where + I: Iterator>, + { + attestations.enumerate().try_for_each(|(i, attestation)| { + process_attestation(state, attestation, i, ctxt, verify_signatures, spec) + }) + } + + pub fn process_attestation( + state: &mut BeaconState, + attestation: AttestationRef, + att_index: usize, + ctxt: &mut ConsensusContext, + verify_signatures: VerifySignatures, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> { + let proposer_index = ctxt.get_proposer_index(state, spec)?; + let previous_epoch = ctxt.previous_epoch; + let current_epoch = ctxt.current_epoch; + + let indexed_att = verify_attestation_for_block_inclusion( + state, + attestation, + ctxt, + verify_signatures, + spec, + ) + .map_err(|e| e.into_with_index(att_index))?; + + // Matching roots, participation flag indices + let data = attestation.data(); + let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); + let participation_flag_indices = + get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; + + // [New in EIP-7732] + let current_epoch_target = data.target.epoch == state.current_epoch(); + let slot_mod = data + .slot + .as_usize() + .safe_rem(E::slots_per_epoch() as usize)?; + let payment_index = if current_epoch_target { + (E::slots_per_epoch() as usize).safe_add(slot_mod)? + } else { + slot_mod + }; + // Accumulate weight for same-slot attestations + let mut accumulated_weight = 0; + + // Update epoch participation flags. + let mut proposer_reward_numerator = 0; + for index in indexed_att.attesting_indices_iter() { + let index = *index as usize; + + let validator_effective_balance = state.epoch_cache().get_effective_balance(index)?; + let validator_slashed = state.slashings_cache().is_slashed(index); + + // [New in EIP7732] + // For same-slot attestations, check if we're setting any new flags + // If we are, this validator hasn't contributed to this slot's quorum yet + let mut will_set_new_flag = false; + + for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { + let epoch_participation = state.get_epoch_participation_mut( + data.target.epoch, + previous_epoch, + current_epoch, + )?; + + if participation_flag_indices.contains(&flag_index) { + let validator_participation = epoch_participation + .get_mut(index) + .ok_or(BeaconStateError::ParticipationOutOfBounds(index))?; + + if !validator_participation.has_flag(flag_index)? { + validator_participation.add_flag(flag_index)?; + proposer_reward_numerator + .safe_add_assign(state.get_base_reward(index)?.safe_mul(weight)?)?; + will_set_new_flag = true; + + update_progressive_balances_on_attestation( + state, + data.target.epoch, + flag_index, + validator_effective_balance, + validator_slashed, + )?; + } + } + } + + // Check that payment_index is valid and get payment amount + let builder_payments = state.builder_pending_payments_mut()?; + let payment_amount = builder_payments + .get(payment_index) + .ok_or(BlockProcessingError::BuilderPaymentIndexOutOfBounds( + payment_index, + ))? + .withdrawal + .amount; + + // Collect validators for Gloas builder payment processing + // We will only add weight for same-slot attestations when any new flag is set + // This ensures each validator contributes exactly once per slot + if will_set_new_flag && state.is_attestation_same_slot(data)? && payment_amount > 0 { + accumulated_weight.safe_add_assign(validator_effective_balance)?; + } + } + + let proposer_reward_denominator = WEIGHT_DENOMINATOR + .safe_sub(PROPOSER_WEIGHT)? + .safe_mul(WEIGHT_DENOMINATOR)? + .safe_div(PROPOSER_WEIGHT)?; + let proposer_reward = proposer_reward_numerator.safe_div(proposer_reward_denominator)?; + increase_balance(state, proposer_index as usize, proposer_reward)?; + + // Update builder payment weight + if accumulated_weight > 0 { + let builder_payments = state.builder_pending_payments_mut()?; + let payment = builder_payments.get_mut(payment_index).ok_or( + BlockProcessingError::BuilderPaymentIndexOutOfBounds(payment_index), + )?; + payment.weight.safe_add_assign(accumulated_weight)?; + } + + Ok(()) + } +} + /// Validates each `ProposerSlashing` and updates the state, short-circuiting on an invalid object. /// /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns @@ -235,6 +387,30 @@ pub fn process_proposer_slashings( verify_proposer_slashing(proposer_slashing, state, verify_signatures, spec) .map_err(|e| e.into_with_index(i))?; + // [New in Gloas:EIP7732] + // Remove the BuilderPendingPayment corresponding to this proposal + // if it is still in the 2-epoch window. + let slot = proposer_slashing.signed_header_1.message.slot; + let proposal_epoch = slot.epoch(E::slots_per_epoch()); + let current_epoch = state.current_epoch(); + let slot_in_epoch = slot.as_u64() % E::slots_per_epoch(); + + let payment_index = if proposal_epoch == current_epoch { + Some(E::slots_per_epoch() + slot_in_epoch) + } else if proposal_epoch == current_epoch.saturating_sub(1u64) { + Some(slot_in_epoch) + } else { + None + }; + + if let Some(index) = payment_index { + if let Ok(builder_pending_payments) = state.builder_pending_payments_mut() { + if let Some(payment) = builder_pending_payments.get_mut(index as usize) { + *payment = BuilderPendingPayment::default(); + } + } + } + slash_validator( state, proposer_slashing.signed_header_1.message.proposer_index as usize, @@ -285,7 +461,15 @@ pub fn process_attestations>( ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - if state.fork_name_unchecked().altair_enabled() { + if state.fork_name_unchecked().gloas_enabled() { + gloas::process_attestations( + state, + block_body.attestations(), + verify_signatures, + ctxt, + spec, + )?; + } else if state.fork_name_unchecked().altair_enabled() { altair_deneb::process_attestations( state, block_body.attestations(), @@ -514,9 +698,10 @@ pub fn process_withdrawal_requests( let validator = state.get_validator(validator_index)?; // Verify withdrawal credentials - let has_correct_credential = validator.has_execution_withdrawal_credential(spec); + let has_correct_credential = + validator.has_execution_withdrawal_credential(spec, state.fork_name_unchecked()); let is_correct_source_address = validator - .get_execution_withdrawal_address(spec) + .get_execution_withdrawal_address(spec, state.fork_name_unchecked()) .map(|addr| addr == request.source_address) .unwrap_or(false); @@ -561,7 +746,7 @@ pub fn process_withdrawal_requests( .safe_add(pending_balance_to_withdraw)?; // Only allow partial withdrawals with compounding withdrawal credentials - if validator.has_compounding_withdrawal_credential(spec) + if validator.has_compounding_withdrawal_credential(spec, state.fork_name_unchecked()) && has_sufficient_effective_balance && has_excess_balance { @@ -730,7 +915,9 @@ pub fn process_consolidation_request( let source_validator = state.get_validator(source_index)?; // Verify the source withdrawal credentials - if let Some(withdrawal_address) = source_validator.get_execution_withdrawal_address(spec) { + if let Some(withdrawal_address) = + source_validator.get_execution_withdrawal_address(spec, state.fork_name_unchecked()) + { if withdrawal_address != consolidation_request.source_address { return Ok(()); } @@ -741,7 +928,7 @@ pub fn process_consolidation_request( let target_validator = state.get_validator(target_index)?; // Verify the target has compounding withdrawal credentials - if !target_validator.has_compounding_withdrawal_credential(spec) { + if !target_validator.has_compounding_withdrawal_credential(spec, state.fork_name_unchecked()) { return Ok(()); } @@ -787,3 +974,52 @@ pub fn process_consolidation_request( Ok(()) } + +// TODO(EIP-7732): Add test cases for `process_payload_attestations` to +// `consensus/state_processing/src/per_block_processing/tests.rs`. +// The tests will require being able to build Gloas blocks with PayloadAttestations, +// which currently fails due to incomplete Gloas block structure as mentioned here +// https://github.com/sigp/lighthouse/pull/8273 +pub fn process_payload_attestation( + state: &mut BeaconState, + payload_attestation: &PayloadAttestation, + att_index: usize, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + verify_payload_attestation(state, payload_attestation, ctxt, verify_signatures, spec) + .map_err(|e| e.into_with_index(att_index)) +} + +pub fn process_payload_attestations<'a, E: EthSpec, I>( + state: &mut BeaconState, + payload_attestations: I, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> +where + I: Iterator>, +{ + // Ensure required caches are all built. These should be no-ops during regular operation. + // TODO(EIP-7732): verify necessary caches + state.build_committee_cache(RelativeEpoch::Current, spec)?; + state.build_committee_cache(RelativeEpoch::Previous, spec)?; + initialize_epoch_cache(state, spec)?; + initialize_progressive_balances_cache(state, spec)?; + state.build_slashings_cache()?; + + payload_attestations + .enumerate() + .try_for_each(|(i, payload_attestation)| { + process_payload_attestation( + state, + payload_attestation, + i, + verify_signatures, + ctxt, + spec, + ) + }) +} diff --git a/consensus/state_processing/src/per_block_processing/process_withdrawals.rs b/consensus/state_processing/src/per_block_processing/process_withdrawals.rs new file mode 100644 index 00000000000..419db5950c6 --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/process_withdrawals.rs @@ -0,0 +1,174 @@ +use super::errors::BlockProcessingError; +use super::get_expected_withdrawals; +use crate::common::decrease_balance; +use milhouse::List; +use safe_arith::SafeArith; +use tree_hash::TreeHash; +use types::{ + AbstractExecPayload, BeaconState, BuilderPendingWithdrawal, ChainSpec, EthSpec, ExecPayload, + Withdrawals, +}; + +/// Check if a builder payment is withdrawable. +/// A builder payment is withdrawable if the builder is not slashed or +/// the builder's withdrawable epoch has been reached. +pub fn is_builder_payment_withdrawable( + state: &BeaconState, + withdrawal: &BuilderPendingWithdrawal, +) -> Result { + let builder = state.get_validator(withdrawal.builder_index as usize)?; + let current_epoch = state.current_epoch(); + + Ok(builder.withdrawable_epoch >= current_epoch || !builder.slashed) +} + +fn process_withdrawals_common( + state: &mut BeaconState, + expected_withdrawals: Withdrawals, + partial_withdrawals_count: Option, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + match state { + BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) + | BeaconState::Fulu(_) + | BeaconState::Gloas(_) => { + // Update pending partial withdrawals [New in Electra:EIP7251] + if let Some(partial_withdrawals_count) = partial_withdrawals_count { + state + .pending_partial_withdrawals_mut()? + .pop_front(partial_withdrawals_count)?; + } + + // Update the next withdrawal index if this block contained withdrawals + if let Some(latest_withdrawal) = expected_withdrawals.last() { + *state.next_withdrawal_index_mut()? = latest_withdrawal.index.safe_add(1)?; + + // Update the next validator index to start the next withdrawal sweep + if expected_withdrawals.len() == E::max_withdrawals_per_payload() { + // Next sweep starts after the latest withdrawal's validator index + let next_validator_index = latest_withdrawal + .validator_index + .safe_add(1)? + .safe_rem(state.validators().len() as u64)?; + *state.next_withdrawal_validator_index_mut()? = next_validator_index; + } + } + + // Advance sweep by the max length of the sweep if there was not a full set of withdrawals + if expected_withdrawals.len() != E::max_withdrawals_per_payload() { + let next_validator_index = state + .next_withdrawal_validator_index()? + .safe_add(spec.max_validators_per_withdrawals_sweep)? + .safe_rem(state.validators().len() as u64)?; + *state.next_withdrawal_validator_index_mut()? = next_validator_index; + } + + Ok(()) + } + // these shouldn't even be encountered but they're here for completeness + BeaconState::Base(_) | BeaconState::Altair(_) | BeaconState::Bellatrix(_) => Ok(()), + } +} + +pub mod capella { + use super::*; + /// Apply withdrawals to the state. + pub fn process_withdrawals>( + state: &mut BeaconState, + payload: Payload::Ref<'_>, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> { + // check if capella enabled because this function will run on the merge block where the fork is technically still Bellatrix + if state.fork_name_unchecked().capella_enabled() { + let (expected_withdrawals, _, partial_withdrawals_count, _, _) = + get_expected_withdrawals(state, spec)?; + + let expected_root = expected_withdrawals.tree_hash_root(); + let withdrawals_root = payload.withdrawals_root()?; + if expected_root != withdrawals_root { + return Err(BlockProcessingError::WithdrawalsRootMismatch { + expected: expected_root, + found: withdrawals_root, + }); + } + + for withdrawal in expected_withdrawals.iter() { + decrease_balance( + state, + withdrawal.validator_index as usize, + withdrawal.amount, + )?; + } + + process_withdrawals_common(state, expected_withdrawals, partial_withdrawals_count, spec) + } else { + // these shouldn't even be encountered but they're here for completeness + Ok(()) + } + } +} +pub mod gloas { + use super::*; + + // TODO(EIP-7732): Add comprehensive tests for Gloas `process_withdrawals`: + // Similar to Capella version, these will be tested via: + // 1. EF consensus-spec tests in `testing/ef_tests/src/cases/operations.rs` + // 2. Integration tests via full block processing + // These tests would currently fail due to incomplete Gloas block structure as mentioned here, so we will implement them after block and payload processing is in a good state. + // https://github.com/sigp/lighthouse/pull/8273 + /// Apply withdrawals to the state. + pub fn process_withdrawals( + state: &mut BeaconState, + spec: &ChainSpec, + ) -> Result<(), BlockProcessingError> { + if !state.is_parent_block_full() { + return Ok(()); + } + + // TODO(EIP-7732): Use processed_builders_sweep_count to call update_next_withdrawal_builder_index + let ( + expected_withdrawals, + builder_withdrawals_count, + partial_withdrawals_count, + _processed_builders_sweep_count, + _, + ) = get_expected_withdrawals(state, spec)?; + + for withdrawal in expected_withdrawals.iter() { + decrease_balance( + state, + withdrawal.validator_index as usize, + withdrawal.amount, + )?; + } + + if let (Ok(builder_pending_withdrawals), Some(builder_count)) = ( + state.builder_pending_withdrawals(), + builder_withdrawals_count, + ) { + let mut updated_builder_withdrawals = + Vec::with_capacity(E::builder_pending_withdrawals_limit()); + + for (i, withdrawal) in builder_pending_withdrawals.iter().enumerate() { + if i < builder_count { + if !is_builder_payment_withdrawable(state, withdrawal)? { + updated_builder_withdrawals.push(withdrawal.clone()); + } + } else { + updated_builder_withdrawals.push(withdrawal.clone()); + } + } + + *state.builder_pending_withdrawals_mut()? = List::new(updated_builder_withdrawals)?; + } + + // [New in Gloas:EIP7732] update_payload_expected_withdrawals + *state.payload_expected_withdrawals_mut()? = List::new(expected_withdrawals.to_vec())?; + + process_withdrawals_common(state, expected_withdrawals, partial_withdrawals_count, spec)?; + + Ok(()) + } +} 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 0e936007eec..7f8c3dfe5c2 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -10,10 +10,11 @@ use typenum::Unsigned; use types::{ AbstractExecPayload, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork, - IndexedAttestation, IndexedAttestationRef, ProposerSlashing, SignedAggregateAndProof, - SignedBeaconBlock, SignedBeaconBlockHeader, SignedBlsToExecutionChange, - SignedContributionAndProof, SignedRoot, SignedVoluntaryExit, SigningData, Slot, SyncAggregate, - SyncAggregatorSelectionData, + IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, ProposerSlashing, + SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, + SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, + SignedExecutionPayloadEnvelope, SignedRoot, SignedVoluntaryExit, SigningData, Slot, + SyncAggregate, SyncAggregatorSelectionData, }; pub type Result = std::result::Result; @@ -299,6 +300,40 @@ where Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) } +pub fn indexed_payload_attestation_signature_set<'a, 'b, E, F>( + state: &'a BeaconState, + get_pubkey: F, + signature: &'a AggregateSignature, + indexed_payload_attestation: &'b IndexedPayloadAttestation, + spec: &'a ChainSpec, +) -> Result> +where + E: EthSpec, + F: Fn(usize) -> Option>, +{ + let mut pubkeys = Vec::with_capacity(indexed_payload_attestation.attesting_indices.len()); + for &validator_idx in indexed_payload_attestation.attesting_indices.iter() { + pubkeys.push( + get_pubkey(validator_idx as usize).ok_or(Error::ValidatorUnknown(validator_idx))?, + ); + } + + let epoch = indexed_payload_attestation + .data + .slot + .epoch(E::slots_per_epoch()); + let domain = spec.get_domain( + epoch, + Domain::PTCAttester, + &state.fork(), + state.genesis_validators_root(), + ); + + let message = indexed_payload_attestation.data.signing_root(domain); + + Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) +} + /// Returns the signature set for the given `indexed_attestation` but pubkeys are supplied directly /// instead of from the state. pub fn indexed_attestation_signature_set_from_pubkeys<'a, 'b, E, F>( @@ -332,6 +367,64 @@ where Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) } +pub fn execution_envelope_signature_set<'a, E, F>( + state: &'a BeaconState, + get_pubkey: F, + signed_envelope: &'a SignedExecutionPayloadEnvelope, + spec: &'a ChainSpec, +) -> Result> +where + E: EthSpec, + F: Fn(usize) -> Option>, +{ + let proposer_index = state.latest_block_header().proposer_index; + let builder_index = signed_envelope.message.builder_index(proposer_index); + let domain = spec.get_domain( + state.current_epoch(), + Domain::BeaconBuilder, + &state.fork(), + state.genesis_validators_root(), + ); + let message = signed_envelope.message.signing_root(domain); + let pubkey = + get_pubkey(builder_index as usize).ok_or(Error::ValidatorUnknown(builder_index))?; + + Ok(SignatureSet::single_pubkey( + &signed_envelope.signature, + pubkey, + message, + )) +} + +pub fn execution_payload_bid_signature_set<'a, E, F>( + state: &'a BeaconState, + get_pubkey: F, + signed_execution_payload_bid: &'a SignedExecutionPayloadBid, + spec: &'a ChainSpec, +) -> Result> +where + E: EthSpec, + F: Fn(usize) -> Option>, +{ + // TODO(EIP-7732): needs to handle self building! + let domain = spec.get_domain( + state.current_epoch(), + Domain::BeaconBuilder, + &state.fork(), + state.genesis_validators_root(), + ); + let execution_payload_bid = &signed_execution_payload_bid.message; + let pubkey = get_pubkey(execution_payload_bid.builder_index as usize) + .ok_or(Error::ValidatorUnknown(execution_payload_bid.builder_index))?; + let message = execution_payload_bid.signing_root(domain); + + Ok(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/state_processing/src/per_block_processing/verify_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_attestation.rs index 0d1fd17768e..68672773cf8 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attestation.rs @@ -66,6 +66,10 @@ pub fn verify_attestation_for_state<'ctxt, E: EthSpec>( // NOTE: choosing a validation based on the attestation's fork // rather than the state's fork makes this simple, but technically the spec // defines this verification based on the state's fork. + // Verify data.index based on attestation variant. + // The attestation variant is determined by the block body variant, which matches the fork. + + // TODO(EIP-7732): discuss if it makes more sense to match on `ForkName` instead of attestation type. A reason against is an edge case like at the Gloas fork boundary, the first gloas block will contain attestations for a Fulu block, so I would think we would want this validation to still be with respect to fulu rules. But perhaps I'm wrong? match attestation { AttestationRef::Base(_) => { verify!( @@ -74,7 +78,12 @@ pub fn verify_attestation_for_state<'ctxt, E: EthSpec>( ); } AttestationRef::Electra(_) => { - verify!(data.index == 0, Invalid::BadCommitteeIndex); + let fork_at_attestation_slot = spec.fork_name_at_slot::(data.slot); + if fork_at_attestation_slot.gloas_enabled() { + verify!(data.index < 2, Invalid::BadOverloadedDataIndex); + } else { + verify!(data.index == 0, Invalid::BadCommitteeIndex); + } } } diff --git a/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs new file mode 100644 index 00000000000..a65f132462c --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs @@ -0,0 +1,46 @@ +use super::VerifySignatures; +use super::errors::{BlockOperationError, PayloadAttestationInvalid as Invalid}; +use crate::ConsensusContext; +use crate::per_block_processing::is_valid_indexed_payload_attestation; +use safe_arith::SafeArith; +use types::*; + +pub fn verify_payload_attestation<'ctxt, E: EthSpec>( + state: &mut BeaconState, + payload_attestation: &'ctxt PayloadAttestation, + ctxt: &'ctxt mut ConsensusContext, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockOperationError> { + let data = &payload_attestation.data; + + // Check that the attestation is for the parent beacon block + verify!( + data.beacon_block_root == state.latest_block_header().parent_root, + Invalid::BlockRootMismatch { + expected: state.latest_block_header().parent_root, + found: data.beacon_block_root, + } + ); + + // Check that the attestation is for the previous slot + verify!( + data.slot.safe_add(1)? == state.slot(), + Invalid::SlotMismatch { + expected: state.slot().saturating_sub(Slot::new(1)), + found: data.slot, + } + ); + + let indexed_payload_attestation = + ctxt.get_indexed_payload_attestation(state, data.slot, payload_attestation, spec)?; + + is_valid_indexed_payload_attestation( + state, + indexed_payload_attestation, + verify_signatures, + spec, + )?; + + Ok(()) +} diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index 3e07803aa6a..b648f23226f 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -15,9 +15,9 @@ use std::collections::{BTreeSet, HashMap}; use tracing::instrument; use typenum::Unsigned; use types::{ - ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Checkpoint, DepositData, Epoch, - EthSpec, ExitCache, ForkName, ParticipationFlags, PendingDeposit, ProgressiveBalancesCache, - RelativeEpoch, Validator, + ActivationQueue, BeaconState, BeaconStateError, BuilderPendingPayment, ChainSpec, Checkpoint, + DepositData, Epoch, EthSpec, ExitCache, ForkName, ParticipationFlags, PendingDeposit, + ProgressiveBalancesCache, RelativeEpoch, Validator, consts::altair::{ NUM_FLAG_INDICES, PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, WEIGHT_DENOMINATOR, @@ -33,6 +33,7 @@ pub struct SinglePassConfig { pub pending_consolidations: bool, pub effective_balance_updates: bool, pub proposer_lookahead: bool, + pub builder_pending_payments: bool, } impl Default for SinglePassConfig { @@ -52,6 +53,7 @@ impl SinglePassConfig { pending_consolidations: true, effective_balance_updates: true, proposer_lookahead: true, + builder_pending_payments: true, } } @@ -65,6 +67,7 @@ impl SinglePassConfig { pending_consolidations: false, effective_balance_updates: false, proposer_lookahead: false, + builder_pending_payments: false, } } } @@ -455,6 +458,12 @@ pub fn process_epoch_single_pass( )?; } + // Process builder pending payments outside the single-pass loop, as they depend on balances for multiple + // validators and cannot be computed accurately inside the loop. + if fork_name.gloas_enabled() && conf.builder_pending_payments { + process_builder_pending_payments(state, state_ctxt, spec)?; + } + // Finally, finish updating effective balance caches. We need this to happen *after* processing // of pending consolidations, which recomputes some effective balances. if conf.effective_balance_updates { @@ -473,7 +482,7 @@ pub fn process_epoch_single_pass( Ok(summary) } -// TOOO(EIP-7917): use balances cache +// TODO(EIP-7917): use balances cache pub fn process_proposer_lookahead( state: &mut BeaconState, spec: &ChainSpec, @@ -503,6 +512,67 @@ pub fn process_proposer_lookahead( Ok(()) } +/// Calculate the quorum threshold for builder payments based on total active balance. +fn get_builder_payment_quorum_threshold( + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result { + let per_slot_balance = state_ctxt + .total_active_balance + .safe_div(E::slots_per_epoch())?; + let quorum = per_slot_balance.safe_mul(spec.builder_payment_threshold_numerator)?; + quorum + .safe_div(spec.builder_payment_threshold_denominator) + .map_err(Error::from) +} + +/// Process builder pending payments, moving qualifying payments to withdrawals. +/// TODO(EIP-7732): Add EF consensus-spec tests for `process_builder_pending_payments` +/// Currently blocked by EF consensus-spec-tests for Gloas not yet integrated. +fn process_builder_pending_payments( + state: &mut BeaconState, + state_ctxt: &StateContext, + spec: &ChainSpec, +) -> Result<(), Error> { + let quorum = get_builder_payment_quorum_threshold::(state_ctxt, spec)?; + + // Collect qualifying payments + let qualifying_payments = state + .builder_pending_payments()? + .iter() + .take(E::slots_per_epoch() as usize) + .filter(|payment| payment.weight > quorum) + .cloned() + .collect::>(); + + // Update `builder_pending_withdrawals` with qualifying `builder_pending_payments` + qualifying_payments + .into_iter() + .try_for_each(|payment| -> Result<(), Error> { + let exit_queue_epoch = + state.compute_exit_epoch_and_update_churn(payment.withdrawal.amount, spec)?; + let _withdrawable_epoch = + exit_queue_epoch.safe_add(spec.min_validator_withdrawability_delay)?; + + let withdrawal = payment.withdrawal.clone(); + state.builder_pending_withdrawals_mut()?.push(withdrawal)?; + Ok(()) + })?; + + // Move remaining `builder_pending_payments` to start of list and set the rest to default + let new_payments = state + .builder_pending_payments()? + .iter() + .skip(E::slots_per_epoch() as usize) + .cloned() + .chain((0..E::slots_per_epoch() as usize).map(|_| BuilderPendingPayment::default())) + .collect::>(); + + *state.builder_pending_payments_mut()? = Vector::new(new_payments)?; + + Ok(()) +} + fn process_single_inactivity_update( inactivity_score: &mut Cow, validator_info: &ValidatorInfo, diff --git a/consensus/state_processing/src/per_slot_processing.rs b/consensus/state_processing/src/per_slot_processing.rs index 0f8e5dc52d8..1d35d28c273 100644 --- a/consensus/state_processing/src/per_slot_processing.rs +++ b/consensus/state_processing/src/per_slot_processing.rs @@ -14,6 +14,7 @@ pub enum Error { EpochProcessingError(EpochProcessingError), ArithError(ArithError), InconsistentStateFork(InconsistentFork), + BitfieldError(ssz::BitfieldError), } impl From for Error { @@ -22,6 +23,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: ssz::BitfieldError) -> Self { + Self::BitfieldError(e) + } +} + /// Advances a state forward by one slot, performing per-epoch processing if required. /// /// If the root of the supplied `state` is known, then it can be passed as `state_root`. If @@ -50,6 +57,18 @@ pub fn per_slot_processing( state.slot_mut().safe_add_assign(1)?; + // Unset the next payload availability + if state.fork_name_unchecked().gloas_enabled() { + let next_slot_index = state + .slot() + .as_usize() + .safe_add(1)? + .safe_rem(E::slots_per_historical_root())?; + state + .execution_payload_availability_mut()? + .set(next_slot_index, false)?; + } + // Process fork upgrades here. Note that multiple upgrades can potentially run // in sequence if they are scheduled in the same Epoch (common in testnets) if state.slot().safe_rem(E::slots_per_epoch())? == 0 { diff --git a/consensus/state_processing/src/upgrade/electra.rs b/consensus/state_processing/src/upgrade/electra.rs index 258b28a45bd..a84b81d85c1 100644 --- a/consensus/state_processing/src/upgrade/electra.rs +++ b/consensus/state_processing/src/upgrade/electra.rs @@ -82,7 +82,7 @@ pub fn upgrade_to_electra( // Ensure early adopters of compounding credentials go through the activation churn let validators = post.validators().clone(); for (index, validator) in validators.iter().enumerate() { - if validator.has_compounding_withdrawal_credential(spec) { + if validator.has_compounding_withdrawal_credential(spec, post.fork_name_unchecked()) { post.queue_excess_active_balance(index, spec)?; } } diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index 81c0fcfe63e..1c37d5e5201 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -1,7 +1,7 @@ -use bls::Hash256; use milhouse::{List, Vector}; use ssz_types::BitVector; use std::mem; +use typenum::Unsigned; use types::{ BeaconState, BeaconStateError as Error, BeaconStateGloas, BuilderPendingPayment, ChainSpec, EthSpec, ExecutionPayloadBid, Fork, @@ -88,15 +88,23 @@ pub fn upgrade_state_to_gloas( pending_deposits: pre.pending_deposits.clone(), pending_partial_withdrawals: pre.pending_partial_withdrawals.clone(), pending_consolidations: pre.pending_consolidations.clone(), + proposer_lookahead: mem::take(&mut pre.proposer_lookahead), // Gloas - execution_payload_availability: BitVector::default(), // All bits set to false initially + builders: List::default(), + next_withdrawal_builder_index: 0, + // All bits set to true per spec: + // execution_payload_availability = [0b1 for _ in range(SLOTS_PER_HISTORICAL_ROOT)] + execution_payload_availability: BitVector::from_bytes( + vec![0xFFu8; E::SlotsPerHistoricalRoot::to_usize() / 8].into(), + ) + .expect("SlotsPerHistoricalRoot is always divisible by 8"), builder_pending_payments: Vector::new(vec![ BuilderPendingPayment::default(); E::builder_pending_payments_limit() ])?, builder_pending_withdrawals: List::default(), // Empty list initially, latest_block_hash: pre.latest_execution_payload_header.block_hash, - latest_withdrawals_root: Hash256::default(), + payload_expected_withdrawals: List::default(), // Caches total_active_balance: pre.total_active_balance, progressive_balances_cache: mem::take(&mut pre.progressive_balances_cache), @@ -105,7 +113,6 @@ pub fn upgrade_state_to_gloas( exit_cache: mem::take(&mut pre.exit_cache), slashings_cache: mem::take(&mut pre.slashings_cache), epoch_cache: mem::take(&mut pre.epoch_cache), - proposer_lookahead: mem::take(&mut pre.proposer_lookahead), }); Ok(post) } diff --git a/consensus/types/presets/mainnet/gloas.yaml b/consensus/types/presets/mainnet/gloas.yaml index 45b7b8ae968..2da198dcae6 100644 --- a/consensus/types/presets/mainnet/gloas.yaml +++ b/consensus/types/presets/mainnet/gloas.yaml @@ -1 +1,23 @@ # Mainnet preset - Gloas + +# Misc +# --------------------------------------------------------------- +# 2**9 (= 512) validators +PTC_SIZE: 512 + +# Max operations per block +# --------------------------------------------------------------- +# 2**2 (= 4) attestations +MAX_PAYLOAD_ATTESTATIONS: 4 + +# State list lengths +# --------------------------------------------------------------- +# 2**40 (= 1,099,511,627,776) builder spots +BUILDER_REGISTRY_LIMIT: 1099511627776 +# 2**20 (= 1,048,576) builder pending withdrawals +BUILDER_PENDING_WITHDRAWALS_LIMIT: 1048576 + +# Withdrawals processing +# --------------------------------------------------------------- +# 2**14 (= 16,384) builders +MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: 16384 \ No newline at end of file diff --git a/consensus/types/presets/minimal/gloas.yaml b/consensus/types/presets/minimal/gloas.yaml index 51b3f048571..7ae61ddf97f 100644 --- a/consensus/types/presets/minimal/gloas.yaml +++ b/consensus/types/presets/minimal/gloas.yaml @@ -1 +1,23 @@ # Minimal preset - Gloas + +# Misc +# --------------------------------------------------------------- +# [customized] 2**1 (= 2) validators +PTC_SIZE: 2 + +# Max operations per block +# --------------------------------------------------------------- +# 2**2 (= 4) attestations +MAX_PAYLOAD_ATTESTATIONS: 4 + +# State list lengths +# --------------------------------------------------------------- +# 2**40 (= 1,099,511,627,776) builder spots +BUILDER_REGISTRY_LIMIT: 1099511627776 +# 2**20 (= 1,048,576) builder pending withdrawals +BUILDER_PENDING_WITHDRAWALS_LIMIT: 1048576 + +# Withdrawals processing +# --------------------------------------------------------------- +# [customized] 2**4 (= 16) builders +MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: 16 diff --git a/consensus/types/src/attestation/mod.rs b/consensus/types/src/attestation/mod.rs index 586d99bd900..5b59b83e726 100644 --- a/consensus/types/src/attestation/mod.rs +++ b/consensus/types/src/attestation/mod.rs @@ -11,6 +11,7 @@ mod payload_attestation; mod payload_attestation_data; mod payload_attestation_message; mod pending_attestation; +mod ptc; mod selection_proof; mod shuffling_id; mod signed_aggregate_and_proof; @@ -36,6 +37,7 @@ pub use payload_attestation::PayloadAttestation; pub use payload_attestation_data::PayloadAttestationData; pub use payload_attestation_message::PayloadAttestationMessage; pub use pending_attestation::PendingAttestation; +pub use ptc::PTC; pub use selection_proof::SelectionProof; pub use shuffling_id::AttestationShufflingId; pub use signed_aggregate_and_proof::{ diff --git a/consensus/types/src/attestation/payload_attestation.rs b/consensus/types/src/attestation/payload_attestation.rs index 192a4a8fea5..115a5ec4d62 100644 --- a/consensus/types/src/attestation/payload_attestation.rs +++ b/consensus/types/src/attestation/payload_attestation.rs @@ -5,7 +5,7 @@ use bls::AggregateSignature; use context_deserialize::context_deserialize; use educe::Educe; use serde::{Deserialize, Serialize}; -use ssz::BitList; +use ssz::BitVector; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; @@ -17,7 +17,7 @@ use tree_hash_derive::TreeHash; #[educe(PartialEq, Hash)] #[context_deserialize(ForkName)] pub struct PayloadAttestation { - pub aggregation_bits: BitList, + pub aggregation_bits: BitVector, pub data: PayloadAttestationData, pub signature: AggregateSignature, } diff --git a/consensus/types/src/attestation/ptc.rs b/consensus/types/src/attestation/ptc.rs new file mode 100644 index 00000000000..39fbee33a23 --- /dev/null +++ b/consensus/types/src/attestation/ptc.rs @@ -0,0 +1,24 @@ +use crate::EthSpec; +use ssz_types::FixedVector; + +/// TODO(EIP-7732): is it easier to return u64 or usize? +#[derive(Clone, Debug, PartialEq)] +pub struct PTC(pub FixedVector); + +impl<'a, E: EthSpec> IntoIterator for &'a PTC { + type Item = &'a usize; + type IntoIter = std::slice::Iter<'a, usize>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl IntoIterator for PTC { + type Item = usize; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} diff --git a/consensus/types/src/builder/builder.rs b/consensus/types/src/builder/builder.rs new file mode 100644 index 00000000000..81ca45046c7 --- /dev/null +++ b/consensus/types/src/builder/builder.rs @@ -0,0 +1,24 @@ +use crate::test_utils::TestRandom; +use crate::{Address, Epoch}; +use bls::PublicKeyBytes; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use test_random_derive::TestRandom; +use tree_hash_derive::TreeHash; + +pub type BuilderIndex = u64; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encode, Decode, TestRandom, TreeHash, +)] +pub struct Builder { + pub pubkey: PublicKeyBytes, + #[serde(with = "serde_utils::quoted_u8")] + pub version: u8, + pub execution_address: Address, + #[serde(with = "serde_utils::quoted_u64")] + pub balance: u64, + pub deposit_epoch: Epoch, + pub withdrawable_epoch: Epoch, +} diff --git a/consensus/types/src/builder/builder_pending_withdrawal.rs b/consensus/types/src/builder/builder_pending_withdrawal.rs index 436d331c003..dbbb029a5d8 100644 --- a/consensus/types/src/builder/builder_pending_withdrawal.rs +++ b/consensus/types/src/builder/builder_pending_withdrawal.rs @@ -1,5 +1,5 @@ use crate::test_utils::TestRandom; -use crate::{Address, Epoch, ForkName}; +use crate::{Address, ForkName}; use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -29,7 +29,6 @@ pub struct BuilderPendingWithdrawal { pub amount: u64, #[serde(with = "serde_utils::quoted_u64")] pub builder_index: u64, - pub withdrawable_epoch: Epoch, } #[cfg(test)] diff --git a/consensus/types/src/builder/mod.rs b/consensus/types/src/builder/mod.rs index 54d0ae4eb73..f4e0e346f24 100644 --- a/consensus/types/src/builder/mod.rs +++ b/consensus/types/src/builder/mod.rs @@ -1,7 +1,9 @@ +mod builder; mod builder_bid; mod builder_pending_payment; mod builder_pending_withdrawal; +pub use builder::{Builder, BuilderIndex}; pub use builder_bid::{ BuilderBid, BuilderBidBellatrix, BuilderBidCapella, BuilderBidDeneb, BuilderBidElectra, BuilderBidFulu, SignedBuilderBid, diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 1bdf6c2cb86..c02456540c1 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -36,6 +36,7 @@ pub enum Domain { SyncCommitteeSelectionProof, BeaconBuilder, PTCAttester, + ProposerPreferences, ApplicationMask(ApplicationDomain), } @@ -130,6 +131,7 @@ pub struct ChainSpec { pub(crate) domain_aggregate_and_proof: u32, pub(crate) domain_beacon_builder: u32, pub(crate) domain_ptc_attester: u32, + pub(crate) domain_proposer_preferences: u32, /* * Fork choice @@ -234,6 +236,7 @@ pub struct ChainSpec { pub gloas_fork_epoch: Option, pub builder_payment_threshold_numerator: u64, pub builder_payment_threshold_denominator: u64, + pub min_builder_withdrawability_delay: Epoch, /* * Networking @@ -500,6 +503,7 @@ impl ChainSpec { Domain::AggregateAndProof => self.domain_aggregate_and_proof, Domain::BeaconBuilder => self.domain_beacon_builder, Domain::PTCAttester => self.domain_ptc_attester, + Domain::ProposerPreferences => self.domain_proposer_preferences, Domain::SyncCommittee => self.domain_sync_committee, Domain::ContributionAndProof => self.domain_contribution_and_proof, Domain::SyncCommitteeSelectionProof => self.domain_sync_committee_selection_proof, @@ -977,8 +981,9 @@ impl ChainSpec { domain_voluntary_exit: 4, domain_selection_proof: 5, domain_aggregate_and_proof: 6, - domain_beacon_builder: 0x1B, + domain_beacon_builder: 0x0B, domain_ptc_attester: 0x0C, + domain_proposer_preferences: 0x0D, /* * Fork choice @@ -1102,6 +1107,7 @@ impl ChainSpec { gloas_fork_epoch: None, builder_payment_threshold_numerator: 6, builder_payment_threshold_denominator: 10, + min_builder_withdrawability_delay: Epoch::new(4096), /* * Network specific @@ -1242,7 +1248,7 @@ impl ChainSpec { fulu_fork_version: [0x06, 0x00, 0x00, 0x01], fulu_fork_epoch: None, // Gloas - gloas_fork_version: [0x07, 0x00, 0x00, 0x00], + gloas_fork_version: [0x07, 0x00, 0x00, 0x01], gloas_fork_epoch: None, // Other network_id: 2, // lighthouse testnet network id @@ -1350,8 +1356,9 @@ impl ChainSpec { domain_voluntary_exit: 4, domain_selection_proof: 5, domain_aggregate_and_proof: 6, - domain_beacon_builder: 0x1B, + domain_beacon_builder: 0x0B, domain_ptc_attester: 0x0C, + domain_proposer_preferences: 0x0D, /* * Fork choice @@ -1474,6 +1481,7 @@ impl ChainSpec { gloas_fork_epoch: None, builder_payment_threshold_numerator: 6, builder_payment_threshold_denominator: 10, + min_builder_withdrawability_delay: Epoch::new(4096), /* * Network specific diff --git a/consensus/types/src/core/consts.rs b/consensus/types/src/core/consts.rs index b6d63c47a88..2c67657ee6d 100644 --- a/consensus/types/src/core/consts.rs +++ b/consensus/types/src/core/consts.rs @@ -25,3 +25,17 @@ pub mod bellatrix { pub mod deneb { pub use kzg::VERSIONED_HASH_VERSION_KZG; } +pub mod gloas { + pub const BUILDER_INDEX_SELF_BUILD: u64 = u64::MAX; + pub const BUILDER_INDEX_FLAG: u64 = 1 << 40; + + // Fork choice constants + pub type PayloadStatus = u8; + pub const PAYLOAD_STATUS_PENDING: PayloadStatus = 0; + pub const PAYLOAD_STATUS_EMPTY: PayloadStatus = 1; + pub const PAYLOAD_STATUS_FULL: PayloadStatus = 2; + + pub const ATTESTATION_TIMELINESS_INDEX: usize = 0; + pub const PTC_TIMELINESS_INDEX: usize = 1; + pub const NUM_BLOCK_TIMELINESS_DEADLINES: usize = 2; +} diff --git a/consensus/types/src/core/eth_spec.rs b/consensus/types/src/core/eth_spec.rs index 2983fc2c629..a4b22da3f88 100644 --- a/consensus/types/src/core/eth_spec.rs +++ b/consensus/types/src/core/eth_spec.rs @@ -7,7 +7,7 @@ use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; use typenum::{ U0, U1, U2, U4, U8, U16, U17, U32, U64, U128, U256, U512, U625, U1024, U2048, U4096, U8192, - U65536, U131072, U262144, U1048576, U16777216, U33554432, U134217728, U1073741824, + U16384, U65536, U131072, U262144, U1048576, U16777216, U33554432, U134217728, U1073741824, U1099511627776, UInt, Unsigned, bit::B0, }; @@ -122,6 +122,10 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + type CellsPerExtBlob: Unsigned + Clone + Sync + Send + Debug + PartialEq; type NumberOfColumns: Unsigned + Clone + Sync + Send + Debug + PartialEq; type ProposerLookaheadSlots: Unsigned + Clone + Sync + Send + Debug + PartialEq; + /* + * New in Gloas + */ + type BuilderRegistryLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; /* * Derived values (set these CAREFULLY) */ @@ -175,6 +179,7 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + type MaxPayloadAttestations: Unsigned + Clone + Sync + Send + Debug + PartialEq; type BuilderPendingPaymentsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; type BuilderPendingWithdrawalsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type MaxBuildersPerWithdrawalsSweep: Unsigned + Clone + Sync + Send + Debug + PartialEq; fn default_spec() -> ChainSpec; @@ -427,6 +432,16 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + fn max_payload_attestations() -> usize { Self::MaxPayloadAttestations::to_usize() } + + /// Returns the `MaxBuildersPerWithdrawalsSweep` constant for this specification. + fn max_builders_per_withdrawals_sweep() -> usize { + Self::MaxBuildersPerWithdrawalsSweep::to_usize() + } + + /// Returns the `PAYLOAD_TIMELY_THRESHOLD` constant (PTC_SIZE / 2). + fn payload_timely_threshold() -> usize { + Self::PTCSize::to_usize() / 2 + } } /// Macro to inherit some type values from another EthSpec. @@ -484,6 +499,7 @@ impl EthSpec for MainnetEthSpec { type CellsPerExtBlob = U128; type NumberOfColumns = U128; type ProposerLookaheadSlots = U64; // Derived from (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH + type BuilderRegistryLimit = U1099511627776; type SyncSubcommitteeSize = U128; // 512 committee size / 4 sync committee subnet count type MaxPendingAttestations = U4096; // 128 max attestations * 32 slots per epoch type SlotsPerEth1VotingPeriod = U2048; // 64 epochs * 32 slots per epoch @@ -500,6 +516,7 @@ impl EthSpec for MainnetEthSpec { type MaxPendingDepositsPerEpoch = U16; type PTCSize = U512; type MaxPayloadAttestations = U4; + type MaxBuildersPerWithdrawalsSweep = U16384; fn default_spec() -> ChainSpec { ChainSpec::mainnet() @@ -543,6 +560,8 @@ impl EthSpec for MinimalEthSpec { type NumberOfColumns = U128; type ProposerLookaheadSlots = U16; // Derived from (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH type BuilderPendingPaymentsLimit = U16; // 2 * SLOTS_PER_EPOCH = 2 * 8 = 16 + type PTCSize = U2; + type MaxBuildersPerWithdrawalsSweep = U16; params_from_eth_spec!(MainnetEthSpec { JustificationBitsLength, @@ -573,8 +592,8 @@ impl EthSpec for MinimalEthSpec { MaxAttestationsElectra, MaxDepositRequestsPerPayload, MaxWithdrawalRequestsPerPayload, - PTCSize, - MaxPayloadAttestations + MaxPayloadAttestations, + BuilderRegistryLimit }); fn default_spec() -> ChainSpec { @@ -647,8 +666,10 @@ impl EthSpec for GnosisEthSpec { type CellsPerExtBlob = U128; type NumberOfColumns = U128; type ProposerLookaheadSlots = U32; // Derived from (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH + type BuilderRegistryLimit = U1099511627776; type PTCSize = U512; type MaxPayloadAttestations = U2; + type MaxBuildersPerWithdrawalsSweep = U16384; fn default_spec() -> ChainSpec { ChainSpec::gnosis() diff --git a/consensus/types/src/execution/execution_payload_envelope.rs b/consensus/types/src/execution/execution_payload_envelope.rs index 64e03cec5a9..a82517a16e8 100644 --- a/consensus/types/src/execution/execution_payload_envelope.rs +++ b/consensus/types/src/execution/execution_payload_envelope.rs @@ -1,3 +1,4 @@ +use crate::consts::gloas::BUILDER_INDEX_SELF_BUILD; use crate::test_utils::TestRandom; use crate::{ EthSpec, ExecutionPayloadGloas, ExecutionRequests, ForkName, Hash256, KzgCommitments, @@ -17,8 +18,10 @@ use tree_hash_derive::TreeHash; pub struct ExecutionPayloadEnvelope { pub payload: ExecutionPayloadGloas, pub execution_requests: ExecutionRequests, + // The builder index is private so that callers are forced to handle the case where it equals + // BUILDER_INDEX_SELF_BUILD. #[serde(with = "serde_utils::quoted_u64")] - pub builder_index: u64, + builder_index: u64, pub beacon_block_root: Hash256, pub slot: Slot, pub blob_kzg_commitments: KzgCommitments, @@ -27,6 +30,28 @@ pub struct ExecutionPayloadEnvelope { impl SignedRoot for ExecutionPayloadEnvelope {} +impl ExecutionPayloadEnvelope { + /// Fetch the validator index of the builder of this execution payload. + /// + /// This falls back to the provided `proposer_index` if the builder index indicates + /// self-building. + pub fn builder_index(&self, proposer_index: u64) -> u64 { + if self.builder_index == BUILDER_INDEX_SELF_BUILD { + proposer_index + } else { + self.builder_index + } + } + + /// Fetch the raw builder index, which may be `BUILDER_INDEX_SELF_BUILD` to indicate + /// self-building. + /// + /// This method should be used sparingly. + pub fn raw_builder_index(&self) -> u64 { + self.builder_index + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/execution/signed_execution_payload_envelope.rs b/consensus/types/src/execution/signed_execution_payload_envelope.rs index 16410416157..58f0ae0565c 100644 --- a/consensus/types/src/execution/signed_execution_payload_envelope.rs +++ b/consensus/types/src/execution/signed_execution_payload_envelope.rs @@ -1,6 +1,9 @@ use crate::test_utils::TestRandom; -use crate::{EthSpec, ExecutionPayloadEnvelope}; -use bls::Signature; +use crate::{ + BeaconState, BeaconStateError, ChainSpec, Domain, Epoch, EthSpec, ExecutionBlockHash, + ExecutionPayloadEnvelope, Fork, Hash256, SignedRoot, Slot, +}; +use bls::{PublicKey, Signature}; use educe::Educe; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -15,6 +18,77 @@ pub struct SignedExecutionPayloadEnvelope { pub signature: Signature, } +impl SignedExecutionPayloadEnvelope { + pub fn slot(&self) -> Slot { + self.message.slot + } + + pub fn epoch(&self) -> Epoch { + self.slot().epoch(E::slots_per_epoch()) + } + + pub fn beacon_block_root(&self) -> Hash256 { + self.message.beacon_block_root + } + + pub fn block_hash(&self) -> ExecutionBlockHash { + self.message.payload.block_hash + } + + /// Verify `self.signature`. + /// + /// The `parent_state` is the post-state of the beacon block with + /// block_root = self.message.beacon_block_root + /// TODO(EIP-7732): maybe delete this function later (it is inefficient) + pub fn verify_signature_with_state( + &self, + parent_state: &BeaconState, + spec: &ChainSpec, + ) -> Result { + let proposer_index = parent_state.latest_block_header().proposer_index; + let builder_index = self.message.builder_index(proposer_index) as usize; + let domain = spec.get_domain( + parent_state.current_epoch(), + Domain::BeaconBuilder, + &parent_state.fork(), + parent_state.genesis_validators_root(), + ); + let pubkey = parent_state + .validators() + .get(builder_index) + .and_then(|v| { + let pk: Option = v.pubkey.decompress().ok(); + pk + }) + .ok_or(BeaconStateError::UnknownValidator(builder_index))?; + let message = self.message.signing_root(domain); + + Ok(self.signature.verify(&pubkey, message)) + } + + /// Verify `self.signature`. + pub fn verify_signature( + &self, + pubkey: &PublicKey, + fork: &Fork, + genesis_validators_root: Hash256, + spec: &ChainSpec, + ) -> bool { + // Signed envelopes using the new BeaconBuilder domain per the spec: + // https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-verify_execution_payload_envelope_signature + let domain = spec.get_domain( + self.epoch(), + Domain::BeaconBuilder, + fork, + genesis_validators_root, + ); + + let message = self.message.signing_root(domain); + + self.signature.verify(pubkey, message) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index f3e5ae411b8..0e6897cf051 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -23,10 +23,11 @@ use tree_hash_derive::TreeHash; use typenum::Unsigned; use crate::{ - BuilderPendingPayment, BuilderPendingWithdrawal, ExecutionBlockHash, ExecutionPayloadBid, + Builder, BuilderIndex, BuilderPendingPayment, BuilderPendingWithdrawal, ExecutionBlockHash, + ExecutionPayloadBid, Withdrawal, attestation::{ - AttestationDuty, BeaconCommittee, Checkpoint, CommitteeIndex, ParticipationFlags, - PendingAttestation, + AttestationData, AttestationDuty, BeaconCommittee, Checkpoint, CommitteeIndex, PTC, + ParticipationFlags, PendingAttestation, }, block::{BeaconBlock, BeaconBlockHeader, SignedBeaconBlockHash}, consolidation::PendingConsolidation, @@ -67,6 +68,7 @@ pub enum BeaconStateError { EpochOutOfBounds, SlotOutOfBounds, UnknownValidator(usize), + UnknownBuilder(u64), UnableToDetermineProducer, InvalidBitfield, EmptyCommittee, @@ -168,12 +170,15 @@ pub enum BeaconStateError { TotalActiveBalanceDiffUninitialized, GeneralizedIndexNotSupported(usize), IndexNotSupported(usize), + BuilderPendingPaymentsIndexNotSupported(usize), InvalidFlagIndex(usize), MerkleTreeError(merkle_proof::MerkleTreeError), PartialWithdrawalCountInvalid(usize), NonExecutionAddressWithdrawalCredential, NoCommitteeFound(CommitteeIndex), InvalidCommitteeIndex(CommitteeIndex), + /// `Attestation.data.index` field is invalid in overloaded data index scenario. + BadOverloadedDataIndex(u64), InvalidSelectionProof { aggregator_index: u64, }, @@ -195,6 +200,9 @@ pub enum BeaconStateError { ProposerLookaheadOutOfBounds { i: usize, }, + InvalidIndicesCount, + PleaseNotifyTheDevs(String), + InvalidExecutionPayloadAvailabilityIndex(usize), } /// Control whether an epoch-indexed field can be indexed at the next epoch or not. @@ -602,8 +610,17 @@ where #[superstruct(only(Fulu, Gloas))] #[serde(with = "ssz_types::serde_utils::quoted_u64_fixed_vec")] pub proposer_lookahead: Vector, - // Gloas + #[compare_fields(as_iter)] + #[test_random(default)] + #[superstruct(only(Gloas))] + pub builders: List, + + #[metastruct(exclude_from(tree_lists))] + #[serde(with = "serde_utils::quoted_u64")] + #[superstruct(only(Gloas), partial_getter(copy))] + pub next_withdrawal_builder_index: BuilderIndex, + #[test_random(default)] #[superstruct(only(Gloas))] #[metastruct(exclude_from(tree_lists))] @@ -625,10 +642,10 @@ where #[metastruct(exclude_from(tree_lists))] pub latest_block_hash: ExecutionBlockHash, + #[compare_fields(as_iter)] #[test_random(default)] #[superstruct(only(Gloas))] - #[metastruct(exclude_from(tree_lists))] - pub latest_withdrawals_root: Hash256, + pub payload_expected_withdrawals: List, // Caching (not in the spec) #[serde(skip_serializing, skip_deserializing)] @@ -1115,13 +1132,22 @@ impl BeaconState { } } + let gloas_enabled = self.fork_name_unchecked().gloas_enabled(); epoch .slot_iter(E::slots_per_epoch()) .map(|slot| { let mut preimage = seed.to_vec(); preimage.append(&mut int_to_bytes8(slot.as_u64())); let seed = hash(&preimage); - self.compute_proposer_index(indices, &seed, spec) + + if gloas_enabled { + self.compute_balance_weighted_selection(indices, &seed, 1, true, spec)? + .first() + .copied() + .ok_or(BeaconStateError::InsufficientValidators) + } else { + self.compute_proposer_index(indices, &seed, spec) + } }) .collect() } @@ -1378,39 +1404,50 @@ impl BeaconState { let epoch = self.current_epoch().safe_add(1)?; let active_validator_indices = self.get_active_validator_indices(epoch, spec)?; - let active_validator_count = active_validator_indices.len(); - let seed = self.get_seed(epoch, Domain::SyncCommittee, spec)?; - let max_effective_balance = spec.max_effective_balance_for_fork(self.fork_name_unchecked()); - let max_random_value = if self.fork_name_unchecked().electra_enabled() { - MAX_RANDOM_VALUE - } else { - MAX_RANDOM_BYTE - }; - let mut i = 0; - let mut sync_committee_indices = Vec::with_capacity(E::SyncCommitteeSize::to_usize()); - while sync_committee_indices.len() < E::SyncCommitteeSize::to_usize() { - let shuffled_index = compute_shuffled_index( - i.safe_rem(active_validator_count)?, - active_validator_count, + if self.fork_name_unchecked().gloas_enabled() { + self.compute_balance_weighted_selection( + &active_validator_indices, seed.as_slice(), - spec.shuffle_round_count, + E::SyncCommitteeSize::to_usize(), + true, + spec, ) - .ok_or(BeaconStateError::UnableToShuffle)?; - let candidate_index = *active_validator_indices - .get(shuffled_index) - .ok_or(BeaconStateError::ShuffleIndexOutOfBounds(shuffled_index))?; - let random_value = self.shuffling_random_value(i, seed.as_slice())?; - let effective_balance = self.get_validator(candidate_index)?.effective_balance; - if effective_balance.safe_mul(max_random_value)? - >= max_effective_balance.safe_mul(random_value)? - { - sync_committee_indices.push(candidate_index); + } else { + let active_validator_count = active_validator_indices.len(); + let max_effective_balance = + spec.max_effective_balance_for_fork(self.fork_name_unchecked()); + let max_random_value = if self.fork_name_unchecked().electra_enabled() { + MAX_RANDOM_VALUE + } else { + MAX_RANDOM_BYTE + }; + + let mut i = 0; + let mut sync_committee_indices = Vec::with_capacity(E::SyncCommitteeSize::to_usize()); + while sync_committee_indices.len() < E::SyncCommitteeSize::to_usize() { + let shuffled_index = compute_shuffled_index( + i.safe_rem(active_validator_count)?, + active_validator_count, + seed.as_slice(), + spec.shuffle_round_count, + ) + .ok_or(BeaconStateError::UnableToShuffle)?; + let candidate_index = *active_validator_indices + .get(shuffled_index) + .ok_or(BeaconStateError::ShuffleIndexOutOfBounds(shuffled_index))?; + let random_value = self.shuffling_random_value(i, seed.as_slice())?; + let effective_balance = self.get_validator(candidate_index)?.effective_balance; + if effective_balance.safe_mul(max_random_value)? + >= max_effective_balance.safe_mul(random_value)? + { + sync_committee_indices.push(candidate_index); + } + i.safe_add_assign(1)?; } - i.safe_add_assign(1)?; + Ok(sync_committee_indices) } - Ok(sync_committee_indices) } /// Compute the next sync committee. @@ -2032,6 +2069,25 @@ impl BeaconState { Ok(cache.get_attestation_duties(validator_index)) } + /// Check if the attestation is for the block proposed at the attestation slot. + /// + /// Returns `true` if the attestation's block root matches the block root at the + /// attestation's slot, and the block root differs from the previous slot's root. + pub fn is_attestation_same_slot( + &self, + data: &AttestationData, + ) -> Result { + if data.slot == 0 { + return Ok(true); + } + + let blockroot = data.beacon_block_root; + let slot_blockroot = *self.get_block_root(data.slot)?; + let prev_blockroot = *self.get_block_root(data.slot.safe_sub(1)?)?; + + Ok(blockroot == slot_blockroot && blockroot != prev_blockroot) + } + /// Compute the total active balance cache from scratch. /// /// This method should rarely be invoked because single-pass epoch processing keeps the total @@ -2292,6 +2348,7 @@ impl BeaconState { } } + /// Return true if the parent block was full (both beacon block and execution payload were present). pub fn is_parent_block_full(&self) -> bool { match self { BeaconState::Base(_) | BeaconState::Altair(_) => false, @@ -2594,11 +2651,16 @@ impl BeaconState { .map_err(Into::into) } + // TODO(EIP-7732): The consensus spec PR for this change mentions that some EF tests will be needed but haven't been created yet. + // We should integrate them once they are available. + // https://github.com/ethereum/consensus-specs/pull/4513 pub fn get_pending_balance_to_withdraw( &self, validator_index: usize, ) -> Result { let mut pending_balance = 0; + + // Sum pending partial withdrawals for withdrawal in self .pending_partial_withdrawals()? .iter() @@ -2606,6 +2668,27 @@ impl BeaconState { { pending_balance.safe_add_assign(withdrawal.amount)?; } + + // Sum builder pending withdrawals + if let Ok(builder_pending_withdrawals) = self.builder_pending_withdrawals() { + for withdrawal in builder_pending_withdrawals + .iter() + .filter(|withdrawal| withdrawal.builder_index as usize == validator_index) + { + pending_balance.safe_add_assign(withdrawal.amount)?; + } + } + + // Sum builder pending payments + if let Ok(builder_pending_payments) = self.builder_pending_payments() { + for payment in builder_pending_payments + .iter() + .filter(|payment| payment.withdrawal.builder_index as usize == validator_index) + { + pending_balance.safe_add_assign(payment.withdrawal.amount)?; + } + } + Ok(pending_balance) } @@ -2878,6 +2961,120 @@ impl BeaconState { Ok(()) } + + /// Get the PTC + /// Requires the committee cache to be initialized. + /// TODO(EIP-7732): definitely gonna have to cache this.. + pub fn get_ptc(&self, slot: Slot, spec: &ChainSpec) -> Result, BeaconStateError> { + let committee_cache = self.committee_cache_at_slot(slot)?; + let committees = committee_cache.get_beacon_committees_at_slot(slot)?; + + let seed = self.get_ptc_attester_seed(slot, spec)?; + + let committee_indices: Vec = committees + .iter() + .flat_map(|committee| committee.committee.iter().copied()) + .collect(); + let selected_indices = self.compute_balance_weighted_selection( + &committee_indices, + &seed, + E::ptc_size(), + false, + spec, + )?; + + Ok(PTC(FixedVector::new(selected_indices)?)) + } + + /// Compute the seed to use for the ptc attester selection at the given `slot`. + /// + /// Spec v0.12.1 + pub fn get_ptc_attester_seed( + &self, + slot: Slot, + spec: &ChainSpec, + ) -> Result, BeaconStateError> { + let epoch = slot.epoch(E::slots_per_epoch()); + let mut preimage = self + .get_seed(epoch, Domain::PTCAttester, spec)? + .as_slice() + .to_vec(); + preimage.append(&mut int_to_bytes8(slot.as_u64())); + Ok(hash(&preimage)) + } + + /// Return size indices sampled by effective balance, using indices as candidates. + /// + /// If shuffle_indices is True, candidate indices are themselves sampled from indices + /// by shuffling it, otherwise indices is traversed in order. + fn compute_balance_weighted_selection( + &self, + indices: &[usize], + seed: &[u8], + size: usize, + shuffle_indices: bool, + spec: &ChainSpec, + ) -> Result, BeaconStateError> { + let total = indices.len(); + if total == 0 { + return Err(BeaconStateError::InvalidIndicesCount); + } + + let mut selected = Vec::with_capacity(size); + let mut count = 0usize; + + while selected.len() < size { + let mut next_index = count.safe_rem(total)?; + + if shuffle_indices { + next_index = + compute_shuffled_index(next_index, total, seed, spec.shuffle_round_count) + .ok_or(BeaconStateError::UnableToShuffle)?; + } + + let candidate_index = indices + .get(next_index) + .ok_or(BeaconStateError::InvalidIndicesCount)?; + + if self.compute_balance_weighted_acceptance(*candidate_index, seed, count, spec)? { + selected.push(*candidate_index); + } + + count.safe_add_assign(1)?; + } + + Ok(selected) + } + + /// Return whether to accept the selection of the validator `index`, with probability + /// proportional to its `effective_balance`, and randomness given by `seed` and `iteration`. + fn compute_balance_weighted_acceptance( + &self, + index: usize, + seed: &[u8], + iteration: usize, + spec: &ChainSpec, + ) -> Result { + // TODO(EIP-7732): Consider grabbing effective balances from the epoch cache here. + // Note that this function will be used in a loop, so using cached values could be nice for performance. + // However, post-gloas, this function will be used in `compute_proposer_indices`, `get_next_sync_committee_indices`, and `get_ptc`, which has ~15 call sites in total + // so we will need to check each one to ensure epoch cache is initialized first, if we deem a good idea. + // Currently, we can't test if making the change would work since the test suite is not ready for gloas. + let effective_balance = self.get_effective_balance(index)?; + let max_effective_balance = spec.max_effective_balance_for_fork(self.fork_name_unchecked()); + + let random_value = self.shuffling_random_value(iteration, seed)?; + + // this codepath should technically never be hit pre-gloas, but added this defensively + let max_random_value = if self.fork_name_unchecked().electra_enabled() { + MAX_RANDOM_VALUE + } else { + MAX_RANDOM_BYTE + }; + + Ok(effective_balance.safe_mul(max_random_value)? + >= max_effective_balance.safe_mul(random_value)?) + } } impl ForkVersionDecode for BeaconState { diff --git a/consensus/types/src/validator/validator.rs b/consensus/types/src/validator/validator.rs index 7898ab9073a..ce8ea4def8a 100644 --- a/consensus/types/src/validator/validator.rs +++ b/consensus/types/src/validator/validator.rs @@ -165,13 +165,41 @@ impl Validator { } /// Check if ``validator`` has an 0x02 prefixed "compounding" withdrawal credential. - pub fn has_compounding_withdrawal_credential(&self, spec: &ChainSpec) -> bool { + pub fn has_compounding_withdrawal_credential( + &self, + spec: &ChainSpec, + current_fork: ForkName, + ) -> bool { + if current_fork.gloas_enabled() { + self.has_compounding_withdrawal_credential_gloas(spec) + } else { + self.has_compounding_withdrawal_credential_electra(spec) + } + } + + /// Check if ``validator`` has an 0x02 prefixed "compounding" withdrawal credential + pub fn has_compounding_withdrawal_credential_electra(&self, spec: &ChainSpec) -> bool { + is_compounding_withdrawal_credential(self.withdrawal_credentials, spec) + } + + /// Check if ``validator`` has an 0x02 prefixed "compounding" withdrawal credential or an 0x03 prefixed "builder" withdrawal credential + pub fn has_compounding_withdrawal_credential_gloas(&self, spec: &ChainSpec) -> bool { is_compounding_withdrawal_credential(self.withdrawal_credentials, spec) + || is_builder_withdrawal_credential(self.withdrawal_credentials, spec) + } + + /// Check if ``validator`` has an 0x03 prefixed "builder" withdrawal credential. + pub fn has_builder_withdrawal_credential(&self, spec: &ChainSpec) -> bool { + is_builder_withdrawal_credential(self.withdrawal_credentials, spec) } /// Get the execution withdrawal address if this validator has one initialized. - pub fn get_execution_withdrawal_address(&self, spec: &ChainSpec) -> Option
{ - self.has_execution_withdrawal_credential(spec) + pub fn get_execution_withdrawal_address( + &self, + spec: &ChainSpec, + current_fork: ForkName, + ) -> Option
{ + self.has_execution_withdrawal_credential(spec, current_fork) .then(|| { self.withdrawal_credentials .as_slice() @@ -202,7 +230,7 @@ impl Validator { current_fork: ForkName, ) -> bool { if current_fork.electra_enabled() { - self.is_fully_withdrawable_validator_electra(balance, epoch, spec) + self.is_fully_withdrawable_validator_electra(balance, epoch, spec, current_fork) } else { self.is_fully_withdrawable_validator_capella(balance, epoch, spec) } @@ -226,8 +254,9 @@ impl Validator { balance: u64, epoch: Epoch, spec: &ChainSpec, + current_fork: ForkName, ) -> bool { - self.has_execution_withdrawal_credential(spec) + self.has_execution_withdrawal_credential(spec, current_fork) && self.withdrawable_epoch <= epoch && balance > 0 } @@ -267,21 +296,25 @@ impl Validator { let max_effective_balance = self.get_max_effective_balance(spec, current_fork); let has_max_effective_balance = self.effective_balance == max_effective_balance; let has_excess_balance = balance > max_effective_balance; - self.has_execution_withdrawal_credential(spec) + self.has_execution_withdrawal_credential(spec, current_fork) && has_max_effective_balance && has_excess_balance } /// Returns `true` if the validator has a 0x01 or 0x02 prefixed withdrawal credential. - pub fn has_execution_withdrawal_credential(&self, spec: &ChainSpec) -> bool { - self.has_compounding_withdrawal_credential(spec) + pub fn has_execution_withdrawal_credential( + &self, + spec: &ChainSpec, + current_fork: ForkName, + ) -> bool { + self.has_compounding_withdrawal_credential(spec, current_fork) || self.has_eth1_withdrawal_credential(spec) } /// Returns the max effective balance for a validator in gwei. pub fn get_max_effective_balance(&self, spec: &ChainSpec, current_fork: ForkName) -> u64 { if current_fork >= ForkName::Electra { - if self.has_compounding_withdrawal_credential(spec) { + if self.has_compounding_withdrawal_credential(spec, current_fork) { spec.max_effective_balance_electra } else { spec.min_activation_balance @@ -319,6 +352,15 @@ pub fn is_compounding_withdrawal_credential( .unwrap_or(false) } +/// Check if the withdrawal credential has the builder withdrawal prefix (0x03). +pub fn is_builder_withdrawal_credential(withdrawal_credentials: Hash256, spec: &ChainSpec) -> bool { + withdrawal_credentials + .as_slice() + .first() + .map(|prefix_byte| *prefix_byte == spec.builder_withdrawal_prefix_byte) + .unwrap_or(false) +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/tests/state.rs b/consensus/types/tests/state.rs index 63ab3b8084b..54abf73f0fb 100644 --- a/consensus/types/tests/state.rs +++ b/consensus/types/tests/state.rs @@ -55,6 +55,15 @@ async fn build_state(validator_count: usize) -> BeaconState { .head_beacon_state_cloned() } +// TODO(EIP-7732): Add tests for PTC (Payload Timeliness Committee) functions: +// - get_ptc: Test committee selection, size, balance-weighted selection +// - get_ptc_attester_seed: Test seed generation and determinism +// - compute_balance_weighted_selection: Test selection algorithm with various balances +// - compute_balance_weighted_acceptance: Test acceptance probability +// These tests require being able to build Gloas states with initialized committee caches, +// which currently fails due to incomplete Gloas block structure as mentioned here: +// https://github.com/sigp/lighthouse/pull/8273 +// Similar to existing committee_consistency_test suite for get_beacon_committee. async fn test_beacon_proposer_index() { let spec = E::default_spec(); diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 0ead9d00472..0c6371f8253 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,6 +1,6 @@ # To download/extract nightly tests, run: # CONSENSUS_SPECS_TEST_VERSION=nightly make -CONSENSUS_SPECS_TEST_VERSION ?= v1.6.0-beta.1 +CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.1 REPO_NAME := consensus-spec-tests OUTPUT_DIR := ./$(REPO_NAME) diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 1f70881a887..947e2f385c1 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -47,6 +47,19 @@ "bls12-381-tests/hash_to_G2", "tests/.*/eip7732", "tests/.*/eip7805", + # TODO(EIP-7732): Gloas execution_payload_bid tests require process_execution_payload_bid + # which is not yet implemented. + "tests/.*/gloas/operations/execution_payload_bid/.*", + # TODO(EIP-7732): Gloas deposit_request tests require builder deposit functionality + # (apply_deposit_for_builder, add_builder_to_registry) which is not yet implemented. + "tests/.*/gloas/operations/deposit_request/.*", + # TODO(EIP-7732): Gloas sanity, transition, random, finality, and fork_choice tests require + # full block processing which is not yet complete. + "tests/.*/gloas/sanity/.*", + "tests/.*/gloas/transition/.*", + "tests/.*/gloas/random/.*", + "tests/.*/gloas/finality/.*", + "tests/.*/gloas/fork_choice/.*", # Ignore MatrixEntry SSZ tests for now. "tests/.*/fulu/ssz_static/MatrixEntry/.*", # EIP-7916 is still in draft and hasn't been implemented yet https://eips.ethereum.org/EIPS/eip-7916 @@ -59,8 +72,6 @@ # Ignore full epoch tests for now (just test the sub-transitions). "tests/.*/.*/epoch_processing/.*/pre_epoch.ssz_snappy", "tests/.*/.*/epoch_processing/.*/post_epoch.ssz_snappy", - # Ignore gloas tests for now - "tests/.*/gloas/.*", # Ignore KZG tests that target internal kzg library functions "tests/.*/compute_verify_cell_kzg_proof_batch_challenge/.*", "tests/.*/compute_challenge/.*", diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 8e9d438a243..6096b3ccc4a 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -296,6 +296,11 @@ impl Case for ForkChoiceTest { self.description.clone() } + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + // Gloas fork choice not yet implemented + fork_name != ForkName::Gloas + } + fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { let tester = Tester::new(self, testing_spec::(fork_name))?; diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index a53bce927cb..914ff8dc4e5 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -7,7 +7,8 @@ use ssz::Decode; use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::per_block_processing::process_operations::{ - process_consolidation_requests, process_deposit_requests, process_withdrawal_requests, + altair_deneb, base, gloas, process_consolidation_requests, process_deposit_requests, + process_payload_attestation, process_withdrawal_requests, }; use state_processing::{ ConsensusContext, @@ -16,8 +17,8 @@ use state_processing::{ errors::BlockProcessingError, process_block_header, process_execution_payload, process_operations::{ - altair_deneb, base, process_attester_slashings, process_bls_to_execution_changes, - process_deposits, process_exits, process_proposer_slashings, + process_attester_slashings, process_bls_to_execution_changes, process_deposits, + process_exits, process_proposer_slashings, }, process_sync_aggregate, process_withdrawals, }, @@ -27,8 +28,8 @@ use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconBlockBody, BeaconBlockBodyBellatrix, BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconBlockBodyFulu, BeaconState, BlindedPayload, ConsolidationRequest, Deposit, DepositRequest, ExecutionPayload, - ForkVersionDecode, FullPayload, ProposerSlashing, SignedBlsToExecutionChange, - SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, + ForkVersionDecode, FullPayload, PayloadAttestation, ProposerSlashing, + SignedBlsToExecutionChange, SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, }; #[derive(Debug, Clone, Default, Deserialize)] @@ -45,7 +46,7 @@ struct ExecutionMetadata { /// Newtype for testing withdrawals. #[derive(Debug, Clone, Deserialize)] pub struct WithdrawalsPayload { - payload: FullPayload, + payload: ExecutionPayload, } #[derive(Debug, Clone)] @@ -99,7 +100,17 @@ impl Operation for Attestation { ) -> Result<(), BlockProcessingError> { initialize_epoch_cache(state, spec)?; let mut ctxt = ConsensusContext::new(state.slot()); - if state.fork_name_unchecked().altair_enabled() { + if state.fork_name_unchecked().gloas_enabled() { + initialize_progressive_balances_cache(state, spec)?; + gloas::process_attestation( + state, + self.to_ref(), + 0, + &mut ctxt, + VerifySignatures::True, + spec, + ) + } else if state.fork_name_unchecked().altair_enabled() { initialize_progressive_balances_cache(state, spec)?; altair_deneb::process_attestation( state, @@ -408,9 +419,7 @@ impl Operation for WithdrawalsPayload { ssz_decode_file_with(path, |bytes| { ExecutionPayload::from_ssz_bytes_by_fork(bytes, fork_name) }) - .map(|payload| WithdrawalsPayload { - payload: payload.into(), - }) + .map(|payload| WithdrawalsPayload { payload }) } fn apply_to( @@ -419,8 +428,16 @@ impl Operation for WithdrawalsPayload { spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { - // TODO(EIP-7732): implement separate gloas and non-gloas variants of process_withdrawals - process_withdrawals::<_, FullPayload<_>>(state, self.payload.to_ref(), spec) + if state.fork_name_unchecked().gloas_enabled() { + process_withdrawals::gloas::process_withdrawals(state, spec) + } else { + let full_payload = FullPayload::from(self.payload.clone()); + process_withdrawals::capella::process_withdrawals::<_, FullPayload<_>>( + state, + full_payload.to_ref(), + spec, + ) + } } } @@ -486,7 +503,10 @@ impl Operation for DepositRequest { } fn is_enabled_for_fork(fork_name: ForkName) -> bool { - fork_name.electra_enabled() + // TODO(EIP-7732): Gloas deposit_request tests require builder deposit functionality + // (apply_deposit_for_builder, add_builder_to_registry) which is not yet implemented. + // https://github.com/sigp/lighthouse/issues/XXXX + fork_name.electra_enabled() && fork_name != ForkName::Gloas } fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { @@ -527,6 +547,32 @@ impl Operation for ConsolidationRequest { } } +impl Operation for PayloadAttestation { + fn handler_name() -> String { + "payload_attestation".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(path) + } + + fn apply_to( + &self, + state: &mut BeaconState, + spec: &ChainSpec, + _extra: &Operations, + ) -> Result<(), BlockProcessingError> { + initialize_epoch_cache(state, spec)?; + initialize_progressive_balances_cache(state, spec)?; + let mut ctxt = ConsensusContext::new(state.slot()); + process_payload_attestation(state, self, 0, VerifySignatures::True, &mut ctxt, spec) + } +} + impl> LoadCase for Operations { fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { let spec = &testing_spec::(fork_name); diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index a5b2ffada37..b3a2759b3b4 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -22,7 +22,7 @@ pub trait Handler { // Add forks here to exclude them from EF spec testing. Helpful for adding future or // unspecified forks. fn disabled_forks(&self) -> Vec { - vec![ForkName::Gloas] + vec![] } fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { @@ -305,6 +305,10 @@ impl SszStaticHandler { Self::for_forks(vec![ForkName::Fulu]) } + pub fn gloas_only() -> Self { + Self::for_forks(vec![ForkName::Gloas]) + } + pub fn altair_and_later() -> Self { Self::for_forks(ForkName::list_all()[1..].to_vec()) } @@ -329,6 +333,10 @@ impl SszStaticHandler { Self::for_forks(ForkName::list_all()[6..].to_vec()) } + pub fn gloas_and_later() -> Self { + Self::for_forks(ForkName::list_all()[7..].to_vec()) + } + pub fn pre_electra() -> Self { Self::for_forks(ForkName::list_all()[0..5].to_vec()) } @@ -362,6 +370,10 @@ impl SszStaticWithSpecHandler { pub fn fulu_and_later() -> Self { Self::for_forks(ForkName::list_all()[6..].to_vec()) } + + pub fn gloas_and_later() -> Self { + Self::for_forks(ForkName::list_all()[7..].to_vec()) + } } impl Handler for SszStaticHandler @@ -479,10 +491,11 @@ impl Handler for SanityBlocksHandler { "blocks".into() } - fn is_enabled_for_fork(&self, _fork_name: ForkName) -> bool { + fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { // NOTE: v1.1.0-beta.4 doesn't mark the historical blocks test as requiring real crypto, so // only run these tests with real crypto for now. - cfg!(not(feature = "fake_crypto")) + // TODO(EIP-7732): Gloas sanity tests require full block processing which is not yet complete + fork_name != ForkName::Gloas && cfg!(not(feature = "fake_crypto")) } } @@ -507,7 +520,9 @@ impl Handler for SanitySlotsHandler { fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { // Some sanity tests compute sync committees, which requires real crypto. - fork_name == ForkName::Base || cfg!(not(feature = "fake_crypto")) + // TODO(EIP-7732): Gloas sanity tests require full block processing which is not yet complete + fork_name != ForkName::Gloas + && (fork_name == ForkName::Base || cfg!(not(feature = "fake_crypto"))) } } @@ -529,6 +544,11 @@ impl Handler for RandomHandler { fn handler_name(&self) -> String { "random".into() } + + fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { + // TODO(EIP-7732): Gloas random tests require full block processing which is not yet complete + fork_name != ForkName::Gloas + } } #[derive(Educe)] @@ -619,6 +639,11 @@ impl Handler for TransitionHandler { fn handler_name(&self) -> String { "core".into() } + + fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { + // TODO(EIP-7732): Gloas transition tests require full block processing which is not yet complete + fork_name != ForkName::Gloas + } } #[derive(Educe)] @@ -640,6 +665,11 @@ impl Handler for FinalityHandler { fn handler_name(&self) -> String { "finality".into() } + + fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { + // TODO(EIP-7732): Gloas finality tests require full block processing which is not yet complete + fork_name != ForkName::Gloas + } } pub struct ForkChoiceHandler { @@ -687,6 +717,12 @@ impl Handler for ForkChoiceHandler { return false; } + // TODO(EIP-7732): Gloas fork choice not yet implemented + // https://github.com/sigp/lighthouse/issues/XXXX + if fork_name == ForkName::Gloas { + return false; + } + // No FCU override tests prior to bellatrix. if self.handler_name == "should_override_forkchoice_update" && !fork_name.bellatrix_enabled() diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index 6dd1df1d8d1..0bfde6a4e7d 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -55,6 +55,7 @@ type_name_generic!(BeaconBlockBodyCapella, "BeaconBlockBody"); type_name_generic!(BeaconBlockBodyDeneb, "BeaconBlockBody"); type_name_generic!(BeaconBlockBodyElectra, "BeaconBlockBody"); type_name_generic!(BeaconBlockBodyFulu, "BeaconBlockBody"); +type_name_generic!(BeaconBlockBodyGloas, "BeaconBlockBody"); type_name!(BeaconBlockHeader); type_name_generic!(BeaconState); type_name!(BlobIdentifier); @@ -69,6 +70,9 @@ type_name!(DepositData); type_name!(DepositMessage); type_name!(DepositRequest); type_name!(Eth1Data); +type_name!(Builder); +type_name!(BuilderPendingPayment); +type_name!(BuilderPendingWithdrawal); type_name!(WithdrawalRequest); type_name_generic!(ExecutionPayload); type_name_generic!(ExecutionPayloadBellatrix, "ExecutionPayload"); @@ -84,6 +88,10 @@ type_name_generic!(ExecutionPayloadHeaderDeneb, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderElectra, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderFulu, "ExecutionPayloadHeader"); type_name_generic!(ExecutionRequests); +type_name!(ExecutionPayloadBid); +type_name!(SignedExecutionPayloadBid); +type_name_generic!(ExecutionPayloadEnvelope); +type_name_generic!(SignedExecutionPayloadEnvelope); type_name_generic!(BlindedPayload, "ExecutionPayloadHeader"); type_name!(Fork); type_name!(ForkData); @@ -91,6 +99,7 @@ type_name_generic!(HistoricalBatch); type_name_generic!(IndexedAttestation); type_name_generic!(IndexedAttestationBase, "IndexedAttestation"); type_name_generic!(IndexedAttestationElectra, "IndexedAttestation"); +type_name_generic!(IndexedPayloadAttestation); type_name_generic!(LightClientBootstrap); type_name_generic!(LightClientBootstrapAltair, "LightClientBootstrap"); type_name_generic!(LightClientBootstrapCapella, "LightClientBootstrap"); @@ -143,6 +152,9 @@ type_name_generic!(LightClientUpdateDeneb, "LightClientUpdate"); type_name_generic!(LightClientUpdateElectra, "LightClientUpdate"); type_name_generic!(LightClientUpdateFulu, "LightClientUpdate"); type_name_generic!(PendingAttestation); +type_name_generic!(PayloadAttestation); +type_name!(PayloadAttestationData); +type_name!(PayloadAttestationMessage); type_name!(PendingConsolidation); type_name!(PendingPartialWithdrawal); type_name!(PendingDeposit); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index b2f65db6f3e..3f104dff8d6 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -118,6 +118,12 @@ fn operations_bls_to_execution_change() { OperationsHandler::::default().run(); } +#[test] +fn operations_payload_attestation() { + OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); +} + #[test] fn sanity_blocks() { SanityBlocksHandler::::default().run(); @@ -241,8 +247,12 @@ mod ssz_static { use ef_tests::{Handler, SszStaticHandler, SszStaticTHCHandler, SszStaticWithSpecHandler}; use types::state::HistoricalSummary; use types::{ - AttesterSlashingBase, AttesterSlashingElectra, ConsolidationRequest, DepositRequest, - LightClientBootstrapAltair, PendingDeposit, PendingPartialWithdrawal, WithdrawalRequest, *, + AttesterSlashingBase, AttesterSlashingElectra, Builder, BuilderPendingPayment, + BuilderPendingWithdrawal, ConsolidationRequest, DepositRequest, ExecutionPayloadBid, + ExecutionPayloadEnvelope, IndexedPayloadAttestation, LightClientBootstrapAltair, + PayloadAttestation, PayloadAttestationData, PayloadAttestationMessage, PendingDeposit, + PendingPartialWithdrawal, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, + WithdrawalRequest, *, }; ssz_static_test!(attestation_data, AttestationData); @@ -368,6 +378,10 @@ mod ssz_static { .run(); SszStaticHandler::, MinimalEthSpec>::fulu_only().run(); SszStaticHandler::, MainnetEthSpec>::fulu_only().run(); + SszStaticHandler::, MinimalEthSpec>::gloas_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::gloas_only() + .run(); } // Altair and later @@ -722,6 +736,81 @@ mod ssz_static { SszStaticHandler::, MinimalEthSpec>::electra_and_later() .run(); } + + // Gloas and later + #[test] + fn builder() { + SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); + } + + #[test] + fn builder_pending_payment() { + SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); + } + + #[test] + fn builder_pending_withdrawal() { + SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); + } + + #[test] + fn payload_attestation_data() { + SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); + } + + #[test] + fn payload_attestation() { + SszStaticHandler::, MinimalEthSpec>::gloas_and_later() + .run(); + SszStaticHandler::, MainnetEthSpec>::gloas_and_later() + .run(); + } + + #[test] + fn payload_attestation_message() { + SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); + } + + #[test] + fn indexed_payload_attestation() { + SszStaticHandler::, MinimalEthSpec>::gloas_and_later() + .run(); + SszStaticHandler::, MainnetEthSpec>::gloas_and_later() + .run(); + } + + #[test] + fn execution_payload_bid() { + SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); + } + + #[test] + fn signed_execution_payload_bid() { + SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); + } + + #[test] + fn execution_payload_envelope() { + SszStaticHandler::, MinimalEthSpec>::gloas_and_later() + .run(); + SszStaticHandler::, MainnetEthSpec>::gloas_and_later() + .run(); + } + + #[test] + fn signed_execution_payload_envelope() { + SszStaticHandler::, MinimalEthSpec>::gloas_and_later() + .run(); + SszStaticHandler::, MainnetEthSpec>::gloas_and_later() + .run(); + } } #[test]