diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapter.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapter.java index 710aa122a9e..f09b3dca32f 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapter.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapter.java @@ -145,6 +145,7 @@ BackgroundEventSource createEventSource(final RemoteValidatorApiChannel beaconNo final List eventTypes = new ArrayList<>(); eventTypes.add(EventType.head); + eventTypes.add(EventType.head_v2); if (shutdownWhenValidatorSlashedEnabled) { eventTypes.add(EventType.attester_slashing); eventTypes.add(EventType.proposer_slashing); diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceHandler.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceHandler.java index 37dddf03c28..6be71f3eb5a 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceHandler.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceHandler.java @@ -93,6 +93,7 @@ public void onMessage(final String event, final MessageEvent messageEvent) { final EventType eventType = EventType.valueOf(event); switch (eventType) { case head -> handleHeadEvent(messageEvent.getData()); + case head_v2 -> handleHeadV2Event(messageEvent.getData()); case attester_slashing -> handleAttesterSlashingEvent(messageEvent.getData()); case proposer_slashing -> handleProposerSlashingEvent(messageEvent.getData()); default -> LOG.warn("Received unexpected event type: " + event); @@ -119,6 +120,18 @@ private void handleHeadEvent(final String data) throws JsonProcessingException { } } + private void handleHeadV2Event(final String data) throws JsonProcessingException { + final HeadV2Event headEvent = JsonUtil.parse(data, HeadV2Event.TYPE_DEFINITION); + validatorTimingChannel.onHeadUpdate( + headEvent.slot(), + headEvent.previousDutyDependentRoot(), + headEvent.currentDutyDependentRoot(), + headEvent.block()); + if (generateEarlyAttestations) { + validatorTimingChannel.onAttestationCreationDue(headEvent.slot()); + } + } + private void handleAttesterSlashingEvent(final String data) throws JsonProcessingException { final DeserializableTypeDefinition attesterSlashingTypeDefinition = spec.getGenesisSchemaDefinitions().getAttesterSlashingSchema().getJsonTypeDefinition(); diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/HeadV2Event.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/HeadV2Event.java new file mode 100644 index 00000000000..ed6aed0caa0 --- /dev/null +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/HeadV2Event.java @@ -0,0 +1,128 @@ +/* + * 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.validator.remote.eventsource; + +import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.BOOLEAN_TYPE; +import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.BYTES32_TYPE; +import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.STRING_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.DeserializableTypeDefinition; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; + +record HeadV2Event( + UInt64 slot, + Bytes32 block, + Bytes32 state, + boolean epochTransition, + Bytes32 previousDutyDependentRoot, + Bytes32 currentDutyDependentRoot, + Boolean executionOptimistic, + String payloadStatus) { + + static final DeserializableTypeDefinition TYPE_DEFINITION = + DeserializableTypeDefinition.object(HeadV2Event.class, Builder.class) + .initializer(Builder::new) + .finisher(Builder::build) + .withField("slot", UINT64_TYPE, HeadV2Event::slot, Builder::slot) + .withField("block", BYTES32_TYPE, HeadV2Event::block, Builder::block) + .withField("state", BYTES32_TYPE, HeadV2Event::state, Builder::state) + .withField( + "epoch_transition", + BOOLEAN_TYPE, + HeadV2Event::epochTransition, + Builder::epochTransition) + .withField( + "previous_duty_dependent_root", + BYTES32_TYPE, + HeadV2Event::previousDutyDependentRoot, + Builder::previousDutyDependentRoot) + .withField( + "current_duty_dependent_root", + BYTES32_TYPE, + HeadV2Event::currentDutyDependentRoot, + Builder::currentDutyDependentRoot) + .withField( + "execution_optimistic", + BOOLEAN_TYPE, + HeadV2Event::executionOptimistic, + Builder::executionOptimistic) + .withField( + "payload_status", STRING_TYPE, HeadV2Event::payloadStatus, Builder::payloadStatus) + .build(); + + private static class Builder { + private UInt64 slot; + private Bytes32 block; + private Bytes32 state; + private boolean epochTransition; + private Bytes32 previousDutyDependentRoot; + private Bytes32 currentDutyDependentRoot; + private boolean executionOptimistic; + private String payloadStatus; + + Builder slot(final UInt64 slot) { + this.slot = slot; + return this; + } + + Builder block(final Bytes32 block) { + this.block = block; + return this; + } + + Builder state(final Bytes32 state) { + this.state = state; + return this; + } + + Builder epochTransition(final boolean epochTransition) { + this.epochTransition = epochTransition; + return this; + } + + Builder previousDutyDependentRoot(final Bytes32 previousDutyDependentRoot) { + this.previousDutyDependentRoot = previousDutyDependentRoot; + return this; + } + + Builder currentDutyDependentRoot(final Bytes32 currentDutyDependentRoot) { + this.currentDutyDependentRoot = currentDutyDependentRoot; + return this; + } + + Builder executionOptimistic(final boolean executionOptimistic) { + this.executionOptimistic = executionOptimistic; + return this; + } + + Builder payloadStatus(final String payloadStatus) { + this.payloadStatus = payloadStatus; + return this; + } + + HeadV2Event build() { + return new HeadV2Event( + slot, + block, + state, + epochTransition, + previousDutyDependentRoot, + currentDutyDependentRoot, + executionOptimistic, + payloadStatus); + } + } +} diff --git a/validator/remote/src/test/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapterTest.java b/validator/remote/src/test/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapterTest.java index 5dd01118127..5743c801a30 100644 --- a/validator/remote/src/test/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapterTest.java +++ b/validator/remote/src/test/java/tech/pegasys/teku/validator/remote/eventsource/EventSourceBeaconChainEventAdapterTest.java @@ -157,8 +157,12 @@ public void verifyEventSourceSubscriptionUrl( final HttpUrl endpoint, final boolean shutdownWhenValidatorSlashedEnabled) { Stream eventTypes = shutdownWhenValidatorSlashedEnabled - ? Stream.of(EventType.head, EventType.attester_slashing, EventType.proposer_slashing) - : Stream.of(EventType.head); + ? Stream.of( + EventType.head, + EventType.head_v2, + EventType.attester_slashing, + EventType.proposer_slashing) + : Stream.of(EventType.head, EventType.head_v2); verify(endpoint) .resolve( ValidatorApiMethod.EVENTS.getPath(emptyMap())