diff --git a/node/rest/src/helpers/error.rs b/node/rest/src/helpers/error.rs index 8b76b1f0b0..94c9ff4633 100644 --- a/node/rest/src/helpers/error.rs +++ b/node/rest/src/helpers/error.rs @@ -18,17 +18,32 @@ use axum::{ response::{IntoResponse, Response}, }; -/// An enum of error handlers for the REST API server. -pub struct RestError(pub String); +/// An error handler for the REST API server. +pub struct RestError { + pub status: StatusCode, + pub message: String, +} + +impl RestError { + /// Creates a new `RestError` with an internal server error status. + pub fn new(message: impl Into) -> Self { + Self { status: StatusCode::INTERNAL_SERVER_ERROR, message: message.into() } + } + + /// Creates a new `RestError` with the given status code. + pub fn with_status(status: StatusCode, message: impl Into) -> Self { + Self { status, message: message.into() } + } +} impl IntoResponse for RestError { fn into_response(self) -> Response { - (StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", self.0)).into_response() + (self.status, format!("Something went wrong: {}", self.message)).into_response() } } impl From for RestError { fn from(err: anyhow::Error) -> Self { - Self(err.to_string()) + Self::new(err.to_string()) } } diff --git a/node/rest/src/routes.rs b/node/rest/src/routes.rs index 0be8597ae8..5ed2b622b6 100644 --- a/node/rest/src/routes.rs +++ b/node/rest/src/routes.rs @@ -14,6 +14,7 @@ // limitations under the License. use super::*; +use axum::http::StatusCode; use snarkos_node_router::messages::UnconfirmedSolution; use snarkvm::{ ledger::puzzle::Solution, @@ -114,9 +115,9 @@ impl, R: Routing> Rest { let block = if let Ok(height) = height_or_hash.parse::() { rest.ledger.get_block(height)? } else { - let hash = height_or_hash - .parse::() - .map_err(|_| RestError("invalid input, it is neither a block height nor a block hash".to_string()))?; + let hash = height_or_hash.parse::().map_err(|_| { + RestError::new("invalid input, it is neither a block height nor a block hash".to_string()) + })?; rest.ledger.get_block_by_hash(&hash)? }; @@ -136,17 +137,28 @@ impl, R: Routing> Rest { // Ensure the end height is greater than the start height. if start_height > end_height { - return Err(RestError("Invalid block range".to_string())); + return Err(RestError::new("Invalid block range".to_string())); } // Ensure the block range is bounded. if end_height - start_height > MAX_BLOCK_RANGE { - return Err(RestError(format!( + return Err(RestError::new(format!( "Cannot request more than {MAX_BLOCK_RANGE} blocks per call (requested {})", end_height - start_height ))); } + // Determine the latest block height in the ledger. + let latest_height = rest.ledger.latest_height(); + if start_height > latest_height || end_height > latest_height.saturating_add(1) { + return Err(RestError::with_status( + StatusCode::NOT_FOUND, + format!( + "Requested block range {start_height}..{end_height} is out of bounds (latest height {latest_height})" + ), + )); + } + // Prepare a closure for the blocking work. let get_json_blocks = move || -> Result { let blocks = cfg_into_iter!(start_height..end_height) @@ -159,7 +171,7 @@ impl, R: Routing> Rest { // Fetch the blocks from ledger and serialize to json. match tokio::task::spawn_blocking(get_json_blocks).await { Ok(json) => json, - Err(err) => Err(RestError(format!("Failed to get blocks '{start_height}..{end_height}' - {err}"))), + Err(err) => Err(RestError::new(format!("Failed to get blocks '{start_height}..{end_height}' - {err}"))), } } @@ -264,7 +276,7 @@ impl, R: Routing> Rest { Some(consensus) => { Ok(ErasedJson::pretty(consensus.unconfirmed_transmissions().collect::>())) } - None => Err(RestError("Route isn't available for this node type".to_string())), + None => Err(RestError::new("Route isn't available for this node type".to_string())), } } @@ -272,7 +284,7 @@ impl, R: Routing> Rest { pub(crate) async fn get_memory_pool_solutions(State(rest): State) -> Result { match rest.consensus { Some(consensus) => Ok(ErasedJson::pretty(consensus.unconfirmed_solutions().collect::>())), - None => Err(RestError("Route isn't available for this node type".to_string())), + None => Err(RestError::new("Route isn't available for this node type".to_string())), } } @@ -280,7 +292,7 @@ impl, R: Routing> Rest { pub(crate) async fn get_memory_pool_transactions(State(rest): State) -> Result { match rest.consensus { Some(consensus) => Ok(ErasedJson::pretty(consensus.unconfirmed_transactions().collect::>())), - None => Err(RestError("Route isn't available for this node type".to_string())), + None => Err(RestError::new("Route isn't available for this node type".to_string())), } } @@ -392,7 +404,9 @@ impl, R: Routing> Rest { ) -> Result { // Return an error if the `all` query parameter is not set to `true`. if metadata.all != Some(true) { - return Err(RestError("Invalid query parameter. At this time, 'all=true' must be included".to_string())); + return Err(RestError::new( + "Invalid query parameter. At this time, 'all=true' must be included".to_string(), + )); } // Retrieve the latest height. @@ -414,8 +428,8 @@ impl, R: Routing> Rest { // Return the full mapping without metadata. Ok(ErasedJson::pretty(mapping_values)) } - Ok(Err(err)) => Err(RestError(format!("Unable to read mapping - {err}"))), - Err(err) => Err(RestError(format!("Unable to read mapping - {err}"))), + Ok(Err(err)) => Err(RestError::new(format!("Unable to read mapping - {err}"))), + Err(err) => Err(RestError::new(format!("Unable to read mapping - {err}"))), } } @@ -460,14 +474,14 @@ impl, R: Routing> Rest { ) -> Result { // Do not process the request if the node is too far behind to avoid sending outdated data. if !rest.routing.is_within_sync_leniency() { - return Err(RestError("Unable to request delegators (node is syncing)".to_string())); + return Err(RestError::new("Unable to request delegators (node is syncing)".to_string())); } // Return the delegators for the given validator. match tokio::task::spawn_blocking(move || rest.ledger.get_delegators_for_validator(&validator)).await { Ok(Ok(delegators)) => Ok(ErasedJson::pretty(delegators)), - Ok(Err(err)) => Err(RestError(format!("Unable to request delegators - {err}"))), - Err(err) => Err(RestError(format!("Unable to request delegators - {err}"))), + Ok(Err(err)) => Err(RestError::new(format!("Unable to request delegators - {err}"))), + Err(err) => Err(RestError::new(format!("Unable to request delegators - {err}"))), } } @@ -548,7 +562,10 @@ impl, R: Routing> Rest { ) -> Result { // Do not process the transaction if the node is too far behind. if !rest.routing.is_within_sync_leniency() { - return Err(RestError(format!("Unable to broadcast transaction '{}' (node is syncing)", fmt_id(tx.id())))); + return Err(RestError::new(format!( + "Unable to broadcast transaction '{}' (node is syncing)", + fmt_id(tx.id()) + ))); } // If the transaction exceeds the transaction size limit, return an error. @@ -557,7 +574,7 @@ impl, R: Routing> Rest { // TODO: Should this be a blocking task? let buffer = Vec::with_capacity(3000); if tx.write_le(LimitedWriter::new(buffer, N::MAX_TRANSACTION_SIZE)).is_err() { - return Err(RestError("Transaction size exceeds the byte limit".to_string())); + return Err(RestError::new("Transaction size exceeds the byte limit".to_string())); } if check_transaction.check_transaction.unwrap_or(false) { @@ -581,13 +598,13 @@ impl, R: Routing> Rest { let prev = counter.fetch_add(1, Ordering::Relaxed); if prev >= limit { counter.fetch_sub(1, Ordering::Relaxed); - return Err(RestError(err_msg.to_string())); + return Err(RestError::new(err_msg.to_string())); } // Perform the check. let res = rest .ledger .check_transaction_basic(&tx, None, &mut rand::thread_rng()) - .map_err(|e| RestError(format!("Invalid transaction: {e}"))); + .map_err(|e| RestError::new(format!("Invalid transaction: {e}"))); // Release the slot. counter.fetch_sub(1, Ordering::Relaxed); // Propagate error if any. @@ -620,7 +637,7 @@ impl, R: Routing> Rest { ) -> Result { // Do not process the solution if the node is too far behind. if !rest.routing.is_within_sync_leniency() { - return Err(RestError(format!( + return Err(RestError::new(format!( "Unable to broadcast solution '{}' (node is syncing)", fmt_id(solution.id()) ))); @@ -644,7 +661,7 @@ impl, R: Routing> Rest { // here prevents the to-be aborted solutions from propagating through the network. let prover_address = solution.address(); if rest.ledger.is_solution_limit_reached(&prover_address, 0) { - return Err(RestError(format!( + return Err(RestError::new(format!( "Invalid solution '{}' - Prover '{prover_address}' has reached their solution limit for the current epoch", fmt_id(solution.id()) ))); @@ -655,9 +672,11 @@ impl, R: Routing> Rest { { Ok(Ok(())) => {} Ok(Err(err)) => { - return Err(RestError(format!("Invalid solution '{}' - {err}", fmt_id(solution.id())))); + return Err(RestError::new(format!("Invalid solution '{}' - {err}", fmt_id(solution.id())))); + } + Err(err) => { + return Err(RestError::new(format!("Invalid solution '{}' - {err}", fmt_id(solution.id())))); } - Err(err) => return Err(RestError(format!("Invalid solution '{}' - {err}", fmt_id(solution.id())))), } } } @@ -693,7 +712,7 @@ impl, R: Routing> Rest { let history = snarkvm::synthesizer::History::new(N::ID, rest.ledger.vm().finalize_store().storage_mode()); let result = history .load_mapping(height, mapping) - .map_err(|_| RestError(format!("Could not load mapping '{mapping}' from block '{height}'")))?; + .map_err(|_| RestError::new(format!("Could not load mapping '{mapping}' from block '{height}'")))?; Ok((StatusCode::OK, [(CONTENT_TYPE, "application/json")], result)) } @@ -727,7 +746,7 @@ impl, R: Routing> Rest { Ok(ErasedJson::pretty(participation_scores)) } - None => Err(RestError("Route isn't available for this node type".to_string())), + None => Err(RestError::new("Route isn't available for this node type".to_string())), } } }