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
17 changes: 14 additions & 3 deletions crates/net/rpc/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,26 @@
//! the role (hot-standby model). See leanSpec PR #636 for the full rationale.

use axum::{
Extension, Json,
Extension, Json, Router,
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
};
use ethlambda_storage::Store;
use ethlambda_types::aggregator::AggregatorController;
use serde::Serialize;
use serde_json::Value;
use tracing::info;

use crate::json_response;

pub(crate) fn routes() -> Router<Store> {
Router::new().route(
"/lean/v0/admin/aggregator",
get(get_aggregator).post(post_aggregator),
)
}

#[derive(Serialize)]
struct StatusResponse {
is_aggregator: bool,
Expand All @@ -44,7 +53,9 @@ struct ToggleResponse {
/// `Extension<T>` would cause axum to short-circuit with a 500 when the
/// extension is missing, whereas `Option` yields `None` and lets us return
/// a clean 503 with a useful message.
pub async fn get_aggregator(controller: Option<Extension<AggregatorController>>) -> Response {
pub(crate) async fn get_aggregator(
controller: Option<Extension<AggregatorController>>,
) -> Response {
match controller {
Some(Extension(controller)) => json_response(StatusResponse {
is_aggregator: controller.is_enabled(),
Expand All @@ -62,7 +73,7 @@ pub async fn get_aggregator(controller: Option<Extension<AggregatorController>>)
/// `Extension<T>` would cause axum to short-circuit with a 500 when the
/// extension is missing, whereas `Option` yields `None` and lets us return
/// a clean 503 with a useful message.
pub async fn post_aggregator(
pub(crate) async fn post_aggregator(
controller: Option<Extension<AggregatorController>>,
body: Option<Json<Value>>,
) -> Response {
Expand Down
12 changes: 10 additions & 2 deletions crates/net/rpc/src/blocks.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
use axum::{
Router,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
routing::get,
};
use ethlambda_storage::Store;
use ethlambda_types::primitives::H256;
use serde_json::json;

use crate::json_response;

pub(crate) fn routes() -> Router<Store> {
Router::new()
.route("/lean/v0/blocks/{block_id}", get(get_block))
.route("/lean/v0/blocks/{block_id}/header", get(get_block_header))
}

/// `GET /lean/v0/blocks/:block_id` — returns the block as JSON.
///
/// `block_id` can be a `0x`-prefixed 32-byte hex root or a decimal slot.
pub async fn get_block(
pub(crate) async fn get_block(
Path(block_id): Path<String>,
State(store): State<Store>,
) -> impl IntoResponse {
Expand All @@ -28,7 +36,7 @@ pub async fn get_block(
}

/// `GET /lean/v0/blocks/:block_id/header` — returns the block header as JSON.
pub async fn get_block_header(
pub(crate) async fn get_block_header(
Path(block_id): Path<String>,
State(store): State<Store>,
) -> impl IntoResponse {
Expand Down
73 changes: 73 additions & 0 deletions crates/net/rpc/src/core.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use axum::{
Json, Router,
http::{HeaderValue, header},
response::IntoResponse,
routing::get,
};
use ethlambda_storage::Store;
use ethlambda_types::primitives::H256;
use libssz::SszEncode;

pub(crate) fn routes() -> Router<Store> {
Router::new()
.route("/lean/v0/health", get(crate::metrics::get_health))
.route("/lean/v0/states/finalized", get(get_latest_finalized_state))
.route("/lean/v0/blocks/finalized", get(get_latest_finalized_block))
.route(
"/lean/v0/checkpoints/justified",
get(get_latest_justified_state),
)
}

pub(crate) async fn get_latest_finalized_state(
axum::extract::State(store): axum::extract::State<Store>,
) -> impl IntoResponse {
let finalized = store.latest_finalized();
let mut state = store
.get_state(&finalized.root)
.expect("finalized state exists");

// Zero state_root to match the canonical post-state representation.
// The spec's state_transition sets state_root to zero during process_block_header,
// and only fills it in lazily at the next slot's process_slots.
// Serving the canonical form ensures checkpoint sync interoperability.
state.latest_block_header.state_root = H256::ZERO;

ssz_response(state.to_ssz())
}

pub(crate) async fn get_latest_finalized_block(
axum::extract::State(store): axum::extract::State<Store>,
) -> impl IntoResponse {
let finalized = store.latest_finalized();
// Returns 404 for genesis since it doesn't have a valid signature
match store.get_signed_block(&finalized.root) {
Some(block) => ssz_response(block.to_ssz()),
None => axum::http::StatusCode::NOT_FOUND.into_response(),
}
}

pub(crate) async fn get_latest_justified_state(
axum::extract::State(store): axum::extract::State<Store>,
) -> impl IntoResponse {
let checkpoint = store.latest_justified();
json_response(checkpoint)
}

pub(crate) fn json_response<T: serde::Serialize>(value: T) -> axum::response::Response {
let mut response = Json(value).into_response();
response.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static(crate::JSON_CONTENT_TYPE),
);
response
}

fn ssz_response(bytes: Vec<u8>) -> axum::response::Response {
let mut response = bytes.into_response();
response.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static(crate::SSZ_CONTENT_TYPE),
);
response
}
19 changes: 11 additions & 8 deletions crates/net/rpc/src/fork_choice.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
use axum::{http::HeaderValue, http::header, response::IntoResponse};
use axum::{Router, http::HeaderValue, http::header, response::IntoResponse, routing::get};
use ethlambda_storage::Store;
use ethlambda_types::{checkpoint::Checkpoint, primitives::H256};
use serde::Serialize;

use crate::json_response;

pub(crate) fn routes() -> Router<Store> {
Router::new()
.route("/lean/v0/fork_choice", get(get_fork_choice))
.route("/lean/v0/fork_choice/ui", get(get_fork_choice_ui))
}

const HTML_CONTENT_TYPE: &str = "text/html; charset=utf-8";
const FORK_CHOICE_HTML: &str = include_str!("../static/fork_choice.html");

Expand All @@ -27,7 +33,7 @@ pub struct ForkChoiceNode {
weight: u64,
}

pub async fn get_fork_choice(
pub(crate) async fn get_fork_choice(
axum::extract::State(store): axum::extract::State<Store>,
) -> impl IntoResponse {
let blocks = store.get_live_chain();
Expand Down Expand Up @@ -75,7 +81,7 @@ pub async fn get_fork_choice(
json_response(response)
}

pub async fn get_fork_choice_ui() -> impl IntoResponse {
pub(crate) async fn get_fork_choice_ui() -> impl IntoResponse {
let mut response = FORK_CHOICE_HTML.into_response();
response.headers_mut().insert(
header::CONTENT_TYPE,
Expand All @@ -87,7 +93,7 @@ pub async fn get_fork_choice_ui() -> impl IntoResponse {
#[cfg(test)]
mod tests {
use super::*;
use axum::{Router, body::Body, http::Request, http::StatusCode, routing::get};
use axum::{Router, body::Body, http::Request, http::StatusCode};
use ethlambda_storage::{Store, backend::InMemoryBackend};
use http_body_util::BodyExt;
use std::sync::Arc;
Expand All @@ -96,10 +102,7 @@ mod tests {
use crate::test_utils::create_test_state;

fn build_test_router(store: Store) -> Router {
Router::new()
.route("/lean/v0/fork_choice", get(get_fork_choice))
.route("/lean/v0/fork_choice/ui", get(get_fork_choice_ui))
.with_state(store)
routes().with_state(store)
}

#[tokio::test]
Expand Down
69 changes: 69 additions & 0 deletions crates/net/rpc/src/genesis.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use axum::{Router, extract::State, response::IntoResponse, routing::get};
use ethlambda_storage::Store;
use serde::Serialize;

use crate::json_response;

#[derive(Serialize)]
struct GenesisResponse {
genesis_time: u64,
validator_count: u64,
}

async fn get_genesis(State(store): State<Store>) -> impl IntoResponse {
let genesis_time = store.config().genesis_time;
// Lean validators are fixed at genesis (no churn), so the current head
// state's validator registry always equals the genesis validator count.
let validator_count = store.head_state().validators.len() as u64;
Comment on lines +14 to +17

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Head state used instead of genesis state for validator count

store.head_state() returns the current, evolving head state, not the genesis state. The PR description says this endpoint returns the "genesis validator set," but as the chain progresses and validators are added or removed, head_state().validators.len() will diverge from the genesis validator count. A client relying on this to "verify they are on the correct network" could observe different counts depending on when they query, making the field unreliable for its stated purpose. The genesis validator set should be read from the anchor/genesis state, not from the live head.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/net/rpc/src/genesis.rs
Line: 14-15

Comment:
**Head state used instead of genesis state for validator count**

`store.head_state()` returns the current, evolving head state, not the genesis state. The PR description says this endpoint returns the "genesis validator set," but as the chain progresses and validators are added or removed, `head_state().validators.len()` will diverge from the genesis validator count. A client relying on this to "verify they are on the correct network" could observe different counts depending on when they query, making the field unreliable for its stated purpose. The genesis validator set should be read from the anchor/genesis state, not from the live head.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Full state clone on every request to read one field

store.head_state() deserializes and clones the entire State object just to call .validators.len(). Since genesis data is immutable by definition, this full-state clone on every GET is wasteful. Consider caching the genesis validator count at startup, or reading only the relevant field rather than materializing the whole state.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/net/rpc/src/genesis.rs
Line: 15

Comment:
**Full state clone on every request to read one field**

`store.head_state()` deserializes and clones the entire `State` object just to call `.validators.len()`. Since genesis data is immutable by definition, this full-state clone on every GET is wasteful. Consider caching the genesis validator count at startup, or reading only the relevant field rather than materializing the whole state.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

json_response(GenesisResponse {
genesis_time,
validator_count,
})
}

pub(crate) fn routes() -> Router<Store> {
Router::new().route("/lean/v0/genesis", get(get_genesis))
}

#[cfg(test)]
mod tests {
use super::*;
use axum::{
body::Body,
http::{Request, StatusCode},
};
use ethlambda_storage::{Store, backend::InMemoryBackend};
use ethlambda_types::state::{State, Validator};
use http_body_util::BodyExt;
use std::sync::Arc;
use tower::ServiceExt;

#[tokio::test]
async fn genesis_returns_time_and_validator_count() {
// Build a state with 3 validators so the assertion is non-vacuous.
let dummy_validator = |index: u64| Validator {
attestation_pubkey: [0u8; 52],
proposal_pubkey: [0u8; 52],
index,
};
let validators = vec![dummy_validator(0), dummy_validator(1), dummy_validator(2)];
let state = State::from_genesis(1000, validators);

let store = Store::from_anchor_state(Arc::new(InMemoryBackend::new()), state);
let app = routes().with_state(store);
let resp = app
.oneshot(
Request::builder()
.uri("/lean/v0/genesis")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["genesis_time"], 1000);
assert_eq!(json["validator_count"], 3);
}
}
Loading
Loading