Event adapter refactor to be fork based#10361
Conversation
tbenr
left a comment
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
we should remove this noise left from the original :)
| public class Eip7732TimeBasedEventAdapter extends TimeBasedEventAdapter { | ||
| private static final Logger LOG = LogManager.getLogger(); | ||
|
|
||
| private UInt64 genesisTime; |
There was a problem hiding this comment.
we could have genesisTime in the abstract class
| final ValidatorTimingChannel validatorTimingChannel; | ||
| final Spec spec; | ||
| final RepeatingTaskScheduler taskScheduler; |
There was a problem hiding this comment.
do we need to extend it over package-private?
|
|
||
| public class ForkAwareTimeBasedEventAdapter implements BeaconChainEventAdapter { | ||
|
|
||
| final TimeBasedEventAdapter delegate; |
|
and, obviously, as pointed out by @StefanBratanov , 7732 -> GLOAS |
|
Addressed all feedback |
| final TimeProvider timeProvider, | ||
| final ValidatorTimingChannel validatorTimingChannel, | ||
| final Spec spec) { | ||
| if (spec.isMilestoneSupported(SpecMilestone.EIP7732)) { |
tbenr
left a comment
There was a problem hiding this comment.
LGTM.
nit: some renames on the new test are needed
|
|
||
| final UInt64 nextSlotStartTimeMillis = secondsToMillis(nextSlotStartTime); | ||
|
|
||
| final UInt64 millisPerSlot = secondsToMillis(secondsPerSlot); |
There was a problem hiding this comment.
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)
|
@zilm13 can we raise this PR against master |
|
ci is stalled here, i don't understand why |


PR Description
Fixed Issue(s)
Documentation
doc-change-requiredlabel to this PR if updates are required.Changelog
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
TimeBasedEventAdapterlogic intoPhase0TimeBasedEventAdapterandGloasTimeBasedEventAdapter, selected via a newForkAwareTimeBasedEventAdapterwrapper.GloasTimeBasedEventAdapteradds EIP-7732 (“Gloas”) scheduling, including the new payload timeliness attestation event and explicit transition behavior across the fork boundary, whileTimeBasedEventAdapterbecomes an abstract base providing shared scheduling/utilities. All beacon node API entrypoints now instantiateForkAwareTimeBasedEventAdapter, 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.