unstable merge

This commit is contained in:
hopinheimer
2026-03-17 02:44:31 -04:00
9 changed files with 172 additions and 62 deletions

View File

@@ -4845,43 +4845,22 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// get_attestation_score(parent, parent_payload_status) where parent_payload_status
// is determined by the head block's relationship to its parent.
let head_weight = info.head_node.weight();
let parent_weight = if let Ok(head_payload_status) = info.head_node.parent_payload_status()
{
// Post-GLOAS: use the payload-filtered weight matching how the head
// extends from its parent.
match head_payload_status {
proto_array::PayloadStatus::Full => {
info.parent_node.full_payload_weight().map_err(|()| {
Box::new(ProposerHeadError::Error(
Error::ProposerHeadForkChoiceError(
fork_choice::Error::ProtoArrayError(
proto_array::Error::InvalidNodeVariant {
block_root: info.parent_node.root(),
},
),
),
))
})?
let parent_weight =
if let (Ok(head_payload_status), Ok(parent_v29)) = (
info.head_node.parent_payload_status(),
info.parent_node.as_v29(),
) {
// Post-GLOAS: use the payload-filtered weight matching how the head
// extends from its parent.
match head_payload_status {
proto_array::PayloadStatus::Full => parent_v29.full_payload_weight,
proto_array::PayloadStatus::Empty => parent_v29.empty_payload_weight,
proto_array::PayloadStatus::Pending => info.parent_node.weight(),
}
proto_array::PayloadStatus::Empty => {
info.parent_node.empty_payload_weight().map_err(|()| {
Box::new(ProposerHeadError::Error(
Error::ProposerHeadForkChoiceError(
fork_choice::Error::ProtoArrayError(
proto_array::Error::InvalidNodeVariant {
block_root: info.parent_node.root(),
},
),
),
))
})?
}
proto_array::PayloadStatus::Pending => info.parent_node.weight(),
}
} else {
// Pre-GLOAS (V17): use total weight.
info.parent_node.weight()
};
} else {
// Pre-GLOAS or fork boundary: use total weight.
info.parent_node.weight()
};
let (head_weak, parent_strong) = if fork_choice_slot == re_org_block_slot {
(

View File

@@ -2104,9 +2104,7 @@ pub fn serve<T: BeaconChainTypes>(
let execution_status_string = node
.execution_status()
.ok()
.map(|status| status.to_string())
.unwrap_or_else(|| "n/a".to_string());
.map_or_else(|_| "irrelevant".to_string(), |s| s.to_string());
ForkChoiceNode {
slot: node.slot(),

View File

@@ -182,8 +182,11 @@ impl Default for ProposerBoost {
/// ancestors can compare children using payload-aware tie breaking.
#[derive(Clone, PartialEq, Debug, Copy)]
pub struct NodeDelta {
/// Total weight change for the node. All votes contribute regardless of payload status.
pub delta: i64,
/// Weight change from `PayloadStatus::Empty` votes.
pub empty_delta: i64,
/// Weight change from `PayloadStatus::Full` votes.
pub full_delta: i64,
}
@@ -399,6 +402,7 @@ impl ProtoArray {
// Apply the delta to the node.
if execution_status_is_invalid {
// Invalid nodes always have a weight of 0.
*node.weight_mut() = 0;
} else {
*node.weight_mut() = apply_delta(node.weight(), delta, node_index)?;
@@ -1127,10 +1131,9 @@ impl ProtoArray {
);
let no_change = (parent.best_child(), parent.best_descendant());
// For V29 (GLOAS) parents, the spec's virtual tree model requires choosing
// FULL or EMPTY direction at each node BEFORE considering concrete children.
// Only children whose parent_payload_status matches the preferred direction
// are eligible for best_child. This is PRIMARY, not a tiebreaker.
// For V29 (GLOAS) parents, the spec's virtual tree model determines a preferred
// FULL or EMPTY direction at each node. Weight is the primary selector among
// viable children; direction matching is the tiebreaker when weights are equal.
let child_matches_dir = child_matches_parent_payload_preference(
parent,
child,

View File

@@ -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<E: EthSpec>(
// 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<E: EthSpec>(
Ok(())
}
/// Process a builder voluntary exit. [New in Gloas:EIP7732]
fn process_builder_voluntary_exit<E: EthSpec>(
state: &mut BeaconState<E>,
signed_exit: &SignedVoluntaryExit,
verify_signatures: VerifySignatures,
spec: &ChainSpec,
) -> Result<(), BlockOperationError<ExitInvalid>> {
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<E: EthSpec>(
state: &mut BeaconState<E>,
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<E: EthSpec>(
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<E: EthSpec>(
state: &BeaconState<E>,
pubkey: &PublicKeyBytes,
spec: &ChainSpec,
) -> Result<bool, BlockProcessingError> {
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<E: EthSpec>(
state: &mut BeaconState<E>,
deposit_request: &DepositRequest,
@@ -835,10 +962,14 @@ pub fn process_deposit_request_post_gloas<E: EthSpec>(
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,

View File

@@ -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;

View File

@@ -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.

View File

@@ -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

View File

@@ -716,8 +716,13 @@ impl<E: EthSpec, O: Operation<E>> LoadCase for Operations<E, O> {
// 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),

View File

@@ -537,11 +537,6 @@ impl<E: EthSpec + TypeName> Handler for RandomHandler<E> {
fn handler_name(&self) -> String {
"random".into()
}
fn disabled_forks(&self) -> Vec<ForkName> {
// TODO(gloas): remove once we have Gloas random tests
vec![ForkName::Gloas]
}
}
#[derive(Educe)]