Просмотр исходного кода

explorer: incorporate metrics into the explorer backend code

This commit integrates metrics capabilities into the explorer backend. The provided metrics include the average, minimum, and maximum values for the following:
- Total Gas Usage: Represents overall gas consumption.
- WASM Gas Usage: Pertains to gas usage for WASM contract calls.
- ZK Circuits Gas Usage: Indicates specific gas usage for ZK circuit operations.
- Signatures Gas Usage: Covers gas usage for transaction signatures.
- Deployments Gas Usage: Covers gas usage for transaction deployments.

Summary of Updates

Blocks Module:
- Updated `put_block` to store metrics for each transaction found in the block

Main Binary Crate:
- Added `calculate_tx_gas_data` to the `ExplorerDb` implementation, which calculates the gas data for a given transaction
- Introduced `get_latest_metrics`, which fetches the most current metrics from the metrics store
- Added code to deploy native contracts need to calculate transaction gas data

RPC Module:
- Added `statistics.get_metric_statistics` request to the RPC request handler

RPC Statistics Module:
- Implemented `statistics_get_metric_statistics` function that returns the latest metrics for the UI

Statistics Module:
- Introduced the `MetricStatistics` structure to represent gas data metrics in the service layer
- Implemented a constructor for `MetricStatistics` to initialize it with `GasMetrics`
- Updated the `to_json_array` method to convert `MetricStatistics` into a JSON array for UI data representation
- Implemented the `get_metrics_statistics` function in the `ExplorerDb` struct to fetch the most recent metrics from the database

Transaction Module:
- Added metrics and timestamp fields to the existing `TransactionRecord` struct
- Updated the `to_json_array` method of `TransactionRecord` to include conversions for metrics and timestamp
- Replaced the previous TransactionRecord From implementation with a ExplorerDb implementation `to_tx_record`, which converts a `Transaction` to a `TransactionRecord`
- Updated all uses of `TransactionRecord::from` to use `to_tx_record`
- Added auxiliary ExplorerDb implements to support metrics:
  - `get_tx_block_info`: Retrieves the `BlockInfo` associated with a given transaction hash
  - `get_block_info`: Fetches the `BlockInfo` associated with a given `HeaderHash`
kalm 1 год назад
Родитель
Сommit
81b6e2acac

+ 36 - 6
script/research/blockchain-explorer/src/blocks.rs

@@ -24,6 +24,7 @@ use darkfi::{
         BlockInfo, BlockchainOverlay, HeaderHash, SLED_BLOCK_DIFFICULTY_TREE,
         SLED_BLOCK_ORDER_TREE, SLED_BLOCK_TREE,
     },
+    util::time::Timestamp,
     Error, Result,
 };
 use darkfi_sdk::crypto::schnorr::Signature;
