From 69a68fae64d675b235d27abba697303ce2ddb10c Mon Sep 17 00:00:00 2001 From: Matilda Clerke Date: Tue, 18 Aug 2026 15:04:23 +1000 Subject: [PATCH 1/4] Implement client side HeadV2Event and listen for head_v2 events in validator clients --- .../EventSourceBeaconChainEventAdapter.java | 1 + .../eventsource/EventSourceHandler.java | 13 ++ .../remote/eventsource/HeadV2Event.java | 131 ++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/HeadV2Event.java 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..5b71309555e 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..e4286a77245 --- /dev/null +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/eventsource/HeadV2Event.java @@ -0,0 +1,131 @@ +/* + * 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 org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.forkchoice.ForkChoicePayloadStatus; + +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; + +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); + } + } +} From 0369dfdbaeefe742eabfa15b75b5c031cf7567fc Mon Sep 17 00:00:00 2001 From: Matilda Clerke Date: Tue, 18 Aug 2026 15:07:16 +1000 Subject: [PATCH 2/4] spotless --- .../eventsource/EventSourceHandler.java | 8 ++-- .../remote/eventsource/HeadV2Event.java | 41 +++++++++---------- 2 files changed, 23 insertions(+), 26 deletions(-) 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 5b71309555e..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 @@ -123,10 +123,10 @@ 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()); + headEvent.slot(), + headEvent.previousDutyDependentRoot(), + headEvent.currentDutyDependentRoot(), + headEvent.block()); if (generateEarlyAttestations) { validatorTimingChannel.onAttestationCreationDue(headEvent.slot()); } 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 index e4286a77245..ed6aed0caa0 100644 --- 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 @@ -13,25 +13,24 @@ package tech.pegasys.teku.validator.remote.eventsource; -import org.apache.tuweni.bytes.Bytes32; -import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition; -import tech.pegasys.teku.infrastructure.unsigned.UInt64; -import tech.pegasys.teku.spec.datastructures.forkchoice.ForkChoicePayloadStatus; - 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) { + 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) @@ -44,26 +43,24 @@ record HeadV2Event( "epoch_transition", BOOLEAN_TYPE, HeadV2Event::epochTransition, - Builder::epochTransition) + Builder::epochTransition) .withField( "previous_duty_dependent_root", BYTES32_TYPE, HeadV2Event::previousDutyDependentRoot, - Builder::previousDutyDependentRoot) + Builder::previousDutyDependentRoot) .withField( "current_duty_dependent_root", BYTES32_TYPE, HeadV2Event::currentDutyDependentRoot, - Builder::currentDutyDependentRoot) + Builder::currentDutyDependentRoot) .withField( "execution_optimistic", BOOLEAN_TYPE, HeadV2Event::executionOptimistic, - Builder::executionOptimistic) - .withField("payload_status", - STRING_TYPE, - HeadV2Event::payloadStatus, - Builder::payloadStatus) + Builder::executionOptimistic) + .withField( + "payload_status", STRING_TYPE, HeadV2Event::payloadStatus, Builder::payloadStatus) .build(); private static class Builder { @@ -125,7 +122,7 @@ HeadV2Event build() { previousDutyDependentRoot, currentDutyDependentRoot, executionOptimistic, - payloadStatus); + payloadStatus); } } } From 15407c099990af7df4af8e1ef7edf7d95bd2f36d Mon Sep 17 00:00:00 2001 From: Matilda Clerke Date: Tue, 18 Aug 2026 16:14:10 +1000 Subject: [PATCH 3/4] Fix EventSourceBeaconChainEventAdapterTest --- .../eventsource/EventSourceBeaconChainEventAdapterTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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..8837fea8fd0 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,8 @@ 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()) From a5f2d1a95ac1bf5ce97b844366f9b36df9909ebb Mon Sep 17 00:00:00 2001 From: Matilda Clerke Date: Wed, 19 Aug 2026 07:26:44 +1000 Subject: [PATCH 4/4] spotless --- .../eventsource/EventSourceBeaconChainEventAdapterTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 8837fea8fd0..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,7 +157,11 @@ public void verifyEventSourceSubscriptionUrl( final HttpUrl endpoint, final boolean shutdownWhenValidatorSlashedEnabled) { Stream eventTypes = shutdownWhenValidatorSlashedEnabled - ? Stream.of(EventType.head, EventType.head_v2, EventType.attester_slashing, EventType.proposer_slashing) + ? Stream.of( + EventType.head, + EventType.head_v2, + EventType.attester_slashing, + EventType.proposer_slashing) : Stream.of(EventType.head, EventType.head_v2); verify(endpoint) .resolve(