routes.rs
1 // Copyright (c) 2025-2026 ACDC Network 2 // This file is part of the alphaos library. 3 // 4 // Alpha Chain | Delta Chain Protocol 5 // International Monetary Graphite. 6 // 7 // Derived from Aleo (https://aleo.org) and ProvableHQ (https://provable.com). 8 // They built world-class ZK infrastructure. We installed the EASY button. 9 // Their cryptography: elegant. Our modifications: bureaucracy-compatible. 10 // Original brilliance: theirs. Robert's Rules: ours. Bugs: definitely ours. 11 // 12 // Original Aleo/ProvableHQ code subject to Apache 2.0 https://www.apache.org/licenses/LICENSE-2.0 13 // All modifications and new work: CC0 1.0 Universal Public Domain Dedication. 14 // No rights reserved. No permission required. No warranty. No refunds. 15 // 16 // https://creativecommons.org/publicdomain/zero/1.0/ 17 // SPDX-License-Identifier: CC0-1.0 18 19 use super::*; 20 use alphaos_node_consensus::clp::ValidatorAddress; 21 use alphaos_node_network::PeerPoolHandling; 22 use alphaos_node_router::messages::UnconfirmedSolution; 23 use alphavm::{ 24 ledger::puzzle::Solution, 25 prelude::{block::Transaction, Address, Identifier, LimitedWriter, Plaintext, Program, ToBytes, VM}, 26 }; 27 28 use axum::{extract::rejection::JsonRejection, Json}; 29 30 use anyhow::{anyhow, Context}; 31 use indexmap::IndexMap; 32 use serde::{Deserialize, Serialize}; 33 use serde_json::json; 34 use serde_with::skip_serializing_none; 35 use std::{collections::HashMap, sync::atomic::Ordering}; 36 37 #[cfg(not(feature = "serial"))] 38 use rayon::prelude::*; 39 40 use version::VersionInfo; 41 42 /// Deserialize a CSV string into a vector of strings. 43 fn de_csv<'de, D>(de: D) -> std::result::Result<Vec<String>, D::Error> 44 where 45 D: serde::Deserializer<'de>, 46 { 47 let s = String::deserialize(de)?; 48 Ok(if s.trim().is_empty() { Vec::new() } else { s.split(',').map(|x| x.trim().to_string()).collect() }) 49 } 50 51 /// The `get_blocks` query object. 52 #[derive(Deserialize, Serialize)] 53 pub(crate) struct BlockRange { 54 /// The starting block height (inclusive). 55 start: u32, 56 /// The ending block height (exclusive). 57 end: u32, 58 } 59 60 #[derive(Deserialize, Serialize)] 61 pub(crate) struct BackupPath { 62 path: std::path::PathBuf, 63 } 64 65 /// The query object for `get_mapping_value` and `get_mapping_values`. 66 #[derive(Copy, Clone, Deserialize, Serialize)] 67 pub(crate) struct Metadata { 68 metadata: Option<bool>, 69 all: Option<bool>, 70 } 71 72 /// The query object for `transaction_broadcast`. 73 #[derive(Copy, Clone, Deserialize, Serialize)] 74 pub(crate) struct CheckTransaction { 75 check_transaction: Option<bool>, 76 } 77 78 /// The query object for `solution_broadcast`. 79 #[derive(Copy, Clone, Deserialize, Serialize)] 80 pub(crate) struct CheckSolution { 81 check_solution: Option<bool>, 82 } 83 84 /// The query object for `get_state_paths_for_commitments`. 85 #[derive(Clone, Deserialize, Serialize)] 86 pub(crate) struct Commitments { 87 #[serde(deserialize_with = "de_csv")] 88 commitments: Vec<String>, 89 } 90 91 /// The return value for a `sync_status` query. 92 #[skip_serializing_none] 93 #[derive(Copy, Clone, Serialize)] 94 struct SyncStatus<'a> { 95 /// Is this node fully synced with the network? 96 is_synced: bool, 97 /// The block height of this node. 98 ledger_height: u32, 99 /// Which way are we sync'ing (either "cdn" or "p2p") 100 sync_mode: &'a str, 101 /// The block height of the CDN (if connected to a CDN). 102 cdn_height: Option<u32>, 103 /// The greatest known block height of a peer. 104 /// None, if no peers are connected yet. 105 p2p_height: Option<u32>, 106 /// The number of outstanding p2p sync requests. 107 outstanding_block_requests: usize, 108 /// The current sync speed in blocks per second. 109 sync_speed_bps: f64, 110 } 111 112 impl<N: Network, C: ConsensusStorage<N>, R: Routing<N>> Rest<N, C, R> { 113 /// GET /<network>/version 114 pub(crate) async fn get_version() -> ErasedJson { 115 ErasedJson::pretty(VersionInfo::get()) 116 } 117 118 /// Get /<network>/consensus_version 119 pub(crate) async fn get_consensus_version(State(rest): State<Self>) -> Result<ErasedJson, RestError> { 120 Ok(ErasedJson::pretty(N::CONSENSUS_VERSION(rest.ledger.latest_height())? as u16)) 121 } 122 123 /// GET /<network>/block/height/latest 124 pub(crate) async fn get_block_height_latest(State(rest): State<Self>) -> ErasedJson { 125 ErasedJson::pretty(rest.ledger.latest_height()) 126 } 127 128 /// GET /<network>/block/hash/latest 129 pub(crate) async fn get_block_hash_latest(State(rest): State<Self>) -> ErasedJson { 130 ErasedJson::pretty(rest.ledger.latest_hash()) 131 } 132 133 /// GET /<network>/block/latest 134 pub(crate) async fn get_block_latest(State(rest): State<Self>) -> ErasedJson { 135 ErasedJson::pretty(rest.ledger.latest_block()) 136 } 137 138 /// GET /<network>/block/{height} 139 /// GET /<network>/block/{blockHash} 140 pub(crate) async fn get_block( 141 State(rest): State<Self>, 142 Path(height_or_hash): Path<String>, 143 ) -> Result<ErasedJson, RestError> { 144 // Manually parse the height or the height of the hash, axum doesn't support different types 145 // for the same path param. 146 let id_name; 147 let block = if let Ok(height) = height_or_hash.parse::<u32>() { 148 id_name = "hash"; 149 rest.ledger.try_get_block(height).with_context(|| "Failed to get block by height")? 150 } else if let Ok(hash) = height_or_hash.parse::<N::BlockHash>() { 151 id_name = "height"; 152 rest.ledger.try_get_block_by_hash(&hash).with_context(|| "Failed to get block by hash")? 153 } else { 154 return Err(RestError::bad_request(anyhow!( 155 "invalid input, it is neither a block height nor a block hash" 156 ))); 157 }; 158 159 match block { 160 Some(block) => Ok(ErasedJson::pretty(block)), 161 None => Err(RestError::not_found(anyhow!("No block with {id_name} {height_or_hash} found"))), 162 } 163 } 164 165 /// GET /<network>/blocks?start={start_height}&end={end_height} 166 pub(crate) async fn get_blocks( 167 State(rest): State<Self>, 168 Query(block_range): Query<BlockRange>, 169 ) -> Result<ErasedJson, RestError> { 170 let start_height = block_range.start; 171 let end_height = block_range.end; 172 173 const MAX_BLOCK_RANGE: u32 = 50; 174 175 // Ensure the end height is greater than the start height. 176 if start_height > end_height { 177 return Err(RestError::bad_request(anyhow!("Invalid block range"))); 178 } 179 180 // Ensure the block range is bounded. 181 if end_height - start_height > MAX_BLOCK_RANGE { 182 return Err(RestError::bad_request(anyhow!( 183 "Cannot request more than {MAX_BLOCK_RANGE} blocks per call (requested {})", 184 end_height - start_height 185 ))); 186 } 187 188 // Prepare a closure for the blocking work. 189 let get_json_blocks = move || -> Result<ErasedJson, RestError> { 190 let blocks = cfg_into_iter!(start_height..end_height) 191 .map(|height| rest.ledger.get_block(height)) 192 .collect::<Result<Vec<_>, _>>()?; 193 194 Ok(ErasedJson::pretty(blocks)) 195 }; 196 197 // Fetch the blocks from ledger and serialize to json. 198 match tokio::task::spawn_blocking(get_json_blocks).await { 199 Ok(json) => json, 200 Err(err) => { 201 let err: anyhow::Error = err.into(); 202 203 Err(RestError::internal_server_error( 204 err.context(format!("Failed to get blocks '{start_height}..{end_height}'")), 205 )) 206 } 207 } 208 } 209 210 /// GET /<network>/sync/status 211 pub(crate) async fn get_sync_status(State(rest): State<Self>) -> Result<ErasedJson, RestError> { 212 // Get the CDN height (if we are syncing from a CDN) 213 let (cdn_sync, cdn_height) = if let Some(cdn_sync) = &rest.cdn_sync { 214 let done = cdn_sync.is_done(); 215 216 // Do not show CDN height if we are already done syncing from the CDN. 217 let cdn_height = if done { None } else { Some(cdn_sync.get_cdn_height().await?) }; 218 219 // Report CDN sync until it is finished. 220 (!done, cdn_height) 221 } else { 222 (false, None) 223 }; 224 225 // Generate a string representing the current sync mode. 226 let sync_mode = if cdn_sync { "cdn" } else { "p2p" }; 227 228 Ok(ErasedJson::pretty(SyncStatus { 229 sync_mode, 230 cdn_height, 231 is_synced: !cdn_sync && rest.routing.is_block_synced(), 232 ledger_height: rest.ledger.latest_height(), 233 p2p_height: rest.block_sync.greatest_peer_block_height(), 234 outstanding_block_requests: rest.block_sync.num_outstanding_block_requests(), 235 sync_speed_bps: rest.block_sync.get_sync_speed(), 236 })) 237 } 238 239 /// GET /<network>/sync/peers 240 pub(crate) async fn get_sync_peers(State(rest): State<Self>) -> Result<ErasedJson, RestError> { 241 let peers: HashMap<String, u32> = 242 rest.block_sync.get_peer_heights().into_iter().map(|(addr, height)| (addr.to_string(), height)).collect(); 243 Ok(ErasedJson::pretty(peers)) 244 } 245 246 /// GET /<network>/sync/requests 247 pub(crate) async fn get_sync_requests_summary(State(rest): State<Self>) -> Result<ErasedJson, RestError> { 248 let summary = rest.block_sync.get_block_requests_summary(); 249 Ok(ErasedJson::pretty(summary)) 250 } 251 252 /// GET /<network>/sync/requests/list 253 pub(crate) async fn get_sync_requests_list(State(rest): State<Self>) -> Result<ErasedJson, RestError> { 254 let requests = rest.block_sync.get_block_requests_info(); 255 Ok(ErasedJson::pretty(requests)) 256 } 257 258 /// GET /<network>/height/{blockHash} 259 pub(crate) async fn get_height( 260 State(rest): State<Self>, 261 Path(hash): Path<N::BlockHash>, 262 ) -> Result<ErasedJson, RestError> { 263 Ok(ErasedJson::pretty(rest.ledger.get_height(&hash)?)) 264 } 265 266 /// GET /<network>/block/{height}/header 267 pub(crate) async fn get_block_header( 268 State(rest): State<Self>, 269 Path(height): Path<u32>, 270 ) -> Result<ErasedJson, RestError> { 271 Ok(ErasedJson::pretty(rest.ledger.get_header(height)?)) 272 } 273 274 /// GET /<network>/block/{height}/transactions 275 pub(crate) async fn get_block_transactions( 276 State(rest): State<Self>, 277 Path(height): Path<u32>, 278 ) -> Result<ErasedJson, RestError> { 279 Ok(ErasedJson::pretty(rest.ledger.get_transactions(height)?)) 280 } 281 282 /// GET /<network>/genesis 283 /// Returns the full genesis block (block at height 0). 284 /// This endpoint is used by late-joining validators to fetch genesis automatically. 285 pub(crate) async fn get_genesis(State(rest): State<Self>) -> Result<ErasedJson, RestError> { 286 let genesis = rest 287 .ledger 288 .try_get_block(0) 289 .with_context(|| "Failed to get genesis block")? 290 .ok_or_else(|| RestError::not_found(anyhow!("Genesis block not found at height 0")))?; 291 Ok(ErasedJson::pretty(genesis)) 292 } 293 294 /// GET /<network>/genesis/hash 295 /// Returns only the genesis block hash (lightweight check). 296 /// This endpoint is used for BFT consensus verification of genesis across governors. 297 pub(crate) async fn get_genesis_hash(State(rest): State<Self>) -> Result<ErasedJson, RestError> { 298 let genesis = rest 299 .ledger 300 .try_get_block(0) 301 .with_context(|| "Failed to get genesis block")? 302 .ok_or_else(|| RestError::not_found(anyhow!("Genesis block not found at height 0")))?; 303 Ok(ErasedJson::pretty(genesis.hash())) 304 } 305 306 /// GET /<network>/governors 307 /// Returns the list of current governors for genesis verification. 308 /// This endpoint is used by late-joining validators to discover the governor set for BFT consensus. 309 pub(crate) async fn get_governors(State(rest): State<Self>) -> Result<ErasedJson, RestError> { 310 // Get the latest committee 311 let committee = rest.ledger.latest_committee()?; 312 313 // Extract governor information 314 let governors: Vec<serde_json::Value> = committee 315 .members() 316 .iter() 317 .map(|(address, (stake, is_open, _))| { 318 json!({ 319 "address": address.to_string(), 320 "stake": stake, 321 "is_open": is_open, 322 }) 323 }) 324 .collect(); 325 326 Ok(ErasedJson::pretty(json!({ 327 "round": committee.starting_round(), 328 "total_stake": committee.total_stake(), 329 "governors": governors, 330 }))) 331 } 332 333 /// GET /<network>/transaction/{transactionID} 334 pub(crate) async fn get_transaction( 335 State(rest): State<Self>, 336 Path(tx_id): Path<N::TransactionID>, 337 ) -> Result<ErasedJson, RestError> { 338 // Ledger returns a generic anyhow::Error, so checking the message is the only way to parse it. 339 Ok(ErasedJson::pretty(rest.ledger.get_transaction(tx_id).map_err(|err| { 340 if err.to_string().contains("Missing") { 341 RestError::not_found(err) 342 } else { 343 RestError::from(err) 344 } 345 })?)) 346 } 347 348 /// GET /<network>/transaction/confirmed/{transactionID} 349 pub(crate) async fn get_confirmed_transaction( 350 State(rest): State<Self>, 351 Path(tx_id): Path<N::TransactionID>, 352 ) -> Result<ErasedJson, RestError> { 353 // Ledger returns a generic anyhow::Error, so checking the message is the only way to parse it. 354 Ok(ErasedJson::pretty(rest.ledger.get_confirmed_transaction(tx_id).map_err(|err| { 355 if err.to_string().contains("Missing") { 356 RestError::not_found(err) 357 } else { 358 RestError::from(err) 359 } 360 })?)) 361 } 362 363 /// GET /<network>/transaction/unconfirmed/{transactionID} 364 pub(crate) async fn get_unconfirmed_transaction( 365 State(rest): State<Self>, 366 Path(tx_id): Path<N::TransactionID>, 367 ) -> Result<ErasedJson, RestError> { 368 // Ledger returns a generic anyhow::Error, so checking the message is the only way to parse it. 369 Ok(ErasedJson::pretty(rest.ledger.get_unconfirmed_transaction(&tx_id).map_err(|err| { 370 if err.to_string().contains("Missing") { 371 RestError::not_found(err) 372 } else { 373 RestError::from(err) 374 } 375 })?)) 376 } 377 378 /// GET /<network>/memoryPool/transmissions 379 pub(crate) async fn get_memory_pool_transmissions(State(rest): State<Self>) -> Result<ErasedJson, RestError> { 380 match rest.consensus { 381 Some(consensus) => { 382 Ok(ErasedJson::pretty(consensus.unconfirmed_transmissions().collect::<IndexMap<_, _>>())) 383 } 384 None => Err(RestError::service_unavailable(anyhow!("Route isn't available for this node type"))), 385 } 386 } 387 388 /// GET /<network>/memoryPool/solutions 389 pub(crate) async fn get_memory_pool_solutions(State(rest): State<Self>) -> Result<ErasedJson, RestError> { 390 match rest.consensus { 391 Some(consensus) => Ok(ErasedJson::pretty(consensus.unconfirmed_solutions().collect::<IndexMap<_, _>>())), 392 None => Err(RestError::service_unavailable(anyhow!("Route isn't available for this node type"))), 393 } 394 } 395 396 /// GET /<network>/memoryPool/transactions 397 pub(crate) async fn get_memory_pool_transactions(State(rest): State<Self>) -> Result<ErasedJson, RestError> { 398 match rest.consensus { 399 Some(consensus) => Ok(ErasedJson::pretty(consensus.unconfirmed_transactions().collect::<IndexMap<_, _>>())), 400 None => Err(RestError::service_unavailable(anyhow!("Route isn't available for this node type"))), 401 } 402 } 403 404 /// GET /<network>/program/{programID} 405 /// GET /<network>/program/{programID}?metadata={true} 406 pub(crate) async fn get_program( 407 State(rest): State<Self>, 408 Path(id): Path<ProgramID<N>>, 409 metadata: Query<Metadata>, 410 ) -> Result<ErasedJson, RestError> { 411 // Get the program from the ledger. 412 let program = rest.ledger.get_program(id).with_context(|| format!("Failed to find program `{id}`"))?; 413 // Check if metadata is requested and return the program with metadata if so. 414 if metadata.metadata.unwrap_or(false) { 415 // Get the edition of the program. 416 let edition = rest.ledger.get_latest_edition_for_program(&id)?; 417 return rest.return_program_with_metadata(program, edition); 418 } 419 // Return the program without metadata. 420 Ok(ErasedJson::pretty(program)) 421 } 422 423 /// GET /<network>/program/{programID}/{edition} 424 /// GET /<network>/program/{programID}/{edition}?metadata={true} 425 pub(crate) async fn get_program_for_edition( 426 State(rest): State<Self>, 427 Path((id, edition)): Path<(ProgramID<N>, u16)>, 428 metadata: Query<Metadata>, 429 ) -> Result<ErasedJson, RestError> { 430 // Get the program from the ledger. 431 match rest 432 .ledger 433 .try_get_program_for_edition(&id, edition) 434 .with_context(|| format!("Failed get program `{id}` for edition {edition}"))? 435 { 436 Some(program) => { 437 // Check if metadata is requested and return the program with metadata if so. 438 if metadata.metadata.unwrap_or(false) { 439 rest.return_program_with_metadata(program, edition) 440 } else { 441 Ok(ErasedJson::pretty(program)) 442 } 443 } 444 None => Err(RestError::not_found(anyhow!("No program `{id}` exists for edition {edition}"))), 445 } 446 } 447 448 /// A helper function to return the program and its metadata. 449 /// This function is used in the `get_program` and `get_program_for_edition` functions. 450 fn return_program_with_metadata(&self, program: Program<N>, edition: u16) -> Result<ErasedJson, RestError> { 451 let id = program.id(); 452 // Get the transaction ID associated with the program and edition. 453 let tx_id = self.ledger.find_transaction_id_from_program_id_and_edition(id, edition)?; 454 // Get the optional program owner associated with the program. 455 // Note: The owner is only available after `ConsensusVersion::V9`. 456 let program_owner = match &tx_id { 457 Some(tid) => self 458 .ledger 459 .vm() 460 .block_store() 461 .transaction_store() 462 .deployment_store() 463 .get_deployment(tid)? 464 .and_then(|deployment| deployment.program_owner()), 465 None => None, 466 }; 467 Ok(ErasedJson::pretty(json!({ 468 "program": program, 469 "edition": edition, 470 "transaction_id": tx_id, 471 "program_owner": program_owner, 472 }))) 473 } 474 475 /// GET /<network>/program/{programID}/latest_edition 476 pub(crate) async fn get_latest_program_edition( 477 State(rest): State<Self>, 478 Path(id): Path<ProgramID<N>>, 479 ) -> Result<ErasedJson, RestError> { 480 Ok(ErasedJson::pretty(rest.ledger.get_latest_edition_for_program(&id)?)) 481 } 482 483 /// GET /<network>/program/{programID}/mappings 484 pub(crate) async fn get_mapping_names( 485 State(rest): State<Self>, 486 Path(id): Path<ProgramID<N>>, 487 ) -> Result<ErasedJson, RestError> { 488 Ok(ErasedJson::pretty(rest.ledger.vm().finalize_store().get_mapping_names_confirmed(&id)?)) 489 } 490 491 /// GET /<network>/program/{programID}/mapping/{mappingName}/{mappingKey} 492 /// GET /<network>/program/{programID}/mapping/{mappingName}/{mappingKey}?metadata={true} 493 pub(crate) async fn get_mapping_value( 494 State(rest): State<Self>, 495 Path((id, name, key)): Path<(ProgramID<N>, Identifier<N>, Plaintext<N>)>, 496 metadata: Query<Metadata>, 497 ) -> Result<ErasedJson, RestError> { 498 // Retrieve the mapping value. 499 let mapping_value = rest.ledger.vm().finalize_store().get_value_confirmed(id, name, &key)?; 500 501 // Check if metadata is requested and return the value with metadata if so. 502 if metadata.metadata.unwrap_or(false) { 503 return Ok(ErasedJson::pretty(json!({ 504 "data": mapping_value, 505 "height": rest.ledger.latest_height(), 506 }))); 507 } 508 509 // Return the value without metadata. 510 Ok(ErasedJson::pretty(mapping_value)) 511 } 512 513 /// GET /<network>/program/{programID}/mapping/{mappingName}?all={true}&metadata={true} 514 pub(crate) async fn get_mapping_values( 515 State(rest): State<Self>, 516 Path((id, name)): Path<(ProgramID<N>, Identifier<N>)>, 517 metadata: Query<Metadata>, 518 ) -> Result<ErasedJson, RestError> { 519 // Return an error if the `all` query parameter is not set to `true`. 520 if metadata.all != Some(true) { 521 return Err(RestError::bad_request(anyhow!( 522 "Invalid query parameter. At this time, 'all=true' must be included" 523 ))); 524 } 525 526 // Retrieve the latest height. 527 let height = rest.ledger.latest_height(); 528 529 // Retrieve all the mapping values from the mapping. 530 match tokio::task::spawn_blocking(move || rest.ledger.vm().finalize_store().get_mapping_confirmed(id, name)) 531 .await 532 { 533 Ok(Ok(mapping_values)) => { 534 // Check if metadata is requested and return the mapping with metadata if so. 535 if metadata.metadata.unwrap_or(false) { 536 return Ok(ErasedJson::pretty(json!({ 537 "data": mapping_values, 538 "height": height, 539 }))); 540 } 541 542 // Return the full mapping without metadata. 543 Ok(ErasedJson::pretty(mapping_values)) 544 } 545 Ok(Err(err)) => Err(RestError::internal_server_error(err.context("Unable to read mapping"))), 546 Err(err) => Err(RestError::internal_server_error(anyhow!("Tokio error: {err}"))), 547 } 548 } 549 550 /// GET /<network>/statePath/{commitment} 551 pub(crate) async fn get_state_path_for_commitment( 552 State(rest): State<Self>, 553 Path(commitment): Path<Field<N>>, 554 ) -> Result<ErasedJson, RestError> { 555 Ok(ErasedJson::pretty(rest.ledger.get_state_path_for_commitment(&commitment)?)) 556 } 557 558 /// GET /<network>/statePaths?commitments=cm1,cm2,... 559 pub(crate) async fn get_state_paths_for_commitments( 560 State(rest): State<Self>, 561 Query(commitments): Query<Commitments>, 562 ) -> Result<ErasedJson, RestError> { 563 // Retrieve the number of commitments. 564 let num_commitments = commitments.commitments.len(); 565 // Return an error if no commitments are provided. 566 if num_commitments == 0 { 567 return Err(RestError::unprocessable_entity(anyhow!("No commitments provided"))); 568 } 569 // Return an error if the number of commitments exceeds the maximum allowed. 570 if num_commitments > N::MAX_INPUTS { 571 return Err(RestError::unprocessable_entity(anyhow!( 572 "Too many commitments provided (max: {}, got: {})", 573 N::MAX_INPUTS, 574 num_commitments 575 ))); 576 } 577 578 // Deserialize the commitments from the query. 579 let commitments = commitments 580 .commitments 581 .iter() 582 .map(|s| { 583 s.parse::<Field<N>>() 584 .map_err(|err| RestError::unprocessable_entity(err.context(format!("Invalid commitment: {s}")))) 585 }) 586 .collect::<Result<Vec<_>, _>>()?; 587 588 Ok(ErasedJson::pretty(rest.ledger.get_state_paths_for_commitments(&commitments)?)) 589 } 590 591 /// GET /<network>/stateRoot/latest 592 pub(crate) async fn get_state_root_latest(State(rest): State<Self>) -> ErasedJson { 593 ErasedJson::pretty(rest.ledger.latest_state_root()) 594 } 595 596 /// GET /<network>/stateRoot/{height} 597 pub(crate) async fn get_state_root( 598 State(rest): State<Self>, 599 Path(height): Path<u32>, 600 ) -> Result<ErasedJson, RestError> { 601 Ok(ErasedJson::pretty(rest.ledger.get_state_root(height)?)) 602 } 603 604 /// GET /<network>/committee/latest 605 pub(crate) async fn get_committee_latest(State(rest): State<Self>) -> Result<ErasedJson, RestError> { 606 Ok(ErasedJson::pretty(rest.ledger.latest_committee()?)) 607 } 608 609 /// GET /<network>/committee/{height} 610 pub(crate) async fn get_committee( 611 State(rest): State<Self>, 612 Path(height): Path<u32>, 613 ) -> Result<ErasedJson, RestError> { 614 Ok(ErasedJson::pretty(rest.ledger.get_committee(height)?)) 615 } 616 617 /// GET /<network>/delegators/{validator} 618 pub(crate) async fn get_delegators_for_validator( 619 State(rest): State<Self>, 620 Path(validator): Path<Address<N>>, 621 ) -> Result<ErasedJson, RestError> { 622 // Do not process the request if the node is too far behind to avoid sending outdated data. 623 if !rest.routing.is_within_sync_leniency() { 624 return Err(RestError::service_unavailable(anyhow!("Unable to request delegators (node is syncing)"))); 625 } 626 627 // Return the delegators for the given validator. 628 match tokio::task::spawn_blocking(move || rest.ledger.get_delegators_for_validator(&validator)).await { 629 Ok(Ok(delegators)) => Ok(ErasedJson::pretty(delegators)), 630 Ok(Err(err)) => Err(RestError::internal_server_error(err.context("Unable to request delegators"))), 631 Err(err) => Err(RestError::internal_server_error(anyhow!(err).context("Tokio error"))), 632 } 633 } 634 635 /// GET /<network>/peers/count 636 pub(crate) async fn get_peers_count(State(rest): State<Self>) -> ErasedJson { 637 ErasedJson::pretty(rest.routing.router().number_of_connected_peers()) 638 } 639 640 /// GET /<network>/peers/all 641 pub(crate) async fn get_peers_all(State(rest): State<Self>) -> ErasedJson { 642 ErasedJson::pretty(rest.routing.router().connected_peers()) 643 } 644 645 /// GET /<network>/peers/all/metrics 646 pub(crate) async fn get_peers_all_metrics(State(rest): State<Self>) -> ErasedJson { 647 ErasedJson::pretty(rest.routing.router().connected_metrics()) 648 } 649 650 /// GET /<network>/node/address 651 pub(crate) async fn get_node_address(State(rest): State<Self>) -> ErasedJson { 652 ErasedJson::pretty(rest.routing.router().address()) 653 } 654 655 /// GET /<network>/find/blockHash/{transactionID} 656 pub(crate) async fn find_block_hash( 657 State(rest): State<Self>, 658 Path(tx_id): Path<N::TransactionID>, 659 ) -> Result<ErasedJson, RestError> { 660 Ok(ErasedJson::pretty(rest.ledger.find_block_hash(&tx_id)?)) 661 } 662 663 /// GET /<network>/find/blockHeight/{stateRoot} 664 pub(crate) async fn find_block_height_from_state_root( 665 State(rest): State<Self>, 666 Path(state_root): Path<N::StateRoot>, 667 ) -> Result<ErasedJson, RestError> { 668 Ok(ErasedJson::pretty(rest.ledger.find_block_height_from_state_root(state_root)?)) 669 } 670 671 /// GET /<network>/find/transactionID/deployment/{programID} 672 pub(crate) async fn find_latest_transaction_id_from_program_id( 673 State(rest): State<Self>, 674 Path(program_id): Path<ProgramID<N>>, 675 ) -> Result<ErasedJson, RestError> { 676 Ok(ErasedJson::pretty(rest.ledger.find_latest_transaction_id_from_program_id(&program_id)?)) 677 } 678 679 /// GET /<network>/find/transactionID/deployment/{programID}/{edition} 680 pub(crate) async fn find_transaction_id_from_program_id_and_edition( 681 State(rest): State<Self>, 682 Path((program_id, edition)): Path<(ProgramID<N>, u16)>, 683 ) -> Result<ErasedJson, RestError> { 684 Ok(ErasedJson::pretty(rest.ledger.find_transaction_id_from_program_id_and_edition(&program_id, edition)?)) 685 } 686 687 /// GET /<network>/find/transactionID/{transitionID} 688 pub(crate) async fn find_transaction_id_from_transition_id( 689 State(rest): State<Self>, 690 Path(transition_id): Path<N::TransitionID>, 691 ) -> Result<ErasedJson, RestError> { 692 Ok(ErasedJson::pretty(rest.ledger.find_transaction_id_from_transition_id(&transition_id)?)) 693 } 694 695 /// GET /<network>/find/transitionID/{inputOrOutputID} 696 pub(crate) async fn find_transition_id( 697 State(rest): State<Self>, 698 Path(input_or_output_id): Path<Field<N>>, 699 ) -> Result<ErasedJson, RestError> { 700 Ok(ErasedJson::pretty(rest.ledger.find_transition_id(&input_or_output_id)?)) 701 } 702 703 /// POST /<network>/transaction/broadcast 704 /// POST /<network>/transaction/broadcast?check_transaction={true} 705 /// 706 /// Transaction Broadcast Flow 707 /// 708 /// /transaction/broadcast 709 /// | 710 /// +----+---------------------------+ 711 /// | | 712 /// v v 713 /// Without Query Params With Query Param 714 /// check_transaction=true 715 /// | | 716 /// +---------+ +---------+ 717 /// | | | | 718 /// v v v v 719 /// Synced Not Synced Synced Not Synced 720 /// | | | | 721 /// v v v v 722 /// 200 200 check_transaction check_transaction 723 /// +---------+ +---------+ 724 /// | | | | 725 /// v v v v 726 /// 200 422 203 503 727 pub(crate) async fn transaction_broadcast( 728 State(rest): State<Self>, 729 check_transaction: Query<CheckTransaction>, 730 json_result: Result<Json<Transaction<N>>, JsonRejection>, 731 ) -> Result<impl axum::response::IntoResponse, RestError> { 732 let Json(tx) = match json_result { 733 Ok(json) => json, 734 Err(JsonRejection::JsonDataError(err)) => { 735 // For JsonDataError, return 422 to let transaction validation handle it 736 return Err(RestError::unprocessable_entity(anyhow!("Invalid transaction data: {err}"))); 737 } 738 Err(other_rejection) => return Err(other_rejection.into()), 739 }; 740 741 // If the transaction exceeds the transaction size limit, return an error. 742 // The buffer is initially roughly sized to hold a `transfer_public`, 743 // most transactions will be smaller and this reduces unnecessary allocations. 744 // TODO: Should this be a blocking task? 745 let buffer = Vec::with_capacity(3000); 746 if tx.write_le(LimitedWriter::new(buffer, N::MAX_TRANSACTION_SIZE)).is_err() { 747 return Err(RestError::bad_request(anyhow!("Transaction size exceeds the byte limit"))); 748 } 749 750 // Prepare the unconfirmed transaction message. 751 let tx_id = tx.id(); 752 let message = Message::UnconfirmedTransaction(UnconfirmedTransaction { 753 transaction_id: tx_id, 754 transaction: Data::Object(tx.clone()), 755 }); 756 757 // Check if the node is within sync leniency. 758 let is_within_sync_leniency = rest.routing.is_within_sync_leniency(); 759 760 // Determine if we need to check the transaction. 761 let check_transaction = check_transaction.check_transaction.unwrap_or(false); 762 763 if check_transaction { 764 // Select counter and limit based on transaction type. 765 let (counter, limit, err_msg) = if tx.is_execute() { 766 ( 767 &rest.num_verifying_executions, 768 VM::<N, C>::MAX_PARALLEL_EXECUTE_VERIFICATIONS, 769 "Too many execution verifications in progress", 770 ) 771 } else { 772 ( 773 &rest.num_verifying_deploys, 774 VM::<N, C>::MAX_PARALLEL_DEPLOY_VERIFICATIONS, 775 "Too many deploy verifications in progress", 776 ) 777 }; 778 779 // Try to acquire a slot. 780 if counter 781 .fetch_update( 782 Ordering::Relaxed, 783 Ordering::Relaxed, 784 |val| { 785 if val < limit { 786 Some(val + 1) 787 } else { 788 None 789 } 790 }, 791 ) 792 .is_err() 793 { 794 return Err(RestError::too_many_requests(anyhow!("{err_msg}"))); 795 } 796 797 // Perform the check. 798 let res = rest.ledger.check_transaction_basic(&tx, None, &mut rand::thread_rng()).map_err(|err| { 799 match is_within_sync_leniency { 800 // The transaction failed to verify. 801 true => RestError::unprocessable_entity(err.context("Invalid transaction")), 802 // The node is out of sync and may not be able to properly validate the transaction. 803 false => { 804 RestError::service_unavailable(err.context("Unable to validate transaction (node is syncing)")) 805 } 806 } 807 }); 808 // Release the slot. 809 counter.fetch_sub(1, Ordering::Relaxed); 810 // Propagate error if any. 811 res?; 812 } 813 814 // If the consensus module is enabled, add the unconfirmed transaction to the memory pool. 815 if let Some(consensus) = rest.consensus { 816 // Add the unconfirmed transaction to the memory pool. 817 consensus.add_unconfirmed_transaction(tx.clone()).await?; 818 } 819 820 // Broadcast the transaction. 821 rest.routing.propagate(message, &[]); 822 823 // Determine if the node is synced and if the transaction was checked. 824 match !is_within_sync_leniency && check_transaction { 825 // If the node is not synced and we validated the transaction, return a 203. 826 true => Ok((StatusCode::NON_AUTHORITATIVE_INFORMATION, ErasedJson::pretty(tx_id))), 827 // Otherwise, return a 200. 828 false => Ok((StatusCode::OK, ErasedJson::pretty(tx_id))), 829 } 830 } 831 832 /// POST /<network>/solution/broadcast 833 /// POST /<network>/solution/broadcast?check_solution={true} 834 /// 835 /// Solution Broadcast Flow 836 /// 837 /// /solution/broadcast 838 /// | 839 /// +----+---------------------------+ 840 /// | | 841 /// v v 842 /// Without Query Params With Query Param 843 /// check_solution=true 844 /// | | 845 /// +---------+ +---------+ 846 /// | | | | 847 /// v v v v 848 /// Synced Not Synced Synced Not Synced 849 /// | | | | 850 /// v v v v 851 /// 200 200 check_solution check_solution 852 /// +---------+ +---------+ 853 /// | | | | 854 /// v v v v 855 /// 200 422 203 503 856 pub(crate) async fn solution_broadcast( 857 State(rest): State<Self>, 858 check_solution: Query<CheckSolution>, 859 Json(solution): Json<Solution<N>>, 860 ) -> Result<impl axum::response::IntoResponse, RestError> { 861 // Check if the node is within sync leniency. 862 let is_within_sync_leniency = rest.routing.is_within_sync_leniency(); 863 // Determine if we need to check the solution. 864 let check_solution = check_solution.check_solution.unwrap_or(false); 865 866 if check_solution { 867 // Select counter and limit. 868 let (counter, limit, err_msg) = 869 (&rest.num_verifying_solutions, N::MAX_SOLUTIONS, "Too many solution verifications in progress"); 870 871 // Try to acquire a slot. 872 if counter 873 .fetch_update( 874 Ordering::Relaxed, 875 Ordering::Relaxed, 876 |val| { 877 if val < limit { 878 Some(val + 1) 879 } else { 880 None 881 } 882 }, 883 ) 884 .is_err() 885 { 886 return Err(RestError::too_many_requests(anyhow!("{err_msg}"))); 887 } 888 889 // Compute the current epoch hash. 890 let epoch_hash = rest.ledger.latest_epoch_hash()?; 891 // Retrieve the current proof target. 892 let proof_target = rest.ledger.latest_proof_target(); 893 // Ensure that the solution is valid for the given epoch. 894 let puzzle = rest.ledger.puzzle().clone(); 895 // Check if the prover has reached their solution limit. 896 // While alphavm will ultimately abort any excess solutions for safety, performing this check 897 // here prevents the to-be aborted solutions from propagating through the network. 898 let prover_address = solution.address(); 899 if rest.ledger.is_solution_limit_reached(&prover_address, 0) { 900 return Err(RestError::unprocessable_entity(anyhow!( 901 "Invalid solution '{}' - Prover '{prover_address}' has reached their solution limit for the current epoch", 902 fmt_id(solution.id()) 903 ))); 904 } 905 // Verify the solution in a blocking task. 906 let res: Result<(), anyhow::Error> = 907 match tokio::task::spawn_blocking(move || puzzle.check_solution(&solution, epoch_hash, proof_target)) 908 .await 909 { 910 Ok(Ok(())) => Ok(()), 911 Ok(Err(err)) => { 912 return match is_within_sync_leniency { 913 // The solution failed to verify. 914 true => Err(RestError::unprocessable_entity( 915 err.context(format!("Invalid solution '{}'", fmt_id(solution.id()))), 916 )), 917 // The node is out of sync and may not be able to properly validate the solution. 918 false => Err(RestError::service_unavailable(anyhow!( 919 "Unable to validate solution '{}' (node is syncing)", 920 fmt_id(solution.id()) 921 ))), 922 }; 923 } 924 Err(err) => { 925 return Err(RestError::internal_server_error(anyhow!("Tokio error: {err}"))); 926 } 927 }; 928 // Release the slot. 929 counter.fetch_sub(1, Ordering::Relaxed); 930 // Propagate error if any. 931 res?; 932 } 933 934 // If the consensus module is enabled, add the unconfirmed solution to the memory pool. 935 if let Some(consensus) = rest.consensus { 936 // Add the unconfirmed solution to the memory pool. 937 let _ = consensus.add_unconfirmed_solution(solution).await; 938 } 939 940 let solution_id = solution.id(); 941 // Prepare the unconfirmed solution message. 942 let message = 943 Message::UnconfirmedSolution(UnconfirmedSolution { solution_id, solution: Data::Object(solution) }); 944 945 // Broadcast the unconfirmed solution message. 946 rest.routing.propagate(message, &[]); 947 948 // Determine if the node is synced and if the solution was checked. 949 match !is_within_sync_leniency && check_solution { 950 // If the node is not synced and we validated the solution, return a 203. 951 true => Ok((StatusCode::NON_AUTHORITATIVE_INFORMATION, ErasedJson::pretty(solution_id))), 952 // Otherwise, return a 200. 953 false => Ok((StatusCode::OK, ErasedJson::pretty(solution_id))), 954 } 955 } 956 957 /// POST /{network}/db_backup?path=new_fs_path 958 pub(crate) async fn db_backup( 959 State(rest): State<Self>, 960 backup_path: Query<BackupPath>, 961 ) -> Result<ErasedJson, RestError> { 962 rest.ledger.backup_database(&backup_path.path)?; 963 964 Ok(ErasedJson::pretty(())) 965 } 966 967 /// GET /{network}/block/{blockHeight}/history/{mapping} 968 #[cfg(feature = "history")] 969 pub(crate) async fn get_history( 970 State(rest): State<Self>, 971 Path((height, mapping)): Path<(u32, alphavm::synthesizer::MappingName)>, 972 ) -> Result<impl axum::response::IntoResponse, RestError> { 973 // Retrieve the history for the given block height and variant. 974 let history = alphavm::synthesizer::History::new(N::ID, rest.ledger.vm().finalize_store().storage_mode()); 975 let result = history.load_mapping(height, mapping).map_err(|err| { 976 RestError::not_found(err.context(format!("Could not load mapping '{mapping}' from block '{height}'"))) 977 })?; 978 979 Ok((StatusCode::OK, [(CONTENT_TYPE, "application/json")], result)) 980 } 981 982 /// GET /{network}/validators/participation 983 /// GET /{network}/validators/participation?metadata={true} 984 #[cfg(feature = "telemetry")] 985 pub(crate) async fn get_validator_participation_scores( 986 State(rest): State<Self>, 987 metadata: Query<Metadata>, 988 ) -> Result<impl axum::response::IntoResponse, RestError> { 989 match rest.consensus { 990 Some(consensus) => { 991 // Retrieve the latest committee. 992 let latest_committee = rest.ledger.latest_committee()?; 993 // Retrieve the latest participation scores. 994 let participation_scores = consensus 995 .bft() 996 .primary() 997 .gateway() 998 .validator_telemetry() 999 .get_participation_scores(&latest_committee); 1000 1001 // Check if metadata is requested and return the participation scores with metadata if so. 1002 if metadata.metadata.unwrap_or(false) { 1003 return Ok(ErasedJson::pretty(json!({ 1004 "participation_scores": participation_scores, 1005 "height": rest.ledger.latest_height(), 1006 }))); 1007 } 1008 1009 Ok(ErasedJson::pretty(participation_scores)) 1010 } 1011 None => Err(RestError::service_unavailable(anyhow!("Route isn't available for this node type"))), 1012 } 1013 } 1014 } 1015 1016 // === CLP (Continuous Liveness Proof) Endpoints === 1017 1018 /// Response type for CLP status. 1019 #[derive(Serialize)] 1020 struct ClpStatusResponse { 1021 /// Whether CLP is active. 1022 is_active: bool, 1023 /// Current epoch number. 1024 current_epoch: u64, 1025 /// Total challenges issued this epoch. 1026 challenges_issued: u32, 1027 /// Number of validators being tracked. 1028 validator_count: usize, 1029 /// Per-validator status summary. 1030 validators: Vec<ClpValidatorSummary>, 1031 } 1032 1033 /// Summary of a validator's CLP status. 1034 #[derive(Serialize)] 1035 struct ClpValidatorSummary { 1036 /// Validator address (hex encoded). 1037 address: String, 1038 /// Total challenges received. 1039 total_challenges: u32, 1040 /// Challenges responded to. 1041 responses: u32, 1042 /// Response rate as percentage. 1043 response_rate_percent: f64, 1044 /// Consecutive failing epochs. 1045 consecutive_failures: u32, 1046 } 1047 1048 /// Response type for a single validator's CLP status. 1049 #[derive(Serialize)] 1050 struct ClpValidatorDetailResponse { 1051 /// Validator address. 1052 address: String, 1053 /// Total challenges received. 1054 total_challenges: u32, 1055 /// Challenges responded to. 1056 responses: u32, 1057 /// Challenges missed (after grace period). 1058 missed: u32, 1059 /// Response rate as percentage. 1060 response_rate_percent: f64, 1061 /// Consecutive failing epochs. 1062 consecutive_failures: u32, 1063 /// Current CLP epoch. 1064 current_epoch: u64, 1065 } 1066 1067 impl<N: Network, C: ConsensusStorage<N>, R: Routing<N>> Rest<N, C, R> { 1068 /// GET /<network>/clp/status 1069 /// 1070 /// Returns the current CLP system status including all validator statuses. 1071 pub(crate) async fn get_clp_status(State(rest): State<Self>) -> Result<ErasedJson, RestError> { 1072 match rest.consensus { 1073 Some(consensus) => { 1074 // Check if CLP manager is configured 1075 match consensus.clp_manager() { 1076 Some(clp_manager) => { 1077 let is_active = clp_manager.is_active(); 1078 let current_epoch = clp_manager.current_epoch(); 1079 let challenges_issued = clp_manager.challenges_issued(); 1080 1081 // Build validator summaries (we need to access the aggregator for this) 1082 // Since we can't directly access all_statuses from ClpManager, 1083 // we'll return basic info for now 1084 let response = ClpStatusResponse { 1085 is_active, 1086 current_epoch, 1087 challenges_issued, 1088 validator_count: 0, // Will be populated when aggregator exposes this 1089 validators: vec![], // Will be populated when aggregator exposes this 1090 }; 1091 1092 Ok(ErasedJson::pretty(response)) 1093 } 1094 None => { 1095 // CLP not enabled - return inactive status 1096 Ok(ErasedJson::pretty(ClpStatusResponse { 1097 is_active: false, 1098 current_epoch: 0, 1099 challenges_issued: 0, 1100 validator_count: 0, 1101 validators: vec![], 1102 })) 1103 } 1104 } 1105 } 1106 None => Err(RestError::service_unavailable(anyhow!("CLP status is only available on validator nodes"))), 1107 } 1108 } 1109 1110 /// GET /<network>/clp/validator/{address} 1111 /// 1112 /// Returns CLP status for a specific validator. 1113 pub(crate) async fn get_clp_validator_status( 1114 State(rest): State<Self>, 1115 Path(address): Path<String>, 1116 ) -> Result<ErasedJson, RestError> { 1117 match rest.consensus { 1118 Some(consensus) => { 1119 // Parse the validator address from hex 1120 let addr_bytes: [u8; 32] = hex_decode(&address) 1121 .map_err(|_| RestError::bad_request(anyhow!("Invalid validator address format")))? 1122 .try_into() 1123 .map_err(|_| RestError::bad_request(anyhow!("Validator address must be 32 bytes")))?; 1124 let validator_addr = ValidatorAddress(addr_bytes); 1125 1126 match consensus.clp_manager() { 1127 Some(clp_manager) => { 1128 let current_epoch = clp_manager.current_epoch(); 1129 1130 // Try to get the response rate for this validator 1131 match clp_manager.validator_response_rate(&validator_addr) { 1132 Some(rate) => { 1133 let response = ClpValidatorDetailResponse { 1134 address, 1135 total_challenges: clp_manager.challenges_issued(), 1136 responses: (rate * clp_manager.challenges_issued() as f64) as u32, 1137 missed: 0, // Not directly accessible from manager 1138 response_rate_percent: rate * 100.0, 1139 consecutive_failures: 0, // Not directly accessible from manager 1140 current_epoch, 1141 }; 1142 Ok(ErasedJson::pretty(response)) 1143 } 1144 None => Err(RestError::not_found(anyhow!( 1145 "Validator {} not found in current CLP epoch", 1146 address 1147 ))), 1148 } 1149 } 1150 None => Err(RestError::service_unavailable(anyhow!("CLP is not enabled on this node"))), 1151 } 1152 } 1153 None => Err(RestError::service_unavailable(anyhow!("CLP status is only available on validator nodes"))), 1154 } 1155 } 1156 } 1157 1158 /// Helper function to decode hex strings. 1159 fn hex_decode(s: &str) -> Result<Vec<u8>, anyhow::Error> { 1160 // Strip optional "0x" or "ax1" prefix 1161 let s = s.strip_prefix("0x").or_else(|| s.strip_prefix("ax1")).unwrap_or(s); 1162 1163 if !s.len().is_multiple_of(2) { 1164 return Err(anyhow!("Hex string must have even length")); 1165 } 1166 1167 (0..s.len()) 1168 .step_by(2) 1169 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| anyhow!("Invalid hex: {}", e))) 1170 .collect() 1171 }