@@ -42,7 +43,7 @@ pub struct BlockRecord {
     /// Block height
     pub height: u32,
     /// Block creation timestamp
-    pub timestamp: u64,
+    pub timestamp: Timestamp,
     /// The block's nonce. This value changes arbitrarily with mining.
     pub nonce: u64,
     /// Merkle tree root of the transactions hashes contained in this block
@@ -59,7 +60,7 @@ impl BlockRecord {
             JsonValue::Number(self.version as f64),
             JsonValue::String(self.previous.clone()),
             JsonValue::Number(self.height as f64),
-            JsonValue::Number(self.timestamp as f64),
+            JsonValue::String(self.timestamp.to_string()),
             JsonValue::Number(self.nonce as f64),
             JsonValue::String(self.root.clone()),
             JsonValue::String(format!("{:?}", self.signature)),
@@ -74,7 +75,7 @@ impl From<&BlockInfo> for BlockRecord {
             version: block.header.version,
             previous: block.header.previous.to_string(),
             height: block.header.height,
-            timestamp: block.header.timestamp.inner(),
+            timestamp: block.header.timestamp,
             nonce: block.header.nonce,
             root: block.header.root.to_string(),
             signature: block.signature,
@@ -100,13 +101,42 @@ impl ExplorerDb {
         Ok(())
     }
 
-    /// Adds a block to the block explorer database.
+    /// Adds the provided [`BlockInfo`] to the block explorer database.
+    ///
+    /// This function processes each transaction in the block, calculating and updating the
+    /// latest [`GasMetrics`] for non-genesis blocks and for transactions that are not
+    /// PoW rewards. After processing all transactions, the block is permanently persisted to
+    /// the explorer database.
     pub async fn put_block(&self, block: &BlockInfo) -> Result<()> {
         let blockchain_overlay = BlockchainOverlay::new(&self.blockchain)?;
-        // Add the synced block and commit the changes
+
+        // Initialize collections to hold gas data and transactions that have gas data
+        let mut tx_gas_data = Vec::with_capacity(block.txs.len());
+        let mut txs_hashes_with_gas_data = Vec::with_capacity(block.txs.len());
+
+        // Calculate gas data for non-PoW reward transactions and non-genesis blocks
+        for (i, tx) in block.txs.iter().enumerate() {
+            if !tx.is_pow_reward() && block.header.height != 0 {
+                tx_gas_data.insert(i, self.calculate_tx_gas_data(tx, false).await?);
+                txs_hashes_with_gas_data.insert(i, tx.hash());
+            }
+        }
+
+        // If the block contains transaction gas data, insert the gas metrics into the metrics store
+        if !tx_gas_data.is_empty() {
+            self.metrics_store.insert_gas_metrics(
+                block.header.height,
+                &block.header.timestamp,
+                &txs_hashes_with_gas_data,
+                &tx_gas_data,
+            )?;
+        }
+
+        // Add the block and commit the changes to persist it
         let _ = blockchain_overlay.lock().unwrap().add_block(block)?;
         blockchain_overlay.lock().unwrap().overlay.lock().unwrap().apply()?;
-        debug!(target:"blockchain_explorer::blocks::put_block", "Added block {:?}", block);
+        debug!(target: "blockchain_explorer::blocks::put_block", "Added block {:?}", block);
+
         Ok(())
     }
 

+ 234 - 6
script/research/blockchain-explorer/src/main.rs

@@ -16,26 +16,45 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::{collections::HashSet, sync::Arc};
+use std::{
+    collections::{HashMap, HashSet},
+    sync::Arc,
+};
 
-use log::{error, info};
+use log::{debug, error, info};
 use sled_overlay::sled;
-use smol::{lock::Mutex, stream::StreamExt};
+use smol::{io::Cursor, lock::Mutex, stream::StreamExt};
 use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
 use url::Url;
 
 use darkfi::{
     async_daemonize,
-    blockchain::Blockchain,
+    blockchain::{Blockchain, BlockchainOverlay},
     cli_desc,
+    error::TxVerifyFailed,
     rpc::{
         client::RpcClient,
         server::{listen_and_serve, RequestHandler},
     },
+    runtime::vm_runtime::Runtime,
     system::{StoppableTask, StoppableTaskPtr},
+    tx::Transaction,
     util::path::expand_path,
+    validator::{
+        fees::{circuit_gas_use, GasData, PALLAS_SCHNORR_SIGNATURE_FEE},
+        utils::deploy_native_contracts,
+    },
+    zk::VerifyingKey,
     Error, Result,
 };
+use darkfi_sdk::{
+    crypto::{ContractId, PublicKey},
+    deploy::DeployParamsV1,
+    pasta::pallas,
+};
+use darkfi_serial::{deserialize_async, serialize_async, AsyncDecodable, AsyncEncodable};
+
+use crate::metrics_store::{GasMetrics, GasMetricsKey, MetricsStore};
 
 /// Crate errors
 mod error;
@@ -104,6 +123,8 @@ pub struct ExplorerDb {
     pub sled_db: sled::Db,
     /// Explorer darkfid blockchain copy
     pub blockchain: Blockchain,
+    /// Metrics store instance
+    pub metrics_store: MetricsStore,
 }
 
 impl ExplorerDb {
@@ -112,8 +133,204 @@ impl ExplorerDb {
         let db_path = expand_path(db_path.as_str())?;
         let sled_db = sled::open(&db_path)?;
         let blockchain = Blockchain::new(&sled_db)?;
+        let metrics_store = MetricsStore::new(&sled_db)?;
         info!(target: "blockchain-explorer", "Initialized explorer database {}, block count: {}", db_path.display(), blockchain.len());
-        Ok(ExplorerDb { sled_db, blockchain })
+        Ok(ExplorerDb { sled_db, blockchain, metrics_store })
+    }
+
+    /// Calculates the fee data for a given transaction, returning a [`GasData`] object detailing various aspects of the gas usage.
+    pub async fn calculate_tx_gas_data(
+        &self,
+        tx: &Transaction,
+        verify_fee: bool,
+    ) -> Result<GasData> {
+        let tx_hash = tx.hash();
+
+        let overlay = BlockchainOverlay::new(&self.blockchain)?;
+
+        // Gas accumulators
+        let mut total_gas_used = 0;
+        let mut zk_circuit_gas_used = 0;
+        let mut wasm_gas_used = 0;
+        let mut deploy_gas_used = 0;
+        let mut gas_paid = 0;
+
+        // Table of public inputs used for ZK proof verification
+        let mut zkp_table = vec![];
+        // Table of public keys used for signature verification
+        let mut sig_table = vec![];
+
+        // Index of the Fee-paying call
+        let fee_call_idx = 0;
+
+        // Map of ZK proof verifying keys for the transaction
+        let mut verifying_keys: HashMap<[u8; 32], HashMap<String, VerifyingKey>> = HashMap::new();
+        for call in &tx.calls {
+            verifying_keys.insert(call.data.contract_id.to_bytes(), HashMap::new());
+        }
+
+        let block_target = self.blockchain.blocks.get_last()?.0 + 1;
+
+        // We'll also take note of all the circuits in a Vec so we can calculate their verification cost.
+        let mut circuits_to_verify = vec![];
+
+        // Iterate over all calls to get the metadata
+        for (idx, call) in tx.calls.iter().enumerate() {
+            // Transaction must not contain a Money::PoWReward(0x02) call
+            if call.data.is_money_pow_reward() {
+                error!(target: "block_explorer::calculate_tx_gas_data", "Reward transaction detected");
+                return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
+            }
+
+            // Write the actual payload data
+            let mut payload = vec![];
+            tx.calls.encode_async(&mut payload).await?;
+
+            let wasm = overlay.lock().unwrap().contracts.get(call.data.contract_id)?;
+
+            let mut runtime = Runtime::new(
+                &wasm,
+                overlay.clone(),
+                call.data.contract_id,
+                block_target,
+                block_target,
+                tx_hash,
+                idx as u8,
+            )?;
+
+            let metadata = runtime.metadata(&payload)?;
+
+            // Decode the metadata retrieved from the execution
+            let mut decoder = Cursor::new(&metadata);
+
+            // The tuple is (zkas_ns, public_inputs)
+            let zkp_pub: Vec<(String, Vec<pallas::Base>)> =
+                AsyncDecodable::decode_async(&mut decoder).await?;
+            let sig_pub: Vec<PublicKey> = AsyncDecodable::decode_async(&mut decoder).await?;
+
+            if decoder.position() != metadata.len() as u64 {
+                error!(
+                    target: "block_explorer::calculate_tx_gas_data",
+                    "[BLOCK_EXPLORER] Failed decoding entire metadata buffer for {}:{}", tx_hash, idx,
+                );
+                return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
+            }
+
+            // Here we'll look up verifying keys and insert them into the per-contract map.
+            for (zkas_ns, _) in &zkp_pub {
+                let inner_vk_map =
+                    verifying_keys.get_mut(&call.data.contract_id.to_bytes()).unwrap();
+
+                // TODO: This will be a problem in case of ::deploy, unless we force a different
+                // namespace and disable updating existing circuit. Might be a smart idea to do
+                // so in order to have to care less about being able to verify historical txs.
+                if inner_vk_map.contains_key(zkas_ns.as_str()) {
+                    continue
+                }
+
+                let (zkbin, vk) =
+                    overlay.lock().unwrap().contracts.get_zkas(&call.data.contract_id, zkas_ns)?;
+
+                inner_vk_map.insert(zkas_ns.to_string(), vk);
+                circuits_to_verify.push(zkbin);
+            }
+
+            zkp_table.push(zkp_pub);
+            sig_table.push(sig_pub);
+
+            // Contracts are not included within blocks. They need to be deployed off-chain so that they can be accessed and utilized for fee data computation
+            if call.data.is_deployment()
+            /* DeployV1 */
+            {
+                // Deserialize the deployment parameters
+                let deploy_params: DeployParamsV1 = deserialize_async(&call.data.data[1..]).await?;
+                let deploy_cid = ContractId::derive_public(deploy_params.public_key);
+
+                // Instantiate the new deployment runtime
+                let mut deploy_runtime = Runtime::new(
+                    &deploy_params.wasm_bincode,
+                    overlay.clone(),
+                    deploy_cid,
+                    block_target,
+                    block_target,
+                    tx_hash,
+                    idx as u8,
+                )?;
+
+                deploy_runtime.deploy(&deploy_params.ix)?;
+
+                deploy_gas_used = deploy_runtime.gas_used();
+
+                // Append the used deployment gas
+                total_gas_used += deploy_gas_used;
+            }
+
+            // At this point we're done with the call and move on to the next one.
+            // Accumulate the WASM gas used.
+            wasm_gas_used = runtime.gas_used();
+
+            // Append the used wasm gas
+            total_gas_used += wasm_gas_used;
+        }
+
+        // The signature fee is tx_size + fixed_sig_fee * n_signatures
+        let signature_gas_used = (PALLAS_SCHNORR_SIGNATURE_FEE * tx.signatures.len() as u64) +
+            serialize_async(tx).await.len() as u64;
+
+        // Append the used signature gas
+        total_gas_used += signature_gas_used;
+
+        // The ZK circuit fee is calculated using a function in validator/fees.rs
+        for zkbin in circuits_to_verify.iter() {
+            zk_circuit_gas_used = circuit_gas_use(zkbin);
+
+            // Append the used zk circuit gas
+            total_gas_used += zk_circuit_gas_used;
+        }
+
+        if verify_fee {
+            // Deserialize the fee call to find the paid fee
+            let fee: u64 = match deserialize_async(&tx.calls[fee_call_idx].data.data[1..9]).await {
+                Ok(v) => v,
+                Err(e) => {
+                    error!(
+                        target: "block_explorer::calculate_tx_gas_data",
+                        "[VALIDATOR] Failed deserializing tx {} fee call: {}", tx_hash, e,
+                    );
+                    return Err(TxVerifyFailed::InvalidFee.into())
+                }
+            };
+
+            // TODO: This counts 1 gas as 1 token unit. Pricing should be better specified.
+            // Check that enough fee has been paid for the used gas in this transaction.
+            if total_gas_used > fee {
+                error!(
+                    target: "block_explorer::calculate_tx_gas_data",
+                    "[VALIDATOR] Transaction {} has insufficient fee. Required: {}, Paid: {}",
+                    tx_hash, total_gas_used, fee,
+                );
+                return Err(TxVerifyFailed::InsufficientFee.into())
+            }
+            debug!(target: "block_explorer::calculate_tx_gas_data", "The gas paid for transaction {}: {}", tx_hash, gas_paid);
+
+            // Store paid fee
+            gas_paid = fee;
+        }
+
+        // Commit changes made to the overlay
+        overlay.lock().unwrap().overlay.lock().unwrap().apply()?;
+
+        let fee_data = GasData {
+            paid: gas_paid,
+            wasm: wasm_gas_used,
+            zk_circuits: zk_circuit_gas_used,
+            signatures: signature_gas_used,
+            deployments: deploy_gas_used,
+        };
+
+        debug!(target: "block_explorer::calculate_tx_gas_data", "The total gas usage for transaction {}: {:?}", tx_hash, fee_data);
+
+        Ok(fee_data)
     }
 }
 
@@ -132,13 +349,24 @@ impl Explorerd {
     async fn new(db_path: String, endpoint: Url, ex: Arc<smol::Executor<'static>>) -> Result<Self> {
         // Initialize rpc client
         let rpc_client = RpcClient::new(endpoint.clone(), ex).await?;
-        info!(target: "explorerd", "Created rpc client: {:?}", endpoint);
+        info!(target: "blockchain-explorer", "Created rpc client: {:?}", endpoint);
 
         // Initialize explorer database
         let explorer_db = ExplorerDb::new(db_path)?;
 
+        // Deploy native contracts need to calculated transaction gas data and commit changes
+        let overlay = BlockchainOverlay::new(&explorer_db.blockchain)?;
+        deploy_native_contracts(&overlay, 10).await?;
+        overlay.lock().unwrap().overlay.lock().unwrap().apply()?;
+
         Ok(Self { rpc_connections: Mutex::new(HashSet::new()), rpc_client, db: explorer_db })
     }
+
+    /// Fetches the most current metrics from the [`MetricsStore`], returning an `Option` containing
+    /// a pair of [`GasMetricsKey`] and [`GasMetrics`] upon success, or `None` if no metrics are found.
+    pub fn get_latest_metrics(&self) -> Result<Option<(GasMetricsKey, GasMetrics)>> {
+        self.db.metrics_store.get_last()
+    }
 }
 
 async_daemonize!(realmain);

+ 3 - 0
script/research/blockchain-explorer/src/rpc.rs

@@ -74,6 +74,9 @@ impl RequestHandler<()> for Explorerd {
             "statistics.get_basic_statistics" => {
                 self.statistics_get_basic_statistics(req.id, req.params).await
             }
+            "statistics.get_metric_statistics" => {
+                self.statistics_get_metric_statistics(req.id, req.params).await
+            }
 
             // TODO: add any other useful methods
 

+ 70 - 1
script/research/blockchain-explorer/src/rpc_statistics.rs

@@ -16,6 +16,8 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use std::vec::Vec;
+
 use log::error;
 use tinyjson::JsonValue;
 
@@ -46,7 +48,7 @@ impl Explorerd {
             return JsonError::new(InvalidParams, None, id).into()
         }
 
-        // Fetch base statistics and return results
+        // Fetch `BaseStatistics`, transform to `JsonResult`, and return results
         match self.db.get_base_statistics() {
             Ok(Some(statistics)) => JsonResponse::new(statistics.to_json_array(), id).into(),
             Ok(None) => JsonResponse::new(JsonValue::Array(vec![]), id).into(),
@@ -59,4 +61,71 @@ impl Explorerd {
             }
         }
     }
+
+    // RPCAPI:
+    // Queries the database to retrieve all metrics statistics.
+    // Returns a collection of metric statistics upon success.
+    //
+    // **Params:**
+    // * `None`
+    //
+    // **Returns:**
+    // * `MetricsStatistics` array encoded into a JSON.
+    //
+    // --> {"jsonrpc": "2.0", "method": "statistics.get_metric_statistics", "params": [], "id": 1}
+    // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
+    pub async fn statistics_get_metric_statistics(&self, id: u16, params: JsonValue) -> JsonResult {
+        // Validate to ensure parameters are empty
+        let params = params.get::<Vec<JsonValue>>().unwrap();
+        if !params.is_empty() {
+            return JsonError::new(InvalidParams, None, id).into()
+        }
+        // Fetch metric statistics and return results
+        let metrics = match self.db.get_metrics_statistics().await {
+            Ok(v) => v,
+            Err(e) => {
+                error!(target: "blockchain-explorer::rpc_statistics::statistics_get_metric_statistics", "Failed fetching metric statistics: {}", e);
+                return JsonError::new(InternalError, None, id).into()
+            }
+        };
+
+        // Transform statistics to JsonResponse and return result
+        let metrics_json: Vec<JsonValue> = metrics.iter().map(|m| m.to_json_array()).collect();
+        JsonResponse::new(JsonValue::Array(metrics_json), id).into()
+    }
+
+    // RPCAPI:
+    // Queries the database to retrieve latest metric statistics.
+    // Returns the readable metric statistics upon success.
+    //
+    // **Params:**
+    // * `None`
+    //
+    // **Returns:**
+    // * `MetricsStatistics` encoded into a JSON.
+    //
+    // --> {"jsonrpc": "2.0", "method": "statistics.get_latest_metric_statistics", "params": [], "id": 1}
+    // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
+    pub async fn statistics_get_latest_metric_statistics(
+        &self,
+        id: u16,
+        params: JsonValue,
+    ) -> JsonResult {
+        // Validate to ensure parameters are empty
+        let params = params.get::<Vec<JsonValue>>().unwrap();
+        if !params.is_empty() {
+            return JsonError::new(InvalidParams, None, id).into()
+        }
+        // Fetch metric statistics and return results
+        let metrics = match self.db.get_latest_metrics_statistics().await {
+            Ok(v) => v,
+            Err(e) => {
+                error!(target: "blockchain-explorer::rpc_statistics::statistics_get_latest_metric_statistics", "Failed fetching metric statistics: {}", e);
+                return JsonError::new(InternalError, None, id).into()
+            }
+        };
+
+        // Transform statistics to JsonResponse and return result
+        JsonResponse::new(metrics.to_json_array(), id).into()
+    }
 }

+ 71 - 2
script/research/blockchain-explorer/src/statistics.rs

@@ -21,7 +21,7 @@ use tinyjson::JsonValue;
 use darkfi::{Error, Result};
 use darkfi_sdk::blockchain::block_epoch;
 
-use crate::ExplorerDb;
+use crate::{metrics_store::GasMetrics, ExplorerDb};
 
 #[derive(Debug, Clone)]
 /// Structure representing basic statistic extracted from the database.
@@ -51,8 +51,42 @@ impl BaseStatistics {
     }
 }
 
+/// Structure representing metrics extracted from the database.
+#[derive(Default)]
+pub struct MetricStatistics {
+    /// Metrics used to store explorer statistics
+    pub metrics: GasMetrics,
+}
+
+impl MetricStatistics {
+    pub fn new(metrics: GasMetrics) -> Self {
+        Self { metrics }
+    }
+
+    /// Auxiliary function to convert [`MetricStatistics`] into a [`JsonValue`] array.
+    pub fn to_json_array(&self) -> JsonValue {
+        JsonValue::Array(vec![
+            JsonValue::Number(self.metrics.avg_total_gas_used() as f64),
+            JsonValue::Number(self.metrics.total_gas.min as f64),
+            JsonValue::Number(self.metrics.total_gas.max as f64),
+            JsonValue::Number(self.metrics.avg_wasm_gas_used() as f64),
+            JsonValue::Number(self.metrics.wasm_gas.min as f64),
+            JsonValue::Number(self.metrics.wasm_gas.max as f64),
+            JsonValue::Number(self.metrics.avg_zk_circuits_gas_used() as f64),
+            JsonValue::Number(self.metrics.zk_circuits_gas.min as f64),
+            JsonValue::Number(self.metrics.zk_circuits_gas.max as f64),
+            JsonValue::Number(self.metrics.avg_signatures_gas_used() as f64),
+            JsonValue::Number(self.metrics.signatures_gas.min as f64),
+            JsonValue::Number(self.metrics.signatures_gas.max as f64),
+            JsonValue::Number(self.metrics.avg_deployments_gas_used() as f64),
+            JsonValue::Number(self.metrics.deployments_gas.min as f64),
+            JsonValue::Number(self.metrics.deployments_gas.max as f64),
+            JsonValue::Number(self.metrics.timestamp.inner() as f64),
+        ])
+    }
+}
 impl ExplorerDb {
-    /// Fetch current database basic statistics.
+    /// Fetches the latest [`BaseStatistics`] from the explorer database, or returns `None` if no block exists.
     pub fn get_base_statistics(&self) -> Result<Option<BaseStatistics>> {
         let last_block = self.last_block();
         Ok(last_block
@@ -71,4 +105,39 @@ impl ExplorerDb {
                 BaseStatistics { height, epoch, last_block: header_hash, total_blocks, total_txs }
             }))
     }
+
+    /// Fetches the latest metrics from the explorer database, returning a vector of
+    /// [`MetricStatistics`] if found, or an empty Vec if no metrics exist.
+    pub async fn get_metrics_statistics(&self) -> Result<Vec<MetricStatistics>> {
+        // Fetch all metrics from the metrics store, handling any potential errors
+        let metrics = self.metrics_store.get_all_metrics().map_err(|e| {
+            Error::DatabaseError(format!(
+                "[get_metrics_statistics] Retrieving metrics failed: {:?}",
+                e
+            ))
+        })?;
+
+        // Transform the fetched metrics into `MetricStatistics`, collect them into a vector
+        let metric_statistics =
+            metrics.iter().map(|metrics| MetricStatistics::new(metrics.clone())).collect();
+
+        Ok(metric_statistics)
+    }
+
+    /// Fetches the latest metrics from the explorer database, returning [`MetricStatistics`] if found,
+    /// or zero-initialized defaults when not.
+    pub async fn get_latest_metrics_statistics(&self) -> Result<MetricStatistics> {
+        // Fetch the latest metrics, handling any potential errors
+        match self.metrics_store.get_last().map_err(|e| {
+            Error::DatabaseError(format!(
+                "[get_metrics_statistics] Retrieving latest metrics failed: {:?}",
+                e
+            ))
+        })? {
+            // Transform metrics into `MetricStatistics` when found
+            Some((_, metrics)) => Ok(MetricStatistics::new(metrics)),
+            // Return default statistics when no metrics exist
+            None => Ok(MetricStatistics::default()),
+        }
+    }
 }

