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}