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 @@ -13,5 +13,6 @@
- Applied spec change to alter `GOSSIP_MAX_SIZE` to `MAX_PAYLOAD_SIZE`.
- `MAX_PAYLOAD_SIZE` is now used instead of `MAX_CHUNK_SIZE`.
- Updated 3rd party products to latest versions.
- Add SSZ support to validator registration via Builder API.

### Bug Fixes
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@

import static com.google.common.base.Preconditions.checkArgument;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static tech.pegasys.teku.spec.SpecMilestone.BELLATRIX;
import static tech.pegasys.teku.spec.SpecMilestone.CAPELLA;
import static tech.pegasys.teku.spec.SpecMilestone.DENEB;
Expand All @@ -26,11 +30,13 @@
import com.google.common.io.Resources;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.ConnectException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import okhttp3.OkHttpClient;
Expand All @@ -45,13 +51,16 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import tech.pegasys.teku.bls.BLSPublicKey;
import tech.pegasys.teku.ethereum.executionclient.BuilderApiMethod;
import tech.pegasys.teku.ethereum.executionclient.schema.BuilderApiResponse;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.json.JsonUtil;
import tech.pegasys.teku.infrastructure.json.exceptions.MissingRequiredFieldException;
import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition;
import tech.pegasys.teku.infrastructure.ssz.SszData;
import tech.pegasys.teku.infrastructure.ssz.SszList;
import tech.pegasys.teku.infrastructure.ssz.sos.SszDeserializeException;
import tech.pegasys.teku.infrastructure.time.StubTimeProvider;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.spec.Spec;
import tech.pegasys.teku.spec.SpecMilestone;
Expand Down Expand Up @@ -98,11 +107,13 @@ class RestBuilderClientTest {
private final OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
private final MockWebServer mockWebServer = new MockWebServer();

private StubTimeProvider timeProvider;
private Spec spec;
private SpecMilestone milestone;
private OkHttpRestClient okHttpRestClient;
private SchemaDefinitionsBellatrix schemaDefinitions;

private RestBuilderClientOptions restBuilderClientOptions;
private RestBuilderClient restBuilderClient;

private String signedValidatorRegistrationsRequest;
Expand All @@ -117,6 +128,7 @@ class RestBuilderClientTest {
@BeforeEach
void setUp(final SpecContext specContext) throws IOException {
mockWebServer.start();
timeProvider = StubTimeProvider.withTimeInMillis(UInt64.ONE);

spec = specContext.getSpec();
final String endpoint = "http://localhost:" + mockWebServer.getPort();
Expand Down Expand Up @@ -152,7 +164,9 @@ void setUp(final SpecContext specContext) throws IOException {
"data")
.orElseThrow();

restBuilderClient = new RestBuilderClient(okHttpRestClient, spec, true);
restBuilderClientOptions = RestBuilderClientOptions.DEFAULT;
restBuilderClient =
new RestBuilderClient(restBuilderClientOptions, okHttpRestClient, timeProvider, spec, true);
}

@AfterEach
Expand Down Expand Up @@ -207,7 +221,7 @@ void registerValidators_success() {
assertThat(response.payload()).isNull();
});

verifyPostRequestJson("/eth/v1/builder/validators", signedValidatorRegistrationsRequest);
verifyPostRequestSsz("/eth/v1/builder/validators", signedValidatorRegistrations);
}

@TestTemplate
Expand All @@ -232,6 +246,8 @@ void registerValidators_failures() {

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

// Both ssz and json requests will fail
mockWebServer.enqueue(new MockResponse().setResponseCode(400).setBody(unknownValidatorError));
mockWebServer.enqueue(new MockResponse().setResponseCode(400).setBody(unknownValidatorError));

final SszList<SignedValidatorRegistration> signedValidatorRegistrations =
Expand All @@ -245,8 +261,10 @@ void registerValidators_failures() {
assertThat(response.errorMessage()).isEqualTo(unknownValidatorError);
});

verifyPostRequestSsz("/eth/v1/builder/validators", signedValidatorRegistrations);
verifyPostRequestJson("/eth/v1/builder/validators", signedValidatorRegistrationsRequest);

