@@ -7,26 +7,27 @@ pub mod metrics;
77
88pub async fn start_rpc_server ( address : SocketAddr , store : Store ) -> Result < ( ) , std:: io:: Error > {
99 let metrics_router = metrics:: start_prometheus_metrics_api ( ) ;
10+ let api_router = build_api_router ( store) ;
1011
11- // Create stateful routes first, then convert to stateless by applying state
12- let api_routes = Router :: new ( )
13- . route ( "/lean/v0/states/finalized" , get ( get_latest_finalized_state) )
14- . route (
15- "/lean/v0/checkpoints/justified" ,
16- get ( get_latest_justified_state) ,
17- )
18- . with_state ( store) ;
19-
20- // Merge stateless routers
21- let app = Router :: new ( ) . merge ( metrics_router) . merge ( api_routes) ;
12+ let app = Router :: new ( ) . merge ( metrics_router) . merge ( api_router) ;
2213
23- // Start the axum app
2414 let listener = tokio:: net:: TcpListener :: bind ( address) . await ?;
2515 axum:: serve ( listener, app) . await ?;
2616
2717 Ok ( ( ) )
2818}
2919
20+ /// Build the API router with the given store.
21+ fn build_api_router ( store : Store ) -> Router {
22+ Router :: new ( )
23+ . route ( "/lean/v0/states/finalized" , get ( get_latest_finalized_state) )
24+ . route (
25+ "/lean/v0/checkpoints/justified" ,
26+ get ( get_latest_justified_state) ,
27+ )
28+ . with_state ( store)
29+ }
30+
3031async fn get_latest_finalized_state (
3132 axum:: extract:: State ( store) : axum:: extract:: State < Store > ,
3233) -> impl IntoResponse {
@@ -43,3 +44,141 @@ async fn get_latest_justified_state(
4344 let checkpoint = store. latest_justified ( ) ;
4445 Json ( checkpoint)
4546}
47+
48+ #[ cfg( test) ]
49+ mod tests {
50+ use super :: * ;
51+ use axum:: {
52+ body:: Body ,
53+ http:: { Request , StatusCode } ,
54+ } ;
55+ use ethlambda_storage:: Store ;
56+ use ethlambda_types:: {
57+ block:: { BlockBody , BlockHeader } ,
58+ primitives:: TreeHash ,
59+ state:: { ChainConfig , Checkpoint , JustificationValidators , JustifiedSlots , State } ,
60+ } ;
61+ use http_body_util:: BodyExt ;
62+ use serde_json:: json;
63+ use tower:: ServiceExt ;
64+
65+ /// Create a minimal test state for testing.
66+ fn create_test_state ( ) -> State {
67+ let genesis_header = BlockHeader {
68+ slot : 0 ,
69+ proposer_index : 0 ,
70+ parent_root : ethlambda_types:: primitives:: H256 :: ZERO ,
71+ state_root : ethlambda_types:: primitives:: H256 :: ZERO ,
72+ body_root : BlockBody :: default ( ) . tree_hash_root ( ) ,
73+ } ;
74+
75+ let genesis_checkpoint = Checkpoint {
76+ root : ethlambda_types:: primitives:: H256 :: ZERO ,
77+ slot : 0 ,
78+ } ;
79+
80+ State {
81+ config : ChainConfig { genesis_time : 1000 } ,
82+ slot : 0 ,
83+ latest_block_header : genesis_header,
84+ latest_justified : genesis_checkpoint,
85+ latest_finalized : genesis_checkpoint,
86+ historical_block_hashes : Default :: default ( ) ,
87+ justified_slots : JustifiedSlots :: with_capacity ( 0 ) . unwrap ( ) ,
88+ validators : Default :: default ( ) ,
89+ justifications_roots : Default :: default ( ) ,
90+ justifications_validators : JustificationValidators :: with_capacity ( 0 ) . unwrap ( ) ,
91+ }
92+ }
93+
94+ #[ tokio:: test]
95+ async fn test_get_latest_justified_checkpoint ( ) {
96+ let state = create_test_state ( ) ;
97+ let store = Store :: from_genesis ( state) ;
98+
99+ let app = build_api_router ( store. clone ( ) ) ;
100+
101+ let response = app
102+ . oneshot (
103+ Request :: builder ( )
104+ . uri ( "/lean/v0/checkpoints/justified" )
105+ . body ( Body :: empty ( ) )
106+ . unwrap ( ) ,
107+ )
108+ . await
109+ . unwrap ( ) ;
110+
111+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
112+
113+ let body = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
114+ let checkpoint: serde_json:: Value = serde_json:: from_slice ( & body) . unwrap ( ) ;
115+
116+ // The justified checkpoint should match the store's latest justified
117+ let expected = store. latest_justified ( ) ;
118+ assert_eq ! (
119+ checkpoint,
120+ json!( {
121+ "slot" : expected. slot,
122+ "root" : format!( "{:#x}" , expected. root)
123+ } )
124+ ) ;
125+ }
126+
127+ #[ tokio:: test]
128+ async fn test_get_latest_finalized_state ( ) {
129+ let state = create_test_state ( ) ;
130+ let store = Store :: from_genesis ( state) ;
131+
132+ // Get the expected state from the store to build expected JSON
133+ let finalized = store. latest_finalized ( ) ;
134+ let expected_state = store. get_state ( & finalized. root ) . unwrap ( ) ;
135+
136+ let app = build_api_router ( store) ;
137+
138+ let response = app
139+ . oneshot (
140+ Request :: builder ( )
141+ . uri ( "/lean/v0/states/finalized" )
142+ . body ( Body :: empty ( ) )
143+ . unwrap ( ) ,
144+ )
145+ . await
146+ . unwrap ( ) ;
147+
148+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
149+
150+ let body = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
151+ let returned_state: serde_json:: Value = serde_json:: from_slice ( & body) . unwrap ( ) ;
152+
153+ let header = & expected_state. latest_block_header ;
154+ assert_eq ! (
155+ returned_state,
156+ json!( {
157+ "config" : {
158+ "genesis_time" : expected_state. config. genesis_time
159+ } ,
160+ "slot" : expected_state. slot,
161+ "latest_block_header" : {
162+ "slot" : header. slot,
163+ "proposer_index" : header. proposer_index,
164+ "parent_root" : format!( "{:#x}" , header. parent_root) ,
165+ "state_root" : format!( "{:#x}" , header. state_root) ,
166+ "body_root" : format!( "{:#x}" , header. body_root)
167+ } ,
168+ "latest_justified" : {
169+ "slot" : expected_state. latest_justified. slot,
170+ "root" : format!( "{:#x}" , expected_state. latest_justified. root)
171+ } ,
172+ "latest_finalized" : {
173+ "slot" : expected_state. latest_finalized. slot,
174+ "root" : format!( "{:#x}" , expected_state. latest_finalized. root)
175+ } ,
176+ "historical_block_hashes" : [ ] ,
177+ "justified_slots" : "0x01" ,
178+ "validators" : [ ] ,
179+ "justifications_roots" : [ ] ,
180+ "justifications_validators" : "0x01"
181+ } )
182+ ) ;
183+ }
184+ }
0 commit comments