/* 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 .
*/
use log::info;
use rusqlite::types::Value;
use tinyjson::JsonValue;
use darkfi::{blockchain::BlockInfo, Error, Result};
use darkfi_sdk::crypto::schnorr::Signature;
use darkfi_serial::{deserialize, serialize};
use drk::{
convert_named_params,
error::{WalletDbError, WalletDbResult},
};
use crate::BlockchainExplorer;
// Database SQL table constant names. These have to represent the `blocks.sql`
// SQL schema.
pub const BLOCKS_TABLE: &str = "blocks";
// BLOCKS_TABLE
pub const BLOCKS_COL_HEADER_HASH: &str = "header_hash";
pub const BLOCKS_COL_VERSION: &str = "version";
pub const BLOCKS_COL_PREVIOUS: &str = "previous";
pub const BLOCKS_COL_HEIGHT: &str = "height";
pub const BLOCKS_COL_TIMESTAMP: &str = "timestamp";
pub const BLOCKS_COL_NONCE: &str = "nonce";
pub const BLOCKS_COL_ROOT: &str = "root";
pub const BLOCKS_COL_SIGNATURE: &str = "signature";
#[derive(Debug, Clone)]
/// Structure representing a `BLOCKS_TABLE` record.
pub struct BlockRecord {
/// Header hash identifier of the block
pub header_hash: String,
/// Block version
pub version: u8,
/// Previous block hash
pub previous: String,
/// Block height
pub height: u32,
/// Block creation timestamp
pub timestamp: u64,
/// The block's nonce. This value changes arbitrarily with mining.
pub nonce: u64,
/// Merkle tree root of the transactions hashes contained in this block
pub root: String,
/// Block producer signature
pub signature: Signature,
}
impl BlockRecord {
/// Auxiliary function to convert a `BlockRecord` into a `JsonValue` array.
pub fn to_json_array(&self) -> JsonValue {
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)),
])
}
}
impl From<&BlockInfo> for BlockRecord {
fn from(block: &BlockInfo) -> Self {
Self {
header_hash: block.hash().to_string(),
version: block.header.version,
previous: block.header.previous.to_string(),
height: block.header.height,
timestamp: block.header.timestamp.inner(),
nonce: block.header.nonce,
root: block.header.root.to_string(),
signature: block.signature,
}
}
}
impl BlockchainExplorer {
/// Initialize database with blocks tables.
pub async fn initialize_blocks(&self) -> WalletDbResult<()> {
// Initialize blocks database schema
let database_schema = include_str!("../blocks.sql");
self.database.exec_batch_sql(database_schema)?;
Ok(())
}
/// Reset blocks table in the database.
pub fn reset_blocks(&self) -> WalletDbResult<()> {
info!(target: "blockchain-explorer::blocks::reset_blocks", "Resetting blocks...");
let query = format!("DELETE FROM {};", BLOCKS_TABLE);
self.database.exec_sql(&query, &[])
}
/// Import given block into the database.
pub async fn put_block(&self, block: &BlockRecord) -> Result<()> {
let query = format!(
"INSERT OR REPLACE INTO {} ({}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);",
BLOCKS_TABLE,
BLOCKS_COL_HEADER_HASH,
BLOCKS_COL_VERSION,
BLOCKS_COL_PREVIOUS,
BLOCKS_COL_HEIGHT,
BLOCKS_COL_TIMESTAMP,
BLOCKS_COL_NONCE,
BLOCKS_COL_ROOT,
BLOCKS_COL_SIGNATURE
);
if let Err(e) = self.database.exec_sql(
&query,
rusqlite::params![
block.header_hash,
block.version,
block.previous,
block.height,
block.timestamp,
block.nonce,
block.root,
serialize(&block.signature),
],
) {
return Err(Error::RusqliteError(format!("[put_block] Block insert failed: {e:?}")))
};
Ok(())
}
/// Auxiliary function to parse a `BLOCKS_TABLE` record.
fn parse_block_record(&self, row: &[Value]) -> Result {
let Value::Text(ref header_hash) = row[0] else {
return Err(Error::ParseFailed("[parse_block_record] Header hash parsing failed"))
};
let header_hash = header_hash.clone();
let Value::Integer(version) = row[1] else {
return Err(Error::ParseFailed("[parse_block_record] Version parsing failed"))
};
let Ok(version) = u8::try_from(version) else {
return Err(Error::ParseFailed("[parse_block_record] Version parsing failed"))
};
let Value::Text(ref previous) = row[2] else {
return Err(Error::ParseFailed("[parse_block_record] Previous parsing failed"))
};
let previous = previous.clone();
let Value::Integer(height) = row[3] else {
return Err(Error::ParseFailed("[parse_block_record] Height parsing failed"))
};
let Ok(height) = u32::try_from(height) else {
return Err(Error::ParseFailed("[parse_block_record] Height parsing failed"))
};
let Value::Integer(timestamp) = row[4] else {
return Err(Error::ParseFailed("[parse_block_record] Timestamp parsing failed"))
};
let Ok(timestamp) = u64::try_from(timestamp) else {
return Err(Error::ParseFailed("[parse_block_record] Timestamp parsing failed"))
};
let Value::Integer(nonce) = row[5] else {
return Err(Error::ParseFailed("[parse_block_record] Nonce parsing failed"))
};
let Ok(nonce) = u64::try_from(nonce) else {
return Err(Error::ParseFailed("[parse_block_record] Nonce parsing failed"))
};
let Value::Text(ref root) = row[6] else {
return Err(Error::ParseFailed("[parse_block_record] Root parsing failed"))
};
let root = root.clone();
let Value::Blob(ref signature_bytes) = row[7] else {
return Err(Error::ParseFailed(
"[parse_block_record] Signature bytes bytes parsing failed",
))
};
let signature = deserialize(signature_bytes)?;
Ok(BlockRecord {
header_hash,
version,
previous,
height,
timestamp,
nonce,
root,
signature,
})
}
/// Fetch all known blocks from the database.
pub fn get_blocks(&self) -> Result> {
let rows = match self.database.query_multiple(BLOCKS_TABLE, &[], &[]) {
Ok(r) => r,
Err(e) => {
return Err(Error::RusqliteError(format!(
"[get_blocks] Blocks retrieval failed: {e:?}"
)))
}
};
let mut blocks = Vec::with_capacity(rows.len());
for row in rows {
blocks.push(self.parse_block_record(&row)?);
}
Ok(blocks)
}
/// Fetch a block given its header hash.
pub fn get_block_by_hash(&self, header_hash: &str) -> Result {
let row = match self.database.query_single(
BLOCKS_TABLE,
&[],
convert_named_params! {(BLOCKS_COL_HEADER_HASH, header_hash)},
) {
Ok(r) => r,
Err(e) => {
return Err(Error::RusqliteError(format!(
"[get_block_by_hash] Block retrieval failed: {e:?}"
)))
}
};
self.parse_block_record(&row)
}
/// 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_HEADER_HASH, BLOCKS_COL_HEIGHT, BLOCKS_TABLE, BLOCKS_COL_HEIGHT
);
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_u32, "".to_string())),
};
// 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)
};
let Ok(height) = u32::try_from(height) else {
return Err(WalletDbError::ParseColumnValueError)
};
Ok((height, header_hash))
}
/// Auxiliary function to parse a `BLOCKS_TABLE` query rows into block records.
fn parse_blocks_query_rows(&self, rows: &mut rusqlite::Rows) -> Result> {
// Loop over returned rows and parse them
let mut records = vec![];
loop {
// Check if an error occured
let row = match rows.next() {
Ok(r) => r,
Err(_) => {
return Err(Error::RusqliteError(format!(
"[get_last_n_blocks] {}",
WalletDbError::QueryExecutionFailed
)))
}
};
// Check if no row was returned
let row = match row {
Some(r) => r,
None => break,
};
// Grab row returned values
let mut row_values = vec![];
let mut idx = 0;
loop {
let Ok(value) = row.get(idx) else { break };
row_values.push(value);
idx += 1;
}
records.push(row_values);
}
// Parse the records into blocks
let mut blocks = Vec::with_capacity(records.len());
for record in records {
blocks.push(self.parse_block_record(&record)?);
}
Ok(blocks)
}
/// Fetch last N blocks from the database.
pub fn get_last_n_blocks(&self, n: u16) -> Result> {
// First we prepare the query
let query = format!(
"SELECT * FROM {} ORDER BY {} DESC LIMIT {};",
BLOCKS_TABLE, BLOCKS_COL_HEIGHT, n
);
let Ok(conn) = self.database.conn.lock() else {
return Err(Error::RusqliteError(format!(
"[get_last_n_blocks] {}",
WalletDbError::FailedToAquireLock
)))
};
let Ok(mut stmt) = conn.prepare(&query) else {
return Err(Error::RusqliteError(format!(
"[get_last_n_blocks] {}",
WalletDbError::QueryPreparationFailed
)))
};
// Execute the query using provided params
let Ok(mut rows) = stmt.query([]) else {
return Err(Error::RusqliteError(format!(
"[get_last_n_blocks] {}",
WalletDbError::QueryExecutionFailed
)))
};
self.parse_blocks_query_rows(&mut rows)
}
/// Fetch last N blocks from the database.
pub fn get_blocks_in_heights_range(&self, start: u32, end: u32) -> Result> {
// First we prepare the query
let query = format!(
"SELECT * FROM {} WHERE {} >= {} AND {} <= {} ORDER BY {} ASC;",
BLOCKS_TABLE, BLOCKS_COL_HEIGHT, start, BLOCKS_COL_HEIGHT, end, BLOCKS_COL_HEIGHT
);
let Ok(conn) = self.database.conn.lock() else {
return Err(Error::RusqliteError(format!(
"[get_blocks_in_height_range] {}",
WalletDbError::FailedToAquireLock
)))
};
let Ok(mut stmt) = conn.prepare(&query) else {
return Err(Error::RusqliteError(format!(
"[get_blocks_in_height_range] {}",
WalletDbError::QueryPreparationFailed
)))
};
// Execute the query using provided params
let Ok(mut rows) = stmt.query([]) else {
return Err(Error::RusqliteError(format!(
"[get_blocks_in_height_range] {}",
WalletDbError::QueryExecutionFailed
)))
};
self.parse_blocks_query_rows(&mut rows)
}
}