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