adds beacon api - #81
Conversation
|
can we add the beacon api to the engine api tile? it seems overkill to have cores assigned to marshalling http requests and responses for each api? we could make this a component that is not an actual |
also - if we run the http server as a separate tile then we should just run Axum in a single thread runtime and not take on the maintenance risk of writing our own web-server. |
I do think the current tile setup is likely not optimal but we should probably wait until everything is running and we've gathered a bunch of data before deciding to consolidate tiles? When feature complete the system will have 3 integration points. p2p, Beacon api, and engine api. Currently it looks like we have 3 tiles for p2p stuff. Putting the Beacon and engine api in the same tile seems premature. |
I don't see why we'd go in that direction. That would pull in all of tokio and so on. Seems like the opposite to our approach elsewhere. The Beacon api is the interface to the validator client and commit boost and so on. If our design goals center low level, high performance, and innovation we probably shouldn't outsource a key interface to the axum, hypr, tokio stack. Axum is just a nice wrapper around hypr and hypr is just state machine that sits on the same two crates I'm using here. Mio and httparse. And unlike the p2p interface, this one is purely internal so minimal security risk. |
well yes to not predetermining the tiles - which is why it makes sense as much as possible to create components that are not tiles but can be called from tiles |
the concern is not security, its just maintaining code which implements something that is already available and widely used - I see no issue in running tokio in a separate tile - or separate process on the same machine - if its for non-hot-path things. |
|
I have worked with Claude.ai to prepare a new design for Beacon API - the basic principles have been added to this branch in d096b51. Comments below will provide more details. |
|
Design notes from Claude.ai Goal (from team, channeled by Bronek)
Mapped facts the designs rest on
Recommendation (Claude's, for team review)
|
First step of the client_server tile consolidation (docs/adr/0001): the HTTP/1.1 connection state machine (parse, keep-alive, pipelining, response framing) moves out of beacon_api into a new transport-free crate with bytes-only interfaces, so it can be tested without sockets and shared with the client role next. beacon_api keeps its tile, poll, and endpoints unchanged; behavior is byte-identical. Framing tests move with the machine and gain deterministic chunking coverage (single-byte feeds, pipelined requests split across feeds, oversize rejection, dispatch-after-drain). Assisted-by: Claude:claude-fable-5
|
Implementation plan from Claude.ai Commit sequenceStrategy: build the new transport alongside the old, migrate consumers one by one, consolidate tiles last. No commit mixes a move with a behavior change. C1 — C2 — httpcore client role; engine consumes it; delete ipc.rs. 42d86da C3 — beacon_api table dispatch + Request/Response/ApiCtx. e7cbd99 C4 — rename C5 — C6 — counting-allocator hot-path test + smoke. bd42255 |
Second step of the client_server consolidation (docs/adr/0001, 0002): the engine's connection byte machine (request framing, Content-Length response parsing, partial-I/O resumption) moves to silver_httpcore as the client-role sibling of the server machine, and the transport becomes the closed-set Stream enum (Tcp | Uds). The newline-framed ipc.rs (dead code) is deleted; Unix-socket support is now the same HTTP pool over Stream::Uds, proven by a real UDS round-trip test asserting the JWT bearer header on the wire. Engine keeps all protocol: pool policy, JSON-RPC, JWT, correlation, ReqKind dispatch. The newPayload transcode path is untouched (verified: one body copy before and after). Request framing is pinned by golden-byte tests captured from the previous implementation. New: EngineConfig::max_connections (default 32) bounds the previously unbounded pool; spine intake gates on pool capacity via consume_one, so excess requests wait on the queue. Healthcheck issuance gates on capacity too. A connect that cannot start (resolve/connect/register error) now fails the rpc through the normal error path instead of stranding it forever -- previously masked by unbounded pool growth, fatal under a cap. Behavior notes: an empty Content-Length value is now rejected instead of read as zero; the Connecting-state error checks for UDS follow the TCP shape (the old distrusting variant was unreachable dead code). Known limitation (follow-up tracked in Linear): no per-request deadline, so an EL that accepts requests but never responds can gate intake while the engine_reqs ring (1024 slots) overwrites oldest entries. Assisted-by: Claude:claude-fable-5
Third step of the client_server consolidation (docs/adr/0003): the inline exact-match path closure becomes a const route table -- (method, pattern, handler fn) compiled once at init into literal/param segments, linearly scanned, with zero-alloc borrowed params (inline capacity 4). The router owns 404 (byte-identical to before) and the new 405 for known-path/wrong- method -- previously the HTTP method was ignored entirely. Handlers own 400, and ApiCtx::read_state_or_503 pins the pre-bootstrap contract: a BeaconStateReader (now threaded from the beacon-state tile) answering None yields 503 with the beacon-api error JSON shape. Identity moves to body-bytes-plus-per-request framing; wire bytes are byte-identical, pinned by a golden test captured from the previous implementation. Duplicate patterns (modulo param names) and >4 params panic at init. Adding an endpoint is now one table row + one handler + one socket-free test through the table. Assisted-by: Claude:claude-fable-5
Names track current reality: since C2 the crate is a pure engine-API protocol client (JSON-RPC, JWT, correlation, ReqKind dispatch) over the shared silver_httpcore transport, and "Engine API" is the established name for the EL protocol it speaks. Package silver_engine becomes silver_engine_api. Purely mechanical; no logic changes. The EngineTile type and the spine-flow doc's "Engine" tile naming are untouched -- the tile itself dissolves in the upcoming consolidation commit, which owns that doc update. Assisted-by: Claude:claude-fable-5
Realizes docs/adr/0001: one spine-attached tile now hosts all API access. BeaconApiTile and EngineTile dissolve into transport-free-of-flux components -- BeaconApi (own mio Poll, now polled with Duration::ZERO: the 100 ms blocking poll is gone) and EngineApi (EngineTile's intake/spin logic verbatim; C2's pool and event paths untouched) -- composed by plain function calls in ClientServerTile::loop_body. The tile attaches at core 5; core 7 is freed. Server activity now feeds flux work-tracking (the old beacon tile ignored its adapter). Config: beacon_api_bind (default 0.0.0.0:5051, preserving today's behavior) via config file, builder, and --beacon-api-bind; binds parse as TCP addr or unix socket path (httpcore Bind/Listener, UDS serving included); execution_endpoint accepts http:// or a socket path, panicking on any other scheme. BeaconApi::local_addr exposes the resolved bind so tests bind port 0 and discover the ephemeral port. New integration tests drive the real tile over a real spine (SpineAdapter::connect_tile) with hand-cranked loop_body: identity served over real TCP and UDS sockets, and the merged-loop invariant from ADR 0004 gets its first test -- a beacon-api request served while four engine calls sit unanswered on a fake EL, with the FCU completion still correlating afterwards. The pool-cap test migrates to the merged tile intact. Accept now drains until WouldBlock (single-accept could strand a simultaneous second connection under edge-triggered registration), and EngineApi::spin no-ops without an EL client instead of panicking, since the merged loop calls it unconditionally in unsafe_no_el mode. Assisted-by: Claude:claude-fable-5
|
@vladimir-ea @0w3n-d note a limitation in the C5, we may want to revisit it: Single beacon_api bind As of C5 the node binds exactly ONE listener — TCP or UDS, chosen by |
Closes out the consolidation plan (C6). The newPayload transcode's
zero-allocation invariant finally gets a failing-capable test: a dedicated
integration binary installs a counting global allocator, warms every
buffer (scratch, connection write buffer, JWT second-cache, pending map)
through real UDS round trips against the fake EL, then asserts the next
send performs exactly zero heap allocations -- with the JWT cache's
wall-clock second handled by retry rather than a weakened assertion.
Server hardening: beacon_api_max_connections (default 64) accepts-and-
drops beyond the cap (leaving the backlog unaccepted would go silent
under edge-triggered registration until the next SYN). ServerConnection's
16 MiB eagerly-boxed read buffer becomes a 4 KiB lazily-doubling Vec with
the same hard cap and byte-identical rejection, and read_space now
compacts the partial tail to the buffer front -- previously a long-lived
pipelined keep-alive connection crept its offsets toward the cap and
would spuriously reject small requests (the new creep test feeds 2x the
cap in small requests and fails against the old code, which also could
not construct on a default test-thread stack).
GET /eth/v1/events is pinned as 404: v1 defers SSE, all surveyed
validator clients poll (.local/beacon-api-vc-surface.md). Real-socket
smoke coverage audited across {server,client} x {TCP,UDS}: all four
combinations already exercised; none added.
Assisted-by: Claude:claude-fable-5
agree that a single |
Probably OK for now. UDS would most likely be used for out bound connections to the EL, as EL and CL often run on the same machine. But on the listener side normally people run a VC on a different machine for security, maybe used by solo stakers or for testing but TCP is probably fine for those cases too. |
|
Proposed API scope by Claude.ai Beacon API: the validator-client surface (scoping beacon_api v1)Date: 2026-08-14. Method: primary sources only — ethereum/beacon-APIs OpenAPI at tag v4.0.0 (current stable, 2025-10-14, Fulu-era; v5.0.0-alpha.x = Gloas dev), plus source of five VCs: Lighthouse Caveat: PR's REST mode is still experimental and flag-gated ( Spec baseline (v4.0.0): v1 variants of pool attestations, aggregate_attestation, aggregate_and_proofs, and block/blinded-block publish are REMOVED (release notes, PR #549); block production is v3-only. So the Electra/Fulu-era surface is v2/v3 where marked below — silver never needs the v1 variants. Gloas (v5-alpha) later adds: v4 blocks, v2 proposer duties, ptc duties, payload_attestation_data, pool/payload_attestations, execution_payload_envelopes/bids, proposer_preferences, v2 node/version, topics head_v2 / execution_payload_available. LH/TK/PR already carry client code for these — out of v1 scope but the routing table should expect them. (a) Endpoint tableSpec link = path under github.com/ethereum/beacon-APIs/blob/v4.0.0/. Verdict: MUST = exercised by ≥1 surveyed VC in default-config normal operation; SHOULD = specific clients / common configs; OPT = niche, subcommand, or DVT. MUST — core duty cycle (all five VCs unless noted)
SHOULD — client- or config-specific
OPT — niche / out of normal operation
(b) /eth/v1/events per client — NO client hard-requires it
Conclusion: polling fallback exists everywhere; the penalty for omitting SSE is degraded attestation latency (no early attest), no reorg-triggered duty refresh, and constant reconnect hammering (VO every 1 s, PR ≤16 s, LH/NB retries) polluting logs on both sides. Union of topics to serve when implemented: (c) SSZ vs JSONMUST accept/serve SSZ (
JSON-only in practice (spec allows SSZ on some, no surveyed VC uses it): attestation_data, aggregate_attestation v2, aggregate_and_proofs v2, pool attestations v2 (NB batch mode v1 rule of thumb: SSZ decode on block publish, SSZ encode on block produce; JSON everywhere (incl. those two, for NB publish and any 415/downgrade path). Everything else JSON-only is spec-conformant and matches observed client behaviour. (d) Conclusion for the client_server tile design
|
note that you do not need an 'accept loop' for each listener - it is simply another token registered with the poll and you handle as an |
Yes, which is why I think this should be a cheap fix, so perhaps we just need to do it. |
Outbound (CL-114): EngineConfig::request_timeout_secs, default 12. Each pooled connection records its request's enqueue time; the poll sweep fails any request older than the deadline through the existing error path, freeing the connection and un-gating spine intake. Age is anchored at enqueue, so a request stuck behind a blackholed connect expires on the same clock. The default clears every per-method floor in the engine-api spec (1s getPayload-class, 8s newPayload/fcu, 10s getPayloadBodies) -- those floors are minimum waits before aborting, and this deadline is a wedge-breaker, not a latency target. Inbound (CL-115): Config::beacon_api_idle_timeout_secs, default 75 -- a keep-alive window spanning several 12s slots. Connections stamp activity on accept and on every read or written byte; a coarse sweep (at most once per second) reaps connections idle past the deadline, treating malformed, partial, and silent input uniformly: a stalled receiver is idle, a trickling-but-progressing peer is not. Reaped connections free their beacon_api_max_connections slot, closing the cap-exhaustion scenario. Assisted-by: Claude:claude-fable-5
beacon_api_bind becomes a list: a TOML array in the config file (default ["0.0.0.0:5051"], single-bind behavior unchanged), comma-delimited values on --beacon-api-bind (a comma cannot appear in a socket address and is pathological in a socket path). BeaconApi holds one listener per bind -- TCP and unix sockets side by side, multiple interfaces, several UDS paths with distinct permissions. Listeners occupy the reserved token range 0..n; connection tokens allocate above it and wrap back to it. The connection cap and idle sweep count connections across all listeners. Bind::parse now rejects a string that contains ':' but is not a valid socket address instead of silently treating it as a unix path: hostnames are not resolved, and with several binds a typo'd address would otherwise bind a stray socket file and half-serve rather than fail loudly at startup. An empty bind list panics at construction: a node with no API surface is the same class of misconfiguration as an unbindable address. Assisted-by: Claude:claude-fable-5
ADR-0002 records the QUIC/HTTP-3 rejection: QUIC mandates TLS 1.3 (RFC 9001), which the ADR already declares a non-goal, and no validator client speaks HTTP/3 -- noted so the alternative is not re-litigated. ADR-0004's SSE paragraph reflected a deferral the team has since reversed: /eth/v1/events will be served, in-process, as the single sanctioned exception to the materialized-response model, fed from a spine events queue. The 404 shipped today is interim behavior; implementation follows the initial endpoint surface, and the SSE design round will amend the ADR with the concrete mechanism. Both ADRs are still status: proposed, so they are amended in place rather than superseded. Assisted-by: Claude:claude-fable-5
|
Updated |
First M2 infrastructure commit (I1). ParsedRequest exposes the three request headers content negotiation needs -- Accept, Content-Type, Eth-Consensus-Version -- as borrowed fields (no general header map). A Query iterator percent-decodes key/value pairs, zero-alloc when no escape is present; '+' stays literal (RFC 3986, not form encoding), and malformed escapes pass through rather than panic. The parse path now distinguishes knowledge from ambiguity (CL-115's framing): definitively malformed input -- httparse errors including more than 64 headers, an unparseable or overflowing Content-Length -- gets an immediate 400-and-close instead of silently stalling until the idle sweep, while genuinely partial input still waits for more bytes. Verified against httparse 1.10.1 at every truncation offset that a request within limits can never be misclassified mid-stream. Side effect: an HTTP/2 preface now draws a 400 instead of a silent stall. Assisted-by: Claude:claude-fable-5
The comments motivating the 415 on registerValidator said Teku posts the registrations as application/octet-stream first and downgrades to JSON inside its 415 handler. Teku's production call site constructs the request with SSZ preference hardcoded off, so the registrations it sends are always JSON; the SSZ-first path its request class carries is reached only from tests. Its block publish does go SSZ-first for real. The 415 is still owed: the schema declares it for the endpoint's octet-stream body variant, and a client sending SSZ keys its downgrade on that code alone — the hardcoded flag flipping, or another client adopting SSZ-first, is what the branch protects against. proposer.v2.yaml names a single dependent root for every epoch, get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1): beacon-APIs #590 superseded the fork split #563 introduced, the head_v2 event having made it unnecessary by supplying the matching root directly. Silver holds Fulu states and later ones only, so the activation-boundary epoch that split's pre-Fulu branch existed for is out of contract here. v2 answers one epoch back unconditionally, and the fork epoch it consulted leaves ApiCtx with it. Assisted-by: Claude:claude-fable-5
|
Minor correction to the "Serve the validator client's receipt POSTs" 93cf03a . It justifies the 415 on The code is unaffected — the 415 is correct because this endpoint's schema declares it, whoever triggers it — and the code comments repeating the Teku claim were already rewritten in "Correct client-behaviour claims and the v2 dependent root". |
| /// An entry differing from its predecessor is a block of the slot's own; a | ||
| /// repeated one is a slot that carried none. Reading the entry itself asks | ||
| /// only that the ring still cover the slot, so an empty slot answers with the | ||
| /// root it repeats. | ||
| #[test] | ||
| fn block_roots_name_the_slots_that_carried_a_block() { | ||
| let empty = RECORDED_STATE_SLOT - 2; | ||
| let (g, id) = recorded_ring(Some(empty)); | ||
| let reader = g.view(id); | ||
| let state_slot = RECORDED_STATE_SLOT; | ||
|
|
||
| assert_eq!(reader.at_slot(empty), root_of(empty - 1), "the ring answers at the wrapped index"); | ||
|
|
||
| assert_eq!(reader.proposed_at(empty - 1, state_slot), Some(root_of(empty - 1))); | ||
| assert_eq!(reader.proposed_at(empty, state_slot), None); | ||
| assert_eq!(reader.proposed_at(empty + 1, state_slot), Some(root_of(empty + 1))); | ||
|
|
||
| assert_eq!(reader.recorded_at(empty, state_slot), Some(root_of(empty - 1))); | ||
| assert_eq!(reader.recorded_at(empty + 1, state_slot), Some(root_of(empty + 1))); | ||
| } | ||
|
|
||
| /// The ring covers the `SLOTS_PER_HISTORICAL_ROOT` slots below the state's own, | ||
| /// and naming a block needs its predecessor's entry too — so the floor itself | ||
| /// cannot be named however distinct its entry is, and the state's own slot has | ||
| /// no entry until the `process_slot` that leaves it. | ||
| #[test] | ||
| fn block_roots_bound_which_slots_can_be_named() { | ||
| let (g, id) = recorded_ring(None); | ||
| let reader = g.view(id); | ||
| let state_slot = RECORDED_STATE_SLOT; | ||
| let floor = state_slot - SLOTS_PER_HISTORICAL_ROOT as u64; | ||
|
|
||
| assert_eq!(reader.proposed_at(floor + 1, state_slot), Some(root_of(floor + 1))); | ||
| assert_eq!(reader.proposed_at(floor, state_slot), None, "the floor has no predecessor"); | ||
| assert_eq!(reader.proposed_at(floor - 1, state_slot), None, "below the floor"); | ||
| assert_eq!(reader.proposed_at(state_slot, state_slot), None, "the state's own slot"); | ||
| assert_eq!(reader.proposed_at(state_slot + 1, state_slot), None, "past it"); | ||
|
|
||
| assert_eq!(reader.recorded_at(floor, state_slot), Some(root_of(floor))); | ||
| assert_eq!(reader.recorded_at(floor - 1, state_slot), None, "below the floor"); | ||
| assert_eq!(reader.recorded_at(state_slot, state_slot), None, "the state's own slot"); | ||
| } | ||
|
|
||
| /// A block's reveal accumulates into the current epoch's bucket; the boundary |
| Phase0, | ||
| Altair, | ||
| Bellatrix, | ||
| Capella, | ||
| Deneb, | ||
| Electra, |
There was a problem hiding this comment.
given that we aren't able to support these forks I would prefer to remove this
| #[serde(default = "default_fork_version::<0x01000000>", with = "hex_0x")] | ||
| pub altair_fork_version: [u8; 4], | ||
| #[serde(default = "default_u64::<74240>")] | ||
| pub altair_fork_epoch: u64, | ||
| #[serde(default = "default_fork_version::<0x02000000>", with = "hex_0x")] | ||
| pub bellatrix_fork_version: [u8; 4], | ||
| #[serde(default = "default_u64::<144896>")] | ||
| pub bellatrix_fork_epoch: u64, |
| #[test] | ||
| fn every_fork_field_is_overridable() { | ||
| let spec: SpecConfig = toml::from_str( | ||
| r#" | ||
| ALTAIR_FORK_VERSION = "0x20000910" | ||
| ALTAIR_FORK_EPOCH = 0 | ||
| FULU_FORK_EPOCH = 50688 | ||
| DEPOSIT_CHAIN_ID = 560048 | ||
| "#, | ||
| ) | ||
| .unwrap(); | ||
| assert_eq!(spec.altair_fork_version, [0x20, 0x00, 0x09, 0x10]); | ||
| assert_eq!(spec.altair_fork_epoch, 0); | ||
| assert_eq!(spec.fulu_fork_epoch, 50688); | ||
| assert_eq!(spec.deposit_chain_id, 560048); | ||
| assert_eq!(spec.bellatrix_fork_epoch, 144896, "untouched fields keep the mainnet default"); | ||
| } |
There was a problem hiding this comment.
doesn't this just test the parser?
| #[derive(Clone, Copy, Default)] | ||
| struct ControlInner { | ||
| state_id: Option<StateId>, | ||
| head_block_root: B256, |
There was a problem hiding this comment.
This field is the coupling you flagged on the tile's status_event — a value no state read uses, in the control word every state read goes through. Agreed it should not be here; it comes out once the head root is published per accepted block on BeaconStateEvent::Status. Answered in full there: #81 (comment)
|
@clanky-gattaca review |
The beacon-api server and the engine-api client each owned an mio `Poll`, so every `loop_body` of the application-boundary tile made two `epoll_wait` calls to learn about sockets on one thread. Both now register through one `Readiness`, and a single wait feeds both dispatches. Sharing a loop shares a token space, and a token both tenants could allocate would deliver one tenant's socket readiness into the other's dispatch — where an API client hanging up fails the engine call that happens to share its number. `TokenRange` partitions the space instead: each tenant takes `share(index, TENANTS)`, allocates only inside its own range, and skips events whose token falls outside it. Both halves are enforced rather than documented: `at` asserts the offset is inside the span, and each tenant asserts at construction that its range holds every socket it can register at once — listeners plus the connection cap for the server, the pool cap plus the first-run healthcheck's overshoot for the client. The server's connection offsets recycle over its range above the listeners. Connections close in any order while the cursor only advances, so the offset it wraps onto may still be held; the allocator probes forward past live offsets, and the construction assert is what guarantees it lands on a free one rather than replacing a live connection's map entry. Because a tenant now sees only the batch the shared wait produced, the order inside `loop_body` decides latency: taking a request off the spine flips its pooled connection's interest to WRITABLE, so the wait runs after that intake and the request goes on the wire in the iteration that took it. Measured on the tile's own tests, produce-to-wire is 1 iteration per request where waiting first cost 2, and 1000 iterations make exactly 1000 `epoll_wait` calls against one epoll instance, down from 2000 against two. The engine's dispatch walks that batch once, indexing connections by token offset, instead of scanning every event once per connection — the batch carries the server's events too, so the old shape cost O(connections x events) over both tenants' sockets. Limits: the timeout stays zero, so the loop still busy-polls; a blocking wait needs an `mio::Waker` on the flux work signal, which is separate. The healthcheck is enqueued inside the engine's spin, after the wait, so it alone still reaches the EL an iteration late. And `share` truncates: `usize::MAX / count` leaves the tokens above the last share owned by nobody, which costs nothing while nothing allocates there. ADR 0004's amendment described this loop as outstanding work; it now describes the loop, leaving the waker and the timeout as what remains. Assisted-by: Claude:claude-opus-5
| u64::MAX | ||
| /// Mainnet deposit contract, live since 2020-11-04. Hoodi reuses the very | ||
| /// same address. | ||
| fn default_deposit_contract_address() -> [u8; 20] { |
There was a problem hiding this comment.
Why not a constant with array? Also from where we get that numbers?
Every endpoint that needed a block store, the validator registry, duty
shuffling, liveness tracking or in-process peer counts answers a routed
501 instead of an answer assembled from the wrong data: blocks/{id}/root,
headers/{id}, the three validators reads, both proposer-duties versions,
sync duties, liveness, and node/peer_count. The route stays in the table
so a client can tell "this node does not serve it" from "no such
endpoint". Each surface returns in its own PR.
What each stub cuts out:
- The validators reads ran a full-registry scan inside the seqlock read
closure — ~2M entries and ~0.9s with render at mainnet scale, against a
reader contract whose lock-free budget is a handful of scalar pulls,
while any ready connection multiplies the engine-call delay through the
shared loop_body. ADR-0004 gains the amendment recording the deferral;
the ~1GiB/~0.9s figures stand as the cost a bounded-render design must
answer.
- Liveness answered not-live for every validator, which is the direction
that silently clears a validator client's doppelganger protection.
Refusing loudly protects better than answering from data silver does
not track.
- node/peer_count read the PeerCounters shmem gauges across tiles — a
second shared-memory read path beside BeaconStateReader, an
application_boundary → silver_peer edge that existed only for it, and a
gauge-zeroing in PeerManager::new against stale mapped files. All three
revert; peer counts can ride the existing peer_stats queue instead.
- The duties and block reads consumed the head-root plumbing (control-word
staging, ring predicates) that the next commit removes at the source.
The fire-and-forget POSTs stay: parsing and acknowledging is the whole
answer their schemas owe. So do the statics, node status, genesis, and
the two per-state scalar reads, which fit the read closure's budget.
Assisted-by: Claude Code:claude-fable-5
The seqlock control word carried the root of the block the published state was applied from, staged by the tile as each block landed, so a beacon-API request could name the head. With the block and duties endpoints answering 501, nothing reads it — and it was the wrong value anyway: fork choice re-heads on attestations without publishing, so the last applied block and the head diverge whenever an import lands on a branch fork choice passes over. A tile conclusion has no place in a tier-1 data structure; whichever design serves those endpoints next carries the head another way. The ring and finality predicates that existed to interpret that root go with it: RootsView::recorded_at/proposed_at, EpochView::finalizes_slot and finalized_block_root. What stays is what the served surface still reads: BeaconStateEvent::Status carries head_optimistic, computed from fork choice where the verdict lives, because node/syncing's is_optimistic answers from it; EPOCHS_PER_SYNC_COMMITTEE_PERIOD stays in the data crate because config/spec renders it. Assisted-by: Claude Code:claude-fable-5
Assisted-by: Claude Code:claude-fable-5
| use silver_httpcore::{Bind, Readiness, TokenRange}; | ||
|
|
||
| /// A tenant added here takes the next share of a raised `TENANTS`, which keeps | ||
| /// every share disjoint without a base to compute. |
There was a problem hiding this comment.
i have no idea what this comment means - what is a tenant and what is a 'TENANTS'?
| use crate::json::Json; | ||
|
|
||
| const PRESET_BASE: &str = "mainnet"; | ||
|
|
There was a problem hiding this comment.
why do we need all of these in this tile? are these all things served on the beacon api?
Assisted-by: Claude Code:claude-fable-5
|
i am going to merge this Pr as is - it is due to change with Flux http server - will follow up on the comments about the dead fork configs |
A request head that filled the whole 16 MiB read buffer without ever completing was dropped where it stood: read_space reported exhaustion, dispatch had no complete request to answer, and the connection died by reset with nothing on the wire. That contradicted the read loop's own rule — a full buffer always leaves something to answer with — and a reset before a parseable response is the failure mode validator-client stacks punish hardest. Every other overrun already had its verdict: a body declared past the cap is a 413 from its headers, garbage is a 400. The unfinished head now gets the 431 of RFC 6585, framed and lingered like every other reject, so the sender reads the status instead of a reset. The equivalent fix landed in flux's HTTP machine during its adversarial review rounds; this carries it to the silver machine the flux one will eventually replace. Assisted-by: Claude Code:claude-fable-5
A crash leaves the socket file behind and every restart then fails with AddrInUse. Bind now probes an existing path first: only an lstat-verified socket whose connect comes back ConnectionRefused is unlinked — a live listener accepts the probe and keeps its path, and a regular file is left for the bind to fail on. Dropping the listener removes its own file so a clean shutdown leaves nothing to probe. Ported from the flux HTTP stack, which hit and fixed the same restart failure (flux 9678523, ADR 0003). Assisted-by: Claude Code:claude-fable-5
A bare path was accepted, connected, and then spoken to in HTTP/1.1. An EL's IPC socket (reth --auth-ipc, geth.ipc) carries raw newline-framed JSON-RPC with no HTTP envelope, so the connection succeeded and every request was garbage to the EL. The path now fails at startup with a message naming http:// as the only served scheme. The UDS transport stays behind EngineClient::new_uds for the pool tests and for the raw JSON-RPC framing that will make the path usable. Assisted-by: Claude Code:claude-fable-5-1
No description provided.