Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
149a447
Include compRefTests
Matilda-Clerke Aug 18, 2026
dc1026c
Make cleanRefTests depend on cleanRefTestsForkChoiceCompliance
Matilda-Clerke Aug 20, 2026
4d89e71
Discard uncomfirmed block timeliness if block import is unsuccessful
Matilda-Clerke Aug 20, 2026
1d05142
Fix compile issue on BlockTimelinessTrackerTest
Matilda-Clerke Aug 23, 2026
583a0e1
spotless
Matilda-Clerke Aug 23, 2026
d041005
Update BlockTimelinessTrackerTest
Matilda-Clerke Aug 24, 2026
956c936
Merge branch 'master' into integrate-comp-ref-tests
Matilda-Clerke Aug 25, 2026
2aa5421
Update viable_for_head_roots_and_weights check in ForkChoiceTestExecutor
Matilda-Clerke Aug 26, 2026
91c01ae
Update applyAttestation to consider deferred attestation not valid
Matilda-Clerke Aug 26, 2026
b655cd6
spotless
Matilda-Clerke Aug 26, 2026
bd42780
Remove head roots and weights set size check, as it doesn't match pre…
Matilda-Clerke Aug 26, 2026
2024f44
Remove unnecessary check on actual head root and weight payload status
Matilda-Clerke Aug 26, 2026
9abcf2f
Correct spelling in assertion message
Matilda-Clerke Aug 26, 2026
a1128ad
Remove no longer used HeadRootAndWeight.equals
Matilda-Clerke Aug 26, 2026
0624cb0
Prevent slot and epoch validation on attestations from blocks
Matilda-Clerke Aug 31, 2026
58f87b2
Spotless
Matilda-Clerke Aug 31, 2026
6176155
Move epoch check out of gossip-only attestation validation. Add unit …
Matilda-Clerke Sep 2, 2026
45700ef
spotless
Matilda-Clerke Sep 2, 2026
54849bb
Include non-viable heads in AttestationStateSelector operations
Matilda-Clerke Sep 3, 2026
2ec1d55
spotless
Matilda-Clerke Sep 3, 2026
7d72ed4
Compute new head immediately before importing block
Matilda-Clerke Sep 8, 2026
7c05bd6
Check if block has matching shuffling dependent root before labelling…
Matilda-Clerke Sep 9, 2026
1d35b23
spotless
Matilda-Clerke Sep 9, 2026
328dfef
Implement and use ForkChoiceUtilGloas.shouldApplyProposerBoost
Matilda-Clerke Sep 10, 2026
a84415b
Merge branch 'master' into integrate-comp-ref-tests
Matilda-Clerke Sep 10, 2026
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
38 changes: 35 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,7 @@ def slashingProtectionInterchangeRefTestBaseUrl = 'https://github.com/eth-client
def refTestDownloadDir = "${buildDir}/refTests/${refTestVersion}"
def blsRefTestDownloadDir = "${buildDir}/blsRefTests/${blsRefTestVersion}"
def slashingProtectionInterchangeRefTestDownloadDir = "${buildDir}/slashingProtectionInterchangeRefTests/${slashingProtectionInterchangeRefTestVersion}"
def compTestDownloadDir = "${buildDir}/compRefTests/${refTestVersion}"
def refTestExpandDir = "${project.rootDir}/eth-reference-tests/src/referenceTest/resources/consensus-spec-tests/"

