Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MEV Boost\Builder] Add a builder rest api client #5521

Merged
merged 11 commits into from
May 17, 2022
Original file line number Diff line number Diff line change
@@ -0,0 +1,379 @@
/*
* Copyright 2022 ConsenSys AG.
*
* 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.ethereum.executionclient.rest;

import static org.assertj.core.api.Assertions.assertThat;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.Resources;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Optional;
import okhttp3.OkHttpClient;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import okio.Buffer;
import org.apache.tuweni.bytes.Bytes32;
import org.apache.tuweni.bytes.Bytes48;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import tech.pegasys.teku.bls.BLSPublicKey;
import tech.pegasys.teku.ethereum.executionclient.schema.BuilderApiResponse;
import tech.pegasys.teku.infrastructure.json.JsonUtil;
import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.spec.Spec;
import tech.pegasys.teku.spec.SpecMilestone;
import tech.pegasys.teku.spec.TestSpecContext;
import tech.pegasys.teku.spec.TestSpecInvocationContextProvider.SpecContext;
import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock;
import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayload;
import tech.pegasys.teku.spec.datastructures.execution.SignedBuilderBidV1;
import tech.pegasys.teku.spec.datastructures.execution.SignedValidatorRegistrationV1;
import tech.pegasys.teku.spec.networks.Eth2Network;
import tech.pegasys.teku.spec.schemas.SchemaDefinitionsBellatrix;

@TestSpecContext(
milestone = SpecMilestone.BELLATRIX,
network = {Eth2Network.MAINNET})
class RestExecutionBuilderClientTest {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

private static final Duration WAIT_FOR_CALL_COMPLETION = Duration.ofSeconds(10);

private static final String INTERNAL_SERVER_ERROR_MESSAGE =
"{\"code\":500,\"message\":\"Internal server error\"}";

private static final String SIGNED_VALIDATOR_REGISTRATION_REQUEST =
readResource("builder/signedValidatorRegistration.json");

private static final String SIGNED_BLINDED_BEACON_BLOCK_REQUEST =
readResource("builder/signedBlindedBeaconBlock.json");

private static final String EXECUTION_PAYLOAD_HEADER_RESPONSE =
readResource("builder/executionPayloadHeaderResponse.json");

private static final String UNBLINDED_EXECUTION_PAYLOAD_RESPONSE =
readResource("builder/unblindedExecutionPayloadResponse.json");

private static final UInt64 SLOT = UInt64.ONE;

private static final Bytes32 PARENT_HASH =
Bytes32.fromHexString("0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2");

private static final BLSPublicKey PUB_KEY =
BLSPublicKey.fromBytesCompressed(
Bytes48.fromHexString(
"0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a"));

private final MockWebServer mockWebServer = new MockWebServer();
private final OkHttpClient okHttpClient = new OkHttpClient.Builder().build();

private SchemaDefinitionsBellatrix schemaDefinitionsBellatrix;

private RestExecutionBuilderClient restExecutionBuilderClient;

@BeforeEach
void setUp(SpecContext specContext) throws IOException {
mockWebServer.start();
Spec spec = specContext.getSpec();
String endpoint = "http://localhost:" + mockWebServer.getPort();
OkHttpRestClient okHttpRestClient = new OkHttpRestClient(okHttpClient, endpoint);
this.schemaDefinitionsBellatrix =
spec.forMilestone(specContext.getSpecMilestone())
.getSchemaDefinitions()
.toVersionBellatrix()
.orElseThrow();
this.restExecutionBuilderClient = new RestExecutionBuilderClient(okHttpRestClient, spec);
}

@AfterEach
void afterEach() throws Exception {
mockWebServer.shutdown();
}

@TestTemplate
void getStatus_success() {
mockWebServer.enqueue(new MockResponse().setResponseCode(200));

assertThat(restExecutionBuilderClient.status())
.succeedsWithin(WAIT_FOR_CALL_COMPLETION)
.satisfies(
response -> {
assertThat(response.isSuccess()).isTrue();
assertThat(response.getPayload()).isNull();
});

verifyGetRequest("/eth1/v1/builder/status");
}

@TestTemplate
void getStatus_failures() {
mockWebServer.enqueue(
new MockResponse().setResponseCode(500).setBody(INTERNAL_SERVER_ERROR_MESSAGE));

assertThat(restExecutionBuilderClient.status())
.succeedsWithin(WAIT_FOR_CALL_COMPLETION)
.satisfies(
response -> {
assertThat(response.isFailure()).isTrue();
assertThat(response.getErrorMessage()).isEqualTo(INTERNAL_SERVER_ERROR_MESSAGE);
});

verifyGetRequest("/eth1/v1/builder/status");
}

@TestTemplate
void registerValidator_success() {

mockWebServer.enqueue(new MockResponse().setResponseCode(200));

SignedValidatorRegistrationV1 signedValidatorRegistration = createSignedValidatorRegistration();

assertThat(restExecutionBuilderClient.registerValidator(SLOT, signedValidatorRegistration))
.succeedsWithin(WAIT_FOR_CALL_COMPLETION)
.satisfies(
response -> {
assertThat(response.isSuccess()).isTrue();
assertThat(response.getPayload()).isNull();
});

verifyPostRequest("/eth/v1/builder/validators", SIGNED_VALIDATOR_REGISTRATION_REQUEST);
}

@TestTemplate
void registerValidator_failures() {

String unknownValidatorError = "{\"code\":400,\"message\":\"unknown validator\"}";

mockWebServer.enqueue(new MockResponse().setResponseCode(400).setBody(unknownValidatorError));

SignedValidatorRegistrationV1 signedValidatorRegistration = createSignedValidatorRegistration();

assertThat(restExecutionBuilderClient.registerValidator(SLOT, signedValidatorRegistration))
.succeedsWithin(WAIT_FOR_CALL_COMPLETION)
.satisfies(
response -> {
assertThat(response.isFailure()).isTrue();
assertThat(response.getErrorMessage()).isEqualTo(unknownValidatorError);
});

verifyPostRequest("/eth/v1/builder/validators", SIGNED_VALIDATOR_REGISTRATION_REQUEST);

mockWebServer.enqueue(
new MockResponse().setResponseCode(500).setBody(INTERNAL_SERVER_ERROR_MESSAGE));

assertThat(restExecutionBuilderClient.registerValidator(SLOT, signedValidatorRegistration))
.succeedsWithin(WAIT_FOR_CALL_COMPLETION)
.satisfies(
response -> {
assertThat(response.isFailure()).isTrue();
assertThat(response.getErrorMessage()).isEqualTo(INTERNAL_SERVER_ERROR_MESSAGE);
});

verifyPostRequest("/eth/v1/builder/validators", SIGNED_VALIDATOR_REGISTRATION_REQUEST);
}

@TestTemplate
void getExecutionPayloadHeader_success() {

mockWebServer.enqueue(
new MockResponse().setResponseCode(200).setBody(EXECUTION_PAYLOAD_HEADER_RESPONSE));

assertThat(restExecutionBuilderClient.getHeader(SLOT, PUB_KEY, PARENT_HASH))
.succeedsWithin(WAIT_FOR_CALL_COMPLETION)
.satisfies(
response -> {
assertThat(response.isSuccess()).isTrue();
SignedBuilderBidV1 responsePayload = response.getPayload();
verifySignedBuilderBidV1Response(responsePayload);
});

verifyGetRequest("/eth/v1/builder/header/1/" + PARENT_HASH + "/" + PUB_KEY);
}

@TestTemplate
void getExecutionPayloadHeader_failures() {

String missingParentHashError =
"{\"code\":400,\"message\":\"Unknown hash: missing parent hash\"}";
mockWebServer.enqueue(new MockResponse().setResponseCode(400).setBody(missingParentHashError));

assertThat(restExecutionBuilderClient.getHeader(SLOT, PUB_KEY, PARENT_HASH))
.succeedsWithin(WAIT_FOR_CALL_COMPLETION)
.satisfies(
response -> {
assertThat(response.isFailure()).isTrue();
assertThat(response.getErrorMessage()).isEqualTo(missingParentHashError);
});

verifyGetRequest("/eth/v1/builder/header/1/" + PARENT_HASH + "/" + PUB_KEY);

mockWebServer.enqueue(
new MockResponse().setResponseCode(500).setBody(INTERNAL_SERVER_ERROR_MESSAGE));

assertThat(restExecutionBuilderClient.getHeader(SLOT, PUB_KEY, PARENT_HASH))
.succeedsWithin(WAIT_FOR_CALL_COMPLETION)
.satisfies(
response -> {
assertThat(response.isFailure()).isTrue();
assertThat(response.getErrorMessage()).isEqualTo(INTERNAL_SERVER_ERROR_MESSAGE);
});

verifyGetRequest("/eth/v1/builder/header/1/" + PARENT_HASH + "/" + PUB_KEY);
}

@TestTemplate
void sendSignedBlindedBlock_success() {

mockWebServer.enqueue(
new MockResponse().setResponseCode(200).setBody(UNBLINDED_EXECUTION_PAYLOAD_RESPONSE));

SignedBeaconBlock signedBlindedBeaconBlock = createSignedBlindedBeaconBlock();

assertThat(restExecutionBuilderClient.getPayload(signedBlindedBeaconBlock))
.succeedsWithin(WAIT_FOR_CALL_COMPLETION)
.satisfies(
response -> {
assertThat(response.isSuccess()).isTrue();
ExecutionPayload responsePayload = response.getPayload();
verifyExecutionPayloadResponse(responsePayload);
});

verifyPostRequest("/eth/v1/builder/blinded_blocks", SIGNED_BLINDED_BEACON_BLOCK_REQUEST);
}

@TestTemplate
void sendSignedBlindedBlock_failures() {

String missingSignatureError =
"{\"code\":400,\"message\":\"Invalid block: missing signature\"}";
mockWebServer.enqueue(new MockResponse().setResponseCode(400).setBody(missingSignatureError));

SignedBeaconBlock signedBlindedBeaconBlock = createSignedBlindedBeaconBlock();

assertThat(restExecutionBuilderClient.getPayload(signedBlindedBeaconBlock))
.succeedsWithin(WAIT_FOR_CALL_COMPLETION)
.satisfies(
response -> {
assertThat(response.isFailure()).isTrue();
assertThat(response.getErrorMessage()).isEqualTo(missingSignatureError);
});

verifyPostRequest("/eth/v1/builder/blinded_blocks", SIGNED_BLINDED_BEACON_BLOCK_REQUEST);

mockWebServer.enqueue(
new MockResponse().setResponseCode(500).setBody(INTERNAL_SERVER_ERROR_MESSAGE));

assertThat(restExecutionBuilderClient.getPayload(signedBlindedBeaconBlock))
.succeedsWithin(WAIT_FOR_CALL_COMPLETION)
.satisfies(
response -> {
assertThat(response.isFailure()).isTrue();
assertThat(response.getErrorMessage()).isEqualTo(INTERNAL_SERVER_ERROR_MESSAGE);
});

verifyPostRequest("/eth/v1/builder/blinded_blocks", SIGNED_BLINDED_BEACON_BLOCK_REQUEST);
}

private void verifyGetRequest(String apiPath) {
verifyRequest("GET", apiPath, Optional.empty());
}

private void verifyPostRequest(String apiPath, String requestBody) {
verifyRequest("POST", apiPath, Optional.of(requestBody));
}

private <T> void verifyRequest(
String method, String apiPath, Optional<String> expectedRequestBody) {
try {
RecordedRequest request = mockWebServer.takeRequest();
assertThat(request.getMethod()).isEqualTo(method);
assertThat(request.getPath()).isEqualTo(apiPath);
Buffer actualRequestBody = request.getBody();
if (expectedRequestBody.isEmpty()) {
assertThat(actualRequestBody.size()).isZero();
} else {
assertThat(actualRequestBody.size()).isNotZero();
assertThat(OBJECT_MAPPER.readTree(expectedRequestBody.get()))
.isEqualTo(OBJECT_MAPPER.readTree(actualRequestBody.readUtf8()));
}
} catch (InterruptedException | JsonProcessingException ex) {
Assertions.fail(ex);
}
}

private SignedValidatorRegistrationV1 createSignedValidatorRegistration() {
try {
return JsonUtil.parse(
SIGNED_VALIDATOR_REGISTRATION_REQUEST,
schemaDefinitionsBellatrix
.getSignedValidatorRegistrationSchema()
.getJsonTypeDefinition());
} catch (JsonProcessingException ex) {
throw new UncheckedIOException(ex);
}
}

private void verifySignedBuilderBidV1Response(SignedBuilderBidV1 actual) {
DeserializableTypeDefinition<BuilderApiResponse<SignedBuilderBidV1>> responseTypeDefinition =
BuilderApiResponse.createTypeDefinition(
schemaDefinitionsBellatrix.getSignedBuilderBidV1Schema().getJsonTypeDefinition());
try {
SignedBuilderBidV1 expected =
JsonUtil.parse(EXECUTION_PAYLOAD_HEADER_RESPONSE, responseTypeDefinition).getData();
assertThat(actual).isEqualTo(expected);
} catch (JsonProcessingException ex) {
Assertions.fail(ex);
}
}

private SignedBeaconBlock createSignedBlindedBeaconBlock() {
try {
return JsonUtil.parse(
SIGNED_BLINDED_BEACON_BLOCK_REQUEST,
schemaDefinitionsBellatrix.getSignedBlindedBeaconBlockSchema().getJsonTypeDefinition());
} catch (JsonProcessingException ex) {
throw new UncheckedIOException(ex);
}
}

private void verifyExecutionPayloadResponse(ExecutionPayload actual) {
DeserializableTypeDefinition<BuilderApiResponse<ExecutionPayload>> responseTypeDefinition =
BuilderApiResponse.createTypeDefinition(
schemaDefinitionsBellatrix.getExecutionPayloadSchema().getJsonTypeDefinition());
try {
ExecutionPayload expected =
JsonUtil.parse(UNBLINDED_EXECUTION_PAYLOAD_RESPONSE, responseTypeDefinition).getData();
assertThat(actual).isEqualTo(expected);
} catch (JsonProcessingException ex) {
Assertions.fail(ex);
}
}

private static String readResource(String resource) {
try {
return Resources.toString(Resources.getResource(resource), StandardCharsets.UTF_8);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
Loading