script/research/blockchain-explorer: added base statistics calls

This commit is contained in:
skoupidi
2024-06-15 15:26:53 +03:00
parent 968f0c7202
commit c680467899
8 changed files with 196 additions and 492 deletions

View File

@@ -68,16 +68,16 @@ pub struct BlockRecord {
impl BlockRecord {
/// Auxiliary function to convert a `BlockRecord` into a `JsonValue` array.
pub fn to_json_array(&self) -> JsonValue {
let mut ret = vec![];
ret.push(JsonValue::String(self.header_hash.clone()));
ret.push(JsonValue::Number(self.version as f64));
ret.push(JsonValue::String(self.previous.clone()));
ret.push(JsonValue::Number(self.height as f64));
ret.push(JsonValue::Number(self.timestamp as f64));
ret.push(JsonValue::Number(self.nonce as f64));
ret.push(JsonValue::String(self.root.clone()));
ret.push(JsonValue::String(format!("{:?}", self.signature)));
JsonValue::Array(ret)
JsonValue::Array(vec![
JsonValue::String(self.header_hash.clone()),
JsonValue::Number(self.version as f64),
JsonValue::String(self.previous.clone()),
JsonValue::Number(self.height as f64),
JsonValue::Number(self.timestamp as f64),
JsonValue::Number(self.nonce as f64),
JsonValue::String(self.root.clone()),
JsonValue::String(format!("{:?}", self.signature)),
])
}
}
@@ -248,12 +248,12 @@ impl BlockchainExplorer {
self.parse_block_record(&row)
}
/// Fetch last block from the database.
pub async fn last_block(&self) -> WalletDbResult<u32> {
/// Fetch last block height from the database.
pub async fn last_block(&self) -> WalletDbResult<(u32, String)> {
// First we prepare the query
let query = format!(
"SELECT {} FROM {} ORDER BY {} DESC LIMIT 1;",
BLOCKS_COL_HEIGHT, BLOCKS_TABLE, BLOCKS_COL_HEIGHT
"SELECT {}, {} FROM {} ORDER BY {} DESC LIMIT 1;",
BLOCKS_COL_HEADER_HASH, BLOCKS_COL_HEIGHT, BLOCKS_TABLE, BLOCKS_COL_HEIGHT
);
let Ok(conn) = self.database.conn.lock() else {
return Err(WalletDbError::FailedToAquireLock)
@@ -269,11 +269,17 @@ impl BlockchainExplorer {
let Ok(next) = rows.next() else { return Err(WalletDbError::QueryExecutionFailed) };
let row = match next {
Some(row_result) => row_result,
None => return Ok(0_u32),
None => return Ok((0_u32, "".to_string())),
};
// Parse returned value
// Parse returned values
let Ok(value) = row.get(0) else { return Err(WalletDbError::ParseColumnValueError) };
let Value::Text(ref header_hash) = value else {
return Err(WalletDbError::ParseColumnValueError)
};
let header_hash = header_hash.clone();
let Ok(value) = row.get(1) else { return Err(WalletDbError::ParseColumnValueError) };
let Value::Integer(height) = value else {
return Err(WalletDbError::ParseColumnValueError)
};
@@ -281,7 +287,7 @@ impl BlockchainExplorer {
return Err(WalletDbError::ParseColumnValueError)
};
Ok(height)
Ok((height, header_hash))
}
/// Auxiliary function to parse a `BLOCKS_TABLE` query rows into block records.

View File

@@ -47,6 +47,7 @@ mod error;
mod rpc;
mod rpc_blocks;
use rpc_blocks::subscribe_blocks;
mod rpc_statistics;
mod rpc_transactions;
/// Database functionality related to blocks
@@ -55,6 +56,9 @@ mod blocks;
/// Database functionality related to transactions
mod transactions;
/// Database functionality related to statistics
mod statistics;
const CONFIG_FILE: &str = "blockchain_explorer_config.toml";
const CONFIG_FILE_CONTENTS: &str = include_str!("../blockchain_explorer_config.toml");

View File

@@ -1,466 +0,0 @@
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2024 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::{fs::File, io::Write};
use clap::Parser;
use darkfi::{
blockchain::{
block_store::{Block, BlockDifficulty, BlockRanks, BlockStore},
contract_store::ContractStore,
header_store::{Header, HeaderHash, HeaderStore},
tx_store::TxStore,
Blockchain,
},
cli_desc,
tx::Transaction,
util::{path::expand_path, time::Timestamp},
Result,
};
use darkfi_sdk::{
blockchain::block_epoch,
crypto::{ContractId, MerkleNode},
tx::TransactionHash,
};
use num_bigint::BigUint;
#[derive(Parser)]
#[command(about = cli_desc!())]
struct Args {
#[arg(short, long, default_value = "../../../contrib/localnet/darkfid-single-node/")]
/// Path containing the node folders
path: String,
#[arg(short, long, default_values = ["darkfid"])]
/// Node folder name (supports multiple values)
node: Vec<String>,
#[arg(short, long, default_value = "")]
/// Node blockchain folder
blockchain: String,
#[arg(short, long)]
/// Export all contents into a JSON file
export: bool,
}
#[derive(Debug)]
struct HeaderInfo {
_hash: HeaderHash,
_version: u8,
_previous: HeaderHash,
_height: u32,
_timestamp: Timestamp,
_nonce: u64,
_root: MerkleNode,
}
impl HeaderInfo {
pub fn new(_hash: HeaderHash, header: &Header) -> HeaderInfo {
HeaderInfo {
_hash,
_version: header.version,
_previous: header.previous,
_height: header.height,
_timestamp: header.timestamp,
_nonce: header.nonce,
_root: header.root,
}
}
}
#[derive(Debug)]
struct HeaderStoreInfo {
_main: Vec<HeaderInfo>,
_sync: Vec<HeaderInfo>,
}
impl HeaderStoreInfo {
pub fn new(headerstore: &HeaderStore) -> HeaderStoreInfo {
let mut _main = Vec::new();
let result = headerstore.get_all();
match result {
Ok(iter) => {
for (hash, header) in iter.iter() {
_main.push(HeaderInfo::new(*hash, header));
}
}
Err(e) => println!("Error: {:?}", e),
}
let mut _sync = Vec::new();
let result = headerstore.get_all_sync();
match result {
Ok(iter) => {
for (_, header) in iter.iter() {
_sync.push(HeaderInfo::new(header.hash(), header));
}
}
Err(e) => println!("Error: {:?}", e),
}
HeaderStoreInfo { _main, _sync }
}
}
#[derive(Debug)]
struct BlockInfo {
_hash: HeaderHash,
_header: HeaderHash,
_txs: Vec<TransactionHash>,
_signature: String,
}
impl BlockInfo {
pub fn new(_hash: HeaderHash, block: &Block) -> BlockInfo {
BlockInfo {
_hash,
_header: block.header,
_txs: block.txs.clone(),
_signature: format!("{:?}", block.signature),
}
}
}
#[derive(Debug)]
struct OrderInfo {
_height: u32,
_hash: HeaderHash,
}
impl OrderInfo {
pub fn new(_height: u32, _hash: HeaderHash) -> OrderInfo {
OrderInfo { _height, _hash }
}
}
#[derive(Debug)]
struct BlockRanksInfo {
_target_rank: BigUint,
_targets_rank: BigUint,
_hash_rank: BigUint,
_hashes_rank: BigUint,
}
impl BlockRanksInfo {
pub fn new(ranks: &BlockRanks) -> BlockRanksInfo {
BlockRanksInfo {
_target_rank: ranks.target_rank.clone(),
_targets_rank: ranks.targets_rank.clone(),
_hash_rank: ranks.hash_rank.clone(),
_hashes_rank: ranks.hashes_rank.clone(),
}
}
}
#[derive(Debug)]
struct BlockDifficultyInfo {
_height: u32,
_timestamp: Timestamp,
_difficulty: BigUint,
_cummulative_difficulty: BigUint,
_ranks: BlockRanksInfo,
}
impl BlockDifficultyInfo {
pub fn new(difficulty: &BlockDifficulty) -> BlockDifficultyInfo {
BlockDifficultyInfo {
_height: difficulty.height,
_timestamp: difficulty.timestamp,
_difficulty: difficulty.difficulty.clone(),
_cummulative_difficulty: difficulty.cummulative_difficulty.clone(),
_ranks: BlockRanksInfo::new(&difficulty.ranks),
}
}
}
#[derive(Debug)]
struct BlockStoreInfo {
_main: Vec<BlockInfo>,
_order: Vec<OrderInfo>,
_difficulty: Vec<BlockDifficultyInfo>,
}
impl BlockStoreInfo {
pub fn new(blockstore: &BlockStore) -> BlockStoreInfo {
let mut _main = Vec::new();
let result = blockstore.get_all();
match result {
Ok(iter) => {
for (hash, block) in iter.iter() {
_main.push(BlockInfo::new(*hash, block));
}
}
Err(e) => println!("Error: {:?}", e),
}
let mut _order = Vec::new();
let result = blockstore.get_all_order();
match result {
Ok(iter) => {
for (height, hash) in iter.iter() {
_order.push(OrderInfo::new(*height, *hash));
}
}
Err(e) => println!("Error: {:?}", e),
}
let mut _difficulty = Vec::new();
let result = blockstore.get_all_difficulty();
match result {
Ok(iter) => {
for (_, difficulty) in iter.iter() {
_difficulty.push(BlockDifficultyInfo::new(difficulty));
}
}
Err(e) => println!("Error: {:?}", e),
}
BlockStoreInfo { _main, _order, _difficulty }
}
}
#[derive(Debug)]
struct TxInfo {
_hash: TransactionHash,
_payload: Transaction,
}
impl TxInfo {
pub fn new(_hash: TransactionHash, tx: &Transaction) -> TxInfo {
TxInfo { _hash, _payload: tx.clone() }
}
}
#[derive(Debug)]
struct TxLocationInfo {
_hash: TransactionHash,
_block_height: u32,
_index: u16,
}
impl TxLocationInfo {
pub fn new(_hash: TransactionHash, _block_height: u32, _index: u16) -> TxLocationInfo {
TxLocationInfo { _hash, _block_height, _index }
}
}
#[derive(Debug)]
struct PendingOrderInfo {
_order: u64,
_hash: TransactionHash,
}
impl PendingOrderInfo {
pub fn new(_order: u64, _hash: TransactionHash) -> PendingOrderInfo {
PendingOrderInfo { _order, _hash }
}
}
#[derive(Debug)]
struct TxStoreInfo {
_main: Vec<TxInfo>,
_location: Vec<TxLocationInfo>,
_pending: Vec<TxInfo>,
_pending_order: Vec<PendingOrderInfo>,
}
impl TxStoreInfo {
pub fn new(txstore: &TxStore) -> TxStoreInfo {
let mut _main = Vec::new();
let result = txstore.get_all();
match result {
Ok(iter) => {
for (hash, tx) in iter.iter() {
_main.push(TxInfo::new(*hash, tx));
}
}
Err(e) => println!("Error: {:?}", e),
}
let mut _location = Vec::new();
let result = txstore.get_all_location();
match result {
Ok(iter) => {
for (hash, location) in iter.iter() {
_location.push(TxLocationInfo::new(*hash, location.0, location.1));
}
}
Err(e) => println!("Error: {:?}", e),
}
let mut _pending = Vec::new();
let result = txstore.get_all_pending();
match result {
Ok(iter) => {
for (hash, tx) in iter.iter() {
_pending.push(TxInfo::new(*hash, tx));
}
}
Err(e) => println!("Error: {:?}", e),
}
let mut _pending_order = Vec::new();
let result = txstore.get_all_pending_order();
match result {
Ok(iter) => {
for (order, hash) in iter.iter() {
_pending_order.push(PendingOrderInfo::new(*order, *hash));
}
}
Err(e) => println!("Error: {:?}", e),
}
TxStoreInfo { _main, _location, _pending, _pending_order }
}
}
#[derive(Debug)]
struct ContractStateInfo {
_id: ContractId,
_state_hashes: Vec<blake3::Hash>,
}
impl ContractStateInfo {
pub fn new(_id: ContractId, state_hashes: &[blake3::Hash]) -> ContractStateInfo {
ContractStateInfo { _id, _state_hashes: state_hashes.to_vec() }
}
}
#[derive(Debug)]
struct WasmInfo {
_id: ContractId,
_bincode_hash: blake3::Hash,
}
impl WasmInfo {
pub fn new(_id: ContractId, bincode: &[u8]) -> WasmInfo {
let _bincode_hash = blake3::hash(bincode);
WasmInfo { _id, _bincode_hash }
}
}
#[derive(Debug)]
struct ContractStoreInfo {
_state: Vec<ContractStateInfo>,
_wasm: Vec<WasmInfo>,
}
impl ContractStoreInfo {
pub fn new(contractsstore: &ContractStore) -> ContractStoreInfo {
let mut _state = Vec::new();
let result = contractsstore.get_all_states();
match result {
Ok(iter) => {
for (id, state_hash) in iter.iter() {
_state.push(ContractStateInfo::new(*id, state_hash));
}
}
Err(e) => println!("Error: {:?}", e),
}
let mut _wasm = Vec::new();
let result = contractsstore.get_all_wasm();
match result {
Ok(iter) => {
for (id, bincode) in iter.iter() {
_wasm.push(WasmInfo::new(*id, bincode));
}
}
Err(e) => println!("Error: {:?}", e),
}
ContractStoreInfo { _state, _wasm }
}
}
#[derive(Debug)]
struct BlockchainInfo {
_headers: HeaderStoreInfo,
_blocks: BlockStoreInfo,
_transactions: TxStoreInfo,
_contracts: ContractStoreInfo,
}
impl BlockchainInfo {
pub fn new(blockchain: &Blockchain) -> BlockchainInfo {
BlockchainInfo {
_headers: HeaderStoreInfo::new(&blockchain.headers),
_blocks: BlockStoreInfo::new(&blockchain.blocks),
_transactions: TxStoreInfo::new(&blockchain.transactions),
_contracts: ContractStoreInfo::new(&blockchain.contracts),
}
}
}
fn statistics(folder: &str, node: &str, blockchain: &str) -> Result<()> {
println!("Retrieving blockchain statistics for {node}...");
// Node folder
let folder = folder.to_owned() + node;
// Initialize or load sled database
let path = folder.to_owned() + blockchain;
let db_path = expand_path(&path).unwrap();
let sled_db = sled::open(db_path)?;
// Retrieve statistics
let blockchain = Blockchain::new(&sled_db)?;
let (height, block) = blockchain.last()?;
let epoch = block_epoch(height);
let blocks = blockchain.len();
let txs = blockchain.txs_len();
drop(sled_db);
// Print statistics
println!("Latest height: {height}");
println!("Epoch: {epoch}");
println!("Latest block: {block}");
println!("Total blocks: {blocks}");
println!("Total transactions: {txs}");
Ok(())
}
fn export(folder: &str, node: &str, blockchain: &str) -> Result<()> {
println!("Exporting data for {node}...");
// Node folder
let folder = folder.to_owned() + node;
// Initialize or load sled database
let path = folder.to_owned() + blockchain;
let db_path = expand_path(&path).unwrap();
let sled_db = sled::open(db_path)?;
// Data export
let blockchain = Blockchain::new(&sled_db)?;
let info = BlockchainInfo::new(&blockchain);
let info_string = format!("{:#?}", info);
let file_name = node.to_owned() + "_db";
let mut file = File::create(file_name.clone())?;
file.write_all(info_string.as_bytes())?;
drop(sled_db);
println!("Data exported to file: {file_name}");
Ok(())
}
fn main() -> Result<()> {
// Parse arguments
let args = Args::parse();
println!("Node folder path: {}", args.path);
// Export data for each node
for node in args.node {
if args.export {
export(&args.path, &node, &args.blockchain)?;
continue
}
statistics(&args.path, &node, &args.blockchain)?;
}
Ok(())
}

View File

@@ -68,8 +68,14 @@ impl RequestHandler for BlockchainExplorer {
self.transactions_get_transaction_by_hash(req.id, req.params).await
}
// TODO: add statistics retrieval method
// TODO: add any other usefull method
// =====================
// Statistics methods
// =====================
"statistics.get_basic_statistics" => {
self.statistics_get_basic_statistics(req.id, req.params).await
}
// TODO: add any other usefull methods
// ==============
// Invalid method

View File

@@ -59,7 +59,7 @@ impl BlockchainExplorer {
/// If reset flag is provided, all tables are reset, and start syncing from beginning.
pub async fn sync_blocks(&self, reset: bool) -> WalletDbResult<()> {
// Grab last synced block height
let mut height = self.last_block().await?;
let (mut height, _) = self.last_block().await?;
// If last synced block is genesis (0) or reset flag
// has been provided we reset, otherwise continue with
// the next block height
@@ -253,7 +253,7 @@ pub async fn subscribe_blocks(
.darkfid_daemon_request("blockchain.last_known_block", &JsonValue::Array(vec![]))
.await?;
let last_known = *rep.get::<f64>().unwrap() as u32;
let last_synced = match explorer.last_block().await {
let (last_synced, _) = match explorer.last_block().await {
Ok(l) => l,
Err(e) => {
return Err(Error::RusqliteError(format!(

View File

@@ -0,0 +1,58 @@
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2024 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use log::error;
use tinyjson::JsonValue;
use darkfi::rpc::jsonrpc::{
ErrorCode::{InternalError, InvalidParams},
JsonError, JsonResponse, JsonResult,
};
use crate::BlockchainExplorer;
impl BlockchainExplorer {
// RPCAPI:
// Queries the database to retrieve current basic statistics.
// Returns the readable transaction upon success.
//
// **Params:**
// * `None`
//
// **Returns:**
// * `BaseStatistics` encoded into a JSON.
//
// --> {"jsonrpc": "2.0", "method": "statistics.get_basic_statistics", "params": [], "id": 1}
// <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
pub async fn statistics_get_basic_statistics(&self, id: u16, params: JsonValue) -> JsonResult {
let params = params.get::<Vec<JsonValue>>().unwrap();
if !params.is_empty() {
return JsonError::new(InvalidParams, None, id).into()
}
let base_statistics = match self.get_base_statistics().await {
Ok(v) => v,
Err(e) => {
error!(target: "blockchain-explorer::rpc_statistics::statistics_get_basic_statistics", "Failed fetching basic statistics: {}", e);
return JsonError::new(InternalError, None, id).into()
}
};
JsonResponse::new(base_statistics.to_json_array(), id).into()
}
}

View File

@@ -0,0 +1,96 @@
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2024 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use rusqlite::types::Value;
use tinyjson::JsonValue;
use darkfi_sdk::blockchain::block_epoch;
use drk::error::{WalletDbError, WalletDbResult};
use crate::{blocks::BLOCKS_TABLE, transactions::TRANSACTIONS_TABLE, BlockchainExplorer};
#[derive(Debug, Clone)]
/// Structure representing basic statistic extracted from the database.
pub struct BaseStatistics {
/// Current blockchain height
pub height: u32,
/// Current blockchain epoch (based on current height)
pub epoch: u8,
/// Blockchains' last block hash
pub last_block: String,
/// Blockchain total blocks
pub total_blocks: u64,
/// Blockchain total transactions
pub total_txs: u64,
}
impl BaseStatistics {
/// Auxiliary function to convert `BaseStatistics` into a `JsonValue` array.
pub fn to_json_array(&self) -> JsonValue {
JsonValue::Array(vec![
JsonValue::Number(self.height as f64),
JsonValue::Number(self.epoch as f64),
JsonValue::String(self.last_block.clone()),
JsonValue::Number(self.total_blocks as f64),
JsonValue::Number(self.total_txs as f64),
])
}
}
impl BlockchainExplorer {
/// Fetch total rows count of given table from the database.
pub async fn get_table_count(&self, table: &str) -> WalletDbResult<u64> {
// First we prepare the query
let query = format!("SELECT COUNT() FROM {};", table);
let Ok(conn) = self.database.conn.lock() else {
return Err(WalletDbError::FailedToAquireLock)
};
let Ok(mut stmt) = conn.prepare(&query) else {
return Err(WalletDbError::QueryPreparationFailed)
};
// Execute the query using provided params
let Ok(mut rows) = stmt.query([]) else { return Err(WalletDbError::QueryExecutionFailed) };
// Check if row exists
let Ok(next) = rows.next() else { return Err(WalletDbError::QueryExecutionFailed) };
let row = match next {
Some(row_result) => row_result,
None => return Ok(0_u64),
};
// Parse returned value
let Ok(count) = row.get(0) else { return Err(WalletDbError::ParseColumnValueError) };
let Value::Integer(count) = count else { return Err(WalletDbError::ParseColumnValueError) };
let Ok(count) = u64::try_from(count) else {
return Err(WalletDbError::ParseColumnValueError)
};
Ok(count)
}
/// Fetch current database basic statistic.
pub async fn get_base_statistics(&self) -> WalletDbResult<BaseStatistics> {
let (height, last_block) = self.last_block().await?;
let epoch = block_epoch(height);
let total_blocks = self.get_table_count(BLOCKS_TABLE).await?;
let total_txs = self.get_table_count(TRANSACTIONS_TABLE).await?;
Ok(BaseStatistics { height, epoch, last_block, total_blocks, total_txs })
}
}

View File

@@ -50,11 +50,11 @@ pub struct TransactionRecord {
impl TransactionRecord {
/// Auxiliary function to convert a `TransactionRecord` into a `JsonValue` array.
pub fn to_json_array(&self) -> JsonValue {
let mut ret = vec![];
ret.push(JsonValue::String(self.transaction_hash.clone()));
ret.push(JsonValue::String(self.header_hash.clone()));
ret.push(JsonValue::String(format!("{:?}", self.payload)));
JsonValue::Array(ret)
JsonValue::Array(vec![
JsonValue::String(self.transaction_hash.clone()),
JsonValue::String(self.header_hash.clone()),
JsonValue::String(format!("{:?}", self.payload)),
])
}
}