-
Notifications
You must be signed in to change notification settings - Fork 298
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[MEV Boost\Builder] Add a builder rest api client (#5521)
- Loading branch information
1 parent
11b316e
commit 005b1e5
Showing
53 changed files
with
928 additions
and
2,764 deletions.
There are no files selected for viewing
379 changes: 379 additions & 0 deletions
379
.../java/tech/pegasys/teku/ethereum/executionclient/rest/RestExecutionBuilderClientTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
Oops, something went wrong.