// Only json will fail (ssz backoff enabled)
mockWebServer.enqueue(
new MockResponse().setResponseCode(500).setBody(INTERNAL_SERVER_ERROR_MESSAGE));

Expand All @@ -261,10 +279,105 @@ void registerValidators_failures() {
verifyPostRequestJson("/eth/v1/builder/validators", signedValidatorRegistrationsRequest);
}

@TestTemplate
void registerValidators_retryWithJsonAfterSszFailure() {
final String errorMsg = "{\"code\":415,\"message\":\"unsupported media type\"}";
mockWebServer.enqueue(new MockResponse().setResponseCode(415).setBody(errorMsg));
mockWebServer.enqueue(
new MockResponse().setResponseCode(200).setBody(signedBuilderBidResponseJson));

final SszList<SignedValidatorRegistration> signedValidatorRegistrations =
createSignedValidatorRegistrations();

assertThat(restBuilderClient.registerValidators(SLOT, signedValidatorRegistrations))
.succeedsWithin(WAIT_FOR_CALL_COMPLETION)
.satisfies(response -> assertThat(response.isSuccess()).isTrue());

verifyPostRequestSsz("/eth/v1/builder/validators", signedValidatorRegistrations);
verifyPostRequestJson("/eth/v1/builder/validators", signedValidatorRegistrationsRequest);

assertThat(mockWebServer.getRequestCount()).isEqualTo(2);
}

@TestTemplate
void registerValidators_trySszAfterBackoffTime() {
final String errorMsg = "{\"code\":400,\"message\":\"bad request\"}";
mockWebServer.enqueue(new MockResponse().setResponseCode(400).setBody(errorMsg));
mockWebServer.enqueue(
new MockResponse().setResponseCode(200).setBody(signedBuilderBidResponseJson));
mockWebServer.enqueue(
new MockResponse().setResponseCode(200).setBody(signedBuilderBidResponseJson));

final SszList<SignedValidatorRegistration> signedValidatorRegistrations =
createSignedValidatorRegistrations();

// First call, will try SSZ and fallback into JSON
assertThat(restBuilderClient.registerValidators(SLOT, signedValidatorRegistrations))
.succeedsWithin(WAIT_FOR_CALL_COMPLETION)
.satisfies(response -> assertThat(response.isSuccess()).isTrue());

verifyPostRequestSsz("/eth/v1/builder/validators", signedValidatorRegistrations);
verifyPostRequestJson("/eth/v1/builder/validators", signedValidatorRegistrationsRequest);

// After backoff period, will try SSZ again
timeProvider.advanceTimeBy(Duration.ofDays(1));
assertThat(restBuilderClient.registerValidators(SLOT, signedValidatorRegistrations))
.succeedsWithin(WAIT_FOR_CALL_COMPLETION)
.satisfies(response -> assertThat(response.isSuccess()).isTrue());

verifyPostRequestSsz("/eth/v1/builder/validators", signedValidatorRegistrations);

assertThat(mockWebServer.getRequestCount()).isEqualTo(3);
}

@TestTemplate
void registerValidators_shouldNotFallbackWhenTimingOut() {
// Creates a RestBuilderClient that always timeout
restBuilderClient =
new RestBuilderClient(
forcedTimeoutRestBuilderClientOptions(), okHttpRestClient, timeProvider, spec, true);

final SszList<SignedValidatorRegistration> signedValidatorRegistrations =
createSignedValidatorRegistrations();

assertThat(restBuilderClient.registerValidators(SLOT, signedValidatorRegistrations))
.failsWithin(WAIT_FOR_CALL_COMPLETION)
.withThrowableThat()
.withCauseInstanceOf(TimeoutException.class);

verifyPostRequestSsz("/eth/v1/builder/validators", signedValidatorRegistrations);
// Check that we do not fallback into JSON (only 1 request)
assertThat(mockWebServer.getRequestCount()).isEqualTo(1);
}