def downloadFile(String url, String token, File outputFile) {
Expand Down Expand Up @@ -657,8 +658,19 @@ tasks.register('downloadSlashingProtectionInterchangeRefTests', Download) {
overwrite false
}

// comptests.tar.gz is only published alongside tagged consensus-specs releases (not the nightly
// vector-generation workflow), so it is skipped for nightly builds.
tasks.register('downloadCompTests', Download) {
onlyIf { !nightly }
src([
"${refTestBaseUrl}/${refTestVersion}/comptests.tar.gz"
])
dest "${compTestDownloadDir}/comptests.tar.gz"
overwrite false
}

tasks.register('downloadRefTests') {
dependsOn downloadEthRefTests, downloadBlsRefTests, downloadSlashingProtectionInterchangeRefTests
dependsOn downloadEthRefTests, downloadBlsRefTests, downloadSlashingProtectionInterchangeRefTests, downloadCompTests
}

tasks.register('cleanRefTestsGeneral', Delete) {
Expand Down Expand Up @@ -719,11 +731,31 @@ tasks.register('expandRefTestsSlashingProtectionInterchange', Copy) {
into "${refTestExpandDir}/tests/slashing-protection-interchange"
}

// comptests.tar.gz bundles the whole consensus-specs tests/ tree (pyspec source included, not
// just vectors), so only the fork_choice_compliance subtree is extracted here rather than the
// full ~3GB archive.
tasks.register('cleanRefTestsForkChoiceCompliance', Delete) {
delete fileTree(refTestExpandDir) {
include "tests/**/fork_choice_compliance/**"
}
}

tasks.register('expandRefTestsForkChoiceCompliance', Copy) {
dependsOn cleanRefTestsForkChoiceCompliance, downloadCompTests
onlyIf { !nightly }
from {
tarTree("${compTestDownloadDir}/comptests.tar.gz").matching {
include "tests/**/fork_choice_compliance/**"
}
}
into refTestExpandDir
}
Comment thread
cursor[bot] marked this conversation as resolved.

tasks.register('expandRefTests') {
dependsOn expandRefTestsGeneral, expandRefTestsMainnet, expandRefTestsMinimal, expandRefTestsBls, expandRefTestsSlashingProtectionInterchange
dependsOn expandRefTestsGeneral, expandRefTestsMainnet, expandRefTestsMinimal, expandRefTestsBls, expandRefTestsSlashingProtectionInterchange, expandRefTestsForkChoiceCompliance
}
tasks.register('cleanRefTests') {
dependsOn cleanRefTestsGeneral, cleanRefTestsMainnet, cleanRefTestsMinimal, cleanRefTestsBls, cleanRefTestsSlashingProtectionInterchange
dependsOn cleanRefTestsGeneral, cleanRefTestsMainnet, cleanRefTestsMinimal, cleanRefTestsBls, cleanRefTestsSlashingProtectionInterchange, cleanRefTestsForkChoiceCompliance
}

tasks.register('deploy') {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -40,6 +41,7 @@
import org.apache.tuweni.bytes.Bytes32;
import org.apache.tuweni.ssz.SSZ;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Assertions;
import org.opentest4j.TestAbortedException;
import tech.pegasys.teku.bls.BLSSignature;
import tech.pegasys.teku.bls.BLSSignatureVerifier;
Expand Down Expand Up @@ -75,7 +77,6 @@
import tech.pegasys.teku.spec.datastructures.forkchoice.FastConfirmationStore;
import tech.pegasys.teku.spec.datastructures.forkchoice.ForkChoiceNode;
import tech.pegasys.teku.spec.datastructures.forkchoice.ForkChoicePayloadStatus;
import tech.pegasys.teku.spec.datastructures.forkchoice.ProtoNodeData;
import tech.pegasys.teku.spec.datastructures.forkchoice.ReadOnlyForkChoiceStrategy;
import tech.pegasys.teku.spec.datastructures.forkchoice.ReadOnlyStore;
import tech.pegasys.teku.spec.datastructures.forkchoice.VoteUpdater;
Expand Down Expand Up @@ -536,15 +537,12 @@ private void applyAttestation(
forkChoice.onAttestation(validatableAttestation);
assertThat(result).isCompleted();
final AttestationProcessingResult processingResult = safeJoin(result);
// A current-slot attestation is valid but deferred by fork choice (stored and applied on the
// next tick). The fast confirmation vectors apply such attestations, so treat deferral as an
// accepted outcome.
final boolean acceptedByForkChoice =
processingResult.isSuccessful()
|| processingResult.getStatus()
== AttestationProcessingResult.Status.DEFER_FORK_CHOICE_PROCESSING;
assertThat(acceptedByForkChoice)
.withFailMessage(processingResult.getInvalidReason())
// If a current-slot attestation is valid but deferred by fork choice (stored and applied on the
// next tick), reference tests seem to expect it to be considered invalid, so we will not
// consider
// it a successful attestation
assertThat(processingResult.isSuccessful())
.withFailMessage("%s failed with processing result: %s", attestationName, processingResult)
.isEqualTo(valid);
}

Expand Down Expand Up @@ -847,27 +845,53 @@ private void applyChecks(

case "viable_for_head_roots_and_weights" -> {
final List<Map<String, Object>> viableHeadRootsAndWeightsData = get(checks, checkType);
final Map<Bytes32, UInt64> viableHeadRootsAndWeights =

final Set<HeadRootAndWeight> viableHeadRootsAndWeights =
viableHeadRootsAndWeightsData.stream()
.collect(
Collectors.toMap(
entry -> Bytes32.fromHexString((String) entry.get("root")),
entry -> UInt64.valueOf(entry.get("weight").toString())));
final Map<Bytes32, UInt64> chainHeadRootsAndWeights =
.map(
entry ->
new HeadRootAndWeight(
Bytes32.fromHexString((String) entry.get("root")),
UInt64.valueOf(entry.get("weight").toString()),
Optional.ofNullable((Integer) entry.get("payload_status"))
.map(this::convertToPayloadStatus)))
.collect(Collectors.toSet());
final Set<HeadRootAndWeight> chainHeadRootsAndWeights =
recentChainData
.getForkChoiceStrategy()
.map(ReadOnlyForkChoiceStrategy::getChainHeads)
.orElse(Collections.emptyList())
.stream()
.collect(Collectors.toMap(ProtoNodeData::getRoot, ProtoNodeData::getWeight));

assertThat(chainHeadRootsAndWeights.keySet())
.containsAll(viableHeadRootsAndWeights.keySet());

for (Bytes32 root : viableHeadRootsAndWeights.keySet()) {
UInt64 weight = viableHeadRootsAndWeights.get(root);
UInt64 actualWeight = chainHeadRootsAndWeights.get(root);
assertThat(actualWeight).describedAs("block %s's weight", root).isEqualTo(weight);
.map(
protoNodeData ->
new HeadRootAndWeight(
protoNodeData.getRoot(),
protoNodeData.getWeight(),
Optional.ofNullable(protoNodeData.getPayloadStatus())))
.collect(Collectors.toSet());

for (HeadRootAndWeight headRootAndWeight : viableHeadRootsAndWeights) {
boolean notPresent =
chainHeadRootsAndWeights.stream()
.noneMatch(
(chainHeadRootAndWeight) -> {
if (headRootAndWeight.root.equals(chainHeadRootAndWeight.root)
&& headRootAndWeight.weight.equals(chainHeadRootAndWeight.weight)) {
// an unset payload status means we don't need to check if payload
// status is correct
if (headRootAndWeight.payloadStatus.isPresent()) {
return headRootAndWeight.payloadStatus.equals(
chainHeadRootAndWeight.payloadStatus);
} else {
return true;
}
}
return false;
});
Assertions.assertFalse(
notPresent,
String.format(
"Unable to find %s in %s", headRootAndWeight, chainHeadRootsAndWeights));
}
}

Expand Down Expand Up @@ -1142,4 +1166,14 @@ public BlsSetting getBlsSetting() {
return BlsSetting.forCode(blsSetting);
}
}

private ForkChoicePayloadStatus convertToPayloadStatus(final int payloadStatus) {
return Arrays.stream(ForkChoicePayloadStatus.values())
.filter(fcps -> fcps.getValue() == payloadStatus)
.findAny()
.get();
}

private record HeadRootAndWeight(
Bytes32 root, UInt64 weight, Optional<ForkChoicePayloadStatus> payloadStatus) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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.datastructures.attestation;

public enum AttestationSource {
GOSSIP,
BLOCK
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import tech.pegasys.teku.spec.config.SpecConfig;
import tech.pegasys.teku.spec.config.SpecConfigAltair;
import tech.pegasys.teku.spec.config.SpecConfigBellatrix;
import tech.pegasys.teku.spec.datastructures.attestation.AttestationSource;
import tech.pegasys.teku.spec.datastructures.attestation.ValidatableAttestation;
import tech.pegasys.teku.spec.datastructures.blobs.versions.deneb.BlobSidecar;
import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlock;
Expand Down Expand Up @@ -536,13 +537,15 @@ private AttestationProcessingResult validateOnAttestation(
UInt64 currentEpoch = miscHelpers.computeEpochAtSlot(getCurrentSlot(store));
final ReadOnlyForkChoiceStrategy forkChoiceStrategy = store.getForkChoiceStrategy();

return validateOnAttestation(forkChoiceStrategy, currentEpoch, attestationData);
return validateOnAttestation(
forkChoiceStrategy, currentEpoch, attestationData, AttestationSource.GOSSIP);
}

public AttestationProcessingResult validateOnAttestation(
final ReadOnlyForkChoiceStrategy forkChoiceStrategy,
final UInt64 currentEpoch,
final AttestationData attestationData) {
final AttestationData attestationData,
final AttestationSource attestationSource) {
final Checkpoint target = attestationData.getTarget();

// Use GENESIS_EPOCH for previous when genesis to avoid underflow
Expand All @@ -551,9 +554,11 @@ public AttestationProcessingResult validateOnAttestation(
? currentEpoch.minus(UInt64.ONE)
: SpecConfig.GENESIS_EPOCH;

if (!target.getEpoch().equals(previousEpoch) && !target.getEpoch().equals(currentEpoch)) {
return AttestationProcessingResult.invalid(
"Attestations must be from the current or previous epoch");
if (attestationSource == AttestationSource.GOSSIP) {
if (!target.getEpoch().equals(previousEpoch) && !target.getEpoch().equals(currentEpoch)) {
return AttestationProcessingResult.invalid(
"Attestations must be from the current or previous epoch");
}
}

if (!target.getEpoch().equals(miscHelpers.computeEpochAtSlot(attestationData.getSlot()))) {
Expand Down Expand Up @@ -923,10 +928,10 @@ public boolean shouldNotifyForkChoiceUpdatedOnBlock() {
}

public boolean shouldApplyProposerBoost(
final ForkChoiceReorgContext context,
final Bytes32 proposerBoostRoot,
final ReadOnlyForkChoiceStrategy forkChoiceStrategy,
final UInt64 reorgThreshold,
final BeaconState justifiedState) {
final UInt64 reorgThreshold) {
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,24 +270,20 @@ public Optional<ForkChoiceUtilGloas> toVersionGloas() {
* If the boosted block's parent was weak and from the previous slot, boost only applies if there
* are no timely equivocations from the same proposer.
*
* <p>Implementation note: the proposer-equivocation branch is intentionally not implemented yet.
* The current code records both block timeliness flags, but it does not yet consume the PTC
* timeliness bit here to suppress proposer boost on same-proposer equivocations. Because that
* branch is still deferred, the weak-parent check has no effect on the return value and is
* intentionally skipped here.
* <p>Spec mapping: {@code should_apply_proposer_boost(store)}.
*
* @param context the fork choice reorg context, used to look up blocks and PTC timeliness
* @param proposerBoostRoot the current proposer boost root, empty if none
* @param forkChoiceStrategy the fork choice strategy for looking up block data
* @param reorgThreshold the threshold for the head weakness check
* @param justifiedState unused until the proposer-equivocation branch is implemented
* @return true if proposer boost should be applied
*/
@Override
public boolean shouldApplyProposerBoost(
final ForkChoiceReorgContext context,
final Bytes32 proposerBoostRoot,
final ReadOnlyForkChoiceStrategy forkChoiceStrategy,
final UInt64 reorgThreshold,
final BeaconState justifiedState) {
final UInt64 reorgThreshold) {
final Optional<Bytes32> maybeParentRoot = forkChoiceStrategy.blockParentRoot(proposerBoostRoot);
final Optional<UInt64> maybeBlockSlot = forkChoiceStrategy.blockSlot(proposerBoostRoot);
if (maybeParentRoot.isEmpty() || maybeBlockSlot.isEmpty()) {
Expand All @@ -299,21 +295,57 @@ public boolean shouldApplyProposerBoost(
if (maybeParentSlot.isEmpty()) {
return true;
}
final UInt64 parentSlot = maybeParentSlot.get();
// Apply proposer boost if parent is not from the previous slot
if (maybeParentSlot.get().increment().isLessThan(blockSlot)) {
if (parentSlot.increment().isLessThan(blockSlot)) {
return true;
}
// TODO-GLOAS: implement the Gloas equivocation suppression branch from
// should_apply_proposer_boost
// using recorded PTC timeliness instead of routing a predicate through ForkChoice.
// The complication is that we need to have a good interaction with gossip datastructures to
// detect equivocations. Spec should probably be updated.
// NOTE: there is no point in implementing the following check without implementing
// equivocation.
// # Apply proposer boost if `parent` is not weak
// if not is_head_weak(store, parent_root):
// return True
return true;
final ReadOnlyStore store = context.getStore();
// Apply proposer boost if `parent` is not weak
if (!isHeadWeak(store, parentRoot, reorgThreshold)) {
return true;
}
// If `parent` is weak and from the previous slot, apply
// proposer boost if there are no early equivocations
return !hasTimelyEquivocatingSiblingProposal(
context, forkChoiceStrategy, parentRoot, parentSlot);
}

/**
* Spec mapping: the {@code equivocations} list comprehension inside {@code
* should_apply_proposer_boost}.
*
* <p>Looks for another block proposed at the same slot as {@code parentRoot}, by the same
* proposer, whose PTC-timeliness bit is set. Finding one means the parent's proposer equivocated
* in a way that was seen in time by the PTC, which should suppress proposer boost on an
* otherwise-weak parent.
*/
private boolean hasTimelyEquivocatingSiblingProposal(
final ForkChoiceReorgContext context,
final ReadOnlyForkChoiceStrategy forkChoiceStrategy,
final Bytes32 parentRoot,
final UInt64 parentSlot) {
final ReadOnlyStore store = context.getStore();
final Optional<UInt64> maybeParentProposerIndex =
store.getBlockIfAvailable(parentRoot).map(block -> block.getMessage().getProposerIndex());
if (maybeParentProposerIndex.isEmpty()) {
return false;
}
final UInt64 parentProposerIndex = maybeParentProposerIndex.get();
return forkChoiceStrategy.getBlockRootsAtSlot(parentSlot).stream()
.filter(siblingRoot -> !siblingRoot.equals(parentRoot))
.anyMatch(
siblingRoot ->
context
.getBlockTimeliness(siblingRoot)
.map(BlockTimeliness::isTimelyPtc)
.orElse(false)
&& store
.getBlockIfAvailable(siblingRoot)
.map(
block ->
block.getMessage().getProposerIndex().equals(parentProposerIndex))
.orElse(false));
}

/**
Expand Down
Loading
Loading