Skip to content

Staked Builder API REST Client - #11026

Open
StefanBratanov wants to merge 26 commits into
Consensys:masterfrom
StefanBratanov:gloas-staked-builder-api-rest-client
Open

Staked Builder API REST Client#11026
StefanBratanov wants to merge 26 commits into
Consensys: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 library on the block-proposal / builder path with auth, timeouts, and SSZ submission; not yet referenced outside the new module, but mistakes here would affect production once wired in.

Overview
Introduces a new builder:rest Gradle module with an OkHttp-based client for the Staked Builder API (Gloas / EPBS). Callers get StakedBuilderClient via StakedBuilderClientProvider, which caches one client per builder URL and runs calls on an AsyncRunner.

The client covers three endpoints: execution payload bid (POST with path params; optional signed auth JSON body; Eth-Consensus-Version, Date-Milliseconds, and X-Timeout-Ms; parses 200 JSON bids or treats 204 as empty), builder preferences (JSON POST per validator pubkey), and signed beacon blocks (SSZ application/octet-stream). Shared plumbing includes BuilderApiMethod path templates, AbstractBuilderRequest, and ResponseHandler mapping status codes to BuilderClientException.

settings.gradle registers builder and builder:rest; the parent builder project disables its jar. MockWebServer integration tests exercise request shape and error handling for each handler.

Reviewed by Cursor Bugbot for commit 685b2d9. 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 Consensys 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)

@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 3ae76c9. Configure here.

@StefanBratanov
StefanBratanov force-pushed the gloas-staked-builder-api-rest-client branch 2 times, most recently from cf13fb3 to 174834e Compare August 28, 2026 08:13
…oas/ePBS

Adds a new builder:rest Gradle submodule with an OkHttp-based REST client
for the Staked Builder API defined in builder-specs PR Consensys#138. The module is
self-contained with no wiring into existing Teku infrastructure yet.

Files added:
- BuilderClient interface (four Gloas builder endpoints)
- BuilderApiMethod enum with {param} URL template substitution
- BuilderIdentity record with JSON type definition and data wrapper
- BuilderClientException for typed error handling (with HTTP status code)
- ResponseHandler with per-status-code deserialization (200/202/204/400/401/500/503)
- AbstractBuilderRequest base class (GET, postJson, postEmpty, postOctetStream helpers)
- Four handler classes: GetBuilderIdentityRequest, GetExecutionPayloadBidRequest,
  SendBuilderPreferencesRequest, SendSignedBeaconBlockRequest
- OkHttpBuilderClient wrapping handlers with AsyncRunner for SafeFuture responses
- Integration tests using MockWebServer for all four handlers
…nt-Type

- SendSignedBeaconBlockRequest now takes Spec and sends Eth-Consensus-Version
  header derived from the block's slot milestone; postOctetStream gains a
  headers overload to support this
- postEmpty no longer sets Content-Type: application/json on a zero-length
  body; uses null MediaType so no Content-Type is sent (empty body is not
  valid JSON and would cause builders to return 400)
@StefanBratanov
StefanBratanov force-pushed the gloas-staked-builder-api-rest-client branch from 174834e to 685b2d9 Compare August 28, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants