Repository navigation
feat(beacon): builder-specs containers and the fulu blinded block's state root - #694
MegaRedHand wants to merge 18 commits into
Conversation
Registrations and builder bids are signed under builder-specs' DOMAIN_APPLICATION_BUILDER, an application domain outside the specification's own. The mask's doc said this crate never uses one; it now does, so the doc names it.
A blinded block carries the header of a payload this node never sees, and blinding a locally built block needs the same conversion. The header also needs Deserialize now, since bids and blinded blocks arrive as JSON.
Real encodings to decode against, so the builder containers are checked against what relays and other clients send rather than only against themselves.
Only the bid is a response; the registrations and the blinded block are request bodies, and the `value` key is OpenAPI's example wrapper. Record the full commit, since nothing else in the repo pins it. The header's JSON round trip moves beside the other round-trip tests.
The builder API's domain and gloas's in-protocol builder domain share a name and nothing else. Copying an existing get_domain call for the builder API would sign under the wrong one, and the only symptom is a relay rejecting every signature.
What a validator client signs and its beacon node forwards to relays through registerValidator, checked against builder-specs' example in both encodings.
The signed offer getHeader returns: a payload header, the blob commitments and execution requests that go into the body with it, and the value paid to the proposer.
The block a validator signs when a relay supplies the payload. Its root equals the full block's because a payload header's root is the payload's, which is what lets one signature serve both; the tests pin that, and builder-specs' example pins the encoding.
The root-equality test used a mostly empty body, and empty lists with the same limit share a root, so swapping two of them still passed under the minimal preset. Build it from the fully populated JSON test block instead. The example tests now also check the JSON this node would send a relay, not only the JSON it reads.
Registrations and bids are signed over their own roots, relays reject an older registration rather than requiring an unchanged one, and from_parts copies a state root its caller must recompute.
A blinded block needs the same body, framing and process_block run as a full one, then patches the post-state before taking its root. No behavior change.
The state transition takes full payloads only. Rather than thread a payload abstraction through it, run it on a stand-in payload built from the builder's header and patch the two post-state fields that depend on the payload itself. Safe because fulu, the last fork with blinded blocks, is final; the guard test fails on any drift.
The stand-in payload is what makes process_block's parent hash, prev_randao, timestamp and blob checks apply to a builder's header, and only if it copies the right fields. One test per check, plus the withdrawals root and the fork gate, which this path checks itself.
Both block paths pass a constant execution engine, so on the blinded path no one checks the builder's header beyond the fields the stand-in copies. That is right for the state root but easy to mistake for bid validation, so the doc now says the caller owns the rest.
The guard's state had no withdrawal due and its body no execution request, so the withdrawals root was only ever checked against the empty list and the "identical" rows of the doc's table were argued rather than tested. Six payload fields the stand-in leaves empty were empty in the test payload too.
They were single lines past the width the rest of the file wraps at.
The stand-in-and-patch approach is unusual (every other client threads a payload abstraction through its STF), so the next person to touch fulu's process_block should find the reason before the guard test does.
fulu_state's doc named a field it deliberately does not cover and a step that has since been written. assemble_blinded_block's list of what the caller must still check was short, and A2 codes against it. The rest are small accuracy fixes found reviewing the branch whole.
🤖 Codex Code ReviewOverall this looks solid: the blinded-block path is carefully documented, and the added guard test around state-root/root equivalence is exactly the right kind of consensus safety net.
One thing I’d still consider tightening:
No blocking issues from me based on the diff shown. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewReview of PR 694: builder-specs containers and I read the diff but did not build it or run the tests. I did not open the The PR is clean. Nothing calls the new public API yet, which limits the risk. I found no blocking bugs. What checks out
Minor points
Overall this looks good to merge, subject to the base-branch retarget the PR already notes. Automated review by Claude (Anthropic) · sonnet · custom prompt |
🤖 Kimi Code ReviewI'll review this PR for blinded block production in ethlambda. Let me analyze the key areas carefully. Overall AssessmentThis is a well-structured PR implementing MEV-Boost blinded block production for Fulu. The core approach of using a "stand-in" payload with patched post-state fields is sound and well-documented. However, I've identified several issues ranging from minor to potentially serious. Critical Issues1. Stand-in payload uses
|
| Priority | Issue | Location |
|---|---|---|
| High | Verify block_hash binding or document caller obligation |
block_production.rs:687-724 |
| Medium | Confirm ExecutionPayloadHeader::from matches spec root exactly |
deneb.rs:180-206 |
| Medium | Stand-in payload default values are fragile to future changes | block_production.rs:724 |
| Low | Unnecessary clone of header |
block_production.rs:743 |
| Low | Test refusal helper panics broadly |
block_production.rs:1525-1535 |
The PR is well-architected and the core approach is sound. The main concern is ensuring all header fields that could affect validity are either checked or documented as caller-checked before signing.
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
First step of MEV-Boost builder support: the containers the relay flow exchanges, and a way to compute a fulu blinded block's state root. Nothing calls the new public API yet; the next PR (the builder client and the
produceBlockV3builder path) does.Base is
nightly, notmain.assemble_blockas this extends it comes from #634, #646, #648, #668 and #678, which sit on separate stacks. This retargets tomainonce those merge.What it adds
DOMAIN_APPLICATION_BUILDER(0x00000001), cross-referenced with gloas'sDOMAIN_BEACON_BUILDERtypes/src/beacon/constants.rsExecutionPayloadHeader: From<&ExecutionPayload>andDeserializetypes/src/beacon/containers/deneb.rsValidatorRegistrationV1,BuilderBid,BlindedBeaconBlock{,Body}and their signed forms (electra/fulu shape)types/src/beacon/builder.rs(new)fce64468types/tests/fixtures/builder_specs/assemble_blocksplit intofull_body,unsigned_blockandpost_state, with no behavior changestate_transition/src/beacon/block_production.rsassemble_blinded_blockThe blinded state root: a stand-in payload and two patches
The state transition takes only full payloads. Other clients either generalize their STF over the payload or keep a separate blinded STF (Grandine).
assemble_blinded_blockdoes neither:header.withdrawals_rootagainst the expected withdrawals;process_blockon a body whose payload is a stand-in:parent_hash,prev_randaoandtimestampcopied from the builder's header, the expected withdrawals, everything else empty;latest_execution_payload_headerandlatest_block_header.body_root.It is fulu only. Fulu is the last fork with blinded blocks (gloas has none), so the
process_blockthe argument rests on cannot change under it. The function's doc walks through fulu'sprocess_blockstep by step.blinded_and_full_blocks_reach_the_same_state_rootis the guard. Around the same payload, the blinded block must equalBlindedBeaconBlock::from(full), which means the same state root and the same block root. Its state has a withdrawal due and its body a deposit request. Commenting out either patch makes it fail.Trust boundary
blob_gas_usedThe last column matches every client reviewed: only an execution client could check those fields, and the payload is revealed only after the proposer signs.
Tests
make lintandcargo test --workspace --profile release-fast --lib --binspass.Also
fulu_stateloses a stale#[allow(dead_code)]and its doc no longer contradicts the file: it already had a caller, and this adds a second.