From a53ca021b68a4b2ecc58e2e9308b9a5feb8c4e82 Mon Sep 17 00:00:00 2001 From: Paul Harris Date: Tue, 3 Mar 2026 14:24:21 +1000 Subject: [PATCH 1/4] Implemented PostPtcDuties api endpoint fixes #10405 --- CHANGELOG.md | 3 +- .../PostPtcDutiesIntegrationTest.java | 88 +++++++++++++ .../_eth_v1_validator_duties_ptc_{epoch}.json | 81 ++++++++++++ .../beacon/schema/GetPtcDutiesResponse.json | 22 ++++ .../beaconrestapi/beacon/schema/PtcDuty.json | 25 ++++ .../JsonTypeDefinitionBeaconRestApi.java | 2 + .../handlers/v1/validator/PostPtcDuties.java | 117 ++++++++++++++++++ .../teku/api/ValidatorDataProvider.java | 5 + 8 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 data/beaconrestapi/src/integration-test/java/tech/pegasys/teku/beaconrestapi/v1/validator/PostPtcDutiesIntegrationTest.java create mode 100644 data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_validator_duties_ptc_{epoch}.json create mode 100644 data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/GetPtcDutiesResponse.json create mode 100644 data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PtcDuty.json create mode 100644 data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostPtcDuties.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e71c14c29a1..c96c6fd1ed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ ### Additions and Improvements - Use jemalloc in our docker images to improve memory allocation -- Nodes with >50% custody requirements will be able to import blocks after downloading 50% of the sidecars, and the remaining sidecars will be handled as a background task. This includes nodes servicing validators in excess of 2048 eth effective balance, as well as voluntary supernodes. +- Nodes with >50% custody requirements will be able to import blocks after downloading 50% of the sidecars, and the remaining sidecars will be handled as a background task. This includes nodes servicing validators in excess of 2048 eth effective balance, as well as voluntary supernodes. +- Implemented PostPtcDuties rest api endpoint (gloas api). ### Bug Fixes diff --git a/data/beaconrestapi/src/integration-test/java/tech/pegasys/teku/beaconrestapi/v1/validator/PostPtcDutiesIntegrationTest.java b/data/beaconrestapi/src/integration-test/java/tech/pegasys/teku/beaconrestapi/v1/validator/PostPtcDutiesIntegrationTest.java new file mode 100644 index 00000000000..f0e25de7c3a --- /dev/null +++ b/data/beaconrestapi/src/integration-test/java/tech/pegasys/teku/beaconrestapi/v1/validator/PostPtcDutiesIntegrationTest.java @@ -0,0 +1,88 @@ +/* + * 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.v1.validator; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static tech.pegasys.teku.ethereum.json.types.validator.PtcDuties.PTC_DUTIES_TYPE_DEFINITION; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_SERVICE_UNAVAILABLE; +import static tech.pegasys.teku.infrastructure.json.JsonUtil.parse; +import static tech.pegasys.teku.infrastructure.unsigned.UInt64.ONE; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import okhttp3.Response; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.beacon.sync.events.SyncState; +import tech.pegasys.teku.beaconrestapi.AbstractDataBackedRestAPIIntegrationTest; +import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostPtcDuties; +import tech.pegasys.teku.ethereum.json.types.validator.PtcDuties; +import tech.pegasys.teku.ethereum.json.types.validator.PtcDuty; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +public class PostPtcDutiesIntegrationTest extends AbstractDataBackedRestAPIIntegrationTest { + @Test + void shouldErrorIfPriorToGloas() throws IOException { + startRestAPIAtGenesis(SpecMilestone.FULU); + + when(syncService.getCurrentSyncState()).thenReturn(SyncState.IN_SYNC); + + final Response response = post(PostPtcDuties.ROUTE.replace("{epoch}", "1"), ""); + + assertThat(response.code()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.body().string()).contains("prior to gloas"); + } + + @Test + void shouldErrorIfSyncing() throws IOException { + startRestAPIAtGenesis(SpecMilestone.GLOAS); + when(syncService.getCurrentSyncState()).thenReturn(SyncState.SYNCING); + final Response response = post(PostPtcDuties.ROUTE.replace("{epoch}", "1"), ""); + + assertThat(response.code()).isEqualTo(SC_SERVICE_UNAVAILABLE); + assertThat(response.body().string()).contains("syncing"); + } + + @Test + void shouldUsePreviousDependentRootForCurrentEpochDuties() throws IOException { + startRestAPIAtGenesis(SpecMilestone.GLOAS); + final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); + when(syncService.getCurrentSyncState()).thenReturn(SyncState.IN_SYNC); + final Bytes32 dependentRoot = dataStructureUtil.randomBytes32(); + final PtcDuty duty = new PtcDuty(VALIDATOR_KEYS.get(1).getPublicKey(), ONE, UInt64.valueOf(13)); + final SafeFuture> out = + SafeFuture.completedFuture(Optional.of(new PtcDuties(false, dependentRoot, List.of(duty)))); + when(validatorApiChannel.getPtcDuties(eq(ONE), any())).thenReturn(out); + + final Response response = post(PostPtcDuties.ROUTE.replace("{epoch}", "1"), "[1]"); + final String responseBody = response.body().string(); + assertThat(responseBody).isNotEmpty(); + assertThat(response.code()).isEqualTo(SC_OK); + + final PtcDuties duties = parse(responseBody, PTC_DUTIES_TYPE_DEFINITION); + + assertThat(duties.dependentRoot()).isEqualTo(dependentRoot); + assertThat(duties.executionOptimistic()).isFalse(); + assertThat(duties.duties().getFirst()).isEqualTo(duty); + } +} diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_validator_duties_ptc_{epoch}.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_validator_duties_ptc_{epoch}.json new file mode 100644 index 00000000000..261ba90adc0 --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_validator_duties_ptc_{epoch}.json @@ -0,0 +1,81 @@ +{ + "post" : { + "tags" : [ "Validator", "Validator Required Api" ], + "operationId" : "getPtcDuties", + "summary" : "Get PTC duties", + "description" : "Requests the beacon node to provide a set of Payload Timeliness Committee (PTC) duties, which should be performed by validators, for a particular epoch. Duties should only need to be checked once per epoch, however a chain reorganization (of > MIN_SEED_LOOKAHEAD epochs) could occur, resulting in a change of duties. For full safety, you should monitor head events and confirm the dependent root in this response matches:\n\n - event.previous_duty_dependent_root when compute_epoch_at_slot(event.slot) == epoch\n - event.current_duty_dependent_root when compute_epoch_at_slot(event.slot) + 1 == epoch\n - event.block otherwise\n\nThe dependent_root value is get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1) or the genesis block root in the case of underflow.", + "parameters" : [ { + "name" : "epoch", + "required" : true, + "in" : "path", + "schema" : { + "type" : "string", + "description" : "`uint64` Epoch number to query.", + "example" : "1", + "format" : "uint64" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "array", + "minItems" : 1, + "items" : { + "type" : "string", + "description" : "integer string", + "example" : "1", + "format" : "integer" + } + } + } + } + }, + "responses" : { + "200" : { + "description" : "Success response", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/GetPtcDutiesResponse" + } + } + } + }, + "204" : { + "description" : "Data is unavailable because the chain has not yet reached genesis", + "content" : { } + }, + "503" : { + "description" : "Beacon node is currently syncing and not serving requests.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HttpErrorResponse" + } + } + } + }, + "400" : { + "description" : "The request could not be processed, check the response for more information.", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HttpErrorResponse" + } + } + } + }, + "500" : { + "description" : "Internal server error", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/HttpErrorResponse" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/GetPtcDutiesResponse.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/GetPtcDutiesResponse.json new file mode 100644 index 00000000000..a54df278809 --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/GetPtcDutiesResponse.json @@ -0,0 +1,22 @@ +{ + "title" : "GetPtcDutiesResponse", + "type" : "object", + "required" : [ "dependent_root", "execution_optimistic", "data" ], + "properties" : { + "dependent_root" : { + "type" : "string", + "description" : "Bytes32 hexadecimal", + "example" : "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "format" : "byte" + }, + "execution_optimistic" : { + "type" : "boolean" + }, + "data" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PtcDuty" + } + } + } +} \ No newline at end of file diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PtcDuty.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PtcDuty.json new file mode 100644 index 00000000000..44002698e0b --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/schema/PtcDuty.json @@ -0,0 +1,25 @@ +{ + "title" : "PtcDuty", + "type" : "object", + "required" : [ "pubkey", "validator_index", "slot" ], + "properties" : { + "pubkey" : { + "type" : "string", + "description" : "`BLSPublicKey Hex` The validator's BLS public key, uniquely identifying them. 48-bytes, hex encoded with 0x prefix, case insensitive.", + "example" : "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "format" : "string" + }, + "validator_index" : { + "type" : "string", + "description" : "unsigned 64 bit integer", + "example" : "1", + "format" : "uint64" + }, + "slot" : { + "type" : "string", + "description" : "unsigned 64 bit integer", + "example" : "1", + "format" : "uint64" + } + } +} \ No newline at end of file diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java index 870d55079d9..519b7e55182 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java @@ -104,6 +104,7 @@ import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostBeaconCommitteeSelections; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostContributionAndProofs; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostPrepareBeaconProposer; +import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostPtcDuties; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostRegisterValidator; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostSubscribeToBeaconCommitteeSubnet; import tech.pegasys.teku.beaconrestapi.handlers.v1.validator.PostSyncCommitteeSelections; @@ -304,6 +305,7 @@ private static RestApi create( .endpoint(new PostContributionAndProofs(dataProvider, schemaCache)) .endpoint(new PostPrepareBeaconProposer(dataProvider)) .endpoint(new PostRegisterValidator(dataProvider)) + .endpoint(new PostPtcDuties(dataProvider, spec)) // Obol DVT Methods .endpoint(new PostBeaconCommitteeSelections()) .endpoint(new PostSyncCommitteeSelections()) diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostPtcDuties.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostPtcDuties.java new file mode 100644 index 00000000000..b8b63df200e --- /dev/null +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostPtcDuties.java @@ -0,0 +1,117 @@ +/* + * 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.validator; + +import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.EPOCH_PARAMETER; +import static tech.pegasys.teku.ethereum.json.types.validator.PtcDuties.PTC_DUTIES_TYPE_DEFINITION; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NO_CONTENT; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_SERVICE_UNAVAILABLE; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_VALIDATOR; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_VALIDATOR_REQUIRED; +import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.INTEGER_TYPE; + +import com.fasterxml.jackson.core.JsonProcessingException; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import java.util.List; +import java.util.Optional; +import tech.pegasys.teku.api.DataProvider; +import tech.pegasys.teku.api.SyncDataProvider; +import tech.pegasys.teku.api.ValidatorDataProvider; +import tech.pegasys.teku.ethereum.json.types.validator.PtcDuties; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition; +import tech.pegasys.teku.infrastructure.restapi.endpoints.AsyncApiResponse; +import tech.pegasys.teku.infrastructure.restapi.endpoints.EndpointMetadata; +import tech.pegasys.teku.infrastructure.restapi.endpoints.RestApiEndpoint; +import tech.pegasys.teku.infrastructure.restapi.endpoints.RestApiRequest; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.SpecMilestone; + +public class PostPtcDuties extends RestApiEndpoint { + public static final String ROUTE = "/eth/v1/validator/duties/ptc/{epoch}"; + private final ValidatorDataProvider validatorDataProvider; + private final SyncDataProvider syncDataProvider; + private final Spec spec; + + public PostPtcDuties(final DataProvider dataProvider, final Spec spec) { + this(dataProvider.getSyncDataProvider(), spec, dataProvider.getValidatorDataProvider()); + } + + PostPtcDuties( + final SyncDataProvider syncDataProvider, + final Spec spec, + final ValidatorDataProvider validatorDataProvider) { + super( + EndpointMetadata.post(ROUTE) + .operationId("getPtcDuties") + .summary("Get PTC duties") + .description( + "Requests the beacon node to provide a set of Payload Timeliness Committee (PTC) duties, which " + + "should be performed by validators, for a particular epoch. Duties should only need to be " + + "checked once per epoch, however a chain reorganization (of > MIN_SEED_LOOKAHEAD epochs) could occur, " + + "resulting in a change of duties. For full safety, you should monitor head events and confirm the " + + "dependent root in this response matches:\n\n" + + " - event.previous_duty_dependent_root when compute_epoch_at_slot(event.slot) == epoch\n" + + " - event.current_duty_dependent_root when compute_epoch_at_slot(event.slot) + 1 == epoch\n" + + " - event.block otherwise\n\n" + + "The dependent_root value is get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1) " + + "or the genesis block root in the case of underflow.") + .tags(TAG_VALIDATOR, TAG_VALIDATOR_REQUIRED) + .requestBodyType( + DeserializableTypeDefinition.listOf(INTEGER_TYPE, Optional.of(1), Optional.empty())) + .pathParam(EPOCH_PARAMETER) + .response(SC_OK, "Success response", PTC_DUTIES_TYPE_DEFINITION) + .response( + SC_NO_CONTENT, "Data is unavailable because the chain has not yet reached genesis") + .withServiceUnavailableResponse() + .build()); + this.syncDataProvider = syncDataProvider; + this.spec = spec; + this.validatorDataProvider = validatorDataProvider; + } + + @Override + public void handleRequest(final RestApiRequest request) throws JsonProcessingException { + if (!validatorDataProvider.isStoreAvailable() || syncDataProvider.isSyncing()) { + request.respondError( + SC_SERVICE_UNAVAILABLE, "Beacon node is currently syncing and not serving requests."); + return; + } + + final UInt64 epoch = request.getPathParameter(EPOCH_PARAMETER); + + if (spec.atEpoch(epoch).getMilestone().isLessThan(SpecMilestone.GLOAS)) { + request.respondError( + SC_BAD_REQUEST, "Cannot request PTC duties for epochs prior to gloas fork."); + return; + } + + final List requestBody = request.getRequestBody(); + final IntList indices = IntArrayList.toList(requestBody.stream().mapToInt(Integer::intValue)); + + final SafeFuture> future = + validatorDataProvider.getPtcDuties(epoch, indices); + + request.respondAsync( + future.thenApply( + attesterDuties -> + attesterDuties + .map(AsyncApiResponse::respondOk) + .orElse(AsyncApiResponse.respondServiceUnavailable()))); + } +} diff --git a/data/provider/src/main/java/tech/pegasys/teku/api/ValidatorDataProvider.java b/data/provider/src/main/java/tech/pegasys/teku/api/ValidatorDataProvider.java index 69fcf64a6e3..cb049b65e08 100644 --- a/data/provider/src/main/java/tech/pegasys/teku/api/ValidatorDataProvider.java +++ b/data/provider/src/main/java/tech/pegasys/teku/api/ValidatorDataProvider.java @@ -23,6 +23,7 @@ import tech.pegasys.teku.bls.BLSSignature; import tech.pegasys.teku.ethereum.json.types.validator.AttesterDuties; import tech.pegasys.teku.ethereum.json.types.validator.ProposerDuties; +import tech.pegasys.teku.ethereum.json.types.validator.PtcDuties; import tech.pegasys.teku.ethereum.json.types.validator.SyncCommitteeDuties; import tech.pegasys.teku.ethereum.json.types.validator.SyncCommitteeSubnetSubscription; import tech.pegasys.teku.infrastructure.async.SafeFuture; @@ -198,6 +199,10 @@ public SafeFuture> getAttesterDuties( return SafeFuture.of(() -> validatorApiChannel.getAttestationDuties(epoch, indices)); } + public SafeFuture> getPtcDuties(final UInt64 epoch, final IntList indices) { + return SafeFuture.of(() -> validatorApiChannel.getPtcDuties(epoch, indices)); + } + public SafeFuture> getProposerDuties(final UInt64 epoch) { return SafeFuture.of(() -> validatorApiChannel.getProposerDuties(epoch, true)); } From 2484ef51b7c066fc3ff0223d36ee14adaf856b58 Mon Sep 17 00:00:00 2001 From: Paul Harris Date: Tue, 17 Mar 2026 06:56:40 +1000 Subject: [PATCH 2/4] updated description ptc duties can no longer lookahead, with PR https://github.com/ethereum/beacon-APIs/pull/586 - updated description. --- .../beaconrestapi/handlers/v1/validator/PostPtcDuties.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostPtcDuties.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostPtcDuties.java index b8b63df200e..a5838ad0ac6 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostPtcDuties.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostPtcDuties.java @@ -63,11 +63,10 @@ public PostPtcDuties(final DataProvider dataProvider, final Spec spec) { .description( "Requests the beacon node to provide a set of Payload Timeliness Committee (PTC) duties, which " + "should be performed by validators, for a particular epoch. Duties should only need to be " - + "checked once per epoch, however a chain reorganization (of > MIN_SEED_LOOKAHEAD epochs) could occur, " + + "checked once per epoch, however a chain reorganization could occur, " + "resulting in a change of duties. For full safety, you should monitor head events and confirm the " + "dependent root in this response matches:\n\n" + " - event.previous_duty_dependent_root when compute_epoch_at_slot(event.slot) == epoch\n" - + " - event.current_duty_dependent_root when compute_epoch_at_slot(event.slot) + 1 == epoch\n" + " - event.block otherwise\n\n" + "The dependent_root value is get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1) " + "or the genesis block root in the case of underflow.") From e9365bc53782fa41433b3ee802edd0ff8353a0c4 Mon Sep 17 00:00:00 2001 From: Paul Harris Date: Tue, 17 Mar 2026 11:10:44 +1000 Subject: [PATCH 3/4] fixed integration test after descriptions changed --- .../beacon/paths/_eth_v1_validator_duties_ptc_{epoch}.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_validator_duties_ptc_{epoch}.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_validator_duties_ptc_{epoch}.json index 261ba90adc0..f2fc9b639a6 100644 --- a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_validator_duties_ptc_{epoch}.json +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_eth_v1_validator_duties_ptc_{epoch}.json @@ -3,7 +3,7 @@ "tags" : [ "Validator", "Validator Required Api" ], "operationId" : "getPtcDuties", "summary" : "Get PTC duties", - "description" : "Requests the beacon node to provide a set of Payload Timeliness Committee (PTC) duties, which should be performed by validators, for a particular epoch. Duties should only need to be checked once per epoch, however a chain reorganization (of > MIN_SEED_LOOKAHEAD epochs) could occur, resulting in a change of duties. For full safety, you should monitor head events and confirm the dependent root in this response matches:\n\n - event.previous_duty_dependent_root when compute_epoch_at_slot(event.slot) == epoch\n - event.current_duty_dependent_root when compute_epoch_at_slot(event.slot) + 1 == epoch\n - event.block otherwise\n\nThe dependent_root value is get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1) or the genesis block root in the case of underflow.", + "description" : "Requests the beacon node to provide a set of Payload Timeliness Committee (PTC) duties, which should be performed by validators, for a particular epoch. Duties should only need to be checked once per epoch, however a chain reorganization could occur, resulting in a change of duties. For full safety, you should monitor head events and confirm the dependent root in this response matches:\n\n - event.previous_duty_dependent_root when compute_epoch_at_slot(event.slot) == epoch\n - event.block otherwise\n\nThe dependent_root value is get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1) or the genesis block root in the case of underflow.", "parameters" : [ { "name" : "epoch", "required" : true, From 422be9c3e306b21ca279ea016cc3afe252c3170a Mon Sep 17 00:00:00 2001 From: Paul Harris Date: Tue, 17 Mar 2026 13:20:58 +1000 Subject: [PATCH 4/4] review feedback --- .../beaconrestapi/handlers/v1/validator/PostPtcDuties.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostPtcDuties.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostPtcDuties.java index a5838ad0ac6..9001d7eae39 100644 --- a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostPtcDuties.java +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/v1/validator/PostPtcDuties.java @@ -14,6 +14,7 @@ package tech.pegasys.teku.beaconrestapi.handlers.v1.validator; import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.EPOCH_PARAMETER; +import static tech.pegasys.teku.ethereum.json.types.SharedApiTypes.BODY_INTEGER_LIST; import static tech.pegasys.teku.ethereum.json.types.validator.PtcDuties.PTC_DUTIES_TYPE_DEFINITION; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_NO_CONTENT; @@ -21,7 +22,6 @@ import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_SERVICE_UNAVAILABLE; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_VALIDATOR; import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_VALIDATOR_REQUIRED; -import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.INTEGER_TYPE; import com.fasterxml.jackson.core.JsonProcessingException; import it.unimi.dsi.fastutil.ints.IntArrayList; @@ -33,7 +33,6 @@ import tech.pegasys.teku.api.ValidatorDataProvider; import tech.pegasys.teku.ethereum.json.types.validator.PtcDuties; import tech.pegasys.teku.infrastructure.async.SafeFuture; -import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition; import tech.pegasys.teku.infrastructure.restapi.endpoints.AsyncApiResponse; import tech.pegasys.teku.infrastructure.restapi.endpoints.EndpointMetadata; import tech.pegasys.teku.infrastructure.restapi.endpoints.RestApiEndpoint; @@ -71,8 +70,7 @@ public PostPtcDuties(final DataProvider dataProvider, final Spec spec) { + "The dependent_root value is get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1) " + "or the genesis block root in the case of underflow.") .tags(TAG_VALIDATOR, TAG_VALIDATOR_REQUIRED) - .requestBodyType( - DeserializableTypeDefinition.listOf(INTEGER_TYPE, Optional.of(1), Optional.empty())) + .requestBodyType(BODY_INTEGER_LIST) .pathParam(EPOCH_PARAMETER) .response(SC_OK, "Success response", PTC_DUTIES_TYPE_DEFINITION) .response(