|
| 1 | +//! Block API endpoints |
| 2 | +
|
| 3 | +use axum::{ |
| 4 | + extract::{Path, State}, |
| 5 | + http::StatusCode, |
| 6 | + Json, |
| 7 | +}; |
| 8 | +use serde::Serialize; |
| 9 | +use std::sync::Arc; |
| 10 | + |
| 11 | +use crate::AppState; |
| 12 | +use bitcell_ca::{Battle, BattleOutcome, Glider, GliderPattern, Position}; |
| 13 | + |
| 14 | +#[derive(Debug, Serialize)] |
| 15 | +pub struct BlockInfo { |
| 16 | + pub height: u64, |
| 17 | + pub hash: String, |
| 18 | + pub timestamp: u64, |
| 19 | + pub proposer: String, |
| 20 | + pub transaction_count: usize, |
| 21 | + pub battle_count: usize, |
| 22 | +} |
| 23 | + |
| 24 | +#[derive(Debug, Serialize)] |
| 25 | +pub struct BlockListResponse { |
| 26 | + pub blocks: Vec<BlockInfo>, |
| 27 | + pub total: usize, |
| 28 | +} |
| 29 | + |
| 30 | +#[derive(Debug, Serialize)] |
| 31 | +pub struct BlockDetailResponse { |
| 32 | + pub height: u64, |
| 33 | + pub hash: String, |
| 34 | + pub timestamp: u64, |
| 35 | + pub proposer: String, |
| 36 | + pub prev_hash: String, |
| 37 | + pub tx_root: String, |
| 38 | + pub state_root: String, |
| 39 | + pub transactions: Vec<TransactionInfo>, |
| 40 | + pub battle_count: usize, |
| 41 | +} |
| 42 | + |
| 43 | +#[derive(Debug, Serialize)] |
| 44 | +pub struct TransactionInfo { |
| 45 | + pub hash: String, |
| 46 | + pub from: String, |
| 47 | + pub to: String, |
| 48 | + pub amount: u64, |
| 49 | +} |
| 50 | + |
| 51 | +#[derive(Debug, Serialize)] |
| 52 | +pub struct BlockBattleFrame { |
| 53 | + pub step: usize, |
| 54 | + pub grid: Vec<Vec<u8>>, |
| 55 | + pub energy_a: u64, |
| 56 | + pub energy_b: u64, |
| 57 | +} |
| 58 | + |
| 59 | +#[derive(Debug, Serialize)] |
| 60 | +pub struct BlockBattleVisualization { |
| 61 | + pub block_height: u64, |
| 62 | + pub battle_index: usize, |
| 63 | + pub glider_a_pattern: String, |
| 64 | + pub glider_b_pattern: String, |
| 65 | + pub winner: String, |
| 66 | + pub steps: usize, |
| 67 | + pub frames: Vec<BlockBattleFrame>, |
| 68 | +} |
| 69 | + |
| 70 | +/// List recent blocks |
| 71 | +pub async fn list_blocks( |
| 72 | + State(state): State<Arc<AppState>>, |
| 73 | +) -> Result<Json<BlockListResponse>, (StatusCode, Json<String>)> { |
| 74 | + // Get all registered nodes |
| 75 | + let nodes = state.process.list_nodes(); |
| 76 | + |
| 77 | + if nodes.is_empty() { |
| 78 | + return Err(( |
| 79 | + StatusCode::SERVICE_UNAVAILABLE, |
| 80 | + Json("No nodes available. Please deploy nodes first.".to_string()), |
| 81 | + )); |
| 82 | + } |
| 83 | + |
| 84 | + // Try to fetch blocks from the first running node |
| 85 | + // In a real implementation, this would query the blockchain via RPC |
| 86 | + // For now, we'll return mock data based on chain height from metrics |
| 87 | + |
| 88 | + let endpoints: Vec<(String, String)> = nodes |
| 89 | + .iter() |
| 90 | + .map(|n| { |
| 91 | + let metrics_port = n.port + 1; |
| 92 | + (n.id.clone(), format!("http://127.0.0.1:{}/metrics", metrics_port)) |
| 93 | + }) |
| 94 | + .collect(); |
| 95 | + |
| 96 | + if endpoints.is_empty() { |
| 97 | + return Err(( |
| 98 | + StatusCode::SERVICE_UNAVAILABLE, |
| 99 | + Json("No running nodes found.".to_string()), |
| 100 | + )); |
| 101 | + } |
| 102 | + |
| 103 | + // Fetch current chain height |
| 104 | + let aggregated = state.metrics_client.aggregate_metrics(&endpoints) |
| 105 | + .await |
| 106 | + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?; |
| 107 | + |
| 108 | + let chain_height = aggregated.chain_height; |
| 109 | + |
| 110 | + // Generate mock block list (most recent 10 blocks) |
| 111 | + let start_height = chain_height.saturating_sub(9); |
| 112 | + let mut blocks = Vec::new(); |
| 113 | + |
| 114 | + for height in start_height..=chain_height { |
| 115 | + blocks.push(BlockInfo { |
| 116 | + height, |
| 117 | + hash: format!("0x{:016x}", height * 12345), |
| 118 | + timestamp: 1700000000 + (height * 600), // 10 min blocks |
| 119 | + proposer: format!("miner-{}", height % 3), |
| 120 | + transaction_count: (height % 5) as usize, |
| 121 | + battle_count: 1, // Each block has 1 battle in simplified model |
| 122 | + }); |
| 123 | + } |
| 124 | + |
| 125 | + // Reverse to show newest first |
| 126 | + blocks.reverse(); |
| 127 | + |
| 128 | + Ok(Json(BlockListResponse { |
| 129 | + total: blocks.len(), |
| 130 | + blocks, |
| 131 | + })) |
| 132 | +} |
| 133 | + |
| 134 | +/// Get block details by height |
| 135 | +pub async fn get_block( |
| 136 | + State(state): State<Arc<AppState>>, |
| 137 | + Path(height): Path<u64>, |
| 138 | +) -> Result<Json<BlockDetailResponse>, (StatusCode, Json<String>)> { |
| 139 | + // In a real implementation, this would fetch the actual block from the blockchain |
| 140 | + // For now, return mock data |
| 141 | + |
| 142 | + let nodes = state.process.list_nodes(); |
| 143 | + if nodes.is_empty() { |
| 144 | + return Err(( |
| 145 | + StatusCode::SERVICE_UNAVAILABLE, |
| 146 | + Json("No nodes available.".to_string()), |
| 147 | + )); |
| 148 | + } |
| 149 | + |
| 150 | + // Handle edge case of height == 0 to prevent underflow |
| 151 | + if height == 0 { |
| 152 | + return Err(( |
| 153 | + StatusCode::BAD_REQUEST, |
| 154 | + Json("Invalid block height: cannot be 0".to_string()), |
| 155 | + )); |
| 156 | + } |
| 157 | + |
| 158 | + Ok(Json(BlockDetailResponse { |
| 159 | + height, |
| 160 | + hash: format!("0x{:016x}", height * 12345), |
| 161 | + timestamp: 1700000000 + (height * 600), |
| 162 | + proposer: format!("miner-{}", height % 3), |
| 163 | + prev_hash: format!("0x{:016x}", (height - 1) * 12345), |
| 164 | + tx_root: format!("0x{:016x}", height * 54321), |
| 165 | + state_root: format!("0x{:016x}", height * 98765), |
| 166 | + transactions: vec![], |
| 167 | + battle_count: 1, |
| 168 | + })) |
| 169 | +} |
| 170 | + |
| 171 | +/// Get battle visualization for a specific block |
| 172 | +pub async fn get_block_battles( |
| 173 | + State(_state): State<Arc<AppState>>, |
| 174 | + Path(height): Path<u64>, |
| 175 | +) -> Result<Json<Vec<BlockBattleVisualization>>, (StatusCode, Json<String>)> { |
| 176 | + tracing::info!("Fetching battle visualization for block {}", height); |
| 177 | + |
| 178 | + // In a real implementation, we would: |
| 179 | + // 1. Fetch the block from the blockchain |
| 180 | + // 2. Extract the glider reveals from the tournament data |
| 181 | + // 3. Re-simulate the battles |
| 182 | + // |
| 183 | + // For now, we'll simulate a deterministic battle based on block height |
| 184 | + // to demonstrate the visualization |
| 185 | + |
| 186 | + let battle_index = 0; |
| 187 | + |
| 188 | + // Deterministically choose glider patterns based on block height |
| 189 | + let patterns = [ |
| 190 | + GliderPattern::Standard, |
| 191 | + GliderPattern::Lightweight, |
| 192 | + GliderPattern::Middleweight, |
| 193 | + GliderPattern::Heavyweight, |
| 194 | + ]; |
| 195 | + |
| 196 | + let pattern_a = patterns[(height % 4) as usize]; |
| 197 | + let pattern_b = patterns[((height + 1) % 4) as usize]; |
| 198 | + |
| 199 | + // Create gliders |
| 200 | + let glider_a = Glider::new(pattern_a, Position::new(256, 512)); |
| 201 | + let glider_b = Glider::new(pattern_b, Position::new(768, 512)); |
| 202 | + |
| 203 | + // Create battle with fewer steps for faster rendering |
| 204 | + let steps = 500; |
| 205 | + let frame_count = 20; |
| 206 | + let downsample_size = 128; |
| 207 | + |
| 208 | + // Generate entropy seed from block height |
| 209 | + let mut entropy_seed = [0u8; 32]; |
| 210 | + let height_bytes = height.to_le_bytes(); |
| 211 | + // Fill entropy seed with deterministic but varied data based on height |
| 212 | + for i in 0..32 { |
| 213 | + entropy_seed[i] = height_bytes[i % 8].wrapping_mul((i as u8).wrapping_add(1)); |
| 214 | + } |
| 215 | + |
| 216 | + let battle = Battle::with_entropy(glider_a, glider_b, steps, entropy_seed); |
| 217 | + |
| 218 | + // Calculate sample steps |
| 219 | + let sample_interval = steps / frame_count; |
| 220 | + let mut sample_steps: Vec<usize> = (0..frame_count) |
| 221 | + .map(|i| i * sample_interval) |
| 222 | + .collect(); |
| 223 | + sample_steps.push(steps); |
| 224 | + |
| 225 | + // Run simulation in blocking task |
| 226 | + let (outcome, frames) = tokio::task::spawn_blocking(move || { |
| 227 | + let outcome = battle.simulate(); |
| 228 | + let grids = battle.grid_states(&sample_steps); |
| 229 | + |
| 230 | + let mut frames = Vec::new(); |
| 231 | + for (i, grid) in grids.iter().enumerate() { |
| 232 | + let step = sample_steps[i]; |
| 233 | + let (energy_a, energy_b) = battle.measure_regional_energy(grid); |
| 234 | + let downsampled = grid.downsample(downsample_size); |
| 235 | + |
| 236 | + frames.push(BlockBattleFrame { |
| 237 | + step, |
| 238 | + grid: downsampled, |
| 239 | + energy_a, |
| 240 | + energy_b, |
| 241 | + }); |
| 242 | + } |
| 243 | + |
| 244 | + (outcome, frames) |
| 245 | + }) |
| 246 | + .await |
| 247 | + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(format!("Task join error: {}", e))))?; |
| 248 | + |
| 249 | + let winner = match outcome { |
| 250 | + BattleOutcome::AWins => "glider_a", |
| 251 | + BattleOutcome::BWins => "glider_b", |
| 252 | + BattleOutcome::Tie => "tie", |
| 253 | + }; |
| 254 | + |
| 255 | + let pattern_name = |p: GliderPattern| match p { |
| 256 | + GliderPattern::Standard => "Standard", |
| 257 | + GliderPattern::Lightweight => "Lightweight", |
| 258 | + GliderPattern::Middleweight => "Middleweight", |
| 259 | + GliderPattern::Heavyweight => "Heavyweight", |
| 260 | + }; |
| 261 | + |
| 262 | + let visualization = BlockBattleVisualization { |
| 263 | + block_height: height, |
| 264 | + battle_index, |
| 265 | + glider_a_pattern: pattern_name(pattern_a).to_string(), |
| 266 | + glider_b_pattern: pattern_name(pattern_b).to_string(), |
| 267 | + winner: winner.to_string(), |
| 268 | + steps, |
| 269 | + frames, |
| 270 | + }; |
| 271 | + |
| 272 | + Ok(Json(vec![visualization])) |
| 273 | +} |
0 commit comments