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}