Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 23 additions & 4 deletions bin/ethlambda/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ use serde::Deserialize;
use tracing::{error, info, warn};
use tracing_subscriber::{EnvFilter, Layer, Registry, layer::SubscriberExt};

use ethlambda_blockchain::BlockChain;
use ethlambda_blockchain::{BlockChain, SyncStatusController};
use ethlambda_rpc::RpcConfig;
use ethlambda_storage::{
MAX_RESUMABLE_DB_STATE_AGE, StorageBackend, Store, backend::RocksDBBackend,
Expand Down Expand Up @@ -92,6 +92,7 @@ async fn main() -> eyre::Result<()> {
http_address: options.http_address,
api_port: options.api_port,
metrics_port: options.metrics_port,
version: version::CLIENT_VERSION,
};

println!("{ASCII_ART}");
Expand Down Expand Up @@ -213,10 +214,17 @@ async fn main() -> eyre::Result<()> {
// and the API server (which exposes GET/POST admin endpoints).
let aggregator = AggregatorController::new(options.is_aggregator);

// Shared, runtime-readable sync status. The blockchain actor writes it each
// tick (alongside the `lean_node_sync_status` metric); the RPC
// `/lean/v0/node/syncing` endpoint reads it. Seeded to Idle, matching the
// metric's startup value.
let sync_status = SyncStatusController::default();

let blockchain = BlockChain::spawn(
store.clone(),
validator_keys,
aggregator.clone(),
sync_status.clone(),
attestation_committee_count,
!options.disable_duty_sync_gate,
ProposerConfig {
Expand All @@ -241,6 +249,10 @@ async fn main() -> eyre::Result<()> {
})
.wrap_err("failed to build swarm")?;

// Capture the local peer ID before `built` is moved into the P2P actor; the
// RPC `/lean/v0/node/identity` endpoint reports it.
let local_peer_id = built.local_peer_id.to_string();

let p2p = P2P::spawn(built, store.clone(), node_names);

// Wire actors together via protocol refs
Expand All @@ -262,9 +274,16 @@ async fn main() -> eyre::Result<()> {
let rpc_shutdown = shutdown_token.clone();

let rpc_handle = tokio::spawn(async move {
let _ = ethlambda_rpc::start_rpc_server(rpc_config, store, aggregator, rpc_shutdown)
.await
.inspect_err(|err| error!(%err, "RPC server failed"));
let _ = ethlambda_rpc::start_rpc_server(
rpc_config,
store,
aggregator,
sync_status,
local_peer_id,
rpc_shutdown,
)
.await
.inspect_err(|err| error!(%err, "RPC server failed"));
});

info!("Node initialized");
Expand Down
11 changes: 11 additions & 0 deletions crates/blockchain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ pub const INTERVALS_PER_SLOT: u64 = 5;
/// Milliseconds in a slot (derived from interval duration and count).
pub const MILLISECONDS_PER_SLOT: u64 = MILLISECONDS_PER_INTERVAL * INTERVALS_PER_SLOT;
pub use ethlambda_types::block::MAX_ATTESTATIONS_DATA;
/// Slots of head-vs-wall-clock lag above which a node is considered syncing.
pub use sync_status::SYNC_LAG_THRESHOLD;
pub use sync_status::SyncStatusController;
/// Future-slot tolerance for gossip attestations, expressed in intervals.
///
/// Bounds the clock skew the time check is willing to absorb when admitting a
Expand Down Expand Up @@ -107,6 +110,7 @@ impl BlockChain {
store: Store,
validator_keys: HashMap<u64, ValidatorKeyPair>,
aggregator: AggregatorController,
sync_status_controller: SyncStatusController,
attestation_committee_count: u64,
gate_duties: bool,
proposer_config: ProposerConfig,
Expand Down Expand Up @@ -137,6 +141,7 @@ impl BlockChain {
proposer_config,
pre_merge_coverage: None,
sync_status: SyncStatusTracker::new(gate_duties),
sync_status_controller,
}
.start();
let time_until_genesis = (SystemTime::UNIX_EPOCH + Duration::from_secs(genesis_time))
Expand Down Expand Up @@ -209,6 +214,11 @@ pub struct BlockChainServer {
/// validator duties while syncing, unless that gating was disabled at
/// startup via `--disable-duty-sync-gate` (then it is metric-only).
sync_status: SyncStatusTracker,

/// Shared, read-only mirror of `sync_status` for readers outside the actor
/// (the RPC `/lean/v0/node/syncing` endpoint). Written from
/// `update_sync_status` with the same `SyncStatus` fed to the metric.
sync_status_controller: SyncStatusController,
}

impl BlockChainServer {
Expand Down Expand Up @@ -971,6 +981,7 @@ impl BlockChainServer {
.sync_status
.update(current_slot, head_slot, max_seen_slot);
metrics::set_node_sync_status(status);
self.sync_status_controller.set(status);
}
}

Expand Down
20 changes: 17 additions & 3 deletions crates/blockchain/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,11 +491,15 @@ static LEAN_BLOCK_PROPOSAL_AGGREGATES_SELECTED: std::sync::LazyLock<Histogram> =
// --- Sync Status ---

/// Node synchronization status.
///
/// The explicit discriminants are the wire encoding used by
/// [`crate::SyncStatusController`] (`*self as u8` / [`SyncStatus::from_u8`]);
/// keep them stable.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncStatus {
Idle,
Syncing,
Synced,
Idle = 0,
Syncing = 1,
Synced = 2,
}

impl SyncStatus {
Expand All @@ -507,6 +511,16 @@ impl SyncStatus {
}
}

/// Decode a discriminant produced by `*self as u8`. Unknown values fall
/// back to [`SyncStatus::Idle`].
pub(crate) fn from_u8(value: u8) -> Self {
match value {
1 => SyncStatus::Syncing,
2 => SyncStatus::Synced,
_ => SyncStatus::Idle,
}
}

const ALL: &[&str] = &["idle", "syncing", "synced"];
}

Expand Down
44 changes: 43 additions & 1 deletion crates/blockchain/src/sync_status.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,53 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};

use tracing::debug;

use crate::metrics::SyncStatus;

/// Shared, runtime-readable node sync status.
///
/// The blockchain actor is the sole writer: once per tick it calls
/// [`SyncStatusController::set`] with the same [`SyncStatus`] it feeds the
/// `lean_node_sync_status` metric (see `BlockChainServer::update_sync_status`).
/// Read paths outside the actor — notably the RPC `/lean/v0/node/syncing`
/// endpoint — clone the controller and read the current status without touching
/// actor state. Mirrors [`ethlambda_types::aggregator::AggregatorController`].
#[derive(Clone, Debug)]
pub struct SyncStatusController {
status: Arc<AtomicU8>,
}

impl SyncStatusController {
/// Construct a controller seeded with `initial` (the actor seeds
/// [`SyncStatus::Idle`], matching the metric's startup value).
pub fn new(initial: SyncStatus) -> Self {
Self {
status: Arc::new(AtomicU8::new(initial as u8)),
}
}

/// Publish the current sync status.
pub fn set(&self, status: SyncStatus) {
self.status.store(status as u8, Ordering::Relaxed);
}

/// Read the current sync status.
pub fn get(&self) -> SyncStatus {
SyncStatus::from_u8(self.status.load(Ordering::Relaxed))
}
}

impl Default for SyncStatusController {
fn default() -> Self {
Self::new(SyncStatus::Idle)
}
}

/// Local head lag beyond which the node is considered to be syncing.
///
/// See: leanSpec PR #708.
const SYNC_LAG_THRESHOLD: u64 = 4;
pub const SYNC_LAG_THRESHOLD: u64 = 4;
/// Freshest-known block lag beyond which the network is considered stalled.
///
/// During a network-wide stall the node remains synced so validators can help
Expand Down
4 changes: 4 additions & 0 deletions crates/net/p2p/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ pub struct SwarmConfig {

/// Result of building the swarm — contains all pieces needed to start the P2P actor.
pub struct BuiltSwarm {
/// This node's libp2p peer ID, derived from the node key. Exposed so the
/// caller can report it (e.g. via the RPC `/lean/v0/node/identity` endpoint).
pub local_peer_id: PeerId,
pub(crate) swarm: libp2p::Swarm<Behaviour>,
pub(crate) attestation_topics: HashMap<u64, libp2p::gossipsub::IdentTopic>,
pub(crate) attestation_committee_count: u64,
Expand Down Expand Up @@ -339,6 +342,7 @@ pub fn build_swarm(
info!(socket=%config.listening_socket, "P2P node started");

Ok(BuiltSwarm {
local_peer_id,
swarm,
attestation_topics,
attestation_committee_count: config.attestation_committee_count,
Expand Down
Loading
Loading