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

Add support to builder validator registration using ssz #9065

Merged
merged 6 commits into from
Feb 3, 2025
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 @@ -12,5 +12,6 @@
### Additions and Improvements
- Applied spec change to alter `GOSSIP_MAX_SIZE` to `MAX_PAYLOAD_SIZE`.
- `MAX_PAYLOAD_SIZE` is now used instead of `MAX_CHUNK_SIZE`.
- Add SSZ support to validator registration via Builder API.

### Bug Fixes
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
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,6 +99,7 @@ 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;
Expand All @@ -117,6 +119,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 +155,7 @@ void setUp(final SpecContext specContext) throws IOException {
"data")
.orElseThrow();

restBuilderClient = new RestBuilderClient(okHttpRestClient, spec, true);
restBuilderClient = new RestBuilderClient(okHttpRestClient, timeProvider, spec, true);
}

@AfterEach
Expand Down Expand Up @@ -207,7 +210,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 +235,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 +250,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 +268,57 @@ 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());

assertThat(mockWebServer.getRequestCount()).isEqualTo(2);
verifyPostRequestSsz("/eth/v1/builder/validators", signedValidatorRegistrations);
verifyPostRequestJson("/eth/v1/builder/validators", signedValidatorRegistrationsRequest);
}

@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();

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

// 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());

assertThat(mockWebServer.getRequestCount()).isEqualTo(3);
verifyPostRequestSsz("/eth/v1/builder/validators", signedValidatorRegistrations);
verifyPostRequestJson("/eth/v1/builder/validators", signedValidatorRegistrationsRequest);
verifyPostRequestSsz("/eth/v1/builder/validators", signedValidatorRegistrations);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we should probably make a tiny change here to show that the first set are verified and the second set is just the ssz call... we basically show that with the asserts above but it'd be clearer if this last check is just seeing 1 request, and just ssz.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I can move the early assertions closer to where we exercise the request to make them a bit more "sequential".

}

@TestTemplate
void getHeader_success_doesNotSetUserAgentHeader() {

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

mockWebServer.enqueue(
new MockResponse().setResponseCode(200).setBody(signedBuilderBidResponseJson));
Expand Down Expand Up @@ -622,6 +676,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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
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 +38,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 +54,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 +78,23 @@ 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 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 RestClient restClient,
final TimeProvider timeProvider,
final Spec spec,
final boolean setUserAgentHeader) {
this.restClient = restClient;
this.timeProvider = timeProvider;
this.spec = spec;
this.setUserAgentHeader = setUserAgentHeader;
}
Expand All @@ -98,12 +114,43 @@ public SafeFuture<Response<Void>> registerValidators(
return SafeFuture.completedFuture(Response.fromNullPayload());
}

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

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);
} else {
return SafeFuture.completedFuture(response);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could avoid else here ;-)

})
.orTimeout(BUILDER_REGISTER_VALIDATOR_TIMEOUT);
}

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
public SafeFuture<Response<Optional<SignedBuilderBid>>> getHeader(
final UInt64 slot, final BLSPublicKey pubKey, final Bytes32 parentHash) {
Expand Down Expand Up @@ -203,6 +250,7 @@ private SchemaDefinitionsBellatrix getSchemaDefinitionsBellatrix(final SpecVersi
() ->
new IllegalArgumentException(
specVersion.getMilestone()
+ " is not a supported milestone for the builder rest api. Milestones >= Bellatrix are supported."));
+ " is not a supported milestone for the builder rest api. Milestones >= Bellatrix are "
+ "supported."));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be unchanged really...

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public static BuilderClient createBuilderClient(
final boolean setUserAgentHeader) {

final RestBuilderClient restBuilderClient =
new RestBuilderClient(restClient, spec, setUserAgentHeader);
new RestBuilderClient(restClient, timeProvider, spec, setUserAgentHeader);
final MetricRecordingBuilderClient metricRecordingBuilderClient =
new MetricRecordingBuilderClient(restBuilderClient, timeProvider, metricsSystem);
return new ThrottlingBuilderClient(
Expand Down