Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
578f8a9
adds beacon api
0w3n-d May 26, 2026
0021e4e
Merge branch 'main' into od/beacon_api
Bronek Aug 14, 2026
d096b51
Documentation of planned Beacon API work
Bronek Aug 17, 2026
87c400b
Merge branch 'main' into od/beacon_api
Bronek Aug 17, 2026
b977f93
Extract HTTP server byte machine into silver_httpcore
Bronek Aug 17, 2026
42d86da
Move HTTP client machine into silver_httpcore; delete dead IPC transport
Bronek Aug 17, 2026
e7cbd99
Table-based dispatch for beacon_api routes
Bronek Aug 17, 2026
96a900c
Rename crates/engine to crates/engine_api
Bronek Aug 17, 2026
36f291e
Consolidate API access into the client_server tile
Bronek Aug 17, 2026
bd42255
Harden API transport: zero-alloc hot-path test, conn cap, lazy buffers
Bronek Aug 17, 2026
cc94b7f
Add inbound and outbound API deadlines (CL-114, CL-115)
Bronek Aug 18, 2026
e68926d
Support multiple simultaneous beacon-api binds
Bronek Aug 18, 2026
828ebdb
Refresh ADRs 0002 and 0004 for team decisions of 2026-08-18
Bronek Aug 18, 2026
bbfee39
httpcore: negotiation headers, query decoding, 400 on provable garbage
Bronek Aug 18, 2026
89d6bcd
Widen Response: any status, extra headers, indexed errors
Bronek Aug 18, 2026
29ee6f2
Spec-flavour JSON writers for beacon-api responses
Bronek Aug 18, 2026
89850ce
Thread NodeStatus and spec config into the beacon API
Bronek Aug 18, 2026
69331e5
Merge branch 'main' into od/beacon_api
Bronek Aug 18, 2026
24b5755
Doc: ClientServer consumes beacon_events and sync_target for node status
Bronek Aug 18, 2026
da35584
Serve the beacon-API bootstrap statics: node/version+health, config/*
Bronek Aug 19, 2026
c2207e2
Serve beacon/genesis and per-state fork + finality checkpoints
Bronek Aug 19, 2026
c4c6517
Publish the head's execution status on BeaconStateEvent::Status
Bronek Aug 19, 2026
040d5d4
Merge branch 'main' into od/beacon_api
Bronek Aug 20, 2026
bb57694
Serve node/syncing and node/peer_count
Bronek Aug 20, 2026
0c5652c
Serve the validator registry
Bronek Aug 20, 2026
6ec5f7a
Amend ADR-0004 for the validator registry's cost
Bronek Aug 20, 2026
cb01bc0
Publish the head block's root beside the state it produced
Bronek Aug 20, 2026
0d7f348
Serve block roots and the head's header
Bronek Aug 20, 2026
3c06453
Serve proposer and sync-committee duties
Bronek Aug 20, 2026
93cf03a
Serve the validator client's receipt POSTs
Bronek Aug 21, 2026
714b011
Merge branch 'main' into od/beacon_api
Bronek Aug 21, 2026
d894b38
Rename the client_server tile to application_boundary
Bronek Aug 21, 2026
ef618e5
Linger on a refused request instead of resetting it
Bronek Aug 21, 2026
bfed02b
Derive the network's name from its genesis fork version
Bronek Aug 21, 2026
b8f688f
Correct client-behaviour claims and the v2 dependent root
Bronek Aug 21, 2026
d5e32b0
Amend ADRs, mark as accepted
Bronek Aug 21, 2026
9c25c25
Share one readiness loop between the tile's two HTTP tenants
Bronek Aug 21, 2026
d2cea25
Name the head from fork choice, not the published state
Bronek Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,12 @@ payloads.
A transport-free library living inside a tile that owns the loop. Hosted
crates are hardcoded into their tile, not plugins.
_Avoid_: plugin, sub-tile, service.

**Beacon API**:
The standard Ethereum REST API a beacon node serves; validator clients are
the primary consumers. Served by the `beacon_api` hosted crate.

**Engine API**:
The standard JSON-RPC protocol between a beacon node and its execution
client. Called by the `engine_api` hosted crate.
_Avoid_: bare "engine" (ambiguous with the execution client itself).
53 changes: 51 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[workspace]
members = [
"crates/application_boundary",
"crates/beacon_api",
"crates/beacon_state/data",
"crates/beacon_state/tile",
"crates/bin",
Expand All @@ -11,7 +13,8 @@ members = [
"crates/discovery",
"crates/e2e",
"crates/gossip",
"crates/engine",
"crates/httpcore",
"crates/engine_api",
"crates/metrics",
"crates/network",
"crates/peer",
Expand Down Expand Up @@ -66,6 +69,8 @@ inherits = "dev"
opt-level = 3

[workspace.dependencies]
silver_application_boundary = { path = "crates/application_boundary" }
silver_beacon_api = { path = "crates/beacon_api" }
silver_beacon_state = { path = "crates/beacon_state/tile" }
silver_beacon_state_data = { path = "crates/beacon_state/data" }
silver_chain_spec = { path = "crates/config/chain_spec" }
Expand All @@ -77,10 +82,11 @@ silver_ssz = { path = "crates/ssz" }
silver_control = { path = "crates/control" }
silver_discovery = {path = "crates/discovery" }
silver_gossip = {path = "crates/gossip" }
silver_httpcore = { path = "crates/httpcore" }
silver_network = {path = "crates/network" }
silver_peer = {path = "crates/peer" }
silver_storage = { path = "crates/storage" }
silver_engine = { path = "crates/engine"}
silver_engine_api = { path = "crates/engine_api" }
flux = { git = "https://github.com/gattaca-com/flux", rev = "b5fbdf6f3e52feb785a527d5c3bba90fee2a56e4"}
flux-utils = { git = "https://github.com/gattaca-com/flux", rev = "b5fbdf6f3e52feb785a527d5c3bba90fee2a56e4", features = ["bytes"]}
flux-profiler = { git = "https://github.com/gattaca-com/flux", rev = "b5fbdf6f3e52feb785a527d5c3bba90fee2a56e4"}
Expand Down
25 changes: 25 additions & 0 deletions crates/application_boundary/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "silver_application_boundary"
edition.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true

[dependencies]
flux.workspace = true
silver_beacon_api.workspace = true
silver_beacon_state_data.workspace = true
silver_common.workspace = true
silver_config.workspace = true
silver_engine_api.workspace = true
silver_httpcore.workspace = true
silver_peer.workspace = true

[dev-dependencies]
hex.workspace = true
serde_json.workspace = true
silver_engine_api = { workspace = true, features = ["test-el"] }
tempfile = "3"

[lints]
workspace = true
121 changes: 121 additions & 0 deletions crates/application_boundary/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::time::Duration;

use flux::{spine::SpineAdapter, tile::Tile};
use silver_beacon_api::{BeaconApi, ChainHead, PeerCounts, SlotStatus};
use silver_beacon_state_data::{BeaconStateReader, SpecConfig};
use silver_common::{
BeaconStateEvent, Enr, Identify, Keypair, SilverSpine, SyncUpdate, TProducer, TRandomAccess,
ssz_view::StatusView,
};
use silver_config::EngineConfig;
use silver_engine_api::EngineApi;
use silver_httpcore::{Bind, Readiness, TokenRange};
use silver_peer::PeerCounters;

/// A tenant added here takes the next share of a raised `TENANTS`, which keeps
/// every share disjoint without a base to compute.
const TENANTS: usize = 2;
const BEACON_TOKENS: TokenRange = TokenRange::share(0, TENANTS);
const ENGINE_TOKENS: TokenRange = TokenRange::share(1, TENANTS);

pub struct ApplicationBoundaryTile {
readiness: Readiness,
pub beacon: BeaconApi,
engine: EngineApi,
}

impl Tile<SilverSpine> for ApplicationBoundaryTile {
fn loop_body(&mut self, adapter: &mut SpineAdapter<SilverSpine>) {
self.engine.intake(adapter);
self.readiness.wait(Duration::ZERO);
self.engine.spin(adapter, self.readiness.events());
self.refresh_node_status(adapter);
if self.beacon.pump(self.readiness.events()) {
adapter.mark_work();
}
}
}

impl ApplicationBoundaryTile {
#[allow(clippy::too_many_arguments)]
pub fn new(
binds: &[Bind],
max_connections: usize,
idle_timeout: Duration,
keypair: &Keypair,
local_enr: Enr,
identify: &Identify,
spec: &SpecConfig,
state: BeaconStateReader,
engine_config: EngineConfig,
gossip_consumer: TRandomAccess,
rpc_consumer: TRandomAccess,
resp_producer: TProducer,
) -> Self {
// A batch too small for every socket the tile can register leaves the
// rest of a busy iteration's readiness for the next one.
let sockets =
binds.len() + max_connections + EngineApi::max_sockets(engine_config.max_connections);

let readiness = Readiness::new(sockets);
let beacon = BeaconApi::new(
readiness.registry(),
BEACON_TOKENS,
binds,
max_connections,
idle_timeout,
keypair,
local_enr,
identify,
spec,
state,
);
let engine = EngineApi::new(
readiness.registry(),
ENGINE_TOKENS,
engine_config,
gossip_consumer,
rpc_consumer,
resp_producer,
);
Self { readiness, beacon, engine }
}

fn refresh_node_status(&mut self, adapter: &mut SpineAdapter<SilverSpine>) {
let status = self.beacon.node_status_mut();

// Consumed every iteration, and never behind the engine's capacity
// gate: a consumer's first `consume` jumps its cursor to the
// producer's write head, so a queue left unread while the pool is
// saturated loses everything published in the meantime.
adapter.consume(|event: BeaconStateEvent, _| {
if let BeaconStateEvent::Status {
ssz,
latest_block_slot,
wall_slot,
head_optimistic,
..
} = event
{
status.slots = Some(SlotStatus {
latest_block_slot,
head: ChainHead {
root: *StatusView::head_root(&ssz),
slot: StatusView::head_slot(&ssz),
},
wall_slot,
head_optimistic,
});
}
});
adapter.consume(|update: SyncUpdate, _| {
status.syncing = !matches!(update, SyncUpdate::Following);
});

status.el = self.engine.sync_status();
status.peers = PeerCounts {
connected: PeerCounters::PeersConnected.get(),
connecting: PeerCounters::PeersConnecting.get(),
};
}
}
Loading