diff --git a/CHANGELOG.md b/CHANGELOG.md index a9b4c981a12..0e55efb3c2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,5 +17,6 @@ - Added jdk 24 docker image build. - Improved performance when scheduling attestations in the beginning of the epoch for a large number of validators. - Improved configuration loading to use builtin configurations to default any fields we need that were missing from a passed in configuration. +- Add `/teku/v1/admin/add_peer` endpoint to allow adding static peers via the REST API. ### Bug Fixes \ No newline at end of file diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_teku_v1_admin_add_peer.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_teku_v1_admin_add_peer.json new file mode 100644 index 00000000000..8629f785ec5 --- /dev/null +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/beacon/paths/_teku_v1_admin_add_peer.json @@ -0,0 +1,44 @@ +{ + "post" : { + "tags" : [ "Teku" ], + "operationId" : "AddPeer", + "summary" : "Add a static peer to the node", + "description" : "Add a static peer to the node passing a multiaddress.", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "string", + "description" : "Multiaddress of the peer to add" + } + } + } + }, + "responses" : { + "200" : { + "description" : "Peer added successfully", + "content" : { } + }, + "400" : { + "description" : "Invalid peer address", + "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/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/JsonTypeDefinitionBeaconRestApi.java index 54ae6ded277..71daf8407d6 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 @@ -25,6 +25,7 @@ import tech.pegasys.teku.beaconrestapi.addon.CapellaRestApiBuilderAddon; import tech.pegasys.teku.beaconrestapi.addon.DenebRestApiBuilderAddon; import tech.pegasys.teku.beaconrestapi.addon.LightClientRestApiBuilderAddon; +import tech.pegasys.teku.beaconrestapi.handlers.tekuv1.admin.AddPeer; import tech.pegasys.teku.beaconrestapi.handlers.tekuv1.admin.Liveness; import tech.pegasys.teku.beaconrestapi.handlers.tekuv1.admin.PutLogLevel; import tech.pegasys.teku.beaconrestapi.handlers.tekuv1.admin.Readiness; @@ -322,7 +323,8 @@ private static RestApi create( .endpoint(new GetEth1VotingSummary(dataProvider, eth1DataProvider)) .endpoint(new GetGlobalValidatorInclusion(dataProvider)) .endpoint(new GetFinalizedStateSlotBefore(dataProvider)) - .endpoint(new GetValidatorInclusion(dataProvider)); + .endpoint(new GetValidatorInclusion(dataProvider)) + .endpoint(new AddPeer(dataProvider)); builder = applyAddons(builder, config, spec, dataProvider, schemaCache); return builder.build(); diff --git a/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/tekuv1/admin/AddPeer.java b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/tekuv1/admin/AddPeer.java new file mode 100644 index 00000000000..8552740dbec --- /dev/null +++ b/data/beaconrestapi/src/main/java/tech/pegasys/teku/beaconrestapi/handlers/tekuv1/admin/AddPeer.java @@ -0,0 +1,78 @@ +/* + * 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.tekuv1.admin; + +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_INTERNAL_SERVER_ERROR; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_TEKU; +import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.STRING_TYPE; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.Optional; +import tech.pegasys.teku.api.DataProvider; +import tech.pegasys.teku.api.NetworkDataProvider; +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.networking.p2p.discovery.DiscoveryNetwork; + +public class AddPeer extends RestApiEndpoint { + + public static final String ROUTE = "/teku/v1/admin/add_peer"; + final NetworkDataProvider networkDataProvider; + + public AddPeer(final DataProvider dataProvider) { + this(dataProvider.getNetworkDataProvider()); + } + + AddPeer(final NetworkDataProvider networkDataProvider) { + super( + EndpointMetadata.post(ROUTE) + .operationId("AddPeer") + .summary("Add a static peer to the node") + .description("Add a static peer to the node passing a multiaddress.") + .tags(TAG_TEKU) + .requestBodyType(STRING_TYPE.withDescription("Multiaddress of the peer to add")) + .response(SC_OK, "Peer added successfully") + .withBadRequestResponse(Optional.of("Invalid peer address")) + .withInternalErrorResponse() + .build()); + this.networkDataProvider = networkDataProvider; + } + + @Override + public void handleRequest(final RestApiRequest request) throws JsonProcessingException { + try { + + final String peerAddress = request.getRequestBody(); + if (peerAddress.isEmpty()) { + request.respondWithCode(SC_OK); + return; + } + + final DiscoveryNetwork discoveryNetwork = + networkDataProvider + .getDiscoveryNetwork() + .orElseThrow(() -> new IllegalStateException("Discovery network not available")); + + discoveryNetwork.addStaticPeer(peerAddress); + request.respondWithCode(SC_OK); + } catch (final IllegalArgumentException e) { + request.respondError(SC_BAD_REQUEST, e.getMessage()); + } catch (final IllegalStateException e) { + request.respondError(SC_INTERNAL_SERVER_ERROR, e.getMessage()); + } + } +} diff --git a/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/tekuv1/admin/AddPeerTest.java b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/tekuv1/admin/AddPeerTest.java new file mode 100644 index 00000000000..a3b44e34ad0 --- /dev/null +++ b/data/beaconrestapi/src/test/java/tech/pegasys/teku/beaconrestapi/handlers/tekuv1/admin/AddPeerTest.java @@ -0,0 +1,95 @@ +/* + * 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.tekuv1.admin; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_INTERNAL_SERVER_ERROR; +import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK; +import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.verifyMetadataEmptyResponse; +import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.verifyMetadataErrorResponse; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.beaconrestapi.AbstractMigratedBeaconHandlerTest; +import tech.pegasys.teku.networking.p2p.discovery.DiscoveryNetwork; + +class AddPeerTest extends AbstractMigratedBeaconHandlerTest { + + private final DiscoveryNetwork discoveryNetwork = mock(DiscoveryNetwork.class); + + @BeforeEach + public void setup() { + setHandler(new AddPeer(network)); + } + + @Test + void metadata_shouldHandle200() { + verifyMetadataEmptyResponse(handler, SC_OK); + } + + @Test + void metadata_shouldHandle400() throws JsonProcessingException { + verifyMetadataErrorResponse(handler, SC_BAD_REQUEST); + } + + @Test + void metadata_shouldHandle500() throws JsonProcessingException { + verifyMetadataErrorResponse(handler, SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void shouldReturnOkWhenAValidListIsProvided() throws Exception { + final String peerAddress = "/ip4/someIP/udp/9001/p2p/somePeerId"; + request.setRequestBody(peerAddress); + when(network.getDiscoveryNetwork()).thenReturn(Optional.of(discoveryNetwork)); + handler.handleRequest(request); + assertThat(request.getResponseCode()).isEqualTo(SC_OK); + } + + @Test + public void shouldReturnOkWhenRequestBodyIsEmpty() throws Exception { + final String peerAddress = ""; + request.setRequestBody(peerAddress); + when(network.getDiscoveryNetwork()).thenReturn(Optional.of(discoveryNetwork)); + handler.handleRequest(request); + assertThat(request.getResponseCode()).isEqualTo(SC_OK); + } + + @Test + public void shouldReturnBadRequestIfInvalidPeerAddress() throws Exception { + final String peerAddress = "invalid-peer-address"; + request.setRequestBody(peerAddress); + when(network.getDiscoveryNetwork()).thenReturn(Optional.of(discoveryNetwork)); + doThrow(new IllegalArgumentException("Invalid peer address")) + .when(discoveryNetwork) + .addStaticPeer(peerAddress); + handler.handleRequest(request); + assertThat(request.getResponseCode()).isEqualTo(SC_BAD_REQUEST); + } + + @Test + public void shouldReturnInternalErrorWhenDiscoveryNetworkNotAvailable() throws Exception { + final String peerAddress = "/ip4/someIP/udp/9001/p2p/somePeerId"; + request.setRequestBody(peerAddress); + when(network.getDiscoveryNetwork()).thenReturn(Optional.empty()); + handler.handleRequest(request); + assertThat(request.getResponseCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR); + } +} diff --git a/data/provider/src/main/java/tech/pegasys/teku/api/NetworkDataProvider.java b/data/provider/src/main/java/tech/pegasys/teku/api/NetworkDataProvider.java index 69d23840255..0e1be14de57 100644 --- a/data/provider/src/main/java/tech/pegasys/teku/api/NetworkDataProvider.java +++ b/data/provider/src/main/java/tech/pegasys/teku/api/NetworkDataProvider.java @@ -148,4 +148,8 @@ private Peer toPeer(final Eth2Peer eth2Peer) { return new Peer(peerId, null, address, state, direction); } + + public Optional> getDiscoveryNetwork() { + return network.getDiscoveryNetwork(); + } }