snarkos_node_rest/
lib.rs

1// Copyright 2024-2025 Aleo Network Foundation
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;
22pub use helpers::*;
23
24mod routes;
25
26use snarkos_node_consensus::Consensus;
27use snarkos_node_router::{
28    Routing,
29    messages::{Message, UnconfirmedTransaction},
30};
31use snarkvm::{
32    console::{program::ProgramID, types::Field},
33    ledger::narwhal::Data,
34    prelude::{Ledger, Network, cfg_into_iter, store::ConsensusStorage},
35};
36
37use anyhow::Result;
38use axum::{
39    Json,
40    body::Body,
41    extract::{ConnectInfo, DefaultBodyLimit, Path, Query, State},
42    http::{Method, Request, StatusCode, header::CONTENT_TYPE},
43    middleware,
44    middleware::Next,
45    response::Response,
46    routing::{get, post},
47};
48use axum_extra::response::ErasedJson;
49#[cfg(feature = "locktick")]
50use locktick::parking_lot::Mutex;
51#[cfg(not(feature = "locktick"))]
52use parking_lot::Mutex;
53use std::{net::SocketAddr, sync::Arc};
54use tokio::{net::TcpListener, task::JoinHandle};
55use tower_governor::{GovernorLayer, governor::GovernorConfigBuilder};
56use tower_http::{
57    cors::{Any, CorsLayer},
58    trace::TraceLayer,
59};
60
61/// A REST API server for the ledger.
62#[derive(Clone)]
63pub struct Rest<N: Network, C: ConsensusStorage<N>, R: Routing<N>> {
64    /// The consensus module.
65    consensus: Option<Consensus<N>>,
66    /// The ledger.
67    ledger: Ledger<N, C>,
68    /// The node (routing).
69    routing: Arc<R>,
70    /// The server handles.
71    handles: Arc<Mutex<Vec<JoinHandle<()>>>>,
72}
73
74impl<N: Network, C: 'static + ConsensusStorage<N>, R: Routing<N>> Rest<N, C, R> {
75    /// Initializes a new instance of the server.
76    pub async fn start(
77        rest_ip: SocketAddr,
78        rest_rps: u32,
79        consensus: Option<Consensus<N>>,
80        ledger: Ledger<N, C>,
81        routing: Arc<R>,
82    ) -> Result<Self> {
83        // Initialize the server.
84        let mut server = Self { consensus, ledger, routing, handles: Default::default() };
85        // Spawn the server.
86        server.spawn_server(rest_ip, rest_rps).await;
87        // Return the server.
88        Ok(server)
89    }
90}
91
92impl<N: Network, C: ConsensusStorage<N>, R: Routing<N>> Rest<N, C, R> {
93    /// Returns the ledger.
94    pub const fn ledger(&self) -> &Ledger<N, C> {
95        &self.ledger
96    }
97
98    /// Returns the handles.
99    pub const fn handles(&self) -> &Arc<Mutex<Vec<JoinHandle<()>>>> {
100        &self.handles
101    }
102}
103
104impl<N: Network, C: ConsensusStorage<N>, R: Routing<N>> Rest<N, C, R> {
105    async fn spawn_server(&mut self, rest_ip: SocketAddr, rest_rps: u32) {
106        let cors = CorsLayer::new()
107            .allow_origin(Any)
108            .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
109            .allow_headers([CONTENT_TYPE]);
110
111        // Log the REST rate limit per IP.
112        debug!("REST rate limit per IP - {rest_rps} RPS");
113
114        // Prepare the rate limiting setup.
115        let governor_config = Box::new(
116            GovernorConfigBuilder::default()
117                .per_nanosecond((1_000_000_000 / rest_rps) as u64)
118                .burst_size(rest_rps)
119                .error_handler(|error| {
120                    // Properly return a 429 Too Many Requests error
121                    let error_message = error.to_string();
122                    let mut response = Response::new(error_message.clone().into());
123                    *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
124                    if error_message.contains("Too Many Requests") {
125                        *response.status_mut() = StatusCode::TOO_MANY_REQUESTS;
126                    }
127                    response
128                })
129                .finish()
130                .expect("Couldn't set up rate limiting for the REST server!"),
131        );
132
133        // Get the network being used.
134        let network = match N::ID {
135            snarkvm::console::network::MainnetV0::ID => "mainnet",
136            snarkvm::console::network::TestnetV0::ID => "testnet",
137            snarkvm::console::network::CanaryV0::ID => "canary",
138            unknown_id => {
139                eprintln!("Unknown network ID ({unknown_id})");
140                return;
141            }
142        };
143
144        let router = {
145            let routes = axum::Router::new()
146
147            // All the endpoints before the call to `route_layer` are protected with JWT auth.
148            .route(&format!("/{network}/node/address"), get(Self::get_node_address))
149            .route(&format!("/{network}/program/:id/mapping/:name"), get(Self::get_mapping_values))
150            .route_layer(middleware::from_fn(auth_middleware))
151
152            // GET ../block/..
153            .route(&format!("/{network}/block/height/latest"), get(Self::get_block_height_latest))
154            .route(&format!("/{network}/block/hash/latest"), get(Self::get_block_hash_latest))
155            .route(&format!("/{network}/block/latest"), get(Self::get_block_latest))
156            .route(&format!("/{network}/block/:height_or_hash"), get(Self::get_block))
157            // The path param here is actually only the height, but the name must match the route
158            // above, otherwise there'll be a conflict at runtime.
159            .route(&format!("/{network}/block/:height_or_hash/header"), get(Self::get_block_header))
160            .route(&format!("/{network}/block/:height_or_hash/transactions"), get(Self::get_block_transactions))
161
162            // GET and POST ../transaction/..
163            .route(&format!("/{network}/transaction/:id"), get(Self::get_transaction))
164            .route(&format!("/{network}/transaction/confirmed/:id"), get(Self::get_confirmed_transaction))
165            .route(&format!("/{network}/transaction/broadcast"), post(Self::transaction_broadcast))
166
167            // POST ../solution/broadcast
168            .route(&format!("/{network}/solution/broadcast"), post(Self::solution_broadcast))
169
170            // GET ../find/..
171            .route(&format!("/{network}/find/blockHash/:tx_id"), get(Self::find_block_hash))
172            .route(&format!("/{network}/find/blockHeight/:state_root"), get(Self::find_block_height_from_state_root))
173            .route(&format!("/{network}/find/transactionID/deployment/:program_id"), get(Self::find_transaction_id_from_program_id))
174            .route(&format!("/{network}/find/transactionID/:transition_id"), get(Self::find_transaction_id_from_transition_id))
175            .route(&format!("/{network}/find/transitionID/:input_or_output_id"), get(Self::find_transition_id))
176
177            // GET ../peers/..
178            .route(&format!("/{network}/peers/count"), get(Self::get_peers_count))
179            .route(&format!("/{network}/peers/all"), get(Self::get_peers_all))
180            .route(&format!("/{network}/peers/all/metrics"), get(Self::get_peers_all_metrics))
181
182            // GET ../program/..
183            .route(&format!("/{network}/program/:id"), get(Self::get_program))
184            .route(&format!("/{network}/program/:id/mappings"), get(Self::get_mapping_names))
185            .route(&format!("/{network}/program/:id/mapping/:name/:key"), get(Self::get_mapping_value))
186
187            // GET misc endpoints.
188            .route(&format!("/{network}/blocks"), get(Self::get_blocks))
189            .route(&format!("/{network}/height/:hash"), get(Self::get_height))
190            .route(&format!("/{network}/memoryPool/transmissions"), get(Self::get_memory_pool_transmissions))
191            .route(&format!("/{network}/memoryPool/solutions"), get(Self::get_memory_pool_solutions))
192            .route(&format!("/{network}/memoryPool/transactions"), get(Self::get_memory_pool_transactions))
193            .route(&format!("/{network}/statePath/:commitment"), get(Self::get_state_path_for_commitment))
194            .route(&format!("/{network}/stateRoot/latest"), get(Self::get_state_root_latest))
195            .route(&format!("/{network}/stateRoot/:height"), get(Self::get_state_root))
196            .route(&format!("/{network}/committee/latest"), get(Self::get_committee_latest))
197            .route(&format!("/{network}/committee/:height"), get(Self::get_committee))
198            .route(&format!("/{network}/delegators/:validator"), get(Self::get_delegators_for_validator));
199
200            // If the node is a validator and `telemetry` features is enabled, enable the additional endpoint.
201            #[cfg(feature = "telemetry")]
202            let routes = match self.consensus {
203                Some(_) => routes.route(
204                    &format!("/{network}/validators/participation"),
205                    get(Self::get_validator_participation_scores),
206                ),
207                None => routes,
208            };
209
210            // If the `history` feature is enabled, enable the additional endpoint.
211            #[cfg(feature = "history")]
212            let routes =
213                routes.route(&format!("/{network}/block/:blockHeight/history/:mapping"), get(Self::get_history));
214
215            routes
216            // Pass in `Rest` to make things convenient.
217            .with_state(self.clone())
218            // Enable tower-http tracing.
219            .layer(TraceLayer::new_for_http())
220            // Custom logging.
221            .layer(middleware::from_fn(log_middleware))
222            // Enable CORS.
223            .layer(cors)
224            // Cap body size at 512KiB.
225            .layer(DefaultBodyLimit::max(512 * 1024))
226            .layer(GovernorLayer {
227                // We can leak this because it is created only once and it persists.
228                config: Box::leak(governor_config),
229            })
230        };
231
232        let rest_listener = TcpListener::bind(rest_ip).await.unwrap();
233        self.handles.lock().push(tokio::spawn(async move {
234            axum::serve(rest_listener, router.into_make_service_with_connect_info::<SocketAddr>())
235                .await
236                .expect("couldn't start rest server");
237        }))
238    }
239}
240
241async fn log_middleware(
242    ConnectInfo(addr): ConnectInfo<SocketAddr>,
243    request: Request<Body>,
244    next: Next,
245) -> Result<Response, StatusCode> {
246    info!("Received '{} {}' from '{addr}'", request.method(), request.uri());
247
248    Ok(next.run(request).await)
249}
250
251/// Formats an ID into a truncated identifier (for logging purposes).
252pub fn fmt_id(id: impl ToString) -> String {
253    let id = id.to_string();
254    let mut formatted_id = id.chars().take(16).collect::<String>();
255    if id.chars().count() > 16 {
256        formatted_id.push_str("..");
257    }
258    formatted_id
259}