From 3ab71715c73c210193b08e56be6cac463df5b0a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:38:31 -0300 Subject: [PATCH 1/5] fix: follow spec in /metrics and /health responses See https://github.com/leanEthereum/leanSpec/blob/914e09333578072c6bf02d2d405f8c13d5b55d38/src/lean_spec/subspecs/api/server.py#L36-L46 --- crates/net/rpc/src/metrics.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/net/rpc/src/metrics.rs b/crates/net/rpc/src/metrics.rs index 075d84c..62a85d1 100644 --- a/crates/net/rpc/src/metrics.rs +++ b/crates/net/rpc/src/metrics.rs @@ -1,13 +1,13 @@ use std::net::SocketAddr; -use axum::{Router, routing::get}; +use axum::{Router, http::HeaderValue, response::IntoResponse, routing::get}; use thiserror::Error; use tracing::warn; pub async fn start_prometheus_metrics_api(address: SocketAddr) -> Result<(), std::io::Error> { let app = Router::new() .route("/metrics", get(get_metrics)) - .route("/health", get("Service Up")); + .route("/health", get(get_health)); // Start the axum app let listener = tokio::net::TcpListener::bind(address).await?; @@ -16,12 +16,20 @@ pub async fn start_prometheus_metrics_api(address: SocketAddr) -> Result<(), std Ok(()) } -pub(crate) async fn get_metrics() -> String { - gather_default_metrics() +pub(crate) async fn get_health() -> impl IntoResponse { + r#"{"status": "healthy", "service": "lean-spec-api"}"# +} + +pub(crate) async fn get_metrics() -> impl IntoResponse { + let mut response = gather_default_metrics() .inspect_err(|err| { warn!(%err, "Failed to gather Prometheus metrics"); }) .unwrap_or_default() + .into_response(); + let content_type = HeaderValue::from_static("text/plain; version=0.0.4; charset=utf-8"); + response.headers_mut().insert("content-type", content_type); + response } #[derive(Debug, Error)] From 4d5fffe405a991fa3af1075f42cf0f63469de4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:55:05 -0300 Subject: [PATCH 2/5] refactor: add new routes with dummy functions --- bin/ethlambda/src/main.rs | 4 +++- crates/net/rpc/src/lib.rs | 23 +++++++++++++++++++++++ crates/net/rpc/src/metrics.rs | 14 +++----------- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index d951e45..4e62bff 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -98,7 +98,9 @@ async fn main() { p2p_rx, )); - start_prometheus_metrics_api(metrics_socket).await.unwrap(); + ethlambda_rpc::start_rpc_server(metrics_socket) + .await + .unwrap(); info!("Node initialized"); diff --git a/crates/net/rpc/src/lib.rs b/crates/net/rpc/src/lib.rs index e144883..914159b 100644 --- a/crates/net/rpc/src/lib.rs +++ b/crates/net/rpc/src/lib.rs @@ -1 +1,24 @@ +use std::net::SocketAddr; + +use axum::{Router, routing::get}; + pub mod metrics; + +pub async fn start_rpc_server(address: SocketAddr) -> Result<(), std::io::Error> { + let metrics_router = metrics::start_prometheus_metrics_api(); + + let app = Router::new() + .merge(metrics_router) + .route("/lean/states/finalized", get(get_latest_finalized_state)) + .route("/lean/states/justified", get(get_latest_justified_state)); + + // Start the axum app + let listener = tokio::net::TcpListener::bind(address).await?; + axum::serve(listener, app).await?; + + Ok(()) +} + +async fn get_latest_finalized_state() {} + +async fn get_latest_justified_state() {} diff --git a/crates/net/rpc/src/metrics.rs b/crates/net/rpc/src/metrics.rs index 62a85d1..2a4ae02 100644 --- a/crates/net/rpc/src/metrics.rs +++ b/crates/net/rpc/src/metrics.rs @@ -1,19 +1,11 @@ -use std::net::SocketAddr; - use axum::{Router, http::HeaderValue, response::IntoResponse, routing::get}; use thiserror::Error; use tracing::warn; -pub async fn start_prometheus_metrics_api(address: SocketAddr) -> Result<(), std::io::Error> { - let app = Router::new() +pub fn start_prometheus_metrics_api() -> Router { + Router::new() .route("/metrics", get(get_metrics)) - .route("/health", get(get_health)); - - // Start the axum app - let listener = tokio::net::TcpListener::bind(address).await?; - axum::serve(listener, app).await?; - - Ok(()) + .route("/health", get(get_health)) } pub(crate) async fn get_health() -> impl IntoResponse { From 1ed82174d17a4780c32c79aa101978d4aea6b30a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:22:54 -0300 Subject: [PATCH 3/5] feat: use new endpoints with versioning and consistent naming See: - https://github.com/leanEthereum/leanSpec/pull/323 - https://github.com/leanEthereum/leanSpec/pull/325 --- bin/ethlambda/src/main.rs | 1 - crates/net/rpc/src/lib.rs | 7 +++++-- crates/net/rpc/src/metrics.rs | 4 +--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 6918071..126a9d6 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -8,7 +8,6 @@ use std::{ use clap::Parser; use ethlambda_p2p::{Bootnode, parse_enrs, start_p2p}; -use ethlambda_rpc::metrics::start_prometheus_metrics_api; use ethlambda_types::primitives::H256; use ethlambda_types::{ genesis::Genesis, diff --git a/crates/net/rpc/src/lib.rs b/crates/net/rpc/src/lib.rs index 914159b..5fb33f3 100644 --- a/crates/net/rpc/src/lib.rs +++ b/crates/net/rpc/src/lib.rs @@ -9,8 +9,11 @@ pub async fn start_rpc_server(address: SocketAddr) -> Result<(), std::io::Error> let app = Router::new() .merge(metrics_router) - .route("/lean/states/finalized", get(get_latest_finalized_state)) - .route("/lean/states/justified", get(get_latest_justified_state)); + .route("/lean/v0/states/finalized", get(get_latest_finalized_state)) + .route( + "/lean/v0/checkpoints/justified", + get(get_latest_justified_state), + ); // Start the axum app let listener = tokio::net::TcpListener::bind(address).await?; diff --git a/crates/net/rpc/src/metrics.rs b/crates/net/rpc/src/metrics.rs index 2afa2ec..2af9545 100644 --- a/crates/net/rpc/src/metrics.rs +++ b/crates/net/rpc/src/metrics.rs @@ -1,5 +1,3 @@ - - use axum::{Router, http::HeaderValue, response::IntoResponse, routing::get}; use thiserror::Error; use tracing::warn; @@ -7,7 +5,7 @@ use tracing::warn; pub fn start_prometheus_metrics_api() -> Router { Router::new() .route("/metrics", get(get_metrics)) - .route("/health", get(get_health)) + .route("/lean/v0/health", get(get_health)) } pub(crate) async fn get_health() -> impl IntoResponse { From 883ccef2f6244e61a36b6bf661987377b703999b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:29:59 -0300 Subject: [PATCH 4/5] feat: implement the new routes --- Cargo.lock | 4 ++++ bin/ethlambda/src/main.rs | 7 ++++--- crates/blockchain/src/lib.rs | 7 +++---- crates/blockchain/src/store.rs | 5 +++++ crates/common/types/Cargo.toml | 1 + crates/common/types/src/block.rs | 3 ++- crates/common/types/src/state.rs | 12 ++++++++++-- crates/net/rpc/Cargo.toml | 3 +++ crates/net/rpc/src/lib.rs | 32 +++++++++++++++++++++++++------- 9 files changed, 57 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8f9e75..41e787d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1981,7 +1981,10 @@ name = "ethlambda-rpc" version = "0.1.0" dependencies = [ "axum", + "ethlambda-blockchain", "prometheus", + "serde", + "serde_json", "thiserror 2.0.17", "tokio", "tracing", @@ -2015,6 +2018,7 @@ dependencies = [ "ethereum-types", "ethereum_ssz", "ethereum_ssz_derive", + "hex", "leansig", "serde", "ssz_types", diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 126a9d6..c272d9b 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -18,7 +18,7 @@ use serde::Deserialize; use tracing::{error, info}; use tracing_subscriber::{EnvFilter, Layer, Registry, layer::SubscriberExt}; -use ethlambda_blockchain::BlockChain; +use ethlambda_blockchain::{BlockChain, store::Store}; const ASCII_ART: &str = r#" _ _ _ _ _ @@ -90,9 +90,10 @@ async fn main() { read_validator_keys(&validators_path, &validator_keys_dir, &options.node_id); let genesis_state = State::from_genesis(&genesis, validators); + let store = Store::from_genesis(genesis_state); let (p2p_tx, p2p_rx) = tokio::sync::mpsc::unbounded_channel(); - let blockchain = BlockChain::spawn(genesis_state, p2p_tx, validator_keys); + let blockchain = BlockChain::spawn(store.clone(), p2p_tx, validator_keys); let p2p_handle = tokio::spawn(start_p2p( node_p2p_key, @@ -102,7 +103,7 @@ async fn main() { p2p_rx, )); - ethlambda_rpc::start_rpc_server(metrics_socket) + ethlambda_rpc::start_rpc_server(metrics_socket, store) .await .unwrap(); diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index f120e18..f610bb1 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -7,7 +7,7 @@ use ethlambda_types::{ block::{BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation}, primitives::TreeHash, signature::ValidatorSecretKey, - state::{Checkpoint, State}, + state::Checkpoint, }; use spawned_concurrency::tasks::{ CallResponse, CastResponse, GenServer, GenServerHandle, send_after, @@ -40,12 +40,11 @@ pub const SECONDS_PER_SLOT: u64 = 4; impl BlockChain { pub fn spawn( - genesis_state: State, + store: Store, p2p_tx: mpsc::UnboundedSender, validator_keys: HashMap, ) -> BlockChain { - let genesis_time = genesis_state.config.genesis_time; - let store = Store::from_genesis(genesis_state); + let genesis_time = store.config().genesis_time; let key_manager = key_manager::KeyManager::new(validator_keys); let handle = BlockChainServer { store, diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index d921d5f..cc1b65c 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -710,6 +710,11 @@ impl Store { .expect("head state is always available") } + /// Returns a reference to the state for the given block root, if it exists. + pub fn get_state(&self, root: H256) -> Option<&State> { + self.states.get(&root) + } + /// Returns the slot of the current safe target block. pub fn safe_target_slot(&self) -> u64 { self.blocks[&self.safe_target].slot diff --git a/crates/common/types/Cargo.toml b/crates/common/types/Cargo.toml index 556d439..c841756 100644 --- a/crates/common/types/Cargo.toml +++ b/crates/common/types/Cargo.toml @@ -12,6 +12,7 @@ version.workspace = true [dependencies] thiserror.workspace = true serde.workspace = true +hex.workspace = true ethereum-types.workspace = true leansig.workspace = true diff --git a/crates/common/types/src/block.rs b/crates/common/types/src/block.rs index 185cbe6..88bcf3c 100644 --- a/crates/common/types/src/block.rs +++ b/crates/common/types/src/block.rs @@ -1,3 +1,4 @@ +use serde::Serialize; use ssz_derive::{Decode, Encode}; use ssz_types::typenum::U1048576; use tree_hash_derive::TreeHash; @@ -136,7 +137,7 @@ pub struct BlockWithAttestation { /// /// Headers are smaller than full blocks. They're useful for tracking the chain /// without storing everything. -#[derive(Debug, Clone, Encode, Decode, TreeHash)] +#[derive(Debug, Clone, Serialize, Encode, Decode, TreeHash)] pub struct BlockHeader { /// The slot in which the block was proposed pub slot: u64, diff --git a/crates/common/types/src/state.rs b/crates/common/types/src/state.rs index f46d7ff..4b4038e 100644 --- a/crates/common/types/src/state.rs +++ b/crates/common/types/src/state.rs @@ -18,7 +18,7 @@ use crate::{ pub type ValidatorRegistryLimit = U4096; /// The main consensus state object -#[derive(Debug, Clone, Encode, Decode, TreeHash)] +#[derive(Debug, Clone, Serialize, Encode, Decode, TreeHash)] pub struct State { /// The chain's configuration parameters pub config: ChainConfig, @@ -62,14 +62,22 @@ pub type JustificationValidators = ssz_types::BitList>; /// Represents a validator's static metadata and operational interface. -#[derive(Debug, Clone, Encode, Decode, TreeHash)] +#[derive(Debug, Clone, Serialize, Encode, Decode, TreeHash)] pub struct Validator { /// XMSS one-time signature public key. + #[serde(serialize_with = "serialize_pubkey_hex")] pub pubkey: ValidatorPubkeyBytes, /// Validator index in the registry. pub index: u64, } +fn serialize_pubkey_hex(pubkey: &ValidatorPubkeyBytes, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&hex::encode(pubkey)) +} + impl Validator { pub fn get_pubkey(&self) -> Result { // TODO: make this unfallible by moving check to the constructor diff --git a/crates/net/rpc/Cargo.toml b/crates/net/rpc/Cargo.toml index 24895cd..da2dfb2 100644 --- a/crates/net/rpc/Cargo.toml +++ b/crates/net/rpc/Cargo.toml @@ -15,3 +15,6 @@ tokio.workspace = true prometheus.workspace = true thiserror.workspace = true tracing.workspace = true +ethlambda-blockchain.workspace = true +serde.workspace = true +serde_json.workspace = true diff --git a/crates/net/rpc/src/lib.rs b/crates/net/rpc/src/lib.rs index 5fb33f3..2201976 100644 --- a/crates/net/rpc/src/lib.rs +++ b/crates/net/rpc/src/lib.rs @@ -1,19 +1,24 @@ use std::net::SocketAddr; -use axum::{Router, routing::get}; +use axum::{Json, Router, response::IntoResponse, routing::get}; +use ethlambda_blockchain::store::Store; pub mod metrics; -pub async fn start_rpc_server(address: SocketAddr) -> Result<(), std::io::Error> { +pub async fn start_rpc_server(address: SocketAddr, store: Store) -> Result<(), std::io::Error> { let metrics_router = metrics::start_prometheus_metrics_api(); - let app = Router::new() - .merge(metrics_router) + // Create stateful routes first, then convert to stateless by applying state + let api_routes = Router::new() .route("/lean/v0/states/finalized", get(get_latest_finalized_state)) .route( "/lean/v0/checkpoints/justified", get(get_latest_justified_state), - ); + ) + .with_state(store); + + // Merge stateless routers + let app = Router::new().merge(metrics_router).merge(api_routes); // Start the axum app let listener = tokio::net::TcpListener::bind(address).await?; @@ -22,6 +27,19 @@ pub async fn start_rpc_server(address: SocketAddr) -> Result<(), std::io::Error> Ok(()) } -async fn get_latest_finalized_state() {} +async fn get_latest_finalized_state( + axum::extract::State(store): axum::extract::State, +) -> impl IntoResponse { + let finalized = store.latest_finalized(); + let state = store + .get_state(finalized.root) + .expect("finalized state exists"); + Json(state.clone()) +} -async fn get_latest_justified_state() {} +async fn get_latest_justified_state( + axum::extract::State(store): axum::extract::State, +) -> impl IntoResponse { + let checkpoint = *store.latest_justified(); + Json(checkpoint) +} From 0815bfd3b4f6b03331d18510ca00feb3d0e9a469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:35:28 -0300 Subject: [PATCH 5/5] fix: make it compile with new Store --- Cargo.lock | 2 +- bin/ethlambda/src/main.rs | 3 ++- crates/net/rpc/Cargo.toml | 2 +- crates/net/rpc/src/lib.rs | 8 ++++---- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41e787d..a9eb1a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1981,7 +1981,7 @@ name = "ethlambda-rpc" version = "0.1.0" dependencies = [ "axum", - "ethlambda-blockchain", + "ethlambda-storage", "prometheus", "serde", "serde_json", diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index c272d9b..4587651 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -18,7 +18,8 @@ use serde::Deserialize; use tracing::{error, info}; use tracing_subscriber::{EnvFilter, Layer, Registry, layer::SubscriberExt}; -use ethlambda_blockchain::{BlockChain, store::Store}; +use ethlambda_blockchain::BlockChain; +use ethlambda_storage::Store; const ASCII_ART: &str = r#" _ _ _ _ _ diff --git a/crates/net/rpc/Cargo.toml b/crates/net/rpc/Cargo.toml index da2dfb2..6053906 100644 --- a/crates/net/rpc/Cargo.toml +++ b/crates/net/rpc/Cargo.toml @@ -15,6 +15,6 @@ tokio.workspace = true prometheus.workspace = true thiserror.workspace = true tracing.workspace = true -ethlambda-blockchain.workspace = true +ethlambda-storage.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/net/rpc/src/lib.rs b/crates/net/rpc/src/lib.rs index 2201976..d1bb391 100644 --- a/crates/net/rpc/src/lib.rs +++ b/crates/net/rpc/src/lib.rs @@ -1,7 +1,7 @@ use std::net::SocketAddr; use axum::{Json, Router, response::IntoResponse, routing::get}; -use ethlambda_blockchain::store::Store; +use ethlambda_storage::Store; pub mod metrics; @@ -32,14 +32,14 @@ async fn get_latest_finalized_state( ) -> impl IntoResponse { let finalized = store.latest_finalized(); let state = store - .get_state(finalized.root) + .get_state(&finalized.root) .expect("finalized state exists"); - Json(state.clone()) + Json(state) } async fn get_latest_justified_state( axum::extract::State(store): axum::extract::State, ) -> impl IntoResponse { - let checkpoint = *store.latest_justified(); + let checkpoint = store.latest_justified(); Json(checkpoint) }