Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,16 @@ public void updateCandidateState(

final MutableBeaconStateGloas stateGloas = MutableBeaconStateGloas.required(state);

// Genesis payload is EMPTY: latestBlockHash stays zero while bid.blockHash tracks the
// eth1 block hash, so the first post-genesis block is treated as having an EMPTY parent.
stateGloas.setLatestBlockHash(Bytes32.ZERO);
Comment thread
cursor[bot] marked this conversation as resolved.
stateGloas.setLatestExecutionPayloadBid(
schemaDefinitionsGloas
.getExecutionPayloadBidSchema()
.create(
Bytes32.ZERO,
Bytes32.ZERO,
Bytes32.ZERO,
eth1BlockHash,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Genesis detection broken in block processor after bid change

High Severity

Changing the genesis bid's blockHash from Bytes32.ZERO to eth1BlockHash breaks the isGenesisBlock check in BlockProcessorGloas.processParentExecutionPayload (line 141), which still uses parentBid.getBlockHash().equals(Bytes32.ZERO). After this change, that check evaluates to false at genesis. The sibling check in WithdrawalsHelpersGloas correctly uses stateGloas.getLatestBlockHash() instead. The new test masks this by constructing a parentBid with blockHash = Bytes32.ZERO, which no longer reflects the actual genesis state.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e3c30cc. Configure here.

Bytes32.ZERO,
Bytes20.ZERO,
UInt64.ZERO,
Expand Down Expand Up @@ -219,7 +222,6 @@ public void updateCandidateState(
.createFromElements(builderPendingPayments));
stateGloas.setBuilderPendingWithdrawals(
schemaDefinitionsGloas.getBuilderPendingWithdrawalsSchema().of());
stateGloas.setLatestBlockHash(Bytes32.ZERO);
stateGloas.setPayloadExpectedWithdrawals(
schemaDefinitionsGloas.getExecutionPayloadSchema().getWithdrawalsSchemaRequired().of());
stateGloas.setPtcWindow(accessorsGloas.initializePtcWindow(state));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import tech.pegasys.teku.spec.datastructures.state.Validator;
import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState;
import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.bellatrix.BeaconStateBellatrix;
import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.gloas.BeaconStateGloas;
import tech.pegasys.teku.spec.datastructures.util.DepositGenerator;
import tech.pegasys.teku.spec.util.DataStructureUtil;

Expand Down Expand Up @@ -195,4 +196,23 @@ public void shouldGenerateStateWithExecutionPayload() {
assertThat(BeaconStateBellatrix.required(actualState).getLatestExecutionPayloadHeader())
.hasValue(payloadHeader);
}

@Test
void shouldInitializeGloasGenesisWithEmptyLatestBlockHashAndEth1BidBlockHash() {
final Spec gloasSpec = TestSpecFactory.createMinimalGloas();
final DataStructureUtil gloasDataStructureUtil = new DataStructureUtil(gloasSpec);
final GenesisGenerator genesisGenerator =
new GenesisGenerator(gloasSpec.getGenesisSpec(), gloasSpec.fork(UInt64.ZERO));
final List<Deposit> deposits =
new MockStartDepositGenerator(gloasSpec, new DepositGenerator(gloasSpec, true))
.createDeposits(VALIDATOR_KEYS).stream().map(Deposit::new).toList();
final Bytes32 eth1BlockHash = gloasDataStructureUtil.randomBytes32();

genesisGenerator.updateCandidateState(eth1BlockHash, UInt64.ONE, deposits);

final BeaconStateGloas actualState =
BeaconStateGloas.required(genesisGenerator.getGenesisState());
assertThat(actualState.getLatestBlockHash()).isEqualTo(Bytes32.ZERO);
assertThat(actualState.getLatestExecutionPayloadBid().getBlockHash()).isEqualTo(eth1BlockHash);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright Consensys Software Inc., 2026
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package tech.pegasys.teku.spec.logic.versions.gloas.block;

import static org.assertj.core.api.Assertions.assertThat;

import org.apache.tuweni.bytes.Bytes32;
import org.junit.jupiter.api.Test;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.spec.Spec;
import tech.pegasys.teku.spec.TestSpecFactory;
import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlock;
import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadBid;
import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.gloas.MutableBeaconStateGloas;
import tech.pegasys.teku.spec.logic.common.statetransition.exceptions.BlockProcessingException;
import tech.pegasys.teku.spec.util.DataStructureUtil;

class BlockProcessorGloasTest {

private final Spec spec = TestSpecFactory.createMinimalGloas();
private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec);
private final UInt64 gloasSlot = UInt64.ONE;

@Test
void processParentExecutionPayload_genesisDoesNotUpdateLatestBlockHash()
throws BlockProcessingException {
final ExecutionPayloadBid parentBid =
dataStructureUtil.randomExecutionPayloadBid(
UInt64.ZERO, UInt64.ZERO, Bytes32.ZERO, dataStructureUtil.randomBytes32());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test uses wrong genesis bid blockHash, exercises wrong path

Medium Severity

The test creates a parentBid with blockHash = Bytes32.ZERO, but the genesis generator now sets blockHash = eth1BlockHash (non-zero). This causes the test to exercise the isGenesisBlock path in processParentExecutionPayload (via parentBid.getBlockHash().equals(Bytes32.ZERO)), which is not the path that actually executes for genesis blocks in production. In reality, isGenesisBlock would be false and the isParentBlockEmpty path would handle the genesis case instead. The test passes but doesn't validate the actual production code path, masking potential regressions in the isParentBlockEmpty logic.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3e53a8f. Configure here.

final MutableBeaconStateGloas state =
MutableBeaconStateGloas.required(
dataStructureUtil
.stateBuilderGloas(16, 4, 4)
.slot(gloasSlot)
.latestBlockHash(Bytes32.ZERO)
.latestExecutionPayloadBid(parentBid)
.build()
.createWritableCopy());

final ExecutionPayloadBid childBid =
dataStructureUtil.randomExecutionPayloadBid(
Bytes32.ZERO, gloasSlot, UInt64.ONE, UInt64.ZERO, UInt64.ZERO);
final BeaconBlock block =
dataStructureUtil.randomBeaconBlock(
gloasSlot,
dataStructureUtil.randomBeaconBlockBody(
gloasSlot,
builder -> {
builder.signedExecutionPayloadBid(
dataStructureUtil.randomSignedExecutionPayloadBid(childBid));
builder.parentExecutionRequests(dataStructureUtil.emptyExecutionRequests());
}));

spec.getBlockProcessor(gloasSlot)
.processParentExecutionPayload(
state,
block,
spec.atSlot(gloasSlot).beaconStateMutators().createValidatorExitContextSupplier(state));

assertThat(state.getLatestBlockHash()).isEqualTo(Bytes32.ZERO);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,22 @@ void getParentPayloadStatus_shouldReturnEmpty_whenParentBlockHashDoesNotMatch()
assertThat(result).isCompletedWithValue(PayloadStatus.PAYLOAD_STATUS_EMPTY);
}

@Test
void getParentPayloadStatus_shouldReturnEmpty_whenParentMessageBlockHashIsZero() {
final BeaconBlock parentBlock = createBlockWithBlockHash(Bytes32.ZERO);
final BeaconBlock currentBlock =
createBlockWithParentAndParentBlockHash(parentBlock.getRoot(), Bytes32.ZERO);

final ReadOnlyStore store = mock(ReadOnlyStore.class);
when(store.retrieveBlock(currentBlock.getParentRoot()))
.thenReturn(SafeFuture.completedFuture(Optional.of(parentBlock)));

final SafeFuture<PayloadStatus> result =
forkChoiceUtil.getParentPayloadStatus(store, currentBlock);

assertThat(result).isCompletedWithValue(PayloadStatus.PAYLOAD_STATUS_EMPTY);
}

@Test
void getParentPayloadStatus_shouldThrowException_whenParentBlockNotFound() {
final SignedBeaconBlock currentBlock = dataStructureUtil.randomSignedBeaconBlock();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright Consensys Software Inc., 2026
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package tech.pegasys.teku.spec.logic.versions.gloas.withdrawals;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import org.apache.tuweni.bytes.Bytes32;
import org.junit.jupiter.api.Test;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.spec.Spec;
import tech.pegasys.teku.spec.TestSpecFactory;
import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadBid;
import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.gloas.MutableBeaconStateGloas;
import tech.pegasys.teku.spec.schemas.SchemaDefinitionsGloas;
import tech.pegasys.teku.spec.util.DataStructureUtil;

class WithdrawalsHelpersGloasTest {

private final Spec spec = TestSpecFactory.createMinimalGloas();
private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec);
private final UInt64 gloasSlot = UInt64.ONE;

@Test
void processWithdrawals_genesisDoesNotAdvanceWithdrawalIndices() {
final SchemaDefinitionsGloas schemaDefinitions =
SchemaDefinitionsGloas.required(spec.getGenesisSpec().getSchemaDefinitions());
final ExecutionPayloadBid parentBid =
dataStructureUtil.randomExecutionPayloadBid(
UInt64.ZERO, UInt64.ZERO, Bytes32.ZERO, dataStructureUtil.randomBytes32());
final MutableBeaconStateGloas state =
MutableBeaconStateGloas.required(
dataStructureUtil
.stateBuilderGloas(16, 4, 4)
.slot(gloasSlot)
.latestBlockHash(Bytes32.ZERO)
.latestExecutionPayloadBid(parentBid)
.nextWithdrawalIndex(UInt64.valueOf(7))
.builderPendingWithdrawals(
schemaDefinitions
.getBuilderPendingWithdrawalsSchema()
.createFromElements(
List.of(
schemaDefinitions
.getBuilderPendingWithdrawalSchema()
.create(
dataStructureUtil.randomEth1Address(),
UInt64.ONE,
UInt64.ZERO))))
.build()
.createWritableCopy());

final UInt64 preNextWithdrawalIndex = state.getNextWithdrawalIndex();
final int prePendingWithdrawalsCount = state.getBuilderPendingWithdrawals().size();

spec.atSlot(gloasSlot).getWithdrawalsHelpers().orElseThrow().processWithdrawals(state);

assertThat(state.getNextWithdrawalIndex()).isEqualTo(preNextWithdrawalIndex);
assertThat(state.getBuilderPendingWithdrawals()).hasSize(prePendingWithdrawalsCount);
}
}
Loading