diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 80f3041d30a..065203226d5 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -108,13 +108,12 @@ jobs: runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }} steps: - uses: actions/checkout@v5 - # Set Java version to 21. (required since Web3Signer 24.12.0). - # On sigp/lighthouse, Java 21 is baked into the snapshot. - - if: github.repository != 'sigp/lighthouse' - uses: actions/setup-java@v4 + # Set Java version to 25. (required since Web3Signer 26.7.0). + # Always install; do not rely on the WarpBuild snapshot containing Java 25. + - uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '21' + java-version: '25' - if: github.repository != 'sigp/lighthouse' name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 @@ -310,8 +309,30 @@ jobs: cache-target: release - name: Run state_transition_vectors in release. run: make run-state-transition-tests - ef-tests-ubuntu: - name: ef-tests-ubuntu + ef-tests-mainnet-ubuntu: + name: ef-tests-mainnet-ubuntu + needs: [check-labels] + if: needs.check-labels.outputs.skip_ci != 'true' + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v5 + - if: github.repository != 'sigp/lighthouse' + name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-nextest + - if: github.repository == 'sigp/lighthouse' + uses: Swatinem/rust-cache@v2 + with: + cache-provider: warpbuild + - name: Run mainnet consensus-spec-tests with blst and fake_crypto + run: make test-ef-mainnet + ef-tests-minimal-ubuntu: + name: ef-tests-minimal-ubuntu needs: [check-labels] if: needs.check-labels.outputs.skip_ci != 'true' runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }} @@ -330,8 +351,8 @@ jobs: uses: Swatinem/rust-cache@v2 with: cache-provider: warpbuild - - name: Run consensus-spec-tests with blst and fake_crypto - run: make test-ef + - name: Run minimal consensus-spec-tests with blst and fake_crypto + run: make test-ef-minimal basic-simulator-ubuntu: name: basic-simulator-ubuntu needs: [check-labels] @@ -549,7 +570,8 @@ jobs: 'http-api-tests', 'debug-tests-ubuntu', 'state-transition-vectors-ubuntu', - 'ef-tests-ubuntu', + 'ef-tests-mainnet-ubuntu', + 'ef-tests-minimal-ubuntu', 'basic-simulator-ubuntu', 'fallback-simulator-ubuntu', 'execution-engine-integration-ubuntu', diff --git a/.github/workflows/warpbuild-ubuntu-latest-snapshot.yml b/.github/workflows/warpbuild-ubuntu-latest-snapshot.yml index 3cbd7377480..46cd26750fd 100644 --- a/.github/workflows/warpbuild-ubuntu-latest-snapshot.yml +++ b/.github/workflows/warpbuild-ubuntu-latest-snapshot.yml @@ -52,11 +52,11 @@ jobs: cargo install --locked cargo-sort cargo install --locked cargo-hack - - name: Install Java (Temurin 21) + - name: Install Java (Temurin 25) uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '21' + java-version: '25' - name: Save snapshot uses: WarpBuilds/snapshot-save@v1 diff --git a/Cargo.lock b/Cargo.lock index 2d08b579c52..6e17b5d6615 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8475,6 +8475,7 @@ dependencies = [ "parking_lot", "reqwest", "serde", + "serde_json", "task_executor", "tracing", "types", diff --git a/Makefile b/Makefile index 085e06ea2bd..34b46efe391 100644 --- a/Makefile +++ b/Makefile @@ -202,10 +202,22 @@ check-benches: # Runs EF test vectors run-ef-tests: + $(MAKE) run-ef-tests-mainnet + $(MAKE) run-ef-tests-minimal + +# Runs the mainnet EF test vectors +run-ef-tests-mainnet: rm -rf $(EF_TESTS)/.accessed_file_log.txt cargo nextest run --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES)" cargo nextest run --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES),fake_crypto" - ./$(EF_TESTS)/check_all_files_accessed.py $(EF_TESTS)/.accessed_file_log.txt $(EF_TESTS)/consensus-spec-tests + ./$(EF_TESTS)/check_all_files_accessed.py $(EF_TESTS)/.accessed_file_log.txt $(EF_TESTS)/consensus-spec-tests "tests/minimal/" + +# Runs the minimal EF test vectors +run-ef-tests-minimal: + rm -rf $(EF_TESTS)/.accessed_file_log.txt + cargo nextest run --release -p ef_tests --features "ef_tests,spec-minimal,$(EF_TEST_FEATURES)" + cargo nextest run --release -p ef_tests --features "ef_tests,spec-minimal,$(EF_TEST_FEATURES),fake_crypto" + ./$(EF_TESTS)/check_all_files_accessed.py $(EF_TESTS)/.accessed_file_log.txt $(EF_TESTS)/consensus-spec-tests "tests/mainnet/" # Run the tests in the `beacon_chain` crate for all known forks. test-beacon-chain: $(patsubst %,test-beacon-chain-%,$(RECENT_FORKS)) @@ -259,6 +271,12 @@ run-state-transition-tests: # Downloads and runs the EF test vectors. test-ef: make-ef-tests run-ef-tests +# Downloads and runs the mainnet EF test vectors. +test-ef-mainnet: make-ef-tests run-ef-tests-mainnet + +# Downloads and runs the minimal EF test vectors. +test-ef-minimal: make-ef-tests run-ef-tests-minimal + # Downloads and runs the nightly EF test vectors. test-ef-nightly: make-ef-tests-nightly run-ef-tests diff --git a/beacon_node/beacon_chain/src/beacon_block_reward.rs b/beacon_node/beacon_chain/src/beacon_block_reward.rs index fdd145b4234..4c48a8a0291 100644 --- a/beacon_node/beacon_chain/src/beacon_block_reward.rs +++ b/beacon_node/beacon_chain/src/beacon_block_reward.rs @@ -45,11 +45,11 @@ impl BeaconChain { let bid_parent_block_hash = block .body() .signed_execution_payload_bid()? - .message - .parent_block_hash; - if bid_parent_block_hash == parent_bid.block_hash { + .message() + .parent_block_hash(); + if bid_parent_block_hash == parent_bid.block_hash() { let availability_index = parent_bid - .slot + .slot() .as_usize() .safe_rem(T::EthSpec::slots_per_historical_root())?; state @@ -291,7 +291,7 @@ impl BeaconChain { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); for attestation in block.body().attestations() { let data = attestation.data(); diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index bd183cca589..31746fcabaf 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -35,7 +35,7 @@ use crate::envelope_times_cache::EnvelopeTimesCache; use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::events::ServerSentEventHandler; use crate::execution_payload::{NotifyExecutionLayer, PreparePayloadHandle, get_execution_payload}; -use crate::execution_proof_verification::{GossipVerifiedExecutionProof, ObservedExecutionProofs}; +use crate::execution_proof_verification::ObservedExecutionProofs; use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiSettings}; use crate::inclusion_list_store::{DependentRoot, InclusionListStore}; @@ -1768,8 +1768,18 @@ impl BeaconChain { let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), epoch) .map_err(Error::IncorrectStateForAttestation)?; - let dependent_root = - state.attester_shuffling_decision_root(dependent_block_root, relative_epoch)?; + let dependent_root = if self.spec.gloas_fork_epoch == Some(epoch) { + let decision_slot = epoch + .start_slot(T::EthSpec::slots_per_epoch()) + .saturating_sub(1_u64); + if state.slot() == decision_slot { + dependent_block_root + } else { + *state.get_block_root(decision_slot)? + } + } else { + state.attester_shuffling_decision_root(dependent_block_root, relative_epoch)? + }; let pubkey_cache = self.validator_pubkey_cache.read(); @@ -3925,7 +3935,7 @@ impl BeaconChain { .message() .body() .signed_execution_payload_bid()? - .clone(), + .clone_as_signed_execution_payload_bid(), ); chain.pending_payload_cache.insert_bid(block_root, bid); } @@ -4230,23 +4240,6 @@ impl BeaconChain { } } - /// Caches an execution proof, importing the payload envelope if that was the last piece. - pub async fn check_execution_proof_availability_and_import( - self: &Arc, - verified_proof: GossipVerifiedExecutionProof, - ) -> Result { - let GossipVerifiedExecutionProof { proof, block_slot } = verified_proof; - let bid = self - .get_or_load_gloas_payload_bid(proof.beacon_block_root()) - .await?; - let availability = self - .pending_payload_cache - .put_execution_proof(proof, &bid) - .map_err(BlockError::from)?; - self.process_payload_envelope_availability(block_slot, availability, || Ok(())) - .await - } - /// Load a persisted Gloas bid without blocking the async runtime. pub(crate) async fn get_or_load_gloas_payload_bid( self: &Arc, diff --git a/beacon_node/beacon_chain/src/block_production/bid_selection.rs b/beacon_node/beacon_chain/src/block_production/bid_selection.rs index 01f756d71d9..ca61944ec22 100644 --- a/beacon_node/beacon_chain/src/block_production/bid_selection.rs +++ b/beacon_node/beacon_chain/src/block_production/bid_selection.rs @@ -153,15 +153,15 @@ impl BidCandidate { /// terms are far below `Uint256::MAX`, so it is exact (a u64 gwei sum could saturate under a /// hostile `execution_payment` with the cap disabled). fn trusted_value(&self) -> Uint256 { - let bid = &self.signed_bid.message; + let bid = self.signed_bid.message(); match &self.source { BidSource::Local { block_value, .. } => *block_value, - BidSource::Gossip => gwei_to_wei(bid.value), // gossip `execution_payment` is zero + BidSource::Gossip => gwei_to_wei(bid.value()), // gossip `execution_payment` is zero BidSource::Direct { max_execution_payment, .. - } => gwei_to_wei(bid.value).saturating_add(gwei_to_wei( - bid.execution_payment.min(*max_execution_payment), + } => gwei_to_wei(bid.value()).saturating_add(gwei_to_wei( + bid.execution_payment().min(*max_execution_payment), )), } } @@ -197,10 +197,10 @@ impl BidCandidate { /// or the **unclamped** `value + execution_payment` (the proposer's real revenue; the clamp is a /// ranking-only trust bound). pub fn payload_value(&self) -> Uint256 { - let bid = &self.signed_bid.message; + let bid = self.signed_bid.message(); match &self.source { BidSource::Local { block_value, .. } => *block_value, - _ => gwei_to_wei(bid.value.saturating_add(bid.execution_payment)), + _ => gwei_to_wei(bid.value().saturating_add(bid.execution_payment())), } } @@ -252,7 +252,7 @@ mod tests { use super::*; use bls::Signature; use ssz_types::VariableList; - use types::{ExecutionPayloadBid, MainnetEthSpec}; + use types::{ExecutionPayloadBidGloas, MainnetEthSpec, SignedExecutionPayloadBidGloas}; type TestSpec = MainnetEthSpec; @@ -277,15 +277,17 @@ mod tests { value_gwei: u64, payment_gwei: u64, ) -> Arc> { - Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - builder_index, - value: value_gwei, - execution_payment: payment_gwei, - ..Default::default() + Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + builder_index, + value: value_gwei, + execution_payment: payment_gwei, + ..Default::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }) + )) } fn gossip(value_gwei: u64, boost: u64) -> BidCandidate { @@ -327,13 +329,13 @@ mod tests { fn local(block_value_gwei: u64, should_override_builder: bool) -> BidCandidate { BidCandidate::local( - SignedExecutionPayloadBid { - message: ExecutionPayloadBid { + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { builder_index: LOCAL_BUILDER, ..Default::default() }, signature: Signature::empty(), - }, + }), ExecutionPayloadData { payload: ExecutionPayloadGloas::default(), execution_requests: ExecutionRequestsGloas::default(), @@ -354,7 +356,7 @@ mod tests { BidSource::Direct { .. } => "direct", }; ( - win.signed_bid.message.builder_index, + win.signed_bid.message().builder_index(), win.is_local(), win.payload_value(), source, diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index a58f13947b6..1a74e735893 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -9,7 +9,7 @@ use execution_layer::{ BlockProposalContentsGloas, BuilderParams, PayloadAttributes, PayloadParameters, }; use operation_pool::CompactAttestationRef; -use ssz::{Encode, ProgressiveBitList, TryFromIter}; +use ssz::{BitVector, Encode, ProgressiveBitList, TryFromIter}; use ssz_types::ProgressiveVariableList; use state_processing::common::{get_attesting_indices_from_state, get_indexed_payload_attestation}; use state_processing::envelope_processing::verify_execution_payload_envelope; @@ -31,10 +31,11 @@ use types::{ Address, Attestation, AttestationGloas, AttesterSlashing, AttesterSlashingGloas, BeaconBlock, BeaconBlockBodyGloas, BeaconBlockBodyHeze, BeaconBlockGloas, BeaconBlockHeze, BeaconState, BeaconStateError, BlobsList, BuilderIndex, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, - ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionRequestsGloas, FullPayload, Graffiti, - Hash256, IndexedAttestation, KzgProofs, PayloadAttestation, ProgressiveTransactions, - ProposerSlashing, RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange, - SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, + ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadEnvelope, + ExecutionRequestsGloas, FullPayload, Graffiti, Hash256, IndexedAttestation, KzgProofs, + PayloadAttestation, ProgressiveTransactions, ProposerSlashing, RelativeEpoch, + SignedBeaconBlock, SignedBlsToExecutionChange, SignedExecutionPayloadBid, + SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, SignedExecutionPayloadEnvelope, SignedExecutionPayloadEnvelopeSummary, SignedProposerPreferences, SignedVoluntaryExit, Slot, SyncAggregate, Uint256, Withdrawal, Withdrawals, }; @@ -252,9 +253,9 @@ impl BeaconChain { // Distinct from the beacon-chain parent (`parent_root`); on the wire this becomes the // bid's `parent_block_hash` and the builder request's `parent_hash` path parameter. let executed_ancestor_hash = if should_build_on_full || parent_is_pre_gloas { - parent_bid.block_hash + parent_bid.block_hash() } else { - parent_bid.parent_block_hash + parent_bid.parent_block_hash() }; // The per-proposal context addressing each `getExecutionPayloadBid`. @@ -451,7 +452,7 @@ impl BeaconChain { // building on the parent's payload) so that attestation packing scores head votes like // block processing does. if should_build_on_full { - let parent_slot = state.latest_execution_payload_bid()?.slot; + let parent_slot = state.latest_execution_payload_bid()?.slot(); let availability_index = parent_slot.as_usize() % T::EthSpec::slots_per_historical_root(); state @@ -722,74 +723,104 @@ impl BeaconChain { "Cannot construct a block pre-Gloas".to_owned(), )); } - BeaconState::Gloas(_) => BeaconBlock::Gloas(BeaconBlockGloas { - slot, - proposer_index, - parent_root, - state_root: Hash256::ZERO, - body: BeaconBlockBodyGloas { - randao_reveal, - eth1_data, - graffiti, - // The operation list lengths are bounded by the op pool packing limits above. - proposer_slashings: ProgressiveVariableList::try_from_iter(proposer_slashings) - .map_err(BlockProductionError::SszTypesError)?, - attester_slashings: ProgressiveVariableList::try_from_iter(attester_slashings) - .map_err(BlockProductionError::SszTypesError)?, - attestations: ProgressiveVariableList::try_from_iter(attestations) + BeaconState::Gloas(_) => { + let SignedExecutionPayloadBid::Gloas(signed_execution_payload_bid) = + signed_execution_payload_bid + else { + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a Gloas block with a non-Gloas execution payload bid" + .to_string(), + )); + }; + + BeaconBlock::Gloas(BeaconBlockGloas { + slot, + proposer_index, + parent_root, + state_root: Hash256::ZERO, + body: BeaconBlockBodyGloas { + randao_reveal, + eth1_data, + graffiti, + // The operation list lengths are bounded by the op pool packing limits above. + proposer_slashings: ProgressiveVariableList::try_from_iter( + proposer_slashings, + ) .map_err(BlockProductionError::SszTypesError)?, - deposits: ProgressiveVariableList::try_from_iter(deposits) + attester_slashings: ProgressiveVariableList::try_from_iter( + attester_slashings, + ) .map_err(BlockProductionError::SszTypesError)?, - voluntary_exits: ProgressiveVariableList::try_from_iter(voluntary_exits) + attestations: ProgressiveVariableList::try_from_iter(attestations) + .map_err(BlockProductionError::SszTypesError)?, + deposits: ProgressiveVariableList::try_from_iter(deposits) + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: ProgressiveVariableList::try_from_iter(voluntary_exits) + .map_err(BlockProductionError::SszTypesError)?, + sync_aggregate, + bls_to_execution_changes: ProgressiveVariableList::try_from_iter( + bls_to_execution_changes, + ) .map_err(BlockProductionError::SszTypesError)?, - sync_aggregate, - bls_to_execution_changes: ProgressiveVariableList::try_from_iter( - bls_to_execution_changes, - ) - .map_err(BlockProductionError::SszTypesError)?, - parent_execution_requests, - signed_execution_payload_bid, - payload_attestations: ProgressiveVariableList::try_from_iter( - payload_attestations, - ) - .map_err(BlockProductionError::SszTypesError)?, - _phantom: PhantomData::>, - }, - }), - BeaconState::Heze(_) => BeaconBlock::Heze(BeaconBlockHeze { - slot, - proposer_index, - parent_root, - state_root: Hash256::ZERO, - body: BeaconBlockBodyHeze { - randao_reveal, - eth1_data, - graffiti, - // The operation list lengths are bounded by the op pool packing limits above. - proposer_slashings: ProgressiveVariableList::try_from_iter(proposer_slashings) + parent_execution_requests, + signed_execution_payload_bid, + payload_attestations: ProgressiveVariableList::try_from_iter( + payload_attestations, + ) .map_err(BlockProductionError::SszTypesError)?, - attester_slashings: ProgressiveVariableList::try_from_iter(attester_slashings) + _phantom: PhantomData::>, + }, + }) + } + BeaconState::Heze(_) => { + let SignedExecutionPayloadBid::Heze(signed_execution_payload_bid) = + signed_execution_payload_bid + else { + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a Heze block with a non-Heze execution payload bid" + .to_string(), + )); + }; + + BeaconBlock::Heze(BeaconBlockHeze { + slot, + proposer_index, + parent_root, + state_root: Hash256::ZERO, + body: BeaconBlockBodyHeze { + randao_reveal, + eth1_data, + graffiti, + // The operation list lengths are bounded by the op pool packing limits above. + proposer_slashings: ProgressiveVariableList::try_from_iter( + proposer_slashings, + ) .map_err(BlockProductionError::SszTypesError)?, - attestations: ProgressiveVariableList::try_from_iter(attestations) + attester_slashings: ProgressiveVariableList::try_from_iter( + attester_slashings, + ) .map_err(BlockProductionError::SszTypesError)?, - deposits: ProgressiveVariableList::try_from_iter(deposits) + attestations: ProgressiveVariableList::try_from_iter(attestations) + .map_err(BlockProductionError::SszTypesError)?, + deposits: ProgressiveVariableList::try_from_iter(deposits) + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: ProgressiveVariableList::try_from_iter(voluntary_exits) + .map_err(BlockProductionError::SszTypesError)?, + sync_aggregate, + bls_to_execution_changes: ProgressiveVariableList::try_from_iter( + bls_to_execution_changes, + ) .map_err(BlockProductionError::SszTypesError)?, - voluntary_exits: ProgressiveVariableList::try_from_iter(voluntary_exits) + parent_execution_requests, + signed_execution_payload_bid, + payload_attestations: ProgressiveVariableList::try_from_iter( + payload_attestations, + ) .map_err(BlockProductionError::SszTypesError)?, - sync_aggregate, - bls_to_execution_changes: ProgressiveVariableList::try_from_iter( - bls_to_execution_changes, - ) - .map_err(BlockProductionError::SszTypesError)?, - parent_execution_requests, - signed_execution_payload_bid, - payload_attestations: ProgressiveVariableList::try_from_iter( - payload_attestations, - ) - .map_err(BlockProductionError::SszTypesError)?, - _phantom: PhantomData::>, - }, - }), + _phantom: PhantomData::>, + }, + }) + } }; let signed_beacon_block = SignedBeaconBlock::from_block( @@ -996,21 +1027,63 @@ impl BeaconChain { should_override_builder, } = block_proposal_contents; - // TODO(gloas) since we are defaulting to local building, execution payment is 0 - // execution payment should only be set to > 0 for trusted building. - let bid = ExecutionPayloadBid:: { - parent_block_hash: executed_ancestor_hash, - parent_block_root: parent_root, - block_hash: payload.block_hash, - prev_randao: payload.prev_randao, - fee_recipient: Address::ZERO, - gas_limit: payload.gas_limit, - builder_index, - slot: produce_at_slot, - value: bid_value, - execution_payment: EXECUTION_PAYMENT_TRUSTLESS_BUILD, - blob_kzg_commitments, - execution_requests_root: execution_requests.tree_hash_root(), + let signature = Signature::infinity().map_err(BlockProductionError::BlsError)?; + let signed_execution_payload_bid = match state { + BeaconState::Base(_) + | BeaconState::Altair(_) + | BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) + | BeaconState::Fulu(_) => { + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot produce an execution payload bid pre-Gloas".to_owned(), + )); + } + BeaconState::Gloas(_) => { + let bid = ExecutionPayloadBidGloas:: { + parent_block_hash: executed_ancestor_hash, + parent_block_root: parent_root, + block_hash: payload.block_hash, + prev_randao: payload.prev_randao, + fee_recipient: Address::ZERO, + gas_limit: payload.gas_limit, + builder_index, + slot: produce_at_slot, + value: bid_value, + execution_payment: EXECUTION_PAYMENT_TRUSTLESS_BUILD, + blob_kzg_commitments, + execution_requests_root: execution_requests.tree_hash_root(), + }; + + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { + message: bid, + signature, + }) + } + BeaconState::Heze(_) => { + let bid = ExecutionPayloadBidHeze:: { + parent_block_hash: executed_ancestor_hash, + parent_block_root: parent_root, + block_hash: payload.block_hash, + prev_randao: payload.prev_randao, + fee_recipient: Address::ZERO, + gas_limit: payload.gas_limit, + builder_index, + slot: produce_at_slot, + value: bid_value, + execution_payment: EXECUTION_PAYMENT_TRUSTLESS_BUILD, + blob_kzg_commitments, + execution_requests_root: execution_requests.tree_hash_root(), + // TODO(heze): set the bits based on the IL committee members whose + // inclusion lists were considered when building the payload + inclusion_list_bits: BitVector::new(), + }; + SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze { + message: bid, + signature, + }) + } }; // Store payload data for envelope construction after block is created @@ -1023,10 +1096,7 @@ impl BeaconChain { }; Ok(( - SignedExecutionPayloadBid { - message: bid, - signature: Signature::infinity().map_err(BlockProductionError::BlsError)?, - }, + signed_execution_payload_bid, LocalBuildResult { payload_data, payload_value, @@ -1092,7 +1162,7 @@ impl BeaconChain { // eligibility or coverage can go stale before production. Re-check against the production // state and drop it if it would now fail `per_block_processing`, so a stale gossip bid // can't outrank a viable candidate and sink the whole proposal. - match verify_bid_state_conditions(&gossip_bid.message, state, &self.spec) { + match verify_bid_state_conditions(gossip_bid.message(), state, &self.spec) { Ok(_) => { externals.push(BidCandidate::gossip( gossip_bid, @@ -1109,10 +1179,23 @@ impl BeaconChain { } } + // Filter out any bid whose variant doesn't match the fork at its slot, making sure + // that we don't fail block production + externals.retain(|candidate| { + if let Err(error) = candidate.signed_bid.fork_name(&self.spec) { + warn!( + ?error, + "Skipping bid with a variant inconsistent with its fork" + ); + return false; + } + true + }); + // The parent's exit requests apply to the state before this block's bid is processed, so a // bid from a builder the parent payload exits fails `process_execution_payload_bid`. externals.retain(|candidate| { - let builder_index = candidate.signed_bid.message.builder_index; + let builder_index = candidate.signed_bid.message().builder_index(); let exit_requested = state .get_builder(builder_index) .is_ok_and(|builder| builder_exit_requested(builder, parent_execution_requests)); @@ -1266,7 +1349,7 @@ fn get_execution_payload_gloas( let random = *state.get_randao_mix(current_epoch)?; let parent_bid = state.latest_execution_payload_bid()?; - let is_parent_block_full = parent_block_hash == parent_bid.block_hash; + let is_parent_block_full = parent_block_hash == parent_bid.block_hash(); let target_gas_limit = preferred_gas_limit.unwrap_or_else(|| spec.default_gas_limit(current_epoch)); diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 698e156722e..5f78a9785e6 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -953,10 +953,10 @@ impl GossipVerifiedBlock { // [New in Gloas]: Verify bid.parent_block_root matches block.parent_root. if let Ok(bid) = block.message().body().signed_execution_payload_bid() - && bid.message.parent_block_root != block.message().parent_root() + && bid.message().parent_block_root() != block.message().parent_root() { return Err(BlockError::BidParentRootMismatch { - bid_parent_root: bid.message.parent_block_root, + bid_parent_root: bid.message().parent_block_root(), block_parent_root: block.message().parent_root(), }); } diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 99172c199e0..624430af247 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -144,12 +144,12 @@ impl RangeSyncBlock { envelope: Option>, ) -> Result { if let Some(envelope) = envelope.as_ref() { - let execution_bid = &block + let execution_bid = block .message() .body() .signed_execution_payload_bid() .map_err(|e| format!("missing signed_execution_payload_bid: {e:?}"))? - .message; + .message(); // Skip the finalized-slot check; range sync imports historical (finalized) blocks. let latest_finalized_slot = Slot::new(0); verify_envelope_consistency( diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index a6b7d9e6f79..3af923a33e7 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -13,7 +13,7 @@ use crate::kzg_utils::{build_data_column_sidecars_fulu, build_data_column_sideca use crate::light_client_server_cache::LightClientServerCache; use crate::migrate::{BackgroundMigrator, MigratorConfig}; use crate::observed_data_sidecars::ObservedDataSidecars; -use crate::pending_payload_cache::{PendingPayloadCache, REQUIRED_EXECUTION_PROOFS}; +use crate::pending_payload_cache::PendingPayloadCache; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::persisted_custody::load_custody_context; use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; @@ -1006,13 +1006,6 @@ where debug!(?custody_context, "Loaded persisted custody context"); let custody_context = Arc::new(custody_context); - // Without a proof engine we can't verify proofs, so we don't require them. - let required_execution_proofs = if self.proof_engine.is_some() { - REQUIRED_EXECUTION_PROOFS - } else { - 0 - }; - let beacon_chain = BeaconChain { spec: self.spec.clone(), config: self.chain_config, @@ -1104,7 +1097,6 @@ where self.kzg.clone(), custody_context, disable_get_blobs, - required_execution_proofs, self.spec.clone(), ) .map_err(|e| format!("Error initializing PendingPayloadCache: {:?}", e))?, diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index 23ef67c2731..7a773313d41 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -365,7 +365,7 @@ impl CachedHead { self.snapshot .beacon_state .latest_execution_payload_bid() - .map(|bid| bid.prev_randao) + .map(|bid| bid.prev_randao()) } else { block.body().execution_payload().map(|p| p.prev_randao()) } diff --git a/beacon_node/beacon_chain/src/custody_context.rs b/beacon_node/beacon_chain/src/custody_context.rs index 4a1dfdbe711..7664c4b6b3e 100644 --- a/beacon_node/beacon_chain/src/custody_context.rs +++ b/beacon_node/beacon_chain/src/custody_context.rs @@ -11,7 +11,7 @@ use std::{ }; use tracing::{debug, warn}; use types::{ - ChainSpec, ColumnIndex, Epoch, EthSpec, SignedBeaconBlock, SignedExecutionPayloadBid, Slot, + ChainSpec, ColumnIndex, Epoch, EthSpec, SignedBeaconBlock, SignedExecutionPayloadBidRef, Slot, }; /// A delay before making the CGC change effective to the data availability checker. @@ -590,7 +590,7 @@ impl CustodyContext { pub fn data_columns_required_for_bid( &self, - bid: &SignedExecutionPayloadBid, + bid: SignedExecutionPayloadBidRef, ) -> bool { bid.num_blobs_expected() > 0 && self.data_columns_required_for_epoch(bid.epoch()) } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 54598e37add..7224c9ea4f9 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -647,7 +647,7 @@ pub fn verify_columns_against_block( .message() .body() .signed_execution_payload_bid() - .map(|bid| bid.message.blob_kzg_commitments.to_vec()) + .map(|bid| bid.message().blob_kzg_commitments().to_vec()) .map_err(|_| { AvailabilityCheckError::Unexpected( "Gloas block missing signed_execution_payload_bid".to_string(), diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index acc6e95aa21..50c7c55fb69 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -413,7 +413,7 @@ impl GossipVerifiedDataColumn )?; verify_data_column_sidecar_with_commitments_len( &column_sidecar, - bid.message.blob_kzg_commitments.len(), + bid.message().blob_kzg_commitments().len(), &chain.spec, )?; } @@ -1236,13 +1236,13 @@ pub fn validate_data_column_sidecar_for_gossip_gloas< slot: column_slot, }, )?; - if bid.message.slot != column_slot { + if bid.message().slot() != column_slot { return Err(GossipDataColumnError::BlockSlotMismatch { - block_slot: bid.message.slot, + block_slot: bid.message().slot(), data_column_slot: column_slot, }); } - let kzg_commitments = &bid.message.blob_kzg_commitments; + let kzg_commitments = bid.message().blob_kzg_commitments(); verify_data_column_sidecar_with_commitments_len( &data_column, kzg_commitments.len(), @@ -1388,10 +1388,10 @@ fn validate_partial_data_column_sidecar_for_gossip_gloas( } }; - if slot != bid.message.slot { + if slot != bid.slot() { return PartialColumnVerificationResult::Err(GossipPartialDataColumnError::IncorrectSlot { group_id_slot: slot, - block_slot: bid.message.slot, + block_slot: bid.slot(), }); } @@ -1402,7 +1402,7 @@ fn validate_partial_data_column_sidecar_for_gossip_gloas( match validate_partial_data_column_common( PartialDataColumnRef::Gloas(&column), - bid.message.blob_kzg_commitments.as_ref(), + bid.message().blob_kzg_commitments().as_ref(), chain, ) { Ok(()) => PartialColumnVerificationResult::Ok(GossipVerifiedPartialDataColumn::PostGloas { @@ -1478,7 +1478,7 @@ impl GossipVerifiedPartialDataColumn { pub fn slot(&self) -> Slot { match self { Self::PreGloas { header, .. } => header.as_header().slot(), - Self::PostGloas { bid, .. } => bid.message.slot, + Self::PostGloas { bid, .. } => bid.slot(), } } @@ -1568,7 +1568,7 @@ pub(crate) fn load_gloas_payload_bid( .body() .signed_execution_payload_bid() .map_err(BeaconChainError::BeaconStateError)? - .clone(), + .clone_as_signed_execution_payload_bid(), ) } else { match chain @@ -1582,7 +1582,7 @@ pub(crate) fn load_gloas_payload_bid( .body() .signed_execution_payload_bid() .map_err(BeaconChainError::BeaconStateError)? - .clone(), + .clone_as_signed_execution_payload_bid(), ), Some(DatabaseBlock::Blinded(block)) => Arc::new( block @@ -1590,7 +1590,7 @@ pub(crate) fn load_gloas_payload_bid( .body() .signed_execution_payload_bid() .map_err(BeaconChainError::BeaconStateError)? - .clone(), + .clone_as_signed_execution_payload_bid(), ), None => { return Ok(None); @@ -1895,8 +1895,8 @@ mod test { Cell, CellBitmap, DataColumnSidecar, DataColumnSidecarFulu, DataColumnSubnetId, EthSpec, ForkName, Hash256, MainnetEthSpec, PartialDataColumn, PartialDataColumnFulu, PartialDataColumnGloas, PartialDataColumnHeader, PartialDataColumnSidecarFulu, - PartialDataColumnSidecarGloas, SignedExecutionPayloadBid, Slot, - test_utils::test_unstructured, + PartialDataColumnSidecarGloas, SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, + Slot, test_utils::test_unstructured, }; type E = MainnetEthSpec; @@ -1973,7 +1973,7 @@ mod test { .body() .signed_execution_payload_bid() .unwrap() - .clone(); + .clone_as_signed_execution_payload_bid(); // Put the block on disk only, so the bid can only be found via the store fallback. harness.chain.store.put_block(&block_root, block).unwrap(); @@ -2410,12 +2410,12 @@ mod test { block_root: Hash256, slot: Slot, ) { - let mut bid = SignedExecutionPayloadBid::::empty(); + let mut bid = SignedExecutionPayloadBidGloas::::empty(); bid.message.slot = slot; harness .chain .pending_payload_cache - .insert_bid(block_root, Arc::new(bid)); + .insert_bid(block_root, Arc::new(SignedExecutionPayloadBid::Gloas(bid))); } async fn gloas_partial_slot_mismatch_returns_error( diff --git a/beacon_node/beacon_chain/src/events.rs b/beacon_node/beacon_chain/src/events.rs index b7b131e5602..9e82992476c 100644 --- a/beacon_node/beacon_chain/src/events.rs +++ b/beacon_node/beacon_chain/src/events.rs @@ -4,7 +4,8 @@ use tokio::sync::broadcast::{Receiver, Sender, error::SendError}; use tracing::trace; use types::EthSpec; -const DEFAULT_CHANNEL_CAPACITY: usize = 16; +// Allow a full slot of payload attestation messages (mainnet PTC_SIZE) to be buffered. +const DEFAULT_CHANNEL_CAPACITY: usize = 512; pub struct ServerSentEventHandler { attestation_tx: Sender>, diff --git a/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs b/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs index 6348f264f16..256c979ee3b 100644 --- a/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs +++ b/beacon_node/beacon_chain/src/execution_proof_verification/gossip_verified_execution_proof.rs @@ -1,21 +1,27 @@ use super::Error; use crate::beacon_chain::BeaconStore; use crate::canonical_head::CanonicalHead; +use crate::data_column_verification::load_gloas_payload_bid; use crate::execution_proof_verification::observed_execution_proofs::{ ObservedExecutionProofs, ProofObservation, }; use crate::shuffling_cache::{ShufflingCache, with_cached_shuffling}; use crate::validator_pubkey_cache::ValidatorPubkeyCache; -use crate::{BeaconChain, BeaconChainTypes}; +use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use parking_lot::RwLock; use proof_engine::{ProofEngine, ProofVerificationOutcome}; use state_processing::builder_deposits_cache::OnboardBuildersCache; +use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; use std::sync::Arc; use tree_hash::TreeHash; -use types::execution::SignedExecutionProof; +use types::execution::{ + ExecutionProof, ExecutionProofEnvelope, NewPayloadRequest, PublicInput, + STATELESS_INPUT_SCHEMA_ID, SignedExecutionProofEnvelope, VersionedHashes, +}; use types::{ChainSpec, Domain, EthSpec, Hash256, SignedRoot, Slot}; pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { + pub chain: &'a Arc>, pub canonical_head: &'a CanonicalHead, pub observed_execution_proofs: &'a RwLock, pub validator_pubkey_cache: &'a RwLock>, @@ -27,15 +33,15 @@ pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub genesis_validators_root: Hash256, } -/// A `SignedExecutionProof` that has been verified for propagation on the gossip network. +/// A `SignedExecutionProofEnvelope` that has been verified for propagation on the gossip network. pub struct GossipVerifiedExecutionProof { - pub proof: Arc, + pub proof: Arc, pub block_slot: Slot, } impl GossipVerifiedExecutionProof { pub async fn new( - proof: Arc, + proof: Arc, ctx: &GossipVerificationContext<'_, T>, ) -> Result { // [REJECT] `proof.proof_data` is non-empty. The `MAX_PROOF_SIZE` upper bound is enforced @@ -44,6 +50,8 @@ impl GossipVerifiedExecutionProof { return Err(Error::EmptyProofData); } + // [DEVIATION] The sidecar decides which proof types are supported. + let proof_root = proof.message.tree_hash_root(); let block_root = proof.beacon_block_root(); let proof_type = proof.proof_type(); @@ -124,6 +132,19 @@ impl GossipVerifiedExecutionProof { } } + // [IGNORE] The payload this proof is about is available. Reads the store and merkleizes a + // payload, so it runs on the blocking pool. + let chain = ctx.chain.clone(); + let signed_proof = proof.clone(); + let execution_proof = ctx + .chain + .spawn_blocking_handle( + move || get_execution_proof(&chain, &signed_proof.message, block_root), + "get_execution_proof", + ) + .await + .map_err(|e| Error::BeaconChainError(Box::new(e)))??; + // Only record the validator's attempt after the signature binds `validator_index`; // recording earlier would let unauthenticated messages suppress honest provers. if !ctx @@ -149,7 +170,7 @@ impl GossipVerifiedExecutionProof { // significantly. let proof_engine = ctx.proof_engine.as_ref().ok_or(Error::ProofEngineMissing)?; match proof_engine - .verify_execution_proof(&proof.message) + .verify_execution_proof(&execution_proof) .await .map_err(Error::ProofEngine)? { @@ -165,8 +186,11 @@ impl GossipVerifiedExecutionProof { } impl BeaconChain { - pub fn execution_proof_gossip_verification_context(&self) -> GossipVerificationContext<'_, T> { + pub fn execution_proof_gossip_verification_context( + self: &Arc, + ) -> GossipVerificationContext<'_, T> { GossipVerificationContext { + chain: self, canonical_head: &self.canonical_head, observed_execution_proofs: &self.observed_execution_proofs, validator_pubkey_cache: &self.validator_pubkey_cache, @@ -180,8 +204,8 @@ impl BeaconChain { } pub async fn verify_execution_proof_for_gossip( - &self, - proof: Arc, + self: &Arc, + proof: Arc, ) -> Result { GossipVerifiedExecutionProof::new( proof, @@ -190,3 +214,56 @@ impl BeaconChain { .await } } + +/// Spec helper `get_execution_proof`. +/// +/// TODO(EIP8025): add cache of result of the hash. The root is the same for every proof of a +/// block, so it is recomputed on each gossip hop. +fn get_execution_proof( + chain: &BeaconChain, + proof_envelope: &ExecutionProofEnvelope, + block_root: Hash256, +) -> Result { + let payload_envelope = chain + .store + .get_signed_payload_envelope(&block_root) + .map_err(|e| Error::BeaconChainError(Box::new(e.into())))? + .ok_or(Error::PayloadUnavailable { + beacon_block_root: block_root, + })? + .message; + + // Commitments live on the bid, not the envelope. + let bid = load_gloas_payload_bid(block_root, chain) + .map_err(|e| Error::BeaconChainError(Box::new(e)))? + .ok_or(Error::UnknownBlockRoot { + beacon_block_root: block_root, + })?; + + let versioned_hashes = VersionedHashes::::new( + bid.message() + .blob_kzg_commitments() + .iter() + .map(kzg_commitment_to_versioned_hash) + .collect(), + ) + .map_err(|e| Error::BeaconChainError(Box::new(BeaconChainError::SszTypesError(e))))?; + + let new_payload_request = NewPayloadRequest:: { + execution_payload: payload_envelope.payload, + versioned_hashes, + parent_beacon_block_root: payload_envelope.parent_beacon_block_root, + execution_requests: payload_envelope.execution_requests, + }; + + Ok(ExecutionProof { + proof_data: proof_envelope.proof_data.clone(), + proof_type: proof_envelope.proof_type, + public_input: PublicInput { + new_payload_request_root: new_payload_request.tree_hash_root(), + successful_validation: true, + chain_id: chain.spec.deposit_chain_id, + schema_id: STATELESS_INPUT_SCHEMA_ID, + }, + }) +} diff --git a/beacon_node/beacon_chain/src/execution_proof_verification/mod.rs b/beacon_node/beacon_chain/src/execution_proof_verification/mod.rs index ce206a43d93..06a3e9bbbdd 100644 --- a/beacon_node/beacon_chain/src/execution_proof_verification/mod.rs +++ b/beacon_node/beacon_chain/src/execution_proof_verification/mod.rs @@ -33,6 +33,10 @@ pub enum Error { slot: Slot, finalized_slot: Slot, }, + /// The payload this proof is about has not been seen (IGNORE). + PayloadUnavailable { + beacon_block_root: Hash256, + }, /// `proof_data` is empty (REJECT). EmptyProofData, /// The validator index does not exist (REJECT). diff --git a/beacon_node/beacon_chain/src/execution_proof_verification/observed_execution_proofs.rs b/beacon_node/beacon_chain/src/execution_proof_verification/observed_execution_proofs.rs index 314b6b76a1d..2fc4614ae7d 100644 --- a/beacon_node/beacon_chain/src/execution_proof_verification/observed_execution_proofs.rs +++ b/beacon_node/beacon_chain/src/execution_proof_verification/observed_execution_proofs.rs @@ -1,4 +1,4 @@ -//! Provides the `ObservedExecutionProofs` struct which allows for ignoring `SignedExecutionProof`s +//! Provides the `ObservedExecutionProofs` struct which allows for ignoring `SignedExecutionProofEnvelope`s //! that we have already seen over the gossip network. //! Only proofs that have completed signature verification can be added to this cache to reduce //! DoS risks. diff --git a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs index d1f45254aa8..1852df457a1 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs @@ -65,7 +65,9 @@ impl PartialHeaderOrBid { block: &SignedBeaconBlock, ) -> Option { if let Ok(bid) = block.message().body().signed_execution_payload_bid() { - Some(PartialHeaderOrBid::Bid(Arc::new(bid.clone()))) + Some(PartialHeaderOrBid::Bid(Arc::new( + bid.clone_as_signed_execution_payload_bid(), + ))) } else { PartialDataColumnHeader::try_from(block) .ok() @@ -76,14 +78,16 @@ impl PartialHeaderOrBid { pub fn kzg_commitments(&self) -> ListRef<'_, KzgCommitment, E::MaxBlobCommitmentsPerBlock> { match self { PartialHeaderOrBid::PartialHeader(header) => ListRef::Basic(&header.kzg_commitments), - PartialHeaderOrBid::Bid(bid) => ListRef::Progressive(&bid.message.blob_kzg_commitments), + PartialHeaderOrBid::Bid(bid) => { + ListRef::Progressive(bid.message().blob_kzg_commitments()) + } } } pub fn slot(&self) -> Slot { match self { PartialHeaderOrBid::PartialHeader(header) => header.slot(), - PartialHeaderOrBid::Bid(bid) => bid.message.slot, + PartialHeaderOrBid::Bid(bid) => bid.slot(), } } } @@ -632,7 +636,7 @@ async fn build_partial_columns_from_v4_response( header.slot(), ), PartialHeaderOrBid::Bid(bid) => { - ObservationKey::new_block_root_key(block_root, bid.message.slot) + ObservationKey::new_block_root_key(block_root, bid.message().slot()) } }; if let Some(observed_columns) = @@ -709,7 +713,7 @@ async fn compute_custody_columns_to_import( header.slot(), ), PartialHeaderOrBid::Bid(bid) => { - ObservationKey::new_block_root_key(block_root, bid.message.slot) + ObservationKey::new_block_root_key(block_root, bid.slot()) } }; diff --git a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs index 10795562159..cf160c17f4a 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs @@ -525,7 +525,7 @@ mod get_blobs_v4 { .body() .signed_execution_payload_bid() .expect("gloas block has a bid") - .clone(), + .clone_as_signed_execution_payload_bid(), ); // Real pending payload cache: the Gloas path inserts the bid and merges partial columns into @@ -543,7 +543,7 @@ mod get_blobs_v4 { spec.clone(), )); let cache = Arc::new( - PendingPayloadCache::::new(get_kzg(&spec), custody_context, false, 0, spec.clone()) + PendingPayloadCache::::new(get_kzg(&spec), custody_context, false, spec.clone()) .expect("create pending payload cache"), ); mock_adapter diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index e6b097ec345..aff2e387c37 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -146,7 +146,7 @@ impl BeaconChain { .body() .signed_execution_payload_bid() .ok() - .map(|bid| bid.message.parent_block_hash) + .map(|bid| bid.message().parent_block_hash()) }); let mut blob_batch = Vec::::new(); @@ -240,7 +240,7 @@ impl BeaconChain { .body() .signed_execution_payload_bid() .ok() - .map(|bid| bid.message.block_hash) + .map(|bid| bid.message().block_hash()) .and_then(|bid_hash| { child_bid_parent_hash.map(|child_parent_hash| bid_hash == child_parent_hash) }); @@ -334,7 +334,7 @@ impl BeaconChain { .body() .signed_execution_payload_bid() .ok() - .map(|bid| bid.message.parent_block_hash); + .map(|bid| bid.message().parent_block_hash()); signed_blocks.push(block); // If we've reached genesis, add the genesis block root to the batch for all slots diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index f6dab3c9a7e..a224af6cdd7 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -465,7 +465,7 @@ pub fn blobs_to_partial_data_columns( } PartialHeaderOrBid::Bid(bid) => build_partial_data_columns_gloas( block_root, - bid.message.slot, + bid.slot(), blob_cells_and_proofs_vec, spec, ) diff --git a/beacon_node/beacon_chain/src/observed_execution_payloads.rs b/beacon_node/beacon_chain/src/observed_execution_payloads.rs index 29086222528..7b40ed3cb11 100644 --- a/beacon_node/beacon_chain/src/observed_execution_payloads.rs +++ b/beacon_node/beacon_chain/src/observed_execution_payloads.rs @@ -51,13 +51,13 @@ impl BeaconChain { .insert(payload.block_hash(), payload.gas_limit()); } } else if block.slot() == self.spec.genesis_slot { - let bid = &block + let bid = block .body() .signed_execution_payload_bid() .map_err(BeaconChainError::BeaconStateError)? - .message; + .message(); self.observed_execution_payloads - .insert(bid.parent_block_hash, bid.gas_limit); + .insert(bid.parent_block_hash(), bid.gas_limit()); } else if let Some(envelope) = head.snapshot.execution_envelope.as_ref() { let payload_header = &envelope.payload_header; self.observed_execution_payloads diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs b/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs index f57d5628695..a063b9c3b0e 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/direct_verified_bid.rs @@ -45,37 +45,44 @@ pub fn verify_direct_bid( state: &BeaconState, spec: &ChainSpec, ) -> Result<(), PayloadBidError> { - let bid = &signed_bid.message; + let bid = signed_bid.message(); + + // Ensure the bid is the correct structure for the fork at `bid.slot()`. + signed_bid + .fork_name(spec) + .map_err(PayloadBidError::InconsistentFork)?; // The bid must be for exactly the slot being produced. - if bid.slot != proposal_slot { - return Err(PayloadBidError::InvalidBidSlot { bid_slot: bid.slot }); + if bid.slot() != proposal_slot { + return Err(PayloadBidError::InvalidBidSlot { + bid_slot: bid.slot(), + }); } // The bid must build on the executed ancestor the producer selected (FULL or EMPTY view). - if bid.parent_block_hash != executed_ancestor_hash { + if bid.parent_block_hash() != executed_ancestor_hash { return Err(PayloadBidError::InvalidParentBlockHash { - bid: bid.parent_block_hash, + bid: bid.parent_block_hash(), expected: executed_ancestor_hash, }); } - if bid.parent_block_root != parent_block_root { + if bid.parent_block_root() != parent_block_root { return Err(PayloadBidError::InvalidParentBlockRoot { - bid: bid.parent_block_root, + bid: bid.parent_block_root(), expected: parent_block_root, }); } // `prev_randao` must be the RANDAO mix from the production state. let expected_prev_randao = *state.get_randao_mix(proposal_slot.epoch(E::slots_per_epoch()))?; - if bid.prev_randao != expected_prev_randao { - return Err(PayloadBidError::InvalidPrevRandao { slot: bid.slot }); + if bid.prev_randao() != expected_prev_randao { + return Err(PayloadBidError::InvalidPrevRandao { slot: bid.slot() }); } // The gas limit must be compatible with the parent payload's, given the proposer's target. if !is_gas_limit_target_compatible( executed_ancestor_gas_limit, - bid.gas_limit, + bid.gas_limit(), proposer_preferences.message.target_gas_limit, )? { return Err(PayloadBidError::InvalidGasLimit); @@ -89,14 +96,14 @@ pub fn verify_direct_bid( // response filter from beacon-APIs #630; an empty list accepts any builder). if !expected_builder_pubkeys.is_empty() { let actual = state - .get_builder(bid.builder_index) + .get_builder(bid.builder_index()) .map_err(|_| PayloadBidError::InvalidBuilder { - builder_index: bid.builder_index, + builder_index: bid.builder_index(), })? .pubkey; if !expected_builder_pubkeys.contains(&actual) { return Err(PayloadBidError::UnexpectedBuilder { - builder_index: bid.builder_index, + builder_index: bid.builder_index(), }); } } @@ -105,7 +112,7 @@ pub fn verify_direct_bid( execution_payload_bid_signature_set( state, |i| get_builder_pubkey_from_state(state, i), - signed_bid, + signed_bid.to_ref(), spec, ) .map_err(|_| PayloadBidError::BadSignature)? @@ -121,7 +128,11 @@ pub fn verify_direct_bid( mod tests { use super::*; use bls::Signature; - use types::{Address, ExecutionPayloadBid, MinimalEthSpec, ProposerPreferences}; + use types::{ + Address, Epoch, ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ForkName, + MinimalEthSpec, ProposerPreferences, SignedExecutionPayloadBidGloas, + SignedExecutionPayloadBidHeze, + }; type E = MinimalEthSpec; @@ -129,7 +140,7 @@ mod tests { const EXECUTED_ANCESTOR_GAS_LIMIT: u64 = 30_000_000; fn state_and_spec() -> (BeaconState, ChainSpec) { - let spec = E::default_spec(); + let spec = ForkName::Gloas.make_genesis_spec(E::default_spec()); let state = BeaconState::new(0, <_>::default(), &spec); (state, spec) } @@ -145,22 +156,40 @@ mod tests { } } + fn bid_message( + slot: Slot, + parent_block_hash: ExecutionBlockHash, + parent_block_root: Hash256, + prev_randao: Hash256, + ) -> ExecutionPayloadBidGloas { + ExecutionPayloadBidGloas { + slot, + parent_block_hash, + parent_block_root, + prev_randao, + ..ExecutionPayloadBidGloas::default() + } + } + + fn sign(message: ExecutionPayloadBidGloas) -> SignedExecutionPayloadBid { + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { + message, + signature: Signature::empty(), + }) + } + fn signed_bid( slot: Slot, parent_block_hash: ExecutionBlockHash, parent_block_root: Hash256, prev_randao: Hash256, ) -> SignedExecutionPayloadBid { - SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - parent_block_hash, - parent_block_root, - prev_randao, - ..ExecutionPayloadBid::default() - }, - signature: Signature::empty(), - } + sign(bid_message( + slot, + parent_block_hash, + parent_block_root, + prev_randao, + )) } #[test] @@ -275,14 +304,15 @@ mod tests { // claims a `block_hash` equal to its `parent_block_hash` — the consensus assert from // `process_execution_payload_bid` that must be front-run before selection. let executed_ancestor = ExecutionBlockHash::repeat_byte(7); - let mut bid = signed_bid( + let mut message = bid_message( Slot::new(1), executed_ancestor, Hash256::ZERO, Hash256::ZERO, ); - bid.message.block_hash = executed_ancestor; - bid.message.gas_limit = EXECUTED_ANCESTOR_GAS_LIMIT; + message.block_hash = executed_ancestor; + message.gas_limit = EXECUTED_ANCESTOR_GAS_LIMIT; + let bid = sign(message); let result = verify_direct_bid( &bid, Slot::new(1), @@ -305,13 +335,14 @@ mod tests { let (state, spec) = state_and_spec(); // Passes the slot, parent and RANDAO checks (a fresh state's mix is zero), then asks for // double the parent's gas limit — far outside the per-block adjustment bound. - let mut bid = signed_bid( + let mut message = bid_message( Slot::new(1), ExecutionBlockHash::zero(), Hash256::ZERO, Hash256::ZERO, ); - bid.message.gas_limit = EXECUTED_ANCESTOR_GAS_LIMIT * 2; + message.gas_limit = EXECUTED_ANCESTOR_GAS_LIMIT * 2; + let bid = sign(message); let result = verify_direct_bid( &bid, Slot::new(1), @@ -331,16 +362,17 @@ mod tests { let (state, spec) = state_and_spec(); // Same as above but holding the parent's gas limit, which is always compatible: the bid // must get past the gas check and fail later, on the fresh state's missing builder. - let mut bid = signed_bid( + let mut message = bid_message( Slot::new(1), ExecutionBlockHash::zero(), Hash256::ZERO, Hash256::ZERO, ); - bid.message.gas_limit = EXECUTED_ANCESTOR_GAS_LIMIT; + message.gas_limit = EXECUTED_ANCESTOR_GAS_LIMIT; // A default (zero) `block_hash` would equal the zero parent hash and trip the // block-hash-equals-parent rejection before the checks this test targets. - bid.message.block_hash = ExecutionBlockHash::repeat_byte(1); + message.block_hash = ExecutionBlockHash::repeat_byte(1); + let bid = sign(message); let result = verify_direct_bid( &bid, Slot::new(1), @@ -357,4 +389,54 @@ mod tests { "expected to fail after the gas check, got {result:?}" ); } + #[test] + fn rejects_gloas_bid_at_heze_slot() { + let (state, mut spec) = state_and_spec(); + let heze_fork_epoch = Epoch::new(1); + spec.heze_fork_epoch = Some(heze_fork_epoch); + let heze_slot = heze_fork_epoch.start_slot(E::slots_per_epoch()); + let bid = signed_bid( + heze_slot, + ExecutionBlockHash::zero(), + Hash256::ZERO, + Hash256::ZERO, + ); + let result = verify_direct_bid( + &bid, + heze_slot, + ExecutionBlockHash::zero(), + Hash256::ZERO, + EXECUTED_ANCESTOR_GAS_LIMIT, + &BuilderPubkeys::default(), + &preferences(), + &state, + &spec, + ); + assert!(matches!(result, Err(PayloadBidError::InconsistentFork(_)))); + } + + #[test] + fn rejects_heze_bid_at_gloas_slot() { + let (state, mut spec) = state_and_spec(); + spec.heze_fork_epoch = Some(Epoch::new(1)); + let bid = SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze { + message: ExecutionPayloadBidHeze { + slot: Slot::new(1), + ..ExecutionPayloadBidHeze::default() + }, + signature: Signature::empty(), + }); + let result = verify_direct_bid( + &bid, + Slot::new(1), + ExecutionBlockHash::zero(), + Hash256::ZERO, + EXECUTED_ANCESTOR_GAS_LIMIT, + &BuilderPubkeys::default(), + &preferences(), + &state, + &spec, + ); + assert!(matches!(result, Err(PayloadBidError::InconsistentFork(_)))); + } } diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs b/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs index c8e6476f673..74cfb08c79c 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs @@ -19,7 +19,7 @@ use state_processing::signature_sets::{ }; use tracing::debug; use types::{ - BeaconState, Builder, ChainSpec, EthSpec, ExecutionPayloadBid, ExecutionRequestsGloas, + BeaconState, Builder, ChainSpec, EthSpec, ExecutionPayloadBidRef, ExecutionRequestsGloas, SignedExecutionPayloadBid, SignedProposerPreferences, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, }; @@ -33,14 +33,14 @@ pub(crate) fn verify_bid_slot(bid_slot: Slot, current_slot: Slot) -> Result<(), } fn verify_bid_payment_and_blobs( - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef, spec: &ChainSpec, ) -> Result<(), PayloadBidError> { // Execution payments are used by off protocol builders. In protocol bids // should always have this value set to zero. - if bid.execution_payment != 0 { + if bid.execution_payment() != 0 { return Err(PayloadBidError::ExecutionPaymentNonZero { - execution_payment: bid.execution_payment, + execution_payment: bid.execution_payment(), }); } @@ -55,28 +55,28 @@ fn verify_bid_payment_and_blobs( /// gossip *and* direct (builder-API) — must front-run it: a bid that fails only at block /// processing has already won selection and costs the proposer the slot. pub(crate) fn verify_bid_block_hash_not_parent( - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef, ) -> Result<(), PayloadBidError> { - if bid.block_hash == bid.parent_block_hash { + if bid.block_hash() == bid.parent_block_hash() { return Err(PayloadBidError::BlockHashEqualsParentBlockHash { - slot: bid.slot, - block_hash: bid.block_hash, + slot: bid.slot(), + block_hash: bid.block_hash(), }); } Ok(()) } fn verify_bid_blobs( - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef, spec: &ChainSpec, ) -> Result<(), PayloadBidError> { let max_blobs_per_block = - spec.max_blobs_per_block(bid.slot.epoch(E::slots_per_epoch())) as usize; + spec.max_blobs_per_block(bid.slot().epoch(E::slots_per_epoch())) as usize; - if bid.blob_kzg_commitments.len() > max_blobs_per_block { + if bid.blob_kzg_commitments().len() > max_blobs_per_block { return Err(PayloadBidError::InvalidBlobKzgCommitments { max_blobs_per_block, - blob_kzg_commitments_len: bid.blob_kzg_commitments.len(), + blob_kzg_commitments_len: bid.blob_kzg_commitments().len(), }); } @@ -94,15 +94,15 @@ fn verify_bid_blobs( /// rules). A check that must cover both intakes belongs in one of those shared helpers — adding /// it only here leaves gossip uncovered. pub(crate) fn verify_direct_bid_consistency( - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef, current_slot: Slot, proposer_preferences: &SignedProposerPreferences, head_state: &BeaconState, spec: &ChainSpec, ) -> Result<(), PayloadBidError> { - verify_bid_slot(bid.slot, current_slot)?; + verify_bid_slot(bid.slot(), current_slot)?; - if bid.fee_recipient != proposer_preferences.message.fee_recipient { + if bid.fee_recipient() != proposer_preferences.message.fee_recipient { return Err(PayloadBidError::InvalidFeeRecipient); } @@ -123,11 +123,11 @@ pub(crate) fn verify_direct_bid_consistency( /// dropping). Re-running them against the production state lets bid selection drop a gossip bid that /// has since become invalid, rather than committing to it and failing the whole block. pub(crate) fn verify_bid_state_conditions( - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef, head_state: &BeaconState, spec: &ChainSpec, ) -> Result<(), PayloadBidError> { - let builder_index = bid.builder_index; + let builder_index = bid.builder_index(); let builder_version = head_state .get_builder(builder_index) .map_err(|_| PayloadBidError::InvalidBuilder { builder_index })? @@ -147,10 +147,10 @@ pub(crate) fn verify_bid_state_conditions( return Err(PayloadBidError::InvalidBuilder { builder_index }); } - if !head_state.can_builder_cover_bid(builder_index, bid.value, spec)? { + if !head_state.can_builder_cover_bid(builder_index, bid.value(), spec)? { return Err(PayloadBidError::BuilderCantCoverBid { builder_index, - builder_bid: bid.value, + builder_bid: bid.value(), }); } @@ -160,24 +160,24 @@ pub(crate) fn verify_bid_state_conditions( /// Returns `true` if the bid builds on the parent's full payload and that payload carries an exit /// request for the bid's builder. pub(crate) fn parent_payload_exits_builder( - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef, parent_block: &ProtoBlock, head_state: &BeaconState, store: &BeaconStore, ) -> Result { - if parent_block.execution_payload_block_hash != Some(bid.parent_block_hash) { + if parent_block.execution_payload_block_hash != Some(bid.parent_block_hash()) { return Ok(false); } - let builder = head_state.get_builder(bid.builder_index)?; + let builder = head_state.get_builder(bid.builder_index())?; // Only the execution requests are needed, and the summary survives payload body pruning. let parent_summary = store - .get_payload_envelope_summary(&bid.parent_block_root) + .get_payload_envelope_summary(&bid.parent_block_root()) .map_err(|e| { PayloadBidError::InternalError(format!("failed to load parent payload summary: {e:?}")) })? .ok_or(PayloadBidError::ParentExecutionPayloadUnknown { - parent_block_hash: bid.parent_block_hash, + parent_block_hash: bid.parent_block_hash(), })?; Ok(builder_exit_requested( @@ -199,7 +199,7 @@ pub(crate) fn builder_exit_requested( pub(crate) fn is_bid_compatible_with_head( cached_head: &CachedHead, fork_choice_read: &ForkChoiceReadGuard<'_, T>, - bid: &ExecutionPayloadBid, + bid: ExecutionPayloadBidRef<'_, T::EthSpec>, spec: &ChainSpec, ) -> Result { let head_block_root = cached_head.head_block_root(); @@ -217,28 +217,28 @@ pub(crate) fn is_bid_compatible_with_head( .fork_name_at_slot::(head_block.slot) .gloas_enabled(); - let builds_on_parent_block = Some(bid.parent_block_root) == head_block.parent_root; + let builds_on_parent_block = Some(bid.parent_block_root()) == head_block.parent_root; let builds_on_parent_payload = if head_is_pre_gloas { head_block .parent_root .and_then(|parent_root| fork_choice_read.get_block(&parent_root)) .is_some_and(|parent| { - parent.block_hash() == PayloadBlockHash::Hash(bid.parent_block_hash) + parent.block_hash() == PayloadBlockHash::Hash(bid.parent_block_hash()) }) } else { - head_block.execution_payload_parent_hash == Some(bid.parent_block_hash) + head_block.execution_payload_parent_hash == Some(bid.parent_block_hash()) }; if builds_on_parent_block && builds_on_parent_payload { return Ok(true); } - if bid.parent_block_root != head_block.root { + if bid.parent_block_root() != head_block.root { return Ok(false); } let builds_on_head_payload = - head_block.block_hash() == PayloadBlockHash::Hash(bid.parent_block_hash); + head_block.block_hash() == PayloadBlockHash::Hash(bid.parent_block_hash()); if head_is_pre_gloas { return Ok(builds_on_head_payload); @@ -248,7 +248,7 @@ pub(crate) fn is_bid_compatible_with_head( .should_build_on_full( &head_block_root, cached_head.head_payload_status(), - bid.slot, + bid.slot(), ) .map_err(|e| { PayloadBidError::InternalError(format!("should_build_on_full failed: {e:?}")) @@ -286,10 +286,15 @@ impl GossipVerifiedPayloadBid { where T: BeaconChainTypes, { - let bid_slot = signed_bid.message.slot; - let bid_parent = BidParent::from_bid(&signed_bid.message); - let bid_parent_block_root = signed_bid.message.parent_block_root; - let bid_value = signed_bid.message.value; + // Ensure the bid is the correct structure for the fork at `bid.slot()`. + signed_bid + .fork_name(ctx.spec) + .map_err(PayloadBidError::InconsistentFork)?; + + let bid_slot = signed_bid.message().slot(); + let bid_parent = BidParent::from_bid(signed_bid.message()); + let bid_parent_block_root = signed_bid.message().parent_block_root(); + let bid_value = signed_bid.message().value(); let current_slot = ctx .slot_clock .now() @@ -298,10 +303,14 @@ impl GossipVerifiedPayloadBid { if ctx .gossip_verified_payload_bid_cache - .seen_builder_bid_for_parent(&bid_slot, bid_parent, signed_bid.message.builder_index) + .seen_builder_bid_for_parent( + &bid_slot, + bid_parent, + signed_bid.message().builder_index(), + ) { return Err(PayloadBidError::BuilderAlreadySeen { - builder_index: signed_bid.message.builder_index, + builder_index: signed_bid.message().builder_index(), slot: bid_slot, }); } @@ -311,10 +320,10 @@ impl GossipVerifiedPayloadBid { if let Some(cached_bid) = ctx .gossip_verified_payload_bid_cache .get_highest_bid(bid_slot, bid_parent) - && bid_value <= cached_bid.message.value + && bid_value <= cached_bid.message().value() { return Err(PayloadBidError::BidValueBelowCached { - cached_value: cached_bid.message.value, + cached_value: cached_bid.message().value(), incoming_value: bid_value, }); } @@ -334,7 +343,7 @@ impl GossipVerifiedPayloadBid { }); } - verify_bid_payment_and_blobs(&signed_bid.message, ctx.spec)?; + verify_bid_payment_and_blobs(signed_bid.message(), ctx.spec)?; let parent_block = parent_block.ok_or(PayloadBidError::ParentBlockRootUnknown { parent_block_root: bid_parent_block_root, @@ -395,19 +404,19 @@ impl GossipVerifiedPayloadBid { return Err(PayloadBidError::NoProposerPreferences { slot: bid_slot }); }; - if signed_bid.message.fee_recipient != proposer_preferences.message.fee_recipient { + if signed_bid.message().fee_recipient() != proposer_preferences.message.fee_recipient { return Err(PayloadBidError::InvalidFeeRecipient); } let parent_gas_limit = ctx .observed_execution_payloads - .get_gas_limit(signed_bid.message.parent_block_hash) + .get_gas_limit(signed_bid.message().parent_block_hash()) .ok_or(PayloadBidError::ParentExecutionPayloadUnknown { - parent_block_hash: signed_bid.message.parent_block_hash, + parent_block_hash: signed_bid.message().parent_block_hash(), })?; if !is_gas_limit_target_compatible( parent_gas_limit, - signed_bid.message.gas_limit, + signed_bid.message().gas_limit(), proposer_preferences.message.target_gas_limit, )? { return Err(PayloadBidError::InvalidGasLimit); @@ -416,7 +425,7 @@ impl GossipVerifiedPayloadBid { let fork_choice = ctx.canonical_head.fork_choice_read_lock(); // TODO(gloas) should we reprocess a dropped bid when the head changes to its parent? - if !is_bid_compatible_with_head(&cached_head, &fork_choice, &signed_bid.message, ctx.spec)? + if !is_bid_compatible_with_head(&cached_head, &fork_choice, signed_bid.message(), ctx.spec)? { return Err(PayloadBidError::BidNotCompatibleWithHead { parent_block_root: bid_parent_block_root, @@ -433,27 +442,27 @@ impl GossipVerifiedPayloadBid { // Head compatibility also permits a bid on the head's parent. cached_head.parent_random()? }; - if signed_bid.message.prev_randao != expected_randao { + if signed_bid.message().prev_randao() != expected_randao { return Err(PayloadBidError::InvalidPrevRandao { slot: bid_slot }); } - verify_bid_state_conditions(&signed_bid.message, head_state, ctx.spec)?; + verify_bid_state_conditions(signed_bid.message(), head_state, ctx.spec)?; if parent_payload_exits_builder::( - &signed_bid.message, + signed_bid.message(), &parent_block, head_state, ctx.store, )? { return Err(PayloadBidError::BuilderExitPending { - builder_index: signed_bid.message.builder_index, + builder_index: signed_bid.message().builder_index(), }); } execution_payload_bid_signature_set( head_state, |i| get_builder_pubkey_from_state(head_state, i), - &signed_bid, + signed_bid.to_ref(), ctx.spec, ) .map_err(|_| PayloadBidError::BadSignature)? @@ -496,9 +505,9 @@ impl BeaconChain { &self, bid: Arc>, ) -> Result, PayloadBidError> { - let slot = bid.message.slot; - let parent_block_root = bid.message.parent_block_root; - let parent_block_hash = bid.message.parent_block_hash; + let slot = bid.message().slot(); + let parent_block_root = bid.message().parent_block_root(); + let parent_block_hash = bid.message().parent_block_hash(); let ctx = self.payload_bid_gossip_verification_context(); match GossipVerifiedPayloadBid::new(bid, &ctx) { diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs index 98ad661adf8..935db42a15b 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs @@ -10,7 +10,7 @@ //! GossipVerifiedPayloadBid -------> Insert into GossipVerifiedPayloadBidCache //! ``` -use types::{BeaconStateError, ExecutionBlockHash, Hash256, Slot}; +use types::{BeaconStateError, ExecutionBlockHash, Hash256, InconsistentFork, Slot}; pub mod direct_verified_bid; pub mod gossip_verified_bid; @@ -94,6 +94,8 @@ pub enum PayloadBidError { InvalidPrevRandao { slot: Slot }, /// Some Beacon State error BeaconStateError(BeaconStateError), + /// The bid's variant does not match the fork at its slot. + InconsistentFork(InconsistentFork), /// Internal error InternalError(String), } diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/payload_bid_cache.rs b/beacon_node/beacon_chain/src/payload_bid_verification/payload_bid_cache.rs index 27f09d9be69..01d9c878d26 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/payload_bid_cache.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/payload_bid_cache.rs @@ -7,7 +7,7 @@ use std::{ sync::Arc, }; use types::{ - BuilderIndex, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, Hash256, + BuilderIndex, EthSpec, ExecutionBlockHash, ExecutionPayloadBidRef, Hash256, SignedExecutionPayloadBid, Slot, }; @@ -19,10 +19,10 @@ pub struct BidParent { } impl BidParent { - pub fn from_bid(bid: &ExecutionPayloadBid) -> Self { + pub fn from_bid(bid: ExecutionPayloadBidRef<'_, E>) -> Self { Self { - parent_block_hash: bid.parent_block_hash, - parent_block_root: bid.parent_block_root, + parent_block_hash: bid.parent_block_hash(), + parent_block_root: bid.parent_block_root(), } } } @@ -90,14 +90,14 @@ impl GossipVerifiedPayloadBidCache { /// Returns `true` if the bid became the new highest bid for its parent, or `false` if an /// existing cached bid had an equal or greater value and was therefore retained. pub fn observe_bid(&self, bid: GossipVerifiedPayloadBid) -> bool { - let slot = bid.signed_bid.message.slot; - let key = BidParent::from_bid(&bid.signed_bid.message); + let slot = bid.signed_bid.message().slot(); + let key = BidParent::from_bid(bid.signed_bid.message()); let mut inner = self.inner.write(); inner .seen_builder_bids .entry(slot) .or_default() - .insert((key, bid.signed_bid.message.builder_index)); + .insert((key, bid.signed_bid.message().builder_index())); match inner.highest_bid.entry(slot).or_default().entry(key) { hash_map::Entry::Vacant(entry) => { @@ -105,7 +105,7 @@ impl GossipVerifiedPayloadBidCache { true } hash_map::Entry::Occupied(mut entry) => { - if entry.get().signed_bid.message.value >= bid.signed_bid.message.value { + if entry.get().signed_bid.message().value() >= bid.signed_bid.message().value() { return false; } entry.insert(bid); @@ -145,8 +145,8 @@ mod tests { use bls::Signature; use types::{ - ExecutionBlockHash, ExecutionPayloadBid, Hash256, MinimalEthSpec, - SignedExecutionPayloadBid, Slot, + ExecutionBlockHash, ExecutionPayloadBidGloas, Hash256, MinimalEthSpec, + SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, Slot, }; use super::{BidParent, GossipVerifiedPayloadBidCache}; @@ -162,17 +162,19 @@ mod tests { value: u64, ) -> GossipVerifiedPayloadBid { GossipVerifiedPayloadBid { - signed_bid: Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - builder_index, - parent_block_hash, - parent_block_root, - value, - ..ExecutionPayloadBid::default() + signed_bid: Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + slot, + builder_index, + parent_block_hash, + parent_block_root, + value, + ..ExecutionPayloadBidGloas::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }), + )), } } @@ -227,11 +229,19 @@ mod tests { // Each parent tuple keeps its own highest bid. assert_eq!( - cache.get_highest_bid(slot, parent_a).unwrap().message.value, + cache + .get_highest_bid(slot, parent_a) + .unwrap() + .message() + .value(), 100 ); assert_eq!( - cache.get_highest_bid(slot, parent_b).unwrap().message.value, + cache + .get_highest_bid(slot, parent_b) + .unwrap() + .message() + .value(), 50 ); @@ -239,19 +249,27 @@ mod tests { // not touch the other tuple. assert!(!cache.observe_bid(make_gossip_verified(slot, 2, hash_a, root_a, 60))); assert_eq!( - cache.get_highest_bid(slot, parent_a).unwrap().message.value, + cache + .get_highest_bid(slot, parent_a) + .unwrap() + .message() + .value(), 100 ); assert_eq!( - cache.get_highest_bid(slot, parent_b).unwrap().message.value, + cache + .get_highest_bid(slot, parent_b) + .unwrap() + .message() + .value(), 50 ); // A higher bid replaces the cached bid for its tuple. assert!(cache.observe_bid(make_gossip_verified(slot, 3, hash_b, root_b, 70))); let highest_b = cache.get_highest_bid(slot, parent_b).unwrap(); - assert_eq!(highest_b.message.value, 70); - assert_eq!(highest_b.message.builder_index, 3); + assert_eq!(highest_b.message().value(), 70); + assert_eq!(highest_b.message().builder_index(), 3); } #[test] diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs index f861760c26c..a11ba912b15 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -13,10 +13,11 @@ use state_processing::genesis::genesis_block; use store::{HotColdDB, StoreConfig, StoreOp}; use types::{ Address, BuilderExitRequest, ChainSpec, Checkpoint, Domain, Epoch, EthSpec, ExecutionBlockHash, - ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionPayloadHeader, - ExecutionPayloadHeaderFulu, Hash256, MinimalEthSpec, ProposerPreferences, SignedBeaconBlock, - SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedRoot, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, + ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef, + ExecutionPayloadEnvelope, ExecutionPayloadHeader, ExecutionPayloadHeaderFulu, ForkName, + Hash256, MinimalEthSpec, ProposerPreferences, SignedBeaconBlock, SignedExecutionPayloadBid, + SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, SignedExecutionPayloadEnvelope, + SignedProposerPreferences, SignedRoot, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, }; use proto_array::{Block as ProtoBlock, ExecutionStatus}; @@ -177,7 +178,8 @@ impl TestContext { let genesis_bid = state .latest_execution_payload_bid() .expect("should have a Gloas payload bid"); - observed_execution_payloads.insert(genesis_bid.parent_block_hash, genesis_bid.gas_limit); + observed_execution_payloads + .insert(genesis_bid.parent_block_hash(), genesis_bid.gas_limit()); let slot_clock = TestingSlotClock::new( Slot::new(0), @@ -199,7 +201,7 @@ impl TestContext { } } - fn sign_bid(&self, bid: ExecutionPayloadBid) -> Arc> { + fn sign_bid(&self, bid: ExecutionPayloadBidGloas) -> Arc> { let head = self.canonical_head.cached_head(); let state = &head.snapshot.beacon_state; let domain = self.spec.get_domain( @@ -208,12 +210,14 @@ impl TestContext { &state.fork(), state.genesis_validators_root(), ); - let message = bid.signing_root(domain); + let message = ExecutionPayloadBidRef::Gloas(&bid).signing_root(domain); let signature = self.keypairs[bid.builder_index as usize].sk.sign(message); - Arc::new(SignedExecutionPayloadBid { - message: bid, - signature, - }) + Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: bid, + signature, + }, + )) } fn gossip_ctx(&self) -> GossipVerificationContext<'_, T> { @@ -256,20 +260,22 @@ impl TestContext { value: u64, parent_block_root: Hash256, ) -> Arc> { - Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - builder_index, - fee_recipient, - gas_limit, - value, - parent_block_root, - parent_block_hash: self.execution_parent_hash(), - prev_randao: self.expected_prev_randao(), - ..ExecutionPayloadBid::default() + Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + slot, + builder_index, + fee_recipient, + gas_limit, + value, + parent_block_root, + parent_block_hash: self.execution_parent_hash(), + prev_randao: self.expected_prev_randao(), + ..ExecutionPayloadBidGloas::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }) + )) } fn slot_1_proto_block( @@ -447,7 +453,7 @@ fn same_builder_new_parent_tuple_not_blocked() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = ctx.sign_bid(ExecutionPayloadBid { + let bid = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 0, fee_recipient: Address::ZERO, @@ -456,7 +462,7 @@ fn same_builder_new_parent_tuple_not_blocked() { parent_block_root: ctx.genesis_block_root, parent_block_hash: ctx.execution_parent_hash(), prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!( @@ -526,6 +532,52 @@ fn invalid_bid_slot() { )); } +#[test] +fn inconsistent_fork() { + // A Heze bid is only inconsistent with the slot's fork before Heze. + if fork_name_from_env() != Some(ForkName::Gloas) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + + let bid = Arc::new(SignedExecutionPayloadBid::Heze( + SignedExecutionPayloadBidHeze { + message: ExecutionPayloadBidHeze { + slot: Slot::new(1), + ..ExecutionPayloadBidHeze::default() + }, + signature: Signature::empty(), + }, + )); + let result = GossipVerifiedPayloadBid::new(bid, &gossip); + assert!(matches!(result, Err(PayloadBidError::InconsistentFork(_)))); +} + +#[test] +fn inconsistent_fork_at_heze_boundary() { + // A Gloas bid for the first Heze slot, + // possible from a peer still on the Gloas topic. + if fork_name_from_env() != Some(ForkName::Gloas) { + return; + } + let mut ctx = TestContext::new(); + let heze_fork_epoch = Epoch::new(1); + ctx.spec.heze_fork_epoch = Some(heze_fork_epoch); + let gossip = ctx.gossip_ctx(); + + let bid = ctx.make_signed_bid( + heze_fork_epoch.start_slot(E::slots_per_epoch()), + 0, + Address::ZERO, + 30_000_000, + 100, + ctx.genesis_block_root, + ); + let result = GossipVerifiedPayloadBid::new(bid, &gossip); + assert!(matches!(result, Err(PayloadBidError::InconsistentFork(_)))); +} + #[test] fn fee_recipient_mismatch() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { @@ -558,7 +610,7 @@ fn gas_limit_mismatch() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = ctx.sign_bid(ExecutionPayloadBid { + let bid = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 0, fee_recipient: Address::ZERO, @@ -567,7 +619,7 @@ fn gas_limit_mismatch() { parent_block_root: ctx.genesis_block_root, parent_block_hash: ctx.execution_parent_hash(), prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!(result, Err(PayloadBidError::InvalidGasLimit))); @@ -616,18 +668,20 @@ fn block_hash_equals_parent_block_hash() { seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); let parent_block_hash = ctx.execution_parent_hash(); - let bid = Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - gas_limit: 30_000_000, - parent_block_root: ctx.genesis_block_root, - parent_block_hash, - block_hash: parent_block_hash, - prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + let bid = Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + slot, + gas_limit: 30_000_000, + parent_block_root: ctx.genesis_block_root, + parent_block_hash, + block_hash: parent_block_hash, + prev_randao: ctx.expected_prev_randao(), + ..ExecutionPayloadBidGloas::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }); + )); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!( result, @@ -643,12 +697,12 @@ fn exit_test_parent_payload_hash() -> ExecutionBlockHash { ExecutionBlockHash::repeat_byte(0xab) } -fn exit_test_bid(parent_block_hash: ExecutionBlockHash) -> ExecutionPayloadBid { - ExecutionPayloadBid { +fn exit_test_bid(parent_block_hash: ExecutionBlockHash) -> ExecutionPayloadBidGloas { + ExecutionPayloadBidGloas { builder_index: 0, parent_block_root: exit_test_parent_root(), parent_block_hash, - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() } } @@ -672,7 +726,12 @@ fn parent_payload_exits_builder_on_matching_exit_request() { ); let bid = exit_test_bid(exit_test_parent_payload_hash()); - let result = parent_payload_exits_builder::(&bid, &parent_block, head_state, &ctx.store); + let result = parent_payload_exits_builder::( + ExecutionPayloadBidRef::Gloas(&bid), + &parent_block, + head_state, + &ctx.store, + ); assert!(matches!(result, Ok(true)), "got: {result:?}"); } @@ -696,7 +755,12 @@ fn parent_payload_exit_from_other_address_does_not_exit_builder() { ); let bid = exit_test_bid(exit_test_parent_payload_hash()); - let result = parent_payload_exits_builder::(&bid, &parent_block, head_state, &ctx.store); + let result = parent_payload_exits_builder::( + ExecutionPayloadBidRef::Gloas(&bid), + &parent_block, + head_state, + &ctx.store, + ); assert!(matches!(result, Ok(false)), "got: {result:?}"); } @@ -712,7 +776,12 @@ fn parent_payload_exit_check_skipped_when_bid_builds_on_empty_parent() { ctx.slot_1_proto_block(exit_test_parent_root(), exit_test_parent_payload_hash()); let bid = exit_test_bid(ExecutionBlockHash::repeat_byte(0x11)); - let result = parent_payload_exits_builder::(&bid, &parent_block, head_state, &ctx.store); + let result = parent_payload_exits_builder::( + ExecutionPayloadBidRef::Gloas(&bid), + &parent_block, + head_state, + &ctx.store, + ); assert!(matches!(result, Ok(false)), "got: {result:?}"); } @@ -741,7 +810,12 @@ fn parent_payload_exit_check_uses_summary_after_body_pruning() { .expect("should prune payload body"); let bid = exit_test_bid(exit_test_parent_payload_hash()); - let result = parent_payload_exits_builder::(&bid, &parent_block, head_state, &ctx.store); + let result = parent_payload_exits_builder::( + ExecutionPayloadBidRef::Gloas(&bid), + &parent_block, + head_state, + &ctx.store, + ); assert!(matches!(result, Ok(true)), "got: {result:?}"); } @@ -757,7 +831,12 @@ fn parent_payload_exit_check_needs_parent_envelope() { ctx.slot_1_proto_block(exit_test_parent_root(), exit_test_parent_payload_hash()); let bid = exit_test_bid(exit_test_parent_payload_hash()); - let result = parent_payload_exits_builder::(&bid, &parent_block, head_state, &ctx.store); + let result = parent_payload_exits_builder::( + ExecutionPayloadBidRef::Gloas(&bid), + &parent_block, + head_state, + &ctx.store, + ); assert!( matches!( result, @@ -777,18 +856,20 @@ fn execution_payment_nonzero() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - gas_limit: 30_000_000, - execution_payment: 42, - parent_block_root: ctx.genesis_block_root, - parent_block_hash: ctx.execution_parent_hash(), - prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + let bid = Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + slot, + gas_limit: 30_000_000, + execution_payment: 42, + parent_block_root: ctx.genesis_block_root, + parent_block_hash: ctx.execution_parent_hash(), + prev_randao: ctx.expected_prev_randao(), + ..ExecutionPayloadBidGloas::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }); + )); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!( result, @@ -897,7 +978,7 @@ fn bid_state_conditions_reject_uncoverable_bid() { 100, ctx.genesis_block_root, ); - assert!(verify_bid_state_conditions(&coverable.message, state, &ctx.spec).is_ok()); + assert!(verify_bid_state_conditions(coverable.message(), state, &ctx.spec).is_ok()); let uncoverable = ctx.make_signed_bid( slot, @@ -908,7 +989,7 @@ fn bid_state_conditions_reject_uncoverable_bid() { ctx.genesis_block_root, ); assert!(matches!( - verify_bid_state_conditions(&uncoverable.message, state, &ctx.spec), + verify_bid_state_conditions(uncoverable.message(), state, &ctx.spec), Err(PayloadBidError::BuilderCantCoverBid { .. }) )); } @@ -931,7 +1012,7 @@ fn bid_state_conditions_reject_inactive_builder_before_coverage() { ctx.genesis_block_root, ); assert!(matches!( - verify_bid_state_conditions(&bid.message, state, &ctx.spec), + verify_bid_state_conditions(bid.message(), state, &ctx.spec), Err(PayloadBidError::InvalidBuilder { .. }) )); } @@ -966,7 +1047,7 @@ fn bid_state_conditions_reject_wrong_version_before_coverage() { ctx.genesis_block_root, ); assert!(matches!( - verify_bid_state_conditions(&bid.message, &state, &ctx.spec), + verify_bid_state_conditions(bid.message(), &state, &ctx.spec), Err(PayloadBidError::InvalidBuilderVersion { .. }) )); } @@ -1064,21 +1145,23 @@ fn invalid_blob_kzg_commitments() { .map(|_| KzgCommitment::empty_for_testing()) .collect(); - let bid = Arc::new(SignedExecutionPayloadBid { - message: ExecutionPayloadBid { - slot, - builder_index: 0, - fee_recipient: Address::ZERO, - gas_limit: 30_000_000, - value: 0, - parent_block_root: ctx.genesis_block_root, - parent_block_hash: ctx.execution_parent_hash(), - prev_randao: ctx.expected_prev_randao(), - blob_kzg_commitments: ProgressiveVariableList::new(commitments).unwrap(), - ..ExecutionPayloadBid::default() + let bid = Arc::new(SignedExecutionPayloadBid::Gloas( + SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + slot, + builder_index: 0, + fee_recipient: Address::ZERO, + gas_limit: 30_000_000, + value: 0, + parent_block_root: ctx.genesis_block_root, + parent_block_hash: ctx.execution_parent_hash(), + prev_randao: ctx.expected_prev_randao(), + blob_kzg_commitments: ProgressiveVariableList::new(commitments).unwrap(), + ..ExecutionPayloadBidGloas::default() + }, + signature: Signature::empty(), }, - signature: Signature::empty(), - }); + )); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!( result, @@ -1105,7 +1188,7 @@ fn bad_signature() { 0, ctx.genesis_block_root, ); - let bid_parent = BidParent::from_bid(&bid.message); + let bid_parent = BidParent::from_bid(bid.message()); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!(matches!(result, Err(PayloadBidError::BadSignature))); assert!( @@ -1125,7 +1208,7 @@ fn valid_bid_after_empty_genesis_uses_parent_payload_gas_limit() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid = ctx.sign_bid(ExecutionPayloadBid { + let bid = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 0, fee_recipient: Address::ZERO, @@ -1134,7 +1217,7 @@ fn valid_bid_after_empty_genesis_uses_parent_payload_gas_limit() { parent_block_root: ctx.genesis_block_root, parent_block_hash: ctx.execution_parent_hash(), prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result = GossipVerifiedPayloadBid::new(bid, &gossip); assert!( @@ -1160,7 +1243,7 @@ fn valid_bid_with_parent_in_previous_epoch() { let slot = Slot::new(bid_slot); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); let parent_state = &ctx.canonical_head.cached_head().snapshot.beacon_state; - let bid = ctx.sign_bid(ExecutionPayloadBid { + let bid = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 0, fee_recipient: Address::ZERO, @@ -1170,7 +1253,7 @@ fn valid_bid_with_parent_in_previous_epoch() { prev_randao: *parent_state .get_randao_mix(parent_state.current_epoch()) .unwrap(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result = GossipVerifiedPayloadBid::new(bid, &ctx.gossip_ctx()); assert!( @@ -1190,7 +1273,7 @@ fn two_builders_coexist_in_cache() { let slot = Slot::new(1); seed_preferences(&ctx, slot, Address::ZERO, 30_000_000); - let bid_0 = ctx.sign_bid(ExecutionPayloadBid { + let bid_0 = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 0, fee_recipient: Address::ZERO, @@ -1199,7 +1282,7 @@ fn two_builders_coexist_in_cache() { parent_block_root: ctx.genesis_block_root, parent_block_hash: ctx.execution_parent_hash(), prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result_0 = GossipVerifiedPayloadBid::new(bid_0, &gossip); assert!( @@ -1209,7 +1292,7 @@ fn two_builders_coexist_in_cache() { ); // Builder 1 must bid strictly higher than builder 0's cached value. - let bid_1 = ctx.sign_bid(ExecutionPayloadBid { + let bid_1 = ctx.sign_bid(ExecutionPayloadBidGloas { slot, builder_index: 1, fee_recipient: Address::ZERO, @@ -1218,7 +1301,7 @@ fn two_builders_coexist_in_cache() { parent_block_root: ctx.genesis_block_root, parent_block_hash: ctx.execution_parent_hash(), prev_randao: ctx.expected_prev_randao(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }); let result_1 = GossipVerifiedPayloadBid::new(bid_1, &gossip); assert!( @@ -1245,8 +1328,8 @@ fn two_builders_coexist_in_cache() { .bid_cache .get_highest_bid(slot, bid_parent) .expect("should have highest bid"); - assert_eq!(highest.message.value, 1); - assert_eq!(highest.message.builder_index, 1); + assert_eq!(highest.message().value(), 1); + assert_eq!(highest.message().builder_index(), 1); } #[test] diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs index f3b1faae2d3..fcfa5d8c595 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs @@ -7,8 +7,9 @@ use store::DatabaseBlock; use tracing::debug; use tree_hash::TreeHash; use types::{ - ChainSpec, EthSpec, ExecutionPayloadBid, ExecutionPayloadEnvelope, Hash256, SignedBeaconBlock, - SignedExecutionPayloadEnvelope, Slot, consts::gloas::BUILDER_INDEX_SELF_BUILD, + ChainSpec, EthSpec, ExecutionPayloadBidRef, ExecutionPayloadEnvelope, Hash256, + SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, + consts::gloas::BUILDER_INDEX_SELF_BUILD, }; use crate::payload_envelope_verification::observed_payload_envelopes::ObservedPayloadEnvelopes; @@ -45,7 +46,7 @@ pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub(crate) fn verify_envelope_consistency( envelope: &ExecutionPayloadEnvelope, block: &SignedBeaconBlock, - execution_bid: &ExecutionPayloadBid, + execution_bid: ExecutionPayloadBidRef, latest_finalized_slot: Slot, ) -> Result<(), EnvelopeError> { // Check that the envelope's slot isn't from a slot prior @@ -66,17 +67,17 @@ pub(crate) fn verify_envelope_consistency( } // Builder index matches committed bid. - if envelope.builder_index != execution_bid.builder_index { + if envelope.builder_index != execution_bid.builder_index() { return Err(EnvelopeError::BuilderIndexMismatch { - committed_bid: execution_bid.builder_index, + committed_bid: execution_bid.builder_index(), envelope: envelope.builder_index, }); } // The block hash should match the block hash of the execution bid. - if envelope.payload.block_hash != execution_bid.block_hash { + if envelope.payload.block_hash != execution_bid.block_hash() { return Err(EnvelopeError::BlockHashMismatch { - committed_bid: execution_bid.block_hash, + committed_bid: execution_bid.block_hash(), envelope: envelope.payload.block_hash, }); } @@ -121,9 +122,9 @@ pub(crate) fn verify_envelope_consistency( // The SSZ root of the envelope's execution requests must match the committed bid, per // `verify_execution_payload_envelope` in the spec. let execution_requests_root = requests.tree_hash_root(); - if execution_requests_root != execution_bid.execution_requests_root { + if execution_requests_root != execution_bid.execution_requests_root() { return Err(EnvelopeError::ExecutionRequestsRootMismatch { - committed_bid: execution_bid.execution_requests_root, + committed_bid: execution_bid.execution_requests_root(), envelope: execution_requests_root, }); } @@ -200,11 +201,11 @@ impl GossipVerifiedEnvelope { ))); } }; - let execution_bid = &block + let execution_bid = block .message() .body() .signed_execution_payload_bid()? - .message; + .message(); verify_envelope_consistency(envelope, &block, execution_bid, latest_finalized_slot)?; @@ -421,9 +422,10 @@ mod tests { use types::{ Address, BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BuilderDepositRequest, BuilderExitRequest, ConsolidationRequest, Eth1Data, EthSpec, ExecutionBlockHash, - ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionPayloadGloas, - ExecutionRequestsGloas, Graffiti, Hash256, MinimalEthSpec, SignedBeaconBlock, - SignedExecutionPayloadBid, Slot, SyncAggregate, Withdrawal, WithdrawalRequest, + ExecutionPayloadBid, ExecutionPayloadBidGloas, ExecutionPayloadBidRef, + ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequestsGloas, Graffiti, Hash256, + MinimalEthSpec, SignedBeaconBlock, SignedExecutionPayloadBidGloas, Slot, SyncAggregate, + Withdrawal, WithdrawalRequest, }; use super::verify_envelope_consistency; @@ -472,7 +474,7 @@ mod tests { sync_aggregate: SyncAggregate::empty(), bls_to_execution_changes: ProgressiveVariableList::empty(), parent_execution_requests: ExecutionRequestsGloas::default(), - signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBidGloas::empty(), payload_attestations: ProgressiveVariableList::empty(), _phantom: PhantomData, }, @@ -481,13 +483,13 @@ mod tests { } fn make_bid(builder_index: u64, block_hash: ExecutionBlockHash) -> ExecutionPayloadBid { - ExecutionPayloadBid { + ExecutionPayloadBid::Gloas(ExecutionPayloadBidGloas { builder_index, block_hash, // Commit to the (default) execution requests carried by `make_envelope`. execution_requests_root: ExecutionRequestsGloas::::default().tree_hash_root(), - ..ExecutionPayloadBid::default() - } + ..ExecutionPayloadBidGloas::default() + }) } #[test] @@ -500,7 +502,9 @@ mod tests { let block = make_block(slot); let bid = make_bid(builder_index, block_hash); - assert!(verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)).is_ok()); + assert!( + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)).is_ok() + ); } #[test] @@ -514,8 +518,12 @@ mod tests { let bid = make_bid(builder_index, block_hash); let latest_finalized_slot = Slot::new(10); - let result = - verify_envelope_consistency::(&envelope, &block, &bid, latest_finalized_slot); + let result = verify_envelope_consistency::( + &envelope, + &block, + bid.to_ref(), + latest_finalized_slot, + ); assert!(matches!( result, Err(EnvelopeError::PriorToFinalization { .. }) @@ -531,7 +539,8 @@ mod tests { let block = make_block(Slot::new(20)); let bid = make_bid(builder_index, block_hash); - let result = verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)); + let result = + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)); assert!(matches!(result, Err(EnvelopeError::SlotMismatch { .. }))); } @@ -544,7 +553,8 @@ mod tests { let block = make_block(slot); let bid = make_bid(2, block_hash); - let result = verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)); + let result = + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)); assert!(matches!( result, Err(EnvelopeError::BuilderIndexMismatch { .. }) @@ -566,7 +576,9 @@ mod tests { // Not a gossip condition; the sync-only rejection is inlined in // `RangeSyncBlock` construction. - assert!(verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)).is_ok()); + assert!( + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)).is_ok() + ); } #[test] @@ -578,7 +590,8 @@ mod tests { let block = make_block(slot); let bid = make_bid(builder_index, ExecutionBlockHash::repeat_byte(0xff)); - let result = verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)); + let result = + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)); assert!(matches!( result, Err(EnvelopeError::BlockHashMismatch { .. }) @@ -604,7 +617,9 @@ mod tests { let max = E::max_withdrawals_per_payload(); envelope.payload.withdrawals = ProgressiveVariableList::new(vec![withdrawal.clone(); max]).unwrap(); - assert!(verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)).is_ok()); + assert!( + verify_envelope_consistency::(&envelope, &block, bid.to_ref(), Slot::new(0)).is_ok() + ); assert!( ProgressiveVariableList::::MaxWithdrawalsPerPayload>::new( @@ -627,14 +642,20 @@ mod tests { let block = make_block(slot); set_len(&mut envelope.execution_requests, max).unwrap(); - let bid = ExecutionPayloadBid { + let bid = ExecutionPayloadBidGloas { builder_index, block_hash, execution_requests_root: envelope.execution_requests.tree_hash_root(), - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }; assert!( - verify_envelope_consistency::(&envelope, &block, &bid, Slot::new(0)).is_ok(), + verify_envelope_consistency::( + &envelope, + &block, + ExecutionPayloadBidRef::Gloas(&bid), + Slot::new(0) + ) + .is_ok(), "{kind} at max should be accepted" ); diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs index b86e6fd8753..4013bb27525 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -61,7 +61,7 @@ impl BeaconChain { .message() .body() .signed_execution_payload_bid()? - .clone(), + .clone_as_signed_execution_payload_bid(), ); // Set observed time if not already set. Usually this should be set by gossip or RPC, diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs index 885e8a37acc..c66fbdbb731 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -30,7 +30,7 @@ use strum::AsRefStr; use tracing::{instrument, warn}; use types::{ BeaconState, BeaconStateError, BuilderIndex, DataColumnSidecarList, EthSpec, - ExecutionBlockHash, ExecutionPayloadEnvelope, Hash256, SignedExecutionPayloadBid, + ExecutionBlockHash, ExecutionPayloadEnvelope, Hash256, SignedExecutionPayloadBidRef, SignedExecutionPayloadEnvelope, Slot, }; @@ -70,7 +70,7 @@ impl AvailableEnvelope { pub fn new( envelope: Arc>, columns: DataColumnSidecarList, - bid: &SignedExecutionPayloadBid, + bid: SignedExecutionPayloadBidRef, custody_context: &CustodyContext, ) -> Result where @@ -409,10 +409,10 @@ pub fn build_new_payload_request<'a, E: EthSpec>( .body() .signed_execution_payload_bid() .map_err(|e| EnvelopeError::BeaconChainError(Box::new(BeaconChainError::from(e))))? - .message; + .message(); let versioned_hashes = bid - .blob_kzg_commitments + .blob_kzg_commitments() .iter() .map(state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash) .collect(); @@ -452,7 +452,7 @@ mod payload_hash_tests { use types::{ BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, Eth1Data, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionPayloadRef, ExecutionRequestsGloas, ExecutionRequestsRef, - Graffiti, Hash256, MinimalEthSpec, SignedBeaconBlock, SignedExecutionPayloadBid, + Graffiti, Hash256, MinimalEthSpec, SignedBeaconBlock, SignedExecutionPayloadBidGloas, SignedExecutionPayloadEnvelope, Slot, SyncAggregate, }; @@ -480,7 +480,7 @@ mod payload_hash_tests { sync_aggregate: SyncAggregate::empty(), bls_to_execution_changes: ProgressiveVariableList::empty(), parent_execution_requests: ExecutionRequestsGloas::default(), - signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBidGloas::empty(), payload_attestations: ProgressiveVariableList::empty(), _phantom: PhantomData, }, diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs index b16de37d331..ce5667fcece 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs @@ -72,15 +72,15 @@ impl PayloadNotifier { envelope: &'a SignedExecutionPayloadEnvelope, block: &'a SignedBeaconBlock, ) -> Result, PayloadVerificationError> { - let bid = &block + let bid = block .message() .body() .signed_execution_payload_bid() .map_err(|e| PayloadVerificationError::BeaconChainError(Box::new(e.into())))? - .message; + .message(); let versioned_hashes = bid - .blob_kzg_commitments + .blob_kzg_commitments() .iter() .map(kzg_commitment_to_versioned_hash) .collect(); diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 8846d570ae4..09bd2115667 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -41,7 +41,6 @@ use crate::metrics::{ use crate::observed_data_sidecars::ObservationStrategy; use crate::partial_data_column_assembler::PartialMergeResult; use pending_components::{PendingComponents, ReconstructColumnsDecision}; -use types::execution::SignedExecutionProof; use types::{SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope}; /// The LRU Cache stores `PendingComponents`, which store the block root, the execution payload bid, and its associated column data. @@ -52,15 +51,6 @@ use types::{SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope}; /// eviction to prevent race conditions (#7961), so we expect this cache to be full all the time. const AVAILABILITY_CACHE_CAPACITY: usize = 32; -/// Distinct proof systems that must prove a payload before import. More than one means a -/// soundness bug in a single prover isn't enough to fool us. -/// -/// Which systems count is the engine's call, we only see its `VALID`. The count is ours because -/// we're the only ones who see the whole set. -/// -/// TODO(9658): make configurable. https://github.com/sigp/lighthouse/issues/9658 -pub const REQUIRED_EXECUTION_PROOFS: usize = 2; - /// What `update_pending_components` returns: the value that the update closure computed, next to /// a read guard on the entry it updated. type UpdatedComponents<'a, E, R> = (R, MappedRwLockReadGuard<'a, PendingComponents>); @@ -111,8 +101,6 @@ pub struct PendingPayloadCache { kzg: Arc, custody_context: Arc>, disable_get_blobs: bool, - /// Distinct proof systems required before a payload is available. Zero disables the gate. - required_execution_proofs: usize, spec: Arc, } @@ -121,7 +109,6 @@ impl PendingPayloadCache { kzg: Arc, custody_context: Arc>, disable_get_blobs: bool, - required_execution_proofs: usize, spec: Arc, ) -> Result { Ok(Self { @@ -129,7 +116,6 @@ impl PendingPayloadCache { kzg, custody_context, disable_get_blobs, - required_execution_proofs, spec, }) } @@ -268,8 +254,7 @@ impl PendingPayloadCache { pending_components.span.in_scope(|| { debug!( component = "executed envelope", - status = pending_components - .status_str(&self.custody_context, self.required_execution_proofs), + status = pending_components.status_str(&self.custody_context), "Component added to data availability checker" ); }); @@ -286,39 +271,6 @@ impl PendingPayloadCache { .or_insert_with(|| PendingComponents::new(block_root, bid)); } - /// Cache an execution proof and check availability. - /// - /// Only the proof that reaches the requirement can unblock the payload, so repeats and - /// extras skip the check rather than importing twice. - pub(crate) fn put_execution_proof( - &self, - proof: Arc, - bid: &Arc>, - ) -> Result, AvailabilityCheckError> { - let block_root = proof.beacon_block_root(); - - let (inserted, pending_components) = - self.update_pending_components(block_root, bid, |pending_components| { - pending_components.insert_execution_proof(proof) - })?; - - pending_components.span.in_scope(|| { - debug!( - component = "execution proof", - status = pending_components - .status_str(&self.custody_context, self.required_execution_proofs), - "Component added to data availability checker" - ); - }); - - match inserted { - Some(count) if count <= self.required_execution_proofs => { - self.check_availability(block_root, pending_components) - } - _ => Ok(Availability::MissingComponents(block_root)), - } - } - /// Perform KZG verification on RPC custody columns and insert them into the cache. /// After insertion check if the envelope becomes available. #[instrument(skip_all, level = "trace")] @@ -330,12 +282,12 @@ impl PendingPayloadCache { ) -> Result, AvailabilityCheckError> { let kzg_verified_columns = KzgVerifiedDataColumn::from_batch_with_scoring_and_commitments( custody_columns, - &bid.message.blob_kzg_commitments, + bid.message().blob_kzg_commitments(), &self.kzg, ) .map_err(AvailabilityCheckError::InvalidColumn)?; - let epoch = bid.message.slot.epoch(T::EthSpec::slots_per_epoch()); + let epoch = bid.message().slot().epoch(T::EthSpec::slots_per_epoch()); let sampling_columns = self.custody_context.sampling_columns_for_epoch(epoch); let verified_custody_columns = kzg_verified_columns .into_iter() @@ -361,7 +313,7 @@ impl PendingPayloadCache { let bid = self .get_bid(&block_root) .ok_or(AvailabilityCheckError::MissingBid(block_root))?; - let epoch = bid.message.slot.epoch(T::EthSpec::slots_per_epoch()); + let epoch = bid.message().slot().epoch(T::EthSpec::slots_per_epoch()); let sampling_columns = self.custody_context.sampling_columns_for_epoch(epoch); let custody_columns = data_columns .into_iter() @@ -460,8 +412,7 @@ impl PendingPayloadCache { pending_components.span.in_scope(|| { debug!( component = "data_columns", - status = pending_components - .status_str(&self.custody_context, self.required_execution_proofs), + status = pending_components.status_str(&self.custody_context), "Component added to data availability checker" ); }); @@ -495,7 +446,7 @@ impl PendingPayloadCache { let all_data_columns = KzgVerifiedCustodyDataColumn::reconstruct_columns( &self.kzg, verified_data_columns, - &bid.message.blob_kzg_commitments, + bid.message().blob_kzg_commitments(), &self.spec, ) .map_err(|e| { @@ -509,7 +460,7 @@ impl PendingPayloadCache { AvailabilityCheckError::ReconstructColumnsError(e) })?; - let slot = bid.message.slot; + let slot = bid.message().slot(); let columns_to_sample = self .custody_context() .sampling_columns_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch())); @@ -565,8 +516,8 @@ impl PendingPayloadCache { block_root: Hash256, pending_components: MappedRwLockReadGuard<'_, PendingComponents>, ) -> Result, AvailabilityCheckError> { - if let Some(available_envelope) = pending_components - .make_available(&self.custody_context, self.required_execution_proofs)? + if let Some(available_envelope) = + pending_components.make_available(&self.custody_context)? { // Explicitly drop read lock before acquiring write lock drop(pending_components); @@ -696,7 +647,6 @@ mod data_availability_checker_tests { use slot_clock::{SlotClock, TestingSlotClock}; use ssz_types::ProgressiveVariableList; use std::time::Duration; - use types::execution::{ExecutionProof, ProofData, ProofType, PublicInput}; use types::{ Cell, CellBitmap, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequestsGloas, ForkName, MinimalEthSpec, PartialDataColumnGloas, PartialDataColumnSidecarGloas, @@ -718,24 +668,7 @@ mod data_availability_checker_tests { setup_with(node_custody, NumBlobs::Number(0)) } - /// `setup`, with the proof gate on. - fn setup_gated(node_custody: NodeCustodyType) -> Setup { - setup_full( - node_custody, - NumBlobs::Number(NUM_BLOBS), - REQUIRED_EXECUTION_PROOFS, - ) - } - fn setup_with(node_custody: NodeCustodyType, num_blobs: NumBlobs) -> Setup { - setup_full(node_custody, num_blobs, 0) - } - - fn setup_full( - node_custody: NodeCustodyType, - num_blobs: NumBlobs, - required_execution_proofs: usize, - ) -> Setup { create_test_tracing_subscriber(); let spec = Arc::new(ForkName::Gloas.make_genesis_spec(E::default_spec())); let kzg = get_kzg(&spec); @@ -753,14 +686,8 @@ mod data_availability_checker_tests { spec.clone(), )); let cache = Arc::new( - PendingPayloadCache::::new( - kzg, - custody_context, - false, - required_execution_proofs, - spec.clone(), - ) - .expect("create cache"), + PendingPayloadCache::::new(kzg, custody_context, false, spec.clone()) + .expect("create cache"), ); let mut u = test_unstructured(); @@ -774,11 +701,11 @@ mod data_availability_checker_tests { .body() .signed_execution_payload_bid() .expect("Gloas block has bid") - .clone(), + .clone_as_signed_execution_payload_bid(), ); cache.insert_bid(block_root, bid.clone()); - let epoch = bid.message.slot.epoch(E::slots_per_epoch()); + let epoch = bid.message().slot().epoch(E::slots_per_epoch()); let sampling = cache.custody_context().sampling_columns_for_epoch(epoch); let custody = columns .into_iter() @@ -822,29 +749,6 @@ mod data_availability_checker_tests { .cached_data_column_indexes(&self.block_root) .expect("entry") } - - fn put_proof(&self, proof_type: ProofType) -> Availability { - let bid = self.cache.get_bid(&self.block_root).expect("bid"); - self.cache - .put_execution_proof(Arc::new(execution_proof(self.block_root, proof_type)), &bid) - .expect("put execution proof") - } - } - - /// Unverified proof: the cache only reads `beacon_block_root` and `proof_type`. - fn execution_proof(block_root: Hash256, proof_type: ProofType) -> SignedExecutionProof { - SignedExecutionProof { - message: ExecutionProof { - proof_data: ProofData::new(vec![1_u8]).expect("proof data"), - proof_type, - public_input: PublicInput { - new_payload_request_root: Hash256::random(), - }, - beacon_block_root: block_root, - }, - validator_index: 0, - signature: bls::Signature::infinity().expect("infinity sig"), - } } /// Hand-rolled executed envelope with bypassed verification; the cache only inspects @@ -1015,33 +919,6 @@ mod data_availability_checker_tests { assert_lengths_match(&s); } - // ─── Tier 4: EIP-8025 execution proof gating ──────────────────────────── - - /// Envelope and columns alone don't import: the gate wants proofs from - /// `REQUIRED_EXECUTION_PROOFS` distinct provers. Repeats of one type don't count, and a proof - /// beyond the requirement doesn't import the envelope a second time. - #[tokio::test] - async fn execution_proof_gates_availability() { - let s = setup_gated(NodeCustodyType::Fullnode); - s.put_envelope(); - assert_missing(s.put_columns(s.custody.clone())); - - // One prover, however many proofs, is never enough. - for _ in 0..=REQUIRED_EXECUTION_PROOFS { - assert_missing(s.put_proof(0)); - } - - // Distinct provers up to the requirement flip it to available. - let mut availability = None; - for proof_type in 1..REQUIRED_EXECUTION_PROOFS { - availability = Some(s.put_proof(proof_type as ProofType)); - } - let envelope = assert_available(availability.expect("gate needs two provers or more")); - assert_eq!(envelope.block_root, s.block_root); - - assert_missing(s.put_proof(REQUIRED_EXECUTION_PROOFS as ProofType)); - } - // ────────── Gloas partial column merge ───────────────────────────────── // // `merge_partial_data_columns` accumulates cells from partial (incomplete) columns until each @@ -1085,7 +962,7 @@ mod data_availability_checker_tests { async fn merge_partial_columns_completes_column_across_arrivals() { let s = setup_with(NodeCustodyType::Fullnode, NumBlobs::Number(2)); let bid = s.cache.get_bid(&s.block_root).expect("bid"); - let slot = bid.message.slot; + let slot = bid.message().slot(); s.cache.availability_cache.write().remove(&s.block_root); assert!(s.cache.get_bid(&s.block_root).is_none()); diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index ed327a02073..988566a9185 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -14,7 +14,6 @@ use std::collections::HashMap; use std::sync::Arc; use tracing::{Span, debug, debug_span}; use types::DataColumnSidecar; -use types::execution::{ProofType, SignedExecutionProof}; use types::{ColumnIndex, EthSpec, Hash256, SignedExecutionPayloadBid}; /// This represents the components of a payload pending data availability. @@ -28,9 +27,6 @@ pub struct PendingComponents { pub envelope: Option>, /// A column entry in this map may only have some cells filled in (i.e. a partial data column) pub verified_data_columns: HashMap>, - /// Execution proofs keyed by proof type, so repeats from one prover count once. Empty - /// without a proof engine. - pub execution_proofs: HashMap>, pub reconstruction_started: bool, /// True once the local `getBlobs` attempt has settled. It is set whether or not the EL /// returned anything. Until then, republished partials request no cells, because the EL @@ -44,7 +40,7 @@ impl PendingComponents { where T: BeaconChainTypes, { - if custody_context.data_columns_required_for_bid(&self.bid) { + if custody_context.data_columns_required_for_bid(self.bid.to_ref()) { custody_context.num_of_data_columns_to_sample(self.bid.epoch()) } else { 0 @@ -53,7 +49,7 @@ impl PendingComponents { /// Returns columns that have all cells present. pub fn get_cached_data_columns(&self) -> Vec>> { - let slot = self.bid.message.slot; + let slot = self.bid.message().slot(); let block_root = self.block_root; self.verified_data_columns .iter() @@ -63,7 +59,7 @@ impl PendingComponents { /// Returns the indices of columns that have all cells present. pub fn get_cached_data_columns_indices(&self) -> Vec { - let slot = self.bid.message.slot; + let slot = self.bid.message().slot(); let block_root = self.block_root; self.verified_data_columns .iter() @@ -80,7 +76,7 @@ impl PendingComponents { &self, ) -> Vec> { let block_root = self.block_root; - let slot = self.bid.message.slot; + let slot = self.bid.slot(); self.verified_data_columns .iter() .filter_map(|(idx, col)| { @@ -166,7 +162,7 @@ impl PendingComponents { outcome: PartialColumnsMergeOutcome, disable_get_blobs: bool, ) -> PartialMergeResult { - let slot = self.bid.message.slot; + let slot = self.bid.slot(); let full_columns = outcome .newly_complete @@ -201,19 +197,6 @@ impl PendingComponents { } } - /// Inserts an execution proof, returning the distinct proof type count, or `None` if we - /// already had this type. - pub fn insert_execution_proof(&mut self, proof: Arc) -> Option { - if self - .execution_proofs - .insert(proof.proof_type(), proof) - .is_some() - { - return None; - } - Some(self.execution_proofs.len()) - } - /// Inserts an executed payload envelope into the cache. pub fn insert_executed_payload_envelope( &mut self, @@ -232,11 +215,10 @@ impl PendingComponents { self.num_completed_columns() >= num_expected_columns } - /// Returns `Some` once the envelope, the required columns and enough proofs have arrived. + /// Returns `Some` if the envelope and all required data columns have been received. pub fn make_available( &self, custody_context: &CustodyContext, - required_execution_proofs: usize, ) -> Result>, AvailabilityCheckError> where T: BeaconChainTypes, @@ -246,11 +228,6 @@ impl PendingComponents { return Ok(None); }; - // Proofs are recursive, so we only need them for this payload, not its ancestors. - if self.execution_proofs.len() < required_execution_proofs { - return Ok(None); - } - let AvailabilityPendingExecutedEnvelope { envelope, block_root, @@ -291,8 +268,12 @@ impl PendingComponents { } }; - let available_envelope = - AvailableEnvelope::new(envelope.clone(), columns, &self.bid, custody_context)?; + let available_envelope = AvailableEnvelope::new( + envelope.clone(), + columns, + self.bid.to_ref(), + custody_context, + )?; Ok(Some(AvailableExecutedEnvelope { envelope: available_envelope, @@ -310,38 +291,23 @@ impl PendingComponents { bid, envelope: None, verified_data_columns: HashMap::new(), - execution_proofs: HashMap::new(), reconstruction_started: false, local_fetch_settled: false, span, } } - pub fn status_str( - &self, - custody_context: &CustodyContext, - required_execution_proofs: usize, - ) -> String + pub fn status_str(&self, custody_context: &CustodyContext) -> String where T: BeaconChainTypes, { let num_columns_required = self.num_columns_required(custody_context); - if required_execution_proofs == 0 { - format!( - "envelope {}, data_columns {}/{}", - self.envelope.is_some(), - self.num_completed_columns(), - num_columns_required - ) - } else { - format!( - "envelope {}, data_columns {}/{}, execution_proofs {}", - self.envelope.is_some(), - self.num_completed_columns(), - num_columns_required, - self.execution_proofs.len() - ) - } + format!( + "envelope {}, data_columns {}/{}", + self.envelope.is_some(), + self.num_completed_columns(), + num_columns_required + ) } } diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 4fd3b4eb018..33651b85fcc 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2997,30 +2997,28 @@ where .is_ok_and(|c| !c.is_empty()); let is_available = !has_blob_commitments || blob_items.is_some(); let block_hash: SignedBeaconBlockHash = if !is_available { - self.chain - .process_block( - block_root, - LookupBlock::new(block), - NotifyExecutionLayer::Yes, - BlockImportSource::Lookup, - || Ok(()), - ) - .await? - .try_into() - .expect("block blobs are available") + Box::pin(self.chain.process_block( + block_root, + LookupBlock::new(block), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + )) + .await? + .try_into() + .expect("block blobs are available") } else { let range_sync_block = self.build_range_sync_block_from_blobs(block, blob_items)?; - self.chain - .process_block( - block_root, - range_sync_block, - NotifyExecutionLayer::Yes, - BlockImportSource::RangeSync, - || Ok(()), - ) - .await? - .try_into() - .expect("block blobs are available") + Box::pin(self.chain.process_block( + block_root, + range_sync_block, + NotifyExecutionLayer::Yes, + BlockImportSource::RangeSync, + || Ok(()), + )) + .await? + .try_into() + .expect("block blobs are available") }; self.chain.recompute_head_at_current_slot().await; @@ -3044,29 +3042,27 @@ where let is_available = !has_blob_commitments || blob_items.is_some(); let block_hash: SignedBeaconBlockHash = if is_available { let range_sync_block = self.build_range_sync_block_from_blobs(block, blob_items)?; - self.chain - .process_block( - block_root, - range_sync_block, - NotifyExecutionLayer::Yes, - BlockImportSource::RangeSync, - || Ok(()), - ) - .await? - .try_into() - .expect("block blobs are available") + Box::pin(self.chain.process_block( + block_root, + range_sync_block, + NotifyExecutionLayer::Yes, + BlockImportSource::RangeSync, + || Ok(()), + )) + .await? + .try_into() + .expect("block blobs are available") } else { - self.chain - .process_block( - block_root, - LookupBlock::new(block), - NotifyExecutionLayer::Yes, - BlockImportSource::Lookup, - || Ok(()), - ) - .await? - .try_into() - .expect("block blobs are available") + Box::pin(self.chain.process_block( + block_root, + LookupBlock::new(block), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + )) + .await? + .try_into() + .expect("block blobs are available") }; self.chain.recompute_head_at_current_slot().await; @@ -3104,15 +3100,15 @@ where .expect("should read block from store") .expect("block should exist in store"); - let bid = &block + let bid = block .message() .body() .signed_execution_payload_bid() .expect("Gloas block should have a payload bid") - .message; + .message(); let versioned_hashes = bid - .blob_kzg_commitments + .blob_kzg_commitments() .iter() .map(kzg_commitment_to_versioned_hash) .collect(); @@ -3421,7 +3417,7 @@ where > { self.set_current_slot(slot); let (block_contents, opt_envelope, new_state) = - self.make_block_with_envelope(state, slot).await; + Box::pin(self.make_block_with_envelope(state, slot)).await; let block_hash = self .process_block( @@ -4218,13 +4214,13 @@ pub fn generate_data_column_sidecars_from_block( // Load the precomputed column sidecar to avoid computing them for every block in the tests. // Then repeat the cells and proofs for every blob if block.fork_name_unchecked().gloas_enabled() { - let kzg_commitments = &block + let kzg_commitments = block .message() .body() .signed_execution_payload_bid() .expect("Gloas block should have a payload bid") - .message - .blob_kzg_commitments; + .message() + .blob_kzg_commitments(); if kzg_commitments.is_empty() { return vec![]; } diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index c3ff55da024..e081c5b0fda 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -3066,7 +3066,6 @@ async fn process_chain_segment_rejects_envelope_with_invalid_signature() { .body() .signed_execution_payload_bid() .unwrap(); - let available_envelope = AvailableEnvelope::new( Arc::new(envelope), columns, diff --git a/beacon_node/beacon_chain/tests/column_verification.rs b/beacon_node/beacon_chain/tests/column_verification.rs index 180e187e900..aebd3c20842 100644 --- a/beacon_node/beacon_chain/tests/column_verification.rs +++ b/beacon_node/beacon_chain/tests/column_verification.rs @@ -158,7 +158,7 @@ async fn gloas_envelope_blobs_produce_valid_columns() { .signed_execution_payload_bid() .expect("Gloas block should have a payload bid"); assert!( - !bid.message.blob_kzg_commitments.is_empty(), + !bid.message().blob_kzg_commitments().is_empty(), "Block should have blob KZG commitments" ); diff --git a/beacon_node/beacon_chain/tests/envelope_verification.rs b/beacon_node/beacon_chain/tests/envelope_verification.rs index e85f53430c9..04fcdd025e4 100644 --- a/beacon_node/beacon_chain/tests/envelope_verification.rs +++ b/beacon_node/beacon_chain/tests/envelope_verification.rs @@ -1,6 +1,5 @@ use beacon_chain::AvailabilityProcessingStatus::{Imported, MissingComponents}; use beacon_chain::NotifyExecutionLayer; -use beacon_chain::execution_proof_verification::GossipVerifiedExecutionProof; use beacon_chain::payload_envelope_verification::{EnvelopeError, EnvelopeSource}; use beacon_chain::test_utils::{ BeaconChainHarness, fork_name_from_env, generate_data_column_sidecars_from_block, test_spec, @@ -9,7 +8,6 @@ use bls::PublicKeyBytes; use eth2::types::EventKind; use proto_array::ExecutionStatus; use std::sync::Arc; -use types::execution::{ExecutionProof, ProofData, PublicInput, SignedExecutionProof}; use types::{ Address, BlockImportSource, Epoch, ExecPayload, ForkName, Hash256, MinimalEthSpec, Slot, WithdrawalRequest, @@ -100,8 +98,8 @@ async fn startup_seeds_gloas_genesis_parent_payload() { harness .chain .observed_execution_payloads - .get_gas_limit(genesis_bid.parent_block_hash), - Some(genesis_bid.gas_limit) + .get_gas_limit(genesis_bid.parent_block_hash()), + Some(genesis_bid.gas_limit()) ); } @@ -162,30 +160,6 @@ async fn lookup_imports_gloas_payload_after_restart() { cache.get_bid(&block_root).is_none(), "the pending bid cache should start empty after restart" ); - let proof_status = chain - .check_execution_proof_availability_and_import(GossipVerifiedExecutionProof { - proof: Arc::new(SignedExecutionProof { - message: ExecutionProof { - proof_data: ProofData::new(vec![1]).expect("proof data"), - proof_type: 0, - public_input: PublicInput { - new_payload_request_root: Hash256::random(), - }, - beacon_block_root: block_root, - }, - validator_index: 0, - signature: bls::Signature::infinity().expect("infinity signature"), - }), - block_slot: target_slot, - }) - .await - .expect("execution proof should be accepted after restart"); - assert!(matches!(proof_status, MissingComponents(..))); - assert!(cache.get_bid(&block_root).is_some()); - - // Evict the recovered bid so columns must also handle a cache miss. - cache.do_maintenance(Epoch::new(1)).unwrap(); - assert!(cache.get_bid(&block_root).is_none()); let column_status = chain .process_rpc_custody_columns(custody_columns.clone()) .await diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index bcb378783ea..991360098b6 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -10,7 +10,8 @@ use types::data::FixedBlobSidecarList; use types::{ Address, BlobSidecar, DataColumnSidecar, DataColumnSidecarFulu, DataColumnSidecarGloas, Domain, EthSpec, MinimalEthSpec, PayloadAttestationData, PayloadAttestationMessage, - ProposerPreferences, SignedExecutionPayloadBid, SignedProposerPreferences, SignedRoot, Slot, + ProposerPreferences, SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, + SignedProposerPreferences, SignedRoot, Slot, }; type E = MinimalEthSpec; @@ -50,12 +51,12 @@ async fn data_column_sidecar_event_on_process_gossip_data_column() { .sampling_columns_for_epoch(epoch)[0]; // For gloas, the bid must be known, e.g. in the pending payload cache - let mut bid = SignedExecutionPayloadBid::::empty(); + let mut bid = SignedExecutionPayloadBidGloas::::empty(); bid.message.slot = Slot::new(10); - harness - .chain - .pending_payload_cache - .insert_bid(random_sidecar.beacon_block_root, Arc::new(bid)); + harness.chain.pending_payload_cache.insert_bid( + random_sidecar.beacon_block_root, + Arc::new(SignedExecutionPayloadBid::Gloas(bid)), + ); DataColumnSidecar::Gloas(random_sidecar) } else { diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index c8dfefffca9..20522780cff 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -96,8 +96,8 @@ impl InvalidPayloadRig { .body() .signed_execution_payload_bid() .unwrap() - .message - .block_hash + .message() + .block_hash() } fn execution_status(&self, block_root: Hash256) -> ExecutionStatus { diff --git a/beacon_node/beacon_chain/tests/prepare_payload.rs b/beacon_node/beacon_chain/tests/prepare_payload.rs index 77c2db0e7cd..b2913f48224 100644 --- a/beacon_node/beacon_chain/tests/prepare_payload.rs +++ b/beacon_node/beacon_chain/tests/prepare_payload.rs @@ -141,7 +141,7 @@ async fn gloas_block_production_parent_root_with_unadvanced_state() { let parent_slot = parent_state.slot(); let slot = parent_slot + 1; let parent_bid = parent_state.latest_execution_payload_bid().unwrap(); - assert_ne!(parent_bid.block_hash, parent_bid.parent_block_hash); + assert_ne!(parent_bid.block_hash(), parent_bid.parent_block_hash()); // The head's full branch has attestation weight, but negative PTC votes should make the // next proposer build on empty. Looking up the grandparent (as the buggy code did) instead @@ -214,13 +214,13 @@ async fn gloas_block_production_parent_root_with_unadvanced_state() { .body() .signed_execution_payload_bid() .unwrap() - .message - .parent_block_hash, - parent_bid.parent_block_hash, + .message() + .parent_block_hash(), + parent_bid.parent_block_hash(), ); let (envelope, _, _) = payload_contents.unwrap(); assert_eq!(envelope.parent_beacon_block_root, parent_root); - assert_eq!(envelope.payload.parent_hash, parent_bid.parent_block_hash); + assert_eq!(envelope.payload.parent_hash, parent_bid.parent_block_hash()); } } @@ -551,8 +551,8 @@ async fn prepare_payload_generic( .body() .signed_execution_payload_bid() .unwrap() - .message - .parent_block_hash; + .message() + .parent_block_hash(); assert_eq!( produced_execution_parent_hash, expected_execution_parent_hash, "block production should independently select the {parent_payload_status:?} parent" diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 6bafe4819b0..75fa094a049 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -4377,19 +4377,15 @@ async fn weak_subjectivity_sync_test( info!(block_root = ?full_block_root, ?state_root, %slot, "Importing block from chain dump"); beacon_chain.slot_clock.set_slot(slot.as_u64()); - beacon_chain - .process_block( - full_block_root, - harness.build_range_sync_block_from_store_blobs( - Some(block_root), - Arc::new(full_block), - ), - NotifyExecutionLayer::Yes, - BlockImportSource::Lookup, - || Ok(()), - ) - .await - .unwrap(); + Box::pin(beacon_chain.process_block( + full_block_root, + harness.build_range_sync_block_from_store_blobs(Some(block_root), Arc::new(full_block)), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + )) + .await + .unwrap(); // Store the envelope, its columns, and apply to fork choice. if let Some(envelope) = &snapshot.execution_envelope { diff --git a/beacon_node/builder_client/src/builder_http_client.rs b/beacon_node/builder_client/src/builder_http_client.rs index 12241036b3f..66322954333 100644 --- a/beacon_node/builder_client/src/builder_http_client.rs +++ b/beacon_node/builder_client/src/builder_http_client.rs @@ -5,8 +5,8 @@ use crate::{ use bls::PublicKeyBytes; use eth2::types::{ BuilderPreferencesRequest, ContentType, EthSpec, ExecutionBlockHash, ForkName, - ForkVersionedResponse, Hash256, SignedBeaconBlock, SignedExecutionPayloadBid, - SignedRequestAuth, Slot, + ForkVersionDecode, ForkVersionedResponse, Hash256, SignedBeaconBlock, + SignedExecutionPayloadBid, SignedRequestAuth, Slot, }; use eth2::{ CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER, @@ -15,7 +15,7 @@ use eth2::{ use reqwest::StatusCode; use reqwest::header::{ACCEPT, HeaderMap, HeaderName, HeaderValue}; use sensitive_url::SensitiveUrl; -use ssz::{Decode, Encode}; +use ssz::Encode; use std::time::Duration; use tracing::warn; @@ -182,13 +182,23 @@ impl BuilderHttpClient { match content_type_from_header(&response_headers) { ContentType::Ssz => { - let bid = SignedExecutionPayloadBid::::from_ssz_bytes(&response_bytes) - .map_err(Error::InvalidSsz)?; + let bid = SignedExecutionPayloadBid::::from_ssz_bytes_by_fork( + &response_bytes, + fork_name, + ) + .map_err(Error::InvalidSsz)?; Ok(Some(bid)) } ContentType::Json => { let versioned: ForkVersionedResponse> = serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson)?; + let received = versioned.data.message().fork_name_unchecked(); + if received != fork_name { + return Err(Error::InvalidFork { + expected: fork_name, + received, + }); + } Ok(Some(versioned.data)) } } @@ -311,7 +321,7 @@ mod tests { use super::*; use arbitrary::Arbitrary; use eth2::types::beacon_response::EmptyMetadata; - use eth2::types::{ForkName, MainnetEthSpec}; + use eth2::types::{ForkName, MainnetEthSpec, SignedExecutionPayloadBidGloas}; use mockito::{Matcher, Server, ServerGuard}; use std::str::FromStr; @@ -330,11 +340,15 @@ mod tests { SignedRequestAuth::arbitrary(&mut u).unwrap() } + fn empty_bid() -> SignedExecutionPayloadBid { + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas::empty()) + } + fn empty_bid_response() -> ForkVersionedResponse> { ForkVersionedResponse { version: ForkName::Gloas, metadata: EmptyMetadata {}, - data: SignedExecutionPayloadBid::empty(), + data: empty_bid(), } } @@ -377,7 +391,7 @@ mod tests { let mut server = Server::new_async().await; mock_bid(&mut server, ContentType::Json); let bid = request_bid(&server).await.expect("should have a bid"); - assert_eq!(bid, SignedExecutionPayloadBid::empty()); + assert_eq!(bid, empty_bid()); } #[tokio::test] @@ -385,7 +399,7 @@ mod tests { let mut server = Server::new_async().await; mock_bid(&mut server, ContentType::Ssz); let bid = request_bid(&server).await.expect("should have a bid"); - assert_eq!(bid, SignedExecutionPayloadBid::empty()); + assert_eq!(bid, empty_bid()); } #[tokio::test] @@ -516,4 +530,28 @@ mod tests { .create(); assert!(request_bid(&server).await.is_none()); } + + #[tokio::test] + async fn get_execution_payload_bid_rejects_invalid_fork() { + let mut server = Server::new_async().await; + mock_bid(&mut server, ContentType::Json); + let result = client_for() + .get_execution_payload_bid::( + &builder_url(&server), + Slot::new(1), + ExecutionBlockHash::repeat_byte(1), + Hash256::repeat_byte(2), + &PublicKeyBytes::empty(), + &signed_request_auth(), + ForkName::Heze, + ) + .await; + assert!(matches!( + result, + Err(Error::InvalidFork { + expected: ForkName::Heze, + received: ForkName::Gloas, + }) + )); + } } diff --git a/beacon_node/builder_client/src/builders.rs b/beacon_node/builder_client/src/builders.rs index 28902118f16..71727699c90 100644 --- a/beacon_node/builder_client/src/builders.rs +++ b/beacon_node/builder_client/src/builders.rs @@ -250,8 +250,9 @@ mod tests { use bls::Signature; use eth2::types::beacon_response::EmptyMetadata; use eth2::types::{ - ExecutionPayloadBid, ForkName, ForkVersionedResponse, MainnetEthSpec, RequestAuth, - RequestAuthData, SignedExecutionPayloadBid, SignedRequestAuth, + ExecutionPayloadBidGloas, ForkName, ForkVersionedResponse, MainnetEthSpec, RequestAuth, + RequestAuthData, SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, + SignedRequestAuth, }; use eth2::{CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER}; use mockito::{Matcher, Mock, Server, ServerGuard}; @@ -281,16 +282,16 @@ mod tests { let body = ForkVersionedResponse { version: ForkName::Gloas, metadata: EmptyMetadata {}, - data: SignedExecutionPayloadBid:: { - message: ExecutionPayloadBid { + data: SignedExecutionPayloadBid::::Gloas(SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { slot: Slot::new(1), parent_block_hash: ExecutionBlockHash::zero(), parent_block_root: Hash256::ZERO, value, - ..ExecutionPayloadBid::default() + ..ExecutionPayloadBidGloas::default() }, signature: Signature::empty(), - }, + }), }; serde_json::to_string(&body).unwrap() } @@ -334,7 +335,10 @@ mod tests { Ok::<(), String>(()) }) .await; - let mut values: Vec = bids.iter().map(|b| b.signed_bid.message.value).collect(); + let mut values: Vec = bids + .iter() + .map(|b| b.signed_bid.message().value()) + .collect(); values.sort_unstable(); assert_eq!(values, vec![100, 200]); } diff --git a/beacon_node/builder_client/src/error.rs b/beacon_node/builder_client/src/error.rs index 1fe4af0fa43..00e89836c62 100644 --- a/beacon_node/builder_client/src/error.rs +++ b/beacon_node/builder_client/src/error.rs @@ -7,7 +7,7 @@ //! 406/415 negotiation) into an opaque status code. The pre-Gloas builder client still uses //! `eth2::Error`. -use eth2::types::{BuilderUrlError, ErrorMessage}; +use eth2::types::{BuilderUrlError, ErrorMessage, ForkName}; use pretty_reqwest_error::PrettyReqwestError; use reqwest::{Response, StatusCode}; use sensitive_url::SensitiveUrl; @@ -32,6 +32,11 @@ pub enum Error { InvalidSsz(ssz::DecodeError), /// Request headers could not be constructed. InvalidHeaders(String), + /// The builder returned a bid for a different fork than the one requested. + InvalidFork { + expected: ForkName, + received: ForkName, + }, } impl From for Error { @@ -59,6 +64,9 @@ impl fmt::Display for Error { Error::InvalidJson(e) => write!(f, "invalid JSON response: {e}"), Error::InvalidSsz(e) => write!(f, "invalid SSZ response: {e:?}"), Error::InvalidHeaders(e) => write!(f, "invalid response headers: {e}"), + Error::InvalidFork { expected, received } => { + write!(f, "expected a {expected} bid, received a {received} bid") + } } } } diff --git a/beacon_node/http_api/src/beacon/execution_payload_bids.rs b/beacon_node/http_api/src/beacon/execution_payload_bids.rs index de469453a29..732f66488b1 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_bids.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_bids.rs @@ -1,17 +1,17 @@ use crate::task_spawner::{Priority, TaskSpawner}; use crate::utils::{ - ChainFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, TaskSpawnerFilter, - publish_pubsub_message, + ChainFilter, ConsensusVersionHeaderFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, + TaskSpawnerFilter, publish_pubsub_message, }; use beacon_chain::{BeaconChain, BeaconChainTypes}; use bytes::Bytes; +use context_deserialize::ContextDeserialize; use lighthouse_network::PubsubMessage; use network::NetworkMessage; -use ssz::Decode; use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, warn}; -use types::SignedExecutionPayloadBid; +use types::{ForkName, ForkVersionDecode, SignedExecutionPayloadBid}; use warp::{ Filter, Rejection, reply::{Reply, Response}, @@ -20,6 +20,7 @@ use warp::{ // POST /eth/v1/beacon/execution_payload_bids (SSZ) pub(crate) fn post_beacon_execution_payload_bids_ssz( eth_v1: EthV1Filter, + consensus_version_header_filter: ConsensusVersionHeaderFilter, task_spawner_filter: TaskSpawnerFilter, chain_filter: ChainFilter, network_tx_filter: NetworkTxFilter, @@ -29,19 +30,24 @@ pub(crate) fn post_beacon_execution_payload_bids_ssz( .and(warp::path("execution_payload_bids")) .and(warp::path::end()) .and(warp::body::bytes()) + .and(consensus_version_header_filter) .and(task_spawner_filter) .and(chain_filter) .and(network_tx_filter) .then( |body_bytes: Bytes, + consensus_version: ForkName, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>| { task_spawner.blocking_response_task(Priority::P0, move || { - let bid = SignedExecutionPayloadBid::::from_ssz_bytes(&body_bytes) - .map_err(|e| { - warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) - })?; + let bid = SignedExecutionPayloadBid::::from_ssz_bytes_by_fork( + &body_bytes, + consensus_version, + ) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; publish_execution_payload_bid(bid, &chain, &network_tx) }) }, @@ -52,6 +58,7 @@ pub(crate) fn post_beacon_execution_payload_bids_ssz( // POST /eth/v1/beacon/execution_payload_bids pub(crate) fn post_beacon_execution_payload_bids( eth_v1: EthV1Filter, + consensus_version_header_filter: ConsensusVersionHeaderFilter, task_spawner_filter: TaskSpawnerFilter, chain_filter: ChainFilter, network_tx_filter: NetworkTxFilter, @@ -61,15 +68,24 @@ pub(crate) fn post_beacon_execution_payload_bids( .and(warp::path("execution_payload_bids")) .and(warp::path::end()) .and(warp::body::json()) + .and(consensus_version_header_filter) .and(task_spawner_filter) .and(chain_filter) .and(network_tx_filter) .then( - |bid: SignedExecutionPayloadBid, + |value: serde_json::Value, + consensus_version: ForkName, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>| { task_spawner.blocking_response_task(Priority::P0, move || { + let bid = SignedExecutionPayloadBid::::context_deserialize( + &value, + consensus_version, + ) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid JSON: {e:?}")) + })?; publish_execution_payload_bid(bid, &chain, &network_tx) }) }, @@ -83,7 +99,7 @@ pub fn publish_execution_payload_bid( network_tx: &UnboundedSender>, ) -> Result { let slot = bid.slot(); - let builder_index = bid.message.builder_index; + let builder_index = bid.message().builder_index(); if !chain.spec.is_gloas_scheduled() { return Err(warp_utils::reject::custom_bad_request( diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelopes.rs b/beacon_node/http_api/src/beacon/execution_payload_envelopes.rs index 3be66431165..e34cfa89dc2 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelopes.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelopes.rs @@ -275,7 +275,7 @@ pub async fn publish_execution_payload_envelope( .message() .body() .signed_execution_payload_bid() - .is_ok_and(|bid| !bid.message.blob_kzg_commitments.is_empty()); + .is_ok_and(|bid| !bid.message().blob_kzg_commitments().is_empty()); if commits_to_blobs { return Err(warp_utils::reject::custom_bad_request( "block commits to blobs but the beacon node has no cached blobs and KZG proofs to attach" diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index f8399689f8b..2fbacf5474a 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -413,7 +413,7 @@ impl BlockId { })?; let blob_indices_opt = query.versioned_hashes.map(|versioned_hashes| { - versioned_hashes + let mut blob_indices = versioned_hashes .iter() .flat_map(|versioned_hash| { blob_kzg_commitments.iter().position(|commitment| { @@ -422,7 +422,11 @@ impl BlockId { }) }) .map(|index| index as u64) - .collect::>() + .collect::>(); + // Blobs must be returned in the order of their KZG commitments in the block, + // regardless of the order of the requested versioned hashes + blob_indices.sort_unstable(); + blob_indices }); let max_blobs_per_block = chain.spec.max_blobs_per_block(block.epoch()) as usize; @@ -615,8 +619,8 @@ mod tests { .body() .signed_execution_payload_bid() .expect("Gloas block should contain an execution payload bid") - .message - .blob_kzg_commitments + .message() + .blob_kzg_commitments() .get(1) .expect("test block should contain at least two blob commitments") .calculate_versioned_hash(); @@ -648,6 +652,68 @@ mod tests { ); } + #[tokio::test] + async fn get_blobs_follow_commitment_order_post_gloas() { + let harness = gloas_supernode_harness(); + harness.execution_block_generator().set_min_blob_count(2); + harness.advance_slot(); + + let slot = harness.get_current_slot(); + let (_, (block, _), _) = harness + .add_block_at_slot(slot, harness.get_current_state()) + .await + .expect("Gloas block should import"); + + assert_eq!(block.fork_name_unchecked(), ForkName::Gloas); + assert!(block.num_expected_blobs() >= 2); + + // The unfiltered response is in commitment order + let all_blobs_response = BlockId(CoreBlockId::Head) + .get_blobs_by_versioned_hashes( + BlobsVersionedHashesQuery { + versioned_hashes: None, + }, + &harness.chain, + ) + .expect("Gloas head blobs should be retrievable"); + assert_eq!(all_blobs_response.data.len(), block.num_expected_blobs()); + + // Blobs must be returned in the order of their KZG commitments in the block, + // regardless of the order of the requested versioned hashes + let reversed_versioned_hashes = block + .message() + .blob_kzg_commitments() + .expect("Gloas block should have blob KZG commitments") + .iter() + .rev() + .map(|commitment| commitment.calculate_versioned_hash()) + .collect::>(); + let reversed_request_response = BlockId(CoreBlockId::Head) + .get_blobs_by_versioned_hashes( + BlobsVersionedHashesQuery { + versioned_hashes: Some(reversed_versioned_hashes), + }, + &harness.chain, + ) + .expect("Gloas head blobs should be retrievable with reversed versioned hashes"); + + assert_eq!( + reversed_request_response.data.len(), + all_blobs_response.data.len() + ); + for (index, (returned, expected)) in reversed_request_response + .data + .iter() + .zip(&all_blobs_response.data) + .enumerate() + { + assert_eq!( + returned.blob, expected.blob, + "blob at position {index} does not follow commitment order" + ); + } + } + #[tokio::test] async fn root_uses_early_attester_cache_for_unpersisted_block() { let Some(fork_name) = fork_name_from_env().filter(|fork_name| fork_name.fulu_enabled()) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index de43b60e5bf..0bbaa506ec3 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1069,7 +1069,7 @@ pub async fn serve( .and(warp::query::()) .and(warp::path::end()) .and(warp_utils::json::json()) - .and(consensus_version_header_filter) + .and(consensus_version_header_filter.clone()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) @@ -1574,6 +1574,7 @@ pub async fn serve( // POST beacon/execution_payload_bids let post_beacon_execution_payload_bids = post_beacon_execution_payload_bids( eth_v1.clone(), + consensus_version_header_filter.clone(), task_spawner_filter.clone(), chain_filter.clone(), network_tx_filter.clone(), @@ -1582,6 +1583,7 @@ pub async fn serve( // POST beacon/execution_payload_bids (SSZ) let post_beacon_execution_payload_bids_ssz = post_beacon_execution_payload_bids_ssz( eth_v1.clone(), + consensus_version_header_filter.clone(), task_spawner_filter.clone(), chain_filter.clone(), network_tx_filter.clone(), diff --git a/beacon_node/http_api/src/ptc_duties.rs b/beacon_node/http_api/src/ptc_duties.rs index 6d27fa565a2..148ce5ab603 100644 --- a/beacon_node/http_api/src/ptc_duties.rs +++ b/beacon_node/http_api/src/ptc_duties.rs @@ -34,19 +34,32 @@ pub fn ptc_duties( .epoch(T::EthSpec::slots_per_epoch()) }; + if !chain.spec.fork_name_at_epoch(request_epoch).gloas_enabled() { + return Err(warp_utils::reject::custom_bad_request(format!( + "PTC duties are not available before the Gloas fork, request epoch {}", + request_epoch + ))); + } + + if chain.spec.gloas_fork_epoch == Some(request_epoch) && tolerant_current_epoch < request_epoch + { + return Err(warp_utils::reject::custom_bad_request(format!( + "PTC duties for the Gloas fork epoch {} are not available until the fork", + request_epoch + ))); + } + let is_within_clock_tolerance = request_epoch == current_epoch || request_epoch == current_epoch + 1 || request_epoch == tolerant_current_epoch + 1; if is_within_clock_tolerance { - let head_epoch = chain - .canonical_head - .cached_head() - .snapshot - .beacon_state - .current_epoch(); + let cached_head = chain.canonical_head.cached_head(); + let head_state = &cached_head.snapshot.beacon_state; + let head_epoch = head_state.current_epoch(); - let head_can_serve_request = request_epoch == head_epoch || request_epoch == head_epoch + 1; + let head_can_serve_request = head_state.fork_name_unchecked().gloas_enabled() + && (request_epoch == head_epoch || request_epoch == head_epoch + 1); if head_can_serve_request { compute_ptc_duties_from_cached_head(request_epoch, request_indices, chain) @@ -100,8 +113,9 @@ fn compute_ptc_duties_from_state( if head.beacon_state.current_epoch() <= request_epoch { Some(( + head.beacon_block_root, head.beacon_state_root(), - head.beacon_state.clone(), + head.beacon_state.slot(), execution_status.is_optimistic_or_invalid(), )) } else { @@ -109,8 +123,19 @@ fn compute_ptc_duties_from_state( } }; - let (state, execution_optimistic) = - if let Some((state_root, mut state, execution_optimistic)) = state_opt { + let (state, dependent_block_root, execution_optimistic) = + if let Some((block_root, state_root, head_slot, execution_optimistic)) = state_opt { + let max_slot = std::cmp::max( + head_slot, + request_epoch.start_slot(T::EthSpec::slots_per_epoch()), + ); + let (state_root, mut state) = chain + .store + .get_advanced_hot_state(block_root, max_slot, state_root) + .map_err(BeaconChainError::DBError) + .map_err(warp_utils::reject::unhandled_error)? + .ok_or(BeaconChainError::MissingBeaconState(state_root)) + .map_err(warp_utils::reject::unhandled_error)?; ensure_state_knows_ptc_duties_for_epoch( &mut state, state_root, @@ -118,12 +143,12 @@ fn compute_ptc_duties_from_state( chain.builder_onboarding_cache.as_deref(), &chain.spec, )?; - (state, execution_optimistic) + (state, block_root, execution_optimistic) } else { let (state, execution_optimistic, _finalized) = StateId::from_slot(request_epoch.start_slot(T::EthSpec::slots_per_epoch())) .state(chain)?; - (state, execution_optimistic) + (state, chain.genesis_block_root, execution_optimistic) }; if !(state.current_epoch() == request_epoch || state.current_epoch() + 1 == request_epoch) { @@ -135,12 +160,7 @@ fn compute_ptc_duties_from_state( } let (duties, dependent_root) = chain - .compute_ptc_duties( - &state, - request_epoch, - request_indices, - chain.genesis_block_root, - ) + .compute_ptc_duties(&state, request_epoch, request_indices, dependent_block_root) .map_err(warp_utils::reject::unhandled_error)?; convert_to_api_response(duties, dependent_root, execution_optimistic) @@ -159,6 +179,20 @@ fn ensure_state_knows_ptc_duties_for_epoch( state.current_epoch(), target_epoch ))); + } else if !state.fork_name_unchecked().gloas_enabled() { + let fork_epoch = spec.gloas_fork_epoch.unwrap_or(target_epoch); + let target_slot = std::cmp::max(fork_epoch, target_epoch.saturating_sub(1_u64)) + .start_slot(E::slots_per_epoch()); + + partial_state_advance( + state, + Some(state_root), + target_slot, + builder_onboarding_cache, + spec, + ) + .map_err(BeaconChainError::from) + .map_err(warp_utils::reject::unhandled_error)?; } else if state.current_epoch() + 1 < target_epoch { let target_slot = target_epoch .saturating_sub(1_u64) diff --git a/beacon_node/http_api/src/utils.rs b/beacon_node/http_api/src/utils.rs index f2b859ebe59..3189fe2c506 100644 --- a/beacon_node/http_api/src/utils.rs +++ b/beacon_node/http_api/src/utils.rs @@ -21,6 +21,7 @@ pub type ValidatorSubscriptionTxFilter = BoxedFilter<(Sender = BoxedFilter<(UnboundedSender::EthSpec>>,)>; pub type OptionalConsensusVersionHeaderFilter = BoxedFilter<(Option,)>; +pub type ConsensusVersionHeaderFilter = BoxedFilter<(ForkName,)>; pub fn from_meta_data( meta_data: &RwLock>, diff --git a/beacon_node/http_api/tests/fork_tests.rs b/beacon_node/http_api/tests/fork_tests.rs index 0ff8ebc452d..d62a6d96664 100644 --- a/beacon_node/http_api/tests/fork_tests.rs +++ b/beacon_node/http_api/tests/fork_tests.rs @@ -5,14 +5,15 @@ use beacon_chain::{ test_utils::{DEFAULT_ETH1_BLOCK_HASH, HARNESS_GENESIS_TIME, RelativeSyncCommittee}, }; use bls::PublicKey; -use eth2::types::{IndexedErrorMessage, StateId, SyncSubcommittee}; +use eth2::types::{IndexedErrorMessage, PtcDuty, StateId, SyncSubcommittee}; use execution_layer::test_utils::generate_genesis_header; use fixed_bytes::FixedBytesExtended; use genesis::{InteropGenesisBuilder, bls_withdrawal_credentials}; use http_api::test_utils::*; +use state_processing::state_advance::complete_state_advance; use std::collections::HashSet; use types::{ - Address, ChainSpec, Epoch, EthSpec, Hash256, MinimalEthSpec, Slot, + Address, BeaconState, ChainSpec, Epoch, EthSpec, ForkName, Hash256, MinimalEthSpec, Slot, test_utils::{generate_deterministic_keypair, generate_deterministic_keypairs}, }; @@ -32,6 +33,12 @@ fn capella_spec(capella_fork_epoch: Epoch) -> ChainSpec { spec } +fn gloas_spec(gloas_fork_epoch: Epoch) -> ChainSpec { + let mut spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); + spec.gloas_fork_epoch = Some(gloas_fork_epoch); + spec +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sync_committee_duties_across_fork() { let validator_count = E::sync_committee_size(); @@ -113,6 +120,120 @@ async fn sync_committee_duties_across_fork() { ); } +fn ptc_duty_slots(duties: &[PtcDuty]) -> Vec<(u64, Slot)> { + let mut slots = duties + .iter() + .map(|duty| (duty.validator_index, duty.slot)) + .collect::>(); + slots.sort(); + slots +} + +fn expected_ptc_duty_slots( + mut state: BeaconState, + epoch: Epoch, + validators: &[u64], + spec: &ChainSpec, +) -> Vec<(u64, Slot)> { + complete_state_advance( + &mut state, + None, + epoch.start_slot(E::slots_per_epoch()), + None, + spec, + ) + .unwrap(); + let mut slots = validators + .iter() + .filter_map(|&validator_index| { + state + .get_ptc_assignment(validator_index as usize, epoch, spec) + .unwrap() + .map(|slot| (validator_index, slot)) + }) + .collect::>(); + slots.sort(); + slots +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn ptc_duties_across_fork() { + let validator_count = E::sync_committee_size(); + let fork_epoch = Epoch::new(2); + let spec = gloas_spec(fork_epoch); + let tester = InteractiveTester::::new(Some(spec.clone()), validator_count).await; + let harness = &tester.harness; + let client = &tester.client; + + let all_validators = harness.get_all_validators(); + let all_validators_u64 = all_validators.iter().map(|x| *x as u64).collect::>(); + + assert_eq!(harness.get_current_slot(), 0); + + for epoch in [Epoch::new(0), fork_epoch - 1] { + assert_eq!( + client + .post_validator_duties_ptc(epoch, &all_validators_u64) + .await + .unwrap_err() + .status() + .unwrap(), + 400 + ); + } + + let fork_slot = fork_epoch.start_slot(E::slots_per_epoch()); + let genesis_state = harness.get_current_state(); + let (pre_fork_block_root, pre_fork_state) = harness + .add_attested_block_at_slot(fork_slot - 1, genesis_state, &all_validators) + .await + .unwrap(); + let pre_fork_block_root = Hash256::from(pre_fork_block_root); + assert_eq!(harness.get_current_slot(), fork_slot - 1); + + let expected_duties = expected_ptc_duty_slots( + pre_fork_state.clone(), + fork_epoch, + &all_validators_u64, + &spec, + ); + assert!(!expected_duties.is_empty()); + + for epoch in [fork_epoch - 1, fork_epoch] { + assert_eq!( + client + .post_validator_duties_ptc(epoch, &all_validators_u64) + .await + .unwrap_err() + .status() + .unwrap(), + 400 + ); + } + + harness.advance_slot(); + assert_eq!(harness.get_current_slot(), fork_slot); + + let fork_slot_response = client + .post_validator_duties_ptc(fork_epoch, &all_validators_u64) + .await + .unwrap(); + assert_eq!(fork_slot_response.dependent_root, pre_fork_block_root); + assert_eq!(ptc_duty_slots(&fork_slot_response.data), expected_duties); + + harness + .add_attested_block_at_slot(fork_slot, pre_fork_state, &all_validators) + .await + .unwrap(); + + let post_fork_response = client + .post_validator_duties_ptc(fork_epoch, &all_validators_u64) + .await + .unwrap(); + assert_eq!(post_fork_response.dependent_root, pre_fork_block_root); + assert_eq!(ptc_duty_slots(&post_fork_response.data), expected_duties); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn attestations_across_fork_with_skip_slots() { let validator_count = E::sync_committee_size(); diff --git a/beacon_node/http_api/tests/gloas_reorg_tests.rs b/beacon_node/http_api/tests/gloas_reorg_tests.rs index f31746ddf70..ab46599d573 100644 --- a/beacon_node/http_api/tests/gloas_reorg_tests.rs +++ b/beacon_node/http_api/tests/gloas_reorg_tests.rs @@ -751,16 +751,16 @@ pub async fn proposer_boost_re_org_test( .body() .signed_execution_payload_bid() .unwrap() - .message - .block_hash + .message() + .block_hash() }; let exec_parent_hash = |block: BeaconBlockRef| -> ExecutionBlockHash { block .body() .signed_execution_payload_bid() .unwrap() - .message - .parent_block_hash + .message() + .parent_block_hash() }; let block_a_exec_hash = exec_block_hash(block_a.0.message()); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 26e9a0e0df2..fb00e2647b6 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -52,9 +52,11 @@ use tokio::time::Duration; use tree_hash::TreeHash; use types::ApplicationDomain; use types::{ - Address, Builder, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, Hash256, - MainnetEthSpec, ProposerPreferences, RelativeEpoch, SelectionProof, SignedExecutionPayloadBid, - SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedRoot, SingleAttestation, Slot, + Address, Builder, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBidGloas, + ExecutionPayloadBidHeze, Hash256, MainnetEthSpec, ProposerPreferences, RelativeEpoch, + SelectionProof, SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, + SignedExecutionPayloadBidHeze, SignedExecutionPayloadEnvelope, SignedProposerPreferences, + SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, consts::gloas::{BUILDER_INDEX_SELF_BUILD, PAYLOAD_BUILDER_VERSION}, }; @@ -2333,6 +2335,53 @@ impl ApiTester { self } + pub async fn test_get_blobs_follow_commitment_order(self) -> Self { + let block_id = BlockId(CoreBlockId::Head); + let (block_root, _, _) = block_id.root(&self.chain).unwrap(); + let (block, _, _) = block_id.full_block(&self.chain).await.unwrap(); + + let versioned_hashes: Vec = block + .message() + .blob_kzg_commitments() + .unwrap() + .iter() + .map(|commitment| commitment.calculate_versioned_hash()) + .collect(); + let num_blobs = versioned_hashes.len(); + assert!(num_blobs >= 2, "test block must contain at least two blobs"); + + // The unfiltered response is in commitment order + let all_blobs = self + .client + .get_blobs::(CoreBlockId::Root(block_root), None) + .await + .unwrap() + .unwrap() + .into_data(); + assert_eq!(all_blobs.len(), num_blobs); + + // Every hash in reverse order + let request: Vec = versioned_hashes.iter().rev().copied().collect(); + + let result = self + .client + .get_blobs::(CoreBlockId::Root(block_root), Some(&request)) + .await + .unwrap() + .unwrap() + .into_data(); + + assert_eq!(result.len(), num_blobs); + for (index, (returned, expected)) in result.iter().zip(&all_blobs).enumerate() { + assert_eq!( + returned.blob, expected.blob, + "blob at position {index} does not follow commitment order" + ); + } + + self + } + pub async fn test_get_blobs_post_fulu_full_node(self, versioned_hashes: bool) -> Self { let block_id = BlockId(CoreBlockId::Head); let (block_root, _, _) = block_id.root(&self.chain).unwrap(); @@ -3424,24 +3473,44 @@ impl ApiTester { let slot = self.chain.slot().unwrap(); let fork_name = self.chain.spec.fork_name_at_slot::(slot); - let bid = ExecutionPayloadBid { - parent_block_hash: ExecutionBlockHash::zero(), - parent_block_root: head.head_block_root(), - block_hash: ExecutionBlockHash::zero(), - prev_randao: Hash256::zero(), - fee_recipient: Address::zero(), - gas_limit: 30_000_000, - builder_index: 0, - slot, - value: 100, - execution_payment: 0, - blob_kzg_commitments: Default::default(), - execution_requests_root: Hash256::zero(), - }; - - let signed = SignedExecutionPayloadBid { - message: bid, - signature: bls::Signature::empty(), + let signature = bls::Signature::empty(); + let signed = if fork_name.heze_enabled() { + SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze { + message: ExecutionPayloadBidHeze { + parent_block_hash: ExecutionBlockHash::zero(), + parent_block_root: head.head_block_root(), + block_hash: ExecutionBlockHash::zero(), + prev_randao: Hash256::zero(), + fee_recipient: Address::zero(), + gas_limit: 30_000_000, + builder_index: 0, + slot, + value: 100, + execution_payment: 0, + blob_kzg_commitments: Default::default(), + execution_requests_root: Hash256::zero(), + inclusion_list_bits: Default::default(), + }, + signature, + }) + } else { + SignedExecutionPayloadBid::Gloas(SignedExecutionPayloadBidGloas { + message: ExecutionPayloadBidGloas { + parent_block_hash: ExecutionBlockHash::zero(), + parent_block_root: head.head_block_root(), + block_hash: ExecutionBlockHash::zero(), + prev_randao: Hash256::zero(), + fee_recipient: Address::zero(), + gas_limit: 30_000_000, + builder_index: 0, + slot, + value: 100, + execution_payment: 0, + blob_kzg_commitments: Default::default(), + execution_requests_root: Hash256::zero(), + }, + signature, + }) }; (signed, fork_name) @@ -11003,6 +11072,8 @@ async fn get_blob_sidecars() { .test_get_blobs(false) .await .test_get_blobs(true) + .await + .test_get_blobs_follow_commitment_order() .await; } @@ -11029,6 +11100,8 @@ async fn get_blobs_post_fulu_supernode() { .test_get_blobs(false) .await .test_get_blobs(true) + .await + .test_get_blobs_follow_commitment_order() .await; } diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 8baf3c20bb0..e640e7d7c70 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -2185,7 +2185,7 @@ impl Network { } /// Returns the fork digest to publish `message` on. Proposer preferences use the digest of their -/// proposal epoch. All other messages use the current digest. +/// proposal epoch, and bids the digest of their epoch. All other messages use the current digest. fn publish_fork_digest( message: &PubsubMessage, fork_context: &ForkContext, @@ -2198,6 +2198,7 @@ fn publish_fork_digest( .proposal_slot .epoch(E::slots_per_epoch()), ), + PubsubMessage::ExecutionPayloadBid(bid) => fork_context.context_bytes(bid.epoch()), _ => current_digest, } } @@ -2207,7 +2208,8 @@ mod tests { use super::*; use bls::Signature; use types::{ - Epoch, Hash256, MinimalEthSpec, ProposerPreferences, SignedProposerPreferences, + Epoch, ExecutionPayloadBidHeze, Hash256, MinimalEthSpec, ProposerPreferences, + SignedExecutionPayloadBid, SignedExecutionPayloadBidHeze, SignedProposerPreferences, SignedVoluntaryExit, VoluntaryExit, }; @@ -2250,4 +2252,29 @@ mod tests { current_digest ); } + + #[test] + fn publish_fork_digest_uses_bid_epoch_for_bids() { + let heze_epoch = Epoch::new(2); + let mut spec = ForkName::Gloas.make_genesis_spec(E::default_spec()); + spec.heze_fork_epoch = Some(heze_epoch); + let fork_context = ForkContext::new::(Slot::new(0), Hash256::ZERO, &spec); + let current_digest = fork_context.current_fork_digest(); + let heze_digest = fork_context.context_bytes(heze_epoch); + + let bid = PubsubMessage::::ExecutionPayloadBid(Box::new( + SignedExecutionPayloadBid::Heze(SignedExecutionPayloadBidHeze { + message: ExecutionPayloadBidHeze { + slot: heze_epoch.start_slot(E::slots_per_epoch()), + ..ExecutionPayloadBidHeze::default() + }, + signature: Signature::empty(), + }), + )); + + assert_eq!( + publish_fork_digest(&bid, &fork_context, current_digest), + heze_digest + ); + } } diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 6a716526d87..6c956d74a7e 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -18,7 +18,7 @@ use types::{ SignedBeaconBlock, SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedInclusionList, SignedProposerPreferences, SignedVoluntaryExit, SingleAttestation, SubnetId, - SyncCommitteeMessage, SyncSubnetId, execution::SignedExecutionProof, + SyncCommitteeMessage, SyncSubnetId, execution::SignedExecutionProofEnvelope, }; #[derive(Debug, Clone, PartialEq)] @@ -52,7 +52,7 @@ pub enum PubsubMessage { /// Gossipsub message providing notification of signed proposer preferences. ProposerPreferences(Arc), /// Gossipsub message providing notification of an EIP-8025 execution proof. - ExecutionProof(Arc), + ExecutionProof(Arc), /// Gossipsub message providing notification of a signed inclusion list. InclusionList(Box), /// Gossipsub message providing notification of a light client finality update. @@ -388,15 +388,29 @@ impl PubsubMessage { ))) } GossipKind::ExecutionPayloadBid => { - if data.len() > E::max_signed_execution_payload_bid_size() { + let fork_name = *fork_context + .get_fork_from_context_bytes(gossip_topic.fork_digest) + .ok_or_else(|| { + format!( + "Unknown gossipsub fork digest: {:?}", + gossip_topic.fork_digest + ) + })?; + let max_size = if fork_name.heze_enabled() { + E::max_signed_execution_payload_bid_size_heze() + } else { + E::max_signed_execution_payload_bid_size() + }; + if data.len() > max_size { return Err(format!( "SignedExecutionPayloadBid size {} exceeds MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE {}", data.len(), - E::max_signed_execution_payload_bid_size() + max_size )); } - let execution_payload_bid = SignedExecutionPayloadBid::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?; + let execution_payload_bid = + SignedExecutionPayloadBid::from_ssz_bytes_by_fork(data, fork_name) + .map_err(|e| format!("{:?}", e))?; Ok(PubsubMessage::ExecutionPayloadBid(Box::new( execution_payload_bid, ))) @@ -416,7 +430,7 @@ impl PubsubMessage { ))) } GossipKind::ExecutionProof => { - let execution_proof = SignedExecutionProof::from_ssz_bytes(data) + let execution_proof = SignedExecutionProofEnvelope::from_ssz_bytes(data) .map_err(|e| format!("{:?}", e))?; Ok(PubsubMessage::ExecutionProof(Arc::new(execution_proof))) } @@ -644,7 +658,8 @@ impl std::fmt::Display for PubsubMessage { write!( f, "Execution payload bid: slot: {:?} value: {:?}", - data.message.slot, data.message.value + data.message().slot(), + data.message().value() ) } PubsubMessage::ProposerPreferences(data) => { @@ -686,7 +701,8 @@ mod tests { use libp2p::gossipsub::partial_messages::Partial; use types::data::{CellBitmap, PartialDataColumnSidecarGloas}; use types::{ - BeaconBlock, Epoch, EthSpec, ForkName, MainnetEthSpec, Slot, data::DataColumnSubnetId, + BeaconBlock, ChainSpec, Epoch, EthSpec, ForkName, MainnetEthSpec, Slot, + data::DataColumnSubnetId, }; type E = MainnetEthSpec; @@ -713,7 +729,8 @@ mod tests { } } - fn gloas_fork_context() -> ForkContext { + /// A spec with every fork up to Fulu scheduled at genesis. + fn pre_gloas_spec() -> ChainSpec { let mut spec = E::default_spec(); spec.altair_fork_epoch = Some(Epoch::new(0)); spec.bellatrix_fork_epoch = Some(Epoch::new(0)); @@ -721,10 +738,22 @@ mod tests { spec.deneb_fork_epoch = Some(Epoch::new(0)); spec.electra_fork_epoch = Some(Epoch::new(0)); spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec + } + + fn gloas_fork_context() -> ForkContext { + let mut spec = pre_gloas_spec(); spec.gloas_fork_epoch = Some(Epoch::new(0)); ForkContext::new::(Slot::new(0), Hash256::ZERO, &spec) } + fn heze_fork_context() -> ForkContext { + let mut spec = pre_gloas_spec(); + spec.gloas_fork_epoch = Some(Epoch::new(0)); + spec.heze_fork_epoch = Some(Epoch::new(0)); + ForkContext::new::(Slot::new(0), Hash256::ZERO, &spec) + } + #[test] fn gloas_group_id_round_trips_through_decode_partial() { let fork_context = gloas_fork_context(); @@ -764,8 +793,11 @@ mod tests { assert_eq!(decoded.index, column.index); } - fn decode_oversized(kind: GossipKind, size: usize) -> Result, String> { - let fork_context = gloas_fork_context(); + fn decode_oversized( + fork_context: &ForkContext, + kind: GossipKind, + size: usize, + ) -> Result, String> { let topic = GossipTopic::new( kind, GossipEncoding::default(), @@ -773,15 +805,25 @@ mod tests { ); let topic_hash = TopicHash::from_raw(String::from(topic)); let data = vec![0u8; size]; - PubsubMessage::decode(&topic_hash, &data, &fork_context) + PubsubMessage::decode(&topic_hash, &data, fork_context) } #[test] fn gloas_aggregate_and_proof_size_bound() { let max = E::max_signed_aggregate_and_proof_size(); - let err = decode_oversized(GossipKind::BeaconAggregateAndProof, max + 1).unwrap_err(); + let err = decode_oversized( + &gloas_fork_context(), + GossipKind::BeaconAggregateAndProof, + max + 1, + ) + .unwrap_err(); assert!(err.contains("MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE"), "{err}"); - let err = decode_oversized(GossipKind::BeaconAggregateAndProof, max).unwrap_err(); + let err = decode_oversized( + &gloas_fork_context(), + GossipKind::BeaconAggregateAndProof, + max, + ) + .unwrap_err(); assert!( !err.contains("MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE"), "{err}" @@ -791,9 +833,11 @@ mod tests { #[test] fn gloas_attester_slashing_size_bound() { let max = E::max_attester_slashing_size(); - let err = decode_oversized(GossipKind::AttesterSlashing, max + 1).unwrap_err(); + let err = decode_oversized(&gloas_fork_context(), GossipKind::AttesterSlashing, max + 1) + .unwrap_err(); assert!(err.contains("MAX_ATTESTER_SLASHING_SIZE"), "{err}"); - let err = decode_oversized(GossipKind::AttesterSlashing, max).unwrap_err(); + let err = + decode_oversized(&gloas_fork_context(), GossipKind::AttesterSlashing, max).unwrap_err(); assert!(!err.contains("MAX_ATTESTER_SLASHING_SIZE"), "{err}"); } @@ -804,9 +848,9 @@ mod tests { .spec .compute_max_data_column_sidecar_size_gloas::(); let kind = GossipKind::DataColumnSidecar(DataColumnSubnetId::new(0)); - let err = decode_oversized(kind.clone(), max + 1).unwrap_err(); + let err = decode_oversized(&gloas_fork_context(), kind.clone(), max + 1).unwrap_err(); assert!(err.contains("MAX_DATA_COLUMN_SIDECAR_SIZE"), "{err}"); - let err = decode_oversized(kind, max).unwrap_err(); + let err = decode_oversized(&gloas_fork_context(), kind, max).unwrap_err(); assert!(!err.contains("MAX_DATA_COLUMN_SIDECAR_SIZE"), "{err}"); } @@ -845,12 +889,39 @@ mod tests { #[test] fn gloas_execution_payload_bid_size_bound() { let max = E::max_signed_execution_payload_bid_size(); - let err = decode_oversized(GossipKind::ExecutionPayloadBid, max + 1).unwrap_err(); + let err = decode_oversized( + &gloas_fork_context(), + GossipKind::ExecutionPayloadBid, + max + 1, + ) + .unwrap_err(); + assert!( + err.contains("MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE"), + "{err}" + ); + let err = decode_oversized(&gloas_fork_context(), GossipKind::ExecutionPayloadBid, max) + .unwrap_err(); + assert!( + !err.contains("MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE"), + "{err}" + ); + } + + #[test] + fn heze_execution_payload_bid_size_bound() { + let fork_context = heze_fork_context(); + let gloas_max = E::max_signed_execution_payload_bid_size(); + let heze_max = E::max_signed_execution_payload_bid_size_heze(); + assert!(heze_max > gloas_max); + + let err = decode_oversized(&fork_context, GossipKind::ExecutionPayloadBid, heze_max + 1) + .unwrap_err(); assert!( err.contains("MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE"), "{err}" ); - let err = decode_oversized(GossipKind::ExecutionPayloadBid, max).unwrap_err(); + let err = + decode_oversized(&fork_context, GossipKind::ExecutionPayloadBid, heze_max).unwrap_err(); assert!( !err.contains("MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE"), "{err}" @@ -860,9 +931,11 @@ mod tests { #[test] fn heze_inclusion_list_size_bound() { let max = E::max_signed_inclusion_list_size(); - let err = decode_oversized(GossipKind::InclusionList, max + 1).unwrap_err(); + let err = decode_oversized(&gloas_fork_context(), GossipKind::InclusionList, max + 1) + .unwrap_err(); assert!(err.contains("MAX_SIGNED_INCLUSION_LIST_SIZE"), "{err}"); - let err = decode_oversized(GossipKind::InclusionList, max).unwrap_err(); + let err = + decode_oversized(&gloas_fork_context(), GossipKind::InclusionList, max).unwrap_err(); assert!(!err.contains("MAX_SIGNED_INCLUSION_LIST_SIZE"), "{err}"); } } diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index be1f2a6c823..f2d580c7dfa 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -56,7 +56,7 @@ use types::{ SignedContributionAndProof, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedInclusionList, SignedProposerPreferences, SignedVoluntaryExit, SingleAttestation, Slot, SubnetId, SyncCommitteeMessage, SyncSubnetId, block::BlockImportSource, data::CellBitmap, - execution::SignedExecutionProof, + execution::SignedExecutionProofEnvelope, }; use beacon_processor::work_reprocessing_queue::QueuedColumnReconstruction; @@ -4153,7 +4153,7 @@ impl NetworkBeaconProcessor { self: Arc, message_id: MessageId, peer_id: PeerId, - execution_proof: Arc, + execution_proof: Arc, ) { let beacon_block_root = execution_proof.beacon_block_root(); let proof_type = execution_proof.proof_type(); @@ -4171,34 +4171,6 @@ impl NetworkBeaconProcessor { "Verified execution proof from gossip" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); - - // This may be the proof the block's envelope was waiting on. - match self - .chain - .check_execution_proof_availability_and_import(verified) - .await - { - Ok(AvailabilityProcessingStatus::Imported(slot, block_root)) => { - info!( - ?block_root, - %slot, - "Execution payload envelope imported after execution proof" - ); - self.chain.recompute_head_at_current_slot().await; - // The payload envelope is imported (`is_payload_received` is now true); - // release any attestations awaiting this block's payload. - self.notify_payload_envelope_imported(block_root, EnvelopeSource::Gossip); - } - Ok(AvailabilityProcessingStatus::MissingComponents(..)) => {} - Err(error) => { - debug!( - %beacon_block_root, - proof_type, - ?error, - "Could not cache execution proof" - ); - } - } } Err(error) => { debug!(%beacon_block_root, proof_type, ?error, "Could not verify execution proof"); @@ -4208,7 +4180,8 @@ impl NetworkBeaconProcessor { | ExecutionProofError::ValidProofAlreadyKnown | ExecutionProofError::DuplicateFromValidator { .. } | ExecutionProofError::UnknownBlockRoot { .. } - | ExecutionProofError::PastFinalizedSlot { .. } => { + | ExecutionProofError::PastFinalizedSlot { .. } + | ExecutionProofError::PayloadUnavailable { .. } => { (MessageAcceptance::Ignore, None) } // REJECT: the proof is invalid. @@ -4239,7 +4212,7 @@ impl NetworkBeaconProcessor { parent = None, level = "debug", skip_all, - fields(parent_block_hash = ?bid.message.parent_block_hash, parent_block_root = ?bid.message.parent_block_root), + fields(parent_block_hash = ?bid.message().parent_block_hash(), parent_block_root = ?bid.message().parent_block_root()), )] pub fn process_gossip_execution_payload_bid( self: &Arc, @@ -4261,7 +4234,8 @@ impl NetworkBeaconProcessor { | PayloadBidError::BlockHashEqualsParentBlockHash { .. } | PayloadBidError::InvalidBlobKzgCommitments { .. } | PayloadBidError::BidNotDescendantOfParent { .. } - | PayloadBidError::InvalidPrevRandao { .. }, + | PayloadBidError::InvalidPrevRandao { .. } + | PayloadBidError::InconsistentFork(_), ) => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 0fcbb1a5972..f5b75a2fb06 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -468,7 +468,7 @@ impl NetworkBeaconProcessor { self: &Arc, message_id: MessageId, peer_id: PeerId, - execution_proof: Arc, + execution_proof: Arc, ) -> Result<(), Error> { let processor = self.clone(); let process_fn = async move { diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index c06e360084f..7f1afac2b08 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -1857,7 +1857,7 @@ async fn rpc_columns_notify_after_deferred_envelope_import() { bundle .commitments .iter() - .eq(bid.message.blob_kzg_commitments.iter()) + .eq(bid.message().blob_kzg_commitments().iter()) }) .expect("blobs for next block") .blobs diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 6962e4bc1e9..05d02500640 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -262,7 +262,7 @@ impl SingleBlockLookup { .body() .signed_execution_payload_bid() .ok() - .map(|bid| bid.message.block_hash) + .map(|bid| bid.message().block_hash()) }) } diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 837b11e1c06..fd81b450d1f 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -608,22 +608,22 @@ mod tests { } fn matching_envelope(block: &SignedBeaconBlock) -> Arc> { - let bid = &block + let bid = block .message() .body() .signed_execution_payload_bid() .expect("Gloas block should have payload bid") - .message; + .message(); let mut envelope = SignedExecutionPayloadEnvelope { message: ExecutionPayloadEnvelope::empty(), signature: Signature::empty(), }; envelope.message.beacon_block_root = block.canonical_root(); envelope.message.parent_beacon_block_root = block.parent_root(); - envelope.message.builder_index = bid.builder_index; + envelope.message.builder_index = bid.builder_index(); envelope.message.payload.slot_number = block.slot(); - envelope.message.payload.parent_hash = bid.parent_block_hash; - envelope.message.payload.block_hash = bid.block_hash; + envelope.message.payload.parent_hash = bid.parent_block_hash(); + envelope.message.payload.block_hash = bid.block_hash(); Arc::new(envelope) } @@ -651,7 +651,7 @@ mod tests { if let Ok(bid) = block .message_mut() .body_mut() - .signed_execution_payload_bid_mut() + .signed_execution_payload_bid_gloas_mut() { bid.message.execution_requests_root = ExecutionRequestsGloas::::default().tree_hash_root(); diff --git a/beacon_node/operation_pool/src/attestation.rs b/beacon_node/operation_pool/src/attestation.rs index 60bd2f2df9d..f9a1372aeaf 100644 --- a/beacon_node/operation_pool/src/attestation.rs +++ b/beacon_node/operation_pool/src/attestation.rs @@ -84,7 +84,7 @@ impl<'a, E: EthSpec> AttMaxCover<'a, E> { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let att_participation_flags = get_attestation_participation_flag_indices( state, diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 46d8fb29a5a..611c5079fa4 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -2723,7 +2723,10 @@ mod release_tests { .unwrap(); // Set the parent slot payload availability - let parent_slot = advanced_state.latest_execution_payload_bid().unwrap().slot; + let parent_slot = advanced_state + .latest_execution_payload_bid() + .unwrap() + .slot(); let availability_index = parent_slot.as_usize() % MinimalEthSpec::slots_per_historical_root(); advanced_state diff --git a/beacon_node/proof_engine/src/lib.rs b/beacon_node/proof_engine/src/lib.rs index 91ceeae1f7e..07f0d6dc5d0 100644 --- a/beacon_node/proof_engine/src/lib.rs +++ b/beacon_node/proof_engine/src/lib.rs @@ -66,16 +66,20 @@ impl ProofEngine { let response: VerifyResponse = self .client .post(url) + // [DEVIATION] The spec public input is `hash_tree_root(public_input)`; the sidecar + // rebuilds it from the fields. .query(&[ ( "new_payload_request_root", format!("{:?}", proof.public_input.new_payload_request_root), ), - ("proof_type", proof.proof_type.to_string()), ( - "beacon_block_root", - format!("{:?}", proof.beacon_block_root), + "successful_validation", + proof.public_input.successful_validation.to_string(), ), + ("chain_id", proof.public_input.chain_id.to_string()), + ("schema_id", proof.public_input.schema_id.to_string()), + ("proof_type", proof.proof_type.to_string()), ]) .header("content-type", "application/octet-stream") .body(proof.proof_data.to_vec()) diff --git a/book/src/contributing_setup.md b/book/src/contributing_setup.md index 62e590e28f8..7e4dfad01fb 100644 --- a/book/src/contributing_setup.md +++ b/book/src/contributing_setup.md @@ -12,7 +12,7 @@ The additional requirements for developers are: - [`cmake`](https://cmake.org/cmake/help/latest/command/install.html). Used by some dependencies. See [`Installation Guide`](./installation.md) for more info. -- [`java 17 runtime`](https://openjdk.java.net/projects/jdk/). 17 is the minimum, +- [`java 25 runtime`](https://openjdk.java.net/projects/jdk/). 25 is the minimum, used by web3signer_tests. ## Using `make` diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index af4bf88bb40..a4031ffc23b 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -82,6 +82,7 @@ const HTTP_SYNC_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_SYNC_AGGREGATOR_TIMEOUT_QUOTIENT: u32 = 24; // For DVT involving middleware only // TODO(EIP-7732): Determine what this quotient should be const HTTP_PTC_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; +const HTTP_INCLUSION_LIST_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_INCLUSION_LIST_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT: u32 = 4; @@ -105,6 +106,7 @@ pub struct Timeouts { pub sync_duties: Duration, pub sync_aggregators: Duration, pub ptc_duties: Duration, + pub inclusion_list: Duration, pub inclusion_list_duties: Duration, pub get_beacon_blocks_ssz: Duration, pub get_debug_beacon_states: Duration, @@ -128,6 +130,7 @@ impl Timeouts { sync_duties: timeout, sync_aggregators: timeout, ptc_duties: timeout, + inclusion_list: timeout, inclusion_list_duties: timeout, get_beacon_blocks_ssz: timeout, get_debug_beacon_states: timeout, @@ -153,6 +156,7 @@ impl Timeouts { sync_duties: base_timeout / HTTP_SYNC_DUTIES_TIMEOUT_QUOTIENT, sync_aggregators: base_timeout / HTTP_SYNC_AGGREGATOR_TIMEOUT_QUOTIENT, ptc_duties: base_timeout / HTTP_PTC_DUTIES_TIMEOUT_QUOTIENT, + inclusion_list: base_timeout / HTTP_INCLUSION_LIST_TIMEOUT_QUOTIENT, inclusion_list_duties: base_timeout / HTTP_INCLUSION_LIST_DUTIES_TIMEOUT_QUOTIENT, get_beacon_blocks_ssz: base_timeout / HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT, get_debug_beacon_states: base_timeout / HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT, @@ -3682,6 +3686,66 @@ impl BeaconNodeHttpClient { self.get_opt(path).await } + /// `GET validator/inclusion_list` + pub async fn get_validator_inclusion_list( + &self, + slot: Slot, + ) -> Result, Error> { + let mut path = self.eth_path(V1)?; + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("inclusion_list"); + + path.query_pairs_mut() + .append_pair("slot", &slot.to_string()); + + self.get_with_timeout(path, self.timeouts.inclusion_list) + .await + } + + /// `POST validator/inclusion_list` + pub async fn post_validator_inclusion_list( + &self, + signed_inclusion_list: &SignedInclusionList, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("inclusion_list"); + + // The request body is wrapped in a `data` object, per the beacon-APIs specs + let body = serde_json::json!({ "data": signed_inclusion_list }); + self.post_generic_with_consensus_version(path, &body, None, fork_name) + .await?; + + Ok(()) + } + + /// `POST validator/inclusion_list` (SSZ) + pub async fn post_validator_inclusion_list_ssz( + &self, + signed_inclusion_list: &SignedInclusionList, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("inclusion_list"); + + let ssz_body = signed_inclusion_list.as_ssz_bytes(); + + self.post_generic_with_consensus_version_and_ssz_body(path, ssz_body, None, fork_name) + .await?; + + Ok(()) + } + /// `POST lighthouse/liveness` pub async fn post_lighthouse_liveness( &self, diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 037c47afe6e..3f7d7b5ee12 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -904,6 +904,13 @@ impl TryFrom> for SkipRandaoVerification { } } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct InclusionListTransactions { + #[serde(with = "ssz_types::serde_utils::prog_list_of_hex_prog_var_list")] + pub transactions: ProgressiveTransactions, +} + #[derive(Clone, Serialize, Deserialize)] pub struct ValidatorAttestationDataQuery { pub slot: Slot, diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index b1fea8e1fd1..818e9bc0461 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -435,9 +435,9 @@ where if let Ok(signed_bid) = anchor_block.message().body().signed_execution_payload_bid() { // Gloas: checkpoint sync fetches the anchor's payload later. ( - ExecutionStatus::NotYetRevealed(signed_bid.message.block_hash), - Some(signed_bid.message.parent_block_hash), - Some(signed_bid.message.block_hash), + ExecutionStatus::NotYetRevealed(signed_bid.message().block_hash()), + Some(signed_bid.message().parent_block_hash()), + Some(signed_bid.message().block_hash()), ) } else if let Ok(execution_payload) = anchor_block.message().execution_payload() { // Pre-Gloas forks: do not set payload hashes, they are only used post-Gloas. @@ -839,7 +839,7 @@ where let builds_on_full = block .body() .signed_execution_payload_bid() - .is_ok_and(|bid| bid.message.parent_block_hash == parent_block_hash); + .is_ok_and(|bid| bid.message().parent_block_hash() == parent_block_hash); if builds_on_full && !self.is_payload_received(&block.parent_root()) { return Err(Error::InvalidBlock(InvalidBlock::ParentPayloadNotVerified( block.parent_root(), @@ -1065,8 +1065,8 @@ where let (execution_payload_parent_hash, execution_payload_block_hash) = if let Ok(signed_bid) = block.body().signed_execution_payload_bid() { ( - Some(signed_bid.message.parent_block_hash), - Some(signed_bid.message.block_hash), + Some(signed_bid.message().parent_block_hash()), + Some(signed_bid.message().block_hash()), ) } else { (None, None) diff --git a/consensus/state_processing/src/envelope_processing.rs b/consensus/state_processing/src/envelope_processing.rs index ff3060c7798..c6d0fd32e9d 100644 --- a/consensus/state_processing/src/envelope_processing.rs +++ b/consensus/state_processing/src/envelope_processing.rs @@ -150,16 +150,16 @@ pub fn verify_execution_payload_envelope( // Verify consistency with the committed bid let committed_bid = state.latest_execution_payload_bid()?; envelope_verify!( - envelope.builder_index == committed_bid.builder_index, + envelope.builder_index == committed_bid.builder_index(), EnvelopeProcessingError::BuilderIndexMismatch { - committed_bid: committed_bid.builder_index, + committed_bid: committed_bid.builder_index(), envelope: envelope.builder_index, } ); envelope_verify!( - committed_bid.prev_randao == payload.prev_randao, + committed_bid.prev_randao() == payload.prev_randao, EnvelopeProcessingError::PrevRandaoMismatch { - committed_bid: committed_bid.prev_randao, + committed_bid: committed_bid.prev_randao(), envelope: payload.prev_randao, } ); @@ -183,18 +183,18 @@ pub fn verify_execution_payload_envelope( // Verify the gas limit envelope_verify!( - committed_bid.gas_limit == payload.gas_limit, + committed_bid.gas_limit() == payload.gas_limit, EnvelopeProcessingError::GasLimitMismatch { - committed_bid: committed_bid.gas_limit, + committed_bid: committed_bid.gas_limit(), envelope: payload.gas_limit, } ); // Verify the block hash envelope_verify!( - committed_bid.block_hash == payload.block_hash, + committed_bid.block_hash() == payload.block_hash, EnvelopeProcessingError::BlockHashMismatch { - committed_bid: committed_bid.block_hash, + committed_bid: committed_bid.block_hash(), envelope: payload.block_hash, } ); @@ -221,9 +221,9 @@ pub fn verify_execution_payload_envelope( // Verify execution requests root matches committed bid let execution_requests_root = envelope.execution_requests.tree_hash_root(); envelope_verify!( - execution_requests_root == committed_bid.execution_requests_root, + execution_requests_root == committed_bid.execution_requests_root(), EnvelopeProcessingError::ExecutionRequestsRootMismatch { - committed_bid: committed_bid.execution_requests_root, + committed_bid: committed_bid.execution_requests_root(), envelope: execution_requests_root, } ); diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index a6914b61f1f..725942b4817 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -170,17 +170,12 @@ pub fn initialize_beacon_state_from_eth1( // The genesis block's bid must have block_hash = 0x00 per spec (empty payload). // Retain the EL genesis hash in latest_block_hash and parent_block_hash so the - // first post-genesis proposer can build on the correct EL head. - let el_genesis_hash = state.latest_execution_payload_bid()?.block_hash; - let bid = state.latest_execution_payload_bid_mut()?; + // first post-genesis proposer can build on the correct EL head. This runs on the + // Gloas genesis state; a later Heze upgrade carries these fields forward. + let bid = state.latest_execution_payload_bid_gloas_mut()?; + let el_genesis_hash = bid.block_hash; bid.parent_block_hash = el_genesis_hash; bid.block_hash = ExecutionBlockHash::default(); - - // Update the `latest_block_header.body_root` so that it matches the body of the - // Gloas genesis block, which embeds `state.latest_execution_payload_bid` in its - // `signed_execution_payload_bid` field (see `genesis_block`). - let genesis_body_root = genesis_block(&state, spec)?.body_root(); - state.latest_block_header_mut().body_root = genesis_body_root; } // Upgrade to heze if configured from genesis. @@ -194,6 +189,14 @@ pub fn initialize_beacon_state_from_eth1( state.fork_mut().previous_version = spec.heze_fork_version; } + // From Gloas onwards the genesis block body embeds `state.latest_execution_payload_bid` + // (see `genesis_block`), so `latest_block_header.body_root` must be computed against the + // final genesis fork, after every upgrade has run + if state.fork_name_unchecked().gloas_enabled() { + let genesis_body_root = genesis_block(&state, spec)?.body_root(); + state.latest_block_header_mut().body_root = genesis_body_root; + } + // Now that we have our validators, initialize the caches (including the committees) state.build_caches(spec)?; @@ -217,8 +220,22 @@ pub fn genesis_block( spec: &ChainSpec, ) -> Result, BeaconStateError> { let mut block = BeaconBlock::empty(spec); - if let Ok(signed_bid) = block.body_mut().signed_execution_payload_bid_mut() { - signed_bid.message = state.latest_execution_payload_bid()?.clone(); + match &mut block { + BeaconBlock::Base(_) + | BeaconBlock::Altair(_) + | BeaconBlock::Bellatrix(_) + | BeaconBlock::Capella(_) + | BeaconBlock::Deneb(_) + | BeaconBlock::Electra(_) + | BeaconBlock::Fulu(_) => {} + BeaconBlock::Gloas(gloas_block) => { + gloas_block.body.signed_execution_payload_bid.message = + state.latest_execution_payload_bid_gloas()?.clone(); + } + BeaconBlock::Heze(heze_block) => { + heze_block.body.signed_execution_payload_bid.message = + state.latest_execution_payload_bid_heze()?.clone(); + } } Ok(block) } diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 37bf4e16ffa..acc4d2791d9 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -195,7 +195,7 @@ pub fn per_block_processing>( if state.fork_name_unchecked().gloas_enabled() { withdrawals::gloas::process_withdrawals::(state, spec)?; let signed_bid = block.body().signed_execution_payload_bid()?; - parent_slot = Some(state.latest_execution_payload_bid()?.slot); + parent_slot = Some(state.latest_execution_payload_bid()?.slot()); process_execution_payload_bid(state, signed_bid, verify_signatures, spec)?; } else { if state.fork_name_unchecked().capella_enabled() { @@ -567,12 +567,12 @@ pub fn process_parent_execution_payload( requests: &ExecutionRequestsGloas, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - let parent_bid = state.latest_execution_payload_bid()?.clone(); - let parent_slot = parent_bid.slot; + let parent_bid = state.latest_execution_payload_bid()?; + let parent_slot = parent_bid.slot(); + let parent_value = parent_bid.value(); + let parent_fee_recipient = parent_bid.fee_recipient(); + let parent_builder_index = parent_bid.builder_index(); + let parent_block_hash = parent_bid.block_hash(); let parent_epoch = parent_slot.epoch(E::slots_per_epoch()); verify_execution_request_list_lengths(requests)?; @@ -627,16 +631,16 @@ pub fn apply_parent_execution_payload( } else if parent_epoch == state.previous_epoch() { let payment_index = parent_slot.as_u64().safe_rem(E::slots_per_epoch())? as usize; settle_builder_payment(state, payment_index)?; - } else if parent_bid.value > 0 { + } else if parent_value > 0 { // Parent is older than previous epoch -- payment entry has already been // settled or evicted by process_builder_pending_payments at epoch boundaries. // Append the withdrawal directly from the bid. state .builder_pending_withdrawals_mut()? .push(BuilderPendingWithdrawal { - fee_recipient: parent_bid.fee_recipient, - amount: parent_bid.value, - builder_index: parent_bid.builder_index, + fee_recipient: parent_fee_recipient, + amount: parent_value, + builder_index: parent_builder_index, }) .map_err(|e| BlockProcessingError::BeaconStateError(e.into()))?; } @@ -651,7 +655,7 @@ pub fn apply_parent_execution_payload( .map_err(BlockProcessingError::BitfieldError)?; // Update latest_block_hash to the parent bid's block_hash - *state.latest_block_hash_mut()? = parent_bid.block_hash; + *state.latest_block_hash_mut()? = parent_block_hash; Ok(()) } @@ -722,14 +726,14 @@ pub fn settle_builder_payment( pub fn process_execution_payload_bid( state: &mut BeaconState, - signed_bid: &SignedExecutionPayloadBid, + signed_bid: SignedExecutionPayloadBidRef, verify_signatures: VerifySignatures, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { // Verify the bid signature - let bid = &signed_bid.message; - let amount = bid.value; - let builder_index = bid.builder_index; + let bid = signed_bid.message(); + let amount = bid.value(); + let builder_index = bid.builder_index(); // For self-builds, amount must be zero regardless of withdrawal credential prefix if builder_index == BUILDER_INDEX_SELF_BUILD { @@ -738,7 +742,7 @@ pub fn process_execution_payload_bid( ExecutionPayloadBidInvalid::SelfBuildNonZeroAmount.into() ); block_verify!( - signed_bid.signature.is_infinity(), + signed_bid.signature().is_infinity(), ExecutionPayloadBidInvalid::BadSignature.into() ); } else { @@ -791,19 +795,19 @@ pub fn process_execution_payload_bid( // Verify commitments are under limit let max_blobs_per_block = spec.max_blobs_per_block(state.current_epoch()) as usize; block_verify!( - bid.blob_kzg_commitments.len() <= max_blobs_per_block, + bid.blob_kzg_commitments().len() <= max_blobs_per_block, ExecutionPayloadBidInvalid::ExcessBlobCommitments { max: max_blobs_per_block, - bid: bid.blob_kzg_commitments.len(), + bid: bid.blob_kzg_commitments().len(), } .into() ); // Verify that the bid is for the current slot block_verify!( - bid.slot == state.slot(), + bid.slot() == state.slot(), ExecutionPayloadBidInvalid::SlotMismatch { - bid_slot: bid.slot, + bid_slot: bid.slot(), state_slot: state.slot(), } .into() @@ -812,39 +816,39 @@ pub fn process_execution_payload_bid( // 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, + bid.parent_block_hash() == *latest_block_hash, ExecutionPayloadBidInvalid::ParentBlockHashMismatch { state_block_hash: *latest_block_hash, - bid_parent_hash: bid.parent_block_hash, + bid_parent_hash: bid.parent_block_hash(), } .into() ); // Verify that the bid's block hash differs from its parent block hash block_verify!( - bid.block_hash != bid.parent_block_hash, + bid.block_hash() != bid.parent_block_hash(), ExecutionPayloadBidInvalid::BlockHashEqualsParentBlockHash { - block_hash: bid.block_hash, + block_hash: bid.block_hash(), } .into() ); let expected_parent_root = *state.get_block_root(state.slot().safe_sub(1)?)?; block_verify!( - bid.parent_block_root == expected_parent_root, + bid.parent_block_root() == expected_parent_root, ExecutionPayloadBidInvalid::ParentBlockRootMismatch { block_parent_root: expected_parent_root, - bid_parent_root: bid.parent_block_root, + bid_parent_root: bid.parent_block_root(), } .into() ); let expected_randao = *state.get_randao_mix(state.current_epoch())?; block_verify!( - bid.prev_randao == expected_randao, + bid.prev_randao() == expected_randao, ExecutionPayloadBidInvalid::PrevRandaoMismatch { expected: expected_randao, - bid: bid.prev_randao, + bid: bid.prev_randao(), } .into() ); @@ -855,15 +859,18 @@ pub fn process_execution_payload_bid( let pending_payment = BuilderPendingPayment { weight: 0, withdrawal: BuilderPendingWithdrawal { - fee_recipient: bid.fee_recipient, + fee_recipient: bid.fee_recipient(), amount, builder_index, }, proposer_index, }; - let payment_index = E::SlotsPerEpoch::to_usize() - .safe_add(bid.slot.as_usize().safe_rem(E::SlotsPerEpoch::to_usize())?)?; + let payment_index = E::SlotsPerEpoch::to_usize().safe_add( + bid.slot() + .as_usize() + .safe_rem(E::SlotsPerEpoch::to_usize())?, + )?; *state .builder_pending_payments_mut()? @@ -873,8 +880,19 @@ pub fn process_execution_payload_bid( ))? = pending_payment; } - // Cache the execution bid - *state.latest_execution_payload_bid_mut()? = bid.clone(); + // Cache the execution bid (the bid's fork must match the state's fork) + match (state.latest_execution_payload_bid_mut()?, bid) { + (ExecutionPayloadBidRefMut::Gloas(bid_mut), ExecutionPayloadBidRef::Gloas(cached)) => { + *bid_mut = cached.clone(); + } + (ExecutionPayloadBidRefMut::Heze(bid_mut), ExecutionPayloadBidRef::Heze(cached)) => { + *bid_mut = cached.clone(); + } + (ExecutionPayloadBidRefMut::Gloas(_), ExecutionPayloadBidRef::Heze(_)) + | (ExecutionPayloadBidRefMut::Heze(_), ExecutionPayloadBidRef::Gloas(_)) => { + return Err(BlockProcessingError::IncorrectStateType); + } + } 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 f084e259f06..7ba71ffd750 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -12,7 +12,7 @@ use types::{ BuilderIndex, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork, IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, - SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, + SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBidRef, SignedInclusionList, SignedProposerPreferences, SignedRoot, SignedVoluntaryExit, SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; @@ -439,15 +439,15 @@ where pub fn execution_payload_bid_signature_set<'a, E, F>( state: &'a BeaconState, get_builder_pubkey: F, - signed_execution_payload_bid: &'a SignedExecutionPayloadBid, + signed_execution_payload_bid: SignedExecutionPayloadBidRef<'a, E>, spec: &'a ChainSpec, ) -> Result>> where E: EthSpec, F: Fn(BuilderIndex) -> Option>, { - let execution_payload_bid = &signed_execution_payload_bid.message; - let builder_index = execution_payload_bid.builder_index; + let execution_payload_bid = signed_execution_payload_bid.message(); + let builder_index = execution_payload_bid.builder_index(); if builder_index == BUILDER_INDEX_SELF_BUILD { // No signatures to verify in case of a self-build, but consensus code MUST check that // the signature is the point at infinity. @@ -456,8 +456,8 @@ where } let bid_epoch = signed_execution_payload_bid - .message - .slot + .message() + .slot() .epoch(E::slots_per_epoch()); let bid_fork = spec.fork_at_epoch(bid_epoch); let domain = spec.get_domain( @@ -471,7 +471,7 @@ where let message = execution_payload_bid.signing_root(domain); Ok(Some(SignatureSet::single_pubkey( - &signed_execution_payload_bid.signature, + signed_execution_payload_bid.signature(), pubkey, message, ))) diff --git a/consensus/state_processing/src/per_block_processing/tests.rs b/consensus/state_processing/src/per_block_processing/tests.rs index ea6c83ffbc7..a6047990c80 100644 --- a/consensus/state_processing/src/per_block_processing/tests.rs +++ b/consensus/state_processing/src/per_block_processing/tests.rs @@ -444,7 +444,7 @@ async fn invalid_attestation_no_committee_for_index() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -499,7 +499,7 @@ async fn invalid_attestation_wrong_justified_checkpoint() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -558,7 +558,7 @@ async fn invalid_attestation_bad_aggregation_bitfield_len() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -603,7 +603,7 @@ async fn invalid_attestation_bad_signature() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -652,7 +652,7 @@ async fn invalid_attestation_included_too_early() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), @@ -708,7 +708,7 @@ async fn invalid_attestation_target_epoch_slot_mismatch() { let parent_slot = state .latest_execution_payload_bid() .ok() - .map(|bid| bid.slot); + .map(|bid| bid.slot()); let result = process_operations::process_attestations( &mut state, head_block.body(), diff --git a/consensus/state_processing/src/per_block_processing/withdrawals.rs b/consensus/state_processing/src/per_block_processing/withdrawals.rs index 1324f7fda6e..aa29f98905f 100644 --- a/consensus/state_processing/src/per_block_processing/withdrawals.rs +++ b/consensus/state_processing/src/per_block_processing/withdrawals.rs @@ -495,7 +495,7 @@ pub mod gloas { spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { // Return early if the parent block is empty. - if *state.latest_block_hash()? != state.latest_execution_payload_bid()?.block_hash { + if *state.latest_block_hash()? != state.latest_execution_payload_bid()?.block_hash() { return Ok(()); } diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index fc1719e36a4..2d81576850d 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -12,8 +12,8 @@ use tree_hash::TreeHash; use typenum::Unsigned; use types::{ Address, BeaconState, BeaconStateError as Error, BeaconStateGloas, Builder, - BuilderPendingPayment, ChainSpec, EthSpec, ExecutionPayloadBid, ExecutionRequestsGloas, Fork, - PendingDeposit, ProgressiveKzgCommitments, + BuilderPendingPayment, ChainSpec, EthSpec, ExecutionPayloadBidGloas, ExecutionRequestsGloas, + Fork, PendingDeposit, ProgressiveKzgCommitments, consts::gloas::{BUILDER_INDEX_SELF_BUILD, PAYLOAD_BUILDER_VERSION}, is_builder_withdrawal_credential, }; @@ -120,7 +120,7 @@ pub fn upgrade_state_to_gloas( current_sync_committee: pre.current_sync_committee.clone(), next_sync_committee: pre.next_sync_committee.clone(), // Execution Bid - latest_execution_payload_bid: ExecutionPayloadBid { + latest_execution_payload_bid: ExecutionPayloadBidGloas { parent_block_hash: pre.latest_execution_payload_header.parent_hash, parent_block_root: pre.latest_block_header.parent_root, block_hash: pre.latest_execution_payload_header.block_hash, diff --git a/consensus/state_processing/src/upgrade/heze.rs b/consensus/state_processing/src/upgrade/heze.rs index e9156418c1e..dd03dcd02fc 100644 --- a/consensus/state_processing/src/upgrade/heze.rs +++ b/consensus/state_processing/src/upgrade/heze.rs @@ -1,5 +1,9 @@ +use ssz_types::BitVector; use std::mem; -use types::{BeaconState, BeaconStateError as Error, BeaconStateHeze, ChainSpec, EthSpec, Fork}; +use types::{ + BeaconState, BeaconStateError as Error, BeaconStateHeze, ChainSpec, EthSpec, + ExecutionPayloadBidHeze, Fork, +}; /// Transform a `Gloas` state into a `Heze` state. pub fn upgrade_to_heze( @@ -64,7 +68,26 @@ pub fn upgrade_state_to_heze( current_sync_committee: pre.current_sync_committee.clone(), next_sync_committee: pre.next_sync_committee.clone(), // Execution Bid - latest_execution_payload_bid: pre.latest_execution_payload_bid.clone(), + // [New in Heze:EIP7805] the bid gains `inclusion_list_bits`; carry the shared Gloas + // fields forward and default the new field to empty (no bits computed at upgrade). + latest_execution_payload_bid: { + let pre_bid = &pre.latest_execution_payload_bid; + ExecutionPayloadBidHeze { + parent_block_hash: pre_bid.parent_block_hash, + parent_block_root: pre_bid.parent_block_root, + block_hash: pre_bid.block_hash, + prev_randao: pre_bid.prev_randao, + fee_recipient: pre_bid.fee_recipient, + gas_limit: pre_bid.gas_limit, + builder_index: pre_bid.builder_index, + slot: pre_bid.slot, + value: pre_bid.value, + execution_payment: pre_bid.execution_payment, + blob_kzg_commitments: pre_bid.blob_kzg_commitments.clone(), + execution_requests_root: pre_bid.execution_requests_root, + inclusion_list_bits: BitVector::new(), + } + }, // Capella next_withdrawal_index: pre.next_withdrawal_index, next_withdrawal_validator_index: pre.next_withdrawal_validator_index, diff --git a/consensus/types/presets/gnosis/heze.yaml b/consensus/types/presets/gnosis/heze.yaml index eb4f5c75ebb..e89d35392ab 100644 --- a/consensus/types/presets/gnosis/heze.yaml +++ b/consensus/types/presets/gnosis/heze.yaml @@ -7,5 +7,7 @@ INCLUSION_LIST_COMMITTEE_SIZE: 16 # Type-specific SSZ bounds # --------------------------------------------------------------- +# 196,934 bytes, ~192 KiB +MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE: 196934 # 41,112 bytes, ~40 KiB MAX_SIGNED_INCLUSION_LIST_SIZE: 41112 diff --git a/consensus/types/presets/mainnet/heze.yaml b/consensus/types/presets/mainnet/heze.yaml index c0d9a4dd382..a685eebc35d 100644 --- a/consensus/types/presets/mainnet/heze.yaml +++ b/consensus/types/presets/mainnet/heze.yaml @@ -7,5 +7,7 @@ INCLUSION_LIST_COMMITTEE_SIZE: 16 # Type-specific SSZ bounds # --------------------------------------------------------------- +# 196,934 bytes, ~192 KiB +MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE: 196934 # 41,112 bytes, ~40 KiB MAX_SIGNED_INCLUSION_LIST_SIZE: 41112 diff --git a/consensus/types/presets/minimal/heze.yaml b/consensus/types/presets/minimal/heze.yaml index d31fa7f3a29..e987c2e1fa0 100644 --- a/consensus/types/presets/minimal/heze.yaml +++ b/consensus/types/presets/minimal/heze.yaml @@ -7,5 +7,7 @@ INCLUSION_LIST_COMMITTEE_SIZE: 16 # Type-specific SSZ bounds # --------------------------------------------------------------- +# 196,934 bytes, ~192 KiB +MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE: 196934 # 41,112 bytes, ~40 KiB MAX_SIGNED_INCLUSION_LIST_SIZE: 41112 diff --git a/consensus/types/src/block/beacon_block.rs b/consensus/types/src/block/beacon_block.rs index 368dc2e3002..f4a667acc0d 100644 --- a/consensus/types/src/block/beacon_block.rs +++ b/consensus/types/src/block/beacon_block.rs @@ -14,7 +14,7 @@ use tree_hash_derive::TreeHash; use typenum::Unsigned; use crate::{ - KzgCommitment, SignedExecutionPayloadBid, + KzgCommitment, SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, attestation::{AttestationBase, AttestationData, IndexedAttestationBase}, block::{ BeaconBlockBodyAltair, BeaconBlockBodyBase, BeaconBlockBodyBellatrix, @@ -731,7 +731,7 @@ impl> EmptyBlock for BeaconBlockGloa sync_aggregate: SyncAggregate::empty(), bls_to_execution_changes: ProgressiveVariableList::empty(), parent_execution_requests: ExecutionRequestsGloas::default(), - signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBidGloas::empty(), payload_attestations: ProgressiveVariableList::empty(), _phantom: PhantomData, }, @@ -763,7 +763,7 @@ impl> EmptyBlock for BeaconBlockHeze sync_aggregate: SyncAggregate::empty(), bls_to_execution_changes: ProgressiveVariableList::empty(), parent_execution_requests: ExecutionRequestsGloas::default(), - signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBidHeze::empty(), payload_attestations: ProgressiveVariableList::empty(), _phantom: PhantomData, }, diff --git a/consensus/types/src/block/beacon_block_body.rs b/consensus/types/src/block/beacon_block_body.rs index 8350a7f8fe0..e6f79ad2e40 100644 --- a/consensus/types/src/block/beacon_block_body.rs +++ b/consensus/types/src/block/beacon_block_body.rs @@ -13,7 +13,8 @@ use tree_hash::TreeHash; use tree_hash_derive::TreeHash; use crate::{ - ListRef, SignedExecutionPayloadBid, + ListRef, SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, + SignedExecutionPayloadBidRef, attestation::{ AttestationBase, AttestationElectra, AttestationGloas, AttestationRef, AttestationRefMut, PayloadAttestation, @@ -215,8 +216,16 @@ pub struct BeaconBlockBody = FullPay pub blob_kzg_commitments: KzgCommitments, #[superstruct(only(Electra, Fulu))] pub execution_requests: ExecutionRequestsElectra, - #[superstruct(only(Gloas, Heze))] - pub signed_execution_payload_bid: SignedExecutionPayloadBid, + #[superstruct( + only(Gloas), + partial_getter(rename = "signed_execution_payload_bid_gloas") + )] + pub signed_execution_payload_bid: SignedExecutionPayloadBidGloas, + #[superstruct( + only(Heze), + partial_getter(rename = "signed_execution_payload_bid_heze") + )] + pub signed_execution_payload_bid: SignedExecutionPayloadBidHeze, #[superstruct(only(Gloas, Heze))] pub payload_attestations: ProgressiveVariableList, E::MaxPayloadAttestations>, @@ -261,6 +270,12 @@ impl> BeaconBlockBody { pub fn fork_name(&self) -> ForkName { self.to_ref().fork_name() } + + pub fn signed_execution_payload_bid( + &self, + ) -> Result, BeaconStateError> { + self.to_ref().signed_execution_payload_bid() + } } impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, Payload> { @@ -277,6 +292,26 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, } } + pub fn signed_execution_payload_bid( + &self, + ) -> Result, BeaconStateError> { + match self { + Self::Base(_) + | Self::Altair(_) + | Self::Bellatrix(_) + | Self::Capella(_) + | Self::Deneb(_) + | Self::Electra(_) + | Self::Fulu(_) => Err(BeaconStateError::IncorrectStateVariant), + Self::Gloas(body) => Ok(SignedExecutionPayloadBidRef::Gloas( + &body.signed_execution_payload_bid, + )), + Self::Heze(body) => Ok(SignedExecutionPayloadBidRef::Heze( + &body.signed_execution_payload_bid, + )), + } + } + pub fn body_merkle_leaves(&self) -> Vec { let mut leaves = vec![]; match self { diff --git a/consensus/types/src/block/signed_beacon_block.rs b/consensus/types/src/block/signed_beacon_block.rs index 29b639c1216..4ae7e97da95 100644 --- a/consensus/types/src/block/signed_beacon_block.rs +++ b/consensus/types/src/block/signed_beacon_block.rs @@ -369,7 +369,7 @@ impl> SignedBeaconBlock self.message() .body() .signed_execution_payload_bid() - .map(|bid| bid.message.blob_kzg_commitments.len()) + .map(|bid| bid.message().blob_kzg_commitments().len()) }) .unwrap_or(0) } @@ -390,7 +390,7 @@ impl> SignedBeaconBlock self.message() .body() .signed_execution_payload_bid() - .map(|bid| bid.message.block_hash) + .map(|bid| bid.message().block_hash()) } /// Convenience accessor for the block's bid's `parent_block_hash`. @@ -400,7 +400,7 @@ impl> SignedBeaconBlock self.message() .body() .signed_execution_payload_bid() - .map(|bid| bid.message.parent_block_hash) + .map(|bid| bid.message().parent_block_hash()) } /// Check if the `parent_hash` in this block's `signed_payload_bid` matches `parent_block_hash`. @@ -416,7 +416,7 @@ impl> SignedBeaconBlock // Prior to Gloas. return false; }; - signed_payload_bid.message.parent_block_hash == parent_block_hash + signed_payload_bid.message().parent_block_hash() == parent_block_hash } } diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index ce071989d80..33ab23aede0 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -135,6 +135,7 @@ pub struct ChainSpec { sync_message_due_gloas: Duration, contribution_and_proof_due: Duration, contribution_and_proof_due_gloas: Duration, + inclusion_list_due: Duration, /* * Reward and penalty quotients @@ -1025,6 +1026,12 @@ impl ChainSpec { } } + /// Spec: `get_inclusion_list_due_ms`. + /// Get the duration into a slot in which an inclusion list production duty is due. + pub fn get_inclusion_list_due(&self) -> Duration { + self.inclusion_list_due + } + /// Calculate the duration into a slot for a given slot component pub fn compute_slot_component_duration( &self, @@ -1141,6 +1148,9 @@ impl ChainSpec { self.contribution_and_proof_due_gloas = self .compute_slot_component_duration(self.contribution_due_bps_gloas) .expect("invalid chain spec: cannot compute contribution_and_proof_due_gloas"); + self.inclusion_list_due = self + .compute_slot_component_duration(self.inclusion_list_due_bps) + .expect("invalid chain spec: cannot compute inclusion_list_due"); self.attestation_subnet_prefix_bits = compute_attestation_subnet_prefix_bits( self.attestation_subnet_count, @@ -1286,6 +1296,7 @@ impl ChainSpec { sync_message_due_gloas: Duration::from_millis(3000), contribution_and_proof_due: Duration::from_millis(8000), contribution_and_proof_due_gloas: Duration::from_millis(6000), + inclusion_list_due: Duration::from_millis(8000), /* * Reward and penalty quotients @@ -1634,6 +1645,7 @@ impl ChainSpec { sync_message_due_gloas: Duration::from_millis(1500), contribution_and_proof_due: Duration::from_millis(4000), contribution_and_proof_due_gloas: Duration::from_millis(3000), + inclusion_list_due: Duration::from_millis(4000), // Networking Fulu blob_schedule: BlobSchedule::default(), @@ -1740,6 +1752,7 @@ impl ChainSpec { sync_message_due_gloas: Duration::from_millis(1250), contribution_and_proof_due: Duration::from_millis(3333), contribution_and_proof_due_gloas: Duration::from_millis(2500), + inclusion_list_due: Duration::from_millis(3333), /* * Reward and penalty quotients diff --git a/consensus/types/src/core/eth_spec.rs b/consensus/types/src/core/eth_spec.rs index 8aef0618c78..9e0dc91ddab 100644 --- a/consensus/types/src/core/eth_spec.rs +++ b/consensus/types/src/core/eth_spec.rs @@ -460,6 +460,9 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + /// Returns the `MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE` constant for this specification. fn max_signed_execution_payload_bid_size() -> usize; + /// Returns the `MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE` constant for this specification. + fn max_signed_execution_payload_bid_size_heze() -> usize; + /// Returns the `MAX_SIGNED_INCLUSION_LIST_SIZE` constant for this specification. fn max_signed_inclusion_list_size() -> usize; @@ -575,6 +578,10 @@ impl EthSpec for MainnetEthSpec { 196932 } + fn max_signed_execution_payload_bid_size_heze() -> usize { + 196934 + } + fn max_signed_inclusion_list_size() -> usize { 41112 } @@ -671,6 +678,10 @@ impl EthSpec for MinimalEthSpec { 196932 } + fn max_signed_execution_payload_bid_size_heze() -> usize { + 196934 + } + fn max_signed_inclusion_list_size() -> usize { 41112 } @@ -764,6 +775,10 @@ impl EthSpec for GnosisEthSpec { 196932 } + fn max_signed_execution_payload_bid_size_heze() -> usize { + 196934 + } + fn max_signed_inclusion_list_size() -> usize { 41112 } diff --git a/consensus/types/src/core/preset.rs b/consensus/types/src/core/preset.rs index 9bf15e3ef83..0afb1664c5b 100644 --- a/consensus/types/src/core/preset.rs +++ b/consensus/types/src/core/preset.rs @@ -374,6 +374,8 @@ pub struct HezePreset { #[serde(with = "serde_utils::quoted_u64")] pub inclusion_list_committee_size: u64, #[serde(with = "serde_utils::quoted_u64")] + pub max_signed_execution_payload_bid_size_heze: u64, + #[serde(with = "serde_utils::quoted_u64")] pub max_signed_inclusion_list_size: u64, } @@ -381,6 +383,8 @@ impl HezePreset { pub fn from_chain_spec(_spec: &ChainSpec) -> Self { Self { inclusion_list_committee_size: E::inclusion_list_committee_size() as u64, + max_signed_execution_payload_bid_size_heze: + E::max_signed_execution_payload_bid_size_heze() as u64, max_signed_inclusion_list_size: E::max_signed_inclusion_list_size() as u64, } } diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index 31b29301160..8072ce565aa 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -1,52 +1,219 @@ use crate::kzg_ext::ProgressiveKzgCommitments; -use crate::{Address, EthSpec, ExecutionBlockHash, ForkName, Hash256, SignedRoot, Slot}; -use context_deserialize::context_deserialize; +use crate::{ + Address, BeaconStateError, ChainSpec, EthSpec, ExecutionBlockHash, ForkName, ForkVersionDecode, + Hash256, InconsistentFork, SignedRoot, Slot, +}; +use context_deserialize::{ContextDeserialize, context_deserialize}; use educe::Educe; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; +use ssz::Decode; use ssz_derive::{Decode, Encode}; +use ssz_types::BitVector; +use superstruct::superstruct; use tree_hash_derive::TreeHash; -#[derive(Default, Debug, Clone, Serialize, Encode, Decode, Deserialize, TreeHash, Educe)] +#[superstruct( + variants(Gloas, Heze), + variant_attributes( + derive( + Default, + Debug, + Clone, + Serialize, + Deserialize, + Encode, + Decode, + TreeHash, + Educe, + ), + context_deserialize(ForkName), + educe(PartialEq, Hash(bound(E: EthSpec))), + serde(bound = "E: EthSpec", deny_unknown_fields), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), + ), + ref_attributes(derive(Debug, PartialEq, TreeHash), tree_hash(enum_behaviour = "transparent")), + specific_variant_attributes( + Gloas(tree_hash( + struct_behaviour = "progressive_container", + active_fields(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1) + )), + Heze(tree_hash( + struct_behaviour = "progressive_container", + active_fields(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1) + )) + ), + cast_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ), + partial_getter_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ) +)] #[cfg_attr( feature = "arbitrary", derive(arbitrary::Arbitrary), arbitrary(bound = "E: EthSpec") )] -#[educe(PartialEq, Hash)] -#[serde(bound = "E: EthSpec")] -#[context_deserialize(ForkName)] -// https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#executionpayloadbid -#[tree_hash( - struct_behaviour = "progressive_container", - active_fields(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1) -)] +#[derive(Debug, Clone, Serialize, Encode, TreeHash, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] +#[serde(bound = "E: EthSpec", untagged)] +#[ssz(enum_behaviour = "transparent")] +#[tree_hash(enum_behaviour = "transparent")] +// https://github.com/ethereum/consensus-specs/blob/master/specs/heze/beacon-chain.md#executionpayloadbid pub struct ExecutionPayloadBid { + #[superstruct(getter(copy))] pub parent_block_hash: ExecutionBlockHash, + #[superstruct(getter(copy))] pub parent_block_root: Hash256, + #[superstruct(getter(copy))] pub block_hash: ExecutionBlockHash, + #[superstruct(getter(copy))] pub prev_randao: Hash256, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::address_hex")] pub fee_recipient: Address, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub gas_limit: u64, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub builder_index: u64, + #[superstruct(getter(copy))] pub slot: Slot, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub value: u64, + #[superstruct(getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub execution_payment: u64, // [Modified in Gloas:EIP7688] pub blob_kzg_commitments: ProgressiveKzgCommitments, + #[superstruct(getter(copy))] pub execution_requests_root: Hash256, + // [New in Heze:EIP7805] + #[superstruct(only(Heze))] + pub inclusion_list_bits: BitVector, } impl SignedRoot for ExecutionPayloadBid {} +impl<'a, E: EthSpec> SignedRoot for ExecutionPayloadBidRef<'a, E> {} + +impl ForkVersionDecode for ExecutionPayloadBid { + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + match fork_name { + ForkName::Base + | ForkName::Altair + | ForkName::Bellatrix + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu => Err(ssz::DecodeError::BytesInvalid(format!( + "unsupported fork for ExecutionPayloadBid: {fork_name}" + ))), + ForkName::Gloas => ExecutionPayloadBidGloas::from_ssz_bytes(bytes).map(Self::Gloas), + ForkName::Heze => ExecutionPayloadBidHeze::from_ssz_bytes(bytes).map(Self::Heze), + } + } +} + +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for ExecutionPayloadBid { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + let convert_err = |e| { + serde::de::Error::custom(format!( + "ExecutionPayloadBid failed to deserialize: {:?}", + e + )) + }; + Ok(match context { + ForkName::Base + | ForkName::Altair + | ForkName::Bellatrix + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu => { + return Err(serde::de::Error::custom(format!( + "ExecutionPayloadBid failed to deserialize: unsupported fork '{}'", + context + ))); + } + ForkName::Gloas => { + Self::Gloas(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Heze => { + Self::Heze(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + }) + } +} + +impl<'a, E: EthSpec> ExecutionPayloadBidRef<'a, E> { + /// Returns the name of the fork pertaining to `self`. + /// + /// Will return an `Err` if `self` has been instantiated to a variant conflicting with the fork + /// dictated by `self.slot()`. + pub fn fork_name(&self, spec: &ChainSpec) -> Result { + let fork_at_slot = spec.fork_name_at_slot::(self.slot()); + let object_fork = self.fork_name_unchecked(); + + if fork_at_slot == object_fork { + Ok(object_fork) + } else { + Err(InconsistentFork { + fork_at_slot, + object_fork, + }) + } + } + + /// Returns the name of the fork pertaining to `self`. + /// Does not check that the fork is consistent with the slot. + pub fn fork_name_unchecked(&self) -> ForkName { + match self { + ExecutionPayloadBidRef::Gloas(_) => ForkName::Gloas, + ExecutionPayloadBidRef::Heze(_) => ForkName::Heze, + } + } +} #[cfg(test)] -mod tests { +mod gloas_tests { use super::*; use crate::Spec; - ssz_and_tree_hash_tests!(ExecutionPayloadBid); + ssz_and_tree_hash_tests!(ExecutionPayloadBidGloas); +} + +#[cfg(test)] +mod heze_tests { + use super::*; + use crate::Spec; + + ssz_and_tree_hash_tests!(ExecutionPayloadBidHeze); + + #[test] + fn inclusion_list_bits_committed_to_signing_root() { + let bid = ExecutionPayloadBidHeze::::default(); + let mut bid_with_bits = bid.clone(); + bid_with_bits + .inclusion_list_bits + .set(0, true) + .expect("bit index should be within the committee size"); + + let domain = Hash256::ZERO; + assert_ne!( + ExecutionPayloadBidRef::Heze(&bid).signing_root(domain), + ExecutionPayloadBidRef::Heze(&bid_with_bits).signing_root(domain), + "inclusion_list_bits must be covered by the Heze bid signing root", + ); + } } diff --git a/consensus/types/src/execution/execution_proof.rs b/consensus/types/src/execution/execution_proof.rs index 2cc34160f32..c951eb67366 100644 --- a/consensus/types/src/execution/execution_proof.rs +++ b/consensus/types/src/execution/execution_proof.rs @@ -12,28 +12,29 @@ pub const MAX_PROOF_SIZE: usize = 4_194_304; /// SSZ bound for `proof_data`. pub type MaxProofSize = typenum::U4194304; -/// Opaque proof bytes. -/// -/// The EIP-8025 spec defines this as `ProgressiveByteList` (EIP-7916), which is not yet -/// supported by `ssz_types`. A `VariableList` serializes identically but merkleizes -/// differently, so signing roots are not interoperable with spec-conformant clients. +/// Spec type `ProofData`. pub type ProofData = VariableList; -/// Identifier for the proof system that produced a proof (EIP-8025 `ProofType`). +/// Spec type `ProofType`. pub type ProofType = u8; +/// Spec constant `STATELESS_INPUT_SCHEMA_ID`: Amsterdam fork (`0x15`), schema revision (`0x01`). +pub const STATELESS_INPUT_SCHEMA_ID: u16 = 0x1501; + +/// Spec type `PublicInput`. #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Encode, Decode, TreeHash)] #[context_deserialize(ForkName)] +#[tree_hash(struct_behaviour = "progressive_container", active_fields(1, 1, 1, 1))] pub struct PublicInput { pub new_payload_request_root: Hash256, + pub successful_validation: bool, + #[serde(with = "serde_utils::quoted_u64")] + pub chain_id: u64, + pub schema_id: u16, } -/// An execution proof attesting to the validity of an execution payload (EIP-8025). -/// -/// Deviation from the spec: `beacon_block_root` binds the proof to the beacon block whose -/// envelope committed the payload, allowing the proof to be resolved without an index from -/// `new_payload_request_root` to block root. +/// Spec type `ExecutionProof`. #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Encode, Decode, TreeHash)] #[context_deserialize(ForkName)] @@ -41,22 +42,31 @@ pub struct ExecutionProof { pub proof_data: ProofData, pub proof_type: ProofType, pub public_input: PublicInput, +} + +/// Spec type `ExecutionProofEnvelope`. +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Encode, Decode, TreeHash)] +#[context_deserialize(ForkName)] +pub struct ExecutionProofEnvelope { + pub proof_data: ProofData, + pub proof_type: ProofType, pub beacon_block_root: Hash256, } -impl SignedRoot for ExecutionProof {} +impl SignedRoot for ExecutionProofEnvelope {} #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Encode, Decode, TreeHash)] #[context_deserialize(ForkName)] -pub struct SignedExecutionProof { - pub message: ExecutionProof, +pub struct SignedExecutionProofEnvelope { + pub message: ExecutionProofEnvelope, #[serde(with = "serde_utils::quoted_u64")] pub validator_index: u64, pub signature: Signature, } -impl SignedExecutionProof { +impl SignedExecutionProofEnvelope { pub fn beacon_block_root(&self) -> Hash256 { self.message.beacon_block_root } @@ -70,5 +80,5 @@ impl SignedExecutionProof { mod tests { use super::*; - ssz_and_tree_hash_tests!(SignedExecutionProof); + ssz_and_tree_hash_tests!(SignedExecutionProofEnvelope); } diff --git a/consensus/types/src/execution/mod.rs b/consensus/types/src/execution/mod.rs index cae31eda45a..f0d5bfb4e83 100644 --- a/consensus/types/src/execution/mod.rs +++ b/consensus/types/src/execution/mod.rs @@ -26,7 +26,10 @@ pub use execution_payload::{ ExecutionPayloadHeze, ExecutionPayloadRef, ProgressiveTransactions, ProgressiveWithdrawals, Transaction, Transactions, TransactionsIter, TransactionsRef, WithdrawalsRef, }; -pub use execution_payload_bid::ExecutionPayloadBid; +pub use execution_payload_bid::{ + ExecutionPayloadBid, ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef, + ExecutionPayloadBidRefMut, +}; pub use execution_payload_envelope::ExecutionPayloadEnvelope; pub use execution_payload_header::{ ExecutionPayloadHeader, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, @@ -34,8 +37,8 @@ pub use execution_payload_header::{ ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, }; pub use execution_proof::{ - ExecutionProof, MAX_PROOF_SIZE, MaxProofSize, ProofData, ProofType, PublicInput, - SignedExecutionProof, + ExecutionProof, ExecutionProofEnvelope, MAX_PROOF_SIZE, MaxProofSize, ProofData, ProofType, + PublicInput, STATELESS_INPUT_SCHEMA_ID, SignedExecutionProofEnvelope, }; pub use execution_requests::{ BuilderDepositRequests, BuilderExitRequests, ConsolidationRequests, DepositRequests, @@ -48,10 +51,13 @@ pub use payload::{ BlindedPayloadDeneb, BlindedPayloadElectra, BlindedPayloadFulu, BlindedPayloadRef, BlockProductionVersion, BlockType, ExecPayload, FullPayload, FullPayloadBellatrix, FullPayloadCapella, FullPayloadDeneb, FullPayloadElectra, FullPayloadFulu, FullPayloadRef, - OwnedExecPayload, + NewPayloadRequest, OwnedExecPayload, VersionedHashes, }; pub use signed_bls_to_execution_change::SignedBlsToExecutionChange; -pub use signed_execution_payload_bid::SignedExecutionPayloadBid; +pub use signed_execution_payload_bid::{ + SignedExecutionPayloadBid, SignedExecutionPayloadBidGloas, SignedExecutionPayloadBidHeze, + SignedExecutionPayloadBidRef, +}; pub use signed_execution_payload_envelope::{ ExecutionPayloadBody, ExecutionPayloadHeaderGloas, SignedExecutionPayloadEnvelope, SignedExecutionPayloadEnvelopeSummary, diff --git a/consensus/types/src/execution/payload.rs b/consensus/types/src/execution/payload.rs index 86376c9b9ec..45706f8a9b6 100644 --- a/consensus/types/src/execution/payload.rs +++ b/consensus/types/src/execution/payload.rs @@ -1,3 +1,4 @@ +use context_deserialize::context_deserialize; use educe::Educe; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -14,9 +15,9 @@ use crate::{ execution::{ ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadFulu, - ExecutionPayloadHeader, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, - ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, - ExecutionPayloadRef, Transactions, + ExecutionPayloadGloas, ExecutionPayloadHeader, ExecutionPayloadHeaderBellatrix, + ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, + ExecutionPayloadHeaderFulu, ExecutionPayloadRef, ExecutionRequestsGloas, Transactions, }, fork::ForkName, map_execution_payload_into_blinded_payload, map_execution_payload_into_full_payload, @@ -1110,3 +1111,18 @@ pub enum BlockProductionVersion { BlindedV2, FullV2, } + +/// Spec type `VersionedHashes`. +pub type VersionedHashes = VariableList::MaxBlobCommitmentsPerBlock>; + +/// Spec type `NewPayloadRequest`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TreeHash)] +#[serde(bound = "E: EthSpec")] +#[context_deserialize(ForkName)] +#[tree_hash(struct_behaviour = "progressive_container", active_fields(1, 1, 1, 1))] +pub struct NewPayloadRequest { + pub execution_payload: ExecutionPayloadGloas, + pub versioned_hashes: VersionedHashes, + pub parent_beacon_block_root: Hash256, + pub execution_requests: ExecutionRequestsGloas, +} diff --git a/consensus/types/src/execution/signed_execution_payload_bid.rs b/consensus/types/src/execution/signed_execution_payload_bid.rs index 850dbc9b937..9d095a9737e 100644 --- a/consensus/types/src/execution/signed_execution_payload_bid.rs +++ b/consensus/types/src/execution/signed_execution_payload_bid.rs @@ -1,52 +1,194 @@ -use crate::execution::ExecutionPayloadBid; -use crate::{EthSpec, ForkName}; +use crate::execution::{ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef}; +use crate::{ChainSpec, Epoch, EthSpec, ForkName, ForkVersionDecode, InconsistentFork, Slot}; use bls::Signature; -use context_deserialize::context_deserialize; +use context_deserialize::{ContextDeserialize, context_deserialize}; use educe::Educe; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; +use ssz::Decode; use ssz_derive::{Decode, Encode}; +use superstruct::superstruct; use tree_hash_derive::TreeHash; -#[derive(TreeHash, Debug, Clone, Encode, Decode, Serialize, Deserialize, Educe)] +/// An `ExecutionPayloadBid` and a signature from its builder. +#[superstruct( + variants(Gloas, Heze), + variant_attributes( + derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, Educe), + educe(PartialEq, Hash(bound(E: EthSpec))), + serde(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), + context_deserialize(ForkName), + ) +)] #[cfg_attr( feature = "arbitrary", derive(arbitrary::Arbitrary), arbitrary(bound = "E: EthSpec") )] -#[educe(PartialEq, Hash)] -#[serde(bound = "E: EthSpec")] -#[context_deserialize(ForkName)] -// https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadbid +#[derive(Debug, Clone, Serialize, Encode, TreeHash, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] +#[serde(bound = "E: EthSpec", untagged)] +#[ssz(enum_behaviour = "transparent")] +#[tree_hash(enum_behaviour = "transparent")] +// https://github.com/ethereum/consensus-specs/blob/master/specs/heze/beacon-chain.md#signedexecutionpayloadbid pub struct SignedExecutionPayloadBid { - pub message: ExecutionPayloadBid, + #[superstruct(only(Gloas), partial_getter(rename = "message_gloas"))] + pub message: ExecutionPayloadBidGloas, + #[superstruct(only(Heze), partial_getter(rename = "message_heze"))] + pub message: ExecutionPayloadBidHeze, pub signature: Signature, } impl SignedExecutionPayloadBid { - pub fn epoch(&self) -> crate::Epoch { - self.message.slot.epoch(E::slots_per_epoch()) + /// The bid message as a fork-agnostic reference. + pub fn message(&self) -> ExecutionPayloadBidRef<'_, E> { + match self { + Self::Gloas(inner) => ExecutionPayloadBidRef::Gloas(&inner.message), + Self::Heze(inner) => ExecutionPayloadBidRef::Heze(&inner.message), + } + } + + pub fn fork_name(&self, spec: &ChainSpec) -> Result { + self.message().fork_name(spec) } pub fn slot(&self) -> crate::Slot { - self.message.slot + self.message().slot() + } + + pub fn epoch(&self) -> crate::Epoch { + self.slot().epoch(E::slots_per_epoch()) + } + + pub fn num_blobs_expected(&self) -> usize { + self.message().blob_kzg_commitments().len() } +} +impl SignedExecutionPayloadBidGloas { pub fn empty() -> Self { Self { - message: ExecutionPayloadBid::default(), + message: ExecutionPayloadBidGloas::default(), signature: Signature::empty(), } } +} + +impl SignedExecutionPayloadBidHeze { + pub fn empty() -> Self { + Self { + message: ExecutionPayloadBidHeze::default(), + signature: Signature::empty(), + } + } +} + +impl<'a, E: EthSpec> SignedExecutionPayloadBidRef<'a, E> { + pub fn slot(&self) -> Slot { + self.message().slot() + } + + pub fn epoch(&self) -> Epoch { + self.slot().epoch(E::slots_per_epoch()) + } pub fn num_blobs_expected(&self) -> usize { - self.message.blob_kzg_commitments.len() + self.message().blob_kzg_commitments().len() + } + + /// The bid message as a fork-agnostic reference. + pub fn message(&self) -> ExecutionPayloadBidRef<'a, E> { + match self { + SignedExecutionPayloadBidRef::Gloas(inner) => { + ExecutionPayloadBidRef::Gloas(&inner.message) + } + SignedExecutionPayloadBidRef::Heze(inner) => { + ExecutionPayloadBidRef::Heze(&inner.message) + } + } } + + /// Clone this reference into an owned `SignedExecutionPayloadBid`. + pub fn clone_as_signed_execution_payload_bid(self) -> SignedExecutionPayloadBid { + match self { + SignedExecutionPayloadBidRef::Gloas(bid) => { + SignedExecutionPayloadBid::Gloas(bid.clone()) + } + SignedExecutionPayloadBidRef::Heze(bid) => SignedExecutionPayloadBid::Heze(bid.clone()), + } + } +} + +impl ForkVersionDecode for SignedExecutionPayloadBid { + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + match fork_name { + ForkName::Base + | ForkName::Altair + | ForkName::Bellatrix + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu => Err(ssz::DecodeError::BytesInvalid(format!( + "unsupported fork for SignedExecutionPayloadBid: {fork_name}" + ))), + ForkName::Gloas => { + SignedExecutionPayloadBidGloas::from_ssz_bytes(bytes).map(Self::Gloas) + } + ForkName::Heze => SignedExecutionPayloadBidHeze::from_ssz_bytes(bytes).map(Self::Heze), + } + } +} + +impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for SignedExecutionPayloadBid { + fn context_deserialize(deserializer: D, context: ForkName) -> Result + where + D: Deserializer<'de>, + { + let convert_err = |e| { + serde::de::Error::custom(format!( + "SignedExecutionPayloadBid failed to deserialize: {:?}", + e + )) + }; + Ok(match context { + ForkName::Base + | ForkName::Altair + | ForkName::Bellatrix + | ForkName::Capella + | ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu => { + return Err(serde::de::Error::custom(format!( + "SignedExecutionPayloadBid failed to deserialize: unsupported fork '{}'", + context + ))); + } + ForkName::Gloas => { + Self::Gloas(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + ForkName::Heze => { + Self::Heze(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } + }) + } +} + +#[cfg(test)] +mod gloas_tests { + use super::*; + use crate::Spec; + + ssz_and_tree_hash_tests!(SignedExecutionPayloadBidGloas); } #[cfg(test)] -mod tests { +mod heze_tests { use super::*; use crate::Spec; - ssz_and_tree_hash_tests!(SignedExecutionPayloadBid); + ssz_and_tree_hash_tests!(SignedExecutionPayloadBidHeze); } diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 2067f39416d..81b32545ed3 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -23,7 +23,8 @@ use tree_hash_derive::TreeHash; use typenum::Unsigned; use crate::{ - ExecutionBlockHash, ExecutionPayloadBid, Withdrawal, + ExecutionBlockHash, ExecutionPayloadBidGloas, ExecutionPayloadBidHeze, ExecutionPayloadBidRef, + ExecutionPayloadBidRefMut, Withdrawal, attestation::{ AttestationData, AttestationDuty, BeaconCommittee, Checkpoint, CommitteeIndex, PTC, ParticipationFlags, PendingAttestation, @@ -783,9 +784,19 @@ where pub builder_pending_withdrawals: ProgressiveList, #[cfg_attr(feature = "arbitrary", arbitrary(default))] - #[superstruct(only(Gloas, Heze))] + #[superstruct( + only(Gloas), + partial_getter(rename = "latest_execution_payload_bid_gloas") + )] + #[metastruct(exclude_from(tree_lists))] + pub latest_execution_payload_bid: ExecutionPayloadBidGloas, + #[cfg_attr(feature = "arbitrary", arbitrary(default))] + #[superstruct( + only(Heze), + partial_getter(rename = "latest_execution_payload_bid_heze") + )] #[metastruct(exclude_from(tree_lists))] - pub latest_execution_payload_bid: ExecutionPayloadBid, + pub latest_execution_payload_bid: ExecutionPayloadBidHeze, #[compare_fields(as_iter)] #[cfg_attr(feature = "arbitrary", arbitrary(default))] @@ -1556,6 +1567,47 @@ impl BeaconState { } } + /// Convenience accessor for `latest_execution_payload_bid` as an `ExecutionPayloadBidRef`. + pub fn latest_execution_payload_bid( + &self, + ) -> Result, BeaconStateError> { + match self { + BeaconState::Base(_) + | BeaconState::Altair(_) + | BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) + | BeaconState::Fulu(_) => Err(BeaconStateError::IncorrectStateVariant), + BeaconState::Gloas(state) => Ok(ExecutionPayloadBidRef::Gloas( + &state.latest_execution_payload_bid, + )), + BeaconState::Heze(state) => Ok(ExecutionPayloadBidRef::Heze( + &state.latest_execution_payload_bid, + )), + } + } + + pub fn latest_execution_payload_bid_mut( + &mut self, + ) -> Result, BeaconStateError> { + match self { + BeaconState::Base(_) + | BeaconState::Altair(_) + | BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) + | BeaconState::Fulu(_) => Err(BeaconStateError::IncorrectStateVariant), + BeaconState::Gloas(state) => Ok(ExecutionPayloadBidRefMut::Gloas( + &mut state.latest_execution_payload_bid, + )), + BeaconState::Heze(state) => Ok(ExecutionPayloadBidRefMut::Heze( + &mut state.latest_execution_payload_bid, + )), + } + } + /// Return `true` if the validator who produced `slot_signature` is eligible to aggregate. /// /// Spec v0.12.1 diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 2fc9f21c42a..2175d6ef941 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -92,6 +92,9 @@ "tests/.*/.*/networking/gossip_partial_data_column_sidecar/.*", ] +# Extra arguments are ignored paths. Useful for skipping another preset's vectors. +excluded_paths += sys.argv[3:] + def normalize_path(path): return path.split("consensus-spec-tests/")[1] diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 54119eeb551..27133b04989 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -64,7 +64,7 @@ pub struct VoluntaryExitChurn { } /// Newtype for testing execution payload bids. -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone)] pub struct ExecutionPayloadBidBlock { signed_bid: SignedExecutionPayloadBid, } @@ -133,7 +133,7 @@ impl Operation for Attestation { initialize_progressive_balances_cache(state, spec)?; let mut ctxt = ConsensusContext::new(state.slot()); if state.fork_name_unchecked().gloas_enabled() { - let parent_slot = Some(state.latest_execution_payload_bid()?.slot); + let parent_slot = Some(state.latest_execution_payload_bid()?.slot()); gloas::process_attestation( state, self.to_ref(), @@ -551,8 +551,11 @@ impl Operation for ExecutionPayloadBidBlock { fork_name.gloas_enabled() } - fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { - ssz_decode_file(path).map(|signed_bid| ExecutionPayloadBidBlock { signed_bid }) + fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { + ssz_decode_file_with(path, |bytes| { + SignedExecutionPayloadBid::from_ssz_bytes_by_fork(bytes, fork_name) + }) + .map(|signed_bid| ExecutionPayloadBidBlock { signed_bid }) } fn apply_to( @@ -561,7 +564,12 @@ impl Operation for ExecutionPayloadBidBlock { spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { - process_execution_payload_bid(state, &self.signed_bid, VerifySignatures::True, spec)?; + process_execution_payload_bid( + state, + self.signed_bid.to_ref(), + VerifySignatures::True, + spec, + )?; Ok(()) } } diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index 133bbc42d01..c1446e0d228 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -98,7 +98,11 @@ type_name_generic!(ExecutionPayloadHeaderDeneb, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderElectra, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderFulu, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadBid); +type_name_generic!(ExecutionPayloadBidGloas, "ExecutionPayloadBid"); +type_name_generic!(ExecutionPayloadBidHeze, "ExecutionPayloadBid"); type_name_generic!(SignedExecutionPayloadBid); +type_name_generic!(SignedExecutionPayloadBidGloas, "SignedExecutionPayloadBid"); +type_name_generic!(SignedExecutionPayloadBidHeze, "SignedExecutionPayloadBid"); type_name_generic!(ExecutionRequestsElectra, "ExecutionRequests"); type_name_generic!(ExecutionRequestsGloas, "ExecutionRequests"); type_name_generic!(ExecutionPayloadEnvelope); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 1a166f18f88..61de98ebba2 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -23,160 +23,134 @@ fn check_typenum_values() { #[test] fn derived_typenum_values() { - check_typenum_values::(); - check_typenum_values::(); + check_typenum_values::(); } #[test] fn shuffling() { - ShufflingHandler::::default().run(); - ShufflingHandler::::default().run(); + ShufflingHandler::::default().run(); } #[test] fn operations_deposit() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn operations_exit() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn operations_proposer_slashing() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn operations_attester_slashing() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_attestation() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_block_header() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_sync_aggregate() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_execution_payload_full() { - OperationsHandler::>>::default().run(); - OperationsHandler::>>::default().run(); + OperationsHandler::>>::default().run(); } #[test] fn operations_execution_payload_blinded() { - OperationsHandler::>>::default().run(); - OperationsHandler::>>::default().run(); + OperationsHandler::>>::default().run(); } #[test] fn operations_execution_payload_envelope() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_execution_payload_bid() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_parent_execution_payload() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_payload_attestation() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_withdrawals() { - OperationsHandler::>::default().run(); - OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); } #[test] fn operations_withdrawal_requests() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] #[cfg(not(feature = "fake_crypto"))] fn operations_deposit_requests() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn operations_consolidations() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] #[cfg(not(feature = "fake_crypto"))] fn operations_builder_deposit_requests() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn operations_builder_exit_requests() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn operations_bls_to_execution_change() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn operations_voluntary_exit_churn() { - OperationsHandler::::default().run(); - OperationsHandler::::default().run(); + OperationsHandler::::default().run(); } #[test] fn sanity_blocks() { - SanityBlocksHandler::::default().run(); - SanityBlocksHandler::::default().run(); + SanityBlocksHandler::::default().run(); } #[test] fn sanity_slots() { - SanitySlotsHandler::::default().run(); - SanitySlotsHandler::::default().run(); + SanitySlotsHandler::::default().run(); } #[test] fn random() { - RandomHandler::::default().run(); - RandomHandler::::default().run(); + RandomHandler::::default().run(); } #[test] @@ -237,7 +211,7 @@ macro_rules! ssz_static_test { /// Generate a function to run the SSZ static tests for a type. /// -/// Quite complex in order to support an optional #[test] attrib, generics, and the two EthSpecs. +/// Quite complex in order to support an optional #[test] attrib and generics. #[cfg(feature = "fake_crypto")] macro_rules! ssz_static_test_no_run { // Top-level @@ -251,8 +225,7 @@ macro_rules! ssz_static_test_no_run { $test_name, $handler, { - ($typ, MinimalEthSpec), - ($typ, MainnetEthSpec) + ($typ, Spec) } ); }; @@ -263,8 +236,7 @@ macro_rules! ssz_static_test_no_run { $test_name, $handler, { - ($typ, MinimalEthSpec), - ($typ, MainnetEthSpec) + ($typ, Spec) } ); }; @@ -285,11 +257,10 @@ mod ssz_static { use types::state::HistoricalSummary; use types::{ AttesterSlashingBase, AttesterSlashingElectra, Builder, BuilderPendingPayment, - BuilderPendingWithdrawal, ConsolidationRequest, DepositRequest, ExecutionPayloadBid, - ExecutionPayloadEnvelope, IndexedPayloadAttestation, LightClientBootstrapAltair, - PayloadAttestation, PayloadAttestationData, PayloadAttestationMessage, PendingDeposit, - PendingPartialWithdrawal, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, - WithdrawalRequest, *, + BuilderPendingWithdrawal, ConsolidationRequest, DepositRequest, ExecutionPayloadEnvelope, + IndexedPayloadAttestation, LightClientBootstrapAltair, PayloadAttestation, + PayloadAttestationData, PayloadAttestationMessage, PendingDeposit, + PendingPartialWithdrawal, SignedExecutionPayloadEnvelope, WithdrawalRequest, *, }; ssz_static_test!(attestation_data, AttestationData); @@ -308,14 +279,12 @@ mod ssz_static { // Altair and Bellatrix. #[test] fn historical_batch() { - SszStaticHandler::, MinimalEthSpec>::pre_capella().run(); - SszStaticHandler::, MainnetEthSpec>::pre_capella().run(); + SszStaticHandler::, Spec>::pre_capella().run(); } // `PendingAttestation` was removed in Altair, so test vectors only exist for Base. #[test] fn pending_attestation() { - SszStaticHandler::, MinimalEthSpec>::base_only().run(); - SszStaticHandler::, MainnetEthSpec>::base_only().run(); + SszStaticHandler::, Spec>::base_only().run(); } ssz_static_test!(proposer_slashing, ProposerSlashing); ssz_static_test!( @@ -331,802 +300,494 @@ mod ssz_static { #[test] fn attestation() { - SszStaticHandler::, MinimalEthSpec>::pre_electra().run(); - SszStaticHandler::, MainnetEthSpec>::pre_electra().run(); - SszStaticHandler::, MinimalEthSpec>::electra_through_fulu() - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_through_fulu() - .run(); - SszStaticHandler::, MinimalEthSpec>::gloas_and_later() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later() - .run(); + SszStaticHandler::, Spec>::pre_electra().run(); + SszStaticHandler::, Spec>::electra_through_fulu().run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn single_attestation() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] fn attester_slashing() { - SszStaticHandler::, MinimalEthSpec>::pre_electra() - .run(); - SszStaticHandler::, MainnetEthSpec>::pre_electra() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_through_fulu() - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_through_fulu() - .run(); - SszStaticHandler::, MinimalEthSpec>::gloas_and_later( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later( - ) - .run(); + SszStaticHandler::, Spec>::pre_electra().run(); + SszStaticHandler::, Spec>::electra_through_fulu().run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn indexed_attestation() { - SszStaticHandler::, MinimalEthSpec>::pre_electra() - .run(); - SszStaticHandler::, MainnetEthSpec>::pre_electra() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_through_fulu() - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_through_fulu() - .run(); - SszStaticHandler::, MinimalEthSpec>::gloas_and_later() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later() - .run(); + SszStaticHandler::, Spec>::pre_electra().run(); + SszStaticHandler::, Spec>::electra_through_fulu().run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn signed_aggregate_and_proof() { - SszStaticHandler::, MinimalEthSpec>::pre_electra( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::pre_electra( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_through_fulu( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_through_fulu( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::gloas_and_later( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later( - ) - .run(); + SszStaticHandler::, Spec>::pre_electra().run(); + SszStaticHandler::, Spec>::electra_through_fulu() + .run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn aggregate_and_proof() { - SszStaticHandler::, MinimalEthSpec>::pre_electra() - .run(); - SszStaticHandler::, MainnetEthSpec>::pre_electra() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_through_fulu( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_through_fulu( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::gloas_and_later( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later( - ) - .run(); + SszStaticHandler::, Spec>::pre_electra().run(); + SszStaticHandler::, Spec>::electra_through_fulu().run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } // BeaconBlockBody has no internal indicator of which fork it is for, so we test it separately. #[test] fn beacon_block_body() { - SszStaticHandler::, MinimalEthSpec>::base_only().run(); - SszStaticHandler::, MainnetEthSpec>::base_only().run(); - SszStaticHandler::, MinimalEthSpec>::altair_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::altair_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::capella_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::capella_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only().run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only().run(); - SszStaticHandler::, MinimalEthSpec>::gloas_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::heze_only().run(); - SszStaticHandler::, MainnetEthSpec>::heze_only().run(); + SszStaticHandler::, Spec>::base_only().run(); + SszStaticHandler::, Spec>::altair_only().run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); + SszStaticHandler::, Spec>::capella_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); + SszStaticHandler::, Spec>::gloas_only().run(); + SszStaticHandler::, Spec>::heze_only().run(); } // Altair and later #[test] fn contribution_and_proof() { - SszStaticHandler::, MinimalEthSpec>::altair_and_later( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::altair_and_later( - ) - .run(); + SszStaticHandler::, Spec>::altair_and_later().run(); } // LightClientBootstrap has no internal indicator of which fork it is for, so we test it separately. #[test] fn light_client_bootstrap() { - SszStaticHandler::, MinimalEthSpec>::altair_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::altair_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::bellatrix_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::capella_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::capella_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only() - .run(); + SszStaticHandler::, Spec>::altair_only().run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); + SszStaticHandler::, Spec>::capella_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); } // LightClientHeader has no internal indicator of which fork it is for, so we test it separately. #[test] fn light_client_header() { - SszStaticHandler::, MinimalEthSpec>::altair_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::altair_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only() - .run(); + SszStaticHandler::, Spec>::altair_only().run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); - SszStaticHandler::, MinimalEthSpec>::capella_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::capella_only( - ) - .run(); + SszStaticHandler::, Spec>::capella_only().run(); - SszStaticHandler::, MinimalEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only() - .run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); } // LightClientOptimisticUpdate has no internal indicator of which fork it is for, so we test it separately. #[test] fn light_client_optimistic_update() { - SszStaticHandler::, MinimalEthSpec>::altair_only().run(); - SszStaticHandler::, MainnetEthSpec>::altair_only().run(); - SszStaticHandler::, MinimalEthSpec>::bellatrix_only().run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only().run(); - SszStaticHandler::, MinimalEthSpec>::capella_only().run(); - SszStaticHandler::, MainnetEthSpec>::capella_only().run(); - SszStaticHandler::, MinimalEthSpec>::deneb_only().run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only().run(); - SszStaticHandler::, MinimalEthSpec>::electra_only().run(); - SszStaticHandler::, MainnetEthSpec>::electra_only().run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only().run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only().run(); + SszStaticHandler::, Spec>::altair_only().run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); + SszStaticHandler::, Spec>::capella_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); } // LightClientFinalityUpdate has no internal indicator of which fork it is for, so we test it separately. #[test] fn light_client_finality_update() { - SszStaticHandler::, MinimalEthSpec>::altair_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::altair_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::bellatrix_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::capella_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::capella_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::deneb_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only( - ) - .run(); + SszStaticHandler::, Spec>::altair_only().run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); + SszStaticHandler::, Spec>::capella_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); } // LightClientUpdate has no internal indicator of which fork it is for, so we test it separately. #[test] fn light_client_update() { - SszStaticHandler::, MinimalEthSpec>::altair_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::altair_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::capella_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::capella_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only() - .run(); + SszStaticHandler::, Spec>::altair_only().run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); + SszStaticHandler::, Spec>::capella_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); } #[test] fn signed_contribution_and_proof() { - SszStaticHandler::, MinimalEthSpec>::altair_and_later().run(); - SszStaticHandler::, MainnetEthSpec>::altair_and_later().run(); + SszStaticHandler::, Spec>::altair_and_later().run(); } #[test] fn sync_aggregate() { - SszStaticHandler::, MinimalEthSpec>::altair_and_later().run(); - SszStaticHandler::, MainnetEthSpec>::altair_and_later().run(); + SszStaticHandler::, Spec>::altair_and_later().run(); } #[test] fn sync_committee() { - SszStaticHandler::, MinimalEthSpec>::altair_and_later().run(); - SszStaticHandler::, MainnetEthSpec>::altair_and_later().run(); + SszStaticHandler::, Spec>::altair_and_later().run(); } #[test] fn sync_committee_contribution() { - SszStaticHandler::, MinimalEthSpec>::altair_and_later().run(); - SszStaticHandler::, MainnetEthSpec>::altair_and_later().run(); + SszStaticHandler::, Spec>::altair_and_later().run(); } #[test] fn sync_committee_message() { - SszStaticHandler::::altair_and_later().run(); - SszStaticHandler::::altair_and_later().run(); + SszStaticHandler::::altair_and_later().run(); } #[test] fn sync_aggregator_selection_data() { - SszStaticHandler::::altair_and_later().run(); - SszStaticHandler::::altair_and_later().run(); + SszStaticHandler::::altair_and_later().run(); } // Bellatrix and later #[test] fn execution_payload() { - SszStaticHandler::, MinimalEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::capella_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::capella_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::electra_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only().run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only().run(); - SszStaticHandler::, MinimalEthSpec>::gloas_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::heze_only().run(); - SszStaticHandler::, MainnetEthSpec>::heze_only().run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); + SszStaticHandler::, Spec>::capella_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); + SszStaticHandler::, Spec>::gloas_only().run(); + SszStaticHandler::, Spec>::heze_only().run(); } #[test] fn execution_payload_header() { - SszStaticHandler::, MinimalEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::bellatrix_only() - .run(); - SszStaticHandler::, MinimalEthSpec> - ::capella_only().run(); - SszStaticHandler::, MainnetEthSpec> - ::capella_only().run(); - SszStaticHandler::, MinimalEthSpec> - ::deneb_only().run(); - SszStaticHandler::, MainnetEthSpec> - ::deneb_only().run(); - SszStaticHandler::, MinimalEthSpec> - ::electra_only().run(); - SszStaticHandler::, MainnetEthSpec> - ::electra_only().run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only() - .run(); + SszStaticHandler::, Spec>::bellatrix_only().run(); + SszStaticHandler::, Spec>::capella_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); } #[test] fn execution_payload_bid() { - SszStaticHandler::, MinimalEthSpec>::gloas_and_later() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later() - .run(); + SszStaticHandler::, Spec>::gloas_only().run(); + SszStaticHandler::, Spec>::heze_only().run(); } #[test] fn signed_execution_payload_bid() { - SszStaticHandler::, MinimalEthSpec>::gloas_and_later().run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later().run(); + SszStaticHandler::, Spec>::gloas_only().run(); + SszStaticHandler::, Spec>::heze_only().run(); } #[test] fn withdrawal() { - SszStaticHandler::::capella_and_later().run(); - SszStaticHandler::::capella_and_later().run(); + SszStaticHandler::::capella_and_later().run(); } #[test] fn bls_to_execution_change() { - SszStaticHandler::::capella_and_later().run(); - SszStaticHandler::::capella_and_later().run(); + SszStaticHandler::::capella_and_later().run(); } #[test] fn signed_bls_to_execution_change() { - SszStaticHandler::::capella_and_later().run(); - SszStaticHandler::::capella_and_later().run(); + SszStaticHandler::::capella_and_later().run(); } #[test] fn blob_sidecar() { - SszStaticHandler::, MinimalEthSpec>::deneb_only().run(); - SszStaticHandler::, MainnetEthSpec>::deneb_only().run(); - SszStaticHandler::, MinimalEthSpec>::electra_only().run(); - SszStaticHandler::, MainnetEthSpec>::electra_only().run(); + SszStaticHandler::, Spec>::deneb_only().run(); + SszStaticHandler::, Spec>::electra_only().run(); } #[test] fn blob_identifier() { - SszStaticHandler::::deneb_only().run(); - SszStaticHandler::::deneb_only().run(); - SszStaticHandler::::electra_only().run(); - SszStaticHandler::::electra_only().run(); + SszStaticHandler::::deneb_only().run(); + SszStaticHandler::::electra_only().run(); } #[test] fn historical_summary() { - SszStaticHandler::::capella_and_later().run(); - SszStaticHandler::::capella_and_later().run(); + SszStaticHandler::::capella_and_later().run(); } #[test] fn data_column_sidecar() { - SszStaticHandler::, MinimalEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::gloas_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::heze_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::heze_only() - .run(); + SszStaticHandler::, Spec>::fulu_only().run(); + SszStaticHandler::, Spec>::gloas_only().run(); + SszStaticHandler::, Spec>::heze_only().run(); } #[test] fn data_column_by_root_identifier() { - SszStaticWithSpecHandler::< - DataColumnsByRootIdentifier, - MinimalEthSpec, - >::fulu_and_later() - .run(); - SszStaticWithSpecHandler::< - DataColumnsByRootIdentifier, - MainnetEthSpec, - >::fulu_and_later() - .run(); + SszStaticWithSpecHandler::, Spec>::fulu_and_later().run(); } #[test] fn consolidation() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] fn deposit_request() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] fn withdrawal_request() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] fn pending_balance_deposit() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] fn pending_consolidation() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] fn pending_partial_withdrawal() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] fn execution_requests() { - SszStaticHandler::, MainnetEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MinimalEthSpec>::electra_only( - ) - .run(); - SszStaticHandler::, MainnetEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::fulu_only() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_only() - .run(); - SszStaticHandler::, MinimalEthSpec>::gloas_only() - .run(); + SszStaticHandler::, Spec>::electra_only().run(); + SszStaticHandler::, Spec>::fulu_only().run(); + SszStaticHandler::, Spec>::gloas_only().run(); } // Gloas and later #[test] fn builder() { - SszStaticHandler::::gloas_and_later().run(); - SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); } #[test] fn builder_deposit_request() { - SszStaticHandler::::gloas_and_later().run(); - SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); } #[test] fn builder_exit_request() { - SszStaticHandler::::gloas_and_later().run(); - 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(); + SszStaticHandler::::gloas_and_later().run(); } #[test] fn builder_pending_withdrawal() { - SszStaticHandler::::gloas_and_later().run(); - 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(); + SszStaticHandler::::gloas_and_later().run(); } #[test] fn payload_attestation() { - SszStaticHandler::, MinimalEthSpec>::gloas_and_later() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later() - .run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn payload_attestation_message() { - SszStaticHandler::::gloas_and_later().run(); - 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(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn execution_payload_envelope() { - SszStaticHandler::, MinimalEthSpec>::gloas_and_later() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later() - .run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn signed_execution_payload_envelope() { - SszStaticHandler::, MinimalEthSpec>::gloas_and_later() - .run(); - SszStaticHandler::, MainnetEthSpec>::gloas_and_later() - .run(); + SszStaticHandler::, Spec>::gloas_and_later().run(); } #[test] fn proposer_preferences() { - SszStaticHandler::::gloas_and_later().run(); - SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); } #[test] fn signed_proposer_preferences() { - SszStaticHandler::::gloas_and_later().run(); - SszStaticHandler::::gloas_and_later().run(); + SszStaticHandler::::gloas_and_later().run(); } } #[test] fn epoch_processing_justification_and_finalization() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_rewards_and_penalties() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_registry_updates() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_slashings() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_eth1_data_reset() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_pending_balance_deposits() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_pending_deposits_churn() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_pending_consolidations() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_effective_balance_updates() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_slashings_reset() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_randao_mixes_reset() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_historical_roots_update() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_historical_summaries_update() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_participation_record_updates() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] +#[cfg(feature = "spec-minimal")] fn epoch_processing_sync_committee_updates() { // There are presently no mainnet tests, see: // https://github.com/ethereum/consensus-spec-tests/issues/29 - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_inactivity_updates() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_participation_flag_updates() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_proposer_lookahead() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_ptc_window() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn epoch_processing_builder_pending_payments() { - EpochProcessingHandler::::default().run(); - EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); } #[test] fn fork_upgrade() { - ForkHandler::::default().run(); - ForkHandler::::default().run(); + ForkHandler::::default().run(); } #[test] fn transition() { - TransitionHandler::::default().run(); - TransitionHandler::::default().run(); + TransitionHandler::::default().run(); } #[test] fn finality() { - FinalityHandler::::default().run(); - FinalityHandler::::default().run(); + FinalityHandler::::default().run(); } #[test] fn fork_choice_get_head() { - ForkChoiceHandler::::new("get_head").run(); - ForkChoiceHandler::::new("get_head").run(); + ForkChoiceHandler::::new("get_head").run(); } #[test] fn fork_choice_on_attestation() { - ForkChoiceHandler::::new("on_attestation").run(); - ForkChoiceHandler::::new("on_attestation").run(); + ForkChoiceHandler::::new("on_attestation").run(); } #[test] fn fork_choice_on_block() { - ForkChoiceHandler::::new("on_block").run(); - ForkChoiceHandler::::new("on_block").run(); + ForkChoiceHandler::::new("on_block").run(); } #[test] fn fork_choice_ex_ante() { - ForkChoiceHandler::::new("ex_ante").run(); - ForkChoiceHandler::::new("ex_ante").run(); + ForkChoiceHandler::::new("ex_ante").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_reorg() { - ForkChoiceHandler::::new("reorg").run(); + ForkChoiceHandler::::new("reorg").run(); // There is no mainnet variant for this test. } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_withholding() { - ForkChoiceHandler::::new("withholding").run(); + ForkChoiceHandler::::new("withholding").run(); // There is no mainnet variant for this test. } #[test] fn fork_choice_get_proposer_head() { - ForkChoiceHandler::::new("get_proposer_head").run(); - ForkChoiceHandler::::new("get_proposer_head").run(); + ForkChoiceHandler::::new("get_proposer_head").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_deposit_with_reorg() { - ForkChoiceHandler::::new("deposit_with_reorg").run(); + ForkChoiceHandler::::new("deposit_with_reorg").run(); // There is no mainnet variant for this test. } @@ -1134,8 +795,9 @@ macro_rules! fast_confirmation_tests { ($($name:ident: $handler:literal),* $(,)?) => { $( #[test] + #[cfg(feature = "spec-minimal")] fn $name() { - FastConfirmationHandler::::new($handler).run(); + FastConfirmationHandler::::new($handler).run(); } )* }; @@ -1156,185 +818,166 @@ fast_confirmation_tests! { #[test] fn fork_choice_on_execution_payload_envelope() { - ForkChoiceHandler::::new("on_execution_payload_envelope").run(); - ForkChoiceHandler::::new("on_execution_payload_envelope").run(); + ForkChoiceHandler::::new("on_execution_payload_envelope").run(); } #[test] fn fork_choice_get_parent_payload_status() { - ForkChoiceHandler::::new("get_parent_payload_status").run(); - ForkChoiceHandler::::new("get_parent_payload_status").run(); + ForkChoiceHandler::::new("get_parent_payload_status").run(); } #[test] fn fork_choice_on_payload_attestation_message() { - ForkChoiceHandler::::new("on_payload_attestation_message").run(); - ForkChoiceHandler::::new("on_payload_attestation_message").run(); + ForkChoiceHandler::::new("on_payload_attestation_message").run(); } #[test] fn fork_choice_payload_timeliness() { - ForkChoiceHandler::::new("payload_timeliness").run(); - ForkChoiceHandler::::new("payload_timeliness").run(); + ForkChoiceHandler::::new("payload_timeliness").run(); } #[test] fn fork_choice_payload_data_availability() { - ForkChoiceHandler::::new("payload_data_availability").run(); - ForkChoiceHandler::::new("payload_data_availability").run(); + ForkChoiceHandler::::new("payload_data_availability").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_should_apply_proposer_boost() { - ForkChoiceHandler::::new("should_apply_proposer_boost").run(); + ForkChoiceHandler::::new("should_apply_proposer_boost").run(); // There is no mainnet variant for this test. } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_compliance_attester_slashing_test() { - ForkChoiceComplianceHandler::::new("attester_slashing_test").run(); + ForkChoiceComplianceHandler::::new("attester_slashing_test").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_compliance_block_cover_test() { - ForkChoiceComplianceHandler::::new("block_cover_test").run(); + ForkChoiceComplianceHandler::::new("block_cover_test").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_compliance_block_tree_test() { - ForkChoiceComplianceHandler::::new("block_tree_test").run(); + ForkChoiceComplianceHandler::::new("block_tree_test").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_compliance_block_weight_test() { - ForkChoiceComplianceHandler::::new("block_weight_test").run(); + ForkChoiceComplianceHandler::::new("block_weight_test").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_compliance_invalid_message_test() { - ForkChoiceComplianceHandler::::new("invalid_message_test").run(); + ForkChoiceComplianceHandler::::new("invalid_message_test").run(); } #[test] +#[cfg(feature = "spec-minimal")] fn fork_choice_compliance_shuffling_test() { - ForkChoiceComplianceHandler::::new("shuffling_test").run(); + ForkChoiceComplianceHandler::::new("shuffling_test").run(); } #[test] fn optimistic_sync() { - OptimisticSyncHandler::::default().run(); - OptimisticSyncHandler::::default().run(); + OptimisticSyncHandler::::default().run(); } #[test] +#[cfg(feature = "spec-minimal")] fn genesis_initialization() { - GenesisInitializationHandler::::default().run(); + GenesisInitializationHandler::::default().run(); } #[test] +#[cfg(feature = "spec-minimal")] fn genesis_validity() { - GenesisValidityHandler::::default().run(); + GenesisValidityHandler::::default().run(); // Note: there are no genesis validity tests for mainnet } #[test] fn light_client_merkle_proof_validity() { - MerkleProofValidityHandler::::default().run(); - MerkleProofValidityHandler::::default().run(); + MerkleProofValidityHandler::::default().run(); } #[test] +#[cfg(feature = "spec-minimal")] fn light_client_update() { - LightClientUpdateHandler::::default().run(); + LightClientUpdateHandler::::default().run(); } #[test] #[cfg(feature = "fake_crypto")] fn kzg_inclusion_merkle_proof_validity() { - KzgInclusionMerkleProofValidityHandler::::default().run(); - KzgInclusionMerkleProofValidityHandler::::default().run(); + KzgInclusionMerkleProofValidityHandler::::default().run(); } #[test] fn rewards() { for handler in &["basic", "leak", "random", "inactivity_scores"] { - RewardsHandler::::new(handler).run(); - RewardsHandler::::new(handler).run(); + RewardsHandler::::new(handler).run(); } } #[test] fn get_custody_groups() { - GetCustodyGroupsHandler::::default().run(); - GetCustodyGroupsHandler::::default().run() + GetCustodyGroupsHandler::::default().run(); } #[test] fn compute_columns_for_custody_group() { - ComputeColumnsForCustodyGroupHandler::::default().run(); - ComputeColumnsForCustodyGroupHandler::::default().run(); + ComputeColumnsForCustodyGroupHandler::::default().run(); } #[test] fn gossip_beacon_block() { - GossipValidationHandler::::new("gossip_beacon_block").run(); - GossipValidationHandler::::new("gossip_beacon_block").run(); + GossipValidationHandler::::new("gossip_beacon_block").run(); } #[test] fn gossip_proposer_slashing() { - GossipValidationHandler::::new("gossip_proposer_slashing").run(); - GossipValidationHandler::::new("gossip_proposer_slashing").run(); + GossipValidationHandler::::new("gossip_proposer_slashing").run(); } #[test] fn gossip_attester_slashing() { - GossipValidationHandler::::new("gossip_attester_slashing").run(); - GossipValidationHandler::::new("gossip_attester_slashing").run(); + GossipValidationHandler::::new("gossip_attester_slashing").run(); } #[test] fn gossip_voluntary_exit() { - GossipValidationHandler::::latest_stable("gossip_voluntary_exit").run(); - GossipValidationHandler::::latest_stable("gossip_voluntary_exit").run(); + GossipValidationHandler::::latest_stable("gossip_voluntary_exit").run(); } #[test] fn gossip_beacon_attestation() { - GossipValidationHandler::::latest_stable("gossip_beacon_attestation").run(); - GossipValidationHandler::::latest_stable("gossip_beacon_attestation").run(); + GossipValidationHandler::::latest_stable("gossip_beacon_attestation").run(); } #[test] fn gossip_beacon_aggregate_and_proof() { - GossipValidationHandler::::latest_stable("gossip_beacon_aggregate_and_proof") - .run(); - GossipValidationHandler::::latest_stable("gossip_beacon_aggregate_and_proof") - .run(); + GossipValidationHandler::::latest_stable("gossip_beacon_aggregate_and_proof").run(); } #[test] fn gossip_bls_to_execution_change() { - GossipValidationHandler::::latest_stable("gossip_bls_to_execution_change") - .run(); - GossipValidationHandler::::latest_stable("gossip_bls_to_execution_change") - .run(); + GossipValidationHandler::::latest_stable("gossip_bls_to_execution_change").run(); } #[test] fn gossip_sync_committee_message() { - GossipValidationHandler::::latest_stable("gossip_sync_committee_message").run(); - GossipValidationHandler::::latest_stable("gossip_sync_committee_message").run(); + GossipValidationHandler::::latest_stable("gossip_sync_committee_message").run(); } #[test] fn gossip_sync_committee_contribution_and_proof() { - GossipValidationHandler::::latest_stable( - "gossip_sync_committee_contribution_and_proof", - ) - .run(); - GossipValidationHandler::::latest_stable( - "gossip_sync_committee_contribution_and_proof", - ) - .run(); + GossipValidationHandler::::latest_stable("gossip_sync_committee_contribution_and_proof") + .run(); } diff --git a/testing/validator_test_rig/src/mock_beacon_node.rs b/testing/validator_test_rig/src/mock_beacon_node.rs index 81c7a7fe74b..7b1ab01c39f 100644 --- a/testing/validator_test_rig/src/mock_beacon_node.rs +++ b/testing/validator_test_rig/src/mock_beacon_node.rs @@ -1,6 +1,6 @@ use eth2::types::{ - GenericResponse, ProduceBlockV4Response, PublishBlockRequest, RootData, - SignedExecutionPayloadEnvelopeContents, SubmittedBuilderPreferences, SyncingData, + GenericResponse, InclusionListTransactions, ProduceBlockV4Response, PublishBlockRequest, + RootData, SignedExecutionPayloadEnvelopeContents, SubmittedBuilderPreferences, SyncingData, }; use eth2::{BLOB_DATA_INCLUDED_HEADER, BeaconNodeHttpClient, CONSENSUS_VERSION_HEADER, Timeouts}; use mockito::{Matcher, Mock, Server, ServerGuard}; @@ -16,8 +16,8 @@ use tracing::info; use types::{ ChainSpec, ConfigAndPreset, Epoch, EthSpec, ExecutionPayloadEnvelope, ForkName, Hash256, PayloadAttestationData, PayloadAttestationMessage, SignedBlindedBeaconBlock, - SignedContributionAndProof, SignedExecutionPayloadEnvelope, Slot, SyncCommitteeContribution, - SyncCommitteeMessage, SyncDuty, + SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedInclusionList, Slot, + SyncCommitteeContribution, SyncCommitteeMessage, SyncDuty, }; pub struct MockBeaconNode { @@ -33,6 +33,7 @@ pub struct MockBeaconNode { pub builder_preferences: Arc>>, pub sync_committee_messages: Arc>>, pub sync_committee_contributions: Arc>>>, + pub received_inclusion_lists: Arc>>, } impl MockBeaconNode { @@ -55,6 +56,7 @@ impl MockBeaconNode { builder_preferences: Arc::new(Mutex::new(Vec::new())), sync_committee_messages: Arc::new(Mutex::new(Vec::new())), sync_committee_contributions: Arc::new(Mutex::new(Vec::new())), + received_inclusion_lists: Arc::new(Mutex::new(Vec::new())), } } @@ -423,6 +425,38 @@ impl MockBeaconNode { .create() } + /// Mocks `GET /eth/v1/validator/inclusion_list` + pub fn mock_get_validator_inclusion_list( + &mut self, + transactions: &InclusionListTransactions, + slot: Slot, + ) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/validator/inclusion_list$").unwrap(); + + let data = GenericResponse::from(transactions.clone()); + + self.server + .mock("GET", Matcher::Regex(path_pattern.to_string())) + .match_query(Matcher::UrlEncoded("slot".into(), slot.to_string())) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(serde_json::to_string(&data).unwrap()) + .create() + } + + /// Mocks `GET /eth/v1/validator/inclusion_list` returning error + pub fn mock_get_validator_inclusion_list_error(&mut self, slot: Slot) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/validator/inclusion_list$").unwrap(); + + self.server + .mock("GET", Matcher::Regex(path_pattern.to_string())) + .match_query(Matcher::UrlEncoded("slot".into(), slot.to_string())) + .with_status(500) + .with_header("content-type", "application/json") + .with_body(r#"{"message":"Internal server error"}"#) + .create() + } + /// Mocks the `post_beacon_blinded_blocks_v2_ssz` response with an optional `delay`. pub fn mock_post_beacon_blinded_blocks_v2_ssz(&mut self, delay: Duration) -> Mock { let path_pattern = Regex::new(r"^/eth/v2/beacon/blinded_blocks$").unwrap(); @@ -694,4 +728,76 @@ impl MockBeaconNode { .with_body(r#"{"message":"Internal server error"}"#) .create() } + + /// Mocks `POST /eth/v1/validator/inclusion_list` (JSON) to receive signed inclusion lists. + pub fn mock_post_validator_inclusion_list_json(&mut self, fork_name: ForkName) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/validator/inclusion_list$").unwrap(); + + let received_inclusion_lists = Arc::clone(&self.received_inclusion_lists); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/json") + .match_header(CONSENSUS_VERSION_HEADER, fork_name.to_string().as_str()) + .with_status(200) + .with_body_from_request(move |request| { + let body = request.body().expect("Failed to get request body"); + // The request body is wrapped in a `data` object, per the beacon-APIs specs + let wrapper: GenericResponse = serde_json::from_slice(body) + .expect("Failed to deserialize SignedInclusionList from JSON"); + received_inclusion_lists.lock().unwrap().push(wrapper.data); + vec![] + }) + .create() + } + + /// Mocks `POST /eth/v1/validator/inclusion_list` (JSON) returning error + pub fn mock_post_validator_inclusion_list_json_error(&mut self, fork_name: ForkName) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/validator/inclusion_list$").unwrap(); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/json") + .match_header(CONSENSUS_VERSION_HEADER, fork_name.to_string().as_str()) + .with_status(500) + .with_body(r#"{"message":"Internal server error"}"#) + .create() + } + + /// Mocks `POST /eth/v1/validator/inclusion_list` (SSZ) to receive signed inclusion lists. + pub fn mock_post_validator_inclusion_list_ssz(&mut self, fork_name: ForkName) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/validator/inclusion_list$").unwrap(); + + let received_inclusion_lists = Arc::clone(&self.received_inclusion_lists); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/octet-stream") + .match_header(CONSENSUS_VERSION_HEADER, fork_name.to_string().as_str()) + .with_status(200) + .with_body_from_request(move |request| { + let body = request.body().expect("Failed to get request body"); + let inclusion_list = SignedInclusionList::from_ssz_bytes(body) + .expect("Failed to deserialize SignedInclusionList from SSZ"); + received_inclusion_lists + .lock() + .unwrap() + .push(inclusion_list); + vec![] + }) + .create() + } + + /// Mocks `POST /eth/v1/validator/inclusion_list` (SSZ) returning error + pub fn mock_post_validator_inclusion_list_ssz_error(&mut self, fork_name: ForkName) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/validator/inclusion_list$").unwrap(); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/octet-stream") + .match_header(CONSENSUS_VERSION_HEADER, fork_name.to_string().as_str()) + .with_status(500) + .with_body(r#"{"message":"Internal server error"}"#) + .create() + } } diff --git a/testing/validator_test_rig/src/mock_validator_store.rs b/testing/validator_test_rig/src/mock_validator_store.rs index 1adb1e0936c..fceafa41b95 100644 --- a/testing/validator_test_rig/src/mock_validator_store.rs +++ b/testing/validator_test_rig/src/mock_validator_store.rs @@ -5,11 +5,12 @@ use futures::{Stream, stream}; use std::future::Future; use std::sync::Arc; use types::{ - Address, Epoch, ExecutionPayloadEnvelope, Graffiti, Hash256, MainnetEthSpec, + Address, Epoch, ExecutionPayloadEnvelope, Graffiti, Hash256, InclusionList, MainnetEthSpec, PayloadAttestationData, PayloadAttestationMessage, ProposerPreferences, SelectionProof, SignedAggregateAndProof, SignedContributionAndProof, SignedExecutionPayloadEnvelope, - SignedProposerPreferences, SignedValidatorRegistrationData, SingleAttestation, Slot, - SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, + SignedInclusionList, SignedProposerPreferences, SignedValidatorRegistrationData, + SingleAttestation, Slot, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + ValidatorRegistrationData, }; use validator_store::{ AggregateToSign, AttestationToSign, ContributionToSign, DoppelgangerStatus, @@ -205,4 +206,12 @@ impl ValidatorStore for MockValidatorStore { ) -> Option { panic!("MockValidatorStore::proposal_data_at_epoch called without a hook") } + + async fn sign_inclusion_list( + &self, + _validator_pubkey: PublicKeyBytes, + _inclusion_list: InclusionList, + ) -> Result> { + panic!("MockValidatorStore::sign_inclusion_list called without a hook") + } } diff --git a/testing/validator_test_rig/src/recording_validator_store.rs b/testing/validator_test_rig/src/recording_validator_store.rs index 295b6c81694..e8bb9ba1128 100644 --- a/testing/validator_test_rig/src/recording_validator_store.rs +++ b/testing/validator_test_rig/src/recording_validator_store.rs @@ -3,11 +3,12 @@ use eth2::types::{RequestAuth, SignedRequestAuth}; use futures::Stream; use std::sync::{Arc, Mutex}; use types::{ - Address, Epoch, ExecutionPayloadEnvelope, Graffiti, Hash256, PayloadAttestationData, - PayloadAttestationMessage, ProposerPreferences, SelectionProof, SignedAggregateAndProof, - SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedValidatorRegistrationData, SingleAttestation, Slot, SyncCommitteeMessage, - SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, + Address, Epoch, ExecutionPayloadEnvelope, Graffiti, Hash256, InclusionList, + PayloadAttestationData, PayloadAttestationMessage, ProposerPreferences, SelectionProof, + SignedAggregateAndProof, SignedContributionAndProof, SignedExecutionPayloadEnvelope, + SignedInclusionList, SignedProposerPreferences, SignedValidatorRegistrationData, + SingleAttestation, Slot, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + ValidatorRegistrationData, }; use validator_store::{ AggregateToSign, AttestationToSign, ContributionToSign, DoppelgangerStatus, @@ -252,4 +253,14 @@ impl ValidatorStore for RecordingValidatorStore ) -> Option { self.inner.proposal_data_at_epoch(pubkey, epoch) } + + async fn sign_inclusion_list( + &self, + validator_pubkey: PublicKeyBytes, + inclusion_list: InclusionList, + ) -> Result> { + self.inner + .sign_inclusion_list(validator_pubkey, inclusion_list) + .await + } } diff --git a/testing/web3signer_tests/src/get_web3signer.rs b/testing/web3signer_tests/src/get_web3signer.rs index 0c3d9b02db5..ede7aa9d43e 100644 --- a/testing/web3signer_tests/src/get_web3signer.rs +++ b/testing/web3signer_tests/src/get_web3signer.rs @@ -1,5 +1,4 @@ -//! This build script downloads the latest Web3Signer release and places it in the `OUT_DIR` so it -//! can be used for integration testing. +//! Downloads a Web3Signer release and places it in a temp directory for integration testing. use reqwest::Client; use std::env; @@ -7,33 +6,41 @@ use std::fs; use std::path::{Path, PathBuf}; use zip::ZipArchive; -/// Use `None` to download the latest Github release. -/// Use `Some("21.8.1")` to download a specific version. -const FIXED_VERSION_STRING: Option<&str> = None; +/// Use `None` to require `LIGHTHOUSE_WEB3SIGNER_VERSION`. +/// Use `Some("26.9.0")` to download a specific version. +/// +/// Pin to 26.9.0 for Gloas (remote-signing-api v1.4.0). +const FIXED_VERSION_STRING: Option<&str> = Some("26.9.0"); // This function no longer makes any attempt to avoid downloads, because in practice we use it // with a fresh temp directory every time we run the tests. We might want to change this in future // to enable reproducible/offline testing. pub async fn download_binary(dest_dir: PathBuf) { - let version = if let Some(version) = FIXED_VERSION_STRING { - version.to_string() - } else if let Ok(env_version) = env::var("LIGHTHOUSE_WEB3SIGNER_VERSION") { + let version = if let Ok(env_version) = env::var("LIGHTHOUSE_WEB3SIGNER_VERSION") { env_version + } else if let Some(version) = FIXED_VERSION_STRING { + version.to_string() } else { - // The Consenys artifact server resolves `latest` to the latest release. We previously hit - // the Github API to establish the version, but that is no longer necessary. - "latest".to_string() + panic!("set FIXED_VERSION_STRING or LIGHTHOUSE_WEB3SIGNER_VERSION"); }; eprintln!("Downloading web3signer version: {version}"); - // Download the release zip. + // Download the release zip from GitHub (artifacts.consensys.net does not host 26.9.0). let client = Client::builder().build().unwrap(); let zip_url = format!( - "https://artifacts.consensys.net/public/web3signer/raw/names/web3signer.zip/versions/{}/web3signer-{}.zip", - version, version + "https://github.com/Consensys-Incorporated/web3signer/releases/download/{version}/web3signer-{version}.zip" ); - let zip_response = client - .get(zip_url) + let mut request = client + .get(&zip_url) + .header("User-Agent", "lighthouse-web3signer-tests"); + if let Some(token) = env::var("LIGHTHOUSE_GITHUB_TOKEN") + .ok() + .or_else(|| env::var("GITHUB_TOKEN").ok()) + .filter(|token| !token.is_empty()) + { + request = request.header("Authorization", format!("Bearer {token}")); + } + let zip_response = request .send() .await .unwrap() diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index c4a67ba8e17..379211bc318 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -319,6 +319,7 @@ mod tests { slashing_protection_config: SlashingProtectionConfig, using_web3signer: bool, spec: Arc, + initial_slot: Slot, ) -> Self { let validator_dir = TempDir::new().unwrap(); @@ -352,6 +353,7 @@ mod tests { let slot_clock = TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); + slot_clock.set_slot(initial_slot.as_u64()); let config = lighthouse_validator_store::Config { enable_web3signer_slashing_protection: slashing_protection_config.local, ..Default::default() @@ -407,6 +409,23 @@ mod tests { slashing_protection_config: SlashingProtectionConfig, spec: Arc, listen_port: u16, + ) -> Self { + Self::new_at_slot( + network, + slashing_protection_config, + spec, + listen_port, + Slot::new(0), + ) + .await + } + + pub async fn new_at_slot( + network: &str, + slashing_protection_config: SlashingProtectionConfig, + spec: Arc, + listen_port: u16, + initial_slot: Slot, ) -> Self { let signer_rig = Web3SignerRig::new(network, WEB3SIGNER_LISTEN_ADDRESS, listen_port).await; @@ -434,6 +453,7 @@ mod tests { slashing_protection_config, false, spec.clone(), + initial_slot, ) .await }; @@ -462,6 +482,7 @@ mod tests { slashing_protection_config, true, spec, + initial_slot, ) .await }; @@ -963,6 +984,90 @@ mod tests { .await; } + /// Test all the Gloas types. + async fn test_gloas_types(network: &str, listen_port: u16) { + let network_config = Eth2NetworkConfig::constant(network).unwrap().unwrap(); + let spec = Arc::new(network_config.chain_spec::().unwrap()); + let gloas_fork_slot = spec + .gloas_fork_epoch + .expect("network must schedule Gloas for these tests") + .start_slot(E::slots_per_epoch()); + + TestingRig::new_at_slot( + network, + SlashingProtectionConfig::default(), + spec.clone(), + listen_port, + gloas_fork_slot, + ) + .await + .assert_signatures_match( + "execution_payload_envelope_gloas", + |pubkey, validator_store| async move { + let mut envelope = ExecutionPayloadEnvelope::::empty(); + envelope.payload.slot_number = gloas_fork_slot; + validator_store + .sign_execution_payload_envelope(pubkey, envelope) + .await + .unwrap() + .signature + }, + ) + .await + .assert_signatures_match( + "payload_attestation_message_gloas", + |pubkey, validator_store| async move { + validator_store.set_validator_index(&pubkey, 0); + let data = PayloadAttestationData { + beacon_block_root: Hash256::zero(), + slot: gloas_fork_slot, + payload_present: true, + blob_data_available: true, + }; + validator_store + .sign_payload_attestation(pubkey, data) + .await + .unwrap() + .signature + }, + ) + .await + .assert_signatures_match( + "proposer_preferences_gloas", + |pubkey, validator_store| async move { + let preferences = ProposerPreferences { + dependent_root: Hash256::zero(), + proposal_slot: gloas_fork_slot, + validator_index: 0, + fee_recipient: Address::repeat_byte(42), + target_gas_limit: 30_000_000, + }; + validator_store + .sign_proposer_preferences(pubkey, preferences) + .await + .unwrap() + .signature + }, + ) + .await + .assert_signatures_match( + "builder_request_auth_gloas", + |pubkey, validator_store| async move { + let request_auth = eth2::types::RequestAuth { + data: eth2::types::RequestAuthData::new(b"http://builder.example.com".to_vec()) + .unwrap(), + slot: gloas_fork_slot, + }; + validator_store + .sign_request_auth_v1(pubkey, request_auth) + .await + .unwrap() + .signature + }, + ) + .await; + } + #[tokio::test] async fn mainnet_base_types() { test_base_types("mainnet", 4242).await @@ -999,6 +1104,11 @@ mod tests { test_bellatrix_types("sepolia", 4252).await } + #[tokio::test] + async fn sepolia_gloas_types() { + test_gloas_types("sepolia", 4255).await + } + #[tokio::test] async fn slashing_protection_disabled_locally() { test_lighthouse_slashing_protection(SlashingProtectionConfig { local: false }, 4253).await diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index 2303633054e..4380932b8e8 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -22,13 +22,14 @@ use tracing::{Instrument, debug, error, info, info_span, instrument, warn}; use types::{ AbstractExecPayload, Address, AggregateAndProof, Attestation, AttestationData, BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, - ExecutionPayloadEnvelope, Fork, FullPayload, Graffiti, Hash256, PayloadAttestationData, - PayloadAttestationMessage, ProposerPreferences, SelectionProof, SignedAggregateAndProof, - SignedBeaconBlock, SignedContributionAndProof, SignedExecutionPayloadEnvelope, - SignedProposerPreferences, SignedRoot, SignedValidatorRegistrationData, SignedVoluntaryExit, - SingleAttestation, Slot, SyncAggregatorSelectionData, SyncCommitteeContribution, - SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, - VoluntaryExit, graffiti::GraffitiString, + ExecutionPayloadEnvelope, Fork, FullPayload, Graffiti, Hash256, InclusionList, + PayloadAttestationData, PayloadAttestationMessage, ProposerPreferences, SelectionProof, + SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, + SignedExecutionPayloadEnvelope, SignedInclusionList, SignedProposerPreferences, SignedRoot, + SignedValidatorRegistrationData, SignedVoluntaryExit, SingleAttestation, Slot, + SyncAggregatorSelectionData, SyncCommitteeContribution, SyncCommitteeMessage, + SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, VoluntaryExit, + graffiti::GraffitiString, }; use validator_store::{ AggregateToSign, AttestationToSign, ContributionToSign, DoppelgangerStatus, @@ -1552,6 +1553,35 @@ impl ValidatorStore for LighthouseValidatorS signature, }) } + + async fn sign_inclusion_list( + &self, + validator_pubkey: PublicKeyBytes, + inclusion_list: InclusionList, + ) -> Result { + let signing_context = self.signing_context( + Domain::InclusionListCommittee, + inclusion_list.slot.epoch(E::slots_per_epoch()), + ); + + // Inclusion list signing is not slashable, bypass doppelganger protection. + let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; + + let signature = signing_method + .get_signature::>( + SignableMessage::InclusionList(&inclusion_list), + signing_context, + &self.spec, + &self.task_executor, + ) + .await + .map_err(Error::SpecificError)?; + + Ok(SignedInclusionList { + message: inclusion_list, + signature, + }) + } } #[cfg(test)] diff --git a/validator_client/signing_method/Cargo.toml b/validator_client/signing_method/Cargo.toml index 2a33382d5e8..f17175ff238 100644 --- a/validator_client/signing_method/Cargo.toml +++ b/validator_client/signing_method/Cargo.toml @@ -18,3 +18,6 @@ tracing = { workspace = true } types = { workspace = true } url = { workspace = true } validator_metrics = { workspace = true } + +[dev-dependencies] +serde_json = { workspace = true } diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index f877afeaa7e..0c708ff4efd 100644 --- a/validator_client/signing_method/src/lib.rs +++ b/validator_client/signing_method/src/lib.rs @@ -54,6 +54,7 @@ pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload = FullP PayloadAttestationData(&'a PayloadAttestationData), ProposerPreferences(&'a ProposerPreferences), RequestAuth(&'a RequestAuth), + InclusionList(&'a InclusionList), } impl> SignableMessage<'_, E, Payload> { @@ -79,6 +80,7 @@ impl> SignableMessage<'_, E, Payload SignableMessage::PayloadAttestationData(d) => d.signing_root(domain), SignableMessage::ProposerPreferences(p) => p.signing_root(domain), SignableMessage::RequestAuth(r) => r.signing_root(domain), + SignableMessage::InclusionList(l) => l.signing_root(domain), } } } @@ -243,20 +245,25 @@ impl SigningMethod { } SignableMessage::VoluntaryExit(e) => Web3SignerObject::VoluntaryExit(e), SignableMessage::ExecutionPayloadEnvelope(e) => { - Web3SignerObject::ExecutionPayloadEnvelope(e) + Web3SignerObject::execution_payload_envelope(e) } SignableMessage::PayloadAttestationData(d) => { - Web3SignerObject::PayloadAttestationData(d) + Web3SignerObject::payload_attestation_message(d) } SignableMessage::ProposerPreferences(p) => { - Web3SignerObject::ProposerPreferences(p) + Web3SignerObject::proposer_preferences(p) } - SignableMessage::RequestAuth(r) => Web3SignerObject::RequestAuth(r), + SignableMessage::RequestAuth(r) => Web3SignerObject::builder_request_auth(r), + SignableMessage::InclusionList(l) => Web3SignerObject::inclusion_list(l), }; // Determine the Web3Signer message type. let message_type = object.message_type(); - if matches!(message_type, MessageType::ValidatorRegistration) && fork_info.is_some() + // VALIDATOR_REGISTRATION and BUILDER_REQUEST_AUTH must omit fork_info. + if matches!( + message_type, + MessageType::ValidatorRegistration | MessageType::BuilderRequestAuth + ) && fork_info.is_some() { return Err(Error::GenesisForkVersionRequired); } diff --git a/validator_client/signing_method/src/web3signer.rs b/validator_client/signing_method/src/web3signer.rs index 505147a46d4..e6340109317 100644 --- a/validator_client/signing_method/src/web3signer.rs +++ b/validator_client/signing_method/src/web3signer.rs @@ -20,11 +20,12 @@ pub enum MessageType { SyncCommitteeSelectionProof, SyncCommitteeContributionAndProof, ValidatorRegistration, - // TODO(gloas) verify w/ web3signer specs ExecutionPayloadEnvelope, - PayloadAttestation, + PayloadAttestationMessage, ProposerPreferences, - RequestAuth, + BuilderRequestAuth, + // TODO(heze) verify w/ web3signer specs + InclusionList, } #[derive(Debug, PartialEq, Copy, Clone, Serialize)] @@ -47,6 +48,30 @@ pub struct ForkInfo { pub genesis_validators_root: Hash256, } +/// `{version, data}` wrapper for Gloas remote-signing payloads. +#[doc(hidden)] +#[derive(Debug, PartialEq, Serialize)] +pub struct VersionedForkData<'a, T> { + version: ForkName, + data: &'a T, +} + +impl<'a, T> VersionedForkData<'a, T> { + fn gloas(data: &'a T) -> Self { + Self { + version: ForkName::Gloas, + data, + } + } + + fn heze(data: &'a T) -> Self { + Self { + version: ForkName::Heze, + data, + } + } +} + #[derive(Debug, PartialEq, Serialize)] #[serde(bound = "E: EthSpec", rename_all = "snake_case")] pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload> { @@ -82,10 +107,11 @@ pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload> { SyncAggregatorSelectionData(&'a SyncAggregatorSelectionData), ContributionAndProof(&'a ContributionAndProof), ValidatorRegistration(&'a ValidatorRegistrationData), - ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), - PayloadAttestationData(&'a PayloadAttestationData), - ProposerPreferences(&'a ProposerPreferences), - RequestAuth(&'a RequestAuth), + ExecutionPayloadEnvelope(VersionedForkData<'a, ExecutionPayloadEnvelope>), + PayloadAttestationMessage(VersionedForkData<'a, PayloadAttestationData>), + ProposerPreferences(VersionedForkData<'a, ProposerPreferences>), + BuilderRequestAuth(VersionedForkData<'a, RequestAuth>), + InclusionList(VersionedForkData<'a, InclusionList>), } impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Payload> { @@ -139,6 +165,26 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Pa } } + pub fn execution_payload_envelope(envelope: &'a ExecutionPayloadEnvelope) -> Self { + Web3SignerObject::ExecutionPayloadEnvelope(VersionedForkData::gloas(envelope)) + } + + pub fn payload_attestation_message(data: &'a PayloadAttestationData) -> Self { + Web3SignerObject::PayloadAttestationMessage(VersionedForkData::gloas(data)) + } + + pub fn proposer_preferences(preferences: &'a ProposerPreferences) -> Self { + Web3SignerObject::ProposerPreferences(VersionedForkData::gloas(preferences)) + } + + pub fn builder_request_auth(request_auth: &'a RequestAuth) -> Self { + Web3SignerObject::BuilderRequestAuth(VersionedForkData::gloas(request_auth)) + } + + pub fn inclusion_list(inclusion_list: &'a InclusionList) -> Self { + Web3SignerObject::InclusionList(VersionedForkData::heze(inclusion_list)) + } + pub fn message_type(&self) -> MessageType { match self { Web3SignerObject::AggregationSlot { .. } => MessageType::AggregationSlot, @@ -157,9 +203,12 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Pa } Web3SignerObject::ValidatorRegistration(_) => MessageType::ValidatorRegistration, Web3SignerObject::ExecutionPayloadEnvelope(_) => MessageType::ExecutionPayloadEnvelope, - Web3SignerObject::PayloadAttestationData(_) => MessageType::PayloadAttestation, + Web3SignerObject::PayloadAttestationMessage(_) => { + MessageType::PayloadAttestationMessage + } Web3SignerObject::ProposerPreferences(_) => MessageType::ProposerPreferences, - Web3SignerObject::RequestAuth(_) => MessageType::RequestAuth, + Web3SignerObject::BuilderRequestAuth(_) => MessageType::BuilderRequestAuth, + Web3SignerObject::InclusionList(_) => MessageType::InclusionList, } } } @@ -181,3 +230,125 @@ pub struct SigningRequest<'a, E: EthSpec, Payload: AbstractExecPayload> { pub struct SigningResponse { pub signature: Signature, } + +#[cfg(test)] +mod tests { + use super::*; + use bls::FixedBytesExtended; + use builder_types::RequestAuthData; + use types::{FullPayload, MainnetEthSpec}; + + type E = MainnetEthSpec; + type Payload = FullPayload; + + fn dummy_fork_info() -> ForkInfo { + ForkInfo { + fork: Fork { + previous_version: [0; 4], + current_version: [1; 4], + epoch: Epoch::new(1), + }, + genesis_validators_root: Hash256::repeat_byte(42), + } + } + + fn serialize( + message_type: MessageType, + fork_info: Option, + object: Web3SignerObject<'_, E, Payload>, + ) -> serde_json::Value { + serde_json::to_value(SigningRequest { + message_type, + fork_info, + signing_root: Hash256::zero(), + object, + }) + .expect("signing request should serialize") + } + + #[test] + fn execution_payload_envelope_request_shape() { + let envelope = ExecutionPayloadEnvelope::::empty(); + let value = serialize( + MessageType::ExecutionPayloadEnvelope, + Some(dummy_fork_info()), + Web3SignerObject::execution_payload_envelope(&envelope), + ); + + assert_eq!(value["type"], "EXECUTION_PAYLOAD_ENVELOPE"); + assert!(value.get("fork_info").is_some()); + assert_eq!(value["execution_payload_envelope"]["version"], "GLOAS"); + assert!(value["execution_payload_envelope"].get("data").is_some()); + assert!(value.get("payload_attestation_data").is_none()); + assert!(value.get("request_auth").is_none()); + } + + #[test] + fn payload_attestation_message_request_shape() { + let data = PayloadAttestationData { + beacon_block_root: Hash256::zero(), + slot: Slot::new(1), + payload_present: true, + blob_data_available: false, + }; + let value = serialize( + MessageType::PayloadAttestationMessage, + Some(dummy_fork_info()), + Web3SignerObject::payload_attestation_message(&data), + ); + + assert_eq!(value["type"], "PAYLOAD_ATTESTATION_MESSAGE"); + assert!(value.get("fork_info").is_some()); + assert_eq!(value["payload_attestation_message"]["version"], "GLOAS"); + assert_eq!( + value["payload_attestation_message"]["data"]["payload_present"], + true + ); + assert!(value.get("payload_attestation_data").is_none()); + assert!(value.get("PAYLOAD_ATTESTATION").is_none()); + } + + #[test] + fn proposer_preferences_request_shape() { + let preferences = ProposerPreferences { + dependent_root: Hash256::zero(), + proposal_slot: Slot::new(32), + validator_index: 1, + fee_recipient: Address::repeat_byte(1), + target_gas_limit: 30_000_000, + }; + let value = serialize( + MessageType::ProposerPreferences, + Some(dummy_fork_info()), + Web3SignerObject::proposer_preferences(&preferences), + ); + + assert_eq!(value["type"], "PROPOSER_PREFERENCES"); + assert!(value.get("fork_info").is_some()); + assert_eq!(value["proposer_preferences"]["version"], "GLOAS"); + assert_eq!( + value["proposer_preferences"]["data"]["target_gas_limit"], + "30000000" + ); + } + + #[test] + fn builder_request_auth_request_shape_omits_fork_info() { + let request_auth = RequestAuth { + data: RequestAuthData::new(b"http://builder.example.com".to_vec()).unwrap(), + slot: Slot::new(32), + }; + let value = serialize( + MessageType::BuilderRequestAuth, + None, + Web3SignerObject::builder_request_auth(&request_auth), + ); + + assert_eq!(value["type"], "BUILDER_REQUEST_AUTH"); + assert!(value.get("fork_info").is_none()); + assert_eq!(value["builder_request_auth"]["version"], "GLOAS"); + assert!(value["builder_request_auth"]["data"].get("data").is_some()); + assert_eq!(value["builder_request_auth"]["data"]["slot"], "32"); + assert!(value.get("request_auth").is_none()); + } +} diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 6cf519bc313..307e2baf471 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -47,6 +47,7 @@ use validator_services::{ block_service::{BlockService, BlockServiceBuilder}, builder_preferences_service::BuilderPreferencesService, duties_service::{self, DutiesService, DutiesServiceBuilder}, + inclusion_list_service::InclusionListService, latency_service, payload_attestation_service::PayloadAttestationService, preparation_service::{PreparationService, PreparationServiceBuilder}, @@ -96,6 +97,7 @@ pub struct ProductionValidatorClient { ProposerPreferencesService, SystemTimeSlotClock>, doppelganger_service: Option>, preparation_service: PreparationService, SystemTimeSlotClock>, + inclusion_list_service: InclusionListService, SystemTimeSlotClock>, validator_store: Arc>, configured_builders: BuilderStore, builder_preferences_service: BuilderPreferencesService, SystemTimeSlotClock>, @@ -616,6 +618,15 @@ impl ProductionValidatorClient { context.eth2_config.spec.clone(), ); + let inclusion_list_service = InclusionListService::new( + duties_service.clone(), + validator_store.clone(), + slot_clock.clone(), + beacon_nodes.clone(), + context.executor.clone(), + context.eth2_config.spec.clone(), + ); + Ok(Self { context, duties_service, @@ -624,6 +635,7 @@ impl ProductionValidatorClient { sync_committee_service, payload_attestation_service, proposer_preferences_service, + inclusion_list_service, doppelganger_service, preparation_service, validator_store, @@ -716,6 +728,13 @@ impl ProductionValidatorClient { .map_err(|e| format!("Unable to start builder preferences service: {}", e))?; } + if self.context.eth2_config.spec.is_heze_scheduled() { + self.inclusion_list_service + .clone() + .start_update_service() + .map_err(|e| format!("Unable to start inclusion list service: {}", e))?; + } + self.preparation_service .clone() .start_update_service(&self.context.eth2_config.spec) diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 7e9f22a6481..d772d0d2a57 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -340,7 +340,7 @@ impl EnvelopeAction { } (true, ProduceBlockV4Response::BlockOnly(block)) => { let action = match block.body().signed_execution_payload_bid() { - Ok(bid) if bid.message.builder_index == BUILDER_INDEX_SELF_BUILD => { + Ok(bid) if bid.message().builder_index() == BUILDER_INDEX_SELF_BUILD => { warn!( slot = slot.as_u64(), "Beacon node omitted the payload contents for a self-built block, \ @@ -351,7 +351,7 @@ impl EnvelopeAction { Ok(bid) => { info!( slot = slot.as_u64(), - builder_index = bid.message.builder_index, + builder_index = bid.message().builder_index(), "Builder bid won; the builder reveals the payload envelope" ); Self::Skip @@ -369,13 +369,13 @@ impl EnvelopeAction { (false, response) => { let block = response.into_block(); let action = match block.body().signed_execution_payload_bid() { - Ok(bid) if bid.message.builder_index == BUILDER_INDEX_SELF_BUILD => { + Ok(bid) if bid.message().builder_index() == BUILDER_INDEX_SELF_BUILD => { Self::Fetch(block.canonical_root()) } Ok(bid) => { info!( slot = slot.as_u64(), - builder_index = bid.message.builder_index, + builder_index = bid.message().builder_index(), "Builder bid won; the builder reveals the payload envelope" ); Self::Skip diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index fd15e0c7241..6439e543488 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -808,7 +808,7 @@ pub fn start_update_service break; }; - if current_epoch + 1 < gloas_fork_epoch { + if current_epoch < gloas_fork_epoch { // Wait until the next slot and check again if let Some(duration) = duties_service.slot_clock.duration_to_next_slot() { sleep(duration).await; diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs new file mode 100644 index 00000000000..9890094d19f --- /dev/null +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -0,0 +1,955 @@ +use crate::duties_service::DutiesService; +use beacon_node_fallback::BeaconNodeFallback; +use eth2::types::InclusionListDuty; +use logging::crit; +use slot_clock::SlotClock; +use std::ops::Deref; +use std::sync::Arc; +use std::time::Duration; +use task_executor::TaskExecutor; +use tokio::time::sleep; +use tracing::{debug, error, info}; +use types::{ + ChainSpec, EthSpec, ForkName, Hash256, InclusionList, ProgressiveTransactions, + SignedInclusionList, Slot, +}; +use validator_store::ValidatorStore; + +type DependentRoot = Hash256; + +/// How long before the inclusion list deadline inclusion lists are produced. By then the execution +/// layer has usually received the `forkchoiceUpdated` for the slot's payload, which drops the +/// payload's transactions from its mempool. +const INCLUSION_LIST_PRODUCTION_MARGIN: Duration = Duration::from_secs(1); + +struct InclusionListData { + dependent_root: DependentRoot, + transactions: ProgressiveTransactions, +} + +pub struct Inner { + duties_service: Arc>, + validator_store: Arc, + slot_clock: T, + beacon_nodes: Arc>, + executor: TaskExecutor, + chain_spec: Arc, +} + +pub struct InclusionListService { + inner: Arc>, +} + +impl Clone for InclusionListService { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl Deref for InclusionListService { + type Target = Inner; + + fn deref(&self) -> &Self::Target { + self.inner.deref() + } +} + +impl InclusionListService +where + S: ValidatorStore + 'static, + T: SlotClock + 'static, +{ + pub fn new( + duties_service: Arc>, + validator_store: Arc, + slot_clock: T, + beacon_nodes: Arc>, + executor: TaskExecutor, + chain_spec: Arc, + ) -> Self { + Self { + inner: Arc::new(Inner { + duties_service, + validator_store, + slot_clock, + beacon_nodes, + executor, + chain_spec, + }), + } + } + + pub fn start_update_service(self) -> Result<(), String> { + info!( + inclusion_list_due_ms = self.chain_spec.get_inclusion_list_due().as_millis(), + inclusion_list_production_due_ms = self.inclusion_list_production_due().as_millis(), + "Inclusion list service started" + ); + let executor = self.executor.clone(); + + let interval_fut = async move { + loop { + if let Err(e) = self.spawn_inclusion_list_tasks().await { + error!(error = e, "Failed to produce inclusion lists"); + } + } + }; + + executor.spawn(interval_fut, "inclusion_list_service"); + + Ok(()) + } + + async fn spawn_inclusion_list_tasks(&self) -> Result<(), String> { + let Some(slot) = self.wait_for_inclusion_list_production_due().await else { + return Ok(()); + }; + + let Some((duties, inclusion_list_data)) = + self.produce_inclusion_list_duties_data(slot).await? + else { + return Ok(()); + }; + + let service = self.clone(); + self.executor.spawn( + async move { + if let Err(e) = service + .sign_and_publish(slot, duties, inclusion_list_data) + .await + { + crit!(error = e, %slot, "Failed to publish inclusion lists"); + } + }, + "inclusion_list_producer", + ); + Ok(()) + } + + /// Duration into the slot at which inclusion lists are produced. + fn inclusion_list_production_due(&self) -> Duration { + self.chain_spec + .get_inclusion_list_due() + .saturating_sub(INCLUSION_LIST_PRODUCTION_MARGIN) + } + + /// Waits until inclusion lists are due for production and returns the slot to produce them for. + async fn wait_for_inclusion_list_production_due(&self) -> Option { + let slot_duration = self.chain_spec.get_slot_duration(); + + let Some(slot) = self.inclusion_list_production_slot() else { + error!("Failed to read slot clock"); + sleep(slot_duration).await; + return None; + }; + + // Ensure that the slot is in the Heze fork + if !self + .chain_spec + .fork_name_at_slot::(slot) + .heze_enabled() + { + let duration_to_next_epoch = self + .slot_clock + .duration_to_next_epoch(S::E::slots_per_epoch()) + .unwrap_or_else(|| slot_duration * S::E::slots_per_epoch() as u32); + sleep(duration_to_next_epoch).await; + return None; + } + + let Some(duration_to_production_due) = self.duration_to_inclusion_list_production_due(slot) + else { + error!("Failed to read slot clock"); + sleep(slot_duration).await; + return None; + }; + sleep(duration_to_production_due).await; + + let Some(current_slot) = self.slot_clock.now() else { + error!("Failed to read slot clock after sleep"); + return None; + }; + + Some(current_slot) + } + + /// The slot to produce inclusion lists for next. + /// Returns the current slot if its production time has not passed yet, otherwise the next slot. + fn inclusion_list_production_slot(&self) -> Option { + let current_slot = self.slot_clock.now()?; + let time_into_slot = self.slot_clock.millis_from_current_slot_start()?; + + if time_into_slot < self.inclusion_list_production_due() { + Some(current_slot) + } else { + Some(current_slot + 1) + } + } + + /// Duration until inclusion lists are due for production at `production_slot`. + fn duration_to_inclusion_list_production_due(&self, production_slot: Slot) -> Option { + let production_due_at = + self.slot_clock.start_of(production_slot)? + self.inclusion_list_production_due(); + Some(production_due_at.saturating_sub(self.slot_clock.now_duration()?)) + } + + /// Produce the inclusion list data for `slot`, returned alongside the duties to sign. + /// + /// Returns `Ok(None)` when there is nothing to produce (the slot's duties have not been + /// downloaded yet, or no local validator has an IL duty at the slot) and `Err` when + /// fetching the inclusion list transactions failed. + async fn produce_inclusion_list_duties_data( + &self, + slot: Slot, + ) -> Result, InclusionListData)>, String> { + let Some((dependent_root, duties)) = self.duties_service.get_il_duties_for_slot(slot) + else { + return Ok(None); + }; + + if duties.is_empty() { + return Ok(None); + } + + debug!( + %slot, + il_duties_count = duties.len(), + "Producing inclusion lists" + ); + + // Fetch the inclusion list transactions for the given slot + let transactions = self + .beacon_nodes + .first_success(|beacon_node| async move { + beacon_node + .get_validator_inclusion_list(slot) + .await + .map(|resp| resp.data.transactions) + }) + .await + .map_err(|e| e.to_string())?; + + debug!( + %slot, + ?dependent_root, + tx_count = transactions.len(), + tx_bytes = transactions + .iter() + .map(|tx| tx.len()) + .sum::(), + "Received inclusion list transactions" + ); + + let inclusion_list_data = InclusionListData { + dependent_root, + transactions, + }; + + Ok(Some((duties, inclusion_list_data))) + } + + async fn sign_and_publish( + &self, + slot: Slot, + duties: Vec, + inclusion_list_data: InclusionListData, + ) -> Result<(), String> { + let mut signed_ils = Vec::with_capacity(duties.len()); + + for duty in duties { + let inclusion_list = InclusionList { + slot, + validator_index: duty.validator_index, + dependent_root: inclusion_list_data.dependent_root, + transactions: inclusion_list_data.transactions.clone(), + }; + + match self + .validator_store + .sign_inclusion_list(duty.pubkey, inclusion_list) + .await + { + Ok(signed_il) => signed_ils.push(signed_il), + Err(e) => { + crit!( + error = ?e, + validator = ?duty.pubkey, + %slot, + "Failed to sign inclusion list" + ); + } + } + } + + if signed_ils.is_empty() { + return Ok(()); + } + + let mut ils_published = 0; + let fork_name = self.chain_spec.fork_name_at_slot::(slot); + for signed_il in &signed_ils { + match self.publish_inclusion_list(signed_il, fork_name).await { + Ok(()) => ils_published += 1, + Err(e) => error!( + %slot, + validator_index = signed_il.message.validator_index, + error = %e, + "Failed to publish inclusion list" + ), + } + } + + if ils_published == 0 { + return Err(format!( + "Failed to publish any of the {} signed inclusion lists", + signed_ils.len() + )); + } + + info!( + %slot, + published = ils_published, + total = signed_ils.len(), + "Published inclusion lists" + ); + + Ok(()) + } + + async fn publish_inclusion_list( + &self, + signed_il: &SignedInclusionList, + fork_name: ForkName, + ) -> Result<(), String> { + let result = self + .beacon_nodes + .first_success(|beacon_node| { + let inclusion_list = signed_il.clone(); + async move { + beacon_node + .post_validator_inclusion_list_ssz(&inclusion_list, fork_name) + .await + .map_err(|e| format!("Failed to publish inclusion list (SSZ): {e:?}")) + } + }) + .await; + + match result { + Ok(()) => Ok(()), + Err(ssz_err) => { + debug!(error = %ssz_err, "SSZ publish failed, falling back to JSON"); + self.beacon_nodes + .first_success(|beacon_node| { + let inclusion_list = signed_il.clone(); + async move { + beacon_node + .post_validator_inclusion_list(&inclusion_list, fork_name) + .await + .map_err(|e| { + format!("Failed to publish inclusion list (JSON): {e:?}") + }) + } + }) + .await + .map_err(|e| e.to_string()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::duties_service::DutiesServiceBuilder; + use eth2::types::InclusionListTransactions; + use futures::FutureExt; + use slot_clock::ManualSlotClock; + use std::time::Duration; + use types::test_utils::generate_deterministic_keypair; + use types::{Domain, Epoch, MainnetEthSpec, SignedRoot}; + use validator_test_rig::validator_client_harness::{S, ValidatorClientHarness}; + + type E = MainnetEthSpec; + + struct TestHarness { + harness: ValidatorClientHarness, + service: InclusionListService, + } + + impl TestHarness { + async fn new_with_validators(num_validators: usize) -> Self { + Self::new_with_heze_at(num_validators, Epoch::new(0)).await + } + + async fn new_with_heze_at(num_validators: usize, heze_fork_epoch: Epoch) -> Self { + let harness = ValidatorClientHarness::new(num_validators).await; + + let mut spec = (*harness.spec).clone(); + spec.heze_fork_epoch = Some(heze_fork_epoch); + let spec = Arc::new(spec); + + let duties_service = Arc::new( + DutiesServiceBuilder::new() + .validator_store(harness.validator_store.clone()) + .slot_clock(harness.slot_clock.clone()) + .beacon_nodes(harness.beacon_nodes.clone()) + .executor(harness.test_runtime.task_executor.clone()) + .spec(spec.clone()) + .build() + .unwrap(), + ); + + let service = InclusionListService::new( + duties_service, + harness.validator_store.clone(), + harness.slot_clock.clone(), + harness.beacon_nodes.clone(), + harness.test_runtime.task_executor.clone(), + spec, + ); + + Self { harness, service } + } + + fn insert_il_duties(&self, slot: Slot, dependent_root: Hash256) { + let duties = self + .harness + .pubkeys + .iter() + .enumerate() + .map(|(i, pubkey)| InclusionListDuty { + pubkey: *pubkey, + validator_index: i as u64, + slot, + }) + .collect(); + self.service + .duties_service + .il_duties + .write() + .insert(slot.epoch(E::slots_per_epoch()), (dependent_root, duties)); + } + } + + async fn advance_time(slot_clock: &ManualSlotClock, duration: Duration) { + slot_clock.advance_time(duration); + tokio::time::advance(duration).await; + } + + /// Pre-Heze the wait sleeps to the next epoch and returns `None` + /// No duties are read and no BN request is made + #[tokio::test] + async fn waits_until_next_epoch_before_heze_fork() { + tokio::time::pause(); + + let harness = TestHarness::new_with_heze_at(1, Epoch::new(1)).await; + let service = &harness.service; + + // Add duties for a pre-Heze slot + // If the il task execution leaks past the wait_for_inclusion_list_production_due check, + // it would fetch the transactions from the mock BNs and fail the final assertion + // in the test + harness.insert_il_duties(Slot::new(1), Hash256::repeat_byte(0xab)); + let service_wait = service.spawn_inclusion_list_tasks(); + tokio::pin!(service_wait); + + // This first call of .now_or_never() starts the timer and registers the sleep timer with tokio + // It calls sleep(duration_to_next_epoch).await which registers a timer with a deadline of 12s * 32 + assert!(service_wait.as_mut().now_or_never().is_none()); + + // Advance both slot_clock and tokio::time slot by slot up to 384s (the sleep deadline) + // This verifies that wait_for_inclusion_list_production_due waits a whole epoch (not just a slot) before completing + for _ in 0..E::slots_per_epoch() { + let duration_to_next_slot = harness.service.slot_clock.duration_to_next_slot().unwrap(); + advance_time(&harness.service.slot_clock, duration_to_next_slot).await; + assert!( + service_wait.as_mut().now_or_never().is_none(), + "Function should return None before the sleep duration has elapsed" + ); + } + + // Advance time for 1 more second, past the epoch boundary. + // The epoch sleep should have completed and the execution should complete as a no-op. + // This call should yield no slot, so nothing should be produced, signed or published. + advance_time(&harness.service.slot_clock, Duration::from_secs(1)).await; + assert_eq!(service_wait.as_mut().now_or_never(), Some(Ok(()))); + + // The next wait targets the first Heze slot, 1s into it + let first_heze_slot = Slot::new(E::slots_per_epoch()); + let production_due = service.inclusion_list_production_due(); + let service_wait = service.wait_for_inclusion_list_production_due(); + tokio::pin!(service_wait); + assert!(service_wait.as_mut().now_or_never().is_none()); + advance_time( + &harness.service.slot_clock, + production_due - Duration::from_secs(1), + ) + .await; + assert!(service_wait.as_mut().now_or_never().is_none()); + advance_time(&harness.service.slot_clock, Duration::from_secs(1)).await; + assert_eq!( + service_wait.as_mut().now_or_never().unwrap(), + Some(first_heze_slot) + ); + } + + #[tokio::test] + async fn targets_first_heze_slot_from_last_gloas_slot() { + tokio::time::pause(); + + let harness = TestHarness::new_with_heze_at(1, Epoch::new(1)).await; + let service = &harness.service; + let production_due = service.inclusion_list_production_due(); + let first_heze_slot = Slot::new(E::slots_per_epoch()); + + // Last pre-Heze slot, past its production time + let last_pre_heze_slot = first_heze_slot - 1; + advance_time( + &service.slot_clock, + service.slot_clock.start_of(last_pre_heze_slot).unwrap() + production_due, + ) + .await; + let service_wait = service.wait_for_inclusion_list_production_due(); + tokio::pin!(service_wait); + assert!(service_wait.as_mut().now_or_never().is_none()); + + // Advance to the first Heze slot's production time + let duration_to_wait = service.slot_clock.duration_to_next_slot().unwrap() + production_due; + advance_time(&service.slot_clock, duration_to_wait).await; + assert!(service_wait.as_mut().now_or_never().is_none()); + advance_time(&service.slot_clock, Duration::from_secs(1)).await; + assert_eq!( + service_wait.as_mut().now_or_never().unwrap(), + Some(first_heze_slot) + ); + } + + #[tokio::test] + async fn waits_until_production_due_in_next_slot() { + tokio::time::pause(); + + let harness = TestHarness::new_with_validators(1).await; + let service = &harness.service; + let production_due = service.inclusion_list_production_due(); + assert_eq!(production_due, Duration::from_secs(7)); + + // Past the current slot's production time, so the next slot is targeted + advance_time(&service.slot_clock, production_due).await; + let service_wait = service.wait_for_inclusion_list_production_due(); + tokio::pin!(service_wait); + + // Start the wait, which registers the sleep timer with tokio + assert!(service_wait.as_mut().now_or_never().is_none()); + + // 7s into the next slot: 1s before the inclusion list deadline + let duration_to_wait = service.slot_clock.duration_to_next_slot().unwrap() + production_due; + // Advance both slot_clock and tokio::time to 19s + advance_time(&harness.service.slot_clock, duration_to_wait).await; + assert!( + service_wait.as_mut().now_or_never().is_none(), + "Function should return None before the sleep duration has elapsed" + ); + + advance_time(&harness.service.slot_clock, Duration::from_secs(1)).await; + assert_eq!( + service_wait.as_mut().now_or_never().unwrap(), + Some(Slot::new(1)) + ); + } + + #[tokio::test] + async fn waits_until_production_due_in_current_slot() { + tokio::time::pause(); + + let harness = TestHarness::new_with_validators(1).await; + let service = &harness.service; + let production_due = service.inclusion_list_production_due(); + + // 2s into the current slot + advance_time(&service.slot_clock, Duration::from_secs(2)).await; + let service_wait = service.wait_for_inclusion_list_production_due(); + tokio::pin!(service_wait); + assert!(service_wait.as_mut().now_or_never().is_none()); + + advance_time(&service.slot_clock, production_due - Duration::from_secs(2)).await; + assert!( + service_wait.as_mut().now_or_never().is_none(), + "Function should return None before the sleep duration has elapsed" + ); + + advance_time(&service.slot_clock, Duration::from_secs(1)).await; + assert_eq!( + service_wait.as_mut().now_or_never().unwrap(), + Some(Slot::new(0)) + ); + } + + #[tokio::test] + async fn no_duties_no_fetch_no_publish() { + let mut harness = TestHarness::new_with_validators(3).await; + let current_slot = harness.service.slot_clock.now().unwrap(); + + let transactions = InclusionListTransactions { + transactions: Default::default(), + }; + let fetch_mock = harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list(&transactions, current_slot); + let publish_mock = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_ssz(ForkName::Heze); + + // Duties for the slot's epoch not downloaded yet + let result = harness + .service + .produce_inclusion_list_duties_data(current_slot) + .await; + assert!(result.unwrap().is_none()); + + // Add duty for next slot, not for the current one + harness.insert_il_duties(current_slot + 1, Hash256::repeat_byte(0xab)); + let result = harness + .service + .produce_inclusion_list_duties_data(current_slot) + .await; + assert!(result.unwrap().is_none()); + + fetch_mock.expect(0).assert(); + publish_mock.expect(0).assert(); + } + + /// Produce endpoint fails on all BNs: produce returns `Err`, nothing is signed or + /// published, and the main service loop handles the error + #[tokio::test] + async fn produce_fetch_error_aborts_slot() { + let mut harness = TestHarness::new_with_validators(3).await; + let slot = Slot::new(1); + harness.insert_il_duties(slot, Hash256::repeat_byte(0xab)); + + harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list_error(slot); + harness + .harness + .mock_beacon_node_2 + .mock_get_validator_inclusion_list_error(slot); + + let result = harness + .service + .produce_inclusion_list_duties_data(slot) + .await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn produce_falls_back_to_second_bn() { + let mut harness = TestHarness::new_with_validators(3).await; + let slot = Slot::new(1); + let dependent_root = Hash256::repeat_byte(0xab); + harness.insert_il_duties(slot, dependent_root); + + let transactions = InclusionListTransactions { + transactions: vec![vec![0xaa; 3].try_into().unwrap()].try_into().unwrap(), + }; + harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list_error(slot); + let bn2_mock = harness + .harness + .mock_beacon_node_2 + .mock_get_validator_inclusion_list(&transactions, slot); + + let (duties, data) = harness + .service + .produce_inclusion_list_duties_data(slot) + .await + .unwrap() + .unwrap(); + assert_eq!(duties.len(), 3); + assert_eq!(data.dependent_root, dependent_root); + assert_eq!(data.transactions, transactions.transactions); + bn2_mock.expect(1).assert(); + } + + #[tokio::test] + async fn publishes_each_inclusion_list_via_ssz() { + let mut harness = TestHarness::new_with_validators(3).await; + let slot = Slot::new(1); + let dependent_root = Hash256::repeat_byte(0xab); + harness.insert_il_duties(slot, dependent_root); + + let transactions = InclusionListTransactions { + transactions: vec![vec![0xaa; 3].try_into().unwrap()].try_into().unwrap(), + }; + harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list(&transactions, slot); + let ssz_mock = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_ssz(ForkName::Heze); + let json_mock = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_json(ForkName::Heze); + + let (duties, data) = harness + .service + .produce_inclusion_list_duties_data(slot) + .await + .unwrap() + .unwrap(); + harness + .service + .sign_and_publish(slot, duties, data) + .await + .unwrap(); + + // One POST per duty, and no JSON fallback + ssz_mock.expect(3).assert(); + json_mock.expect(0).assert(); + + let messages = harness + .harness + .mock_beacon_node_1 + .received_inclusion_lists + .lock() + .unwrap(); + assert_eq!(messages.len(), 3); + + // Check each message + for (i, signed_il) in messages.iter().enumerate() { + assert_eq!(signed_il.message.validator_index, i as u64); + assert_eq!(signed_il.message.slot, slot); + assert_eq!(signed_il.message.dependent_root, dependent_root); + assert_eq!(signed_il.message.transactions, transactions.transactions); + } + // Each message carries its own validator's signature + assert_ne!(messages[0].signature, messages[1].signature); + assert_ne!(messages[1].signature, messages[2].signature); + } + + #[tokio::test] + async fn inclusion_list_ssz_publish_falls_back_to_json() { + let mut harness = TestHarness::new_with_validators(1).await; + let slot = Slot::new(1); + harness.insert_il_duties(slot, Hash256::repeat_byte(0xab)); + + let transactions = InclusionListTransactions { + transactions: Default::default(), + }; + harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list(&transactions, slot); + let ssz_mock = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_ssz_error(ForkName::Heze); + let json_mock = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_json(ForkName::Heze); + + let (duties, data) = harness + .service + .produce_inclusion_list_duties_data(slot) + .await + .unwrap() + .unwrap(); + harness + .service + .sign_and_publish(slot, duties, data) + .await + .unwrap(); + + // `first_success` makes two passes over the BNs, so the failing SSZ mock is hit twice + ssz_mock.expect(2).assert(); + json_mock.expect(1).assert(); + + let messages = harness + .harness + .mock_beacon_node_1 + .received_inclusion_lists + .lock() + .unwrap(); + assert_eq!(messages.len(), 1); + } + + /// One duty's publish fails on both encodings + /// so the succeeded calls are total - 1 + #[tokio::test] + async fn partial_publish_failure_still_publishes_siblings() { + let mut harness = TestHarness::new_with_validators(2).await; + let slot = Slot::new(1); + harness.insert_il_duties(slot, Hash256::repeat_byte(0xab)); + + let transactions = InclusionListTransactions { + transactions: Default::default(), + }; + harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list(&transactions, slot); + + // Each error mock answers exactly one request: the first duty's publish fails on + // both `first_success` passes of both encodings + let ssz_err_1 = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_ssz_error(ForkName::Heze); + let json_err_1 = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_json_error(ForkName::Heze); + let ssz_err_2 = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_ssz_error(ForkName::Heze); + let json_err_2 = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_json_error(ForkName::Heze); + // With the error mocks above spent, the sibling duty publishes here + let ssz_mock = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_ssz(ForkName::Heze); + + let (duties, data) = harness + .service + .produce_inclusion_list_duties_data(slot) + .await + .unwrap() + .unwrap(); + harness + .service + .sign_and_publish(slot, duties, data) + .await + .unwrap(); + + ssz_err_1.expect(1).assert(); + ssz_err_2.expect(1).assert(); + json_err_1.expect(1).assert(); + json_err_2.expect(1).assert(); + // successful call + ssz_mock.expect(1).assert(); + + let messages = harness + .harness + .mock_beacon_node_1 + .received_inclusion_lists + .lock() + .unwrap(); + let indices: Vec = messages + .iter() + .map(|il| il.message.validator_index) + .collect(); + assert_eq!(indices, vec![1]); + } + + #[tokio::test] + async fn total_publish_failure_returns_error() { + let mut harness = TestHarness::new_with_validators(2).await; + let slot = Slot::new(1); + harness.insert_il_duties(slot, Hash256::repeat_byte(0xab)); + + let transactions = InclusionListTransactions { + transactions: Default::default(), + }; + // No POST routes are registered: every publish fails on both encodings + harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list(&transactions, slot); + + let (duties, data) = harness + .service + .produce_inclusion_list_duties_data(slot) + .await + .unwrap() + .unwrap(); + let result = harness.service.sign_and_publish(slot, duties, data).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn signed_inclusion_list_verifies_against_consensus_signature_check() { + let harness = TestHarness::new_with_validators(1).await; + let slot = Slot::new(1); + let pubkey = harness.harness.pubkeys[0]; + + let inclusion_list = InclusionList { + slot, + validator_index: 0, + dependent_root: Hash256::repeat_byte(0xab), + transactions: Default::default(), + }; + let signed = harness + .harness + .validator_store + .sign_inclusion_list(pubkey, inclusion_list) + .await + .unwrap(); + + // Independent consensus-side derivation + let spec = &harness.harness.spec; + let epoch = slot.epoch(E::slots_per_epoch()); + let fork = spec.fork_at_epoch(epoch); + let domain = spec.get_domain(epoch, Domain::InclusionListCommittee, &fork, Hash256::ZERO); + let signing_root = signed.message.signing_root(domain); + assert!( + signed + .signature + .verify(&pubkey.decompress().unwrap(), signing_root) + ); + } + + #[tokio::test] + async fn sign_failure_for_unknown_validator_skips_publish() { + let mut harness = TestHarness::new_with_validators(1).await; + let slot = Slot::new(1); + + // A duty for a validator the store does not hold + let duty = InclusionListDuty { + pubkey: generate_deterministic_keypair(99).pk.into(), + validator_index: 99, + slot, + }; + harness.service.duties_service.il_duties.write().insert( + slot.epoch(E::slots_per_epoch()), + (Hash256::repeat_byte(0xab), vec![duty]), + ); + + let transactions = InclusionListTransactions { + transactions: Default::default(), + }; + harness + .harness + .mock_beacon_node_1 + .mock_get_validator_inclusion_list(&transactions, slot); + let ssz_mock = harness + .harness + .mock_beacon_node_1 + .mock_post_validator_inclusion_list_ssz(ForkName::Heze); + + let (duties, data) = harness + .service + .produce_inclusion_list_duties_data(slot) + .await + .unwrap() + .unwrap(); + let result = harness.service.sign_and_publish(slot, duties, data).await; + + // Nothing signed means nothing to publish, not a total publish failure + assert_eq!(result, Ok(())); + ssz_mock.expect(0).assert(); + } +} diff --git a/validator_client/validator_services/src/lib.rs b/validator_client/validator_services/src/lib.rs index 3db106ac692..9268c5a1d6c 100644 --- a/validator_client/validator_services/src/lib.rs +++ b/validator_client/validator_services/src/lib.rs @@ -2,6 +2,7 @@ pub mod attestation_service; pub mod block_service; pub mod builder_preferences_service; pub mod duties_service; +pub mod inclusion_list_service; pub mod latency_service; pub mod notifier_service; pub mod payload_attestation_service; diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index e76ae929ead..4b17f251346 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -8,11 +8,12 @@ use std::future::Future; use std::sync::Arc; use types::{ Address, Attestation, AttestationData, BlindedBeaconBlock, Epoch, EthSpec, - ExecutionPayloadEnvelope, Graffiti, Hash256, PayloadAttestationData, PayloadAttestationMessage, - ProposerPreferences, SelectionProof, SignedAggregateAndProof, SignedBlindedBeaconBlock, - SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedValidatorRegistrationData, SingleAttestation, Slot, SyncCommitteeContribution, - SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, + ExecutionPayloadEnvelope, Graffiti, Hash256, InclusionList, PayloadAttestationData, + PayloadAttestationMessage, ProposerPreferences, SelectionProof, SignedAggregateAndProof, + SignedBlindedBeaconBlock, SignedContributionAndProof, SignedExecutionPayloadEnvelope, + SignedInclusionList, SignedProposerPreferences, SignedValidatorRegistrationData, + SingleAttestation, Slot, SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, + SyncSubnetId, ValidatorRegistrationData, }; #[derive(Debug, PartialEq, Clone)] @@ -226,6 +227,12 @@ pub trait ValidatorStore: Send + Sync { request_auth_v1: RequestAuth, ) -> impl Future>> + Send; + fn sign_inclusion_list( + &self, + validator_pubkey: PublicKeyBytes, + inclusion_list: InclusionList, + ) -> impl Future>> + Send; + /// Returns `ProposalData` for the provided `pubkey` if it exists in `InitializedValidators`. /// `ProposalData` fields include defaulting logic described in `get_fee_recipient_defaulting`, /// `get_gas_limit_defaulting`, and `get_builder_proposals_defaulting`.