Skip to main content

snarkos_node_rest/
lib.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16#![forbid(unsafe_code)]
17
18#[macro_use]
19extern crate tracing;
20
21mod helpers;
22// Imports custom `Path` type, to be used instead of `axum`'s.
23pub use helpers::*;
24
25mod routes;
26
27mod version;
28
29use snarkos_node_cdn::CdnBlockSync;
30use snarkos_node_consensus::Consensus;
31use snarkos_node_router::{
32    Routing,
33    messages::{Message, UnconfirmedTransaction},
34};
35use snarkos_node_sync::BlockSync;
36use snarkvm::{
37    console::{program::ProgramID, types::Field},
38    ledger::narwhal::Data,
39    prelude::{Ledger, Network, VM, cfg_into_iter, store::ConsensusStorage},
40};
41
42use anyhow::{Context, Result};
43use axum::{
44    body::Body,
45    extract::{ConnectInfo, DefaultBodyLimit, Query, State},
46    http::{Method, Request, StatusCode, header::CONTENT_TYPE},
47    middleware,
48    response::Response,
49    routing::{get, post},
50};
51use axum_extra::response::ErasedJson;
52#[cfg(feature = "locktick")]
53use locktick::parking_lot::Mutex;
54use lru::LruCache;
55#[cfg(not(feature = "locktick"))]
56use parking_lot::Mutex;
57use std::{net::SocketAddr, num::NonZeroUsize, sync::Arc, time::Duration};
58use tokio::{net::TcpListener, sync::Semaphore, task::JoinHandle};
59use tower_governor::{GovernorLayer, governor::GovernorConfigBuilder};
60use tower_http::{
61    cors::{Any, CorsLayer},
62    trace::TraceLayer,
63};
64use tracing::Span;
65
66/// The default port used for the REST API
67pub const DEFAULT_REST_PORT: u16 = 3030;
68
69/// The API version prefixes.
70pub const API_VERSION_V1: &str = "v1";
71pub const API_VERSION_V2: &str = "v2";
72
73/// The capacity of the LRU holding recently requested blocks.
74const BLOCK_CACHE_SIZE: usize = 128;
75
76/// A REST API server for the ledger.
77#[derive(Clone)]
78pub struct Rest<N: Network, C: ConsensusStorage<N>, R: Routing<N>> {
79    /// CDN sync (only if node is using the CDN to sync).
80    cdn_sync: Option<Arc<CdnBlockSync>>,
81    /// The consensus module.
82    consensus: Option<Consensus<N>>,
83    /// The ledger.
84    ledger: Ledger<N, C>,
85    /// The node (routing).
86    routing: Arc<R>,
87    /// The server handles.
88    handles: Arc<Mutex<Vec<JoinHandle<()>>>>,
89    /// A reference to BlockSync,
90    block_sync: Arc<BlockSync<N>>,
91    /// The number of ongoing deploy transaction verifications via REST.
92    num_verifying_deploys: Arc<Semaphore>,
93    /// The number of ongoing execute transaction verifications via REST.
94    num_verifying_executions: Arc<Semaphore>,
95    /// The number of ongoing solution verifications via REST.
96    num_verifying_solutions: Arc<Semaphore>,
97    /// A cache containing recently requested blocks.
98    block_cache: Arc<Mutex<LruCache<N::BlockHash, ErasedJson>>>,
99}
100
101impl<N: Network, C: 'static + ConsensusStorage<N>, R: Routing<N>> Rest<N, C, R> {
102    /// Initializes a new instance of the server.
103    pub async fn start(
104        rest_ip: SocketAddr,
105        rest_rps: u32,
106        consensus: Option<Consensus<N>>,
107        ledger: Ledger<N, C>,
108        routing: Arc<R>,
109        cdn_sync: Option<Arc<CdnBlockSync>>,
110        block_sync: Arc<BlockSync<N>>,
111    ) -> Result<Self> {
112        // Initialize the server.
113        let mut server = Self {
114            consensus,
115            ledger,
116            routing,
117            cdn_sync,
118            block_sync,
119            handles: Default::default(),
120            num_verifying_deploys: Arc::new(Semaphore::new(VM::<N, C>::MAX_PARALLEL_DEPLOY_VERIFICATIONS)),
121            num_verifying_executions: Arc::new(Semaphore::new(VM::<N, C>::MAX_PARALLEL_EXECUTE_VERIFICATIONS)),
122            num_verifying_solutions: Arc::new(Semaphore::new(N::MAX_SOLUTIONS)),
123            block_cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(BLOCK_CACHE_SIZE).unwrap()))),
124        };
125        // Spawn the server.
126        server.spawn_server(rest_ip, rest_rps).await?;
127        // Return the server.
128        Ok(server)
129    }
130}
131
132impl<N: Network, C: ConsensusStorage<N>, R: Routing<N>> Rest<N, C, R> {
133    /// Returns the ledger.
134    pub const fn ledger(&self) -> &Ledger<N, C> {
135        &self.ledger
136    }
137
138    /// Returns the handles.
139    pub const fn handles(&self) -> &Arc<Mutex<Vec<JoinHandle<()>>>> {
140        &self.handles
141    }
142
143    /// Shuts down the REST instance.
144    pub fn shut_down(&self) {
145        self.handles.lock().iter().for_each(|handle| handle.abort());
146    }
147}
148
149impl<N: Network, C: ConsensusStorage<N>, R: Routing<N>> Rest<N, C, R> {
150    fn build_routes(&self, rest_rps: u32) -> axum::Router {
151        let cors = CorsLayer::new()
152            .allow_origin(Any)
153            .allow_methods([Method::GET, Method::POST, Method::DELETE, Method::OPTIONS])
154            .allow_headers([CONTENT_TYPE]);
155
156        // Prepare the rate limiting setup.
157        let governor_config = Box::new(
158            GovernorConfigBuilder::default()
159                .per_nanosecond((1_000_000_000 / rest_rps) as u64)
160                .burst_size(rest_rps)
161                .error_handler(|error| {
162                    // Properly return a 429 Too Many Requests error
163                    let error_message = error.to_string();
164                    let mut response = Response::new(error_message.clone().into());
165                    *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
166                    if error_message.contains("Too Many Requests") {
167                        *response.status_mut() = StatusCode::TOO_MANY_REQUESTS;
168                    }
169                    response
170                })
171                .finish()
172                .expect("Couldn't set up rate limiting for the REST server!"),
173        );
174
175        // Build the JWT auth-protected endpoints. #[cfg] cannot appear inside a method chain, so we
176        // build this router as a named binding and conditionally extend it before applying the layer.
177        let auth_routes = axum::Router::new()
178            .route("/node/address", get(Self::get_node_address))
179            .route("/program/{id}/mapping/{name}", get(Self::get_mapping_values))
180            .route("/db_backup", post(Self::db_backup));
181
182        // Slipstream plugin management endpoints require auth.
183        #[cfg(feature = "slipstream-plugins")]
184        let auth_routes = auth_routes
185            .route("/slipstream/plugins", get(Self::slipstream_list_plugins).post(Self::slipstream_load_plugin))
186            .route(
187                "/slipstream/plugins/{name}",
188                // TODO: PUT (reload) is not yet implemented.
189                axum::routing::delete(Self::slipstream_unload_plugin),
190            );
191
192        let routes = axum::Router::new()
193            .merge(auth_routes.route_layer(middleware::from_fn(auth_middleware)))
194
195            // All endpoints declared after here are not protected
196
197             // Get ../consensus_version
198            .route("/consensus_version", get(Self::get_consensus_version))
199
200            // GET ../block/..
201            .route("/block/height/latest", get(Self::get_block_height_latest))
202            .route("/block/hash/latest", get(Self::get_block_hash_latest))
203            .route("/block/latest", get(Self::get_block_latest))
204            .route("/block/{height_or_hash}", get(Self::get_block))
205            // The path param here is actually only the height, but the name must match the route
206            // above, otherwise there'll be a conflict at runtime.
207            .route("/block/{height_or_hash}/header", get(Self::get_block_header))
208            .route("/block/{height_or_hash}/transactions", get(Self::get_block_transactions))
209
210            // GET and POST ../transaction/..
211            .route("/transaction/{id}", get(Self::get_transaction))
212            .route("/transaction/confirmed/{id}", get(Self::get_confirmed_transaction))
213            .route("/transaction/unconfirmed/{id}", get(Self::get_unconfirmed_transaction))
214            .route("/transaction/rejected/{id}/reason", get(Self::get_transaction_rejection_reason))
215            .route("/transaction/broadcast", post(Self::transaction_broadcast))
216
217            // GET and POST ../solution/..
218            .route("/solution/limits/{prover_address}", get(Self::get_solution_limits_for_prover))
219            .route("/solution/broadcast", post(Self::solution_broadcast))
220
221            // GET ../find/..
222            .route("/find/blockHash/{tx_id}", get(Self::find_block_hash))
223            .route("/find/blockHeight/{state_root}", get(Self::find_block_height_from_state_root))
224            .route("/find/transactionID/deployment/{program_id}", get(Self::find_latest_transaction_id_from_program_id))
225            .route("/find/transactionID/deployment/{program_id}/{edition}", get(Self::find_latest_transaction_id_from_program_id_and_edition))
226            .route("/find/transactionID/deployment/{program_id}/{edition}/original", get(Self::find_original_deployment_transaction_id))
227            .route("/find/transactionID/deployment/{program_id}/{edition}/{amendment}", get(Self::find_transaction_id_from_program_id_edition_and_amendment))
228            .route("/find/transactionID/{transition_id}", get(Self::find_transaction_id_from_transition_id))
229            .route("/find/transitionID/{input_or_output_id}", get(Self::find_transition_id))
230
231            // GET ../connections/p2p/.. (with ../peers/.. aliases)
232            .route("/peers/count", get(Self::get_peers_count))
233            .route("/peers/all", get(Self::get_peers_all))
234            .route("/peers/all/metrics", get(Self::get_peers_all_metrics))
235            .route("/connections/p2p/count", get(Self::get_peers_count))
236            .route("/connections/p2p/all", get(Self::get_peers_all))
237            .route("/connections/p2p/all/metrics", get(Self::get_peers_all_metrics))
238
239            // GET ../program/..
240            .route("/program/{id}", get(Self::get_program))
241            .route("/program/{id}/latest_edition", get(Self::get_latest_program_edition))
242            .route("/program/{id}/{edition}", get(Self::get_program_for_edition))
243            .route("/program/{id}/mappings", get(Self::get_mapping_names))
244            .route("/program/{id}/mapping/{name}/{key}", get(Self::get_mapping_value))
245            .route("/program/{id}/amendment_count", get(Self::get_program_amendment_count))
246            .route("/program/{id}/{edition}/amendment_count", get(Self::get_program_amendment_count_for_edition))
247
248            // GET ../sync/..
249            // Note: keeping ../sync_status for compatibility
250            .route("/sync_status", get(Self::get_sync_status))
251            .route("/sync/status", get(Self::get_sync_status))
252            .route("/sync/peers", get(Self::get_sync_peers))
253            .route("/sync/requests", get(Self::get_sync_requests_summary))
254            .route("/sync/requests/list", get(Self::get_sync_requests_list))
255
256            // GET misc endpoints.
257            .route("/version", get(Self::get_version))
258            .route("/blocks", get(Self::get_blocks))
259            .route("/height/{hash}", get(Self::get_height))
260            .route("/memoryPool/transmissions", get(Self::get_memory_pool_transmissions))
261            .route("/memoryPool/solutions", get(Self::get_memory_pool_solutions))
262            .route("/memoryPool/transactions", get(Self::get_memory_pool_transactions))
263            .route("/statePath/{commitment}", get(Self::get_state_path_for_commitment))
264            .route("/statePaths", get(Self::get_state_paths_for_commitments))
265            .route("/stateRoot/latest", get(Self::get_state_root_latest))
266            .route("/stateRoot/{height}", get(Self::get_state_root))
267            .route("/committee/latest", get(Self::get_committee_latest))
268            .route("/committee/{height}", get(Self::get_committee))
269            .route("/delegators/{validator}", get(Self::get_delegators_for_validator));
270
271        // If the node is a validator, enable the BFT connections endpoints.
272        let routes = match self.consensus {
273            Some(_) => routes
274                .route("/connections/bft/count", get(Self::get_bft_connections_count))
275                .route("/connections/bft/all", get(Self::get_bft_connections_all)),
276            None => routes,
277        };
278
279        // If the node is a validator and `telemetry` features is enabled, enable the additional endpoint.
280        #[cfg(feature = "telemetry")]
281        let routes = match self.consensus {
282            Some(_) => routes.route("/validators/participation", get(Self::get_validator_participation_scores)),
283            None => routes,
284        };
285
286        // Register the view-at-latest-height endpoint (always available, no history required).
287        let routes = routes.route("/program/{id}/view/{function}", post(Self::evaluate_view_latest));
288
289        // If the `history` feature is enabled, enable the additional endpoints.
290        #[cfg(feature = "history")]
291        let routes = routes
292            .route("/program/{id}/mapping/{name}/{key}/history/{height}", get(Self::get_history))
293            .route("/program/{id}/mapping/{name}/history/{height}", get(Self::get_history_batch))
294            .route("/program/{id}/view/{function}/{height}", post(Self::evaluate_view));
295
296        // If the `history-staking-rewards` feature is enabled, enable the additional endpoint.
297        #[cfg(feature = "history-staking-rewards")]
298        let routes = routes.route("/staking/rewards/{address}/{height}", get(Self::get_staking_reward));
299
300        let trace_layer = TraceLayer::new_for_http()
301            .make_span_with(|request: &Request<_>| {
302                let addr = request
303                    .extensions()
304                    .get::<ConnectInfo<SocketAddr>>()
305                    .map(|ConnectInfo(addr)| addr.to_string())
306                    .unwrap_or_else(|| "unknown".to_string());
307
308                // Create a span that includes method, path, and our extracted IP
309                tracing::info_span!(
310                    "REST",
311                    method = %request.method(),
312                    uri = %request.uri().path(),
313                    addr = %addr,
314                )
315            })
316            .on_request(|_request: &Request<_>, _span: &Span| {
317                info!("Received a request");
318            })
319            .on_response(|_response: &Response<_>, latency: Duration, _span: &Span| {
320                info!("Finished request in {:?}", latency);
321            });
322
323        routes
324            // Pass in `Rest` to make things convenient.
325            .with_state(self.clone())
326            // Cap the request body size at 1.5MiB.
327            .layer(DefaultBodyLimit::max(2 * 768 * 1024))
328            .layer(GovernorLayer {
329                config: governor_config.into(),
330            })
331            // Enable CORS.
332            .layer(cors)
333            // Enable tower-http tracing.
334            .layer(trace_layer)
335    }
336
337    async fn spawn_server(&mut self, rest_ip: SocketAddr, rest_rps: u32) -> Result<()> {
338        // Log the REST rate limit per IP.
339        debug!("REST rate limit per IP - {rest_rps} RPS");
340
341        // Add the v1 API as default and under "/v1".
342        let default_router = axum::Router::new().nest(
343            &format!("/{}", N::SHORT_NAME),
344            self.build_routes(rest_rps).layer(middleware::map_response(v1_error_middleware)),
345        );
346        let v1_router = axum::Router::new().nest(
347            &format!("/{API_VERSION_V1}/{}", N::SHORT_NAME),
348            self.build_routes(rest_rps).layer(middleware::map_response(v1_error_middleware)),
349        );
350
351        // Add the v2 API under "/v2".
352        let v2_router =
353            axum::Router::new().nest(&format!("/{API_VERSION_V2}/{}", N::SHORT_NAME), self.build_routes(rest_rps));
354
355        // Combine all routes.
356        let router = default_router.merge(v1_router).merge(v2_router);
357
358        let rest_listener =
359            TcpListener::bind(rest_ip).await.with_context(|| "Failed to bind TCP port for REST endpoints")?;
360
361        let handle = tokio::spawn(async move {
362            axum::serve(rest_listener, router.into_make_service_with_connect_info::<SocketAddr>())
363                .await
364                .expect("couldn't start rest server");
365        });
366
367        self.handles.lock().push(handle);
368        Ok(())
369    }
370}
371
372/// Converts errors to the old style for the v1 API.
373/// The error code will always be 500 and the content a simple string.
374async fn v1_error_middleware(response: Response) -> Response {
375    // The status code used by all v1 errors
376    const V1_STATUS_CODE: StatusCode = StatusCode::INTERNAL_SERVER_ERROR;
377
378    if response.status().is_success() {
379        return response;
380    }
381
382    // Returns a opaque error instead of panicking.
383    let fallback = || {
384        let mut response = Response::new(Body::from("Failed to convert error"));
385        *response.status_mut() = V1_STATUS_CODE;
386        response
387    };
388
389    let Ok(bytes) = axum::body::to_bytes(response.into_body(), usize::MAX).await else {
390        return fallback();
391    };
392
393    // Deserialize REST error so we can convert it to a string
394    let Ok(json_err) = serde_json::from_slice::<SerializedRestError>(&bytes) else {
395        return fallback();
396    };
397
398    let mut message = json_err.message;
399    for next in json_err.chain.into_iter() {
400        message = format!("{message} — {next}");
401    }
402
403    let mut response = Response::new(Body::from(message));
404
405    *response.status_mut() = V1_STATUS_CODE;
406
407    response
408}
409
410/// Formats an ID into a truncated identifier (for logging purposes).
411pub fn fmt_id(id: impl ToString) -> String {
412    let id = id.to_string();
413    let mut formatted_id = id.chars().take(16).collect::<String>();
414    if id.chars().count() > 16 {
415        formatted_id.push_str("..");
416    }
417    formatted_id
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423    use anyhow::anyhow;
424    use axum::{
425        Router,
426        body::Body,
427        http::{Request, StatusCode},
428        middleware,
429        routing::get,
430    };
431    use tower::ServiceExt; // for `oneshot`
432
433    fn test_app() -> Router {
434        let build_routes = || {
435            Router::new()
436                .route("/not_found", get(|| async { Err::<(), RestError>(RestError::not_found(anyhow!("missing"))) }))
437                .route("/bad_request", get(|| async { Err::<(), RestError>(RestError::bad_request(anyhow!("bad"))) }))
438                .route(
439                    "/service_unavailable",
440                    get(|| async { Err::<(), RestError>(RestError::service_unavailable(anyhow!("gone"))) }),
441                )
442        };
443        let router_v1 = build_routes().route_layer(middleware::map_response(v1_error_middleware));
444        let router_v2 = Router::new().nest(&format!("/{API_VERSION_V2}"), build_routes());
445        router_v1.merge(router_v2)
446    }
447
448    #[tokio::test]
449    async fn v1_routes_force_internal_server_error() {
450        let app = test_app();
451
452        let res = app.clone().oneshot(Request::builder().uri("/not_found").body(Body::empty()).unwrap()).await.unwrap();
453        assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
454
455        let res =
456            app.clone().oneshot(Request::builder().uri("/bad_request").body(Body::empty()).unwrap()).await.unwrap();
457        assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
458
459        let res =
460            app.oneshot(Request::builder().uri("/service_unavailable").body(Body::empty()).unwrap()).await.unwrap();
461        assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
462    }
463
464    #[tokio::test]
465    async fn v2_routes_return_specific_errors() {
466        let app = test_app();
467
468        let res =
469            app.clone().oneshot(Request::builder().uri("/v2/not_found").body(Body::empty()).unwrap()).await.unwrap();
470        assert_eq!(res.status(), StatusCode::NOT_FOUND);
471
472        let res =
473            app.clone().oneshot(Request::builder().uri("/v2/bad_request").body(Body::empty()).unwrap()).await.unwrap();
474        assert_eq!(res.status(), StatusCode::BAD_REQUEST);
475
476        let res =
477            app.oneshot(Request::builder().uri("/v2/service_unavailable").body(Body::empty()).unwrap()).await.unwrap();
478        assert_eq!(res.status(), StatusCode::SERVICE_UNAVAILABLE);
479    }
480}