Repository navigation
feat(rpc): serve and publish builder blocks (fulu) - #704
MegaRedHand wants to merge 12 commits into
Conversation
The Engine API's fulu answer carries the execution client's wish to build locally whatever a builder pays; the type dropped it, so the builder flow could not honour it.
Block production asks the builder only for proposers that registered through this node, and needs their registered gas limit to check a bid. A newer registration replaces an older one, and one not resent for two epochs is gone, since validator clients resend every epoch.
builder-specs requires proposers to stop using builders when the chain looks unhealthy and leaves the conditions to the client. These follow Lighthouse's defaults and read only the proposal state, where an empty slot shows as a block root repeated from the slot before.
A bid must pay something, sit on the proposal's parent with its prev_randao, timestamp and withdrawals, carry blob gas for exactly its blobs, step its gas limit toward the proposer's registered target, and be signed by its builder. Each refusal has its own reason so the metric can tell a misbehaving relay from a misconfigured one. The gas rule is gloas's gossip predicate, pinned to Lighthouse's vectors.
beacon-APIs' builder_boost_factor rule taken literally, with the execution client's shouldOverrideBuilder above it and a tie going to the local payload, which never depends on a relay to reveal it.
The builder client, the registrations it was sent, the circuit breaker's thresholds and the sync status travel together, layered only when --builder is set, so every handler and test router without one keeps the local path untouched. register_validator now records what it forwards, which block production reads next. The breaker's thresholds are flags, defaulting to Lighthouse's.
With a builder configured, the proposer registered and the circuit breaker quiet, the bid is fetched alongside the local payload. A bid that validates is built into a blinded block, the state root computed by assemble_blinded_block, and wins if it pays more after the caller's builder_boost_factor; the execution client's shouldOverrideBuilder keeps the local payload whatever the bid. Either side failing leaves the other, and with no builder in play nothing changes.
publishBlindedBlockV2 checks a blinded block the way publishBlock checks a full one, the signature through a root-based check the two now share, then submits it on its own task so a validator client hanging up cannot cancel the reveal. Both endpoints claim the slot first: a second, different block for a slot is refused, since this node handing out two would get its validator slashed.
produceBlockV3's builder path, the blinded_blocks endpoint and the slot guard both publish endpoints share, the new metrics, and the one deviation from builder-specs: the bid's fee recipient is not checked, since relays pay the proposer with a transaction instead.
A claim below the finalized slot was inserted and then pruned, so the same slot could be claimed again with a different root; it is now refused. The blinded_blocks route gets the publish body limit, and the builder's answer to a signed block is logged from the submission task, which survives a validator client hanging up. A bid's block number is checked against the parent's, which is always known. produceBlockV3 now checks the local payload and the bid without a state transition, picks one, and builds only that block, falling back to the other only if the first fails to build. Tests now assert the blinded header, the value and the number of getHeader calls, and cover the cross-endpoint guard, a failed submit, an unreachable relay and both sides failing, plus the circuit breaker's edges, bid signature variants and the fallback flags.
The 413 test passed with the body limit removed, since its body was over axum's default too; a 3 MiB sibling now fails without the layer. Also covers a winning bid whose block and the local block both fail to build, tightens the status assertions of the no-block and unreachable-builder cases, and skips the packing, whose sync aggregate costs a BLS verification, when there is nothing to build a block from. A refused second block now logs the root that holds the slot.
🤖 Kimi Code ReviewI'll review this PR which implements the Fulu builder flow (MEV-Boost integration) for ethlambda. Let me analyze the key components systematically. Overall AssessmentThis is a substantial, well-structured PR adding builder/MEV-Boost support. The code is generally high quality with good test coverage, but I found several correctness and safety issues that need attention. Critical Issues1. Integer Overflow in
|
| Severity | Count | Items |
|---|---|---|
| Critical | 1 | Item 1: Integer overflow/precision loss in boost calculation |
| High | 1 | Item 7: Division by zero-like behavior in choice (same root cause) |
| Medium | 3 | Items 12, 13, 14 |
| Low/Style | 5 | Items 15, 16, 17, 18, 10 |
Required Changes
- Fix
choice.rsline 58 — usesaturating_mul(bid).saturating_div(100)instead ofsaturating_mul(bid / 100) - Add error logging in
post_blinded_blockforadvance_to_slotfailure - Fix typo "gloas" → "gas" in bid.rs comment
- Verify/document
submit_blinded_blocktimeout behavior - Add cross-reference in
rpc.mdtospec_deviations.mdfor fee recipient
Good Practices Observed
- Comprehensive test coverage with property-based style tests
- Clear separation of concerns (bid validation, breaker, choice, cache)
- Proper use of
tokio::spawnfor builder submission to prevent client hangup cancellation PublishedProposalsprevents equivocation across full and blinded block paths- Metrics for
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
🤖 Codex Code ReviewOverall this looks solid. The new builder flow is thoughtfully guarded: bid validation is fairly strict, the anti-equivocation slot claim is shared across both publish paths, and the circuit-breaker logic is explicit and tested.
If you want, I can turn these into ready-to-post GitHub review comments. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewReview of PR 704: builder flow for fulu block productionThis covers the builder bid fetch, bid validation, the circuit breaker, the local-vs-builder choice, What looks good
Minor points
Tests Automated review by Claude (Anthropic) · sonnet · custom prompt |
Fourth step of MEV-Boost builder support. With this PR, a fulu proposer registered through
register_validatorcan receive a builder's blinded block fromproduceBlockV3and publish it back throughblinded_blocks. Stacked on #696.Scope: the beacon node only. The validator client does not yet register, ask for blinded blocks or publish them; that is the next PR. Gloas (the builder API alongside gossip bids) comes after.
What it adds
shouldOverrideBuilderengine_getPayloadV5, carried on the built payload--builder-fallback-skips(3) consecutive empty slots, on more than--builder-fallback-skips-per-epoch(8) empty slots in the last 32, or on more than--builder-fallback-epochs-since-finalization(3) epochs since the state's finalized checkpointDOMAIN_APPLICATION_BUILDER). Every refusal is counted by reasonbuilder_boost_factor. The local payload wins ifflocal >= factor * bid / 100. A tie goes local;shouldOverrideBuilderbeats everything;u64::MAXmeans builder; the default factor is 100. The winner is chosen before assembly, so only one state transition runs. If the chosen block fails to assemble, the other one is used. This includes a local choice falling back to the bid, which Lighthouse would fail instead, following beacon-APIs' "prefer local unless it is not viable"produceBlockV3Eth-Execution-Payload-Blinded: trueand the bid's value. A local win answers as beforePOST /eth/v2/beacon/blinded_blockspost_block, the gloaspost_blockandblinded_blocksall claim the slot after the signature check and before anything leaves the node. A different root for a claimed slot gets a 400, and so does a slot below the finalized slotlean_beacon_builder_bid_rejections_total{reason},lean_beacon_builder_payload_source_total{source},lean_beacon_builder_fallback_total{reason}Known limitations
--builder-header-timeout(1 s), so it adds at most the difference.docs/spec_deviations.md);Tests
block_rootswrap.produceBlockV3against the mock relay:blinded_blocks:make lintpasses, as do the rpc lib tests and the binary's tests.