Skip to content

Comments

Event adapter refactor to be fork based#10361

Open
zilm13 wants to merge 6 commits intoConsensys:epbsfrom
zilm13:event-adapter-refactor
Open

Event adapter refactor to be fork based#10361
zilm13 wants to merge 6 commits intoConsensys:epbsfrom
zilm13:event-adapter-refactor

Conversation

@zilm13
Copy link
Contributor

@zilm13 zilm13 commented Feb 11, 2026

PR Description

Fixed Issue(s)

Documentation

  • I thought about documentation and added the doc-change-required label to this PR if updates are required.

Changelog

  • I thought about adding a changelog entry, and added one if I deemed necessary.

Note

Medium Risk
Changes core validator timing/scheduling and fork-transition behavior (including new EIP-7732 duties), which could impact block/attestation production timing if incorrect despite added test coverage.

Overview
Refactors validator time-based duty scheduling to be fork-aware by splitting the previous monolithic TimeBasedEventAdapter logic into Phase0TimeBasedEventAdapter and GloasTimeBasedEventAdapter, selected via a new ForkAwareTimeBasedEventAdapter wrapper.

GloasTimeBasedEventAdapter adds EIP-7732 (“Gloas”) scheduling, including the new payload timeliness attestation event and explicit transition behavior across the fork boundary, while TimeBasedEventAdapter becomes an abstract base providing shared scheduling/utilities. All beacon node API entrypoints now instantiate ForkAwareTimeBasedEventAdapter, and new/updated tests cover EIP-7732 scheduling and pre-/post-fork behavior.

Written by Cursor Bugbot for commit a0b37cf. This will update automatically on new commits. Configure here.

Copy link
Contributor

@tbenr tbenr left a comment

Choose a reason for hiding this comment

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

some suggestions and an additional AI generated test to verify the transition

  @Test
  void shouldTransitionFromPhase0ToEip7732DutiesAcrossForkBoundary() {
    // Spec: minimal config, EIP7732 fork at epoch 1 (slot 8)
    // SLOTS_PER_EPOCH=8, SECONDS_PER_SLOT=6, millisPerSlot=6000
    // genesisTime=100 sec, eip7732 starts at slot 8 = 148 sec
    final UInt64 genesisTime = timeProvider.getTimeInSeconds(); // 100 sec
    when(genesisDataProvider.getGenesisTime()).thenReturn(SafeFuture.completedFuture(genesisTime));

    // Start at mid-slot 5 (133 sec), so next slot is 6 (pre-EIP7732)
    timeProvider.advanceTimeBySeconds(33); // 100 + 33 = 133 sec

    assertThat(eventAdapter.start()).isCompleted();
    asyncRunner.executeDueActionsRepeatedly();
    verifyNoMoreInteractions(validatorTimingChannel);

    // --- Pre-EIP7732 slot 6: duties at 1/3 and 2/3 timing ---

    // Advance to slot 6 + 1/3 (138 sec). Slot 6 start at 136 sec also fires.
    timeProvider.advanceTimeByMillis(5000); // 133000 + 5000 = 138000 ms
    asyncRunner.executeDueActionsRepeatedly();
    verify(validatorTimingChannel).onSlot(UInt64.valueOf(6));
    verify(validatorTimingChannel).onBlockProductionDue(UInt64.valueOf(6));
    verify(validatorTimingChannel, times(1)).onAttestationCreationDue(UInt64.valueOf(6));
    verify(validatorTimingChannel, never()).onAttestationAggregationDue(UInt64.valueOf(6));

    // Advance to slot 6 + 2/3 (140 sec)
    timeProvider.advanceTimeByMillis(2000); // 140000 ms
    asyncRunner.executeDueActionsRepeatedly();
    verify(validatorTimingChannel, times(1)).onAttestationAggregationDue(UInt64.valueOf(6));
    verify(validatorTimingChannel, never()).onPayloadAttestationDue(UInt64.valueOf(6));

    // --- Slot 7: last pre-EIP7732 slot, old duties expire after firing ---

    // Advance to slot 7 + 1/3 (144 sec). Slot 7 start at 142 sec also fires.
    // After attestation fires, nextDue (150000) > expiration (148000) -> expires!
    // Expiration callback schedules new attestation at slot 8 start + 1/4
    timeProvider.advanceTimeByMillis(4000); // 144000 ms
    asyncRunner.executeDueActionsRepeatedly();
    verify(validatorTimingChannel).onSlot(UInt64.valueOf(7));
    verify(validatorTimingChannel, times(1)).onAttestationCreationDue(UInt64.valueOf(7));

    // Advance to slot 7 + 2/3 (146 sec)
    // After aggregation fires, nextDue (152000) > expiration (148000) -> expires!
    // Expiration callback schedules new aggregation + payload attestation at slot 8
    timeProvider.advanceTimeByMillis(2000); // 146000 ms
    asyncRunner.executeDueActionsRepeatedly();
    verify(validatorTimingChannel, times(1)).onAttestationAggregationDue(UInt64.valueOf(7));
    verify(validatorTimingChannel, never()).onPayloadAttestationDue(UInt64.valueOf(7));

    // --- Post-EIP7732 slot 8: fork boundary! Duties at 1/4, 2/4, 3/4 timing ---

    // Advance to slot 8 start (148 sec)
    timeProvider.advanceTimeByMillis(2000); // 148000 ms
    asyncRunner.executeDueActionsRepeatedly();
    verify(validatorTimingChannel).onSlot(UInt64.valueOf(8));
    // No attestation/aggregation/payload duties should have fired yet at slot 8 start
    verify(validatorTimingChannel, never()).onAttestationCreationDue(UInt64.valueOf(8));
    verify(validatorTimingChannel, never()).onAttestationAggregationDue(UInt64.valueOf(8));
    verify(validatorTimingChannel, never()).onPayloadAttestationDue(UInt64.valueOf(8));

    // Advance to slot 8 + 1/4 (149.5 sec = 149500 ms) - EIP7732 attestation timing
    timeProvider.advanceTimeByMillis(1500); // 149500 ms
    asyncRunner.executeDueActionsRepeatedly();
    verify(validatorTimingChannel, times(1)).onAttestationCreationDue(UInt64.valueOf(8));

    // Advance to slot 8 + 2/4 (151 sec = 151000 ms) - EIP7732 aggregation timing
    timeProvider.advanceTimeByMillis(1500); // 151000 ms
    asyncRunner.executeDueActionsRepeatedly();
    verify(validatorTimingChannel, times(1)).onAttestationAggregationDue(UInt64.valueOf(8));

    // Advance to slot 8 + 3/4 (152.5 sec = 152500 ms) - payload attestation (new in EIP7732!)
    timeProvider.advanceTimeByMillis(1500); // 152500 ms
    asyncRunner.executeDueActionsRepeatedly();
    verify(validatorTimingChannel, times(1)).onPayloadAttestationDue(UInt64.valueOf(8));
  }