+ 156 - 46
script/research/blockchain-explorer/src/transactions.rs

@@ -21,10 +21,12 @@ use tinyjson::JsonValue;
 
 use darkfi::{
     blockchain::{
-        HeaderHash, SLED_PENDING_TX_ORDER_TREE, SLED_PENDING_TX_TREE, SLED_TX_LOCATION_TREE,
-        SLED_TX_TREE,
+        BlockInfo, HeaderHash, SLED_PENDING_TX_ORDER_TREE, SLED_PENDING_TX_TREE,
+        SLED_TX_LOCATION_TREE, SLED_TX_TREE,
     },
     tx::Transaction,
+    util::time::Timestamp,
+    validator::fees::GasData,
     Error, Result,
 };
 use darkfi_sdk::tx::TransactionHash;
@@ -41,6 +43,18 @@ pub struct TransactionRecord {
     // TODO: Split the payload into a more easily readable fields
     /// Transaction payload
     pub payload: Transaction,
+    /// Time transaction was added to the block
+    pub timestamp: Timestamp,
+    /// Total gas used for processing transaction
+    pub total_gas_used: u64,
+    /// Gas used by WASM
+    pub wasm_gas_used: u64,
+    /// Gas used by ZK circuit operations
+    pub zk_circuit_gas_used: u64,
+    /// Gas used for creating the transaction signature
+    pub signature_gas_used: u64,
+    /// Gas used for deployments
+    pub deployment_gas_used: u64,
 }
 
 impl TransactionRecord {
@@ -50,20 +64,16 @@ impl TransactionRecord {
             JsonValue::String(self.transaction_hash.clone()),
             JsonValue::String(self.header_hash.clone()),
             JsonValue::String(format!("{:?}", self.payload)),
+            JsonValue::String(self.timestamp.to_string()),
+            JsonValue::Number(self.total_gas_used as f64),
+            JsonValue::Number(self.wasm_gas_used as f64),
+            JsonValue::Number(self.zk_circuit_gas_used as f64),
+            JsonValue::Number(self.signature_gas_used as f64),
+            JsonValue::Number(self.deployment_gas_used as f64),
         ])
     }
 }
 
-impl From<(&String, &Transaction)> for TransactionRecord {
-    fn from((header_hash, transaction): (&String, &Transaction)) -> Self {
-        Self {
-            transaction_hash: transaction.hash().to_string(),
-            header_hash: header_hash.clone(),
-            payload: transaction.clone(),
-        }
-    }
-}
-
 impl ExplorerDb {
     /// Resets transactions in the database by clearing transaction-related trees, returning an Ok result on success.
     pub fn reset_transactions(&self) -> Result<()> {
@@ -87,23 +97,33 @@ impl ExplorerDb {
         self.blockchain.txs_len()
     }
 
-    /// Fetch all known transactions from the database.
+    /// Fetches all known transactions from the database.
+    ///
+    /// This function retrieves all transactions stored in the database and transforms
+    /// them into a vector of [`TransactionRecord`]s. If no transactions are found,
+    /// it returns an empty vector.
     pub fn get_transactions(&self) -> Result<Vec<TransactionRecord>> {
         // Retrieve all transactions and handle any errors encountered
-        let transactions = self.blockchain.transactions.get_all().map_err(|e| {
+        let txs = self.blockchain.transactions.get_all().map_err(|e| {
             Error::DatabaseError(format!("[get_transactions] Trxs retrieval: {e:?}"))
         })?;
 
-        // Transform the found transactions into a vector of transaction records
-        let transaction_records: Vec<TransactionRecord> = transactions
+        // Transform the found `Transactions` into a vector of `TransactionRecords`
+        let txs_records = txs
             .iter()
-            .map(|(tx_hash, tx)| TransactionRecord::from((&tx_hash.as_string(), tx)))
-            .collect();
+            .map(|(_, tx)| self.to_tx_record(None, tx))
+            .collect::<Result<Vec<TransactionRecord>>>()?;
 
-        Ok(transaction_records)
+        Ok(txs_records)
     }
 
-    /// Fetch all transactions from the database for the given block header hash.
+    /// Fetches all transactions from the database for the given block `header_hash`.
+    ///
+    /// This function retrieves all transactions associated with the specified
+    /// block header hash. It first parses the header hash and then fetches
+    /// the corresponding [`BlockInfo`]. If the block is found, it transforms its
+    /// transactions into a vector of [`TransactionRecord`]s. If no transactions
+    /// are found, it returns an empty vector.
     pub fn get_transactions_by_header_hash(
         &self,
         header_hash: &str,
@@ -114,8 +134,8 @@ impl ExplorerDb {
             .map_err(|_| Error::ParseFailed("[get_transactions_by_header_hash] Invalid hash"))?;
 
         // Fetch block by hash and handle encountered errors
-        let blocks = match self.blockchain.get_blocks_by_hash(&[header_hash]) {
-            Ok(blocks) => blocks,
+        let block = match self.blockchain.get_blocks_by_hash(&[header_hash]) {
+            Ok(blocks) => blocks.first().cloned().unwrap(),
             Err(Error::BlockNotFound(_)) => return Ok(vec![]),
             Err(e) => {
                 return Err(Error::DatabaseError(format!(
@@ -125,14 +145,18 @@ impl ExplorerDb {
         };
 
         // Transform block transactions into transaction records
-        Ok(blocks[0]
+        block
             .txs
             .iter()
-            .map(|tx| TransactionRecord::from((&blocks[0].header.hash().as_string(), tx)))
-            .collect::<Vec<TransactionRecord>>())
+            .map(|tx| self.to_tx_record(self.get_block_info(block.header.hash())?, tx))
+            .collect::<Result<Vec<TransactionRecord>>>()
     }
 
-    /// Fetch a transaction given its header hash.
+    /// Fetches a transaction given its header hash.
+    ///
+    /// This function retrieves the transaction associated with the provided
+    /// [`TransactionHash`] and transforms it into a [`TransactionRecord`] if found.
+    /// If no transaction is found, it returns `None`.
     pub fn get_transaction_by_hash(
         &self,
         tx_hash: &TransactionHash,
@@ -140,35 +164,121 @@ impl ExplorerDb {
         let tx_store = &self.blockchain.transactions;
 
         // Attempt to retrieve the transaction using the provided hash handling any potential errors
-        let txs = tx_store.get(&[*tx_hash], false).map_err(|e| {
+        let tx_opt = &tx_store.get(&[*tx_hash], false).map_err(|e| {
             Error::DatabaseError(format!(
                 "[get_transaction_by_hash] Transaction retrieval failed: {e:?}"
             ))
-        })?;
+        })?[0];
+
+        // Transform `Transaction` to a `TransactionRecord`, returning None if no transaction was found
+        tx_opt.as_ref().map(|tx| self.to_tx_record(None, tx)).transpose()
+    }
 
-        // Check if transaction was found
-        if txs[0].is_none() {
-            return Ok(None);
+    /// Fetches the [`BlockInfo`] associated with a given transaction hash.
+    ///
+    /// This auxiliary function first fetches the location of the transaction in the blockchain.
+    /// If the location is found, it retrieves the associated [`HeaderHash`] and then fetches
+    /// the block information corresponding to that header hash. The function returns the
+    /// [`BlockInfo`] if successful, or `None` if no location or header hash is found.
+    fn get_tx_block_info(&self, tx_hash: &TransactionHash) -> Result<Option<BlockInfo>> {
+        // Retrieve the location of the transaction
+        let location =
+            self.blockchain.transactions.get_location(&[*tx_hash], false).map_err(|e| {
+                Error::DatabaseError(format!(
+                    "[get_tx_block_info] Location retrieval failed: {e:?}"
+                ))
+            })?[0];
+
+        // Fetch the `HeaderHash` associated with the location
+        let header_hash = match location {
+            None => return Ok(None),
+            Some((block_height, _)) => {
+                self.blockchain.blocks.get_order(&[block_height], false).map_err(|e| {
+                    Error::DatabaseError(format!(
+                        "[get_tx_block_info] Block retrieval failed: {e:?}"
+                    ))
+                })?[0]
+            }
         };
 
-        // Retrieve the location of the transaction to obtain its header hash
-        let (block_height, _) = tx_store.get_location(&[*tx_hash], true).map_err(|e| {
+        // Return the associated `BlockInfo` if the header hash is found; otherwise, return `None`.
+        match header_hash {
+            None => Ok(None),
+            Some(header_hash) => self.get_block_info(header_hash).map_err(|e| {
+                Error::DatabaseError(format!(
+                    "[get_tx_block_info] BlockInfo retrieval failed: {e:?}"
+                ))
+            }),
+        }
+    }
+
+    /// Fetches the [`BlockInfo`] associated with a given [`HeaderHash`].
+    ///
+    /// This auxiliary function attempts to retrieve the block information using
+    /// the specified [`HeaderHash`]. It returns the associated [`BlockInfo`] if found,
+    /// or `None` when not found.
+    fn get_block_info(&self, header_hash: HeaderHash) -> Result<Option<BlockInfo>> {
+        match self.blockchain.get_blocks_by_hash(&[header_hash]) {
+            Err(Error::BlockNotFound(_)) => Ok(None),
+            Ok(block_info) => Ok(block_info.into_iter().next()),
+            Err(e) => Err(Error::DatabaseError(format!(
+                "[get_transactions_by_header_hash] Block retrieval failed: {e:?}"
+            ))),
+        }
+    }
+
+    /// Converts a [`Transaction`] and its associated block information into a [`TransactionRecord`].
+    ///
+    /// This auxiliary function first retrieves the gas data associated with the provided transaction.
+    /// If [`BlockInfo`] is not provided, it attempts to fetch it using the transaction's hash,
+    /// returning an error if the block information cannot be found. Upon success, the function
+    /// returns a [`TransactionRecord`] containing relevant details about the transaction.
+    fn to_tx_record(
+        &self,
+        block_info_opt: Option<BlockInfo>,
+        tx: &Transaction,
+    ) -> Result<TransactionRecord> {
+        // Fetch the gas data associated with the transaction
+        let gas_data_option = self.metrics_store.get_tx_gas_data(&tx.hash()).map_err(|e| {
             Error::DatabaseError(format!(
-                "[get_transaction_by_hash] Location retrieval failed: {e:?}"
+                "[to_tx_record] Failed to fetch the gas data associated with transaction {}: {e:?}",
+                tx.hash()
             ))
-        })?[0]
-            .unwrap();
+        })?;
 
-        // Retrieve the block corresponding to the transaction's height
-        let header_hash =
-            &self.blockchain.blocks.get_order(&[block_height], true).map_err(|e| {
-                Error::DatabaseError(format!(
-                    "[get_transaction_by_hash] Block retrieval failed: {e:?}"
-                ))
-            })?[0]
-                .unwrap();
+        // Unwrap the option, providing a default value when `None`
+        let gas_data = gas_data_option.unwrap_or_else(GasData::default);
+
+        // Process provided block_info option
+        let block_info = match block_info_opt {
+            // Use provided block_info when present
+            Some(block_info) => block_info,
+            // Fetch the block info associated with the transaction when block info not provided
+            None => {
+                match self.get_tx_block_info(&tx.hash())? {
+                    Some(block_info) => block_info,
+                    // If no associated block info found, throw an error as this should not happen
+                    None => {
+                        return Err(Error::BlockNotFound(format!(
+                            "[to_tx_record] Required `BlockInfo` was not found for transaction: {}",
+                            tx.hash()
+                        )))
+                    }
+                }
+            }
+        };
 
-        // Transform the transaction into a TransactionRecord
-        Ok(Some(TransactionRecord::from((&header_hash.as_string(), txs[0].as_ref().unwrap()))))
+        // Return transformed transaction record
+        Ok(TransactionRecord {
+            transaction_hash: tx.hash().to_string(),
+            header_hash: block_info.hash().to_string(),
+            timestamp: block_info.header.timestamp,
+            payload: tx.clone(),
+            total_gas_used: gas_data.total_gas_used(),
+            wasm_gas_used: gas_data.wasm,
+            zk_circuit_gas_used: gas_data.zk_circuits,
+            signature_gas_used: gas_data.signatures,
+            deployment_gas_used: gas_data.deployments,
+        })
     }
 }