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

This commit is contained in:
skoupidi
2024-06-15 14:26:33 +03:00
parent 3dedf1e30e
commit 968f0c7202
8 changed files with 375 additions and 25 deletions

View File

@@ -1,4 +1,4 @@
-- Database block table definition.
-- Database blocks table definition.
-- We store data in a usable format.
CREATE TABLE IF NOT EXISTS blocks (
-- Header hash identifier of the block

View File

@@ -30,7 +30,7 @@ use drk::{
use crate::BlockchainExplorer;
// Dtabase SQL table constant names. These have to represent the `block.sql`
// Database SQL table constant names. These have to represent the `blocks.sql`
// SQL schema.
pub const BLOCKS_TABLE: &str = "blocks";
@@ -81,8 +81,8 @@ impl BlockRecord {
}
}
impl From<BlockInfo> for BlockRecord {
fn from(block: BlockInfo) -> Self {
impl From<&BlockInfo> for BlockRecord {
fn from(block: &BlockInfo) -> Self {
Self {
header_hash: block.hash().to_string(),
version: block.header.version,

View File

@@ -47,10 +47,14 @@ mod error;
mod rpc;
mod rpc_blocks;
use rpc_blocks::subscribe_blocks;
mod rpc_transactions;
/// Database functionality related to blocks
mod blocks;
/// Database functionality related to transactions
mod transactions;
const CONFIG_FILE: &str = "blockchain_explorer_config.toml";
const CONFIG_FILE_CONTENTS: &str = include_str!("../blockchain_explorer_config.toml");
@@ -153,10 +157,15 @@ impl BlockchainExplorer {
// Initialize all the database tables
if let Err(e) = explorer.initialize_blocks().await {
let err = format!("{e:?}");
error!(target: "blockchain-explorer", "Error initializing database tables: {err}");
error!(target: "blockchain-explorer", "Error initializing blocks database table: {err}");
return Err(Error::RusqliteError(err))
}
// TODO: map transaction structure to their corresponding files with sql table and retrieval methods
if let Err(e) = explorer.initialize_transactions().await {
let err = format!("{e:?}");
error!(target: "blockchain-explorer", "Error initializing transactions database table: {err}");
return Err(Error::RusqliteError(err))
}
// TODO: Map deployed contracts to their corresponding files with sql table and retrieval methods
Ok(explorer)
}

View File

@@ -49,18 +49,26 @@ impl RequestHandler for BlockchainExplorer {
"ping" => self.pong(req.id, req.params).await,
"ping_darkfid" => self.ping_darkfid(req.id, req.params).await,
// ==================
// =====================
// Blocks methods
// ==================
// =====================
"blocks.get_last_n_blocks" => self.blocks_get_last_n_blocks(req.id, req.params).await,
"blocks.get_blocks_in_heights_range" => {
self.blocks_get_blocks_in_heights_range(req.id, req.params).await
}
"blocks.get_block_by_hash" => self.blocks_get_block_by_hash(req.id, req.params).await,
// =====================
// Transactions methods
// =====================
"transactions.get_transactions_by_header_hash" => {
self.transactions_get_transactions_by_header_hash(req.id, req.params).await
}
"transactions.get_transaction_by_hash" => {
self.transactions_get_transaction_by_hash(req.id, req.params).await
}
// TODO: add statistics retrieval method
// TODO: add transactions retrieval method by their block hash
// TODO: add transaction retrieval method by its hash
// TODO: add any other usefull method
// ==============

View File

@@ -56,11 +56,11 @@ impl BlockchainExplorer {
}
/// Syncs the blockchain starting from the last synced block.
/// If reset flag is provided, all tables are reset, and start scanning from beginning.
/// 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 scanned block height
// Grab last synced block height
let mut height = self.last_block().await?;
// If last scanned block is genesis (0) or reset flag
// If last synced block is genesis (0) or reset flag
// has been provided we reset, otherwise continue with
// the next block height
if height == 0 || reset {
@@ -83,10 +83,10 @@ impl BlockchainExplorer {
};
let last = *rep.get::<f64>().unwrap() as u32;
info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Requested to scan from block number: {height}");
info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Requested to sync from block number: {height}");
info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Last known block number reported by darkfid: {last}");
// Already scanned last known block
// Already synced last known block
if height > last {
return Ok(())
}
@@ -102,11 +102,20 @@ impl BlockchainExplorer {
}
};
if let Err(e) = self.put_block(&block.into()).await {
error!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "[sync_blocks] Scan block failed: {e:?}");
if let Err(e) = self.put_block(&(&block).into()).await {
error!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "[sync_blocks] Insert block failed: {e:?}");
return Err(WalletDbError::GenericError)
};
let block_hash = block.hash().to_string();
for transaction in block.txs {
if let Err(e) = self.put_transaction(&(&block_hash, &transaction).into()).await
{
error!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "[sync_blocks] Insert block transaction failed: {e:?}");
return Err(WalletDbError::GenericError)
};
}
height += 1;
}
}
@@ -244,20 +253,20 @@ 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_scanned = match explorer.last_block().await {
let last_synced = match explorer.last_block().await {
Ok(l) => l,
Err(e) => {
return Err(Error::RusqliteError(format!(
"[subscribe_blocks] Retrieving last scanned block failed: {e:?}"
"[subscribe_blocks] Retrieving last synced block failed: {e:?}"
)))
}
};
if last_known != last_scanned {
warn!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Warning: Last scanned block is not the last known block.");
warn!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "You should first fully scan the blockchain, and then subscribe");
if last_known != last_synced {
warn!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Warning: Last synced block is not the last known block.");
warn!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "You should first fully sync the blockchain, and then subscribe");
return Err(Error::RusqliteError(
"[subscribe_blocks] Blockchain not fully scanned".to_string(),
"[subscribe_blocks] Blockchain not fully synced".to_string(),
))
}
@@ -332,11 +341,20 @@ pub async fn subscribe_blocks(
info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "=======================================");
info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Deserialized successfully. Storring block...");
if let Err(e) = explorer.put_block(&block_data.into()).await {
if let Err(e) = explorer.put_block(&(&block_data).into()).await {
return Err(Error::RusqliteError(format!(
"[subscribe_blocks] Scanning block failed: {e:?}"
"[subscribe_blocks] Insert block failed: {e:?}"
)))
}
let block_hash = block_data.hash().to_string();
for transaction in block_data.txs {
if let Err(e) = explorer.put_transaction(&(&block_hash, &transaction).into()).await {
return Err(Error::RusqliteError(format!(
"[subscribe_blocks] Insert block transaction failed: {e:?}"
)))
};
}
}
}

View File

@@ -0,0 +1,101 @@
/* 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 the transactions corresponding to the provided block header hash.
// Returns the readable transactions upon success.
//
// **Params:**
// * `array[0]`: `String` Block header hash
//
// **Returns:**
// * Array of `TransactionRecord` encoded into a JSON.
//
// --> {"jsonrpc": "2.0", "method": "transactions.get_transactions_by_header_hash", "params": ["5cc...2f9"], "id": 1}
// <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
pub async fn transactions_get_transactions_by_header_hash(
&self,
id: u16,
params: JsonValue,
) -> JsonResult {
let params = params.get::<Vec<JsonValue>>().unwrap();
if params.len() != 1 || !params[0].is_string() {
return JsonError::new(InvalidParams, None, id).into()
}
let header_hash = params[0].get::<String>().unwrap();
let transactions = match self.get_transactions_by_header_hash(header_hash) {
Ok(v) => v,
Err(e) => {
error!(target: "blockchain-explorer::rpc_transactions::transactions_get_transaction_by_header_hash", "Failed fetching block transactions: {}", e);
return JsonError::new(InternalError, None, id).into()
}
};
let mut ret = vec![];
for transaction in transactions {
ret.push(transaction.to_json_array());
}
JsonResponse::new(JsonValue::Array(ret), id).into()
}
// RPCAPI:
// Queries the database to retrieve the transaction corresponding to the provided hash.
// Returns the readable transaction upon success.
//
// **Params:**
// * `array[0]`: `String` Transaction hash
//
// **Returns:**
// * `TransactionRecord` encoded into a JSON.
//
// --> {"jsonrpc": "2.0", "method": "transactions.get_transaction_by_hash", "params": ["7e7...b4d"], "id": 1}
// <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
pub async fn transactions_get_transaction_by_hash(
&self,
id: u16,
params: JsonValue,
) -> JsonResult {
let params = params.get::<Vec<JsonValue>>().unwrap();
if params.len() != 1 || !params[0].is_string() {
return JsonError::new(InvalidParams, None, id).into()
}
let transaction_hash = params[0].get::<String>().unwrap();
let transaction = match self.get_transaction_by_hash(transaction_hash) {
Ok(v) => v,
Err(e) => {
error!(target: "blockchain-explorer::rpc_transactions::transactions_get_transaction_by_hash", "Failed fetching transaction: {}", e);
return JsonError::new(InternalError, None, id).into()
}
};
JsonResponse::new(transaction.to_json_array(), id).into()
}
}

View File

@@ -0,0 +1,200 @@
/* 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::info;
use rusqlite::types::Value;
use tinyjson::JsonValue;
use darkfi::{tx::Transaction, Error, Result};
use darkfi_serial::{deserialize, serialize};
use drk::{convert_named_params, error::WalletDbResult};
use crate::BlockchainExplorer;
// Database SQL table constant names. These have to represent the `transactions.sql`
// SQL schema.
pub const TRANSACTIONS_TABLE: &str = "transactions";
// TRANSACTIONS_TABLE
pub const TRANSACTIONS_COL_TRANSACTION_HASH: &str = "transaction_hash";
pub const TRANSACTIONS_COL_HEADER_HASH: &str = "header_hash";
pub const TRANSACTIONS_COL_PAYLOAD: &str = "payload";
#[derive(Debug, Clone)]
/// Structure representing a `TRANSACTIONS_TABLE` record.
pub struct TransactionRecord {
/// Transaction hash identifier
pub transaction_hash: String,
/// Header hash identifier of the block this transaction was included in
pub header_hash: String,
// TODO: Split the payload into a more easily readable fields
/// Transaction payload
pub payload: Transaction,
}
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)
}
}
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 BlockchainExplorer {
/// Initialize database with transactions tables.
pub async fn initialize_transactions(&self) -> WalletDbResult<()> {
// Initialize transactions database schema
let database_schema = include_str!("../transactions.sql");
self.database.exec_batch_sql(database_schema)?;
Ok(())
}
/// Reset transactions table in the database.
pub fn reset_transactions(&self) -> WalletDbResult<()> {
info!(target: "blockchain-explorer::transactions::reset_transactions", "Resetting transactions...");
let query = format!("DELETE FROM {};", TRANSACTIONS_TABLE);
self.database.exec_sql(&query, &[])
}
/// Import given transaction into the database.
pub async fn put_transaction(&self, transaction: &TransactionRecord) -> Result<()> {
let query = format!(
"INSERT OR REPLACE INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);",
TRANSACTIONS_TABLE,
TRANSACTIONS_COL_TRANSACTION_HASH,
TRANSACTIONS_COL_HEADER_HASH,
TRANSACTIONS_COL_PAYLOAD
);
if let Err(e) = self.database.exec_sql(
&query,
rusqlite::params![
transaction.transaction_hash,
transaction.header_hash,
serialize(&transaction.payload),
],
) {
return Err(Error::RusqliteError(format!(
"[put_transaction] Transaction insert failed: {e:?}"
)))
};
Ok(())
}
/// Auxiliary function to parse a `TRANSACTIONS_TABLE` record.
fn parse_transaction_record(&self, row: &[Value]) -> Result<TransactionRecord> {
let Value::Text(ref transaction_hash) = row[0] else {
return Err(Error::ParseFailed(
"[parse_transaction_record] Transaction hash parsing failed",
))
};
let transaction_hash = transaction_hash.clone();
let Value::Text(ref header_hash) = row[1] else {
return Err(Error::ParseFailed("[parse_transaction_record] Header hash parsing failed"))
};
let header_hash = header_hash.clone();
let Value::Blob(ref payload_bytes) = row[2] else {
return Err(Error::ParseFailed(
"[parse_transaction_record] Payload bytes bytes parsing failed",
))
};
let payload = deserialize(payload_bytes)?;
Ok(TransactionRecord { transaction_hash, header_hash, payload })
}
/// Fetch all known transactions from the database.
pub fn get_transactions(&self) -> Result<Vec<TransactionRecord>> {
let rows = match self.database.query_multiple(TRANSACTIONS_TABLE, &[], &[]) {
Ok(r) => r,
Err(e) => {
return Err(Error::RusqliteError(format!(
"[get_transactions] Transactions retrieval failed: {e:?}"
)))
}
};
let mut transactions = Vec::with_capacity(rows.len());
for row in rows {
transactions.push(self.parse_transaction_record(&row)?);
}
Ok(transactions)
}
/// Fetch all transactions from the database for the given block header hash.
pub fn get_transactions_by_header_hash(
&self,
header_hash: &str,
) -> Result<Vec<TransactionRecord>> {
let rows = match self.database.query_multiple(
TRANSACTIONS_TABLE,
&[],
convert_named_params! {(TRANSACTIONS_COL_HEADER_HASH, header_hash)},
) {
Ok(r) => r,
Err(e) => {
return Err(Error::RusqliteError(format!(
"[get_transactions_by_header_hash] Transactions retrieval failed: {e:?}"
)))
}
};
let mut transactions = Vec::with_capacity(rows.len());
for row in rows {
transactions.push(self.parse_transaction_record(&row)?);
}
Ok(transactions)
}
/// Fetch a transaction given its header hash.
pub fn get_transaction_by_hash(&self, transaction_hash: &str) -> Result<TransactionRecord> {
let row = match self.database.query_single(
TRANSACTIONS_TABLE,
&[],
convert_named_params! {(TRANSACTIONS_COL_TRANSACTION_HASH, transaction_hash)},
) {
Ok(r) => r,
Err(e) => {
return Err(Error::RusqliteError(format!(
"[get_transaction_by_hash] Transaction retrieval failed: {e:?}"
)))
}
};
self.parse_transaction_record(&row)
}
}

View File

@@ -0,0 +1,14 @@
-- Database transactions table definition.
-- We store data in a usable format.
CREATE TABLE IF NOT EXISTS transactions (
-- Transaction hash identifier
transaction_hash TEXT PRIMARY KEY NOT NULL,
-- Header hash identifier of the block this transaction was included in
header_hash TEXT NOT NULL,
-- TODO: Split the payload into a more easily readable fields
-- Transaction payload
payload BLOB NOT NULL,
FOREIGN KEY(header_hash) REFERENCES blocks(header_hash) ON DELETE CASCADE ON UPDATE CASCADE
);