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 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 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}