@TestTemplate
void registerValidators_shouldNotFallbackWhenFailingForNonHttpReasons() {
okHttpRestClient = spy(okHttpRestClient);
// Mock asyncPost with ssz to fail
doReturn(SafeFuture.failedFuture(new ConnectException()))
.when(okHttpRestClient)
.postAsync(eq(BuilderApiMethod.REGISTER_VALIDATOR.getPath()), any(), eq(true));
restBuilderClient =
new RestBuilderClient(restBuilderClientOptions, okHttpRestClient, timeProvider, spec, true);

final SszList<SignedValidatorRegistration> signedValidatorRegistrations =
createSignedValidatorRegistrations();

assertThat(restBuilderClient.registerValidators(SLOT, signedValidatorRegistrations))
.failsWithin(WAIT_FOR_CALL_COMPLETION)
.withThrowableThat()
.withCauseInstanceOf(ConnectException.class);

// No requests are made (including no fallback to json)
assertThat(mockWebServer.getRequestCount()).isEqualTo(0);
}

@TestTemplate
void getHeader_success_doesNotSetUserAgentHeader() {

restBuilderClient = new RestBuilderClient(okHttpRestClient, spec, false);
restBuilderClient =
new RestBuilderClient(
restBuilderClientOptions, okHttpRestClient, timeProvider, spec, false);

mockWebServer.enqueue(
new MockResponse().setResponseCode(200).setBody(signedBuilderBidResponseJson));
Expand Down Expand Up @@ -622,6 +735,10 @@ private void verifyPostRequestJson(final String apiPath, final String requestBod
verifyRequest("POST", apiPath, Optional.of(requestBody), Optional.empty());
}

private <T extends SszData> void verifyPostRequestSsz(final String apiPath, final T requestBody) {
verifyRequest("POST", apiPath, Optional.empty(), Optional.of(requestBody));
}

private void verifyRequest(
final String method,
final String apiPath,
Expand Down Expand Up @@ -725,4 +842,9 @@ private static String changeResponseVersion(final String json, final SpecMilesto
return json.replaceFirst(
"(?<=version\":\\s?\")\\w+", newVersion.toString().toLowerCase(Locale.ROOT));
}

private RestBuilderClientOptions forcedTimeoutRestBuilderClientOptions() {
return new RestBuilderClientOptions(
Duration.ofSeconds(0), Duration.ofSeconds(0), Duration.ofSeconds(0), Duration.ofSeconds(0));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,16 @@
package tech.pegasys.teku.ethereum.executionclient.rest;

import static tech.pegasys.teku.infrastructure.http.RestApiConstants.HEADER_CONSENSUS_VERSION;
import static tech.pegasys.teku.spec.config.Constants.BUILDER_GET_PAYLOAD_TIMEOUT;
import static tech.pegasys.teku.spec.config.Constants.BUILDER_PROPOSAL_DELAY_TOLERANCE;
import static tech.pegasys.teku.spec.config.Constants.BUILDER_REGISTER_VALIDATOR_TIMEOUT;
import static tech.pegasys.teku.spec.config.Constants.BUILDER_STATUS_TIMEOUT;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tuweni.bytes.Bytes32;
import tech.pegasys.teku.bls.BLSPublicKey;
import tech.pegasys.teku.ethereum.executionclient.BuilderApiMethod;
Expand All @@ -35,6 +34,7 @@
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition;
import tech.pegasys.teku.infrastructure.ssz.SszList;
import tech.pegasys.teku.infrastructure.time.TimeProvider;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.infrastructure.version.VersionProvider;
import tech.pegasys.teku.spec.Spec;
Expand All @@ -50,6 +50,8 @@

public class RestBuilderClient implements BuilderClient {

private static final Logger LOG = LogManager.getLogger();

private static final Map.Entry<String, String> USER_AGENT_HEADER =
Map.entry(
"User-Agent",
Expand All @@ -72,13 +74,26 @@ public class RestBuilderClient implements BuilderClient {
private final Map<SpecMilestone, ResponseSchemaAndDeserializableTypeDefinition<SignedBuilderBid>>
cachedBuilderApiSignedBuilderBidResponseType = new ConcurrentHashMap<>();

public static final UInt64 REGISTER_VALIDATOR_SSZ_BACKOFF_TIME_MILLIS =
UInt64.valueOf(TimeUnit.DAYS.toMillis(1));

private final RestBuilderClientOptions options;
private final RestClient restClient;
private final TimeProvider timeProvider;
private final Spec spec;
private final boolean setUserAgentHeader;

private UInt64 nextSszRegisterValidatorsTryMillis = UInt64.ZERO;

public RestBuilderClient(
final RestClient restClient, final Spec spec, final boolean setUserAgentHeader) {
final RestBuilderClientOptions options,
final RestClient restClient,
final TimeProvider timeProvider,
final Spec spec,
final boolean setUserAgentHeader) {
this.options = options;
this.restClient = restClient;
this.timeProvider = timeProvider;
this.spec = spec;
this.setUserAgentHeader = setUserAgentHeader;
}
Expand All @@ -87,7 +102,7 @@ public RestBuilderClient(
public SafeFuture<Response<Void>> status() {
return restClient
.getAsync(BuilderApiMethod.GET_STATUS.getPath())
.orTimeout(BUILDER_STATUS_TIMEOUT);
.orTimeout(options.builderStatusTimeout());
}

@Override
Expand All @@ -98,10 +113,41 @@ public SafeFuture<Response<Void>> registerValidators(
return SafeFuture.completedFuture(Response.fromNullPayload());
}

return restClient
.postAsync(
BuilderApiMethod.REGISTER_VALIDATOR.getPath(), signedValidatorRegistrations, false)
.orTimeout(BUILDER_REGISTER_VALIDATOR_TIMEOUT);
if (nextSszRegisterValidatorsTryMillis.isGreaterThan(timeProvider.getTimeInMillis())) {
return registerValidatorsUsingJson(signedValidatorRegistrations)
.orTimeout(options.builderRegisterValidatorTimeout());
}

return registerValidatorsUsingSsz(signedValidatorRegistrations)
.thenCompose(
response -> {
// Any failure will trigger a retry, not only Unsupported Media Type (415)
if (response.isFailure()) {
LOG.debug(
"Failed to register validator using SSZ. Will retry using JSON (error: {})",
response.errorMessage());

nextSszRegisterValidatorsTryMillis =
timeProvider.getTimeInMillis().plus(REGISTER_VALIDATOR_SSZ_BACKOFF_TIME_MILLIS);

return registerValidatorsUsingJson(signedValidatorRegistrations);
}

return SafeFuture.completedFuture(response);
})
.orTimeout(options.builderRegisterValidatorTimeout());
}

private SafeFuture<Response<Void>> registerValidatorsUsingJson(
final SszList<SignedValidatorRegistration> signedValidatorRegistrations) {
return restClient.postAsync(
BuilderApiMethod.REGISTER_VALIDATOR.getPath(), signedValidatorRegistrations, false);
}

private SafeFuture<Response<Void>> registerValidatorsUsingSsz(
final SszList<SignedValidatorRegistration> signedValidatorRegistrations) {
return restClient.postAsync(
BuilderApiMethod.REGISTER_VALIDATOR.getPath(), signedValidatorRegistrations, true);
}

@Override
Expand Down Expand Up @@ -138,7 +184,7 @@ public SafeFuture<Response<Optional<SignedBuilderBid>>> getHeader(
? GET_HEADER_HTTP_HEADERS_WITH_USER_AGENT
: GET_HEADER_HTTP_HEADERS_WITHOUT_USER_AGENT,
responseTypeDefinition)
.orTimeout(BUILDER_PROPOSAL_DELAY_TOLERANCE)
.orTimeout(options.builderProposalDelayTolerance())
.thenApply(
response ->
response.unwrapVersioned(
Expand Down Expand Up @@ -174,7 +220,7 @@ public SafeFuture<Response<BuilderPayload>> getPayload(
response ->
response.unwrapVersioned(
this::extractBuilderPayload, milestone, BuilderApiResponse::version, false))
.orTimeout(BUILDER_GET_PAYLOAD_TIMEOUT);
.orTimeout(options.builderGetPayloadTimeout());
}

private <T extends BuilderPayload>
Expand Down
Loading