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

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