Repository navigation
ePBS (Gloas) support, part 3: bid streaming - #509
Open
JasonVranek wants to merge 7 commits into
Open
JasonVranek wants to merge 7 commits into
JasonVranek wants to merge 7 commits into
Conversation
The get_header stream's handshake, TLS config, frame format and read loop move to bid_stream.rs, behind a reader that holds the last frame a parse closure accepts. get_header decodes and validates that frame at the deadline, as before, so the result is unchanged. Another bid stream can use the same reader with its own parse closure. RelayClient::get_header_request becomes get_header_stream_url, which returns the stream URL only for a relay that streams. test_decode_streamed_bid goes: every stream test decodes a streamed bid.
The stream kept the latest frame that parsed and validated only that one at the deadline, so a last bid that failed validation (a bad signature, the wrong parent, below min_bid) discarded a valid bid the relay had sent before it. The reader now holds the newest 8 frames, validated newest first at the deadline, and the first that passes is returned. The usual case is still a single validation. When all 8 fail, the stream returns the newest one's error, as before. The cap bounds the memory a stream holds and the validation left for the deadline. A relay that sends 8 invalid bids after a valid one loses the valid one.
A relay with no bid for the slot can answer the stream handshake 204, as it answers get_header. The stream treated every 2xx without an upgrade as a failed connect, recorded it as 556 and fell back to HTTP. A 204 is now no bid: recorded as 204, no fallback. Any other 2xx without an upgrade is still a failed connect.
A relay with get_header = "stream" now also gets the relay's normal HTTP get_header, timing games included, sent at the same time as the stream handshake. The two are separate candidates in the bid selection: the stream's latest valid bid and the HTTP bid compete like bids from any two relays. A stream whose handshake times out or is refused, or that breaks before a bid, leaves the HTTP bid in the auction, and an operator with a slow link to the relay no longer needs a second HTTP entry for it. This replaces the HTTP fallback that ran only when the handshake failed with bid window left. cb_pbs_relay_stream_fallback_total keeps its name and now counts every stream that failed while the HTTP request ran alongside, including a window whose held bids all fail validation. relay_header_value reports the relay's best bid across both transports, and relay_last_slot is set when either delivers one. The four relay_stream metrics gain an endpoint label (get_header_stream) and help text that holds for any bid stream, and the auction winner log names the transport.
…line A relay honouring the timing headers ends the stream at Date-Milliseconds + X-Timeout-Ms, which CB set to its own read deadline, so every frame sent in the last one-way latency arrived after CB stopped reading. Measured against Helix with 7 rising frames: at 100 ms one-way 2 frames were lost, at 200 ms 4, and at 300 ms or more all 7, so HTTP served a 1.1 s old bid. The stream now opens TCP first and takes the connect time as the round trip (SYN to SYN-ACK, no clock skew), stamps Date-Milliseconds then, and sends X-Timeout-Ms as the time left to CB's deadline less half that round trip, before completing TLS and the upgrade on the same connection. CB's own deadline is unchanged; with nothing left after the connect, the stream times out as before. The reader adds the timing headers itself, so the ePBS stream gets the same behaviour.
A relay with get_header = "stream" now also streams its ePBS bids. Each
bid request addressed to it opens
ws(s)://<relay>/eth/v1/builder/execution_payload_bid_stream/{slot}/{parent_hash}/{parent_root}/{proposer_pubkey}
on the PBS stream reader, with the SignedBuilderRequestAuth as padded
standard base64 of its SSZ in X-Request-Auth, and races it against the
relay's HTTP request. The bid with the higher value + execution_payment
wins, unclamped, and the stream's bid wins a tie. A builder's HTTP 400
or 401 reaches the proposer only when neither leg has a bid. A builder
Commit-Boost dials is asked over HTTP only. The stream's bid keeps its
SSZ frame, so a beacon node that asks for SSZ gets it unchanged. Each
frame is decoded on arrival, so the stream holds only its newest bid.
A streamed request can last until the deadline less
proposer_deadline_buffer_ms, which #506 requires to be above 0. A failed
stream is logged and counted in one place, and the race's docs mark what
is unreleased.
The stream reports the bid stream series under
endpoint="get_execution_payload_bid_stream". The ePBS page gets a Bid
streaming section and two troubleshooting rows: a relay that streams
get_header but not ePBS bids, and a stream that times out or cannot
connect.
JasonVranek
added this pull request to stack #507
October 9, 2026 19:27
This branch has not been deployed
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #506. A relay entry with
get_header = "stream"reads that relay's bids over a websocket. This PR:getExecutionPayloadBidalongside its stream on every request rather than only retrying HTTP if the handshake fails with time left (what the current PBS implementation does and is fragile for high latency connections).Relays with
get_header = "http", the default, behave as before. No new config keys or metric names.How it works
ws(s)://<relay>/eth/v1/builder/execution_payload_bid_stream/{slot}/{parent_hash}/{parent_root}/{proposer_pubkey}. TheSignedBuilderRequestAuthgoes inX-Request-Auth(padded standard base64 of its SSZ), since an upgrade has no body. Frames use the PBSheader_streamlayout.value + execution_payment, the stream's on a tie. Each transport offers its latest bid that decodes (PBS: the latest of the last 8 that validates). The beacon node still validates the bid and applies the key's cap.proposer_deadline_buffer_ms. The handshake'sX-Timeout-Mssubtracts an estimate of one trip from the relay, so the last bids arrive in time: without it, during testing, 2 or 3 of 7 rising bids were lost per slot at 100 ms one-way latency, and all 7 in 7 of 8 slots at 300 ms.400or401reaches the beacon node only when the stream has no bid. The stream's SSZ bid passes through unchanged.Commits
merge: the PBS bid stream race: five commits written on v0.11.0-rc3 (a7b273d..48ebe07). The ePBS stream reads through the generic stream reader they add, so they land together.git show --remerge-diff <merge>shows only the hand resolutions.feat(pbs): stream the ePBS bid alongside the HTTP bid request: the ePBS stream, tests and docs.Testing
cargo test --all-features: 427 pass (413 at the merge alone).204with no failed streams; Helix ended every stream with a close frame. Helix refused with a4xxall 7 bad handshakes sent to it directly (wrong signer or pubkey, corrupted signature, wrong slot, missing or malformedX-Request-Auth).wssrelay (the devnet streams over plainws), and real network latency (the loss figures above come from netem against the same Helix setup).