Skip to content

Staked Builder API REST Client - #11026

Merged
StefanBratanov merged 26 commits into
Consensys-Incorporated:masterfrom
StefanBratanov:gloas-staked-builder-api-rest-client
Sep 1, 2026
Merged

Staked Builder API REST Client#11026
StefanBratanov merged 26 commits into
Consensys-Incorporated:masterfrom
StefanBratanov:gloas-staked-builder-api-rest-client

Conversation

@StefanBratanov

@StefanBratanov StefanBratanov commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

PR Description

Client for the builder rest api as per ethereum/builder-specs#138 . There is a repetition but I found it difficult to create a generic rest client implementation that both the VC client and builder client can use.

Fixed Issue(s)

related to #10822

Documentation

  • I thought about documentation and added the doc-change-required label to this PR if updates are required.

Changelog

  • I thought about adding a changelog entry, and added one if I deemed necessary.

Note

Medium Risk
New client for execution-payload bids, builder preferences, and signed beacon block submission; incorrect HTTP/SSZ/auth handling could affect block production once wired in, but this PR is an isolated library with integration tests and no main-node wiring in the diff.

Overview
Adds a new builder:rest Gradle module (plus a builder parent with no jar) and registers both in settings.gradle.

Introduces an OkHttp-based Staked Builder API client aligned with builder-specs#138: StakedBuilderClient with getExecutionPayloadBid, submitBuilderPreferences, and submitSignedBeaconBlock, exposed via StakedBuilderClientProvider (one cached client per full URL, shared OkHttpClient with BUILDER_CALL_TIMEOUT).

Request plumbing includes BuilderApiMethod paths, AbstractBuilderRequest (JSON, empty POST, or SSZ octet-stream), ResponseHandler for 200/202/204 and error statuses → BuilderClientException, and Gloas-specific headers (e.g. Eth-Consensus-Version, timing headers on payload-bid requests). MockWebServer integration tests cover the three handlers and provider caching behavior.

Reviewed by Cursor Bugbot for commit f85f884. Bugbot is set up for automated code reviews on this repo. Configure here.

@StefanBratanov StefanBratanov changed the title Gloas staked builder api rest client Staked Builder API REST Client Jul 29, 2026
@StefanBratanov
StefanBratanov force-pushed the gloas-staked-builder-api-rest-client branch from 0baf796 to 71a3287 Compare July 29, 2026 15:06
Comment thread builder/rest/src/main/java/tech/pegasys/teku/builder/rest/BuilderApiMethod.java Outdated
@StefanBratanov
StefanBratanov force-pushed the gloas-staked-builder-api-rest-client branch from 5c4ae8d to 68b3483 Compare July 30, 2026 09:37

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 68b3483. Configure here.

@StefanBratanov
StefanBratanov force-pushed the gloas-staked-builder-api-rest-client branch from 75a1255 to f07ae53 Compare July 31, 2026 07:57
@Consensys-Incorporated Consensys-Incorporated deleted a comment from cursor Bot Jul 31, 2026
@StefanBratanov
StefanBratanov force-pushed the gloas-staked-builder-api-rest-client branch 3 times, most recently from 921918d to 1f5f5d0 Compare August 3, 2026 08:00
@StefanBratanov
StefanBratanov force-pushed the gloas-staked-builder-api-rest-client branch 2 times, most recently from 6ed7989 to f48c9d8 Compare August 5, 2026 13:17
@tbenr

tbenr commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
  • Missing Eth-Consensus-Version request header on two endpoints. The spec marks this header required: true on both getExecutionPayloadBid and submitBuilderPreferences (the description qualifies it as "Required
    if the request body is SSZ encoded", but the OpenAPI declares it required unconditionally). The client sends it only for submitSignedBeaconBlock. A builder validating strictly against the OpenAPI schema would
    reject the other two requests. Since the milestone is trivially available, I'd send it on all three (it also future-proofs the JSON body across forks).

  • ResponseHandler.VOID is a shared mutable singleton. withHandler() is public and mutates the internal Int2ObjectOpenHashMap, which is not thread-safe. Any future caller doing
    ResponseHandler.VOID.withHandler(...) would mutate global state visible to all concurrent requests. Either make withHandler copy-on-write, drop it from the shared instance, or replace VOID with a factory method
    (ResponseHandler.voidHandler()).

  • All three central names already exist in the codebase for the legacy MEV-Boost Builder API. When this gets wired into ExecutionLayerManager (which already imports the old BuilderClient), both interfaces will coexist in the same call sites and imports will be genuinely confusing. Suggest
    StakedBuilderClient / StakedBuilderApiMethod or similar. Also note the old enum uses :param placeholders while the new one uses {param} — if both survive, converging on one convention would help.

  • AbstractBuilderRequestTestBase is named Abstract... but declared as a plain public class — declare it abstract.

  • getErrorMessage reads the full error body unbounded; a broken/hostile builder could return a huge payload that ends up in an exception message and log. Consider truncating (e.g. body.source().readUtf8(MAX)).

    Test coverage

    • shouldIncludeSignedRequestAuthInBodyWhenPresent only asserts body.size() > 0 — it should assert Content-Type: application/json and ideally round-trip the body back to the SignedRequestAuth to prove the schema
      used is the right one.
    • No test for the unknownResponseCodeHandler path (e.g. a 418) or for 503, and no test for a malformed 200 JSON body on the bid endpoint.

@rolfyone

rolfyone commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

would be good to add the references like OpenApiIntegrationTest does so that we can easily see the api definitions...

@StefanBratanov
StefanBratanov force-pushed the gloas-staked-builder-api-rest-client branch from f48c9d8 to 01d69a4 Compare August 6, 2026 13:55
Comment thread builder/rest/src/main/java/tech/pegasys/teku/builder/rest/ResponseHandler.java Outdated
@StefanBratanov

Copy link
Copy Markdown
Contributor Author

@tbenr fixed the 5 points, will tackle testing one separately

@StefanBratanov

Copy link
Copy Markdown
Contributor Author

would be good to add the references like OpenApiIntegrationTest does so that we can easily see the api definitions...

They will show eventually anyways when we pipe them through the Beacon API.

@tbenr

tbenr commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

just one minor thing: Eth-Consensus-Version presence is not tested in all methods

@StefanBratanov
StefanBratanov force-pushed the gloas-staked-builder-api-rest-client branch from bdcdaec to fa389c9 Compare August 10, 2026 17:33
@StefanBratanov

Copy link
Copy Markdown
Contributor Author

just one minor thing: Eth-Consensus-Version presence is not tested in all methods

Done

@StefanBratanov
StefanBratanov force-pushed the gloas-staked-builder-api-rest-client branch 3 times, most recently from b5ffe69 to 246a35a Compare August 13, 2026 14:27
@rolfyone

Copy link
Copy Markdown
Contributor

claude review comments:

  1. Missing Accept: application/json header on getExecutionPayloadBid

builder/rest/src/main/java/tech/pegasys/teku/builder/rest/handlers/GetExecutionPayloadBidRequest.java

The response handler only parses JSON, but no Accept header is sent. A builder that supports multiple content types might respond with SSZ, causing a JsonProcessingException (which becomes UncheckedIOException thrown as a failed future). Other
Teku REST clients set Accept: application/json explicitly. Should add "Accept", "application/json" to the headers map.


  1. BuilderApiMethod.getPath() silently leaves unreplaced path templates

builder/rest/src/main/java/tech/pegasys/teku/builder/rest/BuilderApiMethod.java:32

If a caller omits a required URL param, the template literal {param} is sent as-is in the URL, resulting in a 404 or a malformed request with no hint of what went wrong. A guard like:

if (result.contains("{")) {
throw new IllegalArgumentException("Unreplaced path parameters in: " + result);
}

after the loop would catch misuse early.


  1. New request handler objects created per call

builder/rest/src/main/java/tech/pegasys/teku/builder/rest/OkHttpStakedBuilderClient.java:57,67,76

new GetExecutionPayloadBidRequest(spec, baseEndpoint, httpClient) (and the two others) are constructed on every call. These are stateless given spec and baseEndpoint — they could be final fields set in the constructor. No functional bug, but
avoids unnecessary allocation on the hot path.


  1. Test/impl inconsistency for milestone name

builder/rest/src/integration-test/.../SubmitSignedBeaconBlockRequestTest.java:84

The test derives the expected header value using spec.atSlot(...).getMilestone().name().toLowerCase(Locale.ROOT), while the implementation uses lowerCaseName(). They produce identical output today, but the test should mirror the implementation
(lowerCaseName()) so it would catch a future change to either.


  1. withHandler is public but mutates a mostly-final object

builder/rest/src/main/java/tech/pegasys/teku/builder/rest/ResponseHandler.java:58

withHandler is chained from the constructor to register defaults, then exposed publicly. A caller could replace e.g. the SC_OK handler post-construction, introducing subtle bugs. Making it package-private (or protected) would limit the risk since
external callers have no reason to override default mappings.

@StefanBratanov
StefanBratanov force-pushed the gloas-staked-builder-api-rest-client branch from 246a35a to 47f4e14 Compare August 27, 2026 08:01
@StefanBratanov

Copy link
Copy Markdown
Contributor Author

claude review comments:

  1. Missing Accept: application/json header on getExecutionPayloadBid

builder/rest/src/main/java/tech/pegasys/teku/builder/rest/handlers/GetExecutionPayloadBidRequest.java

The response handler only parses JSON, but no Accept header is sent. A builder that supports multiple content types might respond with SSZ, causing a JsonProcessingException (which becomes UncheckedIOException thrown as a failed future). Other Teku REST clients set Accept: application/json explicitly. Should add "Accept", "application/json" to the headers map.

  1. BuilderApiMethod.getPath() silently leaves unreplaced path templates

builder/rest/src/main/java/tech/pegasys/teku/builder/rest/BuilderApiMethod.java:32

If a caller omits a required URL param, the template literal {param} is sent as-is in the URL, resulting in a 404 or a malformed request with no hint of what went wrong. A guard like:

if (result.contains("{")) { throw new IllegalArgumentException("Unreplaced path parameters in: " + result); }

after the loop would catch misuse early.

  1. New request handler objects created per call

builder/rest/src/main/java/tech/pegasys/teku/builder/rest/OkHttpStakedBuilderClient.java:57,67,76

new GetExecutionPayloadBidRequest(spec, baseEndpoint, httpClient) (and the two others) are constructed on every call. These are stateless given spec and baseEndpoint — they could be final fields set in the constructor. No functional bug, but avoids unnecessary allocation on the hot path.

  1. Test/impl inconsistency for milestone name

builder/rest/src/integration-test/.../SubmitSignedBeaconBlockRequestTest.java:84

The test derives the expected header value using spec.atSlot(...).getMilestone().name().toLowerCase(Locale.ROOT), while the implementation uses lowerCaseName(). They produce identical output today, but the test should mirror the implementation (lowerCaseName()) so it would catch a future change to either.

  1. withHandler is public but mutates a mostly-final object

builder/rest/src/main/java/tech/pegasys/teku/builder/rest/ResponseHandler.java:58

withHandler is chained from the constructor to register defaults, then exposed publicly. A caller could replace e.g. the SC_OK handler post-construction, introducing subtle bugs. Making it package-private (or protected) would limit the risk since external callers have no reason to override default mappings.

  1. I added the "Accept" header
  2. That one it's fine, all calls are covered by tests of the request endpoint anyways
  3. Fixed
  4. Fixed
  5. That's by design (similar to ResponseHandler on the VC side)

@StefanBratanov
StefanBratanov force-pushed the gloas-staked-builder-api-rest-client branch from bcd802f to f85f884 Compare September 1, 2026 04:43
@StefanBratanov
StefanBratanov enabled auto-merge (squash) September 1, 2026 04:43
@StefanBratanov
StefanBratanov merged commit 1ef46b9 into Consensys-Incorporated:master Sep 1, 2026
92 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 1, 2026
@StefanBratanov
StefanBratanov deleted the gloas-staked-builder-api-rest-client branch September 1, 2026 06:56
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants