Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

### Additions and Improvements

- Implemented PostPtcDuties rest api endpoint (gloas api).
- Added `/eth/v2/node/version` endpoint to retrieve structured version information for both beacon node and execution client.
- Added deprecation warning on startup for any leveldb database types.
- Increased default timeout of Engine API Get Payload requests to 2 seconds.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Optional<PtcDuties>> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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 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,
"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"
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -308,6 +309,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())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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.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;
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 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.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 could occur, "
Comment thread
rolfyone marked this conversation as resolved.
+ "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\n"
Comment thread
rolfyone marked this conversation as resolved.
+ "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.")
Comment thread
rolfyone marked this conversation as resolved.
.tags(TAG_VALIDATOR, TAG_VALIDATOR_REQUIRED)
.requestBodyType(BODY_INTEGER_LIST)
.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<Integer> requestBody = request.getRequestBody();
final IntList indices = IntArrayList.toList(requestBody.stream().mapToInt(Integer::intValue));

final SafeFuture<Optional<PtcDuties>> future =
validatorDataProvider.getPtcDuties(epoch, indices);

request.respondAsync(
future.thenApply(
attesterDuties ->
attesterDuties
.map(AsyncApiResponse::respondOk)
.orElse(AsyncApiResponse.respondServiceUnavailable())));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -204,6 +205,10 @@ public SafeFuture<Optional<AttesterDuties>> getAttesterDuties(
return SafeFuture.of(() -> validatorApiChannel.getAttestationDuties(epoch, indices));
}

public SafeFuture<Optional<PtcDuties>> getPtcDuties(final UInt64 epoch, final IntList indices) {
return SafeFuture.of(() -> validatorApiChannel.getPtcDuties(epoch, indices));
}

public SafeFuture<Optional<ProposerDuties>> getProposerDuties(final UInt64 epoch) {
return SafeFuture.of(() -> validatorApiChannel.getProposerDuties(epoch, true));
}
Expand Down
Loading