Skip to content

Add a block-building role to helix-builder #550

Description

@0w3n-d

Status

helix-builder has two config-selected roles today: merging (merges
relay-supplied blocks) and simulation (validates relay submissions over
SSZ, #527). Neither builds a block. There is no way to drive the relay's own
submission path — submit -> simulate -> get_header -> get_payload ->
publish — on a testnet without an external builder.

This issue adds a third role, building: it builds a block for the next slot
and submits it to the relay.

Why

To exercise the relay end to end on testnets with a builder we control.

Affected surface

  • crates/builder/src/config.rsRoles becomes a struct of three
    Options. As an enum, a third role needs seven variants.
  • crates/builder/src/cli.rs--build.config.
  • crates/builder/src/main.rs — spawn the role on the tokio runtime before
    spine.start(...), which blocks until its tiles stop.
  • crates/builder/src/building/ (new) — slot context, block assembly,
    submission, slot loop.
  • crates/builder/src/engine/convert.rs — add the ethrex BlobsBundle ->
    helix_types::BlobsBundle conversion, the missing inverse of eblobs.

Reused unchanged: node::start (store, Arc<Blockchain> with a devp2p-fed
mempool, head watch), engine/convert.rs,
helix_types::{payload_from_v3, requests_from_v4},
helix_common::signing::RelaySigningContext::sign_builder_message,
helix_common::beacon::types::chain::PayloadAttributesEvent,
and testing.rs.

The ethrex primitives are all pub and all already driven by
engine/session.rs: create_payload, PayloadBuildContext,
apply_system_operations, fill_transactions, apply_tx_to_payload,
extract_requests, apply_withdrawals, finalize_payload.

Design decisions

The builder is the coinbase, and the block ends with a payout to the
proposer.
The simpler option — set the coinbase to the proposer's fee
recipient and omit the payout tx — passes our validator's paid_by_balance,
and the relay does not constrain the payload's coinbase. But the relay rejects
value == 0 (ZeroValueBlock), so on an idle testnet that builder submits
nothing, which is when we most want it submitting. Instead:

payout = tips (ctx.block_value) - payout_gas_reserve * base_fee + subsidy_wei

subsidy_wei guarantees a non-zero bid every slot. This is also the model that
bidding and a builder cut need later, so stage 2 needs no rework.

Gas for the payout is reserved by lowering ctx.remaining_gas before
fill_transactions and restoring it after. fill_transactions gates only on
that field, and it is pub. No ethrex change is needed.

Slot data comes from two sources, as HeadInfo carries no consensus
fields: the beacon node's payload_attributes SSE topic (parent hash,
timestamp, prev_randao, withdrawals, parent_beacon_block_root) and the
relay's get_validators (proposer pubkey, fee recipient, registered gas
limit). Passing the registered gas limit as BuildPayloadArgs::gas_ceil gets
the 1/1024 clamp for free, since create_payload applies calc_gas_limit.

New env vars. RELAY_KEY is already used for two different key types: the
merging role parses it as secp256k1, helix_common::config::load_keypair
parses it as BLS. The building role needs both kinds, so it takes
BUILDER_BLS_KEY and BUILDER_PAYOUT_KEY, loaded eagerly before the node
boots.

Steps (each becomes one PR)

  • Step 1: Roles becomes a struct; add BuildingConfig,
    --build.config, build-config.example.yml, and eager loading of both
    key env vars (tests: config parse and reject, resolve() combinations)
    (PR: Select the building role from a supplied config #554)

  • Step 2: slot context — payload_attributes SSE client plus a
    get_validators poll, merged into one SlotContext (tests: merge,
    missing duty skips the slot, duplicate/stale event ignored, duty refresh
    across epochs) (PR: Merge beacon payload attributes with relay duties into a slot context #556)

  • Step 3: block assembly — create_payload -> context -> system ops ->
    reserve gas -> fill_transactions -> payout tx -> requests ->
    withdrawals -> finalize_payload (tests: mempool txs included; payout is
    last and pays exactly the bid; the reservation holds under a full block;
    zero payout refused; a failing payout aborts the slot) (PR: Build a block for each slot from the node's mempool #557)

  • Step 4: submission — blobs conversion, BidTrace, BLS signature, SSZ
    POST /relay/v1/builder/blocks (tests: SSZ round-trip, signature
    verifies under the builder domain, BidTrace mirrors the payload,
    blobs rejected without 128 cell proofs per blob) (PR: Sign the built block and submit it to the relay #558)

  • Step 5: the slot loop — build and submit at configured offsets, and
    resubmit only on a strictly higher value (tests: scheduling, including a
    late event; the improvement gate, including a re-orged parent)
    (PR: Build at each configured offset and resubmit only on a higher value #559)

    Self-validation was dropped: assembly already executes every
    transaction and computes the state root, so re-simulating in the slot
    buys nothing and costs latency. `self_validate` went with it.
    
  • Step 6: document the third role in the README (PR: Document the builder's third role #560)

Deliberately out of scope

Bundles and eth_sendBundle; custom ordering (stage 1 uses ethrex's tip-sorted
fill_transactions); cancellations and replacement; top-of-block simulation and
the nonce-dependency DAG; optimistic submission and bid adjustments;
multi-relay submission; keeping a builder cut. The MVP is shaped so each is
additive.

Known limitations to carry into the PRs

  • The payout gas limit is a fixed payout_gas_reserve (default 21000). A
    contract fee recipient that needs more makes the payout fail, and the slot is
    skipped.
  • Blob transactions are included only when their sidecar arrived over devp2p,
    because sidecars come from the mempool.
  • ethrex's impl AddAssign for BlobsBundle does not propagate version, so an
    aggregated bundle always claims version 0. Assert 128 proofs per blob instead
    of trusting that field.

Open questions

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions