explorerd/store/metrics.rs
1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::{
20 fmt,
21 sync::{Arc, Mutex, MutexGuard},
22};
23
24use log::{debug, info};
25use sled_overlay::{sled, SledDbOverlay};
26
27use darkfi::{
28 blockchain::SledDbOverlayPtr,
29 util::time::{DateTime, Timestamp},
30 validator::fees::GasData,
31 Error, Result,
32};
33use darkfi_sdk::{num_traits::ToBytes, tx::TransactionHash};
34use darkfi_serial::{async_trait, deserialize, serialize, SerialDecodable, SerialEncodable};
35
36/// Gas metrics tree name.
37pub const SLED_GAS_METRICS_TREE: &[u8] = b"_gas_metrics";
38
39/// Gas metrics `by_height` tree that contains all metrics by height.
40pub const SLED_GAS_METRICS_BY_HEIGHT_TREE: &[u8] = b"_gas_metrics_by_height";
41
42/// Transaction gas data tree name.
43pub const SLED_TX_GAS_DATA_TREE: &[u8] = b"_tx_gas_data";
44
45/// The time interval for [`GasMetricsKey`]s in the main tree, specified in seconds.
46/// Metrics are stored in hourly intervals (3600 seconds), meaning all metrics accumulated
47/// within a specific hour are stored using a key representing the start of that hour.
48pub const GAS_METRICS_KEY_TIME_INTERVAL: u64 = 3600;
49
50#[derive(Debug, Clone, Default, Eq, PartialEq, SerialEncodable, SerialDecodable)]
51/// Represents metrics used to capture key statistical data.
52pub struct Metrics {
53 /// An aggregate value that represents the sum of the metrics.
54 pub sum: u64,
55 /// The smallest value in the series of measured metrics.
56 pub min: u64,
57 /// The largest value in the series of measured metrics.
58 pub max: u64,
59}
60
61// Temporarily disable unused warnings until the store is integrated with the explorer
62#[allow(dead_code)]
63impl Metrics {
64 /// Constructs a [`Metrics`] instance with provided parameters.
65 pub fn new(sum: u64, min: u64, max: u64) -> Self {
66 Self { sum, min, max }
67 }
68}
69
70/// Structure for managing gas metrics across all transactions in the store.
71///
72/// This struct maintains running totals, extrema, and transaction counts to efficiently calculate
73/// metrics without the need to iterate through previous transactions when new data is added. It is used to build a
74/// comprehensive view of gas metrics across the blockchain's history, including total gas, WASM gas,
75/// ZK circuit gas, and signature gas. The structure allows for O(1) performance in calculating
76/// averages and updating min/max values.
77#[derive(Clone, Default, Eq, PartialEq, SerialEncodable, SerialDecodable)]
78pub struct GasMetrics {
79 /// Represents the total count of transactions tracked by the metrics store.
80 pub txs_count: u64,
81 /// Overall gas consumed metrics across all transactions.
82 pub total_gas: Metrics,
83 /// Gas used across all executed wasm transactions.
84 pub wasm_gas: Metrics,
85 /// Gas consumed across all zk circuit computations.
86 pub zk_circuits_gas: Metrics,
87 /// Gas used metrics related to signatures across transactions.
88 pub signatures_gas: Metrics,
89 /// Gas consumed for deployments across transactions.
90 pub deployments_gas: Metrics,
91 /// The time the metrics was calculated
92 pub timestamp: Timestamp,
93}
94
95// Temporarily disable unused warnings until the store is integrated with the explorer
96#[allow(dead_code)]
97impl GasMetrics {
98 /// Creates a [`GasMetrics`] instance.
99 pub fn new(
100 txs_count: u64,
101 total_gas: Metrics,
102 wasm_gas: Metrics,
103 zk_circuit_gas: Metrics,
104 signature_gas: Metrics,
105 deployment_gas: Metrics,
106 timestamp: Timestamp,
107 ) -> Self {
108 Self {
109 txs_count,
110 total_gas,
111 wasm_gas,
112 zk_circuits_gas: zk_circuit_gas,
113 signatures_gas: signature_gas,
114 deployments_gas: deployment_gas,
115 timestamp,
116 }
117 }
118
119 /// Provides the average of the total gas used.
120 pub fn avg_total_gas_used(&self) -> u64 {
121 self.total_gas.sum.checked_div(self.txs_count).unwrap_or_default()
122 }
123
124 /// Provides the average of the gas used across WASM transactions.
125 pub fn avg_wasm_gas_used(&self) -> u64 {
126 self.wasm_gas.sum.checked_div(self.txs_count).unwrap_or_default()
127 }
128
129 /// Provides the average of the gas consumed across Zero-Knowledge Circuit computations.
130 pub fn avg_zk_circuits_gas_used(&self) -> u64 {
131 self.zk_circuits_gas.sum.checked_div(self.txs_count).unwrap_or_default()
132 }
133
134 /// Provides the average of the gas used to sign transactions.
135 pub fn avg_signatures_gas_used(&self) -> u64 {
136 self.signatures_gas.sum.checked_div(self.txs_count).unwrap_or_default()
137 }
138
139 /// Provides the average of the gas used for deployments.
140 pub fn avg_deployments_gas_used(&self) -> u64 {
141 self.deployments_gas.sum.checked_div(self.txs_count).unwrap_or_default()
142 }
143
144 /// Adds new [`GasData`] to the existing accumulated values.
145 ///
146 /// This method updates running totals, transaction counts, and min/max values
147 /// for various gas metric categories. It accumulates new data without reading existing
148 /// averages, minimums, or maximums from the database to optimize performance.
149 pub fn add(&mut self, tx_gas_data: &[GasData]) {
150 for gas_data in tx_gas_data {
151 // Increment number of transactions included in stats
152 self.txs_count += 1;
153
154 // Update the statistics related to total gas
155 self.total_gas.sum += gas_data.total_gas_used();
156
157 // Update the statistics related to WASM gas
158 self.wasm_gas.sum += gas_data.wasm;
159
160 // Update the statistics related to ZK circuit gas
161 self.zk_circuits_gas.sum += gas_data.zk_circuits;
162
163 // Update the statistics related to signature gas
164 self.signatures_gas.sum += gas_data.signatures;
165
166 // Update the statistics related to deployment gas
167 self.deployments_gas.sum += gas_data.deployments;
168
169 if self.txs_count == 1 {
170 // For the first transaction, set min/max to the transaction values
171 self.total_gas.min = gas_data.total_gas_used();
172 self.total_gas.max = gas_data.total_gas_used();
173 self.wasm_gas.min = gas_data.wasm;
174 self.wasm_gas.max = gas_data.wasm;
175 self.zk_circuits_gas.min = gas_data.zk_circuits;
176 self.zk_circuits_gas.max = gas_data.zk_circuits;
177 self.signatures_gas.min = gas_data.signatures;
178 self.signatures_gas.max = gas_data.signatures;
179 self.deployments_gas.min = gas_data.deployments;
180 self.deployments_gas.max = gas_data.deployments;
181 return;
182 }
183
184 // For subsequent transactions, compare with min/max
185 self.total_gas.min = self.total_gas.min.min(gas_data.total_gas_used());
186 self.total_gas.max = self.total_gas.max.max(gas_data.total_gas_used());
187 self.wasm_gas.min = self.wasm_gas.min.min(gas_data.wasm);
188 self.wasm_gas.max = self.wasm_gas.max.max(gas_data.wasm);
189 self.zk_circuits_gas.min = self.zk_circuits_gas.min.min(gas_data.zk_circuits);
190 self.zk_circuits_gas.max = self.zk_circuits_gas.max.max(gas_data.zk_circuits);
191 self.signatures_gas.min = self.signatures_gas.min.min(gas_data.signatures);
192 self.signatures_gas.max = self.signatures_gas.max.max(gas_data.signatures);
193 self.deployments_gas.min = self.deployments_gas.min.min(gas_data.deployments);
194 self.deployments_gas.max = self.deployments_gas.max.max(gas_data.deployments);
195 }
196 }
197}
198
199/// Debug formatting support for [`GasMetrics`] instances to include averages.
200impl fmt::Debug for GasMetrics {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 f.debug_struct("GasMetrics")
203 .field("txs_count", &self.txs_count)
204 .field("avg_total_gas_used", &self.avg_total_gas_used())
205 .field("avg_wasm_gas_used", &self.avg_wasm_gas_used())
206 .field("avg_zk_circuits_gas_used", &self.avg_zk_circuits_gas_used())
207 .field("avg_signatures_gas_used", &self.avg_signatures_gas_used())
208 .field("avg_deployments_gas_used", &self.avg_deployments_gas_used())
209 .field("total_gas", &format_args!("{:?}", self.total_gas))
210 .field("wasm_gas", &format_args!("{:?}", self.wasm_gas))
211 .field("zk_circuits_gas", &format_args!("{:?}", self.zk_circuits_gas))
212 .field("signatures_gas", &format_args!("{:?}", self.signatures_gas))
213 .field("deployments_gas", &format_args!("{:?}", self.deployments_gas))
214 .field("timestamp", &self.timestamp)
215 .finish()
216 }
217}
218
219/// The `MetricStore` serves as the entry point for managing metrics,
220/// offering an API for fetching, inserting, and resetting metrics backed by a Sled database.
221///
222/// It organizes data into separate Sled trees, including main storage for gas metrics by a defined time interval,
223/// a tree containing metrics by height for handling reorgs, and a transaction-specific gas data tree.
224/// Different keys, such as gas metric keys, block heights, and transaction hashes, are used to handle
225/// various use cases.
226///
227/// The `MetricStore` utilizes an overlay pattern for write operations, allowing unified management of metrics,
228/// by internally delegating write-related actions like adding metrics and handling reorgs to [`MetricsStoreOverlay`].
229#[derive(Clone)]
230pub struct MetricsStore {
231 /// Pointer to the underlying sled database used by the store and its associated overlay
232 pub sled_db: sled::Db,
233
234 /// Primary sled tree for storing gas metrics, utilizing [`GasMetricsKey`] as keys and
235 /// serialized [`GasMetrics`] as values.
236 pub main: sled::Tree,
237
238 /// Sled tree for storing gas metrics by height, utilizing block `height` as keys
239 /// and serialized [`GasMetrics`] as values.
240 pub by_height: sled::Tree,
241
242 /// Sled tree for storing transaction gas data, utilizing [`TransactionHash`] inner value as keys
243 /// and serialized [`GasData`] as values.
244 pub tx_gas_data: sled::Tree,
245}
246
247// Temporarily disable unused warnings until the store is integrated with the explorer
248#[allow(dead_code)]
249impl MetricsStore {
250 /// Creates a [`MetricsStore`] instance by opening the necessary trees in the provided sled database [`Db`]
251 pub fn new(db: &sled::Db) -> Result<Self> {
252 let main = db.open_tree(SLED_GAS_METRICS_TREE)?;
253 let tx_gas_data = db.open_tree(SLED_TX_GAS_DATA_TREE)?;
254 let metrcs_by_height = db.open_tree(SLED_GAS_METRICS_BY_HEIGHT_TREE)?;
255
256 Ok(Self { sled_db: db.clone(), main, tx_gas_data, by_height: metrcs_by_height })
257 }
258
259 /// Fetches [`GasMetrics`]s associated with the provided slice of [`GasMetricsKey`]s.
260 pub fn get(&self, keys: &[GasMetricsKey]) -> Result<Vec<GasMetrics>> {
261 let mut ret = Vec::with_capacity(keys.len());
262 for key in keys {
263 if let Some(metrics_bytes) = self.main.get(key.to_sled_key())? {
264 let metrics = deserialize(&metrics_bytes).map_err(Error::from)?;
265 ret.push(metrics);
266 }
267 }
268 Ok(ret)
269 }
270
271 /// Fetches [`GasMetrics`]s associated with the provided slice of [`u32`] heights.
272 pub fn get_by_height(&self, heights: &[u32]) -> Result<Vec<GasMetrics>> {
273 let mut ret = Vec::with_capacity(heights.len());
274 for height in heights {
275 if let Some(metrics_bytes) = self.by_height.get(height.to_be_bytes())? {
276 let metrics = deserialize(&metrics_bytes).map_err(Error::from)?;
277 ret.push(metrics);
278 }
279 }
280 Ok(ret)
281 }
282
283 /// Fetches the most recent [`GasMetrics`] and its associated [`GasMetricsKey`] from the main tree,
284 /// returning `None` if no metrics are found.
285 pub fn get_last(&self) -> Result<Option<(GasMetricsKey, GasMetrics)>> {
286 self.main
287 .last()?
288 .map(|(key_bytes, metrics_bytes)| {
289 // Deserialize gas metrics key and value
290 let key = GasMetricsKey::from_sled_key(&key_bytes)?;
291 let metrics: GasMetrics = deserialize(&metrics_bytes).map_err(Error::from)?;
292 debug!(target: "explorerd::metrics_store::get_last", "Deserialized metrics at key {key}: {metrics:?}");
293 Ok((key, metrics))
294 })
295 .transpose()
296 }
297
298 /// Fetches all [`GasMetrics`] from the main tree without corresponding key, returning an empty `Vec`
299 /// if no metrics are found.
300 pub fn get_all_metrics(&self) -> Result<Vec<GasMetrics>> {
301 // Iterate through all metrics, deserialize each one, and collect results
302 self.main
303 .iter()
304 .map(|iter_result| match iter_result {
305 Ok((_, metrics_bytes)) => deserialize(&metrics_bytes).map_err(Error::from),
306 Err(e) => Err(Error::from(e)),
307 })
308 .collect()
309 }
310
311 /// Fetches the most recent [`GasMetrics`] and its associated `height` from the `by_height` tree, returning `None` if no metrics are found.
312 pub fn get_last_by_height(&self) -> Result<Option<(u32, GasMetrics)>> {
313 self.by_height
314 .last()?
315 .map(|(height_bytes, metrics_bytes)| {
316 // Deserialize height key and value
317 let key_bytes: [u8; 4] = height_bytes.as_ref().try_into().unwrap();
318 let height = u32::from_be_bytes(key_bytes);
319 let metrics: GasMetrics = deserialize(&metrics_bytes).map_err(Error::from)?;
320 debug!(target: "explorerd::metrics_store::get_last_by_height", "Deserialized metrics at height {height:?}: {metrics:?}");
321 Ok((height, metrics))
322 })
323 .transpose()
324 }
325
326 /// Fetches the [`GasData`] associated with the provided [`TransactionHash`], or `None` if no gas data is found.
327 pub fn get_tx_gas_data(&self, tx_hash: &TransactionHash) -> Result<Option<GasData>> {
328 // Query transaction gas data tree using provided hash
329 let opt = self.tx_gas_data.get(tx_hash.inner())?;
330
331 // Deserialize gas data, map error if needed, return result
332 opt.map(|value| deserialize(&value).map_err(Error::from)).transpose()
333 }
334
335 /// Adds gas metrics for a specific block of transactions to the store.
336 ///
337 /// This function takes block `height`, [`Timestamp`], with associated pairs of [`TransactionHash`] and [`GasData`],
338 /// and updates the accumulated gas metrics in the store. It handles the storage of metrics for both regular use and
339 /// blockchain reorganizations.
340 ///
341 /// Delegates operation to [`MetricsStoreOverlay::insert_gas_metrics`], whose documentation
342 /// provides more details.
343 pub fn insert_gas_metrics(
344 &self,
345 block_height: u32,
346 block_timestamp: &Timestamp,
347 tx_hashes: &[TransactionHash],
348 tx_gas_data: &[GasData],
349 ) -> Result<GasMetricsKey> {
350 let overlay = MetricsStoreOverlay::new(self.sled_db.clone())?;
351 overlay.insert_gas_metrics(block_height, block_timestamp, tx_hashes, tx_gas_data)
352 }
353
354 /// Resets the gas metrics in the store to a specified `height` [`u32`].
355 ///
356 /// This function reverts all gas metrics data after the given height, effectively
357 /// undoing changes made beyond that point. It's useful for handling blockchain
358 /// reorganizations.
359 ///
360 /// Delegates operation to [`MetricsStoreOverlay::reset_gas_metrics`], whose documentation
361 /// provides more details.
362 pub fn reset_gas_metrics(&self, height: u32) -> Result<()> {
363 let overlay = MetricsStoreOverlay::new(self.sled_db.clone())?;
364 overlay.reset_gas_metrics(height)
365 }
366
367 /// Checks if provided [`GasMetricsKey`] exists in the store's main tree.
368 pub fn contains(&self, key: &GasMetricsKey) -> Result<bool> {
369 Ok(self.main.contains_key(key.to_sled_key())?)
370 }
371
372 /// Provides the number of stored metrics in the main tree.
373 pub fn len(&self) -> usize {
374 self.main.len()
375 }
376
377 /// Provides the number of stored metrics by height.
378 pub fn len_by_height(&self) -> usize {
379 self.by_height.len()
380 }
381
382 /// Returns the number of transaction gas usage metrics stored.
383 pub fn len_tx_gas_data(&self) -> usize {
384 self.tx_gas_data.len()
385 }
386
387 /// Checks if there are any gas metrics stored.
388 pub fn is_empty(&self) -> bool {
389 self.main.is_empty()
390 }
391
392 /// Checks if transaction gas data metrics are stored.
393 pub fn is_empty_tx_gas_data(&self) -> bool {
394 self.tx_gas_data.is_empty()
395 }
396}
397
398/// The `MetricsStoreOverlay` provides write operations for managing metrics in conjunction with the
399/// underlying sled database. It supports inserting new [`GasData`] into the stored accumulated metrics,
400/// adding transaction gas data, and reverting metric changes after a specified height.
401struct MetricsStoreOverlay {
402 /// Pointer to the overlay used for accessing and performing database write operations to the store.
403 overlay: SledDbOverlayPtr,
404 /// Pointer managed by the [`MetricsStore`] that references the sled instance on which the overlay operates.
405 db: sled::Db,
406}
407
408impl MetricsStoreOverlay {
409 /// Instantiate a [`MetricsStoreOverlay`] over the provided [`SledDbPtr`] instance.
410 pub fn new(db: sled::Db) -> Result<Self> {
411 // Create overlay pointer
412 let overlay = Arc::new(Mutex::new(SledDbOverlay::new(&db, vec![])));
413
414 // Open trees
415 overlay.lock().unwrap().open_tree(SLED_GAS_METRICS_TREE, true)?;
416 overlay.lock().unwrap().open_tree(SLED_GAS_METRICS_BY_HEIGHT_TREE, true)?;
417 overlay.lock().unwrap().open_tree(SLED_TX_GAS_DATA_TREE, true)?;
418
419 Ok(Self { overlay: overlay.clone(), db })
420 }
421
422 /// Adds the provided [`TransactionHash`] and [`GasData`] pairs to the accumulated [`GasMetrics`]
423 /// in the store's [`SLED_GAS_METRICS_BY_HEIGHT_TREE`] and [`SLED_GAS_METRICS_TREE`] trees, while
424 /// also storing transaction gas data in the [`SLED_TX_GAS_DATA_TREE`], committing all changes upon success.
425 ///
426 /// This function retrieves the latest recorded metrics, updates them with the new gas data, and
427 /// stores the accumulated result. It uses the provided `block_timestamp` to create a normalied time-sequenced
428 /// [`GasMetricsKey`] for metrics storage. The `block_height` is used as a key to store metrics by height
429 /// which are used to handle chain reorganizations. After updating the aggregate metrics, it stores
430 /// the transaction gas data for each transaction in the block.
431 ///
432 /// Returns the created [`GasMetricsKey`] that can be used to retrieve the metric upon success.
433 pub fn insert_gas_metrics(
434 &self,
435 block_height: u32,
436 block_timestamp: &Timestamp,
437 tx_hashes: &[TransactionHash],
438 tx_gas_data: &[GasData],
439 ) -> Result<GasMetricsKey> {
440 // Ensure lengths of tx_hashes and gas_data arrays match
441 if tx_hashes.len() != tx_gas_data.len() {
442 return Err(Error::Custom(String::from(
443 "The lengths of tx_hashes and gas_data arrays must match",
444 )));
445 }
446
447 // Ensure gas data is provided
448 if tx_gas_data.is_empty() {
449 return Err(Error::Custom(String::from("No transaction gas data was provided")));
450 }
451
452 // Lock the database
453 let mut lock = self.overlay.lock().unwrap();
454
455 // Retrieve latest recorded metrics, returning default if not exist
456 let mut metrics = match self.get_last_by_height(&mut lock)? {
457 None => GasMetrics::default(),
458 Some((_, metrics)) => metrics,
459 };
460
461 // Update the accumulated metrics with the provided transaction gas data
462 metrics.add(tx_gas_data);
463
464 // Update the time that the metrics was recorded
465 metrics.timestamp = *block_timestamp;
466
467 // Insert metrics by height
468 self.insert_by_height(&[block_height], &[metrics.clone()], &mut lock)?;
469
470 // Create metrics key based on block_timestamp
471 let metrics_key = GasMetricsKey::new(block_timestamp)?;
472
473 // Normalize metric timestamp based on the key's time interval
474 metrics.timestamp = GasMetricsKey::normalize_timestamp(block_timestamp)?;
475
476 // Insert the gas metrics using metrics key
477 self.insert(&[metrics_key.clone()], &[metrics], &mut lock)?;
478
479 // Insert the transaction gas data for each transaction in the block
480 self.insert_tx_gas_data(tx_hashes, tx_gas_data, &mut lock)?;
481
482 // Commit the changes
483 lock.apply()?;
484
485 Ok(metrics_key)
486 }
487
488 /// Inserts [`TransactionHash`] and [`GasData`] pairs into the store's [`SLED_TX_GAS_DATA_TREE`],
489 /// committing the changes upon success.
490 ///
491 /// This function locks the overlay, verifies that the tx_hashes and gas_data arrays have matching lengths,
492 /// then inserts them into the store while handling serialization and potential errors. Returns a
493 /// successful result upon success.
494 fn insert_tx_gas_data(
495 &self,
496 tx_hashes: &[TransactionHash],
497 gas_data: &[GasData],
498 lock: &mut MutexGuard<SledDbOverlay>,
499 ) -> Result<()> {
500 // Ensure lengths of tx_hashes and gas_data arrays match
501 if tx_hashes.len() != gas_data.len() {
502 return Err(Error::Custom(String::from(
503 "The lengths of tx_hashes and gas_data arrays must match",
504 )));
505 }
506
507 // Insert each transaction hash and gas data pair
508 for (tx_hash, gas_data) in tx_hashes.iter().zip(gas_data.iter()) {
509 // Serialize the gas data
510 let serialized_gas_data = serialize(gas_data);
511
512 // Insert serialized gas data
513 lock.insert(SLED_TX_GAS_DATA_TREE, tx_hash.inner(), &serialized_gas_data)?;
514 debug!(target: "explorerd::metrics_store::insert_tx_gas_data", "Inserted gas data for transaction {tx_hash}: {gas_data:?}");
515 }
516
517 Ok(())
518 }
519
520 /// Resets gas metrics in the [`SLED_GAS_METRICS_TREE`] and [`SLED_GAS_METRICS_BY_HEIGHT_TREE`]
521 /// to a specified block height, undoing all entries after provided height and committing the
522 /// changes upon success.
523 ///
524 /// This function first obtains a lock on the overlay, then reverts changes by calling
525 /// [`Self::revert_by_height_metrics`] and [`Self::revert_metrics`]. Upon successful revert,
526 /// all modifications made after the specified height are permanently reverted.
527 pub fn reset_gas_metrics(&self, height: u32) -> Result<()> {
528 // Obtain lock
529 let mut lock = self.overlay.lock().unwrap();
530
531 // Revert the metrics by height
532 self.revert_by_height_metrics(height, &mut lock)?;
533
534 // Revert the main metrics entries now that `by_height` tree is reset
535 self.revert_metrics(&mut lock)?;
536
537 // Commit the changes
538 lock.apply()?;
539
540 Ok(())
541 }
542
543 /// Inserts [`GasMetricsKey`] and [`GasMetrics`] pairs into the store's [`SLED_GAS_METRICS_TREE`].
544 ///
545 /// This function verifies that the provided keys and metrics arrays have matching lengths,
546 /// then inserts each pair while handling serialization. Returns a successful result
547 /// if all insertions are completed without errors.
548 fn insert(
549 &self,
550 keys: &[GasMetricsKey],
551 metrics: &[GasMetrics],
552 lock: &mut MutexGuard<SledDbOverlay>,
553 ) -> Result<()> {
554 // Ensure lengths of keys and metrics match
555 if keys.len() != metrics.len() {
556 return Err(Error::Custom(String::from(
557 "The lengths of keys and metrics arrays must match",
558 )));
559 }
560
561 // Insert each metric corresponding to respective gas metrics key
562 for (key, metric) in keys.iter().zip(metrics.iter()) {
563 // Insert metric
564 lock.insert(SLED_GAS_METRICS_TREE, &key.to_sled_key(), &serialize(metric))?;
565 debug!(target: "explorerd::metrics_store::insert", "Added gas metrics using key {key}: {metric:?}");
566 }
567
568 Ok(())
569 }
570
571 /// Inserts provided [`u32`] height and [`GasMetrics`] pairs into the store's [`SLED_GAS_METRICS_BY_HEIGHT_TREE`].
572 ///
573 /// This function verifies matching lengths of provided heights and metrics arrays,
574 /// and inserts each pair while handling serialization and errors. Returns a successful result
575 /// if all insertions are completed without errors.
576 fn insert_by_height(
577 &self,
578 heights: &[u32],
579 metrics: &[GasMetrics],
580 lock: &mut MutexGuard<SledDbOverlay>,
581 ) -> Result<()> {
582 // Ensure lengths of heights and metrics match
583 if heights.len() != metrics.len() {
584 return Err(Error::Custom(String::from(
585 "The lengths of heights and metrics arrays must match",
586 )));
587 }
588
589 // Insert each metric corresponding to respective height
590 for (height, metric) in heights.iter().zip(metrics.iter()) {
591 // Serialize the metric and handle potential errors
592 let serialized_metric = serialize(metric);
593
594 // Insert the serialized metric
595 lock.insert(
596 SLED_GAS_METRICS_BY_HEIGHT_TREE,
597 &height.to_be_bytes(),
598 &serialized_metric,
599 )?;
600 debug!(target: "explorerd::metrics_store::insert_by_height", "Added gas metrics using height {height}: {metric:?}");
601 }
602
603 Ok(())
604 }
605
606 /// This function reverts gas metric entries in the [`SLED_GAS_METRICS_TREE`] to align
607 /// with the latest metrics state in the [`SLED_GAS_METRICS_BY_HEIGHT_TREE`].
608 ///
609 /// It first determines the target timestamp to revert to based on the latest entry
610 /// in the by_height tree timestamp. Then, it iteratively removes entries from the main metrics
611 /// tree that are newer than the target timestamp. Once all that is complete, it adds the latest
612 /// metrics by height to the main metrics tree, returning a successful result if revert processes
613 /// without error.
614 fn revert_metrics(&self, lock: &mut MutexGuard<SledDbOverlay>) -> Result<()> {
615 /*** Determine Metrics To Revert ***/
616
617 // Get the last metrics by height and determine the target timestamp to revert to
618 let latest_by_height = self.get_last_by_height(lock)?;
619 let target_timestamp = match &latest_by_height {
620 None => 0,
621 Some((_, metrics)) => GasMetricsKey::normalize_timestamp(&metrics.timestamp)?.inner(),
622 };
623
624 // Get the timestamp of the latest metrics entry in the metrics store
625 let mut current_timestamp = match self.get_last(lock)? {
626 None => return Ok(()),
627 Some((_, metrics)) => metrics.timestamp.inner(),
628 };
629
630 /*** Revert Main Tree Gas Metrics ***/
631
632 // Iterate through at most the total number of gas metric tree entries
633 for _ in 0..self.db.open_tree(SLED_GAS_METRICS_TREE)?.len() {
634 // Stop the loop if the current timestamp is less than or equal to the target timestamp,
635 // as there are no more entries to revert
636 if current_timestamp <= target_timestamp {
637 break;
638 }
639
640 // Create a `GasMetricsKey` for the current timestamp to locate the entry to be reverted.
641 let key_to_revert = GasMetricsKey::new(current_timestamp)?;
642
643 // Remove the corresponding entry from the gas metrics tree.
644 lock.remove(SLED_GAS_METRICS_TREE, &key_to_revert.to_sled_key())?;
645 info!(target: "explorerd:metrics_store:revert_metrics", "Successfully reverted metrics with key: {key_to_revert}");
646
647 // Move to the previous valid timestamp by subtracting the defined time interval
648 current_timestamp = current_timestamp.saturating_sub(GAS_METRICS_KEY_TIME_INTERVAL);
649 }
650
651 /*** Add the Latest Reverted Metrics To Main Tree ***/
652
653 // Retrieve the latest metrics from the `by_height` tree and normalize its timestamp so it can be added to the main tree.
654 // If there are no metrics in the `by_height` tree, we may have reset to 0, so return as there is nothing add.
655 let latest_metrics = match latest_by_height {
656 None => return Ok(()),
657 Some((_, mut metrics)) => {
658 metrics.timestamp = GasMetricsKey::normalize_timestamp(&metrics.timestamp)?;
659 metrics
660 }
661 };
662
663 // Add the latest metrics to the main tree based on latest reverted metrics by height
664 let gas_metrics_key = GasMetricsKey::new(&latest_metrics.timestamp)?;
665 self.insert(&[gas_metrics_key], &[latest_metrics], lock)?;
666
667 Ok(())
668 }
669
670 /// Reverts gas metric entries from [`SLED_GAS_METRICS_BY_HEIGHT_TREE`] to provided `height`.
671 ///
672 /// This function iterates through the entries in gas metrics by height tree and removes all entries
673 /// with heights greater than the specified `height`, effectively reverting all gas metrics beyond that point.
674 fn revert_by_height_metrics(
675 &self,
676 height: u32,
677 lock: &mut MutexGuard<SledDbOverlay>,
678 ) -> Result<()> {
679 // Retrieve the last stored block height
680 let (last_height, _) = match self.get_last_by_height(lock)? {
681 None => return Ok(()),
682 Some(v) => v,
683 };
684
685 // Return early if the requested height is after the last stored height
686 if height >= last_height {
687 return Ok(());
688 }
689
690 // Remove keys greater than `height`
691 while let Some((cur_height_bytes, _)) = lock.last(SLED_GAS_METRICS_BY_HEIGHT_TREE)? {
692 // Convert height bytes to u32
693 let cur_height = u32::from_be_bytes(cur_height_bytes.as_ref().try_into()?);
694
695 // Process all heights that are bigger than provided `height`
696 if cur_height <= height {
697 break;
698 }
699
700 // Remove height being reverted
701 lock.remove(SLED_GAS_METRICS_BY_HEIGHT_TREE, &cur_height_bytes)?;
702 info!(target: "explorerd:metrics_store:revert_by_height_metrics", "Successfully reverted metrics with height: {cur_height}");
703 }
704
705 Ok(())
706 }
707
708 /// Fetches the most recent gas metrics from [`SLED_GAS_METRICS_TREE`], returning an option
709 /// containing a metrics key [`GasMetricsKey`] and [`GasMetrics`] pair, or `None` if no metrics exist.
710 fn get_last(
711 &self,
712 lock: &mut MutexGuard<SledDbOverlay>,
713 ) -> Result<Option<(GasMetricsKey, GasMetrics)>> {
714 // Fetch and deserialize key and metric pair
715 lock.last(SLED_GAS_METRICS_TREE)?
716 .map(|(key_bytes, metrics_bytes)| {
717 // Deserialize the metrics key
718 let key = GasMetricsKey::from_sled_key(&key_bytes)?;
719 // Deserialize the stored gas metrics
720 let metrics: GasMetrics = deserialize(&metrics_bytes).map_err(Error::from)?;
721 Ok((key, metrics))
722 })
723 .transpose()
724 }
725
726 /// Fetches the most recent gas metrics from [`SLED_GAS_METRICS_BY_HEIGHT_TREE`], returning an option
727 /// containing a height [`u32`] and [`GasMetrics`] pair, or `None` if no metrics exist.
728 fn get_last_by_height(
729 &self,
730 lock: &mut MutexGuard<SledDbOverlay>,
731 ) -> Result<Option<(u32, GasMetrics)>> {
732 // Fetch and deserialize height and metric pair
733 lock.last(SLED_GAS_METRICS_BY_HEIGHT_TREE)?
734 .map(|(height_bytes, metrics_bytes)| {
735 // Deserialize the height
736 let key_bytes: [u8; 4] = height_bytes.as_ref().try_into().unwrap();
737 let height = u32::from_be_bytes(key_bytes);
738 // Deserialize the stored gas metrics
739 let metrics: GasMetrics = deserialize(&metrics_bytes).map_err(Error::from)?;
740 Ok((height, metrics))
741 })
742 .transpose()
743 }
744}
745
746/// Represents a key used to store and fetch metrics in the metrics store.
747///
748/// This struct provides methods for creating, serializing, and deserializing gas metrics keys.
749/// It supports creation from various time representations through the [`GasMetricsKeySource`] trait
750/// and offers conversion methods for use with a sled database.
751#[derive(Debug, Eq, PartialEq, Clone)]
752pub struct GasMetricsKey(pub DateTime);
753
754impl GasMetricsKey {
755 /// Creates a new [`GasMetricsKey`] from a source that implements [`GasMetricsKeySource`].
756 /// Depending on the use case, the key supports different input sources such as `Timestamp`, `u64` timestamp,
757 /// or `&str` timestamp to create the key.
758 pub fn new<T: GasMetricsKeySource>(source: T) -> Result<GasMetricsKey> {
759 source.to_key()
760 }
761
762 /// Gets the inner [`DateTime`] value.
763 pub fn inner(&self) -> &DateTime {
764 &self.0
765 }
766
767 /// Converts the [`GasMetricsKey`] into a key suitable for use with a sled database.
768 pub fn to_sled_key(&self) -> Vec<u8> {
769 // Create a new vector with a capacity of 28 bytes
770 let mut sled_key = Vec::with_capacity(28);
771
772 // Push the byte representations of each field into the vector
773 sled_key.extend_from_slice(&self.inner().year.to_be_bytes());
774 sled_key.extend_from_slice(&self.inner().month.to_be_bytes());
775 sled_key.extend_from_slice(&self.inner().day.to_be_bytes());
776 sled_key.extend_from_slice(&self.inner().hour.to_be_bytes());
777 sled_key.extend_from_slice(&self.inner().min.to_be_bytes());
778 sled_key.extend_from_slice(&self.inner().sec.to_be_bytes());
779 sled_key.extend_from_slice(&self.inner().nanos.to_be_bytes());
780
781 // Return sled key
782 sled_key
783 }
784
785 /// Converts a `sled` key into a [`GasMetricsKey`] by deserializing a slice of bytes.
786 pub fn from_sled_key(bytes: &[u8]) -> Result<Self> {
787 if bytes.len() != 28 {
788 return Err(Error::Custom(String::from("Invalid byte length for GasMetricsKey")));
789 }
790
791 // Deserialize byte representations into each field
792 let key = DateTime {
793 year: u32::from_be_bytes(bytes[0..4].try_into()?),
794 month: u32::from_be_bytes(bytes[4..8].try_into()?),
795 day: u32::from_be_bytes(bytes[8..12].try_into()?),
796 hour: u32::from_be_bytes(bytes[12..16].try_into()?),
797 min: u32::from_be_bytes(bytes[16..20].try_into()?),
798 sec: u32::from_be_bytes(bytes[20..24].try_into()?),
799 nanos: u32::from_be_bytes(bytes[24..28].try_into()?),
800 };
801
802 Ok(Self(key))
803 }
804
805 /// Normalizes the given [`DateTime`] to the start of hour.
806 pub fn normalize_date_time(date_time: DateTime) -> DateTime {
807 DateTime {
808 nanos: 0,
809 sec: 0,
810 min: 0,
811 hour: date_time.hour,
812 day: date_time.day,
813 month: date_time.month,
814 year: date_time.year,
815 }
816 }
817
818 /// Normalizes a given [`Timestamp`] to the start of the hour.
819 pub fn normalize_timestamp(timestamp: &Timestamp) -> Result<Timestamp> {
820 let remainder = timestamp.inner() % GAS_METRICS_KEY_TIME_INTERVAL;
821 timestamp.checked_sub(Timestamp::from_u64(remainder))
822 }
823}
824
825impl fmt::Display for GasMetricsKey {
826 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
827 write!(f, "{}", self.inner())
828 }
829}
830
831/// Provides a unified method for creating new instances of GasMetricKeys using
832/// various time representations: [`Timestamp`], `u64` timestamp, or `&str` timestamp.
833pub trait GasMetricsKeySource {
834 fn to_key(&self) -> Result<GasMetricsKey>;
835}
836
837/// Implements [`GasMetricsKeySource`] for &[`Timestamp`], converting it to a [`GasMetricsKey`].
838impl GasMetricsKeySource for &Timestamp {
839 fn to_key(&self) -> Result<GasMetricsKey> {
840 let date_time = DateTime::from_timestamp(self.inner(), 0);
841 Ok(GasMetricsKey(GasMetricsKey::normalize_date_time(date_time)))
842 }
843}
844
845/// Implements [`GasMetricsKeySource`] for `u64`, converting it to a [`GasMetricsKey`].
846impl GasMetricsKeySource for u64 {
847 fn to_key(&self) -> Result<GasMetricsKey> {
848 let date_time = DateTime::from_timestamp(*self, 0);
849 Ok(GasMetricsKey(GasMetricsKey::normalize_date_time(date_time)))
850 }
851}
852
853/// Implements [`GasMetricsKeySource`] for string slices, converting a `&str` in the `YYYY-MM-DD HH:mm:ss UTC` format
854/// to a [`GasMetricsKey`]. Returns an [`Error::ParseFailed`] error if the provided timestamp string slice is invalid.
855impl GasMetricsKeySource for &str {
856 fn to_key(&self) -> Result<GasMetricsKey> {
857 let date_time = DateTime::from_timestamp_str(self)?;
858 Ok(GasMetricsKey(GasMetricsKey::normalize_date_time(date_time)))
859 }
860}
861
862#[cfg(test)]
863/// This test module verifies the correct insertion, retrieval, and reset of metrics in the store.
864/// It covers adding metrics, searching metrics by time and transaction hash, and resetting metrics with specified heights.
865mod tests {
866
867 use darkfi::util::time::DateTime;
868 use std::{
869 str::FromStr,
870 time::{Duration, SystemTime, UNIX_EPOCH},
871 };
872 use structopt::lazy_static::lazy_static;
873
874 use super::*;
875 use crate::test_utils::init_logger;
876
877 /// Number of heights to simulate.
878 const HEIGHT: u32 = 10;
879
880 /// Fixed timestamp in seconds since UNIX epoch.
881 const FIXED_TIMESTAMP: u64 = 1732042800;
882
883 /// [`FIXED_TIMESTAMP`] timestamp as a string in UTC format.
884 const FIXED_TIMESTAMP_STR: &str = "2024-11-19T19:00:00";
885
886 lazy_static! {
887 /// Test transaction hash.
888 pub static ref TX_HASH: TransactionHash = TransactionHash::from_str(
889 "92225ff00a3755d8df93c626b59f6e36cf021d85ebccecdedc38f3f1890a15fc"
890 ).expect("Invalid transaction hash");
891 }
892 /// Tests inserting gas metrics, verifying the correctness of stored metrics.
893 #[test]
894 fn test_insert_gas_metrics() -> Result<()> {
895 // Declare constants used for test
896 const EXPECTED_HEIGHT: usize = HEIGHT as usize - 1;
897
898 // Setup test, returning initialized metrics store
899 let store = setup()?;
900
901 // Load test data into the store and get the expected metrics results
902 let test_data = load_random_metrics(&store, |_, _| {})?;
903
904 // Verify metrics were inserted with the expected counts
905 assert_eq!(store.len(), EXPECTED_HEIGHT);
906
907 // Process height 0 test data separately
908 let mut test_data_iter = test_data.iter();
909
910 // For height 0, confirm there are no metrics stored in the store
911 if let Some(test_data_height0) = test_data_iter.next() {
912 let actual_height0 = store.get(&[GasMetricsKey::new(&test_data_height0.timestamp)?])?;
913 assert!(
914 actual_height0.is_empty(),
915 "Timestamp associated with height 0 should not have any metrics stored"
916 );
917 }
918
919 // Process remaining test data, verifying that each stored metric matches expected results
920 for expected in test_data_iter {
921 let actual = store.get(&[GasMetricsKey::new(&expected.timestamp)?])?;
922 let expected_normalized = normalize_metrics_timestamp(expected)?;
923 assert_eq!(&expected_normalized, &actual[0]);
924 }
925
926 Ok(())
927 }
928
929 /// Tests inserting gas metrics into the `by_height` tree, verifying the correctness of stored metrics.
930 #[test]
931 fn test_insert_by_height_gas_metrics() -> Result<()> {
932 // Declare constants used for test
933 const EXPECTED_HEIGHT: usize = HEIGHT as usize - 1;
934
935 // Setup test, returning initialized metrics store
936 let store = setup()?;
937
938 // Load test data into the store and get the expected metrics results
939 let test_data = load_random_metrics(&store, |_, _| {})?;
940
941 // Verify metrics were inserted with the expected counts
942 assert_eq!(store.len(), EXPECTED_HEIGHT);
943
944 // For height 0, confirm there are no metrics stored in metrics by height
945 let actual_height0 = store.get_by_height(&[0])?;
946 assert!(actual_height0.is_empty(), "Height 0 should not have any metrics stored");
947
948 // Process remaining heights, verifying that each stored metric matches expected results
949 for (height, expected) in (1..).zip(test_data.iter().skip(1)) {
950 let actual = store.get_by_height(&[height])?;
951 assert!(!actual.is_empty(), "No metrics found for height {height}");
952 assert_eq!(expected, &actual[0]);
953 }
954
955 Ok(())
956 }
957
958 /// Tests searching gas metrics by the hour, verifying the correct metrics are found
959 /// and match expected values.
960 #[test]
961 fn test_search_metrics_by_hour() -> Result<()> {
962 // Setup test, returning initialized metrics store
963 let store = setup()?;
964
965 // Load test data, initializing expected with the fourth loaded record
966 let expected = &load_random_metrics(&store, |_, _| {})?[3];
967
968 // Create search criteria based on the expected timestamp value
969 let search_criteria = DateTime::from_timestamp(expected.timestamp.inner(), 0);
970
971 // Search metrics by the hour
972 let actual_opt = store.main.iter().find_map(|res| {
973 res.ok().and_then(|(k, v)| {
974 let key = GasMetricsKey::from_sled_key(&k).ok()?;
975 if key.inner().hour == search_criteria.hour {
976 deserialize::<GasMetrics>(&v).ok()
977 } else {
978 None
979 }
980 })
981 });
982
983 // Verify the found metrics match expected results
984 assert!(actual_opt.is_some());
985 assert_eq!(normalize_metrics_timestamp(expected)?, actual_opt.unwrap());
986
987 Ok(())
988 }
989
990 /// Tests fetching gas metrics by a timestamp string, verifying the retrieved metrics
991 /// match expected values.
992 #[test]
993 fn test_get_metrics_by_timestamp_str() -> Result<()> {
994 // Setup test, returning initialized metrics store
995 let store = setup()?;
996
997 // Load fixed data needed for test, initializing expected with the first loaded record
998 let (expected, _) = &load_fixed_metrics(&store)?[0];
999
1000 // Create gas metrics key using a test fixed timestamp
1001 let gas_metrics_key = GasMetricsKey::new(FIXED_TIMESTAMP_STR)?;
1002
1003 // Verify the key retrieves the correct metrics and matches the expected value
1004 let actual = store.get(&[gas_metrics_key])?;
1005 assert_eq!(expected, &actual[0]);
1006 Ok(())
1007 }
1008
1009 /// Tests the insertion and retrieval of transaction gas data in the store, verifying expected results.
1010 /// Additionally, it tests that transactions not found in the store correctly return a `None` result.
1011 #[test]
1012 fn test_tx_gas_data() -> Result<()> {
1013 let tx_hash_not_found: TransactionHash = TransactionHash::from_str(
1014 "93325ff00a3755d8df93c626b59f6e36cf021d85ebccecdedc38f3f1890a15fc",
1015 )
1016 .expect("Invalid hash");
1017
1018 // Setup test, returning initialized metrics store
1019 let store = setup()?;
1020 // Load data needed for test, initializing expected with the first loaded record
1021 let (_, expected) = &load_fixed_metrics(&store)?[0];
1022
1023 // Verify that existing transaction is found
1024 let actual_opt = store.get_tx_gas_data(&TX_HASH)?;
1025 assert!(actual_opt.is_some());
1026 assert_eq!(*expected, actual_opt.unwrap());
1027
1028 // Verify that transactions that do not exist return None result
1029 let actual_not_found = store.get_tx_gas_data(&tx_hash_not_found)?;
1030 assert_eq!(None, actual_not_found);
1031
1032 Ok(())
1033 }
1034
1035 /// Tests resetting gas metrics within a specified height range, verifying that both the `by_height` and `main` trees
1036 /// are properly set to the reset height.
1037 #[test]
1038 fn test_reset_metrics_within_height_range() -> Result<()> {
1039 // Declare constants used for test
1040 const RESET_HEIGHT: u32 = 6;
1041
1042 // Setup test, returning initialized metrics store
1043 let store = setup()?;
1044
1045 // Load test data into the store and get the expected reset metrics result
1046 let expected = load_reset_metrics(&store, RESET_HEIGHT)?;
1047
1048 // Reset metrics
1049 store.reset_gas_metrics(RESET_HEIGHT)?;
1050
1051 // Fetch reset metrics by height
1052 let actual_by_height_opt = store.get_last_by_height()?;
1053 assert!(actual_by_height_opt.is_some(), "Expected get_last_by_height to return metrics");
1054
1055 // Verify metrics by height are properly reset
1056 let (_, actual_by_height) = actual_by_height_opt.unwrap();
1057 assert_eq!(&expected, &actual_by_height);
1058
1059 // Fetch reset main metrics
1060 let actual_main_opt = store.get_last()?;
1061 assert!(actual_main_opt.is_some(), "Expected get_last to return metrics");
1062
1063 // Verify main metrics are properly reset
1064 let (_, actual_main_metrics) = actual_main_opt.unwrap();
1065 assert_eq!(&normalize_metrics_timestamp(&expected)?, &actual_main_metrics);
1066
1067 Ok(())
1068 }
1069
1070 /// Tests resetting the metrics store to height 0, ensuring it handles the operation gracefully without errors
1071 /// and verifies that no metrics remain in the store afterward.
1072 #[test]
1073 fn test_reset_metrics_height_to_0() -> Result<()> {
1074 // Declare constants used for test
1075 const RESET_HEIGHT: u32 = 0;
1076 const EXPECTED_RESET_HEIGHT: usize = 0;
1077
1078 // Setup test, returning initialized metrics store
1079 let store = setup()?;
1080
1081 // Load reset test data needed for test
1082 _ = load_reset_metrics(&store, RESET_HEIGHT)?;
1083
1084 // Reset metrics
1085 store.reset_gas_metrics(RESET_HEIGHT)?;
1086
1087 // Verify metrics were reset with the expected counts
1088 assert_eq!(store.len_by_height(), EXPECTED_RESET_HEIGHT);
1089 assert_eq!(store.len(), EXPECTED_RESET_HEIGHT);
1090
1091 // Verify metrics by height are empty
1092 let actual_by_height_opt = store.get_last_by_height()?;
1093 assert!(actual_by_height_opt.is_none(), "Expected None from get_last_by_height");
1094
1095 // Confirm main metrics are empty
1096 let actual_main_opt = store.get_last()?;
1097 assert!(actual_main_opt.is_none(), "Expected None from get_last");
1098
1099 Ok(())
1100 }
1101
1102 /// Tests that resetting beyond the number of available metrics does not change
1103 /// the store and no errors are thrown since there are no metrics to reset.
1104 #[test]
1105 fn test_reset_metrics_beyond_height() -> Result<()> {
1106 // Declare constants used for test
1107 const RESET_HEIGHT: u32 = HEIGHT + 1;
1108 const EXPECTED_RESET_HEIGHT: usize = HEIGHT as usize - 1;
1109
1110 // Setup test, returning initialized metrics store
1111 let store = setup()?;
1112
1113 // Load reset test data needed for test, storing the expected result
1114 let expected = load_reset_metrics(&store, RESET_HEIGHT)?;
1115
1116 // Reset metrics to given height
1117 store.reset_gas_metrics(RESET_HEIGHT)?;
1118
1119 // Verify metrics were reset with the expected counts
1120 assert_eq!(store.len_by_height(), EXPECTED_RESET_HEIGHT);
1121 assert_eq!(store.len(), EXPECTED_RESET_HEIGHT);
1122
1123 // Verify that the last record for metrics by height is correctly reset
1124 let actual_by_height_opt = store.get_last_by_height()?;
1125 assert!(actual_by_height_opt.is_some(), "Expected get_last_by_height to return metrics");
1126 let (_, actual_by_height) = actual_by_height_opt.unwrap();
1127 assert_eq!(&expected, &actual_by_height);
1128
1129 // Verify that the last record for main metrics is correctly reset
1130 let actual_main_opt = store.get_last()?;
1131 assert!(actual_main_opt.is_some(), "Expected get_last to return metrics");
1132 let (_, actual_main) = actual_main_opt.unwrap();
1133 assert_eq!(&normalize_metrics_timestamp(&expected)?, &actual_main);
1134
1135 Ok(())
1136 }
1137
1138 /// Tests resetting metrics at the last available height to verify that the code
1139 /// can handle the boundary condition.
1140 #[test]
1141 fn test_reset_metrics_at_height() -> Result<()> {
1142 // Declare constants used for test
1143 const RESET_HEIGHT: u32 = HEIGHT;
1144 const EXPECTED_RESET_HEIGHT: usize = HEIGHT as usize - 1;
1145
1146 // Setup test, returning initialized metrics store
1147 let store = setup()?;
1148
1149 // Load reset test data needed for test
1150 let expected = load_reset_metrics(&store, RESET_HEIGHT)?;
1151
1152 // Reset metrics to given height
1153 store.reset_gas_metrics(RESET_HEIGHT)?;
1154
1155 // Verify metrics were reset with the expected counts
1156 assert_eq!(store.len_by_height(), EXPECTED_RESET_HEIGHT);
1157 assert_eq!(store.len(), EXPECTED_RESET_HEIGHT);
1158
1159 // Verify that the last record for metrics by height is correctly reset
1160 let actual_by_height_opt = store.get_last_by_height()?;
1161 assert!(actual_by_height_opt.is_some(), "Expected get_last_by_height to return metrics");
1162 let (_, actual_by_height) = actual_by_height_opt.unwrap();
1163 assert_eq!(&expected, &actual_by_height);
1164
1165 // Verify that the last record for main metrics is correctly reset
1166 let actual_main_opt = store.get_last()?;
1167 assert!(actual_main_opt.is_some(), "Expected get_last to return metrics");
1168 let (_, actual_main) = actual_main_opt.unwrap();
1169 assert_eq!(&normalize_metrics_timestamp(&expected)?, &actual_main);
1170
1171 Ok(())
1172 }
1173
1174 /// Tests that resetting an empty metrics store gracefully handles
1175 /// the operation without errors and ensures the store remains empty.
1176 #[test]
1177 fn test_reset_empty_store() -> Result<()> {
1178 const RESET_HEIGHT: u32 = 6;
1179
1180 // Setup test, returning initialized metrics store
1181 let store = setup()?;
1182
1183 // Reset metrics with an empty store
1184 store.reset_gas_metrics(RESET_HEIGHT)?;
1185
1186 // Verify no metrics with the expected counts
1187 assert_eq!(store.len_by_height(), 0);
1188 assert_eq!(store.len(), 0);
1189
1190 // Verify that metrics by height is empty
1191 let actual_by_height = store.get_last_by_height()?;
1192 assert!(actual_by_height.is_none(), "Expected get_last_by_height to return None");
1193
1194 // Verify main metrics is empty
1195 let actual_main = store.get_last()?;
1196 assert!(actual_main.is_none(), "Expected get_last to return None");
1197
1198 Ok(())
1199 }
1200
1201 /// Sets up a test case for metrics store testing by initializing the logger,
1202 /// creating a temporary database, and returning an initialized metrics store.
1203 fn setup() -> Result<MetricsStore> {
1204 // Initialize logger to show execution output
1205 init_logger(simplelog::LevelFilter::Off, vec!["sled", "runtime", "net"]);
1206
1207 // Create a temporary directory for the sled database
1208 let db =
1209 sled::Config::new().temporary(true).open().expect("Unable to open test sled database");
1210
1211 // Initialize the metrics store
1212 let metrics_store = MetricsStore::new(&db.clone())?;
1213
1214 Ok(metrics_store)
1215 }
1216
1217 /// Loads random test gas metrics data into the given metrics store, simulating height 0 as a
1218 /// genesis block with no metrics.
1219 ///
1220 /// Computes the starting block timestamp from the current system time for the first metric,
1221 /// then inserts each subsequent metric at intervals of [`GAS_METRICS_KEY_TIME_INTERVAL`],
1222 /// resulting in metrics being inserted one hour apart. The function iterates through a predefined
1223 /// height range, as defined by [`HEIGHT`], to accumulate and insert gas metrics. After each
1224 /// metric is stored, the `metric_loaded` closure is invoked, allowing the caller to perform
1225 /// specific actions as the data is loaded.
1226 ///
1227 /// NOTE: A fixed transaction hash is used to insert the metrics, as this test data is solely intended
1228 /// to validate gas metrics and not transaction-specific gas data.
1229 ///
1230 /// Upon success, it returns a list of snapshots of the accumulated metrics that were loaded.
1231 fn load_random_metrics<F>(
1232 metrics_store: &MetricsStore,
1233 mut metrics_loaded: F,
1234 ) -> Result<Vec<GasMetrics>>
1235 where
1236 F: FnMut(u32, &GasMetrics),
1237 {
1238 // Calculate the start block timestamp
1239 let start_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
1240
1241 // Define variables to store accumulated loaded data
1242 let mut accumulated_metrics = GasMetrics::default();
1243 let mut metrics = Vec::with_capacity(HEIGHT as usize);
1244
1245 // Iterate and load data
1246 for height in 0..HEIGHT {
1247 let timestamp_secs = (UNIX_EPOCH +
1248 Duration::from_secs(start_time + height as u64 * GAS_METRICS_KEY_TIME_INTERVAL))
1249 .duration_since(UNIX_EPOCH)?
1250 .as_secs();
1251
1252 // Initialize simulated block_timestamp
1253 let block_timestamp = Timestamp::from(timestamp_secs);
1254 accumulated_metrics.timestamp = block_timestamp;
1255
1256 // Simulate genesis block, metrics are stored after height 0
1257 if height > 0 {
1258 let tx_gas_data = random_gas_data(height as u64 + start_time);
1259 accumulated_metrics.add(&[tx_gas_data.clone()]);
1260 metrics_store.insert_gas_metrics(
1261 height,
1262 &block_timestamp,
1263 &[*TX_HASH],
1264 &[tx_gas_data],
1265 )?;
1266 }
1267
1268 // Invoke passed in metrics loaded closure
1269 metrics_loaded(height, &accumulated_metrics);
1270
1271 // Add a snapshot of the accumulated metrics
1272 metrics.push(accumulated_metrics.clone());
1273 }
1274
1275 Ok(metrics)
1276 }
1277
1278 /// Loads fixed test data into the metrics store using fixed timestamps,
1279 /// returning snapshots of accumulated [`GasMetrics`] with corresponding [`GasData`]
1280 /// used to update the metrics.
1281 ///
1282 /// Currently, this function only loads a single record but is designed to be extendable
1283 /// to insert additional records in the future without affecting the method's return signature,
1284 /// making it suitable for use in tests.
1285 fn load_fixed_metrics(metrics_store: &MetricsStore) -> Result<Vec<(GasMetrics, GasData)>> {
1286 // Convert the fixed timestamp constant to a Timestamp object
1287 let fixed_timestamp = Timestamp::from_u64(FIXED_TIMESTAMP);
1288
1289 // Initialize an empty GasMetrics object to accumulate the data
1290 let height: u32 = 1;
1291 let mut accumulated_metrics = GasMetrics::default();
1292 let mut metrics_vec = Vec::with_capacity(HEIGHT as usize);
1293
1294 // Initialize the block_timestamp using the fixed timestamp
1295 let block_timestamp = fixed_timestamp;
1296 accumulated_metrics.timestamp = block_timestamp;
1297
1298 // Generate random gas data for the given height
1299 let gas_data = random_gas_data(height as u64);
1300 accumulated_metrics.add(&[gas_data.clone()]);
1301
1302 // Insert the gas metrics into the metrics store
1303 metrics_store.insert_gas_metrics(
1304 height,
1305 &block_timestamp,
1306 &[*TX_HASH],
1307 &[gas_data.clone()],
1308 )?;
1309 metrics_vec.push((accumulated_metrics, gas_data));
1310
1311 Ok(metrics_vec)
1312 }
1313
1314 /// Loads reset test data into the store, returning the accumulated gas metrics at the specified reset height.
1315 fn load_reset_metrics(metrics_store: &MetricsStore, reset_height: u32) -> Result<GasMetrics> {
1316 let mut reset_metrics = GasMetrics::default();
1317
1318 // Load metrics, passing in a closure to store the reset metrics
1319 _ = load_random_metrics(metrics_store, |height, acc_metrics| {
1320 // Store accumulated metrics at reset height
1321 if reset_height == height || reset_height >= HEIGHT {
1322 reset_metrics = acc_metrics.clone();
1323 }
1324 })?;
1325
1326 Ok(reset_metrics)
1327 }
1328
1329 /// Generates random [`GasData`] based on the provided seed value, allowing for the simulation
1330 /// of varied gas data values.
1331 fn random_gas_data(seed: u64) -> GasData {
1332 /// Defines a limit for gas data values.
1333 const GAS_LIMIT: u64 = 100_000;
1334
1335 // Initialize gas usage with the provided seed
1336 let mut gas_used = seed;
1337
1338 // Closure to generate a random gas value
1339 let mut random_gas = || {
1340 // Introduce variability using the seed and current gas_used
1341 let variation = seed.wrapping_add(gas_used);
1342 gas_used = gas_used.wrapping_mul(6364136223846793005).wrapping_add(variation);
1343 gas_used
1344 };
1345
1346 // Create GasData with random values constrained by GAS_LIMIT
1347 GasData {
1348 paid: random_gas() % GAS_LIMIT,
1349 wasm: random_gas() % GAS_LIMIT,
1350 zk_circuits: random_gas() % GAS_LIMIT,
1351 signatures: random_gas() % GAS_LIMIT,
1352 deployments: random_gas() % GAS_LIMIT,
1353 }
1354 }
1355
1356 /// Normalizes the [`GasMetrics`] timestamp to the start of the hour for test comparisons.
1357 fn normalize_metrics_timestamp(metrics: &GasMetrics) -> Result<GasMetrics> {
1358 let mut normalized_metrics = metrics.clone();
1359 normalized_metrics.timestamp = GasMetricsKey::normalize_timestamp(&metrics.timestamp)?;
1360 Ok(normalized_metrics)
1361 }
1362}