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
1fdab07
ForkChoice Confirmation Rule feature flag, noop
zilm13 Jul 7, 2026
d547aab
FCR: Add votes snapshot interface + tests
zilm13 Jul 7, 2026
8dcade5
FCR: Add FastConfirmationTracker
zilm13 Jul 7, 2026
f9a796b
FCR: wire FastConfirmationTracker
zilm13 Jul 7, 2026
0c64a04
FCR: updateFastConfirmationVariables
zilm13 Jul 8, 2026
4c53299
Fix race bugs in fast confirmation input updates
zilm13 Jul 8, 2026
f3f6252
FCR: estimateCommitteeWeightBetweenSlots
zilm13 Jul 8, 2026
917478a
FCR: add FastConfirmationCalculator
zilm13 Jul 8, 2026
332fb81
FCR: provide FastConfirmation states
zilm13 Jul 8, 2026
1239d67
fix unneeded class cast
zilm13 Jul 8, 2026
8327a30
Calculator gains FastConfirmationStates; add get_pulled_up_head_state…
zilm13 Jul 8, 2026
c1ef086
vote/committee sums; adds VoteSnapshot
zilm13 Jul 8, 2026
0ce0180
more FastConfirmationCalculator utils
zilm13 Jul 8, 2026
5a5f923
FFG helpers
zilm13 Jul 8, 2026
923be21
is_confirmed_chain_safe
zilm13 Jul 8, 2026
7e7f00f
find_latest_confirmed_descendant
zilm13 Jul 8, 2026
e4afc2c
get_latest_confirmed
zilm13 Jul 8, 2026
5c6bbcf
wire tracker (state-load → calculator → log confirmed root)
zilm13 Jul 8, 2026
dd2874e
Reference tests for FCR
zilm13 Jul 13, 2026
9f5c39c
FCR Gloas reference tests fixed
zilm13 Jul 13, 2026
7db3055
Clarify excluded referenece tests
zilm13 Jul 13, 2026
98cb9df
Fix forkchoice equivocating votes update
zilm13 Jul 14, 2026
75d83ba
Add SSE event fast_confirmation
zilm13 Jul 14, 2026
bf21d7a
Add metrics for FFCR calculation
zilm13 Jul 14, 2026
c7037de
fix pruned state retrieval
zilm13 Jul 14, 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
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"in" : "query",
"schema" : {
"type" : "string",
"description" : "Event types to subscribe to. Supported event types: [`attestation`, `attester_slashing`, `blob_sidecar`, `block_gossip`, `block`, `bls_to_execution_change`, `chain_reorg`, `contribution_and_proof`, `data_column_sidecar`, `execution_payload_available`, `execution_payload_bid`, `execution_payload_gossip`, `execution_payload`, `finalized_checkpoint`, `head_v2`, `head`, `payload_attestation_message`, `payload_attributes`, `proposer_preferences`, `proposer_slashing`, `single_attestation`, `sync_state`, `voluntary_exit`]",
"description" : "Event types to subscribe to. Supported event types: [`attestation`, `attester_slashing`, `blob_sidecar`, `block_gossip`, `block`, `bls_to_execution_change`, `chain_reorg`, `contribution_and_proof`, `data_column_sidecar`, `execution_payload_available`, `execution_payload_bid`, `execution_payload_gossip`, `execution_payload`, `fast_confirmation`, `finalized_checkpoint`, `head_v2`, `head`, `payload_attestation_message`, `payload_attributes`, `proposer_preferences`, `proposer_slashing`, `single_attestation`, `sync_state`, `voluntary_exit`]",
"example" : "head"
}
} ],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import tech.pegasys.teku.statetransition.execution.ReceivedExecutionPayloadBidEventsChannel;
import tech.pegasys.teku.statetransition.execution.ReceivedExecutionPayloadEventsChannel;
import tech.pegasys.teku.statetransition.forkchoice.ForkChoiceUpdatedResultSubscriber.ForkChoiceUpdatedResultNotification;
import tech.pegasys.teku.statetransition.forkchoice.fastconfirmation.FastConfirmationEventChannel;
import tech.pegasys.teku.statetransition.validation.InternalValidationResult;
import tech.pegasys.teku.storage.api.ChainHeadChannel;
import tech.pegasys.teku.storage.api.FinalizedCheckpointChannel;
Expand All @@ -70,7 +71,8 @@ public class EventSubscriptionManager
FinalizedCheckpointChannel,
ReceivedBlockEventsChannel,
ReceivedExecutionPayloadEventsChannel,
ReceivedExecutionPayloadBidEventsChannel {
ReceivedExecutionPayloadBidEventsChannel,
FastConfirmationEventChannel {
private static final Logger LOG = LogManager.getLogger();

private final Spec spec;
Expand Down Expand Up @@ -104,6 +106,7 @@ public EventSubscriptionManager(
eventChannels.subscribe(ReceivedBlockEventsChannel.class, this);
eventChannels.subscribe(ReceivedExecutionPayloadEventsChannel.class, this);
eventChannels.subscribe(ReceivedExecutionPayloadBidEventsChannel.class, this);
eventChannels.subscribe(FastConfirmationEventChannel.class, this);
syncDataProvider.subscribeToSyncStateChanges(this::onSyncStateChange);
nodeDataProvider.subscribeToReceivedBlobSidecar(this::onNewBlobSidecar);
nodeDataProvider.subscribeToAttesterSlashing(this::onNewAttesterSlashing);
Expand Down Expand Up @@ -202,6 +205,14 @@ public void onNewFinalizedCheckpoint(
notifySubscribersOfEvent(EventType.finalized_checkpoint, event);
}

@Override
public void onFastConfirmation(
final Bytes32 confirmedRoot, final UInt64 confirmedSlot, final UInt64 currentSlot) {
notifySubscribersOfEvent(
EventType.fast_confirmation,
new FastConfirmationEvent(confirmedRoot, confirmedSlot, currentSlot));
}

@Override
public void onBlockValidated(final SignedBeaconBlock block) {
onNewBlockGossip(block);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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.beaconrestapi.handlers.v1.events;

import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.BYTES32_TYPE;
import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.UINT64_TYPE;

import org.apache.tuweni.bytes.Bytes32;
import tech.pegasys.teku.infrastructure.json.types.SerializableTypeDefinition;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;

public class FastConfirmationEvent extends Event<FastConfirmationEvent.FastConfirmationData> {

static final SerializableTypeDefinition<FastConfirmationData> FAST_CONFIRMATION_EVENT_TYPE =
SerializableTypeDefinition.object(FastConfirmationData.class)
.name("FastConfirmationEvent")
.withField("block", BYTES32_TYPE, FastConfirmationData::getBlock)
.withField("slot", UINT64_TYPE, FastConfirmationData::getSlot)
.withField("current_slot", UINT64_TYPE, FastConfirmationData::getCurrentSlot)
.build();

FastConfirmationEvent(final Bytes32 block, final UInt64 slot, final UInt64 currentSlot) {
super(FAST_CONFIRMATION_EVENT_TYPE, new FastConfirmationData(block, slot, currentSlot));
}

public static class FastConfirmationData {
public final Bytes32 block;
public final UInt64 slot;
public final UInt64 currentSlot;

FastConfirmationData(final Bytes32 block, final UInt64 slot, final UInt64 currentSlot) {
this.block = block;
this.slot = slot;
this.currentSlot = currentSlot;
}

private Bytes32 getBlock() {
return block;
}

private UInt64 getSlot() {
return slot;
}

private UInt64 getCurrentSlot() {
return currentSlot;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ public class EventSubscriptionManagerTest {
private final FinalizedCheckpointEvent sampleCheckpointEvent =
new FinalizedCheckpointEvent(data.randomBytes32(), data.randomBytes32(), epoch, false);

private final FastConfirmationEvent sampleFastConfirmationEvent =
new FastConfirmationEvent(data.randomBytes32(), data.randomUInt64(), data.randomUInt64());

private final SyncState sampleSyncState = SyncState.IN_SYNC;
private final SignedBeaconBlock sampleBlock = data.randomSignedBeaconBlock(0);
private final BlobSidecar sampleBlobSidecar = data.randomBlobSidecar();
Expand Down Expand Up @@ -281,6 +284,15 @@ void shouldPropagateFinalizedCheckpointMessages() throws IOException {
checkEvent("finalized_checkpoint", sampleCheckpointEvent);
}

@Test
void shouldPropagateFastConfirmationMessages() throws IOException {
when(req.getQueryString()).thenReturn("&topics=fast_confirmation");
manager.registerClient(client1);

triggerFastConfirmationEvent();
checkEvent("fast_confirmation", sampleFastConfirmationEvent);
}

@Test
void shouldPropagateSyncState() throws IOException {
when(req.getQueryString()).thenReturn("&topics=sync_state");
Expand Down Expand Up @@ -613,6 +625,14 @@ private void triggerFinalizedCheckpointEvent() {
asyncRunner.executeQueuedActions();
}

private void triggerFastConfirmationEvent() {
manager.onFastConfirmation(
sampleFastConfirmationEvent.getData().block,
sampleFastConfirmationEvent.getData().slot,
sampleFastConfirmationEvent.getData().currentSlot);
asyncRunner.executeQueuedActions();
}

private void triggerReorgEvent() {
manager.chainHeadUpdated(
chainReorgEvent.getData().getSlot(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public enum EventType {
execution_payload_available,
execution_payload_bid,
payload_attestation_message,
proposer_preferences;
proposer_preferences,
fast_confirmation;

public static List<EventType> getTopics(final List<String> topics) {
return topics.stream().map(EventType::valueOf).toList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,12 @@ public abstract class Eth2ReferenceTestCase {
.putAll(OperationsTestExecutor.OPERATIONS_TEST_TYPES)
.putAll(SanityTests.SANITY_TEST_TYPES)
.putAll(GossipTests.GOSSIP_TEST_TYPES)
.putAll(ForkChoiceTestExecutor.FAST_CONFIRMATION_TEST_TYPES)
.put("slashing-protection-interchange", new SlashingProtectionInterchangeTestExecutor())
.put("light_client/single_merkle_proof", TestExecutor.IGNORE_TESTS)
.put("light_client/sync", TestExecutor.IGNORE_TESTS)
.put("light_client/update_ranking", TestExecutor.IGNORE_TESTS)
.put("light_client/data_collection", TestExecutor.IGNORE_TESTS)
// TODO: Fast confirmation reference tests implementation
.put("fast_confirmation/basic", TestExecutor.IGNORE_TESTS)
.put("fast_confirmation/current_epoch", TestExecutor.IGNORE_TESTS)
.put("fast_confirmation/empty_slots", TestExecutor.IGNORE_TESTS)
.put("fast_confirmation/ffg", TestExecutor.IGNORE_TESTS)
.put("fast_confirmation/is_one_confirmed", TestExecutor.IGNORE_TESTS)
.put("fast_confirmation/previous_epoch", TestExecutor.IGNORE_TESTS)
.put("fast_confirmation/reconfirmation", TestExecutor.IGNORE_TESTS)
.put("fast_confirmation/restart_gu", TestExecutor.IGNORE_TESTS)
.put("fast_confirmation/revert_finality", TestExecutor.IGNORE_TESTS)
.put("fast_confirmation/variables", TestExecutor.IGNORE_TESTS)
.build();

private static final ImmutableMap<String, TestExecutor> PHASE_0_TEST_TYPES =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@
import tech.pegasys.teku.infrastructure.async.AsyncRunnerFactory;
import tech.pegasys.teku.infrastructure.async.MetricTrackingExecutorFactory;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.async.SyncAsyncRunner;
import tech.pegasys.teku.infrastructure.async.eventthread.InlineEventThread;
import tech.pegasys.teku.infrastructure.metrics.StubMetricsSystem;
import tech.pegasys.teku.infrastructure.time.StubTimeProvider;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.kzg.KZG;
import tech.pegasys.teku.kzg.KZGProof;
Expand All @@ -68,6 +70,7 @@
import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.PayloadAttestationMessage;
import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope;
import tech.pegasys.teku.spec.datastructures.execution.PowBlock;
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;
Expand Down Expand Up @@ -97,6 +100,8 @@
import tech.pegasys.teku.statetransition.forkchoice.MergeTransitionBlockValidator;
import tech.pegasys.teku.statetransition.forkchoice.NoopForkChoiceNotifier;
import tech.pegasys.teku.statetransition.forkchoice.TickProcessor;
import tech.pegasys.teku.statetransition.forkchoice.fastconfirmation.FastConfirmationEventChannel;
import tech.pegasys.teku.statetransition.forkchoice.fastconfirmation.FastConfirmationTracker;
import tech.pegasys.teku.statetransition.payloadattestation.ValidatablePayloadAttestationMessage;
import tech.pegasys.teku.statetransition.util.DebugDataDumper;
import tech.pegasys.teku.statetransition.util.RPCFetchDelayProvider;
Expand Down Expand Up @@ -141,9 +146,30 @@ public class ForkChoiceTestExecutor implements TestExecutor {
.put("fork_choice_compliance/shuffling_test", new ForkChoiceTestExecutor())
.build();

public static final ImmutableMap<String, TestExecutor> FAST_CONFIRMATION_TEST_TYPES =
ImmutableMap.<String, TestExecutor>builder()
.put("fast_confirmation/basic", new ForkChoiceTestExecutor(true))
.put("fast_confirmation/current_epoch", new ForkChoiceTestExecutor(true))
.put("fast_confirmation/empty_slots", new ForkChoiceTestExecutor(true))
.put("fast_confirmation/ffg", new ForkChoiceTestExecutor(true))
.put("fast_confirmation/is_one_confirmed", new ForkChoiceTestExecutor(true))
.put("fast_confirmation/previous_epoch", new ForkChoiceTestExecutor(true))
.put("fast_confirmation/reconfirmation", new ForkChoiceTestExecutor(true))
.put("fast_confirmation/restart_gu", new ForkChoiceTestExecutor(true))
.put("fast_confirmation/revert_finality", new ForkChoiceTestExecutor(true))
.put("fast_confirmation/variables", new ForkChoiceTestExecutor(true))
.build();

private final List<?> testsToSkip;
private final boolean fastConfirmationEnabled;

public ForkChoiceTestExecutor(final String... testsToSkip) {
this(false, testsToSkip);
}

public ForkChoiceTestExecutor(
final boolean fastConfirmationEnabled, final String... testsToSkip) {
this.fastConfirmationEnabled = fastConfirmationEnabled;
this.testsToSkip = List.of(testsToSkip);
}

Expand Down Expand Up @@ -203,6 +229,17 @@ spec, new SignedBlockAndState(anchorBlock, anchorState)),
final AsyncBLSSignatureVerifier signatureVerifier =
AsyncBLSSignatureVerifier.wrap(
blsDisabled ? BLSSignatureVerifier.NOOP : BLSSignatureVerifier.SIMPLE);
// Run the fast confirmation side work synchronously so it completes within each onTick, before
// the following checks step reads the confirmation store.
final FastConfirmationTracker fastConfirmationTracker =
fastConfirmationEnabled
? FastConfirmationTracker.create(
spec,
Optional.of(SyncAsyncRunner.SYNC_RUNNER),
FastConfirmationEventChannel.NOOP,
new StubMetricsSystem(),
StubTimeProvider.withTimeInMillis(0))
: FastConfirmationTracker.NOOP;
final ForkChoice forkChoice =
new ForkChoice(
spec,
Expand All @@ -212,6 +249,7 @@ spec, new SignedBlockAndState(anchorBlock, anchorState)),
new ForkChoiceStateProvider(eventThread, recentChainData),
new TickProcessor(spec, recentChainData),
transitionBlockValidator,
fastConfirmationTracker,
// forkChoiceLateBlockReorgEnabled is true here always because this is the reference
// test executor
true,
Expand Down Expand Up @@ -243,6 +281,7 @@ spec, new SignedBlockAndState(anchorBlock, anchorState)),
blobSidecarManager,
dataColumnSidecarManager,
forkChoice,
fastConfirmationTracker,
executionLayer,
maybeAttestationValidator,
gossipValidationHelper,
Expand Down Expand Up @@ -289,6 +328,7 @@ private void runSteps(
final StubBlobSidecarManager blobSidecarManager,
final StubDataColumnSidecarManager dataColumnSidecarManager,
final ForkChoice forkChoice,
final FastConfirmationTracker fastConfirmationTracker,
final ExecutionLayerChannelStub executionLayer,
final Optional<AttestationValidator> maybeAttestationValidator,
final GossipValidationHelper gossipValidationHelper,
Expand All @@ -299,7 +339,7 @@ private void runSteps(
for (Map<String, Object> step : steps) {
LOG.info("Executing step {}", step);
if (step.containsKey("checks")) {
applyChecks(recentChainData, forkChoice, step);
applyChecks(recentChainData, forkChoice, fastConfirmationTracker, step);

} else if (step.containsKey("tick")) {
forkChoice.onTick(secondsToMillis(getUInt64(step, "tick")), Optional.empty());
Expand Down Expand Up @@ -449,8 +489,15 @@ private void applyAttestation(
final SafeFuture<AttestationProcessingResult> result =
forkChoice.onAttestation(validatableAttestation);
assertThat(result).isCompleted();
AttestationProcessingResult processingResult = safeJoin(result);
assertThat(processingResult.isSuccessful())
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())
.isEqualTo(valid);
}
Expand Down Expand Up @@ -660,6 +707,7 @@ private List<Map<String, Object>> loadSteps(final TestDefinition testDefinition)
private void applyChecks(
final RecentChainData recentChainData,
final ForkChoice forkChoice,
final FastConfirmationTracker fastConfirmationTracker,
final Map<String, Object> step) {
assertThat(forkChoice.processHead()).isCompleted();
final UpdatableStore store = recentChainData.getStore();
Expand Down Expand Up @@ -788,6 +836,42 @@ private void applyChecks(
get(checks, checkType),
ReadOnlyForkChoiceStrategy::getPayloadDataAvailabilityVote);

case "previous_epoch_observed_justified_checkpoint" ->
assertCheckpoint(
checkType,
fastConfirmationStore(fastConfirmationTracker)
.previousEpochObservedJustifiedCheckpoint(),
get(checks, checkType));

case "current_epoch_observed_justified_checkpoint" ->
assertCheckpoint(
checkType,
fastConfirmationStore(fastConfirmationTracker)
.currentEpochObservedJustifiedCheckpoint(),
get(checks, checkType));

case "previous_epoch_greatest_unrealized_checkpoint" ->
assertCheckpoint(
checkType,
fastConfirmationStore(fastConfirmationTracker)
.previousEpochGreatestUnrealizedCheckpoint(),
get(checks, checkType));

case "previous_slot_head" ->
assertThat(fastConfirmationStore(fastConfirmationTracker).previousSlotHead())
.describedAs(checkType)
.isEqualTo(getBytes32(checks, checkType));

case "current_slot_head" ->
assertThat(fastConfirmationStore(fastConfirmationTracker).currentSlotHead())
.describedAs(checkType)
.isEqualTo(getBytes32(checks, checkType));

case "confirmed_root" ->
assertThat(fastConfirmationStore(fastConfirmationTracker).confirmedRoot())
.describedAs(checkType)
.isEqualTo(getBytes32(checks, checkType));

default ->
throw new UnsupportedOperationException("Unsupported check type: " + checkType);
}
Expand All @@ -805,6 +889,12 @@ private void applyChecks(
}
}

private FastConfirmationStore fastConfirmationStore(final FastConfirmationTracker tracker) {
return tracker
.getFastConfirmationStore()
.orElseThrow(() -> new IllegalStateException("Fast confirmation store is not available"));
}

private void assertCheckpoint(
final String checkpointType,
final Checkpoint actual,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import tech.pegasys.teku.statetransition.forkchoice.MergeTransitionBlockValidator;
import tech.pegasys.teku.statetransition.forkchoice.NoopForkChoiceNotifier;
import tech.pegasys.teku.statetransition.forkchoice.TickProcessor;
import tech.pegasys.teku.statetransition.forkchoice.fastconfirmation.FastConfirmationTracker;
import tech.pegasys.teku.statetransition.util.DebugDataDumper;
import tech.pegasys.teku.statetransition.validation.AggregateAttestationValidator;
import tech.pegasys.teku.statetransition.validation.AttestationValidator;
Expand Down Expand Up @@ -118,6 +119,7 @@ public void runTest(final TestDefinition testDefinition) throws Throwable {
new ForkChoiceStateProvider(eventThread, recentChainData),
new TickProcessor(spec, recentChainData),
transitionBlockValidator,
FastConfirmationTracker.NOOP,
true,
LateBlockReorgPreparationHandler.NOOP,
DebugDataDumper.NOOP,
Expand Down
Loading
Loading