Skip to content

Commit ee922d7

Browse files
authored
feat(rpc): add new /lean/v0/blocks/finalized endpoint (lambdaclass#353)
## 🗒️ Description / Motivation leanEthereum/leanSpec#713 added a new endpoint to the spec, to be used during checkpoint-sync, similar to how the beacon chain uses it. New checkpoint-sync functionality is still missing, tracked in lambdaclass#354 ## What Changed The `rpc` crate was updated with the new endpoint. ## Tests Added / Run Unit tests were added for the base case and for when a signed block is unavailable. ## ✅ Verification Checklist - [X] Ran `make fmt` — clean - [X] Ran `make lint` (clippy with `-D warnings`) — clean - [X] Ran `cargo test --workspace --release` — all passing
1 parent 9b21dce commit ee922d7

1 file changed

Lines changed: 109 additions & 6 deletions

File tree

crates/net/rpc/src/lib.rs

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use std::net::{IpAddr, SocketAddr};
22

33
use axum::{
4-
Extension, Json, Router, http::HeaderValue, http::header, response::IntoResponse, routing::get,
4+
Extension, Json, Router,
5+
http::{HeaderValue, StatusCode, header},
6+
response::IntoResponse,
7+
routing::get,
58
};
69
use ethlambda_storage::Store;
710
use ethlambda_types::aggregator::AggregatorController;
@@ -75,6 +78,7 @@ fn build_api_router(store: Store) -> Router {
7578
Router::new()
7679
.route("/lean/v0/health", get(metrics::get_health))
7780
.route("/lean/v0/states/finalized", get(get_latest_finalized_state))
81+
.route("/lean/v0/blocks/finalized", get(get_latest_finalized_block))
7882
.route(
7983
"/lean/v0/checkpoints/justified",
8084
get(get_latest_justified_state),
@@ -118,6 +122,17 @@ async fn get_latest_finalized_state(
118122
ssz_response(state.to_ssz())
119123
}
120124

125+
async fn get_latest_finalized_block(
126+
axum::extract::State(store): axum::extract::State<Store>,
127+
) -> impl IntoResponse {
128+
let finalized = store.latest_finalized();
129+
// Returns 404 for genesis since it doesn't have a valid signature
130+
match store.get_signed_block(&finalized.root) {
131+
Some(block) => ssz_response(block.to_ssz()),
132+
None => StatusCode::NOT_FOUND.into_response(),
133+
}
134+
}
135+
121136
async fn get_latest_justified_state(
122137
axum::extract::State(store): axum::extract::State<Store>,
123138
) -> impl IntoResponse {
@@ -185,11 +200,8 @@ pub(crate) mod test_utils {
185200
#[cfg(test)]
186201
mod tests {
187202
use super::*;
188-
use axum::{
189-
body::Body,
190-
http::{Request, StatusCode},
191-
};
192-
use ethlambda_storage::{Store, backend::InMemoryBackend};
203+
use axum::{body::Body, http::Request};
204+
use ethlambda_storage::{ForkCheckpoints, Store, backend::InMemoryBackend};
193205
use http_body_util::BodyExt;
194206
use serde_json::json;
195207
use std::sync::Arc;
@@ -266,4 +278,95 @@ mod tests {
266278
let body = response.into_body().collect().await.unwrap().to_bytes();
267279
assert_eq!(body.as_ref(), expected_ssz.as_slice());
268280
}
281+
282+
#[tokio::test]
283+
async fn test_get_latest_finalized_block() {
284+
use ethlambda_types::{
285+
attestation::XmssSignature,
286+
block::{Block, BlockBody, BlockSignatures, SignedBlock},
287+
checkpoint::Checkpoint,
288+
primitives::{H256, HashTreeRoot as _},
289+
signature::SIGNATURE_SIZE,
290+
};
291+
use libssz::SszEncode;
292+
293+
let state = create_test_state();
294+
let backend = Arc::new(InMemoryBackend::new());
295+
let mut store = Store::from_anchor_state(backend, state);
296+
297+
// Build a non-genesis signed block with empty body and zero proposer signature.
298+
let block = Block {
299+
slot: 1,
300+
proposer_index: 0,
301+
parent_root: store.latest_finalized().root,
302+
state_root: H256::ZERO,
303+
body: BlockBody::default(),
304+
};
305+
let block_root = block.header().hash_tree_root();
306+
let signed_block = SignedBlock {
307+
message: block,
308+
signature: BlockSignatures {
309+
attestation_signatures: Default::default(),
310+
proposer_signature: XmssSignature::try_from(vec![0u8; SIGNATURE_SIZE]).unwrap(),
311+
},
312+
};
313+
314+
// Persist the signed block and mark it as the latest finalized checkpoint.
315+
store.insert_signed_block(block_root, signed_block.clone());
316+
store.update_checkpoints(ForkCheckpoints::new(
317+
block_root,
318+
None,
319+
Some(Checkpoint {
320+
root: block_root,
321+
slot: 1,
322+
}),
323+
));
324+
325+
let expected_ssz = signed_block.to_ssz();
326+
327+
let app = build_api_router(store);
328+
329+
let response = app
330+
.oneshot(
331+
Request::builder()
332+
.uri("/lean/v0/blocks/finalized")
333+
.body(Body::empty())
334+
.unwrap(),
335+
)
336+
.await
337+
.unwrap();
338+
339+
assert_eq!(response.status(), StatusCode::OK);
340+
assert_eq!(
341+
response.headers().get(header::CONTENT_TYPE).unwrap(),
342+
SSZ_CONTENT_TYPE
343+
);
344+
345+
let body = response.into_body().collect().await.unwrap().to_bytes();
346+
assert_eq!(body.as_ref(), expected_ssz.as_slice());
347+
}
348+
349+
#[tokio::test]
350+
async fn test_get_latest_finalized_block_returns_404_when_absent() {
351+
// Genesis-anchored store: init_store writes header + state but no
352+
// BlockSignatures entry, so get_signed_block(genesis_root) returns None
353+
// and the endpoint must report 404 rather than panic.
354+
let state = create_test_state();
355+
let backend = Arc::new(InMemoryBackend::new());
356+
let store = Store::from_anchor_state(backend, state);
357+
358+
let app = build_api_router(store);
359+
360+
let response = app
361+
.oneshot(
362+
Request::builder()
363+
.uri("/lean/v0/blocks/finalized")
364+
.body(Body::empty())
365+
.unwrap(),
366+
)
367+
.await
368+
.unwrap();
369+
370+
assert_eq!(response.status(), StatusCode::NOT_FOUND);
371+
}
269372
}

0 commit comments

Comments
 (0)