slot);
return;
}
LOG.info("EVENT *** onPayloadTimelinessAttestationDue");
Copy link
Contributor

Choose a reason for hiding this comment

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

we should remove this noise left from the original :)

public class Eip7732TimeBasedEventAdapter extends TimeBasedEventAdapter {
private static final Logger LOG = LogManager.getLogger();

private UInt64 genesisTime;
Copy link
Contributor

Choose a reason for hiding this comment

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

we could have genesisTime in the abstract class

Comment on lines +34 to +36
final ValidatorTimingChannel validatorTimingChannel;
final Spec spec;
final RepeatingTaskScheduler taskScheduler;
Copy link
Contributor

Choose a reason for hiding this comment

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

protected?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

do we need to extend it over package-private?


public class ForkAwareTimeBasedEventAdapter implements BeaconChainEventAdapter {

final TimeBasedEventAdapter delegate;
Copy link
Contributor

Choose a reason for hiding this comment

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

private?

@tbenr
Copy link
Contributor

tbenr commented Feb 11, 2026

and, obviously, as pointed out by @StefanBratanov , 7732 -> GLOAS

@zilm13
Copy link
Contributor Author

zilm13 commented Feb 11, 2026

Addressed all feedback
I've renamed to Gloas.. but it's still a fork where there is no Gloas Milestone

@zilm13 zilm13 requested a review from tbenr February 11, 2026 23:27
final TimeProvider timeProvider,
final ValidatorTimingChannel validatorTimingChannel,
final Spec spec) {
if (spec.isMilestoneSupported(SpecMilestone.EIP7732)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

EIP7732 ?

Copy link
Contributor

@tbenr tbenr left a comment

Choose a reason for hiding this comment

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

there are still a lot of references to EIP7732 milestone

oh.. but this is not master... why??

Copy link
Contributor

@tbenr tbenr left a comment

Choose a reason for hiding this comment

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

LGTM.
nit: some renames on the new test are needed

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.


final UInt64 nextSlotStartTimeMillis = secondsToMillis(nextSlotStartTime);

final UInt64 millisPerSlot = secondsToMillis(secondsPerSlot);
Copy link

Choose a reason for hiding this comment

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

Duplicated initialization logic across fork-specific subclasses

Low Severity

Both GloasTimeBasedEventAdapter.start() and Phase0TimeBasedEventAdapter.start() contain an identical block of initialization: setGenesisTime, computing currentSlot/nextSlotStartTime/secondsPerSlot, scheduling onStartSlot, and converting to millis — plus the same NOTE comment. This duplicated logic could live in the abstract TimeBasedEventAdapter base class (e.g., as a common setup method or template method), reducing maintenance burden and eliminating the risk that a future subclass forgets to call setGenesisTime, which would cause a NPE.

Additional Locations (1)

Fix in Cursor Fix in Web

@StefanBratanov
Copy link
Contributor

@zilm13 can we raise this PR against master

@zilm13
Copy link
Contributor Author

zilm13 commented Feb 18, 2026

ci is stalled here, i don't understand why
master pr is #10385

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants