snarkos_node_rest/
lib.rs

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