Skip to content
Merged
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
3 changes: 3 additions & 0 deletions Cargo.lock

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

5 changes: 5 additions & 0 deletions crates/net/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ tracing.workspace = true
ethlambda-storage.workspace = true
serde.workspace = true
serde_json.workspace = true

[dev-dependencies]
ethlambda-types.workspace = true
tower = { version = "0.5", features = ["util"] }
http-body-util = "0.1"
163 changes: 151 additions & 12 deletions crates/net/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,27 @@ pub mod metrics;

pub async fn start_rpc_server(address: SocketAddr, store: Store) -> Result<(), std::io::Error> {
let metrics_router = metrics::start_prometheus_metrics_api();
let api_router = build_api_router(store);

// 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);
let app = Router::new().merge(metrics_router).merge(api_router);

// Start the axum app
let listener = tokio::net::TcpListener::bind(address).await?;
axum::serve(listener, app).await?;

Ok(())
}

/// Build the API router with the given store.
fn build_api_router(store: Store) -> Router {
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)
}

async fn get_latest_finalized_state(
axum::extract::State(store): axum::extract::State<Store>,
) -> impl IntoResponse {
Expand All @@ -43,3 +44,141 @@ async fn get_latest_justified_state(
let checkpoint = store.latest_justified();
Json(checkpoint)
}

#[cfg(test)]
mod tests {
use super::*;
use axum::{
body::Body,
http::{Request, StatusCode},
};
use ethlambda_storage::Store;
use ethlambda_types::{
block::{BlockBody, BlockHeader},
primitives::TreeHash,
state::{ChainConfig, Checkpoint, JustificationValidators, JustifiedSlots, State},
};
use http_body_util::BodyExt;
use serde_json::json;
use tower::ServiceExt;

/// Create a minimal test state for testing.
fn create_test_state() -> State {
let genesis_header = BlockHeader {
slot: 0,
proposer_index: 0,
parent_root: ethlambda_types::primitives::H256::ZERO,
state_root: ethlambda_types::primitives::H256::ZERO,
body_root: BlockBody::default().tree_hash_root(),
};

let genesis_checkpoint = Checkpoint {
root: ethlambda_types::primitives::H256::ZERO,
slot: 0,
};

State {
config: ChainConfig { genesis_time: 1000 },
slot: 0,
latest_block_header: genesis_header,
latest_justified: genesis_checkpoint,
latest_finalized: genesis_checkpoint,
historical_block_hashes: Default::default(),
justified_slots: JustifiedSlots::with_capacity(0).unwrap(),
validators: Default::default(),
justifications_roots: Default::default(),
justifications_validators: JustificationValidators::with_capacity(0).unwrap(),
}
}

#[tokio::test]
async fn test_get_latest_justified_checkpoint() {
let state = create_test_state();
let store = Store::from_genesis(state);

let app = build_api_router(store.clone());

let response = app
.oneshot(
Request::builder()
.uri("/lean/v0/checkpoints/justified")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::OK);

let body = response.into_body().collect().await.unwrap().to_bytes();
let checkpoint: serde_json::Value = serde_json::from_slice(&body).unwrap();

// The justified checkpoint should match the store's latest justified
let expected = store.latest_justified();
assert_eq!(
checkpoint,
json!({
"slot": expected.slot,
"root": format!("{:#x}", expected.root)
})
);
}

#[tokio::test]
async fn test_get_latest_finalized_state() {
let state = create_test_state();
let store = Store::from_genesis(state);

// Get the expected state from the store to build expected JSON
let finalized = store.latest_finalized();
let expected_state = store.get_state(&finalized.root).unwrap();

let app = build_api_router(store);

let response = app
.oneshot(
Request::builder()
.uri("/lean/v0/states/finalized")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::OK);

let body = response.into_body().collect().await.unwrap().to_bytes();
let returned_state: serde_json::Value = serde_json::from_slice(&body).unwrap();

let header = &expected_state.latest_block_header;
assert_eq!(
returned_state,
json!({
"config": {
"genesis_time": expected_state.config.genesis_time
},
"slot": expected_state.slot,
"latest_block_header": {
"slot": header.slot,
"proposer_index": header.proposer_index,
"parent_root": format!("{:#x}", header.parent_root),
"state_root": format!("{:#x}", header.state_root),
"body_root": format!("{:#x}", header.body_root)
},
"latest_justified": {
"slot": expected_state.latest_justified.slot,
"root": format!("{:#x}", expected_state.latest_justified.root)
},
"latest_finalized": {
"slot": expected_state.latest_finalized.slot,
"root": format!("{:#x}", expected_state.latest_finalized.root)
},
"historical_block_hashes": [],
"justified_slots": "0x01",
"validators": [],
"justifications_roots": [],
"justifications_validators": "0x01"
})
);
}
}