feat(beacon): gloas builder market: bid and preference gossip, endpoints, building on bids - #669
MegaRedHand wants to merge 15 commits into
Conversation
…ences, from JSON `Builder` serialized `version`, `balance`, `deposit_epoch` and `withdrawable_epoch` as bare numbers, which the Beacon API wants quoted (this also affected the gloas state's JSON). Give it the same `quoted_or_bare` adapters the other containers use, and `Deserialize`, along with the proposer preferences containers a builder-market endpoint takes in a request body. Round-trip tests cover the bid, the preferences and the builder, and the example bid and preferences events from beacon-APIs' event stream parse.
…and in parallel Adds the shared `BuilderMarket`, the `execution_payload_bid` and `proposer_preferences` gossip rule modules, the p2p triage, verdict and publish plumbing, the `RpcToP2P` publish methods, and the Beacon API route modules, all with the final signatures and placeholder bodies: every rule answers `Ignore(NoConsumer)`, the market holds nothing, and no route is registered yet. Splitting the wiring from the logic lets the rules and market, the p2p handling and the Beacon API be filled in on separate branches without touching each other's files. New reason variants, `Validated` arms and `RpcToP2P` methods sit beside the envelope ones, and everything else is a new file, so a parallel sync committee branch merges cleanly.
Bids, proposer preferences and revealed execution payloads are judged and consumed from three places (gossip rules on blocking threads, the Beacon API and block production), so they live in one shared object rather than in the chain actor. The market keeps the spec's seen sets and best-bid bar apart from the pooled bids: the pool is truncated to the top values per parent, but the bar a new bid must strictly beat is not. Known payloads are a bounded LRU, and every collection is bounded so spam cannot grow memory. Also adds the test-utils fixtures (builder registry state, bid and preference signing, envelopes) the gossip rules, p2p and rpc tests build on.
…nces Subscribe the execution_payload_bid and proposer_preferences topics under gloas digests only, decode them, triage them (size cap, decode, cheap rules) and hand the rest to the blocking pool on a permit pool of their own so a bid burst cannot starve blocks, columns or attestations. Neither type reaches the chain actor; accepted ones are recorded in the shared builder market. Envelopes accepted from gossip, and envelopes this node publishes itself, are recorded as known payloads, since bid validation reads them and gossip never echoes a node's own message. Bids and preferences are published on the digest of their own slot, so preferences for the first gloas epoch go out on the gloas digest during the epoch before the fork.
An engine that wants its own payload used whatever a builder pays says so with this flag; dropping it would let a bid comparison override that.
Add POST execution_payload_bids, POST proposer_preferences and POST
states/{id}/builders. Bids and preferences run through the gossip rules and
land in the shared builder market that p2p also fills.
produceBlockV4 now reads the real BuilderConfig, builds locally when it has an
engine and takes the best pooled bid when the config's min_bid and boost factor
say so (the local payload wins ties, and shouldOverrideBuilder keeps it). A
bid-won block comes back bare and caches nothing, since the builder reveals the
payload. A self-built payload takes its fee recipient and gas target from the
proposer's signed preferences when the market holds them.
…o end Cover the three endpoints, the verdicts they turn into 400s, idempotent resubmission, SSZ bodies, produceBlockV4 choosing between a stand-in engine's payload and a pooled bid, the preferences-driven fee recipient and gas target, and the envelope endpoints' behaviour for a block built on a bid.
… blocks on a bid Gives the builder market its rules: the spec's execution_payload_bid and proposer_preferences gossip validation, split into cheap and stateful halves like the other topics, plus block assembly on a builder's bid. The rules read cached states only and never queue, so a verdict gossipsub waits on is never stalled by a replay. Two shortcuts keep that honest: the parent's own state stands in for the advanced one within its epoch (gloas's process_slot touches none of the fields the later rules read), and a proposer's lookahead comes from the head state when it shares the dependent root, since a dependent block about an epoch old is usually out of the state cache. Preference signatures verify under the fork versions on both sides of the gloas boundary, because clients disagree there. assemble_gloas_block_on_bid and bid_is_includable let produceBlockV4 commit a block to another builder's bid; the pre-filter keeps a bid that would fail process_block from being tried. Both gossip vector handlers now run (44 and 26 mainnet cases pass). The runner records a delivered envelope the way the spec generators do, without re-verifying it, and applies the fixture's finalized-checkpoint override to the stored states, which is how those generators activate builders. docs/spec_deviations.md lists what departs from the spec.
CLAUDE.md still said gloas added two topics and that duties were self-build only.
The spec ignores a bid whose value does not exceed the best recorded for the same (slot, parent hash, parent root), so a value-10 bid after a value-10 one answered NotHighestBid, not Accept. The fixture was wrong; the rule is spec.
🤖 Claude Code ReviewReview of PR 669: gloas builder market (bids, proposer preferences,
|
🤖 Codex Code Review
Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Kimi Code ReviewI'll review this PR systematically, focusing on correctness, security, performance, and Rust best practices for consensus-critical code. OverviewThis PR implements the Gloas builder market: gossip validation and Beacon API endpoints for Critical Issues1. Race condition in bid recording (
|
| Priority | Item | File | Line | Issue |
|---|---|---|---|---|
| Critical | 3 | builder_market.rs |
82-86 | Poison-ignoring lock helper |
| Critical | 4 | execution_payload_bid.rs |
395-402 | BLS error conflation |
| High | 5 | bids.rs |
108-110 | Missing JSON size limit |
| High | 11 | execution_payload_bid.rs |
187-196 | Stale payload status |
| Medium | 2 | bid_selection.rs |
52-56 | Verify division matches spec |
| Medium | 9 | execution_payload_bid.rs |
47-62 | Verify gas limit edge cases |
| Medium | 13 | builder_market.rs |
112-117 | Sorting performance |
| Low | 18 | gloas_proposal.rs |
376-386 | Large enum variant |
| Low | 20 | builder_config.rs |
40-48 | Unused field |
The PR is well-structured and extensively tested. The main concerns are the
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
…zed epoch The scene put a funded, active builder in an epoch-1 state: it is active only once the finalized epoch passes its deposit epoch, but a chain never finalizes past the previous epoch, so no real chain holds that state. Advancing it through the end of epoch 1 underflows get_previous_epoch - finalized epoch, which the spec treats as invalid and a stricter get_finality_delay now errors on. scene_at builds the parent in a later epoch by advancing with nothing finalized and finalizing epoch 1 afterwards. The epoch-crossing bid test uses it with its slots and epochs shifted by one epoch; its assertions are unchanged.
…633-636-638-gloas-live Both the sync committee (#668) and the builder market sides are carried through every shared surface: P2P::spawn, P2PServer, BeaconApiHandles, RecordingNetwork, the Validated enum and its matches, the gossip reason enums, the spec gossip runner (no bid, preferences or sync handler is ignored) and the docs. Sync and builder gossip each validate on their own permit pool. Non-obvious resolutions: - GloasBidBlockInputs gains sync_aggregate and assemble_gloas_block_on_bid uses it. assemble_on_bid verifies the pooled aggregate for (slot - 1, parent_root) against the block's pre-state and falls back to the empty aggregate on every retry path, as the self-build does. - produce() runs the local build and the execution client version lookup concurrently, each skipped when no engine is configured, and the bid selection reads the pooled sync candidate. - Test fixtures follow tmp: no attestation pool extension, the sync pool and OwnVersion layers in the builder market app, BuiltGloasPayload and Prepared initializers with the builder market fields, and the ActiveBalanceCache argument of gloas process_block. - New test: a block built on a bid carries the pooled sync aggregate. Known failure: gossip::execution_payload_bid::tests:: a_bid_across_an_epoch_uses_and_caches_the_checkpoint_state fails. Its scene has finalized_checkpoint.epoch 1 at slot 32, and tmp's get_finality_delay now errors when the finalized epoch is past the previous epoch, so advancing the scene to epoch 2 returns StateUnavailable. The fixture state is unreachable on a real chain; left unchanged pending a decision.
…626-63-64-633-636-638-gloas-live
Motivation
Gloas makes payload building a market: builders gossip signed bids on
execution_payload_bid, proposers gossip signedproposer_preferences(fee recipient, gas target) so builders can bid for their slots, and a proposer may build on the best bid instead of its own payload. Our node subscribed to neither topic, served none of the endpoints (lighthouse and prysm validator clients already callPOST /eth/v1/validator/proposer_preferencesand got a 404), and always self-built. This adds the node side of the market. Stacked on #667.Changes
execution_payload_bidandproposer_preferencesunder gloas digests: every rule of gloasp2p-interface.md, seen caches, their own validation permit pool, relayed on accept. Never queued, never sent to the chain actor.SharedBuilderMarketshared by p2p and rpc: proposer preferences per proposal slot, the best bids per slot and parent, and the known execution payloads the bid rules check parents against (gossip envelopes plus the node's own). Bounded and pruned.POST /eth/v1/beacon/execution_payload_bids(JSON or SSZ, gossip-validated, published),POST /eth/v1/validator/proposer_preferences(verified, cached, published),POST /eth/v1/beacon/states/{state_id}/builders(filterable builder registry).produceBlockV4BuilderConfig: the best gossiped bid competes with the local build undermin_bidandbuilder_boost_factor(local wins ties,shouldOverrideBuilderrespected). A winning bid returns the block only (no envelope,Eth-Execution-Payload-Included: false); the builder releases the envelope. A bid that fails to assemble falls back to the local build.prepare_beacon_proposer, as beacon-APIs requires.Builderintegers are quoted in JSON (they were bare, also in the gloas state JSON); preferences deserialize.Phase 2 (not here): the builder API (bids fetched from
BuilderConfig.buildersURLs,POST /eth/v1/validator/builder_preferences, forwarding the block to the winning builder). That route is a 404 for now.Deviations (in
docs/spec_deviations.md)P - MIN_SEED_LOOKAHEADand atP, which coincide outside a fork's first epoch: lighthouse signs with the fork atP, the spec withP - 1, and refusing either would drop honest preferences at the gloas boundary.Validation
gossip_execution_payload_bid45/45,gossip_proposer_preferences27/27 (previously ignored). Minimal not run locally.process_blockaccepts it), topics, decode, verdict settle, publish, bid selection,BuilderConfig, and router tests driving every endpoint andproduceBlockV4against a fake execution client.--lib: state-transition 544, types 221, p2p 312, rpc 234, engine 27, validator 269, blockchain 236, storage 152; clippy and fmt clean.