diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 06ec26185f..802b090f6a 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -1940,6 +1940,7 @@ fn load_parent>( { if block.as_block().is_parent_block_full(parent_bid_block_hash) { // TODO(gloas): loading the envelope here is not very efficient + // TODO(gloas): check parent payload existence prior to this point? let envelope = chain.store.get_payload_envelope(&root)?.ok_or_else(|| { BeaconChainError::DBInconsistent(format!( "Missing envelope for parent block {root:?}", diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 9743812632..ac64398655 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -4,7 +4,10 @@ use crate::common::{ get_attestation_participation_flag_indices, increase_balance, initiate_validator_exit, slash_validator, }; -use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; +use crate::per_block_processing::builder::{ + convert_validator_index_to_builder_index, is_builder_index, +}; +use crate::per_block_processing::errors::{BlockProcessingError, ExitInvalid, IntoWithIndex}; use crate::per_block_processing::verify_payload_attestation::verify_payload_attestation; use bls::{PublicKeyBytes, SignatureBytes}; use ssz_types::FixedVector; @@ -507,7 +510,26 @@ pub fn process_exits( // Verify and apply each exit in series. We iterate in series because higher-index exits may // become invalid due to the application of lower-index ones. for (i, exit) in voluntary_exits.iter().enumerate() { - verify_exit(state, None, exit, verify_signatures, spec) + // Exits must specify an epoch when they become valid; they are not valid before then. + let current_epoch = state.current_epoch(); + if current_epoch < exit.message.epoch { + return Err(BlockOperationError::invalid(ExitInvalid::FutureEpoch { + state: current_epoch, + exit: exit.message.epoch, + }) + .into_with_index(i)); + } + + // [New in Gloas:EIP7732] + if state.fork_name_unchecked().gloas_enabled() + && is_builder_index(exit.message.validator_index) + { + process_builder_voluntary_exit(state, exit, verify_signatures, spec) + .map_err(|e| e.into_with_index(i))?; + continue; + } + + verify_exit(state, Some(current_epoch), exit, verify_signatures, spec) .map_err(|e| e.into_with_index(i))?; initiate_validator_exit(state, exit.message.validator_index as usize, spec)?; @@ -515,6 +537,87 @@ pub fn process_exits( Ok(()) } +/// Process a builder voluntary exit. [New in Gloas:EIP7732] +fn process_builder_voluntary_exit( + state: &mut BeaconState, + signed_exit: &SignedVoluntaryExit, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockOperationError> { + let builder_index = + convert_validator_index_to_builder_index(signed_exit.message.validator_index); + + let builder = state + .builders()? + .get(builder_index as usize) + .cloned() + .ok_or(BlockOperationError::invalid(ExitInvalid::ValidatorUnknown( + signed_exit.message.validator_index, + )))?; + + // Verify the builder is active + let finalized_epoch = state.finalized_checkpoint().epoch; + if !builder.is_active_at_finalized_epoch(finalized_epoch, spec) { + return Err(BlockOperationError::invalid(ExitInvalid::NotActive( + signed_exit.message.validator_index, + ))); + } + + // Only exit builder if it has no pending withdrawals in the queue + let pending_balance = state.get_pending_balance_to_withdraw_for_builder(builder_index)?; + if pending_balance != 0 { + return Err(BlockOperationError::invalid( + ExitInvalid::PendingWithdrawalInQueue(signed_exit.message.validator_index), + )); + } + + // Verify signature (using EIP-7044 domain: capella_fork_version for Deneb+) + if verify_signatures.is_true() { + let pubkey = builder.pubkey; + let domain = spec.compute_domain( + Domain::VoluntaryExit, + spec.capella_fork_version, + state.genesis_validators_root(), + ); + let message = signed_exit.message.signing_root(domain); + // TODO(gloas): use builder pubkey cache once available + let bls_pubkey = pubkey + .decompress() + .map_err(|_| BlockOperationError::invalid(ExitInvalid::BadSignature))?; + if !signed_exit.signature.verify(&bls_pubkey, message) { + return Err(BlockOperationError::invalid(ExitInvalid::BadSignature)); + } + } + + // Initiate builder exit + initiate_builder_exit(state, builder_index, spec)?; + + Ok(()) +} + +/// Initiate the exit of a builder. [New in Gloas:EIP7732] +fn initiate_builder_exit( + state: &mut BeaconState, + builder_index: u64, + spec: &ChainSpec, +) -> Result<(), BeaconStateError> { + let current_epoch = state.current_epoch(); + let builder = state + .builders_mut()? + .get_mut(builder_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(builder_index))?; + + // Return if builder already initiated exit + if builder.withdrawable_epoch != spec.far_future_epoch { + return Ok(()); + } + + // Set builder exit epoch + builder.withdrawable_epoch = current_epoch.safe_add(spec.min_builder_withdrawability_delay)?; + + Ok(()) +} + /// Validates each `bls_to_execution_change` and updates the state /// /// Returns `Ok(())` if the validation and state updates completed successfully. Otherwise returns @@ -814,6 +917,30 @@ pub fn process_deposit_requests_post_gloas( Ok(()) } +/// Check if there is a pending deposit for a new validator with the given pubkey. +// TODO(gloas): cache the deposit signature validation or remove this loop entirely if possible, +// it is `O(n * m)` where `n` is max 8192 and `m` is max 128M. +fn is_pending_validator( + state: &BeaconState, + pubkey: &PublicKeyBytes, + spec: &ChainSpec, +) -> Result { + for deposit in state.pending_deposits()?.iter() { + if deposit.pubkey == *pubkey { + let deposit_data = DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: deposit.signature.clone(), + }; + if is_valid_deposit_signature(&deposit_data, spec).is_ok() { + return Ok(true); + } + } + } + Ok(false) +} + pub fn process_deposit_request_post_gloas( state: &mut BeaconState, deposit_request: &DepositRequest, @@ -835,10 +962,14 @@ pub fn process_deposit_request_post_gloas( let validator_index = state.get_validator_index(&deposit_request.pubkey)?; let is_validator = validator_index.is_some(); - let is_builder_prefix = + let has_builder_prefix = is_builder_withdrawal_credential(deposit_request.withdrawal_credentials, spec); - if is_builder || (is_builder_prefix && !is_validator) { + if is_builder + || (has_builder_prefix + && !is_validator + && !is_pending_validator(state, &deposit_request.pubkey, spec)?) + { // Apply builder deposits immediately apply_deposit_for_builder( state, diff --git a/consensus/types/src/core/consts.rs b/consensus/types/src/core/consts.rs index 0d4c0591cb..049094da76 100644 --- a/consensus/types/src/core/consts.rs +++ b/consensus/types/src/core/consts.rs @@ -31,9 +31,9 @@ pub mod gloas { // Fork choice constants pub type PayloadStatus = u8; - pub const PAYLOAD_STATUS_PENDING: PayloadStatus = 0; - pub const PAYLOAD_STATUS_EMPTY: PayloadStatus = 1; - pub const PAYLOAD_STATUS_FULL: PayloadStatus = 2; + pub const PAYLOAD_STATUS_EMPTY: PayloadStatus = 0; + pub const PAYLOAD_STATUS_FULL: PayloadStatus = 1; + pub const PAYLOAD_STATUS_PENDING: PayloadStatus = 2; pub const ATTESTATION_TIMELINESS_INDEX: usize = 0; pub const PTC_TIMELINESS_INDEX: usize = 1; diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index fd8a3f6da0..48378a4c95 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,6 +1,6 @@ # To download/extract nightly tests, run: # CONSENSUS_SPECS_TEST_VERSION=nightly make -CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.2 +CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.3 REPO_NAME := consensus-spec-tests OUTPUT_DIR := ./$(REPO_NAME) diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 782b554ff1..dd6be14306 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -47,6 +47,8 @@ excluded_paths = [ "bls12-381-tests/hash_to_G2", "tests/.*/eip7732", "tests/.*/eip7805", + # Heze fork is not implemented + "tests/.*/heze/.*", # TODO(gloas): remove these ignores as Gloas consensus is implemented "tests/.*/gloas/fork_choice/.*", # Ignore MatrixEntry SSZ tests for now. diff --git a/testing/ef_tests/download_test_vectors.sh b/testing/ef_tests/download_test_vectors.sh index ff5b61bb47..f91b2d1c38 100755 --- a/testing/ef_tests/download_test_vectors.sh +++ b/testing/ef_tests/download_test_vectors.sh @@ -10,7 +10,7 @@ if [[ "$version" == "nightly" || "$version" =~ ^nightly-[0-9]+$ ]]; then exit 1 fi - for cmd in unzip jq; do + for cmd in jq; do if ! command -v "${cmd}" >/dev/null 2>&1; then echo "Error ${cmd} is not installed" exit 1 @@ -48,13 +48,10 @@ if [[ "$version" == "nightly" || "$version" =~ ^nightly-[0-9]+$ ]]; then echo "Downloading artifact: ${name}" curl --progress-bar --location --show-error --retry 3 --retry-all-errors --fail \ -H "${auth_header}" -H "Accept: application/vnd.github+json" \ - --output "${name}.zip" "${url}" || { + --output "${name}" "${url}" || { echo "Failed to download ${name}" exit 1 } - - unzip -qo "${name}.zip" - rm -f "${name}.zip" done else for test in "${TESTS[@]}"; do diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index ca0124e1aa..798c66b666 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -716,8 +716,13 @@ impl> LoadCase for Operations { // Check BLS setting here before SSZ deserialization, as most types require signatures // to be valid. - let (operation, bls_error) = if metadata.bls_setting.unwrap_or_default().check().is_ok() { - match O::decode(&path.join(O::filename()), fork_name, spec) { + let operation_path = path.join(O::filename()); + let (operation, bls_error) = if !operation_path.is_file() { + // Some test cases (e.g. builder_voluntary_exit__success) have no operation file. + // TODO(gloas): remove this once the test vectors are fixed + (None, None) + } else if metadata.bls_setting.unwrap_or_default().check().is_ok() { + match O::decode(&operation_path, fork_name, spec) { Ok(op) => (Some(op), None), Err(Error::InvalidBLSInput(error)) => (None, Some(error)), Err(e) => return Err(e), diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index da3c5533b6..f8c16aec0b 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -537,11 +537,6 @@ impl Handler for RandomHandler { fn handler_name(&self) -> String { "random".into() } - - fn disabled_forks(&self) -> Vec { - // TODO(gloas): remove once we have Gloas random tests - vec![ForkName::Gloas] - } } #[derive(Educe)]