amareleo_chain_api/api/
api_node.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at:
4
5// http://www.apache.org/licenses/LICENSE-2.0
6
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12
13use crate::{
14    AmareleoApiData,
15    AmareleoApiState,
16    AmareleoLog,
17    ValidatorApi,
18    ValidatorNewData,
19    clean_tmp_ledger,
20    is_valid_network_id,
21};
22
23use std::{net::SocketAddr, path::PathBuf, sync::OnceLock};
24
25use anyhow::{Result, bail};
26#[cfg(feature = "locktick")]
27use locktick::parking_lot::Mutex;
28#[cfg(not(feature = "locktick"))]
29use parking_lot::Mutex;
30use snarkvm::console::network::{Network, TestnetV0};
31
32use amareleo_chain_tracing::TracingHandler;
33use amareleo_node_bft::helpers::amareleo_storage_mode;
34
35// ===================================================================
36// AmareleoApi is a simple thread-safe singleton.
37// There should be little scope for accessing this object
38// from concurrent threads. Thus we adopt a simple non-rentrant
39// Mutex that locks all data both on reading and writing.
40//
41// The non-rentrant Mutex, requires that functions acquiring
42// the Mutex lock do not call other functions that would also lock
43// the same Mutex. That would lead to a deadlock.
44//
45// The general rule is to only let public functions lock the Mutex,
46// such that private functions can be safely shared. However this
47// rule is not strictly followed. See the start(), end() and try_*()
48// functions for example.
49//
50// The AmareleoApiData struct is a lock-free private space most
51// appropriate for reusable code. This struct is private and has no
52// access to the Mutex.
53//
54// ===================================================================
55
56/// Amareleo node object, for creating and managing a node instance
57pub struct AmareleoApi {
58    data: Mutex<AmareleoApiData>,
59}
60
61// Global storage for the singleton instance
62static INSTANCE: OnceLock<AmareleoApi> = OnceLock::new();
63
64impl AmareleoApi {
65    /// Create or return the singleton instance of AmareleoApi.
66    ///
67    /// When called for the first time the node is initialized with:
68    /// - Testnet network
69    /// - Bound to localhost port 3030
70    /// - REST limited to 10 requests per second
71    /// - No ledger state retenttion across runs
72    /// - Unique ledger folder naming
73    /// - Logging disabled
74    pub fn new() -> &'static Self {
75        INSTANCE.get_or_init(|| Self {
76            data: Mutex::new(AmareleoApiData {
77                state: AmareleoApiState::Init,
78                prev_state: AmareleoApiState::Init,
79                network_id: TestnetV0::ID,
80                rest_ip: "0.0.0.0:3030".parse().unwrap(),
81                rest_rps: 10u32,
82                keep_state: false,
83                ledger_base_path: None,
84                ledger_default_naming: false,
85                log_mode: AmareleoLog::None,
86                tracing: None,
87                verbosity: 1u8,
88                shutdown: Default::default(),
89                validator: ValidatorApi::None,
90            }),
91        })
92    }
93}
94
95// AmareleoApi configuration setters
96impl AmareleoApi {
97    /// Configure network ID. Valid network values include:
98    /// - CanaryV0::ID
99    /// - TestnetV0::ID
100    /// - MainnetV0::ID
101    ///
102    /// # Parameters
103    ///
104    /// * network_id: `u16` - network ID to use.
105    ///
106    /// # Returns
107    ///
108    /// * `Err` if node is NOT in the `Init` state.
109    /// * `Err` if network_id is invalid.
110    pub fn cfg_network(&self, network_id: u16) -> Result<()> {
111        let mut data = self.data.lock();
112
113        if !data.is_state(AmareleoApiState::Init) {
114            bail!("Invalid node state {:?}", data.state);
115        }
116
117        if !is_valid_network_id(network_id) {
118            bail!("Invalid network id {network_id}");
119        }
120        data.network_id = network_id;
121        Ok(())
122    }
123
124    /// Same as `cfg_network` but fails silently.
125    ///
126    /// # Returns
127    ///
128    /// `&Self` allowing for chaining `try_cfg_*()` calls
129    pub fn try_cfg_network(&self, network_id: u16) -> &Self {
130        let _ = self.cfg_network(network_id);
131        self
132    }
133
134    /// Configure REST server IP, port, and requests per second limit.
135    ///
136    /// # Parameters
137    ///
138    /// * ip_port: `SocketAddr` - IP and port to listen to
139    /// * rps: `u32` - requests per second limit
140    ///
141    /// # Returns
142    ///
143    /// * `Err` if node is NOT in the `Init` state.
144    pub fn cfg_rest(&self, ip_port: SocketAddr, rps: u32) -> Result<()> {
145        let mut data = self.data.lock();
146
147        if !data.is_state(AmareleoApiState::Init) {
148            bail!("Invalid node state {:?}", data.state);
149        }
150        data.rest_ip = ip_port;
151        data.rest_rps = rps;
152        Ok(())
153    }
154
155    /// Same as `cfg_rest` but fails silently.
156    ///
157    /// # Returns
158    ///
159    /// `&Self` allowing for chaining `try_cfg_*()` calls
160    pub fn try_cfg_rest(&self, ip_port: SocketAddr, rps: u32) -> &Self {
161        let _ = self.cfg_rest(ip_port, rps);
162        self
163    }
164
165    /// Configure ledger storage properties.
166    ///
167    /// # Parameters
168    ///
169    /// * keep_state: `bool` - keep ledger state between restarts
170    /// * base_path: `Option<PathBuf>` - custom ledger folder path
171    /// * default_naming: `bool` - if `true` use fixed ledger folder naming, instead of deriving a unique name from the rest IP:port configuration.
172    ///
173    /// # Returns
174    ///
175    /// * `Err` if node is NOT in the `Init` state.
176    pub fn cfg_ledger(&self, keep_state: bool, base_path: Option<PathBuf>, default_naming: bool) -> Result<()> {
177        let mut data = self.data.lock();
178
179        if !data.is_state(AmareleoApiState::Init) {
180            bail!("Invalid node state {:?}", data.state);
181        }
182        data.keep_state = keep_state;
183        data.ledger_base_path = base_path;
184        data.ledger_default_naming = default_naming;
185        Ok(())
186    }
187
188    /// Same as `cfg_ledger` but fails silently.
189    ///
190    /// # Returns
191    ///
192    /// `&Self` allowing for chaining `try_cfg_*()` calls
193    pub fn try_cfg_ledger(&self, keep_state: bool, base_path: Option<PathBuf>, default_naming: bool) -> &Self {
194        let _ = self.cfg_ledger(keep_state, base_path, default_naming);
195        self
196    }
197
198    /// Configure file logging path and verbosity level.
199    ///
200    /// # Parameters
201    ///
202    /// * log_file: `Option<PathBuf>` - custom log file path, or None for default path
203    /// * verbosity: `u8` - log verbosity level between 0 and 4, where 0 is the least verbose
204    ///
205    /// # Returns
206    ///
207    /// * `Err` if node is NOT in the `Init` or `Stopped` state.
208    pub fn cfg_file_log(&self, log_file: Option<PathBuf>, verbosity: u8) -> Result<()> {
209        let mut data = self.data.lock();
210
211        if !data.is_state(AmareleoApiState::Init) && !data.is_state(AmareleoApiState::Stopped) {
212            bail!("Invalid node state {:?}", data.state);
213        }
214        data.log_mode = AmareleoLog::File(log_file);
215        data.verbosity = verbosity;
216        Ok(())
217    }
218
219    /// Same as `cfg_file_log` but fails silently.
220    ///
221    /// # Returns
222    ///
223    /// `&Self` allowing for chaining `try_cfg_*()` calls
224    pub fn try_cfg_file_log(&self, log_file: Option<PathBuf>, verbosity: u8) -> &Self {
225        let _ = self.cfg_file_log(log_file, verbosity);
226        self
227    }
228
229    /// Configure custom tracing subscribers.
230    ///
231    /// # Parameters
232    ///
233    /// * tracing: `TracingHandler` - custom tracing subscriber
234    ///
235    /// # Returns
236    ///
237    /// * `Err` if node is NOT in the `Init` or `Stopped` state.
238    pub fn cfg_custom_log(&self, tracing: TracingHandler) -> Result<()> {
239        let mut data = self.data.lock();
240
241        if !data.is_state(AmareleoApiState::Init) && !data.is_state(AmareleoApiState::Stopped) {
242            bail!("Invalid node state {:?}", data.state);
243        }
244        data.log_mode = AmareleoLog::Custom(tracing);
245        Ok(())
246    }
247
248    /// Same as `cfg_custom_log` but fails silently.
249    ///
250    /// # Returns
251    ///
252    /// `&Self` allowing for chaining `try_cfg_*()` calls
253    pub fn try_cfg_custom_log(&self, tracing: TracingHandler) -> &Self {
254        let _ = self.cfg_custom_log(tracing);
255        self
256    }
257
258    /// Disable logging.
259    ///
260    /// # Returns
261    ///
262    /// * `Err` if node is NOT in the `Init` or `Stopped` state.
263    pub fn cfg_no_log(&self) -> Result<()> {
264        let mut data = self.data.lock();
265
266        if !data.is_state(AmareleoApiState::Init) && !data.is_state(AmareleoApiState::Stopped) {
267            bail!("Invalid node state {:?}", data.state);
268        }
269        data.log_mode = AmareleoLog::None;
270        Ok(())
271    }
272
273    /// Same as `cfg_no_log` but fails silently.
274    ///
275    /// # Returns
276    ///
277    /// `&Self` allowing for chaining `try_cfg_*()` calls
278    pub fn try_cfg_no_log(&self) -> &Self {
279        let _ = self.cfg_no_log();
280        self
281    }
282}
283
284// AmareleoApi public getters
285impl AmareleoApi {
286    /// Get the node object state
287    ///
288    /// # Returns
289    ///
290    /// * `AmareleoApiState` - one of the enum state values.
291    pub fn get_state(&self) -> AmareleoApiState {
292        self.data.lock().state
293    }
294
295    /// Check if the node is started
296    ///
297    /// # Returns
298    ///
299    /// * `bool` - `true` if the node is running, `false` otherwise
300    pub fn is_started(&self) -> bool {
301        self.data.lock().is_state(AmareleoApiState::Started)
302    }
303
304    /// Get log file path, based on current configuration.
305    /// Fails if file logging is not configured.
306    ///
307    /// Note: Changes in configuration may cause the log file path to change.<br>
308    /// Properties that effect the log file path include:<br>
309    /// - REST endpont
310    /// - network ID
311    /// - ledger state retention flag
312    ///
313    /// # Returns
314    ///
315    /// * `Result<PathBuf>` - log file path
316    pub fn get_log_file(&self) -> Result<PathBuf> {
317        self.data.lock().get_log_file()
318    }
319
320    /// Get ledger folder path, based on the current configuration.
321    ///
322    /// Note: Changes in configuration may cause the ledger path to change.<br>
323    /// Properties that effect the ledger path include:<br>
324    /// - REST endpont (if default naming is disabled)
325    /// - network ID
326    /// - ledger state retention flag
327    ///
328    /// # Returns
329    ///
330    /// * `Result<PathBuf>` - ledger file path
331    pub fn get_ledger_folder(&self) -> Result<PathBuf> {
332        self.data.lock().get_ledger_folder()
333    }
334}
335
336// AmareleoApi node start operation
337impl AmareleoApi {
338    /// Start node instance
339    ///
340    /// Method should be called when node is in the `Init` or `Stopped` state.<br>
341    /// If the node is in any other state, the method fails.
342    pub async fn start(&self) -> Result<()> {
343        // The state rules are enforced within start_prep/start_complete
344        // Sandwiching validator::new between them, secures against
345        // concurrency.
346        let validator_data = self.start_prep()?;
347        let res = ValidatorApi::new(validator_data).await;
348
349        match res {
350            Ok(vapi) => {
351                self.start_complete(vapi);
352            }
353            Err(error) => {
354                self.start_revert();
355                return Err(error);
356            }
357        };
358
359        Ok(())
360    }
361
362    fn start_prep(&self) -> Result<ValidatorNewData> {
363        let mut data = self.data.lock();
364
365        if !data.is_state(AmareleoApiState::Init) && !data.is_state(AmareleoApiState::Stopped) {
366            bail!("Invalid node state {:?}", data.state);
367        }
368
369        let ledger_path = data.get_ledger_folder()?;
370        let storage_mode = amareleo_storage_mode(ledger_path.clone());
371
372        data.trace_init()?;
373
374        if !data.keep_state {
375            // Clean the temporary ledger.
376            clean_tmp_ledger(ledger_path)?;
377        }
378        data.prev_state = data.state;
379        data.state = AmareleoApiState::StartPending;
380
381        Ok(ValidatorNewData {
382            network_id: data.network_id,
383            rest_ip: data.rest_ip,
384            rest_rps: data.rest_rps,
385            keep_state: data.keep_state,
386            storage_mode: storage_mode.clone(),
387            tracing: data.tracing.clone(),
388            shutdown: data.shutdown.clone(),
389        })
390    }
391
392    fn start_complete(&self, validator: ValidatorApi) {
393        let mut data = self.data.lock();
394        data.validator = validator;
395        data.prev_state = data.state;
396        data.state = AmareleoApiState::Started;
397    }
398
399    fn start_revert(&self) {
400        let mut data = self.data.lock();
401        data.state = data.prev_state;
402    }
403}
404
405// AmareleoApi node stop operation
406impl AmareleoApi {
407    /// Stop node instance
408    ///
409    /// Method should be called when node is in the `Started` state.<br>
410    /// If the node is in any other state, the method fails.
411    pub async fn end(&self) -> Result<()> {
412        // The state rules are enforced within end_prep/end_complete
413        // Sandwiching validator::shut_down between them, secures
414        // against concurrency.
415        let validator_ = self.end_prep()?;
416        validator_.shut_down().await;
417        self.end_complete();
418        Ok(())
419    }
420
421    fn end_prep(&self) -> Result<ValidatorApi> {
422        let mut data = self.data.lock();
423
424        if !data.is_state(AmareleoApiState::Started) {
425            bail!("Invalid node state {:?}", data.state);
426        }
427        data.prev_state = data.state;
428        data.state = AmareleoApiState::StopPending;
429
430        Ok(data.validator.clone())
431    }
432
433    fn end_complete(&self) {
434        let mut data = self.data.lock();
435
436        data.validator = ValidatorApi::None;
437        data.prev_state = data.state;
438        data.state = AmareleoApiState::Stopped;
439    }
440}