Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -9,5 +9,6 @@
### Breaking Changes

### Additions and Improvements
- Added beacon-api `/eth/v1/beacon/states/{state_id}/pending_partial_withdrawals` endpoint for use post-electra.

### Bug Fixes
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Consensys Software Inc., 2025
*
* 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.beacon;

import static org.assertj.core.api.Assertions.assertThat;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION;

import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import okhttp3.Response;
import org.junit.jupiter.api.Test;
import tech.pegasys.teku.api.schema.Version;
import tech.pegasys.teku.beaconrestapi.AbstractDataBackedRestAPIIntegrationTest;
import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.GetStatePendingPartialWithdrawals;
import tech.pegasys.teku.infrastructure.json.JsonTestUtil;
import tech.pegasys.teku.spec.SpecMilestone;

public class GetStatePendingPartialWithdrawalsIntegrationTest
extends AbstractDataBackedRestAPIIntegrationTest {
@Test
public void shouldGetElectraDepositsJson() throws Exception {
startRestAPIAtGenesis(SpecMilestone.ELECTRA);
createBlocksAtSlots(10);
final Response response = get("head");

final String responseText = response.body().string();
final JsonNode node = JsonTestUtil.parseAsJsonNode(responseText);
assertThat(node.get("version").asText()).isEqualTo("electra");
assertThat(node.get("execution_optimistic").asBoolean()).isFalse();
assertThat(node.get("finalized").asBoolean()).isFalse();
assertThat(node.get("data").size()).isEqualTo(0);
assertThat(response.header(HEADER_CONSENSUS_VERSION)).isEqualTo(Version.electra.name());
}

public Response get(final String stateId) throws IOException {
return getResponse(GetStatePendingPartialWithdrawals.ROUTE.replace("{state_id}", stateId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
{
"get" : {
"tags" : [ "Beacon", "Experimental" ],
"operationId" : "getPendingPartialWithdrawals",
"summary" : "Get pending partial withdrawals from state",
"description" : "Returns pending partial withdrawals for state with given 'stateId'. Should return 400 if requested before electra.",
"parameters" : [ {
"name" : "state_id",
"required" : true,
"in" : "path",
"schema" : {
"type" : "string",
"description" : "State identifier. Can be one of: \"head\" (canonical head in node's view), \"genesis\", \"finalized\", \"justified\", <slot>, <hex encoded stateRoot with 0x prefix>.",
"example" : "head"
}
} ],
"responses" : {
"200" : {
"description" : "Request successful",
"headers" : {
"Eth-Consensus-Version" : {
"description" : "Required in response so client can deserialize returned json or ssz data more effectively.",
"required" : true,
"schema" : {
"type" : "string",
"enum" : [ "phase0", "altair", "bellatrix", "capella", "deneb", "electra" ],
"example" : "phase0"
}
}
},
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/GetPendingPartialWithdrawalsResponse"
}
},
"application/octet-stream" : {
"schema" : {
"type" : "string",
"format" : "binary"
}
}
}
},
"404" : {
"description" : "Not found",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/HttpErrorResponse"
}
}
}
},
"415" : {
"description" : "Unsupported media type",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/HttpErrorResponse"
}
}
}
},
"503" : {
"description" : "Service unavailable",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/HttpErrorResponse"
}
}
}
},
"204" : {
"description" : "Data is unavailable because the chain has not yet reached genesis",
"content" : { }
},
"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,23 @@
{
"title" : "GetPendingPartialWithdrawalsResponse",
"type" : "object",
"required" : [ "version", "execution_optimistic", "finalized", "data" ],
"properties" : {
"version" : {
"type" : "string",
"enum" : [ "phase0", "altair", "bellatrix", "capella", "deneb", "electra" ]
},
"execution_optimistic" : {
"type" : "boolean"
},
"finalized" : {
"type" : "boolean"
},
"data" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/PendingPartialWithdrawal"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.GetStateCommittees;
import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.GetStateFinalityCheckpoints;
import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.GetStateFork;
import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.GetStatePendingPartialWithdrawals;
import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.GetStateRandao;
import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.GetStateRoot;
import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.GetStateSyncCommittees;
Expand Down Expand Up @@ -251,6 +252,7 @@ private static RestApi create(
.endpoint(new PostVoluntaryExit(dataProvider))
.endpoint(new PostSyncCommittees(dataProvider))
.endpoint(new PostValidatorLiveness(dataProvider))
.endpoint(new GetStatePendingPartialWithdrawals(dataProvider, schemaCache))
.endpoint(new GetDepositSnapshot(eth1DataProvider))
// Event Handler
.endpoint(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright Consensys Software Inc., 2025
*
* 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.beacon;

import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.PARAMETER_STATE_ID;
import static tech.pegasys.teku.ethereum.json.types.EthereumTypes.ETH_CONSENSUS_HEADER_TYPE;
import static tech.pegasys.teku.ethereum.json.types.EthereumTypes.MILESTONE_TYPE;
import static tech.pegasys.teku.ethereum.json.types.EthereumTypes.sszResponseType;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.EXECUTION_OPTIMISTIC;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.FINALIZED;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_BEACON;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_EXPERIMENTAL;
import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.BOOLEAN_TYPE;
import static tech.pegasys.teku.infrastructure.json.types.SerializableTypeDefinition.listOf;

import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.List;
import java.util.Optional;
import tech.pegasys.teku.api.ChainDataProvider;
import tech.pegasys.teku.api.DataProvider;
import tech.pegasys.teku.api.schema.Version;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.json.types.SerializableTypeDefinition;
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.ssz.SszList;
import tech.pegasys.teku.spec.SpecMilestone;
import tech.pegasys.teku.spec.datastructures.metadata.ObjectAndMetaData;
import tech.pegasys.teku.spec.datastructures.state.versions.electra.PendingPartialWithdrawal;
import tech.pegasys.teku.spec.schemas.SchemaDefinitionCache;
import tech.pegasys.teku.spec.schemas.SchemaDefinitionsElectra;

public class GetStatePendingPartialWithdrawals extends RestApiEndpoint {
public static final String ROUTE = "/eth/v1/beacon/states/{state_id}/pending_partial_withdrawals";

private final ChainDataProvider chainDataProvider;

public GetStatePendingPartialWithdrawals(
final DataProvider dataProvider, final SchemaDefinitionCache schemaDefinitionCache) {
this(dataProvider.getChainDataProvider(), schemaDefinitionCache);
}

GetStatePendingPartialWithdrawals(
final ChainDataProvider provider, final SchemaDefinitionCache schemaDefinitionCache) {
super(
EndpointMetadata.get(ROUTE)
.operationId("getPendingPartialWithdrawals")
.summary("Get pending partial withdrawals from state")
.description(
"Returns pending partial withdrawals for state with given 'stateId'. Should return 400 if requested before electra.")
.pathParam(PARAMETER_STATE_ID)
.tags(TAG_BEACON, TAG_EXPERIMENTAL)
.response(
SC_OK,
"Request successful",
getResponseType(schemaDefinitionCache),
sszResponseType(),
ETH_CONSENSUS_HEADER_TYPE)
.withNotFoundResponse()
.withUnsupportedMediaTypeResponse()
.withChainDataResponses()
.build());
this.chainDataProvider = provider;
}

@Override
public void handleRequest(final RestApiRequest request) throws JsonProcessingException {

final SafeFuture<Optional<ObjectAndMetaData<SszList<PendingPartialWithdrawal>>>> future =
chainDataProvider.getPendingPartialWithdrawals(
request.getPathParameter(PARAMETER_STATE_ID));

request.respondAsync(
future.thenApply(
maybeData ->
maybeData
.map(
objectAndMetadata -> {
request.header(
HEADER_CONSENSUS_VERSION,
Version.fromMilestone(objectAndMetadata.getMilestone()).name());
return AsyncApiResponse.respondOk(objectAndMetadata);
})
.orElseGet(AsyncApiResponse::respondNotFound)));
}

private static SerializableTypeDefinition<ObjectAndMetaData<List<PendingPartialWithdrawal>>>
getResponseType(final SchemaDefinitionCache schemaDefinitionCache) {
final SchemaDefinitionsElectra schemaDefinitionsElectra =
schemaDefinitionCache
.getSchemaDefinition(SpecMilestone.ELECTRA)
.toVersionElectra()
.orElseThrow();

final SerializableTypeDefinition<PendingPartialWithdrawal> pendingPartialWithdrawalType =
schemaDefinitionsElectra.getPendingPartialWithdrawalSchema().getJsonTypeDefinition();

return SerializableTypeDefinition.<ObjectAndMetaData<List<PendingPartialWithdrawal>>>object()
.name("GetPendingPartialWithdrawalsResponse")
.withField("version", MILESTONE_TYPE, ObjectAndMetaData::getMilestone)
.withField(EXECUTION_OPTIMISTIC, BOOLEAN_TYPE, ObjectAndMetaData::isExecutionOptimistic)
.withField(FINALIZED, BOOLEAN_TYPE, ObjectAndMetaData::isFinalized)
.withField("data", listOf(pendingPartialWithdrawalType), ObjectAndMetaData::getData)
.build();
}
}
Loading