Skip to main content

pot_o_validator/
hex_api.rs

1use std::sync::Arc;
2
3use axum::{
4    extract::{Path, Query, State},
5    http::StatusCode,
6    middleware,
7    response::IntoResponse,
8    routing::{get, post},
9    Json, Router,
10};
11use hexchain_p2p::hex_consensus::HexProof;
12use hexchain_p2p::lattice_geometry::HCPCoord;
13use hexchain_p2p::lattice_store::LatticeSnapshot;
14use serde::Deserialize;
15
16use crate::consensus::{accept_block, rollback_ledger_to, AppState, RELAYED_BLOCKS_MAX};
17use crate::rate_limit::{rate_limit_middleware, RateLimiter};
18
19type HexApiResponse = (StatusCode, Json<serde_json::Value>);
20
21pub fn hex_routes(state: Arc<AppState>) -> Router {
22    let hex_submit_limiter = RateLimiter::new(3, 1);
23
24    let hex_submit_route = Router::new()
25        .route("/hexchain/submit", post(post_hex_submit))
26        .layer(middleware::from_fn(rate_limit_middleware))
27        .layer(axum::Extension(hex_submit_limiter));
28
29    Router::new()
30        .route("/hexchain/challenge", post(post_hex_challenge))
31        .route("/hexchain/status", get(get_hex_status))
32        .route("/hexchain/lattice", get(get_hex_lattice_all))
33        .route("/hexchain/lattice/sync", post(post_hex_lattice_sync))
34        .route("/hexchain/lattice/{q}/{r}/{s}", get(get_hex_lattice_coord))
35        .route("/hexchain/block/{height}", get(get_hex_block_by_height))
36        .route("/hexchain/blocks", get(get_hex_blocks_batch))
37        .merge(hex_submit_route)
38        .with_state(state)
39}
40
41#[derive(Deserialize)]
42struct HexChallengeRequest {
43    slot: u64,
44    slot_hash: String,
45}
46
47async fn post_hex_challenge(
48    State(state): State<Arc<AppState>>,
49    Json(body): Json<HexChallengeRequest>,
50) -> impl IntoResponse {
51    tracing::debug!(slot = body.slot, "POST /hexchain/challenge");
52    let challenge = state
53        .hex_consensus
54        .generate_challenge(body.slot, &body.slot_hash);
55    {
56        let mut current = state.hex_current_challenge.write().await;
57        *current = Some(challenge.clone());
58    }
59    tracing::info!(
60        challenge_id = %challenge.id,
61        coord = ?challenge.coord,
62        "POST /hexchain/challenge issued"
63    );
64    (
65        StatusCode::OK,
66        Json(serde_json::to_value(&challenge).unwrap_or_default()),
67    )
68}
69
70async fn post_hex_submit(
71    State(state): State<Arc<AppState>>,
72    Json(proof): Json<HexProof>,
73) -> impl IntoResponse {
74    tracing::debug!(challenge_id = %proof.challenge_id, "POST /hexchain/submit");
75
76    match state.hex_consensus.verify_proof(&proof) {
77        Ok(true) => match state.hex_consensus.submit_block(&proof) {
78            Ok(depth) => {
79                if state.extensions.tribechain_enabled {
80                    let mempool = state.extensions.mempool.as_deref();
81                    let block_store = state.extensions.block_store.as_deref();
82                    let mut ledger = state.extensions.ledger.write().await;
83                    if let Err(e) = accept_block(&proof.block, &mut ledger, mempool, block_store) {
84                        tracing::warn!(error = %e, "Tribechain block acceptance failed");
85                        return (
86                            StatusCode::INTERNAL_SERVER_ERROR,
87                            Json(
88                                serde_json::json!({ "accepted": false, "error": "internal error" }),
89                            ),
90                        );
91                    }
92                    *state.canonical_tip_height.write().await += 1;
93                    drop(ledger);
94                    relay_block(&state, &proof).await;
95                }
96                // Persist lattice immediately so restarts never lose accepted blocks
97                if let Err(e) = state.hex_consensus.store.save_to_file() {
98                    tracing::warn!(error = %e, "Hex lattice post-submit persist failed (non-fatal)");
99                }
100                tracing::info!(
101                    challenge_id = %proof.challenge_id,
102                    coord = ?proof.block.coord,
103                    depth = depth,
104                    "POST /hexchain/submit accepted"
105                );
106                (
107                    StatusCode::OK,
108                    Json(serde_json::json!({
109                        "accepted": true,
110                        "depth": depth,
111                        "block_hash": hex::encode(proof.block.pow_hash()),
112                    })),
113                )
114            }
115            Err(e) => {
116                tracing::warn!(error = ?e, "POST /hexchain/submit block insertion failed");
117                (
118                    StatusCode::INTERNAL_SERVER_ERROR,
119                    Json(serde_json::json!({ "accepted": false, "error": "internal error" })),
120                )
121            }
122        },
123        Ok(false) => {
124            tracing::info!(challenge_id = %proof.challenge_id, "POST /hexchain/submit rejected (genesis mode, no validation)");
125            match state.hex_consensus.submit_block(&proof) {
126                Ok(depth) => {
127                    if state.extensions.tribechain_enabled {
128                        let mempool = state.extensions.mempool.as_deref();
129                        let block_store = state.extensions.block_store.as_deref();
130                        let mut ledger = state.extensions.ledger.write().await;
131                        if let Err(e) =
132                            accept_block(&proof.block, &mut ledger, mempool, block_store)
133                        {
134                            tracing::warn!(error = %e, "Tribechain genesis block acceptance failed");
135                            return (
136                                StatusCode::INTERNAL_SERVER_ERROR,
137                                Json(
138                                    serde_json::json!({ "accepted": false, "error": "internal error" }),
139                                ),
140                            );
141                        }
142                        *state.canonical_tip_height.write().await += 1;
143                        drop(ledger);
144                        relay_block(&state, &proof).await;
145                    }
146                    // Persist lattice immediately (genesis mode)
147                    if let Err(e) = state.hex_consensus.store.save_to_file() {
148                        tracing::warn!(error = %e, "Hex lattice genesis persist failed (non-fatal)");
149                    }
150                    (
151                        StatusCode::OK,
152                        Json(serde_json::json!({
153                            "accepted": true,
154                            "depth": depth,
155                            "block_hash": hex::encode(proof.block.pow_hash()),
156                        })),
157                    )
158                }
159                Err(e) => {
160                    tracing::warn!(error = ?e, "POST /hexchain/submit genesis block insertion failed");
161                    (
162                        StatusCode::INTERNAL_SERVER_ERROR,
163                        Json(serde_json::json!({
164                            "accepted": false, "error": "internal error"
165                        })),
166                    )
167                }
168            }
169        }
170        Err(e) => {
171            tracing::warn!(error = ?e, "POST /hexchain/submit verification failed");
172            (
173                StatusCode::BAD_REQUEST,
174                Json(serde_json::json!({ "accepted": false, "error": "verification failed" })),
175            )
176        }
177    }
178}
179
180async fn get_hex_status(State(state): State<Arc<AppState>>) -> HexApiResponse {
181    let occupied = state.hex_consensus.store.all_coords().len();
182    let all_blocks = state.hex_consensus.store.all_blocks();
183    let latest_depth = all_blocks
184        .iter()
185        .filter_map(|(_, h)| state.hex_consensus.store.depth_of(h))
186        .max();
187    let current_challenge = state.hex_current_challenge.read().await.clone();
188
189    (
190        StatusCode::OK,
191        Json(serde_json::json!({
192            "occupied_coords": occupied,
193            "latest_depth": latest_depth,
194            "current_challenge": current_challenge,
195        })),
196    )
197}
198
199async fn get_hex_lattice_all(State(state): State<Arc<AppState>>) -> HexApiResponse {
200    let coords: Vec<HCPCoord> = state.hex_consensus.store.all_coords().into_iter().collect();
201    let blocks: Vec<serde_json::Value> = coords
202        .iter()
203        .filter_map(|c| {
204            let hash = state.hex_consensus.store.hash_at(*c)?;
205            let depth = state.hex_consensus.store.depth_of(&hash)?;
206            Some(serde_json::json!({
207                "coord": c,
208                "block_hash": hex::encode(hash),
209                "depth": depth,
210            }))
211        })
212        .collect();
213    (
214        StatusCode::OK,
215        Json(serde_json::json!({ "blocks": blocks })),
216    )
217}
218
219async fn post_hex_lattice_sync(
220    State(state): State<Arc<AppState>>,
221    Json(snapshot): Json<LatticeSnapshot>,
222) -> HexApiResponse {
223    let before = state.hex_consensus.store.all_coords().len();
224    state.hex_consensus.store.merge_snapshot(&snapshot);
225    let after = state.hex_consensus.store.all_coords().len();
226    let merged = after.saturating_sub(before);
227    if let Err(e) = state.hex_consensus.store.save_to_file() {
228        tracing::warn!(error = %e, "POST /hexchain/lattice/sync persist failed");
229    }
230
231    if state.extensions.tribechain_enabled {
232        let new_max_depth = state
233            .hex_consensus
234            .store
235            .all_blocks()
236            .iter()
237            .filter_map(|(_, h)| state.hex_consensus.store.depth_of(h))
238            .max()
239            .unwrap_or(0) as i64;
240        let canonical_height = *state.canonical_tip_height.read().await as i64;
241        if new_max_depth < canonical_height {
242            tracing::warn!(
243                old_height = canonical_height,
244                new_height = new_max_depth,
245                "Chain reorg detected, rolling back ledger"
246            );
247            if let Some(block_store) = &state.extensions.block_store {
248                let mut ledger = state.extensions.ledger.write().await;
249                let mut tip_height = state.canonical_tip_height.write().await;
250                if let Err(e) = rollback_ledger_to(
251                    &mut ledger,
252                    block_store.as_ref(),
253                    new_max_depth as u64,
254                    &mut tip_height,
255                ) {
256                    tracing::error!(error = %e, "Ledger rollback failed");
257                }
258            }
259        }
260    }
261
262    tracing::info!(merged, total = after, "POST /hexchain/lattice/sync");
263    (
264        StatusCode::OK,
265        Json(serde_json::json!({
266            "accepted": true,
267            "merged": merged,
268            "total_coords": after,
269        })),
270    )
271}
272
273async fn get_hex_lattice_coord(
274    State(state): State<Arc<AppState>>,
275    Path((q, r, s)): Path<(i32, i32, i32)>,
276) -> HexApiResponse {
277    let coord = HCPCoord { q, r, s };
278    match state.hex_consensus.store.hash_at(coord) {
279        Some(hash) => {
280            let depth = state.hex_consensus.store.depth_of(&hash);
281            (
282                StatusCode::OK,
283                Json(serde_json::json!({
284                    "coord": coord,
285                    "block_hash": hex::encode(hash),
286                    "depth": depth,
287                })),
288            )
289        }
290        None => (
291            StatusCode::NOT_FOUND,
292            Json(serde_json::json!({ "error": "No block at this coordinate" })),
293        ),
294    }
295}
296
297async fn get_hex_block_by_height(
298    State(state): State<Arc<AppState>>,
299    Path(height): Path<u64>,
300) -> HexApiResponse {
301    let block_store = match &state.extensions.block_store {
302        Some(bs) => bs,
303        None => {
304            return (
305                StatusCode::SERVICE_UNAVAILABLE,
306                Json(serde_json::json!({"error": "block store not available"})),
307            )
308        }
309    };
310
311    let stored = match block_store.at_height(height) {
312        Some(s) => s,
313        None => {
314            return (
315                StatusCode::NOT_FOUND,
316                Json(serde_json::json!({"error": "No block at this height"})),
317            )
318        }
319    };
320
321    let block: hexchain_p2p::block::HexBlock = match serde_json::from_str(&stored.block_json) {
322        Ok(b) => b,
323        Err(e) => {
324            tracing::warn!(error = %e, "Failed to parse stored block");
325            return (
326                StatusCode::INTERNAL_SERVER_ERROR,
327                Json(serde_json::json!({"error": "internal error"})),
328            );
329        }
330    };
331
332    (
333        StatusCode::OK,
334        Json(serde_json::json!({
335            "height": stored.height,
336            "hash": hex::encode(stored.hash),
337            "block": block,
338        })),
339    )
340}
341
342#[derive(Deserialize)]
343struct BlocksBatchQuery {
344    from_height: Option<u64>,
345    limit: Option<usize>,
346}
347
348async fn get_hex_blocks_batch(
349    State(state): State<Arc<AppState>>,
350    Query(query): Query<BlocksBatchQuery>,
351) -> HexApiResponse {
352    let block_store = match &state.extensions.block_store {
353        Some(bs) => bs,
354        None => {
355            return (
356                StatusCode::SERVICE_UNAVAILABLE,
357                Json(serde_json::json!({"error": "block store not available"})),
358            )
359        }
360    };
361
362    let from_height = query.from_height.unwrap_or(0);
363    let limit = query.limit.unwrap_or(50).min(200);
364    let latest = block_store.latest_height();
365
366    let mut blocks = Vec::new();
367    for h in from_height..=latest {
368        if blocks.len() >= limit {
369            break;
370        }
371        if let Some(stored) = block_store.at_height(h) {
372            blocks.push(serde_json::json!({
373                "height": stored.height,
374                "hash": hex::encode(stored.hash),
375                "block_json": stored.block_json,
376            }));
377        }
378    }
379
380    (
381        StatusCode::OK,
382        Json(serde_json::json!({
383            "blocks": blocks,
384            "count": blocks.len(),
385        })),
386    )
387}
388
389async fn relay_block(state: &AppState, proof: &HexProof) {
390    let block_hash = proof.block.pow_hash();
391    {
392        let mut relayed = state.relayed_blocks.write().await;
393        if relayed.contains(&block_hash) {
394            return;
395        }
396        relayed.push_back(block_hash);
397        while relayed.len() > RELAYED_BLOCKS_MAX {
398            relayed.pop_front();
399        }
400    }
401
402    let network = state.extensions.network.clone();
403    let proof_json = match serde_json::to_value(proof) {
404        Ok(v) => v,
405        Err(e) => {
406            tracing::warn!(error = %e, "Failed to serialize block for relay");
407            return;
408        }
409    };
410    let block_height = proof.block.height;
411
412    tokio::spawn(async move {
413        match network.broadcast_block(&proof_json).await {
414            Ok(n) if n > 0 => {
415                tracing::info!(peers = n, height = block_height, "Block relayed to peers");
416            }
417            Ok(_) => {}
418            Err(e) => {
419                tracing::warn!(error = %e, "Block relay to peers failed");
420            }
421        }
422    });
423}