From 5299be4e255855b79230e8d6de138532cde49474 Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Wed, 29 Apr 2026 17:15:10 +0200 Subject: [PATCH 01/16] Update ethspecify. Simplify applyParentExecutionPayload --- .../gloas/block/BlockProcessorGloas.java | 4 +- specrefs/.ethspecify.yml | 5 +- specrefs/functions.yml | 73 +++++++++++++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/block/BlockProcessorGloas.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/block/BlockProcessorGloas.java index 8a7dea815e1..b8712476387 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/block/BlockProcessorGloas.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/block/BlockProcessorGloas.java @@ -152,15 +152,15 @@ public void processParentExecutionPayload( "The execution requests root in the latest committed bid does not match the parent execution requests in the block"); } - applyParentExecutionPayload(stateGloas, parentBid, requests, validatorExitContextSupplier); + applyParentExecutionPayload(stateGloas, requests, validatorExitContextSupplier); } // apply_parent_execution_payload protected void applyParentExecutionPayload( final MutableBeaconStateGloas state, - final ExecutionPayloadBid parentBid, final ExecutionRequests requests, final Supplier validatorExitContextSupplier) { + final ExecutionPayloadBid parentBid = state.getLatestExecutionPayloadBid(); final UInt64 parentSlot = parentBid.getSlot(); final UInt64 parentEpoch = miscHelpers.computeEpochAtSlot(parentSlot); diff --git a/specrefs/.ethspecify.yml b/specrefs/.ethspecify.yml index 240ffd5b569..1179dea7328 100644 --- a/specrefs/.ethspecify.yml +++ b/specrefs/.ethspecify.yml @@ -1,4 +1,4 @@ -version: v1.7.0-alpha.5 +version: v1.7.0-alpha.6 style: full specrefs: @@ -401,10 +401,7 @@ specrefs: - get_inclusion_list_committee_assignment#heze - get_inclusion_list_signature#heze - get_inclusion_list_store#heze - - get_inclusion_list_submission_due_ms#heze - get_inclusion_list_transactions#heze - - get_proposer_inclusion_list_cutoff_ms#heze - - get_view_freeze_cutoff_ms#heze - is_inclusion_list_bits_inclusive#heze - is_payload_inclusion_list_satisfied#heze - is_valid_inclusion_list_signature#heze diff --git a/specrefs/functions.yml b/specrefs/functions.yml index 3b91620c346..bd4c06094f0 100644 --- a/specrefs/functions.yml +++ b/specrefs/functions.yml @@ -10233,6 +10233,79 @@ state.deposit_balance_to_consume = Gwei(0) +- name: process_pending_deposits#gloas + sources: + - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/statetransition/epoch/EpochProcessorElectra.java + search: public void processPendingDeposits( + spec: | + + def process_pending_deposits(state: BeaconState) -> None: + next_epoch = Epoch(get_current_epoch(state) + 1) + # [Modified in Gloas:EIP8061] + # Deposits still consume the activation-only churn budget in Gloas. + available_for_processing = state.deposit_balance_to_consume + get_activation_churn_limit(state) + processed_amount = 0 + next_deposit_index = 0 + deposits_to_postpone = [] + is_churn_limit_reached = False + finalized_slot = compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) + + for deposit in state.pending_deposits: + # Do not process deposit requests if Eth1 bridge deposits are not yet applied. + if ( + # Is deposit request + deposit.slot > GENESIS_SLOT + and + # There are pending Eth1 bridge deposits + state.eth1_deposit_index < state.deposit_requests_start_index + ): + break + + # Check if deposit has been finalized, otherwise, stop processing. + if deposit.slot > finalized_slot: + break + + # Check if number of processed deposits has not reached the limit, otherwise, stop processing. + if next_deposit_index >= MAX_PENDING_DEPOSITS_PER_EPOCH: + break + + # Read validator state + is_validator_exited = False + is_validator_withdrawn = False + validator_pubkeys = [v.pubkey for v in state.validators] + if deposit.pubkey in validator_pubkeys: + validator = state.validators[ValidatorIndex(validator_pubkeys.index(deposit.pubkey))] + is_validator_exited = validator.exit_epoch < FAR_FUTURE_EPOCH + is_validator_withdrawn = validator.withdrawable_epoch < next_epoch + + if is_validator_withdrawn: + # Deposited balance will never become active. Increase balance but do not consume churn + apply_pending_deposit(state, deposit) + elif is_validator_exited: + # Validator is exiting, postpone the deposit until after withdrawable epoch + deposits_to_postpone.append(deposit) + else: + # Check if deposit fits in the churn, otherwise, do no more deposit processing in this epoch. + is_churn_limit_reached = processed_amount + deposit.amount > available_for_processing + if is_churn_limit_reached: + break + + # Consume churn and apply deposit. + processed_amount += deposit.amount + apply_pending_deposit(state, deposit) + + # Regardless of how the deposit was handled, we move on in the queue. + next_deposit_index += 1 + + state.pending_deposits = state.pending_deposits[next_deposit_index:] + deposits_to_postpone + + # Accumulate churn only if the churn limit has been hit. + if is_churn_limit_reached: + state.deposit_balance_to_consume = available_for_processing - processed_amount + else: + state.deposit_balance_to_consume = Gwei(0) + + - name: process_proposer_lookahead#fulu sources: - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/statetransition/epoch/EpochProcessorFulu.java From 00f7a1e02faa470403b3fdc371bc2eb6ecc085f9 Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 10:00:46 +0200 Subject: [PATCH 02/16] Update ethspecify for alpha.6 exits --- specrefs/configs.yml | 30 +++++++++++++++- specrefs/functions.yml | 77 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/specrefs/configs.yml b/specrefs/configs.yml index 9498ccf0982..f7b3f02a1f0 100644 --- a/specrefs/configs.yml +++ b/specrefs/configs.yml @@ -164,12 +164,22 @@ - name: CHURN_LIMIT_QUOTIENT#phase0 sources: - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml - search: "CHURN_LIMIT_QUOTIENT:" + search: "^CHURN_LIMIT_QUOTIENT:" + regex: true spec: | CHURN_LIMIT_QUOTIENT: uint64 = 65536 +- name: CHURN_LIMIT_QUOTIENT_GLOAS#gloas + sources: + - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml + search: "CHURN_LIMIT_QUOTIENT_GLOAS:" + spec: | + + CHURN_LIMIT_QUOTIENT_GLOAS: uint64 = 32768 + + - name: CONFIRMATION_BYZANTINE_THRESHOLD#phase0 sources: [] spec: | @@ -177,6 +187,15 @@ CONFIRMATION_BYZANTINE_THRESHOLD: uint64 = 25 +- name: CONSOLIDATION_CHURN_LIMIT_QUOTIENT#gloas + sources: + - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml + search: "CONSOLIDATION_CHURN_LIMIT_QUOTIENT:" + spec: | + + CONSOLIDATION_CHURN_LIMIT_QUOTIENT: uint64 = 65536 + + - name: CONTRIBUTION_DUE_BPS#altair sources: - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml @@ -431,6 +450,15 @@ MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: uint64 = 8 +- name: MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS#gloas + sources: + - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml + search: "MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS:" + spec: | + + MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS: Gwei = 256000000000 + + - name: MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT#electra sources: - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml diff --git a/specrefs/functions.yml b/specrefs/functions.yml index bd4c06094f0..242ed59e500 100644 --- a/specrefs/functions.yml +++ b/specrefs/functions.yml @@ -941,6 +941,40 @@ return state.earliest_exit_epoch +- name: compute_exit_epoch_and_update_churn#gloas + sources: + - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/BeaconStateMutatorsGloas.java + search: public UInt64 computeExitEpochAndUpdateChurn( + spec: | + + def compute_exit_epoch_and_update_churn(state: BeaconState, exit_balance: Gwei) -> Epoch: + earliest_exit_epoch = max( + state.earliest_exit_epoch, compute_activation_exit_epoch(get_current_epoch(state)) + ) + # [Modified in Gloas:EIP8061] + # Exits use the uncapped per-epoch exit churn limit instead of the + # combined activation/exit churn shared in Electra. + per_epoch_churn = get_exit_churn_limit(state) + # New epoch for exits. + if state.earliest_exit_epoch < earliest_exit_epoch: + exit_balance_to_consume = per_epoch_churn + else: + exit_balance_to_consume = state.exit_balance_to_consume + + # Exit doesn't fit in the current earliest epoch. + if exit_balance > exit_balance_to_consume: + balance_to_process = exit_balance - exit_balance_to_consume + additional_epochs = (balance_to_process - 1) // per_epoch_churn + 1 + earliest_exit_epoch += additional_epochs + exit_balance_to_consume += additional_epochs * per_epoch_churn + + # Consume the balance and update state variables. + state.exit_balance_to_consume = exit_balance_to_consume - exit_balance + state.earliest_exit_epoch = earliest_exit_epoch + + return state.earliest_exit_epoch + + - name: compute_fork_data_root#phase0 sources: - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/helpers/MiscHelpers.java @@ -2715,6 +2749,19 @@ return KZGCommitment(bls.G1_to_bytes48(result)) +- name: get_activation_churn_limit#gloas + sources: + - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/BeaconStateAccessorsGloas.java + search: public UInt64 getActivationChurnLimit( + spec: | + + def get_activation_churn_limit(state: BeaconState) -> Gwei: + """ + Return the churn limit for the current epoch dedicated to activations. + """ + return min(MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS, get_balance_churn_limit(state)) + + - name: get_activation_exit_churn_limit#electra sources: - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/helpers/BeaconStateAccessorsElectra.java @@ -3723,6 +3770,23 @@ return get_balance_churn_limit(state) - get_activation_exit_churn_limit(state) +- name: get_consolidation_churn_limit#gloas + sources: + - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/BeaconStateAccessorsGloas.java + search: public UInt64 getConsolidationChurnLimit( + spec: | + + def get_consolidation_churn_limit(state: BeaconState) -> Gwei: + # [Modified in Gloas:EIP8061] + # Consolidation churn is now derived independently from the + # activation/exit churn via CONSOLIDATION_CHURN_LIMIT_QUOTIENT. + churn = max( + MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA, + get_total_active_balance(state) // CONSOLIDATION_CHURN_LIMIT_QUOTIENT, + ) + return churn - churn % EFFECTIVE_BALANCE_INCREMENT + + - name: get_contribution_and_proof#altair sources: [] spec: | @@ -4364,6 +4428,19 @@ ] +- name: get_exit_churn_limit#gloas + sources: + - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/BeaconStateAccessorsGloas.java + search: public UInt64 getExitChurnLimit( + spec: | + + def get_exit_churn_limit(state: BeaconState) -> Gwei: + """ + Return the uncapped churn limit for the current epoch dedicated to exits. + """ + return get_balance_churn_limit(state) + + - name: get_expected_withdrawals#capella sources: - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/capella/withdrawals/WithdrawalsHelpersCapella.java From 97230ca71e20db87e0403ca3f66716b474be8cbc Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 10:53:27 +0200 Subject: [PATCH 03/16] Clarify payment eviction if older than previous epoch --- .../spec/logic/versions/gloas/block/BlockProcessorGloas.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/block/BlockProcessorGloas.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/block/BlockProcessorGloas.java index b8712476387..07bc55ad5bb 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/block/BlockProcessorGloas.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/block/BlockProcessorGloas.java @@ -180,6 +180,8 @@ protected void applyParentExecutionPayload( final UInt64 paymentIndex = parentSlot.mod(specConfig.getSlotsPerEpoch()); beaconStateMutatorsGloas.settleBuilderPayment(state, paymentIndex); } else if (parentBid.getValue().isGreaterThan(UInt64.ZERO)) { + // Parent is older than the previous epoch, its payment entry has been + // evicted from builder_pending_payments. Append the withdrawal directly. state .getBuilderPendingWithdrawals() .append( From d95cfae8d16c946713b392934f77f03d15109f39 Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 11:53:46 +0200 Subject: [PATCH 04/16] Update ethspecify to alpha.7 and ExecutionPayloadEnvelope and ProposerPreferences and everything linked --- ...nblindingExecutionPayloadProviderTest.java | 1 + .../BlindedExecutionPayloadEnvelope.java | 22 +++- ...BlindedExecutionPayloadEnvelopeSchema.java | 18 ++- .../gloas/ExecutionPayloadEnvelope.java | 24 +++- .../ExecutionPayloadEnvelopeInvariants.java | 4 +- .../gloas/ExecutionPayloadEnvelopeSchema.java | 19 ++- .../versions/gloas/ProposerPreferences.java | 21 +++- .../gloas/ProposerPreferencesSchema.java | 12 +- .../util/ExecutionPayloadProposalUtil.java | 3 +- .../ExecutionPayloadVerifierGloas.java | 13 +- .../ExecutionPayloadProposalTestUtil.java | 3 +- .../teku/spec/util/DataStructureUtil.java | 6 +- .../forkchoice/ForkChoice.java | 3 +- .../validation/GossipValidationHelper.java | 16 +-- .../ProposerPreferencesGossipValidator.java | 119 +++++++++++------- ...roposerPreferencesGossipValidatorTest.java | 52 ++++++-- specrefs/.ethspecify.yml | 2 +- specrefs/containers.yml | 6 +- .../client/ProposerPreferencesPublisher.java | 11 +- 19 files changed, 243 insertions(+), 112 deletions(-) diff --git a/ethereum/dataproviders/src/test/java/tech/pegasys/teku/dataproviders/lookup/UnblindingExecutionPayloadProviderTest.java b/ethereum/dataproviders/src/test/java/tech/pegasys/teku/dataproviders/lookup/UnblindingExecutionPayloadProviderTest.java index b0880751348..72964c24342 100644 --- a/ethereum/dataproviders/src/test/java/tech/pegasys/teku/dataproviders/lookup/UnblindingExecutionPayloadProviderTest.java +++ b/ethereum/dataproviders/src/test/java/tech/pegasys/teku/dataproviders/lookup/UnblindingExecutionPayloadProviderTest.java @@ -135,6 +135,7 @@ void shouldSkipWhenElReturnsNullBlockAccessList() { executionPayload, dataStructureUtil.randomExecutionRequests(), dataStructureUtil.randomBuilderIndex(), + dataStructureUtil.randomBytes32(), dataStructureUtil.randomBytes32()), dataStructureUtil.randomSignature()); final Bytes32 blockRoot = originalEnvelope.getBeaconBlockRoot(); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/BlindedExecutionPayloadEnvelope.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/BlindedExecutionPayloadEnvelope.java index cf437e20ad0..8e6c83eb98b 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/BlindedExecutionPayloadEnvelope.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/BlindedExecutionPayloadEnvelope.java @@ -16,7 +16,7 @@ import static com.google.common.base.Preconditions.checkState; import org.apache.tuweni.bytes.Bytes32; -import tech.pegasys.teku.infrastructure.ssz.containers.Container4; +import tech.pegasys.teku.infrastructure.ssz.containers.Container5; import tech.pegasys.teku.infrastructure.ssz.primitive.SszBytes32; import tech.pegasys.teku.infrastructure.ssz.primitive.SszUInt64; import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; @@ -29,11 +29,12 @@ import tech.pegasys.teku.spec.schemas.SchemaDefinitionsGloas; public class BlindedExecutionPayloadEnvelope - extends Container4< + extends Container5< BlindedExecutionPayloadEnvelope, ExecutionPayloadHeader, ExecutionRequests, SszUInt64, + SszBytes32, SszBytes32> { BlindedExecutionPayloadEnvelope( @@ -41,13 +42,15 @@ public class BlindedExecutionPayloadEnvelope final ExecutionPayloadHeader payloadHeader, final ExecutionRequests executionRequests, final UInt64 builderIndex, - final Bytes32 beaconBlockRoot) { + final Bytes32 beaconBlockRoot, + final Bytes32 parentBeaconBlockRoot) { super( schema, payloadHeader, executionRequests, SszUInt64.of(builderIndex), - SszBytes32.of(beaconBlockRoot)); + SszBytes32.of(beaconBlockRoot), + SszBytes32.of(parentBeaconBlockRoot)); } BlindedExecutionPayloadEnvelope( @@ -71,6 +74,10 @@ public Bytes32 getBeaconBlockRoot() { return getField3().get(); } + public Bytes32 getParentBeaconBlockRoot() { + return getField4().get(); + } + public UInt64 getSlot() { return ExecutionPayloadHeaderGloas.required(getPayloadHeader()).getSlotNumber(); } @@ -92,7 +99,12 @@ public ExecutionPayloadEnvelope unblind( final ExecutionPayloadEnvelope executionPayloadEnvelope = schemaDefinitions .getExecutionPayloadEnvelopeSchema() - .create(payload, getExecutionRequests(), getBuilderIndex(), getBeaconBlockRoot()); + .create( + payload, + getExecutionRequests(), + getBuilderIndex(), + getBeaconBlockRoot(), + getParentBeaconBlockRoot()); checkState( executionPayloadEnvelope.hashTreeRoot().equals(hashTreeRoot()), "unblinded execution payload envelope root does not match original envelope root"); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/BlindedExecutionPayloadEnvelopeSchema.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/BlindedExecutionPayloadEnvelopeSchema.java index 739892d244a..30907652050 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/BlindedExecutionPayloadEnvelopeSchema.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/BlindedExecutionPayloadEnvelopeSchema.java @@ -17,7 +17,7 @@ import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.EXECUTION_REQUESTS_SCHEMA; import org.apache.tuweni.bytes.Bytes32; -import tech.pegasys.teku.infrastructure.ssz.containers.ContainerSchema4; +import tech.pegasys.teku.infrastructure.ssz.containers.ContainerSchema5; import tech.pegasys.teku.infrastructure.ssz.primitive.SszBytes32; import tech.pegasys.teku.infrastructure.ssz.primitive.SszUInt64; import tech.pegasys.teku.infrastructure.ssz.schema.SszPrimitiveSchemas; @@ -29,11 +29,12 @@ import tech.pegasys.teku.spec.schemas.registry.SchemaRegistry; public class BlindedExecutionPayloadEnvelopeSchema - extends ContainerSchema4< + extends ContainerSchema5< BlindedExecutionPayloadEnvelope, ExecutionPayloadHeader, ExecutionRequests, SszUInt64, + SszBytes32, SszBytes32> { public BlindedExecutionPayloadEnvelopeSchema(final SchemaRegistry schemaRegistry) { @@ -45,16 +46,23 @@ public BlindedExecutionPayloadEnvelopeSchema(final SchemaRegistry schemaRegistry ExecutionPayloadHeader.class, schemaRegistry.get(EXECUTION_PAYLOAD_HEADER_SCHEMA))), namedSchema("execution_requests", schemaRegistry.get(EXECUTION_REQUESTS_SCHEMA)), namedSchema("builder_index", SszPrimitiveSchemas.UINT64_SCHEMA), - namedSchema("beacon_block_root", SszPrimitiveSchemas.BYTES32_SCHEMA)); + namedSchema("beacon_block_root", SszPrimitiveSchemas.BYTES32_SCHEMA), + namedSchema("parent_beacon_block_root", SszPrimitiveSchemas.BYTES32_SCHEMA)); } public BlindedExecutionPayloadEnvelope create( final ExecutionPayloadHeader payloadHeader, final ExecutionRequests executionRequests, final UInt64 builderIndex, - final Bytes32 beaconBlockRoot) { + final Bytes32 beaconBlockRoot, + final Bytes32 parentBeaconBlockRoot) { return new BlindedExecutionPayloadEnvelope( - this, payloadHeader, executionRequests, builderIndex, beaconBlockRoot); + this, + payloadHeader, + executionRequests, + builderIndex, + beaconBlockRoot, + parentBeaconBlockRoot); } @Override diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ExecutionPayloadEnvelope.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ExecutionPayloadEnvelope.java index c0db591ef48..2efee207449 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ExecutionPayloadEnvelope.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ExecutionPayloadEnvelope.java @@ -16,7 +16,7 @@ import static com.google.common.base.Preconditions.checkState; import org.apache.tuweni.bytes.Bytes32; -import tech.pegasys.teku.infrastructure.ssz.containers.Container4; +import tech.pegasys.teku.infrastructure.ssz.containers.Container5; import tech.pegasys.teku.infrastructure.ssz.primitive.SszBytes32; import tech.pegasys.teku.infrastructure.ssz.primitive.SszUInt64; import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; @@ -29,21 +29,28 @@ import tech.pegasys.teku.spec.schemas.SchemaDefinitionsGloas; public class ExecutionPayloadEnvelope - extends Container4< - ExecutionPayloadEnvelope, ExecutionPayload, ExecutionRequests, SszUInt64, SszBytes32> { + extends Container5< + ExecutionPayloadEnvelope, + ExecutionPayload, + ExecutionRequests, + SszUInt64, + SszBytes32, + SszBytes32> { ExecutionPayloadEnvelope( final ExecutionPayloadEnvelopeSchema schema, final ExecutionPayload payload, final ExecutionRequests executionRequests, final UInt64 builderIndex, - final Bytes32 beaconBlockRoot) { + final Bytes32 beaconBlockRoot, + final Bytes32 parentBeaconBlockRoot) { super( schema, payload, executionRequests, SszUInt64.of(builderIndex), - SszBytes32.of(beaconBlockRoot)); + SszBytes32.of(beaconBlockRoot), + SszBytes32.of(parentBeaconBlockRoot)); } ExecutionPayloadEnvelope(final ExecutionPayloadEnvelopeSchema type, final TreeNode backingNode) { @@ -66,6 +73,10 @@ public Bytes32 getBeaconBlockRoot() { return getField3().get(); } + public Bytes32 getParentBeaconBlockRoot() { + return getField4().get(); + } + public UInt64 getSlot() { return ExecutionPayloadGloas.required(getPayload()).getSlotNumber(); } @@ -93,7 +104,8 @@ public BlindedExecutionPayloadEnvelope blind(final SchemaDefinitionsGloas schema .createFromExecutionPayload(getPayload()), getExecutionRequests(), getBuilderIndex(), - getBeaconBlockRoot()); + getBeaconBlockRoot(), + getParentBeaconBlockRoot()); checkState( blinded.hashTreeRoot().equals(hashTreeRoot()), "Blinded root does not match the unblinded root"); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ExecutionPayloadEnvelopeInvariants.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ExecutionPayloadEnvelopeInvariants.java index 04cc4cac391..4ffb57a5046 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ExecutionPayloadEnvelopeInvariants.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ExecutionPayloadEnvelopeInvariants.java @@ -33,11 +33,13 @@ public class ExecutionPayloadEnvelopeInvariants { BYTES_PER_LENGTH_OFFSET + SszSignatureSchema.INSTANCE.getSszFixedPartSize(); // Fixed part of ExecutionPayloadEnvelope: - // payload_offset(4) + execution_requests_offset(4) + builder_index(8) + beacon_block_root(32) + // payload_offset(4) + execution_requests_offset(4) + builder_index(8) + + // beacon_block_root(32) + parent_beacon_block_root(32) private static final int EXECUTION_PAYLOAD_ENVELOPE_FIXED_PART_SIZE = BYTES_PER_LENGTH_OFFSET + BYTES_PER_LENGTH_OFFSET + UINT64_SCHEMA.getSszFixedPartSize() + + BYTES32_SCHEMA.getSszFixedPartSize() + BYTES32_SCHEMA.getSszFixedPartSize(); // Common fixed-part prefix shared by both ExecutionPayload and ExecutionPayloadHeader diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ExecutionPayloadEnvelopeSchema.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ExecutionPayloadEnvelopeSchema.java index 285a0f3a4ba..f84a778c507 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ExecutionPayloadEnvelopeSchema.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ExecutionPayloadEnvelopeSchema.java @@ -17,7 +17,7 @@ import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.EXECUTION_REQUESTS_SCHEMA; import org.apache.tuweni.bytes.Bytes32; -import tech.pegasys.teku.infrastructure.ssz.containers.ContainerSchema4; +import tech.pegasys.teku.infrastructure.ssz.containers.ContainerSchema5; import tech.pegasys.teku.infrastructure.ssz.primitive.SszBytes32; import tech.pegasys.teku.infrastructure.ssz.primitive.SszUInt64; import tech.pegasys.teku.infrastructure.ssz.schema.SszPrimitiveSchemas; @@ -29,8 +29,13 @@ import tech.pegasys.teku.spec.schemas.registry.SchemaRegistry; public class ExecutionPayloadEnvelopeSchema - extends ContainerSchema4< - ExecutionPayloadEnvelope, ExecutionPayload, ExecutionRequests, SszUInt64, SszBytes32> { + extends ContainerSchema5< + ExecutionPayloadEnvelope, + ExecutionPayload, + ExecutionRequests, + SszUInt64, + SszBytes32, + SszBytes32> { public ExecutionPayloadEnvelopeSchema(final SchemaRegistry schemaRegistry) { super( @@ -40,16 +45,18 @@ public ExecutionPayloadEnvelopeSchema(final SchemaRegistry schemaRegistry) { SszSchema.as(ExecutionPayload.class, schemaRegistry.get(EXECUTION_PAYLOAD_SCHEMA))), namedSchema("execution_requests", schemaRegistry.get(EXECUTION_REQUESTS_SCHEMA)), namedSchema("builder_index", SszPrimitiveSchemas.UINT64_SCHEMA), - namedSchema("beacon_block_root", SszPrimitiveSchemas.BYTES32_SCHEMA)); + namedSchema("beacon_block_root", SszPrimitiveSchemas.BYTES32_SCHEMA), + namedSchema("parent_beacon_block_root", SszPrimitiveSchemas.BYTES32_SCHEMA)); } public ExecutionPayloadEnvelope create( final ExecutionPayload payload, final ExecutionRequests executionRequests, final UInt64 builderIndex, - final Bytes32 beaconBlockRoot) { + final Bytes32 beaconBlockRoot, + final Bytes32 parentBeaconBlockRoot) { return new ExecutionPayloadEnvelope( - this, payload, executionRequests, builderIndex, beaconBlockRoot); + this, payload, executionRequests, builderIndex, beaconBlockRoot, parentBeaconBlockRoot); } @Override diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ProposerPreferences.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ProposerPreferences.java index 978422ccaad..6bf470dd581 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ProposerPreferences.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ProposerPreferences.java @@ -13,24 +13,29 @@ package tech.pegasys.teku.spec.datastructures.epbs.versions.gloas; +import org.apache.tuweni.bytes.Bytes32; import tech.pegasys.teku.ethereum.execution.types.Eth1Address; import tech.pegasys.teku.infrastructure.ssz.collections.SszByteVector; -import tech.pegasys.teku.infrastructure.ssz.containers.Container4; +import tech.pegasys.teku.infrastructure.ssz.containers.Container5; +import tech.pegasys.teku.infrastructure.ssz.primitive.SszBytes32; import tech.pegasys.teku.infrastructure.ssz.primitive.SszUInt64; import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; import tech.pegasys.teku.infrastructure.unsigned.UInt64; public class ProposerPreferences - extends Container4 { + extends Container5< + ProposerPreferences, SszBytes32, SszUInt64, SszUInt64, SszByteVector, SszUInt64> { protected ProposerPreferences( final ProposerPreferencesSchema schema, + final Bytes32 dependentRoot, final UInt64 proposalSlot, final UInt64 validatorIndex, final Eth1Address feeRecipient, final UInt64 gasLimit) { super( schema, + SszBytes32.of(dependentRoot), SszUInt64.of(proposalSlot), SszUInt64.of(validatorIndex), SszByteVector.fromBytes(feeRecipient.getWrappedBytes()), @@ -42,20 +47,24 @@ protected ProposerPreferences( super(schema, backingTree); } - public UInt64 getProposalSlot() { + public Bytes32 getDependentRoot() { return getField0().get(); } - public UInt64 getValidatorIndex() { + public UInt64 getProposalSlot() { return getField1().get(); } + public UInt64 getValidatorIndex() { + return getField2().get(); + } + public Eth1Address getFeeRecipient() { - return Eth1Address.fromBytes(getField2().getBytes()); + return Eth1Address.fromBytes(getField3().getBytes()); } public UInt64 getGasLimit() { - return getField3().get(); + return getField4().get(); } @Override diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ProposerPreferencesSchema.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ProposerPreferencesSchema.java index 1a7354b6f83..0385e1644b7 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ProposerPreferencesSchema.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ProposerPreferencesSchema.java @@ -13,10 +13,12 @@ package tech.pegasys.teku.spec.datastructures.epbs.versions.gloas; +import org.apache.tuweni.bytes.Bytes32; import tech.pegasys.teku.ethereum.execution.types.Eth1Address; import tech.pegasys.teku.infrastructure.bytes.Bytes20; import tech.pegasys.teku.infrastructure.ssz.collections.SszByteVector; -import tech.pegasys.teku.infrastructure.ssz.containers.ContainerSchema4; +import tech.pegasys.teku.infrastructure.ssz.containers.ContainerSchema5; +import tech.pegasys.teku.infrastructure.ssz.primitive.SszBytes32; import tech.pegasys.teku.infrastructure.ssz.primitive.SszUInt64; import tech.pegasys.teku.infrastructure.ssz.schema.SszPrimitiveSchemas; import tech.pegasys.teku.infrastructure.ssz.schema.collections.SszByteVectorSchema; @@ -24,11 +26,13 @@ import tech.pegasys.teku.infrastructure.unsigned.UInt64; public class ProposerPreferencesSchema - extends ContainerSchema4 { + extends ContainerSchema5< + ProposerPreferences, SszBytes32, SszUInt64, SszUInt64, SszByteVector, SszUInt64> { public ProposerPreferencesSchema() { super( "ProposerPreferences", + namedSchema("dependent_root", SszPrimitiveSchemas.BYTES32_SCHEMA), namedSchema("proposal_slot", SszPrimitiveSchemas.UINT64_SCHEMA), namedSchema("validator_index", SszPrimitiveSchemas.UINT64_SCHEMA), namedSchema("fee_recipient", SszByteVectorSchema.create(Bytes20.SIZE)), @@ -36,11 +40,13 @@ public ProposerPreferencesSchema() { } public ProposerPreferences create( + final Bytes32 dependentRoot, final UInt64 proposalSlot, final UInt64 validatorIndex, final Eth1Address feeRecipient, final UInt64 gasLimit) { - return new ProposerPreferences(this, proposalSlot, validatorIndex, feeRecipient, gasLimit); + return new ProposerPreferences( + this, dependentRoot, proposalSlot, validatorIndex, feeRecipient, gasLimit); } @Override diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/util/ExecutionPayloadProposalUtil.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/util/ExecutionPayloadProposalUtil.java index 6afd7cc80b7..7b72b965b08 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/util/ExecutionPayloadProposalUtil.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/util/ExecutionPayloadProposalUtil.java @@ -49,6 +49,7 @@ public SafeFuture createNewUnsignedExecutionPayload( executionPayloadProposalData.executionPayload, executionPayloadProposalData.executionRequests, builderIndex, - blockAndState.getRoot())); + blockAndState.getRoot(), + blockAndState.getBlock().getParentRoot())); } } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/execution/ExecutionPayloadVerifierGloas.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/execution/ExecutionPayloadVerifierGloas.java index 7bf2c057a91..d0c570a257a 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/execution/ExecutionPayloadVerifierGloas.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/execution/ExecutionPayloadVerifierGloas.java @@ -78,6 +78,10 @@ public void verifyExecutionPayloadEnvelope( throw new ExecutionPayloadVerificationException( "Envelope beacon block root is not consistent with the latest beacon block from the state"); } + if (!envelope.getParentBeaconBlockRoot().equals(state.getLatestBlockHeader().getParentRoot())) { + throw new ExecutionPayloadVerificationException( + "Envelope parent beacon block root is not consistent with the latest beacon block parent root from the state"); + } if (!envelope.getSlot().equals(state.getSlot())) { throw new ExecutionPayloadVerificationException( "Envelope slot is not consistent with the state slot"); @@ -130,7 +134,7 @@ public void verifyExecutionPayloadEnvelope( } if (payloadExecutor.isPresent()) { final NewPayloadRequest payloadToExecute = - computeNewPayloadRequest(state, envelope, committedBid.getBlobKzgCommitments()); + computeNewPayloadRequest(envelope, committedBid.getBlobKzgCommitments()); final boolean optimisticallyAccept = payloadExecutor.get().optimisticallyExecute(Optional.empty(), payloadToExecute); if (!optimisticallyAccept) { @@ -163,19 +167,16 @@ public boolean verifyExecutionPayloadEnvelopeSignature( } protected NewPayloadRequest computeNewPayloadRequest( - final BeaconState state, - final ExecutionPayloadEnvelope envelope, - final SszList blobKzgCommitments) { + final ExecutionPayloadEnvelope envelope, final SszList blobKzgCommitments) { final List versionedHashes = blobKzgCommitments.stream() .map(SszKZGCommitment::getKZGCommitment) .map(miscHelpers::kzgCommitmentToVersionedHash) .toList(); - final Bytes32 parentBeaconBlockRoot = state.getLatestBlockHeader().getParentRoot(); return new NewPayloadRequest( envelope.getPayload(), versionedHashes, - parentBeaconBlockRoot, + envelope.getParentBeaconBlockRoot(), executionRequestsDataCodec.encode(envelope.getExecutionRequests())); } } diff --git a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/ExecutionPayloadProposalTestUtil.java b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/ExecutionPayloadProposalTestUtil.java index 8940ac274ee..bf2206a9b24 100644 --- a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/ExecutionPayloadProposalTestUtil.java +++ b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/ExecutionPayloadProposalTestUtil.java @@ -47,7 +47,8 @@ public SafeFuture createExecutionPayload( executionPayloadProposalData.executionPayload(), executionPayloadProposalData.executionRequests(), BUILDER_INDEX_SELF_BUILD, - blockAndState.getRoot()); + blockAndState.getRoot(), + blockAndState.getBlock().getParentRoot()); // Sign execution payload and set signature return signer .signExecutionPayloadEnvelope(executionPayload, blockAndState.getState().getForkInfo()) diff --git a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java index 74f2ac1f511..d7b21f2aa17 100644 --- a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java +++ b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java @@ -3340,7 +3340,7 @@ public SignedExecutionPayloadBid randomSignedExecutionPayloadBidWithCommitments( public ProposerPreferences randomProposerPreferences() { return getGloasSchemaDefinitions() .getProposerPreferencesSchema() - .create(randomSlot(), randomUInt64(), randomEth1Address(), randomUInt64()); + .create(randomBytes32(), randomSlot(), randomUInt64(), randomEth1Address(), randomUInt64()); } public SignedProposerPreferences randomSignedProposerPreferences() { @@ -3364,7 +3364,8 @@ public ExecutionPayloadEnvelope randomExecutionPayloadEnvelopeForBlock( .getSignedExecutionPayloadBid() .getMessage() .getBuilderIndex(), - block.getRoot()); + block.getRoot(), + block.getParentRoot()); } public ExecutionPayloadEnvelope randomExecutionPayloadEnvelope(final UInt64 slot) { @@ -3374,6 +3375,7 @@ public ExecutionPayloadEnvelope randomExecutionPayloadEnvelope(final UInt64 slot randomExecutionPayload(slot), randomExecutionRequests(), randomBuilderIndex(), + randomBytes32(), randomBytes32()); } diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java index b9a40a5c57f..d709153a11d 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java @@ -839,7 +839,8 @@ public SafeFuture applyGenesisExecutionPayloadForGloas() { genesisPayload, schemaDefinitions.getExecutionRequestsSchema().getDefault(), UInt64.ZERO, - recentChainData.getBestBlockRoot().orElseThrow()), + recentChainData.getBestBlockRoot().orElseThrow(), + Bytes32.ZERO), BLSSignature.empty()); return onForkChoiceThread( diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java index dd58a473bb7..b4c54f301b1 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java @@ -225,13 +225,15 @@ public boolean isSlotInNextEpoch(final UInt64 slot) { return recentChainData .getCurrentSlot() .map( - currentSlot -> { - final int slotsPerEpoch = spec.getSlotsPerEpoch(currentSlot); - final UInt64 currentEpochStart = currentSlot.minus(currentSlot.mod(slotsPerEpoch)); - final UInt64 nextEpochStart = currentEpochStart.plus(slotsPerEpoch); - return slot.isGreaterThanOrEqualTo(nextEpochStart) - && slot.isLessThan(nextEpochStart.plus(slotsPerEpoch)); - }) + currentSlot -> + spec.computeEpochAtSlot(slot).equals(spec.computeEpochAtSlot(currentSlot).plus(1))) + .orElse(false); + } + + public boolean isSlotInCurrentEpoch(final UInt64 slot) { + return recentChainData + .getCurrentSlot() + .map(currentSlot -> spec.computeEpochAtSlot(slot).equals(spec.computeEpochAtSlot(currentSlot))) .orElse(false); } diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java index a2bc2fea82b..1fcc900a6c0 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java @@ -18,18 +18,18 @@ import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.ignore; import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.reject; -import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; import tech.pegasys.teku.infrastructure.async.SafeFuture; -import tech.pegasys.teku.infrastructure.collections.LimitedMap; +import tech.pegasys.teku.infrastructure.collections.LimitedSet; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ProposerPreferences; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedProposerPreferences; +import tech.pegasys.teku.spec.datastructures.state.Checkpoint; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.fulu.BeaconStateFulu; import tech.pegasys.teku.spec.signatures.SigningRootUtil; @@ -39,15 +39,15 @@ public class ProposerPreferencesGossipValidator { private static final Logger LOG = LogManager.getLogger(); - private static final int MAX_SLOTS_TO_TRACK = 10; + private static final int RECENT_SEEN_PROPOSER_PREFERENCES_CACHE_SIZE = 1024; private final Spec spec; private final GossipValidationHelper gossipValidationHelper; private final SigningRootUtil signingRootUtil; private final RecentChainData recentChainData; - private final Map> seenProposerPreferences = - LimitedMap.createSynchronizedLRU(MAX_SLOTS_TO_TRACK); + private final Set seenProposerPreferences = + LimitedSet.createSynchronized(RECENT_SEEN_PROPOSER_PREFERENCES_CACHE_SIZE); public ProposerPreferencesGossipValidator( final Spec spec, @@ -63,41 +63,74 @@ public SafeFuture validate( final SignedProposerPreferences signedProposerPreferences) { final ProposerPreferences proposerPreferences = signedProposerPreferences.getMessage(); final UInt64 proposalSlot = proposerPreferences.getProposalSlot(); + final Bytes32 dependentRoot = proposerPreferences.getDependentRoot(); /* - * [IGNORE] preferences.proposal_slot is in the next epoch -- i.e. - * compute_epoch_at_slot(preferences.proposal_slot) == get_current_epoch(state) + 1 + * [IGNORE] preferences.proposal_slot is in the current or next epoch */ - if (!gossipValidationHelper.isSlotInNextEpoch(proposalSlot)) { - LOG.trace("Proposer preferences proposal slot {} is not in the next epoch", proposalSlot); + if (!gossipValidationHelper.isSlotInCurrentEpoch(proposalSlot) + && !gossipValidationHelper.isSlotInNextEpoch(proposalSlot)) { + LOG.trace( + "Proposer preferences proposal slot {} is not in the current or next epoch", + proposalSlot); return completedFuture( - ignore("Proposer preferences proposal slot %s is not in the next epoch", proposalSlot)); + ignore( + "Proposer preferences proposal slot %s is not in the current or next epoch", + proposalSlot)); } /* - * [IGNORE] The signed_proposer_preferences is the first valid message received from the - * validator with index preferences.validator_index and the given slot preferences.slot + * [IGNORE] preferences.proposal_slot has not already passed */ - if (seenProposerPreferences - .getOrDefault(proposalSlot, Set.of()) - .contains(proposerPreferences.getValidatorIndex())) { - LOG.trace( - "Already received proposer preferences from validator {} for slot {}", - proposerPreferences.getValidatorIndex(), - proposalSlot); + if (!gossipValidationHelper.isSlotFromFuture(proposalSlot) + && !gossipValidationHelper.isSlotCurrent(proposalSlot)) { + LOG.trace("Proposer preferences proposal slot {} has already passed", proposalSlot); return completedFuture( - ignore( - "Already received proposer preferences from validator %s for slot %s", - proposerPreferences.getValidatorIndex(), proposalSlot)); + ignore("Proposer preferences proposal slot %s has already passed", proposalSlot)); + } + + /* + * [IGNORE] The block with root preferences.dependent_root has been seen + */ + if (!gossipValidationHelper.isBlockAvailable(dependentRoot)) { + LOG.trace("Proposer preferences dependent root {} has not been seen", dependentRoot); + return completedFuture( + ignore("Proposer preferences dependent root %s has not been seen", dependentRoot)); + } + + /* + * [IGNORE] The signed_proposer_preferences is the first valid message for the tuple + * (preferences.dependent_root, preferences.proposal_slot, preferences.validator_index) + */ + final DedupKey dedupKey = + new DedupKey(dependentRoot, proposalSlot, proposerPreferences.getValidatorIndex()); + if (seenProposerPreferences.contains(dedupKey)) { + return completedFuture(ignoreAlreadySeen(dedupKey)); } - return getState() + /* + * Look up the checkpoint state at (proposal_epoch - 1, dependent_root). The state used by + * is_valid_proposal_slot has current_epoch == proposal_epoch - 1, so the lookahead index for + * proposal_slot is always SLOTS_PER_EPOCH + (proposal_slot % SLOTS_PER_EPOCH). + */ + final UInt64 checkpointEpoch = spec.computeEpochAtSlot(proposalSlot).minusMinZero(1); + return recentChainData + .retrieveCheckpointState(new Checkpoint(checkpointEpoch, dependentRoot)) .thenApply( - state -> { + maybeState -> { + if (maybeState.isEmpty()) { + LOG.trace( + "Could not retrieve checkpoint state for ({}, {})", + checkpointEpoch, + dependentRoot); + return ignore( + "Could not retrieve checkpoint state for (%s, %s)", + checkpointEpoch, dependentRoot); + } + final BeaconState state = maybeState.get(); + /* - * [REJECT] preferences.validator_index is present at the correct slot in the - * next epoch's portion of state.proposer_lookahead -- i.e. - * is_valid_proposal_slot(state, preferences) returns True + * [REJECT] is_valid_proposal_slot(state, preferences) returns True */ final int slotsPerEpoch = spec.atSlot(proposalSlot).getConfig().getSlotsPerEpoch(); final int lookaheadIndex = slotsPerEpoch + proposalSlot.mod(slotsPerEpoch).intValue(); @@ -123,16 +156,8 @@ public SafeFuture validate( return reject("Invalid proposer preferences signature"); } - if (!seenProposerPreferences - .computeIfAbsent(proposalSlot, __ -> ConcurrentHashMap.newKeySet()) - .add(proposerPreferences.getValidatorIndex())) { - LOG.trace( - "Another proposer preferences from validator {} for slot {} already processed", - proposerPreferences.getValidatorIndex(), - proposalSlot); - return ignore( - "Another proposer preferences from validator %s for slot %s already processed", - proposerPreferences.getValidatorIndex(), proposalSlot); + if (!seenProposerPreferences.add(dedupKey)) { + return ignoreAlreadySeen(dedupKey); } return ACCEPT; @@ -151,12 +176,16 @@ private boolean isSignatureValid( state); } - private SafeFuture getState() { - return recentChainData - .getBestState() - .orElseThrow( - () -> - new IllegalStateException( - "Unable to get best state for proposer preferences processing.")); + private InternalValidationResult ignoreAlreadySeen(final DedupKey dedupKey) { + LOG.trace( + "Already received proposer preferences for tuple ({}, {}, {})", + dedupKey.dependentRoot(), + dedupKey.proposalSlot(), + dedupKey.validatorIndex()); + return ignore( + "Already received proposer preferences for tuple (%s, %s, %s)", + dedupKey.dependentRoot(), dedupKey.proposalSlot(), dedupKey.validatorIndex()); } + + private record DedupKey(Bytes32 dependentRoot, UInt64 proposalSlot, UInt64 validatorIndex) {} } diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java index afb5d99e355..e3a95b67e6c 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java @@ -23,6 +23,7 @@ import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.ACCEPT; import java.util.Optional; +import org.apache.tuweni.bytes.Bytes32; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestTemplate; import tech.pegasys.teku.infrastructure.async.SafeFuture; @@ -35,6 +36,7 @@ import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ProposerPreferencesSchema; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedProposerPreferences; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedProposerPreferencesSchema; +import tech.pegasys.teku.spec.datastructures.state.Checkpoint; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.gloas.BeaconStateGloas; import tech.pegasys.teku.spec.schemas.SchemaDefinitionsGloas; @@ -54,6 +56,7 @@ public class ProposerPreferencesGossipValidatorTest { private BeaconState state; private UInt64 proposalSlot; private UInt64 validatorIndex; + private Bytes32 dependentRoot; private SignedProposerPreferences signedProposerPreferences; @BeforeEach @@ -73,14 +76,20 @@ void setUp(final SpecContext specContext) { final UInt64 currentEpoch = spec.computeEpochAtSlot(state.getSlot()); proposalSlot = spec.computeStartSlotAtEpoch(currentEpoch.plus(1)); + dependentRoot = dataStructureUtil.randomBytes32(); - signedProposerPreferences = createSignedProposerPreferences(proposalSlot, validatorIndex); + signedProposerPreferences = + createSignedProposerPreferences(dependentRoot, proposalSlot, validatorIndex); + when(gossipValidationHelper.isSlotInCurrentEpoch(proposalSlot)).thenReturn(false); when(gossipValidationHelper.isSlotInNextEpoch(proposalSlot)).thenReturn(true); + when(gossipValidationHelper.isSlotFromFuture(proposalSlot)).thenReturn(true); + when(gossipValidationHelper.isBlockAvailable(dependentRoot)).thenReturn(true); when(gossipValidationHelper.isSignatureValidWithRespectToProposerIndex( any(), any(), any(), any())) .thenReturn(true); - when(recentChainData.getBestState()).thenReturn(Optional.of(SafeFuture.completedFuture(state))); + when(recentChainData.retrieveCheckpointState(any(Checkpoint.class))) + .thenReturn(SafeFuture.completedFuture(Optional.of(state))); } @TestTemplate @@ -90,11 +99,28 @@ void shouldAccept() { } @TestTemplate - void shouldIgnore_whenSlotNotInNextEpoch() { + void shouldIgnore_whenSlotNotInCurrentOrNextEpoch() { when(gossipValidationHelper.isSlotInNextEpoch(proposalSlot)).thenReturn(false); assertThatSafeFuture(validator.validate(signedProposerPreferences)) .isCompletedWithValueMatching(InternalValidationResult::isIgnore); - verify(recentChainData, never()).getBestState(); + verify(recentChainData, never()).retrieveCheckpointState(any(Checkpoint.class)); + } + + @TestTemplate + void shouldIgnore_whenSlotHasAlreadyPassed() { + when(gossipValidationHelper.isSlotFromFuture(proposalSlot)).thenReturn(false); + when(gossipValidationHelper.isSlotCurrent(proposalSlot)).thenReturn(false); + assertThatSafeFuture(validator.validate(signedProposerPreferences)) + .isCompletedWithValueMatching(InternalValidationResult::isIgnore); + verify(recentChainData, never()).retrieveCheckpointState(any(Checkpoint.class)); + } + + @TestTemplate + void shouldIgnore_whenDependentRootBlockNotSeen() { + when(gossipValidationHelper.isBlockAvailable(dependentRoot)).thenReturn(false); + assertThatSafeFuture(validator.validate(signedProposerPreferences)) + .isCompletedWithValueMatching(InternalValidationResult::isIgnore); + verify(recentChainData, never()).retrieveCheckpointState(any(Checkpoint.class)); } @TestTemplate @@ -104,14 +130,14 @@ void shouldIgnore_whenAlreadySeen() { assertThatSafeFuture(validator.validate(signedProposerPreferences)) .isCompletedWithValueMatching(InternalValidationResult::isIgnore); - verify(recentChainData, times(1)).getBestState(); + verify(recentChainData, times(1)).retrieveCheckpointState(any(Checkpoint.class)); } @TestTemplate void shouldReject_whenValidatorIndexDoesNotMatchLookahead() { final UInt64 wrongValidatorIndex = validatorIndex.plus(9999); final SignedProposerPreferences wrongIndexPreferences = - createSignedProposerPreferences(proposalSlot, wrongValidatorIndex); + createSignedProposerPreferences(dependentRoot, proposalSlot, wrongValidatorIndex); assertThatSafeFuture(validator.validate(wrongIndexPreferences)) .isCompletedWithValueMatching(InternalValidationResult::isReject); @@ -150,16 +176,17 @@ void shouldNotMarkAsSeenIfValidationFails() { @TestTemplate void shouldIgnore_whenDuplicateArrivesWhileValidating() { - // Create a slow state future - final SafeFuture slowStateFuture = new SafeFuture<>(); - when(recentChainData.getBestState()).thenReturn(Optional.of(slowStateFuture)); + // Create a slow checkpoint state future + final SafeFuture> slowStateFuture = new SafeFuture<>(); + when(recentChainData.retrieveCheckpointState(any(Checkpoint.class))).thenReturn(slowStateFuture); // Start first validation (will block on state) final SafeFuture firstResult = validator.validate(signedProposerPreferences); // Reset to return completed state for second validation - when(recentChainData.getBestState()).thenReturn(Optional.of(SafeFuture.completedFuture(state))); + when(recentChainData.retrieveCheckpointState(any(Checkpoint.class))) + .thenReturn(SafeFuture.completedFuture(Optional.of(state))); // Second validation completes first final SafeFuture secondResult = @@ -167,13 +194,13 @@ void shouldIgnore_whenDuplicateArrivesWhileValidating() { assertThatSafeFuture(secondResult).isCompletedWithValue(ACCEPT); // Now complete first validation - should be ignored due to race condition - slowStateFuture.complete(state); + slowStateFuture.complete(Optional.of(state)); assertThatSafeFuture(firstResult) .isCompletedWithValueMatching(InternalValidationResult::isIgnore); } private SignedProposerPreferences createSignedProposerPreferences( - final UInt64 proposalSlot, final UInt64 validatorIndex) { + final Bytes32 dependentRoot, final UInt64 proposalSlot, final UInt64 validatorIndex) { final SchemaDefinitionsGloas schemaDefinitions = SchemaDefinitionsGloas.required(spec.atSlot(proposalSlot).getSchemaDefinitions()); final ProposerPreferencesSchema proposerPreferencesSchema = @@ -183,6 +210,7 @@ private SignedProposerPreferences createSignedProposerPreferences( final ProposerPreferences proposerPreferences = proposerPreferencesSchema.create( + dependentRoot, proposalSlot, validatorIndex, dataStructureUtil.randomEth1Address(), diff --git a/specrefs/.ethspecify.yml b/specrefs/.ethspecify.yml index 1179dea7328..501d31354ec 100644 --- a/specrefs/.ethspecify.yml +++ b/specrefs/.ethspecify.yml @@ -1,4 +1,4 @@ -version: v1.7.0-alpha.6 +version: v1.7.0-alpha.7 style: full specrefs: diff --git a/specrefs/containers.yml b/specrefs/containers.yml index c67ff7a19a0..3563264dfbf 100644 --- a/specrefs/containers.yml +++ b/specrefs/containers.yml @@ -1024,12 +1024,13 @@ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ExecutionPayloadEnvelope.java - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/ExecutionPayloadEnvelopeSchema.java spec: | - + class ExecutionPayloadEnvelope(Container): payload: ExecutionPayload execution_requests: ExecutionRequests builder_index: BuilderIndex beacon_block_root: Root + parent_beacon_block_root: Root - name: ExecutionPayloadHeader#bellatrix @@ -1454,8 +1455,9 @@ - name: ProposerPreferences#gloas sources: [] spec: | - + class ProposerPreferences(Container): + dependent_root: Root proposal_slot: Slot validator_index: ValidatorIndex fee_recipient: ExecutionAddress diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/ProposerPreferencesPublisher.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/ProposerPreferencesPublisher.java index f316ee3bb89..3cd4899cab7 100644 --- a/validator/client/src/main/java/tech/pegasys/teku/validator/client/ProposerPreferencesPublisher.java +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/ProposerPreferencesPublisher.java @@ -104,13 +104,19 @@ private SafeFuture createAndSendProposerPreferences(final ProposerDuties p return SafeFuture.COMPLETE; } + // Gloas's get_proposer_dependent_root(state, e) returns the block root at + // start_of_(e-1) - 1. For next-epoch duties, BlockProposalUtilFulu's + // getBlockProposalDependentRoot returns the same value, so we reuse it here. + final Bytes32 dependentRoot = proposerDuties.getDependentRoot(); return forkProvider .getForkInfo(ourDuties.getFirst().getSlot()) .thenCompose( forkInfo -> SafeFuture.collectAll( ourDuties.stream() - .map(duty -> createSignedProposerPreferences(duty, forkInfo))) + .map( + duty -> + createSignedProposerPreferences(duty, dependentRoot, forkInfo))) .thenCompose( signedPreferences -> { final List preferencesList = @@ -130,7 +136,7 @@ private SafeFuture createAndSendProposerPreferences(final ProposerDuties p } private SafeFuture> createSignedProposerPreferences( - final ProposerDuty duty, final ForkInfo forkInfo) { + final ProposerDuty duty, final Bytes32 dependentRoot, final ForkInfo forkInfo) { final Optional maybeValidator = ownedValidators.getValidator(duty.getPublicKey()); if (maybeValidator.isEmpty()) { return SafeFuture.completedFuture(Optional.empty()); @@ -151,6 +157,7 @@ private SafeFuture> createSignedProposerPref schemaDefinitions .getProposerPreferencesSchema() .create( + dependentRoot, duty.getSlot(), UInt64.valueOf(duty.getValidatorIndex()), feeRecipient, From d89113cd76385ac47d9801d819b38cb6d8214422 Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 11:56:40 +0200 Subject: [PATCH 05/16] spotlessApply --- .../statetransition/validation/GossipValidationHelper.java | 4 +++- .../validation/ProposerPreferencesGossipValidatorTest.java | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java index b4c54f301b1..289bcbcc51e 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java @@ -233,7 +233,9 @@ public boolean isSlotInNextEpoch(final UInt64 slot) { public boolean isSlotInCurrentEpoch(final UInt64 slot) { return recentChainData .getCurrentSlot() - .map(currentSlot -> spec.computeEpochAtSlot(slot).equals(spec.computeEpochAtSlot(currentSlot))) + .map( + currentSlot -> + spec.computeEpochAtSlot(slot).equals(spec.computeEpochAtSlot(currentSlot))) .orElse(false); } diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java index e3a95b67e6c..447b9bb7ff4 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java @@ -178,7 +178,8 @@ void shouldNotMarkAsSeenIfValidationFails() { void shouldIgnore_whenDuplicateArrivesWhileValidating() { // Create a slow checkpoint state future final SafeFuture> slowStateFuture = new SafeFuture<>(); - when(recentChainData.retrieveCheckpointState(any(Checkpoint.class))).thenReturn(slowStateFuture); + when(recentChainData.retrieveCheckpointState(any(Checkpoint.class))) + .thenReturn(slowStateFuture); // Start first validation (will block on state) final SafeFuture firstResult = From 50e1074161c3cb7821a609028c777d04542fbed9 Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 12:03:16 +0200 Subject: [PATCH 06/16] use save for future where it makes sense --- .../ProposerPreferencesGossipValidator.java | 15 ++++++++------- .../ProposerPreferencesGossipValidatorTest.java | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java index 1fcc900a6c0..0909a443dc5 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java @@ -15,6 +15,7 @@ import static tech.pegasys.teku.infrastructure.async.SafeFuture.completedFuture; import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.ACCEPT; +import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.SAVE_FOR_FUTURE; import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.ignore; import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.reject; @@ -91,11 +92,13 @@ public SafeFuture validate( /* * [IGNORE] The block with root preferences.dependent_root has been seen + * (a client MAY queue preferences for processing once the block is retrieved). */ if (!gossipValidationHelper.isBlockAvailable(dependentRoot)) { - LOG.trace("Proposer preferences dependent root {} has not been seen", dependentRoot); - return completedFuture( - ignore("Proposer preferences dependent root %s has not been seen", dependentRoot)); + LOG.trace( + "Proposer preferences dependent root {} has not been seen. Saving for future processing", + dependentRoot); + return completedFuture(SAVE_FOR_FUTURE); } /* @@ -120,12 +123,10 @@ public SafeFuture validate( maybeState -> { if (maybeState.isEmpty()) { LOG.trace( - "Could not retrieve checkpoint state for ({}, {})", + "Could not retrieve checkpoint state for ({}, {}). Saving for future processing", checkpointEpoch, dependentRoot); - return ignore( - "Could not retrieve checkpoint state for (%s, %s)", - checkpointEpoch, dependentRoot); + return SAVE_FOR_FUTURE; } final BeaconState state = maybeState.get(); diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java index 447b9bb7ff4..571245c8b18 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidatorTest.java @@ -116,10 +116,10 @@ void shouldIgnore_whenSlotHasAlreadyPassed() { } @TestTemplate - void shouldIgnore_whenDependentRootBlockNotSeen() { + void shouldSaveForFuture_whenDependentRootBlockNotSeen() { when(gossipValidationHelper.isBlockAvailable(dependentRoot)).thenReturn(false); assertThatSafeFuture(validator.validate(signedProposerPreferences)) - .isCompletedWithValueMatching(InternalValidationResult::isIgnore); + .isCompletedWithValueMatching(InternalValidationResult::isSaveForFuture); verify(recentChainData, never()).retrieveCheckpointState(any(Checkpoint.class)); } From 41e4a0e918aeb37c7b6c63fa09b51fa20c8f5864 Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 12:06:33 +0200 Subject: [PATCH 07/16] Change minimal PTC_SIZE to 16 validators --- .../tech/pegasys/teku/spec/config/presets/minimal/gloas.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/presets/minimal/gloas.yaml b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/presets/minimal/gloas.yaml index 2fa08c838f1..51291759f44 100644 --- a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/presets/minimal/gloas.yaml +++ b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/presets/minimal/gloas.yaml @@ -2,8 +2,8 @@ # Misc # --------------------------------------------------------------- -# [customized] 2**1 (= 2) validators -PTC_SIZE: 2 +# [customized] 2**4 (= 16) validators +PTC_SIZE: 16 # Max operations per block # --------------------------------------------------------------- From a75bd923bcbb65410abc2fcc6997d64ba191bb79 Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 14:54:58 +0200 Subject: [PATCH 08/16] fix method name --- .../p2p/libp2p/gossip/LibP2PGossipNetworkBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/gossip/LibP2PGossipNetworkBuilder.java b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/gossip/LibP2PGossipNetworkBuilder.java index 89b766c6436..e2635215c2f 100644 --- a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/gossip/LibP2PGossipNetworkBuilder.java +++ b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/gossip/LibP2PGossipNetworkBuilder.java @@ -121,7 +121,7 @@ protected GossipRouter createGossipRouter( new TTLSeenCache<>( new FastIdSeenCache<>(msg -> Bytes.wrap(msg.messageSha256())), gossipParams.getSeenTTL(), - builder.getCurrentTimeSuppluer()); + builder.getCurrentTimeSupplier()); builder.setParams(gossipParams); builder.setScoreParams(scoreParams); From bd45bf596e9123dd8cf0949ade13ef0a06eccd09 Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 14:55:08 +0200 Subject: [PATCH 09/16] Cancel networking tests that are not supported --- .../tech/pegasys/teku/reference/phase0/gossip/GossipTests.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/gossip/GossipTests.java b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/gossip/GossipTests.java index 24b72c1e6ee..fa079190a27 100644 --- a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/gossip/GossipTests.java +++ b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/gossip/GossipTests.java @@ -26,6 +26,9 @@ public class GossipTests { // TODO: https://github.com/Consensys/teku/issues/10578 .put("networking/gossip_beacon_attestation", TestExecutor.IGNORE_TESTS) .put("networking/gossip_beacon_block", TestExecutor.IGNORE_TESTS) + .put("networking/gossip_sync_committee_contribution_and_proof", TestExecutor.IGNORE_TESTS) + .put("networking/gossip_sync_committee_message", TestExecutor.IGNORE_TESTS) + .put("networking/gossip_bls_to_execution_change", TestExecutor.IGNORE_TESTS) .put("networking/gossip_proposer_slashing", new GossipProposerSlashingTestExecutor()) .put("networking/gossip_voluntary_exit", new GossipVoluntaryExitTestExecutor()) .build(); From 30147dc84a424e184d6e20f6bc28b73b19c30e43 Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 15:43:53 +0200 Subject: [PATCH 10/16] light client structure PartialDataColumnGroupID not supported, add to ignore --- .../teku/reference/phase0/ssz_static/SszTestExecutor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/ssz_static/SszTestExecutor.java b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/ssz_static/SszTestExecutor.java index b87757cf723..07e39fb1293 100644 --- a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/ssz_static/SszTestExecutor.java +++ b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/ssz_static/SszTestExecutor.java @@ -153,6 +153,7 @@ public class SszTestExecutor implements TestExecutor { .put("ssz_static/PartialDataColumnHeader", IGNORE_TESTS) .put("ssz_static/PartialDataColumnPartsMetadata", IGNORE_TESTS) .put("ssz_static/PartialDataColumnSidecar", IGNORE_TESTS) + .put("ssz_static/PartialDataColumnGroupID", IGNORE_TESTS) // Bellatrix types .put( From c670a548619613079bf44679b05fe2cd686150fe Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 15:44:37 +0200 Subject: [PATCH 11/16] Add new test types runners --- .../common/epoch_processing/EpochProcessingTestExecutor.java | 3 +++ .../reference/common/operations/OperationsTestExecutor.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/common/epoch_processing/EpochProcessingTestExecutor.java b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/common/epoch_processing/EpochProcessingTestExecutor.java index 674a5c3beba..01be13da89f 100644 --- a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/common/epoch_processing/EpochProcessingTestExecutor.java +++ b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/common/epoch_processing/EpochProcessingTestExecutor.java @@ -84,6 +84,9 @@ public class EpochProcessingTestExecutor implements TestExecutor { .put( "epoch_processing/pending_deposits", new EpochProcessingTestExecutor(EpochOperation.PENDING_DEPOSITS)) + .put( + "epoch_processing/pending_deposits_churn", + new EpochProcessingTestExecutor(EpochOperation.PENDING_DEPOSITS)) .put( "epoch_processing/proposer_lookahead", new EpochProcessingTestExecutor(EpochOperation.PROPOSER_LOOKAHEAD)) diff --git a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/common/operations/OperationsTestExecutor.java b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/common/operations/OperationsTestExecutor.java index 88e15037067..b0fca02a600 100644 --- a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/common/operations/OperationsTestExecutor.java +++ b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/common/operations/OperationsTestExecutor.java @@ -117,6 +117,9 @@ private enum Operation { .put( "operations/voluntary_exit", new OperationsTestExecutor<>("voluntary_exit.ssz_snappy", Operation.VOLUNTARY_EXIT)) + .put( + "operations/voluntary_exit_churn", + new OperationsTestExecutor<>("voluntary_exit.ssz_snappy", Operation.VOLUNTARY_EXIT)) .put( "operations/attestation", new OperationsTestExecutor<>("attestation.ssz_snappy", Operation.ATTESTATION)) From e9a169e889f6bedab8f20611ce95e0766c1b255c Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 16:01:00 +0200 Subject: [PATCH 12/16] bump reference tests --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 00118a89702..8ef8fc3ffce 100644 --- a/build.gradle +++ b/build.gradle @@ -394,7 +394,7 @@ allprojects { } def nightly = System.getenv("NIGHTLY") != null -def refTestVersion = nightly ? "nightly" : "v1.7.0-alpha.5" +def refTestVersion = nightly ? "nightly" : "v1.7.0-alpha.7" def blsRefTestVersion = 'v0.1.2' def slashingProtectionInterchangeRefTestVersion = 'v5.3.0' def refTestBaseUrl = 'https://github.com/ethereum/consensus-specs/releases/download' From 8fa7a5ac3487402afcbdf9cc305aa77a9a494a51 Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 16:50:29 +0200 Subject: [PATCH 13/16] Fix forkchoice full node search bug --- .../protoarray/ForkChoiceModelGloas.java | 16 ++++---- .../storage/protoarray/ProtoArrayTest.java | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/storage/src/main/java/tech/pegasys/teku/storage/protoarray/ForkChoiceModelGloas.java b/storage/src/main/java/tech/pegasys/teku/storage/protoarray/ForkChoiceModelGloas.java index 01e2eeaa88f..9a6a4054b52 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/protoarray/ForkChoiceModelGloas.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/protoarray/ForkChoiceModelGloas.java @@ -358,13 +358,15 @@ && isPayloadDataAvailable(blockNodeIndex, blockRoot)) { if (!proposerNode.get().getParentRoot().equals(blockRoot)) { return true; } - return blockNodeIndex - .getFullNode(blockRoot) - .flatMap(protoArray::getNode) - .map( - fullNode -> - fullNode.getExecutionBlockHash().equals(proposerNode.get().getExecutionBlockHash())) - .orElse(false); + // Spec: is_parent_node_full(store, proposer_block) — i.e. proposer's bid.parent_block_hash + // equals this block's bid.block_hash. resolveParentNode already encoded that decision when + // the proposer block was imported by attaching it to either the FULL or the EMPTY node, so + // checking the protoarray attachment is equivalent and avoids false positives from comparing + // the proposer BASE's inherited executionBlockHash (which collides with FULL's hash whenever + // the proposer attached to EMPTY but EMPTY's inherited hash happens to match FULL's). + final Optional fullNodeIndex = + blockNodeIndex.getFullNode(blockRoot).flatMap(protoArray::getNodeIndex); + return fullNodeIndex.isPresent() && fullNodeIndex.equals(proposerNode.get().getParentIndex()); } private boolean isPayloadTimely( diff --git a/storage/src/test/java/tech/pegasys/teku/storage/protoarray/ProtoArrayTest.java b/storage/src/test/java/tech/pegasys/teku/storage/protoarray/ProtoArrayTest.java index e4b4b8d4e72..8cd02dc542c 100644 --- a/storage/src/test/java/tech/pegasys/teku/storage/protoarray/ProtoArrayTest.java +++ b/storage/src/test/java/tech/pegasys/teku/storage/protoarray/ProtoArrayTest.java @@ -952,6 +952,44 @@ void tiebreaker_fullWinsOverEmpty_whenPayloadNotTimely_butBoostOnChildWithFullPa assertThat(block1aNode.getBestChildIndex()).isEqualTo(Optional.of(fullNodeIndex)); } + @Test + void tiebreaker_emptyWins_whenBoostedChildBuiltOnEmpty_evenIfInheritedHashCollidesWithFull() { + // Regression for the on_execution_payload_envelope__valid reference test. + // + // shouldExtendPayload must mirror the spec's is_parent_node_full(store, proposer_block) by + // checking the proposer block's protoarray attachment (FULL vs EMPTY) — that attachment was + // computed by resolveParentNode at on_block time from the proposer's bid.parent_block_hash. + // Comparing FULL.executionBlockHash against the proposer BASE's executionBlockHash is unsafe + // because the BASE hash is inherited from whichever node it attached to. When the proposer + // attaches to EMPTY but EMPTY's inherited hash happens to equal FULL's hash, the + // hash-equality check incorrectly returns true and the Gloas tiebreaker picks FULL. + addValidBlock(5, block1a, GENESIS_CHECKPOINT.getRoot()); + protoArray.createEmptyNode(block1a); + protoArray.onExecutionPayload(block1a, EXECUTION_BLOCK_NUMBER, EXECUTION_BLOCK_HASH); + protoArray.markNodeValid(block1a); + + final int emptyNodeIndex = protoArray.getEmptyNodeIndices().getInt(block1a); + final int fullNodeIndex = protoArray.getFullNodeIndices().getInt(block1a); + + // block2a attaches to EMPTY (its bid did NOT extend block1a's payload), but its stored + // executionBlockHash collides with block1a's FULL.executionBlockHash — simulating the + // inherited-hash collision seen in the reference test. + addValidBlockWithParentIndex( + 6, block2a, block1a, Optional.of(emptyNodeIndex), EXECUTION_BLOCK_HASH); + + // currentSlot = blockSlot + 1 → effective weight is 0 for both EMPTY and FULL → tiebreaker + // decides. No PTC votes → not timely / not available. Proposer-boost is on block2a, which + // attached to EMPTY → is_parent_node_full(block2a) must be false → should_extend_payload + // returns false → FULL tiebreaker is 0, EMPTY tiebreaker is 1 → EMPTY wins. + applyScoreChanges(gloasModel, UInt64.valueOf(6), Optional.of(block2a)); + + final ProtoNode block1aNode = protoArray.getProtoNode(block1a).orElseThrow(); + assertThat(block1aNode.getBestChildIndex()) + .describedAs("EMPTY must win when proposer attached to EMPTY, regardless of hash collision") + .isEqualTo(Optional.of(emptyNodeIndex)) + .isNotEqualTo(Optional.of(fullNodeIndex)); + } + @Test void emptyPathWinsOverFullPath_whenEmptyHasMoreWeight_notPreviousSlot() { // Block at slot 5, currentSlot = 100 → not previous slot → effectiveWeight = node.getWeight() From 545505dc33231f145d97c9ae1efb034e5fb7aa36 Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 17:21:08 +0200 Subject: [PATCH 14/16] Fix ethspecify + Heze changes --- .../teku/spec/config/SpecConfigHeze.java | 6 +- .../teku/spec/config/SpecConfigHezeImpl.java | 36 +- .../teku/spec/config/builder/HezeBuilder.java | 32 +- .../teku/spec/config/configs/mainnet.yaml | 6 +- .../teku/spec/config/configs/minimal.yaml | 6 +- .../teku/spec/config/SpecConfigHezeTest.java | 2 - specrefs/configs.yml | 32 +- specrefs/dataclasses.yml | 49 +- specrefs/functions.yml | 795 ++++++++++++++++-- 9 files changed, 792 insertions(+), 172 deletions(-) diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigHeze.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigHeze.java index a42a2f3aff2..263c52985ea 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigHeze.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigHeze.java @@ -26,11 +26,7 @@ static SpecConfigHeze required(final SpecConfig specConfig) { "Expected Heze spec config but got: " + specConfig.getClass().getSimpleName())); } - int getViewFreezeCutoffBps(); - - int getInclusionListSubmissionDueBps(); - - int getProposerInclusionListCutoffBps(); + int getInclusionListDueBps(); int getInclusionListCommitteeSize(); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigHezeImpl.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigHezeImpl.java index be190462e45..ce026221270 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigHezeImpl.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/SpecConfigHezeImpl.java @@ -19,25 +19,19 @@ public class SpecConfigHezeImpl extends DelegatingSpecConfigGloas implements SpecConfigHeze { - private final int viewFreezeCutoffBps; - private final int inclusionListSubmissionDueBps; - private final int proposerInclusionListCutoffBps; + private final int inclusionListDueBps; private final int maxRequestInclusionList; private final int maxBytesPerInclusionList; private final int inclusionListCommitteeSize; public SpecConfigHezeImpl( final SpecConfigGloas specConfig, - final int viewFreezeCutoffBps, - final int inclusionListSubmissionDueBps, - final int proposerInclusionListCutoffBps, + final int inclusionListDueBps, final int maxRequestInclusionList, final int maxBytesPerInclusionList, final int inclusionListCommitteeSize) { super(specConfig); - this.viewFreezeCutoffBps = viewFreezeCutoffBps; - this.inclusionListSubmissionDueBps = inclusionListSubmissionDueBps; - this.proposerInclusionListCutoffBps = proposerInclusionListCutoffBps; + this.inclusionListDueBps = inclusionListDueBps; this.maxRequestInclusionList = maxRequestInclusionList; this.maxBytesPerInclusionList = maxBytesPerInclusionList; this.inclusionListCommitteeSize = inclusionListCommitteeSize; @@ -49,18 +43,8 @@ public SpecMilestone getMilestone() { } @Override - public int getViewFreezeCutoffBps() { - return viewFreezeCutoffBps; - } - - @Override - public int getInclusionListSubmissionDueBps() { - return inclusionListSubmissionDueBps; - } - - @Override - public int getProposerInclusionListCutoffBps() { - return proposerInclusionListCutoffBps; + public int getInclusionListDueBps() { + return inclusionListDueBps; } @Override @@ -92,9 +76,7 @@ public boolean equals(final Object o) { return false; } SpecConfigHezeImpl that = (SpecConfigHezeImpl) o; - return viewFreezeCutoffBps == that.viewFreezeCutoffBps - && inclusionListSubmissionDueBps == that.inclusionListSubmissionDueBps - && proposerInclusionListCutoffBps == that.proposerInclusionListCutoffBps + return inclusionListDueBps == that.inclusionListDueBps && maxRequestInclusionList == that.maxRequestInclusionList && maxBytesPerInclusionList == that.maxBytesPerInclusionList && inclusionListCommitteeSize == that.inclusionListCommitteeSize; @@ -104,11 +86,9 @@ public boolean equals(final Object o) { public int hashCode() { return Objects.hash( super.hashCode(), - viewFreezeCutoffBps, - inclusionListSubmissionDueBps, - proposerInclusionListCutoffBps, + inclusionListDueBps, maxRequestInclusionList, maxBytesPerInclusionList, inclusionListCommitteeSize); } -} +} \ No newline at end of file diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/HezeBuilder.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/HezeBuilder.java index 585390d3bab..ffbc28da294 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/HezeBuilder.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/builder/HezeBuilder.java @@ -26,9 +26,7 @@ public class HezeBuilder extends BaseForkBuilder implements ForkConfigBuilder { - private Integer viewFreezeCutoffBps; - private Integer inclusionListSubmissionDueBps; - private Integer proposerInclusionListCutoffBps; + private Integer inclusionListDueBps; private Integer maxRequestInclusionList; private Integer maxBytesPerInclusionList; @@ -43,30 +41,16 @@ public SpecConfigAndParent build( return SpecConfigAndParent.of( new SpecConfigHezeImpl( specConfigAndParent.specConfig(), - viewFreezeCutoffBps, - inclusionListSubmissionDueBps, - proposerInclusionListCutoffBps, + inclusionListDueBps, maxRequestInclusionList, maxBytesPerInclusionList, inclusionListCommitteeSize), specConfigAndParent); } - public HezeBuilder viewFreezeCutoffBps(final Integer viewFreezeCutoffBps) { - checkNotNull(viewFreezeCutoffBps); - this.viewFreezeCutoffBps = viewFreezeCutoffBps; - return this; - } - - public HezeBuilder inclusionListSubmissionDueBps(final Integer inclusionListSubmissionDueBps) { - checkNotNull(inclusionListSubmissionDueBps); - this.inclusionListSubmissionDueBps = inclusionListSubmissionDueBps; - return this; - } - - public HezeBuilder proposerInclusionListCutoffBps(final Integer proposerInclusionListCutoffBps) { - checkNotNull(proposerInclusionListCutoffBps); - this.proposerInclusionListCutoffBps = proposerInclusionListCutoffBps; + public HezeBuilder inclusionListDueBps(final Integer inclusionListDueBps) { + checkNotNull(inclusionListDueBps); + this.inclusionListDueBps = inclusionListDueBps; return this; } @@ -97,9 +81,7 @@ public void validate() { @Override public Map getValidationMap() { final Map constants = new HashMap<>(); - constants.put("viewFreezeCutoffBps", viewFreezeCutoffBps); - constants.put("inclusionListSubmissionDueBps", inclusionListSubmissionDueBps); - constants.put("proposerInclusionListCutoffBps", proposerInclusionListCutoffBps); + constants.put("inclusionListDueBps", inclusionListDueBps); constants.put("maxRequestInclusionList", maxRequestInclusionList); constants.put("maxBytesPerInclusionList", maxBytesPerInclusionList); constants.put("inclusionListCommitteeSize", inclusionListCommitteeSize); @@ -108,4 +90,4 @@ public Map getValidationMap() { @Override public void addOverridableItemsToRawConfig(final BiConsumer rawConfig) {} -} +} \ No newline at end of file diff --git a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml index ee511d46137..edf4edc2010 100644 --- a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml +++ b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml @@ -109,12 +109,8 @@ CONSOLIDATION_CHURN_LIMIT_QUOTIENT: 65536 MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS: 256000000000 # Heze -# 7500 basis points, 75% of SLOT_DURATION_MS -VIEW_FREEZE_CUTOFF_BPS: 7500 # 6667 basis points, ~67% of SLOT_DURATION_MS -INCLUSION_LIST_SUBMISSION_DUE_BPS: 6667 -# 9167 basis points, ~92% of SLOT_DURATION_MS -PROPOSER_INCLUSION_LIST_CUTOFF_BPS: 9167 +INCLUSION_LIST_DUE_BPS: 6667 # Validator cycle # --------------------------------------------------------------- diff --git a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/minimal.yaml b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/minimal.yaml index f13cc17da90..0f050efaf06 100644 --- a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/minimal.yaml +++ b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/minimal.yaml @@ -105,12 +105,8 @@ CONSOLIDATION_CHURN_LIMIT_QUOTIENT: 32 MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS: 128000000000 # Heze -# 7500 basis points, 75% of SLOT_DURATION_MS -VIEW_FREEZE_CUTOFF_BPS: 7500 # 6667 basis points, ~67% of SLOT_DURATION_MS -INCLUSION_LIST_SUBMISSION_DUE_BPS: 6667 -# 9167 basis points, ~92% of SLOT_DURATION_MS -PROPOSER_INCLUSION_LIST_CUTOFF_BPS: 9167 +INCLUSION_LIST_DUE_BPS: 6667 # Validator cycle # --------------------------------------------------------------- diff --git a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/config/SpecConfigHezeTest.java b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/config/SpecConfigHezeTest.java index fb3d92a0db3..36dc241edfe 100644 --- a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/config/SpecConfigHezeTest.java +++ b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/config/SpecConfigHezeTest.java @@ -84,8 +84,6 @@ private SpecConfigHeze createRandomHezeConfig(final SpecConfigGloas gloasConfig, dataStructureUtil.randomPositiveInt(), dataStructureUtil.randomPositiveInt(), dataStructureUtil.randomPositiveInt(), - dataStructureUtil.randomPositiveInt(), - dataStructureUtil.randomPositiveInt(), dataStructureUtil.randomPositiveInt()) {}; } } diff --git a/specrefs/configs.yml b/specrefs/configs.yml index f7b3f02a1f0..bab78620b21 100644 --- a/specrefs/configs.yml +++ b/specrefs/configs.yml @@ -176,7 +176,7 @@ - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml search: "CHURN_LIMIT_QUOTIENT_GLOAS:" spec: | - + CHURN_LIMIT_QUOTIENT_GLOAS: uint64 = 32768 @@ -192,7 +192,7 @@ - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml search: "CONSOLIDATION_CHURN_LIMIT_QUOTIENT:" spec: | - + CONSOLIDATION_CHURN_LIMIT_QUOTIENT: uint64 = 65536 @@ -386,13 +386,13 @@ INACTIVITY_SCORE_RECOVERY_RATE: uint64 = 16 -- name: INCLUSION_LIST_SUBMISSION_DUE_BPS#heze +- name: INCLUSION_LIST_DUE_BPS#heze sources: - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml - search: "INCLUSION_LIST_SUBMISSION_DUE_BPS:" + search: "INCLUSION_LIST_DUE_BPS:" spec: | - - INCLUSION_LIST_SUBMISSION_DUE_BPS: uint64 = 6667 + + INCLUSION_LIST_DUE_BPS: uint64 = 6667 - name: MAXIMUM_GOSSIP_CLOCK_DISPARITY#phase0 @@ -455,7 +455,7 @@ - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml search: "MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS:" spec: | - + MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS: Gwei = 256000000000 @@ -612,15 +612,6 @@ PAYLOAD_ATTESTATION_DUE_BPS: uint64 = 7500 -- name: PROPOSER_INCLUSION_LIST_CUTOFF_BPS#heze - sources: - - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml - search: "PROPOSER_INCLUSION_LIST_CUTOFF_BPS:" - spec: | - - PROPOSER_INCLUSION_LIST_CUTOFF_BPS: uint64 = 9167 - - - name: PROPOSER_REORG_CUTOFF_BPS#phase0 sources: - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml @@ -764,12 +755,3 @@ VALIDATOR_CUSTODY_REQUIREMENT = 8 - -- name: VIEW_FREEZE_CUTOFF_BPS#heze - sources: - - file: ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml - search: "VIEW_FREEZE_CUTOFF_BPS:" - spec: | - - VIEW_FREEZE_CUTOFF_BPS: uint64 = 7500 - diff --git a/specrefs/dataclasses.yml b/specrefs/dataclasses.yml index 2c5fdfb270f..1926d71c2aa 100644 --- a/specrefs/dataclasses.yml +++ b/specrefs/dataclasses.yml @@ -300,6 +300,45 @@ slot_number: uint64 +- name: Seen#altair + sources: [] + spec: | + + class Seen(object): + proposer_slots: Set[Tuple[ValidatorIndex, Slot]] + aggregator_epochs: Set[Tuple[ValidatorIndex, Epoch]] + aggregate_data_roots: Dict[Root, Set[Tuple[boolean, ...]]] + voluntary_exit_indices: Set[ValidatorIndex] + proposer_slashing_indices: Set[ValidatorIndex] + attester_slashing_indices: Set[ValidatorIndex] + attestation_validator_epochs: Set[Tuple[ValidatorIndex, Epoch]] + # [New in Altair] + sync_contribution_aggregator_slots: Set[Tuple[ValidatorIndex, Slot, uint64]] + # [New in Altair] + sync_contribution_data: Dict[Tuple[Slot, Root, uint64], Set[Tuple[boolean, ...]]] + # [New in Altair] + sync_message_validator_slots: Set[Tuple[Slot, ValidatorIndex, uint64]] + + +- name: Seen#capella + sources: [] + spec: | + + class Seen(object): + proposer_slots: Set[Tuple[ValidatorIndex, Slot]] + aggregator_epochs: Set[Tuple[ValidatorIndex, Epoch]] + aggregate_data_roots: Dict[Root, Set[Tuple[boolean, ...]]] + voluntary_exit_indices: Set[ValidatorIndex] + proposer_slashing_indices: Set[ValidatorIndex] + attester_slashing_indices: Set[ValidatorIndex] + attestation_validator_epochs: Set[Tuple[ValidatorIndex, Epoch]] + sync_contribution_aggregator_slots: Set[Tuple[ValidatorIndex, Slot, uint64]] + sync_contribution_data: Dict[Tuple[Slot, Root, uint64], Set[Tuple[boolean, ...]]] + sync_message_validator_slots: Set[Tuple[Slot, ValidatorIndex, uint64]] + # [New in Capella] + bls_to_execution_change_indices: Set[ValidatorIndex] + + - name: Store#phase0 sources: [] spec: | @@ -324,10 +363,10 @@ - name: Store#gloas sources: [] spec: | - + --- phase0 +++ gloas - @@ -9,7 +9,14 @@ + @@ -9,7 +9,16 @@ equivocating_indices: Set[ValidatorIndex] blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) block_states: Dict[Root, BeaconState] = field(default_factory=dict) @@ -339,8 +378,10 @@ latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict) unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict) + payloads: Dict[Root, ExecutionPayloadEnvelope] = field(default_factory=dict) - + payload_timeliness_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field(default_factory=dict) - + payload_data_availability_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field( + + payload_timeliness_vote: Dict[Root, Vector[Optional[boolean], PTC_SIZE]] = field( + + default_factory=dict + + ) + + payload_data_availability_vote: Dict[Root, Vector[Optional[boolean], PTC_SIZE]] = field( + default_factory=dict + ) diff --git a/specrefs/functions.yml b/specrefs/functions.yml index 242ed59e500..84056d92e75 100644 --- a/specrefs/functions.yml +++ b/specrefs/functions.yml @@ -243,12 +243,12 @@ - name: apply_parent_execution_payload#gloas sources: [] spec: | - + def apply_parent_execution_payload( state: BeaconState, - parent_bid: ExecutionPayloadBid, requests: ExecutionRequests, ) -> None: + parent_bid = state.latest_execution_payload_bid parent_slot = parent_bid.slot parent_epoch = compute_epoch_at_slot(parent_slot) @@ -270,6 +270,8 @@ payment_index = parent_slot % SLOTS_PER_EPOCH settle_builder_payment(state, payment_index) elif parent_bid.value > 0: + # Parent is older than the previous epoch, its payment entry has been + # evicted from builder_pending_payments. Append the withdrawal directly. state.builder_pending_withdrawals.append( BuilderPendingWithdrawal( fee_recipient=parent_bid.fee_recipient, @@ -946,14 +948,12 @@ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/BeaconStateMutatorsGloas.java search: public UInt64 computeExitEpochAndUpdateChurn( spec: | - + def compute_exit_epoch_and_update_churn(state: BeaconState, exit_balance: Gwei) -> Epoch: earliest_exit_epoch = max( state.earliest_exit_epoch, compute_activation_exit_epoch(get_current_epoch(state)) ) # [Modified in Gloas:EIP8061] - # Exits use the uncapped per-epoch exit churn limit instead of the - # combined activation/exit churn shared in Electra. per_epoch_churn = get_exit_churn_limit(state) # New epoch for exits. if state.earliest_exit_epoch < earliest_exit_epoch: @@ -1020,7 +1020,7 @@ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/fulu/helpers/MiscHelpersFulu.java search: public Bytes4 computeForkDigest( spec: | - + def compute_fork_digest( genesis_validators_root: Root, epoch: Epoch, @@ -1034,6 +1034,10 @@ fork_version = compute_fork_version(epoch) base_digest = compute_fork_data_root(fork_version, genesis_validators_root) + # [New in Fulu:EIP7892] + if epoch < FULU_FORK_EPOCH: + return ForkDigest(base_digest[:4]) + # [Modified in Fulu:EIP7892] # Bitmask digest with hash of blob parameters blob_parameters = get_blob_parameters(epoch) @@ -1432,6 +1436,25 @@ return uint64(MAX_REQUEST_BLOCKS_DENEB * NUMBER_OF_COLUMNS) +- name: compute_merkle_branch_root#phase0 + sources: [] + spec: | + + def compute_merkle_branch_root( + leaf: Bytes32, branch: Sequence[Bytes32], depth: uint64, index: uint64 + ) -> Root: + """ + Return the Merkle root obtained by hashing ``leaf`` at ``index`` with ``branch``. + """ + value = leaf + for i in range(depth): + if index // (2**i) % 2: + value = hash(branch[i] + value) + else: + value = hash(value + branch[i]) + return Root(value) + + - name: compute_merkle_proof#altair sources: [] spec: | @@ -2071,6 +2094,29 @@ + return MIN_VALIDATOR_WITHDRAWABILITY_DELAY + epochs_for_validator_set_churn +- name: compute_weak_subjectivity_period#gloas + sources: [] + spec: | + + def compute_weak_subjectivity_period(state: BeaconState) -> uint64: + """ + Returns the weak subjectivity period for the current ``state``. + This computation takes into account the effect of: + - exit churn (weighted 2/3) + - activation churn (weighted 1/3) + - consolidation churn (weighted 1) + """ + t = get_total_active_balance(state) + # [Modified in Gloas:EIP8061] + delta = ( + 2 * get_exit_churn_limit(state) // 3 + + get_activation_churn_limit(state) // 3 + + get_consolidation_churn_limit(state) + ) + epochs_for_validator_set_churn = SAFETY_DECAY * t // (2 * delta * 100) + return MIN_VALIDATOR_WITHDRAWABILITY_DELAY + epochs_for_validator_set_churn + + - name: construct_vanishing_polynomial#fulu sources: [] spec: | @@ -2754,12 +2800,18 @@ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/BeaconStateAccessorsGloas.java search: public UInt64 getActivationChurnLimit( spec: | - + def get_activation_churn_limit(state: BeaconState) -> Gwei: """ - Return the churn limit for the current epoch dedicated to activations. + Per-epoch churn limit for activations, rounded to + ``EFFECTIVE_BALANCE_INCREMENT``. """ - return min(MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS, get_balance_churn_limit(state)) + churn = max( + MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA, + get_total_active_balance(state) // CHURN_LIMIT_QUOTIENT_GLOAS, + ) + churn = churn - churn % EFFECTIVE_BALANCE_INCREMENT + return min(MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS, churn) - name: get_activation_exit_churn_limit#electra @@ -3775,15 +3827,14 @@ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/BeaconStateAccessorsGloas.java search: public UInt64 getConsolidationChurnLimit( spec: | - + def get_consolidation_churn_limit(state: BeaconState) -> Gwei: - # [Modified in Gloas:EIP8061] - # Consolidation churn is now derived independently from the - # activation/exit churn via CONSOLIDATION_CHURN_LIMIT_QUOTIENT. - churn = max( - MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA, - get_total_active_balance(state) // CONSOLIDATION_CHURN_LIMIT_QUOTIENT, - ) + """ + Per-epoch churn limit reserved for consolidations (EIP-7521). + Derived from total active balance and rounded to + ``EFFECTIVE_BALANCE_INCREMENT``. + """ + churn = get_total_active_balance(state) // CONSOLIDATION_CHURN_LIMIT_QUOTIENT return churn - churn % EFFECTIVE_BALANCE_INCREMENT @@ -4433,12 +4484,17 @@ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/BeaconStateAccessorsGloas.java search: public UInt64 getExitChurnLimit( spec: | - + def get_exit_churn_limit(state: BeaconState) -> Gwei: """ - Return the uncapped churn limit for the current epoch dedicated to exits. + Per-epoch churn limit for exits, rounded to + ``EFFECTIVE_BALANCE_INCREMENT``. """ - return get_balance_churn_limit(state) + churn = max( + MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA, + get_total_active_balance(state) // CHURN_LIMIT_QUOTIENT_GLOAS, + ) + return churn - churn % EFFECTIVE_BALANCE_INCREMENT - name: get_expected_withdrawals#capella @@ -4645,7 +4701,7 @@ - name: get_forkchoice_store#gloas sources: [] spec: | - + def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) -> Store: assert anchor_block.state_root == hash_tree_root(anchor_state) anchor_root = hash_tree_root(anchor_block) @@ -4671,13 +4727,9 @@ # [New in Gloas:EIP7732] payloads={}, # [New in Gloas:EIP7732] - payload_timeliness_vote={ - anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE)) - }, + payload_timeliness_vote={}, # [New in Gloas:EIP7732] - payload_data_availability_vote={ - anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE)) - }, + payload_data_availability_vote={}, ) @@ -4857,6 +4909,14 @@ return rewards, penalties +- name: get_inclusion_list_due_ms#heze + sources: [] + spec: | + + def get_inclusion_list_due_ms() -> uint64: + return get_slot_component_duration_ms(INCLUSION_LIST_DUE_BPS) + + - name: get_index_for_new_builder#gloas sources: - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/BeaconStateAccessorsGloas.java @@ -5275,16 +5335,11 @@ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/util/ForkChoiceUtilGloas.java search: SafeFuture getParentPayloadStatus( spec: | - + def get_parent_payload_status(store: Store, block: BeaconBlock) -> PayloadStatus: parent = store.blocks[block.parent_root] parent_block_hash = block.body.signed_execution_payload_bid.message.parent_block_hash message_block_hash = parent.body.signed_execution_payload_bid.message.block_hash - - # Check for uninitialized genesis block hash - if message_block_hash == Hash32(): - return PAYLOAD_STATUS_EMPTY - return PAYLOAD_STATUS_FULL if parent_block_hash == message_block_hash else PAYLOAD_STATUS_EMPTY @@ -5456,6 +5511,17 @@ return GENESIS_EPOCH if current_epoch == GENESIS_EPOCH else Epoch(current_epoch - 1) +- name: get_proposer_dependent_root#gloas + sources: [] + spec: | + + def get_proposer_dependent_root(state: BeaconState, epoch: Epoch) -> Root: + """ + Return the dependent root for the proposer lookahead at ``epoch``. + """ + return get_block_root_at_slot(state, Slot(compute_start_slot_at_epoch(Epoch(epoch - 1)) - 1)) + + - name: get_proposer_head#phase0 sources: - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/util/ForkChoiceUtil.java @@ -6801,6 +6867,22 @@ ) +- name: is_current_slot#altair + sources: [] + spec: | + + def is_current_slot( + state: BeaconState, + slot: Slot, + current_time_ms: uint64, + ) -> bool: + """ + Check if the given slot is the current slot + (with MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance). + """ + return is_within_slot_range(state, slot, 0, current_time_ms) + + - name: is_data_available#deneb sources: - file: ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/BlobSidecarsAvailabilityChecker.java @@ -7126,6 +7208,29 @@ return store.next_sync_committee != SyncCommittee() +- name: is_non_strict_superset#phase0 + sources: [] + spec: | + + def is_non_strict_superset( + seen_bits_set: Set[Tuple[boolean, ...]], + new_bits: Tuple[boolean, ...], + ) -> bool: + """ + Return True if any prior bitset in ``seen_bits_set`` is a non-strict + superset of ``new_bits`` (every bit set in new is also set in that prior). + """ + for prior_bits in seen_bits_set: + is_superset = True + for prior_bit, new_bit in zip(prior_bits, new_bits): + if new_bit and not prior_bit: + is_superset = False + break + if is_superset: + return True + return False + + - name: is_one_confirmed#phase0 sources: [] spec: | @@ -7248,7 +7353,7 @@ - name: is_payload_timely#gloas sources: [] spec: | - + def is_payload_timely(store: Store, root: Root) -> bool: """ Return whether the execution payload for the beacon block with root ``root`` @@ -7262,7 +7367,8 @@ if not is_payload_verified(store, root): return False - return sum(store.payload_timeliness_vote[root]) > PAYLOAD_TIMELY_THRESHOLD + votes = store.payload_timeliness_vote[root] + return sum(vote is True for vote in votes) > PAYLOAD_TIMELY_THRESHOLD - name: is_payload_verified#gloas @@ -7600,20 +7706,16 @@ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/helpers/Predicates.java search: public boolean isValidMerkleBranch( spec: | - + def is_valid_merkle_branch( leaf: Bytes32, branch: Sequence[Bytes32], depth: uint64, index: uint64, root: Root ) -> bool: """ Check if ``leaf`` at ``index`` verifies against the Merkle ``root`` and ``branch``. """ - value = leaf - for i in range(depth): - if index // (2**i) % 2: - value = hash(branch[i] + value) - else: - value = hash(value + branch[i]) - return value == root + if depth != len(branch): + return False + return compute_merkle_branch_root(leaf, branch, depth, index) == root - name: is_valid_normalized_merkle_branch#altair @@ -8162,7 +8264,7 @@ - name: on_block#gloas sources: [] spec: | - + def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: """ Run ``on_block`` upon receiving a new block. @@ -8203,8 +8305,8 @@ # Add new state for this block to the store store.block_states[block_root] = state # Add a new PTC voting for this block to the store - store.payload_timeliness_vote[block_root] = [False] * PTC_SIZE - store.payload_data_availability_vote[block_root] = [False] * PTC_SIZE + store.payload_timeliness_vote[block_root] = [None] * PTC_SIZE + store.payload_data_availability_vote[block_root] = [None] * PTC_SIZE # Notify the store about the payload_attestations in the block notify_ptc_messages(store, state, block.body.payload_attestations) @@ -8501,7 +8603,7 @@ - name: prepare_execution_payload#gloas sources: [] spec: | - + def prepare_execution_payload( # [New in Gloas:EIP7732] store: Store, @@ -8519,7 +8621,7 @@ # Make a copy of the state to avoid mutability issues state = copy(state) # Apply parent payload before computing withdrawals - apply_parent_execution_payload(state, parent_bid, envelope.execution_requests) + apply_parent_execution_payload(state, envelope.execution_requests) withdrawals = get_expected_withdrawals(state).withdrawals head_block_hash = parent_bid.block_hash else: @@ -9470,7 +9572,7 @@ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/common/statetransition/epoch/AbstractEpochProcessor.java search: public BeaconState processEpoch( spec: | - + def process_epoch(state: BeaconState) -> None: process_justification_and_finalization(state) process_inactivity_updates(state) @@ -9478,6 +9580,7 @@ process_registry_updates(state) process_slashings(state) process_eth1_data_reset(state) + # [Modified in Gloas:EIP8061] process_pending_deposits(state) process_pending_consolidations(state) # [New in Gloas:EIP7732] @@ -10144,22 +10247,20 @@ - name: process_parent_execution_payload#gloas sources: [] spec: | - + def process_parent_execution_payload(state: BeaconState, block: BeaconBlock) -> None: bid = block.body.signed_execution_payload_bid.message parent_bid = state.latest_execution_payload_bid requests = block.body.parent_execution_requests - is_genesis_block = parent_bid.block_hash == Hash32() - is_parent_block_empty = bid.parent_block_hash != parent_bid.block_hash - if is_genesis_block or is_parent_block_empty: + if bid.parent_block_hash != parent_bid.block_hash: # Parent was EMPTY -- no execution requests expected assert requests == ExecutionRequests() return # Parent was FULL -- verify the bid commitment and apply the payload assert hash_tree_root(requests) == parent_bid.execution_requests_root - apply_parent_execution_payload(state, parent_bid, requests) + apply_parent_execution_payload(state, requests) - name: process_participation_flag_updates#altair @@ -10315,7 +10416,7 @@ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/statetransition/epoch/EpochProcessorElectra.java search: public void processPendingDeposits( spec: | - + def process_pending_deposits(state: BeaconState) -> None: next_epoch = Epoch(get_current_epoch(state) + 1) # [Modified in Gloas:EIP8061] @@ -11123,7 +11224,7 @@ - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/withdrawals/WithdrawalsHelpersGloas.java search: public void processWithdrawals( spec: | - + def process_withdrawals( state: BeaconState, # [Modified in Gloas:EIP7732] @@ -11131,9 +11232,7 @@ ) -> None: # [New in Gloas:EIP7732] # Return early if the parent block is empty - is_genesis_block = state.latest_block_hash == Hash32() - is_parent_block_empty = state.latest_block_hash != state.latest_execution_payload_bid.block_hash - if is_genesis_block or is_parent_block_empty: + if state.latest_block_hash != state.latest_execution_payload_bid.block_hash: return # Get expected withdrawals @@ -12035,6 +12134,18 @@ ) +- name: upgrade_lc_bootstrap_to_gloas#gloas + sources: [] + spec: | + + def upgrade_lc_bootstrap_to_gloas(pre: electra.LightClientBootstrap) -> LightClientBootstrap: + return LightClientBootstrap( + header=upgrade_lc_header_to_gloas(pre.header), + current_sync_committee=pre.current_sync_committee, + current_sync_committee_branch=pre.current_sync_committee_branch, + ) + + - name: upgrade_lc_finality_update_to_capella#capella sources: [] spec: | @@ -12083,6 +12194,22 @@ ) +- name: upgrade_lc_finality_update_to_gloas#gloas + sources: [] + spec: | + + def upgrade_lc_finality_update_to_gloas( + pre: electra.LightClientFinalityUpdate, + ) -> LightClientFinalityUpdate: + return LightClientFinalityUpdate( + attested_header=upgrade_lc_header_to_gloas(pre.attested_header), + finalized_header=upgrade_lc_header_to_gloas(pre.finalized_header), + finality_branch=pre.finality_branch, + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) + + - name: upgrade_lc_header_to_capella#capella sources: [] spec: | @@ -12139,6 +12266,64 @@ ) +- name: upgrade_lc_header_to_gloas#gloas + sources: [] + spec: | + + def upgrade_lc_header_to_gloas(pre: electra.LightClientHeader) -> LightClientHeader: + if pre == electra.LightClientHeader(): + return LightClientHeader() + + epoch = compute_epoch_at_slot(pre.beacon.slot) + + if epoch >= DENEB_FORK_EPOCH: + BLOCK_HASH_GINDEX = get_generalized_index(deneb.ExecutionPayloadHeader, "block_hash") + return LightClientHeader( + beacon=pre.beacon, + execution_block_hash=pre.execution.block_hash, + execution_branch=ExecutionBranch( + normalize_merkle_branch( + list(compute_merkle_proof(pre.execution, BLOCK_HASH_GINDEX)) + + list(pre.execution_branch), + EXECUTION_BLOCK_HASH_GINDEX_GLOAS, + ) + ), + ) + + if epoch >= CAPELLA_FORK_EPOCH: + execution_header = capella.ExecutionPayloadHeader( + parent_hash=pre.execution.parent_hash, + fee_recipient=pre.execution.fee_recipient, + state_root=pre.execution.state_root, + receipts_root=pre.execution.receipts_root, + logs_bloom=pre.execution.logs_bloom, + prev_randao=pre.execution.prev_randao, + block_number=pre.execution.block_number, + gas_limit=pre.execution.gas_limit, + gas_used=pre.execution.gas_used, + timestamp=pre.execution.timestamp, + extra_data=pre.execution.extra_data, + base_fee_per_gas=pre.execution.base_fee_per_gas, + block_hash=pre.execution.block_hash, + transactions_root=pre.execution.transactions_root, + withdrawals_root=pre.execution.withdrawals_root, + ) + BLOCK_HASH_GINDEX = get_generalized_index(capella.ExecutionPayloadHeader, "block_hash") + return LightClientHeader( + beacon=pre.beacon, + execution_block_hash=pre.execution.block_hash, + execution_branch=ExecutionBranch( + normalize_merkle_branch( + list(compute_merkle_proof(execution_header, BLOCK_HASH_GINDEX)) + + list(pre.execution_branch), + EXECUTION_BLOCK_HASH_GINDEX_GLOAS, + ) + ), + ) + + return LightClientHeader(beacon=pre.beacon) + + - name: upgrade_lc_optimistic_update_to_capella#capella sources: [] spec: | @@ -12181,6 +12366,20 @@ ) +- name: upgrade_lc_optimistic_update_to_gloas#gloas + sources: [] + spec: | + + def upgrade_lc_optimistic_update_to_gloas( + pre: electra.LightClientOptimisticUpdate, + ) -> LightClientOptimisticUpdate: + return LightClientOptimisticUpdate( + attested_header=upgrade_lc_header_to_gloas(pre.attested_header), + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) + + - name: upgrade_lc_store_to_capella#capella sources: [] spec: | @@ -12241,6 +12440,26 @@ ) +- name: upgrade_lc_store_to_gloas#gloas + sources: [] + spec: | + + def upgrade_lc_store_to_gloas(pre: electra.LightClientStore) -> LightClientStore: + if pre.best_valid_update is None: + best_valid_update = None + else: + best_valid_update = upgrade_lc_update_to_gloas(pre.best_valid_update) + return LightClientStore( + finalized_header=upgrade_lc_header_to_gloas(pre.finalized_header), + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + best_valid_update=best_valid_update, + optimistic_header=upgrade_lc_header_to_gloas(pre.optimistic_header), + previous_max_active_participants=pre.previous_max_active_participants, + current_max_active_participants=pre.current_max_active_participants, + ) + + - name: upgrade_lc_update_to_capella#capella sources: [] spec: | @@ -12291,6 +12510,22 @@ ) +- name: upgrade_lc_update_to_gloas#gloas + sources: [] + spec: | + + def upgrade_lc_update_to_gloas(pre: electra.LightClientUpdate) -> LightClientUpdate: + return LightClientUpdate( + attested_header=upgrade_lc_header_to_gloas(pre.attested_header), + next_sync_committee=pre.next_sync_committee, + next_sync_committee_branch=pre.next_sync_committee_branch, + finalized_header=upgrade_lc_header_to_gloas(pre.finalized_header), + finality_branch=pre.finality_branch, + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) + + - name: upgrade_to_altair#altair sources: - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/altair/forktransition/AltairStateUpgrade.java @@ -12839,7 +13074,7 @@ - file: ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/AggregateAttestationValidator.java search: validate(final ValidatableAttestation attestation) spec: | - + def validate_beacon_aggregate_and_proof_gossip( seen: Seen, store: Store, @@ -12885,15 +13120,9 @@ # [IGNORE] A valid aggregate with a superset of aggregation bits has not already been seen aggregate_data_root = hash_tree_root(aggregate.data) aggregate_bits = tuple(bool(bit) for bit in aggregation_bits) - seen_aggregation_bits = seen.aggregate_data_roots.get(aggregate_data_root, set()) - for prior_aggregation_bits in seen_aggregation_bits: - is_non_strict_superset = True - for prior_bit, aggregate_bit in zip(prior_aggregation_bits, aggregate_bits): - if aggregate_bit and not prior_bit: - is_non_strict_superset = False - break - if is_non_strict_superset: - raise GossipIgnore("already seen aggregate for this data") + seen_bits = seen.aggregate_data_roots.get(aggregate_data_root, set()) + if is_non_strict_superset(seen_bits, aggregate_bits): + raise GossipIgnore("already seen aggregate for this data") # [IGNORE] This is the first valid aggregate for this aggregator in this epoch aggregator_index = aggregate_and_proof.aggregator_index @@ -13053,7 +13282,7 @@ - file: ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/BlockGossipValidator.java search: public SafeFuture validate( spec: | - + def validate_beacon_block_gossip( seen: Seen, store: Store, @@ -13101,7 +13330,203 @@ # [REJECT] The block's parent passes validation if block.parent_root not in store.block_states: - raise GossipReject("block's parent failed validation") + raise GossipReject("block's parent is invalid") + + # [REJECT] The block is from a higher slot than its parent + if block.slot <= store.blocks[block.parent_root].slot: + raise GossipReject("block is not from a higher slot than its parent") + + # [REJECT] The current finalized checkpoint is an ancestor of the block + checkpoint_block = get_checkpoint_block( + store, block.parent_root, store.finalized_checkpoint.epoch + ) + if checkpoint_block != store.finalized_checkpoint.root: + raise GossipReject("finalized checkpoint is not an ancestor of block") + + # [REJECT] The block is proposed by the expected proposer for the slot + # (if shuffling is not available, IGNORE instead and MAY be queued for later) + parent_state = store.block_states[block.parent_root].copy() + process_slots(parent_state, block.slot) + expected_proposer = get_beacon_proposer_index(parent_state) + if block.proposer_index != expected_proposer: + raise GossipReject("block proposer_index does not match expected proposer") + + # Mark this block as seen for this proposer/slot combination + seen.proposer_slots.add((block.proposer_index, block.slot)) + + +- name: validate_beacon_block_gossip#bellatrix + sources: [] + spec: | + + def validate_beacon_block_gossip( + seen: Seen, + store: Store, + state: BeaconState, + signed_beacon_block: SignedBeaconBlock, + current_time_ms: uint64, + # [New in Bellatrix] + block_payload_statuses: Dict[Root, PayloadValidationStatus] = {}, + ) -> None: + """ + Validate a SignedBeaconBlock for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + block = signed_beacon_block.message + execution_payload = block.body.execution_payload + + # [IGNORE] The block is not from a future slot + # (MAY be queued for processing at the appropriate slot) + if not is_not_from_future_slot(state, block.slot, current_time_ms): + raise GossipIgnore("block is from a future slot") + + # [IGNORE] The block is from a slot greater than the latest finalized slot + # (MAY choose to validate and store such blocks for additional purposes + # -- e.g. slashing detection, archive nodes, etc). + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + if block.slot <= finalized_slot: + raise GossipIgnore("block is not from a slot greater than the latest finalized slot") + + # [IGNORE] The block is the first block with valid signature received for the proposer for the slot + if (block.proposer_index, block.slot) in seen.proposer_slots: + raise GossipIgnore("block is not the first valid block for this proposer and slot") + + # [REJECT] The proposer index is a valid validator index + if block.proposer_index >= len(state.validators): + raise GossipReject("proposer index out of range") + + # [REJECT] The proposer signature is valid + proposer = state.validators[block.proposer_index] + domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(block.slot)) + signing_root = compute_signing_root(block, domain) + if not bls.Verify(proposer.pubkey, signing_root, signed_beacon_block.signature): + raise GossipReject("invalid proposer signature") + + # [IGNORE] The block's parent has been seen (via gossip or non-gossip sources) + # (MAY be queued until parent is retrieved) + if block.parent_root not in store.blocks: + raise GossipIgnore("block's parent has not been seen") + + # [New in Bellatrix] + if is_execution_enabled(state, block.body): + # [REJECT] The block's execution payload timestamp is correct with respect to the slot + if execution_payload.timestamp != compute_time_at_slot(state, block.slot): + raise GossipReject("incorrect execution payload timestamp") + + parent_payload_status = PAYLOAD_STATUS_NOT_VALIDATED + if block.parent_root in block_payload_statuses: + parent_payload_status = block_payload_statuses[block.parent_root] + + if block.parent_root not in store.block_states: + if parent_payload_status == PAYLOAD_STATUS_NOT_VALIDATED: + # [REJECT] The block's parent passes validation + raise GossipReject("block's parent is invalid and EL result is unknown") + + # [IGNORE] The block's parent passes validation + raise GossipIgnore("block's parent is invalid and EL result is known") + + # [IGNORE] The block's parent's execution payload passes validation + if parent_payload_status == PAYLOAD_STATUS_INVALIDATED: + raise GossipIgnore("block's parent is valid and EL result is invalid") + else: + # [REJECT] The block's parent passes validation + if block.parent_root not in store.block_states: + # [Modified in Bellatrix] + raise GossipReject("block's parent is invalid and execution is not enabled") + + # [REJECT] The block is from a higher slot than its parent + if block.slot <= store.blocks[block.parent_root].slot: + raise GossipReject("block is not from a higher slot than its parent") + + # [REJECT] The current finalized checkpoint is an ancestor of the block + checkpoint_block = get_checkpoint_block( + store, block.parent_root, store.finalized_checkpoint.epoch + ) + if checkpoint_block != store.finalized_checkpoint.root: + raise GossipReject("finalized checkpoint is not an ancestor of block") + + # [REJECT] The block is proposed by the expected proposer for the slot + # (if shuffling is not available, IGNORE instead and MAY be queued for later) + parent_state = store.block_states[block.parent_root].copy() + process_slots(parent_state, block.slot) + expected_proposer = get_beacon_proposer_index(parent_state) + if block.proposer_index != expected_proposer: + raise GossipReject("block proposer_index does not match expected proposer") + + # Mark this block as seen for this proposer/slot combination + seen.proposer_slots.add((block.proposer_index, block.slot)) + + +- name: validate_beacon_block_gossip#capella + sources: [] + spec: | + + def validate_beacon_block_gossip( + seen: Seen, + store: Store, + state: BeaconState, + signed_beacon_block: SignedBeaconBlock, + current_time_ms: uint64, + block_payload_statuses: Dict[Root, PayloadValidationStatus] = {}, + ) -> None: + """ + Validate a SignedBeaconBlock for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + block = signed_beacon_block.message + execution_payload = block.body.execution_payload + + # [IGNORE] The block is not from a future slot + # (MAY be queued for processing at the appropriate slot) + if not is_not_from_future_slot(state, block.slot, current_time_ms): + raise GossipIgnore("block is from a future slot") + + # [IGNORE] The block is from a slot greater than the latest finalized slot + # (MAY choose to validate and store such blocks for additional purposes + # -- e.g. slashing detection, archive nodes, etc). + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + if block.slot <= finalized_slot: + raise GossipIgnore("block is not from a slot greater than the latest finalized slot") + + # [IGNORE] The block is the first block with valid signature received for the proposer for the slot + if (block.proposer_index, block.slot) in seen.proposer_slots: + raise GossipIgnore("block is not the first valid block for this proposer and slot") + + # [REJECT] The proposer index is a valid validator index + if block.proposer_index >= len(state.validators): + raise GossipReject("proposer index out of range") + + # [REJECT] The proposer signature is valid + proposer = state.validators[block.proposer_index] + domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(block.slot)) + signing_root = compute_signing_root(block, domain) + if not bls.Verify(proposer.pubkey, signing_root, signed_beacon_block.signature): + raise GossipReject("invalid proposer signature") + + # [IGNORE] The block's parent has been seen (via gossip or non-gossip sources) + # (MAY be queued until parent is retrieved) + if block.parent_root not in store.blocks: + raise GossipIgnore("block's parent has not been seen") + + # [REJECT] The block's execution payload timestamp is correct with respect to the slot + if execution_payload.timestamp != compute_time_at_slot(state, block.slot): + raise GossipReject("incorrect execution payload timestamp") + + parent_payload_status = PAYLOAD_STATUS_NOT_VALIDATED + if block.parent_root in block_payload_statuses: + parent_payload_status = block_payload_statuses[block.parent_root] + + if block.parent_root not in store.block_states: + if parent_payload_status == PAYLOAD_STATUS_NOT_VALIDATED: + # [REJECT] The block's parent passes validation + raise GossipReject("block's parent is invalid and EL result is unknown") + + # [IGNORE] The block's parent passes validation + raise GossipIgnore("block's parent is invalid and EL result is known") + + # [IGNORE] The block's parent's execution payload passes validation + if parent_payload_status == PAYLOAD_STATUS_INVALIDATED: + raise GossipIgnore("block's parent is valid and EL result is invalid") # [REJECT] The block is from a higher slot than its parent if block.slot <= store.blocks[block.parent_root].slot: @@ -13126,6 +13551,65 @@ seen.proposer_slots.add((block.proposer_index, block.slot)) +- name: validate_bls_to_execution_change_gossip#capella + sources: [] + spec: | + + def validate_bls_to_execution_change_gossip( + seen: Seen, + state: BeaconState, + signed_bls_to_execution_change: SignedBLSToExecutionChange, + current_time_ms: uint64, + ) -> None: + """ + Validate a SignedBLSToExecutionChange for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + bls_to_execution_change = signed_bls_to_execution_change.message + validator_index = bls_to_execution_change.validator_index + + # [IGNORE] The current epoch is at or after the Capella fork epoch + # (where current_epoch is defined by the current wall-clock time) + time_since_genesis_ms = current_time_ms - state.genesis_time * 1000 + current_slot = Slot(time_since_genesis_ms // SLOT_DURATION_MS) + current_epoch = compute_epoch_at_slot(current_slot) + if current_epoch < CAPELLA_FORK_EPOCH: + raise GossipIgnore("current epoch is pre-capella") + + # [IGNORE] This is the first valid bls_to_execution_change received for the validator + if validator_index in seen.bls_to_execution_change_indices: + raise GossipIgnore("already seen BLS to execution change for this validator") + + # [REJECT] The validator index is valid + if validator_index >= len(state.validators): + raise GossipReject("validator index out of range") + + validator = state.validators[validator_index] + + # [REJECT] The validator has BLS withdrawal credentials + if validator.withdrawal_credentials[:1] != BLS_WITHDRAWAL_PREFIX: + raise GossipReject("validator does not have BLS withdrawal credentials") + + # [REJECT] The bls_to_execution_change is for the validator's withdrawal pubkey + if validator.withdrawal_credentials[1:] != hash(bls_to_execution_change.from_bls_pubkey)[1:]: + raise GossipReject("pubkey does not match validator withdrawal credentials") + + # [REJECT] The signature is valid + domain = compute_domain( + DOMAIN_BLS_TO_EXECUTION_CHANGE, genesis_validators_root=state.genesis_validators_root + ) + signing_root = compute_signing_root(bls_to_execution_change, domain) + if not bls.Verify( + bls_to_execution_change.from_bls_pubkey, + signing_root, + signed_bls_to_execution_change.signature, + ): + raise GossipReject("invalid BLS to execution change signature") + + # Mark this bls_to_execution_change as seen + seen.bls_to_execution_change_indices.add(validator_index) + + - name: validate_kzg_g1#deneb sources: [] spec: | @@ -13422,6 +13906,170 @@ seen.proposer_slashing_indices.add(proposer_index) +- name: validate_sync_committee_contribution_and_proof_gossip#altair + sources: [] + spec: | + + def validate_sync_committee_contribution_and_proof_gossip( + seen: Seen, + state: BeaconState, + signed_contribution_and_proof: SignedContributionAndProof, + current_time_ms: uint64, + ) -> None: + """ + Validate a SignedContributionAndProof for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + contribution_and_proof = signed_contribution_and_proof.message + contribution = contribution_and_proof.contribution + + # [IGNORE] The contribution's slot is for the current slot + # (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) + if not is_current_slot(state, contribution.slot, current_time_ms): + raise GossipIgnore("contribution is not for the current slot") + + # [REJECT] The subcommittee index is in the allowed range + if contribution.subcommittee_index >= SYNC_COMMITTEE_SUBNET_COUNT: + raise GossipReject("subcommittee index out of range") + + # [REJECT] The contribution has participants + if not any(contribution.aggregation_bits): + raise GossipReject("contribution has no participants") + + # [REJECT] The selection_proof selects the validator as an aggregator for the slot + if not is_sync_committee_aggregator(contribution_and_proof.selection_proof): + raise GossipReject("validator is not selected as aggregator") + + # [REJECT] The aggregator index is valid + if contribution_and_proof.aggregator_index >= len(state.validators): + raise GossipReject("aggregator index out of range") + + # [REJECT] The aggregator's validator index is in the declared subcommittee + # of the current sync committee + aggregator_pubkey = state.validators[contribution_and_proof.aggregator_index].pubkey + subcommittee_pubkeys = get_sync_subcommittee_pubkeys(state, contribution.subcommittee_index) + if aggregator_pubkey not in subcommittee_pubkeys: + raise GossipReject("aggregator not in subcommittee") + + # [IGNORE] A valid sync committee contribution with equal slot, beacon_block_root + # and subcommittee_index whose aggregation_bits is non-strict superset + # has not already been seen + contribution_key = ( + contribution.slot, + contribution.beacon_block_root, + contribution.subcommittee_index, + ) + contribution_bits = tuple(bool(bit) for bit in contribution.aggregation_bits) + seen_bits = seen.sync_contribution_data.get(contribution_key, set()) + if is_non_strict_superset(seen_bits, contribution_bits): + raise GossipIgnore("already seen contribution for this data") + + # [IGNORE] The sync committee contribution is the first valid contribution received + # for the aggregator with index contribution_and_proof.aggregator_index + # for the slot contribution.slot and subcommittee index contribution.subcommittee_index + aggregator_key = ( + contribution_and_proof.aggregator_index, + contribution.slot, + contribution.subcommittee_index, + ) + if aggregator_key in seen.sync_contribution_aggregator_slots: + raise GossipIgnore("already seen contribution from this aggregator") + + # [REJECT] The contribution_and_proof.selection_proof is a valid signature + # of the SyncAggregatorSelectionData derived from the contribution + # by the validator with index contribution_and_proof.aggregator_index + selection_data = SyncAggregatorSelectionData( + slot=contribution.slot, + subcommittee_index=contribution.subcommittee_index, + ) + domain = get_domain( + state, DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF, compute_epoch_at_slot(contribution.slot) + ) + signing_root = compute_signing_root(selection_data, domain) + if not bls.Verify(aggregator_pubkey, signing_root, contribution_and_proof.selection_proof): + raise GossipReject("invalid selection proof signature") + + # [REJECT] The aggregator signature, signed_contribution_and_proof.signature, is valid + domain = get_domain( + state, DOMAIN_CONTRIBUTION_AND_PROOF, compute_epoch_at_slot(contribution.slot) + ) + signing_root = compute_signing_root(contribution_and_proof, domain) + if not bls.Verify(aggregator_pubkey, signing_root, signed_contribution_and_proof.signature): + raise GossipReject("invalid aggregator signature") + + # [REJECT] The aggregate signature is valid for the message beacon_block_root + # and aggregate pubkey derived from the participation info in aggregation_bits + # for the subcommittee specified by the contribution.subcommittee_index + participant_pubkeys = [ + subcommittee_pubkeys[i] for i, bit in enumerate(contribution.aggregation_bits) if bit + ] + domain = get_domain(state, DOMAIN_SYNC_COMMITTEE, compute_epoch_at_slot(contribution.slot)) + signing_root = compute_signing_root(contribution.beacon_block_root, domain) + if not eth_fast_aggregate_verify(participant_pubkeys, signing_root, contribution.signature): + raise GossipReject("invalid aggregate signature") + + # Mark this contribution as seen + seen.sync_contribution_aggregator_slots.add(aggregator_key) + if contribution_key not in seen.sync_contribution_data: + seen.sync_contribution_data[contribution_key] = set() + seen.sync_contribution_data[contribution_key].add(contribution_bits) + + +- name: validate_sync_committee_message_gossip#altair + sources: [] + spec: | + + def validate_sync_committee_message_gossip( + seen: Seen, + state: BeaconState, + sync_committee_message: SyncCommitteeMessage, + subnet_id: uint64, + current_time_ms: uint64, + ) -> None: + """ + Validate a SyncCommitteeMessage for gossip propagation on a subnet. + Raises GossipIgnore or GossipReject on validation failure. + """ + # [IGNORE] The message's slot is for the current slot + # (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) + if not is_current_slot(state, sync_committee_message.slot, current_time_ms): + raise GossipIgnore("message is not for the current slot") + + # [REJECT] The validator index is valid + if sync_committee_message.validator_index >= len(state.validators): + raise GossipReject("validator index out of range") + + # [REJECT] The subnet_id is valid for the given validator + # (this implies the validator is part of the broader current sync committee + # along with the correct subcommittee) + valid_subnets = compute_subnets_for_sync_committee( + state, sync_committee_message.validator_index + ) + if subnet_id not in valid_subnets: + raise GossipReject("subnet_id is not valid for the validator") + + # [IGNORE] There has been no other valid sync committee message for the declared slot + # for the validator referenced by sync_committee_message.validator_index + # (this validation is per topic so that for a given slot, multiple messages could be + # forwarded with the same validator_index as long as the subnet_ids are distinct) + message_key = (sync_committee_message.slot, sync_committee_message.validator_index, subnet_id) + if message_key in seen.sync_message_validator_slots: + raise GossipIgnore("already seen message from this validator for this slot and subnet") + + # [REJECT] The signature is valid for the message beacon_block_root + # for the validator referenced by validator_index + validator = state.validators[sync_committee_message.validator_index] + domain = get_domain( + state, DOMAIN_SYNC_COMMITTEE, compute_epoch_at_slot(sync_committee_message.slot) + ) + signing_root = compute_signing_root(sync_committee_message.beacon_block_root, domain) + if not bls.Verify(validator.pubkey, signing_root, sync_committee_message.signature): + raise GossipReject("invalid sync committee message signature") + + # Mark this message as seen + seen.sync_message_validator_slots.add(message_key) + + - name: validate_target_epoch_against_current_time#phase0 sources: [] spec: | @@ -13913,7 +14561,7 @@ - name: verify_execution_payload_envelope#gloas sources: [] spec: | - + def verify_execution_payload_envelope( state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope, @@ -13929,6 +14577,7 @@ header = copy(state.latest_block_header) header.state_root = hash_tree_root(state) assert envelope.beacon_block_root == hash_tree_root(header) + assert envelope.parent_beacon_block_root == state.latest_block_header.parent_root # Verify consistency with the committed bid bid = state.latest_execution_payload_bid @@ -13950,7 +14599,7 @@ kzg_commitment_to_versioned_hash(commitment) for commitment in bid.blob_kzg_commitments ], - parent_beacon_block_root=state.latest_block_header.parent_root, + parent_beacon_block_root=envelope.parent_beacon_block_root, execution_requests=envelope.execution_requests, ) ) From 09bcb26a66a768e5cba2277d1e9852a0931a30a6 Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 17:21:16 +0200 Subject: [PATCH 15/16] missed --- specrefs/constants.yml | 21 +++++++++++++++++++++ specrefs/containers.yml | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/specrefs/constants.yml b/specrefs/constants.yml index 20531c03e66..acdbb451dc6 100644 --- a/specrefs/constants.yml +++ b/specrefs/constants.yml @@ -465,6 +465,20 @@ PAYLOAD_STATUS_FULL: PayloadStatus = 1 +- name: PAYLOAD_STATUS_INVALIDATED#bellatrix + sources: [] + spec: | + + PAYLOAD_STATUS_INVALIDATED: PayloadValidationStatus = 1 + + +- name: PAYLOAD_STATUS_NOT_VALIDATED#bellatrix + sources: [] + spec: | + + PAYLOAD_STATUS_NOT_VALIDATED: PayloadValidationStatus = 2 + + - name: PAYLOAD_STATUS_PENDING#gloas sources: - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/forkchoice/PayloadStatus.java @@ -474,6 +488,13 @@ PAYLOAD_STATUS_PENDING: PayloadStatus = 2 +- name: PAYLOAD_STATUS_VALID#bellatrix + sources: [] + spec: | + + PAYLOAD_STATUS_VALID: PayloadValidationStatus = 0 + + - name: PRIMITIVE_ROOT_OF_UNITY#deneb sources: [] spec: | diff --git a/specrefs/containers.yml b/specrefs/containers.yml index 3563264dfbf..f7a60cae29a 100644 --- a/specrefs/containers.yml +++ b/specrefs/containers.yml @@ -1298,6 +1298,20 @@ + execution_branch: ExecutionBranch +- name: LightClientHeader#gloas + sources: [] + spec: | + + class LightClientHeader(Container): + beacon: BeaconBlockHeader + # [Modified in Gloas:EIP7732] + # Removed `execution` + # [New in Gloas:EIP7732] + execution_block_hash: Hash32 + # [Modified in Gloas:EIP7732] + execution_branch: ExecutionBranch + + - name: LightClientOptimisticUpdate#altair sources: [] spec: | @@ -1358,6 +1372,25 @@ row_index: RowIndex +- name: PartialDataColumnGroupID#gloas + sources: [] + spec: | + + class PartialDataColumnGroupID(Container): + slot: Slot + beacon_block_root: Root + + +- name: PartialDataColumnSidecar#gloas + sources: [] + spec: | + + class PartialDataColumnSidecar(Container): + cells_present_bitmap: Bitlist[MAX_BLOB_COMMITMENTS_PER_BLOCK] + partial_column: List[Cell, MAX_BLOB_COMMITMENTS_PER_BLOCK] + kzg_proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK] + + - name: PayloadAttestation#gloas sources: - file: ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/epbs/versions/gloas/PayloadAttestation.java From f650b27dc38942960f5bf3feca869012892dbb8f Mon Sep 17 00:00:00 2001 From: Dmitrii Shmatko Date: Thu, 30 Apr 2026 17:39:33 +0200 Subject: [PATCH 16/16] Revert "Fix forkchoice full node search bug" This reverts commit 8fa7a5ac3487402afcbdf9cc305aa77a9a494a51. --- .../protoarray/ForkChoiceModelGloas.java | 16 ++++---- .../storage/protoarray/ProtoArrayTest.java | 38 ------------------- 2 files changed, 7 insertions(+), 47 deletions(-) diff --git a/storage/src/main/java/tech/pegasys/teku/storage/protoarray/ForkChoiceModelGloas.java b/storage/src/main/java/tech/pegasys/teku/storage/protoarray/ForkChoiceModelGloas.java index 9a6a4054b52..01e2eeaa88f 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/protoarray/ForkChoiceModelGloas.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/protoarray/ForkChoiceModelGloas.java @@ -358,15 +358,13 @@ && isPayloadDataAvailable(blockNodeIndex, blockRoot)) { if (!proposerNode.get().getParentRoot().equals(blockRoot)) { return true; } - // Spec: is_parent_node_full(store, proposer_block) — i.e. proposer's bid.parent_block_hash - // equals this block's bid.block_hash. resolveParentNode already encoded that decision when - // the proposer block was imported by attaching it to either the FULL or the EMPTY node, so - // checking the protoarray attachment is equivalent and avoids false positives from comparing - // the proposer BASE's inherited executionBlockHash (which collides with FULL's hash whenever - // the proposer attached to EMPTY but EMPTY's inherited hash happens to match FULL's). - final Optional fullNodeIndex = - blockNodeIndex.getFullNode(blockRoot).flatMap(protoArray::getNodeIndex); - return fullNodeIndex.isPresent() && fullNodeIndex.equals(proposerNode.get().getParentIndex()); + return blockNodeIndex + .getFullNode(blockRoot) + .flatMap(protoArray::getNode) + .map( + fullNode -> + fullNode.getExecutionBlockHash().equals(proposerNode.get().getExecutionBlockHash())) + .orElse(false); } private boolean isPayloadTimely( diff --git a/storage/src/test/java/tech/pegasys/teku/storage/protoarray/ProtoArrayTest.java b/storage/src/test/java/tech/pegasys/teku/storage/protoarray/ProtoArrayTest.java index 8cd02dc542c..e4b4b8d4e72 100644 --- a/storage/src/test/java/tech/pegasys/teku/storage/protoarray/ProtoArrayTest.java +++ b/storage/src/test/java/tech/pegasys/teku/storage/protoarray/ProtoArrayTest.java @@ -952,44 +952,6 @@ void tiebreaker_fullWinsOverEmpty_whenPayloadNotTimely_butBoostOnChildWithFullPa assertThat(block1aNode.getBestChildIndex()).isEqualTo(Optional.of(fullNodeIndex)); } - @Test - void tiebreaker_emptyWins_whenBoostedChildBuiltOnEmpty_evenIfInheritedHashCollidesWithFull() { - // Regression for the on_execution_payload_envelope__valid reference test. - // - // shouldExtendPayload must mirror the spec's is_parent_node_full(store, proposer_block) by - // checking the proposer block's protoarray attachment (FULL vs EMPTY) — that attachment was - // computed by resolveParentNode at on_block time from the proposer's bid.parent_block_hash. - // Comparing FULL.executionBlockHash against the proposer BASE's executionBlockHash is unsafe - // because the BASE hash is inherited from whichever node it attached to. When the proposer - // attaches to EMPTY but EMPTY's inherited hash happens to equal FULL's hash, the - // hash-equality check incorrectly returns true and the Gloas tiebreaker picks FULL. - addValidBlock(5, block1a, GENESIS_CHECKPOINT.getRoot()); - protoArray.createEmptyNode(block1a); - protoArray.onExecutionPayload(block1a, EXECUTION_BLOCK_NUMBER, EXECUTION_BLOCK_HASH); - protoArray.markNodeValid(block1a); - - final int emptyNodeIndex = protoArray.getEmptyNodeIndices().getInt(block1a); - final int fullNodeIndex = protoArray.getFullNodeIndices().getInt(block1a); - - // block2a attaches to EMPTY (its bid did NOT extend block1a's payload), but its stored - // executionBlockHash collides with block1a's FULL.executionBlockHash — simulating the - // inherited-hash collision seen in the reference test. - addValidBlockWithParentIndex( - 6, block2a, block1a, Optional.of(emptyNodeIndex), EXECUTION_BLOCK_HASH); - - // currentSlot = blockSlot + 1 → effective weight is 0 for both EMPTY and FULL → tiebreaker - // decides. No PTC votes → not timely / not available. Proposer-boost is on block2a, which - // attached to EMPTY → is_parent_node_full(block2a) must be false → should_extend_payload - // returns false → FULL tiebreaker is 0, EMPTY tiebreaker is 1 → EMPTY wins. - applyScoreChanges(gloasModel, UInt64.valueOf(6), Optional.of(block2a)); - - final ProtoNode block1aNode = protoArray.getProtoNode(block1a).orElseThrow(); - assertThat(block1aNode.getBestChildIndex()) - .describedAs("EMPTY must win when proposer attached to EMPTY, regardless of hash collision") - .isEqualTo(Optional.of(emptyNodeIndex)) - .isNotEqualTo(Optional.of(fullNodeIndex)); - } - @Test void emptyPathWinsOverFullPath_whenEmptyHasMoreWeight_notPreviousSlot() { // Block at slot 5, currentSlot = 100 → not previous slot → effectiveWeight = node.getWeight()