| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856 |
- /* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2026 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::{io, str::FromStr};
- use bytemuck::{Pod, Zeroable};
- use darkfi::{
- blockchain::{BlockInfo, Header},
- tx::Transaction,
- };
- use darkfi_deployooor_contract::{model::LockParamsV1, DeployFunction};
- use darkfi_sdk::{
- crypto::{schnorr::Signature, ContractId, DEPLOYOOOR_CONTRACT_ID},
- deploy::DeployParamsV1,
- };
- use darkfi_serial::{
- async_trait, deserialize, deserialize_async, serialize, serialize_async, SerialDecodable,
- SerialEncodable,
- };
- use sled::{transaction::TransactionError, Transactional};
- use tapes::{
- BlobTape, FixedSizedTape, Persistence, TapeOpenOptions, Tapes, TapesAppend, TapesRead,
- TapesTruncate,
- };
- use tracing::info;
- use super::Explorer;
- /// Contract information stored in sled
- #[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
- pub struct ContractData {
- pub contract_id: ContractId,
- pub locked: bool,
- pub wasm_size: u64,
- pub deploy_block: u64,
- pub deploy_tx_hash: [u8; 32],
- }
- /// Index entry for a block pointing to block data in the blob tape
- #[derive(Debug, Copy, Clone, Pod, Zeroable)]
- #[repr(C)]
- pub struct BlockIndex {
- pub offset: u64,
- pub length: u64,
- pub tx_count: u64,
- pub tx_start_idx: u64,
- }
- /// Index entry for a transaction pointing to tx data in the blob tape
- #[derive(Debug, Copy, Clone, Pod, Zeroable)]
- #[repr(C)]
- pub struct TxIndex {
- pub offset: u64,
- pub length: u64,
- pub block_height: u64,
- }
- /// Difficulty data for a block
- #[derive(Debug, Copy, Clone, Pod, Zeroable)]
- #[repr(C)]
- pub struct DifficultyIndex {
- pub difficulty: u64,
- pub cumulative: u64,
- }
- /// Structure holding all tapes in the database.
- pub struct TapesDatabase {
- pub block_index: FixedSizedTape<BlockIndex>,
- pub tx_index: FixedSizedTape<TxIndex>,
- pub difficulty_index: FixedSizedTape<DifficultyIndex>,
- pub blocks: BlobTape,
- pub transactions: BlobTape,
- }
- impl Explorer {
- pub fn open_tapes(db: &Tapes, options: &TapeOpenOptions) -> io::Result<TapesDatabase> {
- let mut tx = db.append();
- let block_index = tx.open_fixed_sized_tape("block_index", options)?;
- let tx_index = tx.open_fixed_sized_tape("tx_index", options)?;
- let difficulty_index = tx.open_fixed_sized_tape("diff_index", options)?;
- let blocks = tx.open_blob_tape("blocks", options)?;
- let transactions = tx.open_blob_tape("transactions", options)?;
- tx.commit(Persistence::Buffer)?;
- Ok(TapesDatabase { block_index, tx_index, difficulty_index, blocks, transactions })
- }
- /// Append a new block
- pub async fn append_block(&self, block: &BlockInfo, diff: &DifficultyIndex) -> io::Result<()> {
- let mut tx = self.tapes_db.append();
- let block_offset = tx.blob_tape_len(&self.database.blocks).unwrap_or(0);
- let tx_blob_offset = tx.blob_tape_len(&self.database.transactions).unwrap_or(0);
- let tx_start_idx = tx.fixed_sized_tape_len(&self.database.tx_index).unwrap_or(0);
- // Append block header
- let header_data = serialize_async(&block.header).await;
- tx.append_bytes(&self.database.blocks, &header_data)?;
- // Append all block transactions
- let mut current_tx_offset = tx_blob_offset;
- for transaction in &block.txs {
- let tx_data = serialize_async(transaction).await;
- tx.append_bytes(&self.database.transactions, &tx_data)?;
- let tx_idx = TxIndex {
- offset: current_tx_offset,
- length: tx_data.len() as u64,
- block_height: block.header.height as u64,
- };
- tx.append_entries(&self.database.tx_index, std::slice::from_ref(&tx_idx))?;
- current_tx_offset += tx_data.len() as u64;
- }
- // Append block index
- let block_idx = BlockIndex {
- offset: block_offset,
- length: header_data.len() as u64,
- tx_count: block.txs.len() as u64,
- tx_start_idx,
- };
- tx.append_entries(&self.database.block_index, std::slice::from_ref(&block_idx))?;
- // Append difficulty
- tx.append_entries(&self.database.difficulty_index, std::slice::from_ref(diff))?;
- // Commit Tapes first
- tx.commit(Persistence::SyncData)?;
- // Prepare data for atomic sled transaction
- let header_hash = serialize_async(&block.header.hash()).await;
- // Store height as u64 (8 bytes) to match lookup format
- let height_bytes = (block.header.height as u64).to_le_bytes();
- // Collect tx hashes and their indices
- let mut tx_entries: Vec<([u8; 32], [u8; 8])> = Vec::with_capacity(block.txs.len());
- for (i, transaction) in block.txs.iter().enumerate() {
- let tx_hash = *transaction.hash().inner();
- let tx_idx_pos = tx_start_idx + i as u64;
- tx_entries.push((tx_hash, tx_idx_pos.to_le_bytes()));
- }
- // Scan for contract deployments and locks
- let (new_contracts, locked_contracts) =
- self.scan_contract_calls(block, block.header.height as u64).await;
- // Atomic sled transaction for tx_indices, header_indices, and contracts
- (&self.tx_indices, &self.header_indices, &self.contracts)
- .transaction(|(tx_tree, header_tree, contracts_tree)| {
- // Insert all transaction indices
- for (hash, idx) in &tx_entries {
- tx_tree.insert(hash.as_slice(), idx.as_slice())?;
- }
- // Insert header hash -> height mapping
- header_tree.insert(header_hash.as_slice(), height_bytes.as_slice())?;
- // Insert new contracts
- for contract in &new_contracts {
- contracts_tree
- .insert(serialize(&contract.contract_id.inner()), serialize(contract))?;
- }
- // Update locked contracts
- for contract_id in &locked_contracts {
- let data = contracts_tree.get(serialize(&contract_id.inner()))?.unwrap();
- let mut contract: ContractData = deserialize(&data).unwrap();
- contract.locked = true;
- contracts_tree.insert(serialize(&contract_id.inner()), serialize(&contract))?;
- }
- Ok(())
- })
- .map_err(|e: TransactionError<sled::Error>| {
- io::Error::other(format!("sled transaction error: {e}"))
- })?;
- info!(
- target: "explorer::append_block",
- "Appended block {} ({} bytes header, {} txs)",
- block.header.height,
- header_data.len(),
- block.txs.len(),
- );
- // Update stats
- let block_size = header_data.len() as u64 + (current_tx_offset - tx_blob_offset);
- self.update_stats_for_block(
- block.header.timestamp.inner(),
- block.txs.len() as u64,
- block_size,
- )
- .await?;
- Ok(())
- }
- /// Revert n blocks from the database
- pub async fn revert_blocks(&self, count: u64) -> io::Result<()> {
- if count == 0 {
- return Ok(())
- }
- let reader = self.tapes_db.reader();
- let current_len = reader.fixed_sized_tape_len(&self.database.block_index).unwrap_or(0);
- if count > current_len {
- return Err(io::Error::new(
- io::ErrorKind::InvalidInput,
- "Cannot revert more blocks than exist",
- ))
- }
- let new_block_count = current_len - count;
- let current_tx_idx_len = reader.fixed_sized_tape_len(&self.database.tx_index).unwrap_or(0);
- // Collect data to remove from sled
- let mut header_hashes_to_remove: Vec<Vec<u8>> = Vec::new();
- let mut tx_hashes_to_remove: Vec<Vec<u8>> = Vec::new();
- let mut contracts_to_remove: Vec<ContractId> = Vec::new();
- for height in new_block_count..current_len {
- // Get the block index to find transactions
- let block_idx = reader
- .read_entry(&self.database.block_index, height)?
- .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "block index not found"))?;
- // Read header to get its hash
- let mut header_data = vec![0u8; block_idx.length as usize];
- reader.read_bytes(&self.database.blocks, block_idx.offset, &mut header_data)?;
- let header: Header = deserialize_async(&header_data).await?;
- header_hashes_to_remove.push(serialize_async(&header.hash()).await);
- // Read each tx to get its hash and check for contract deployments
- for i in 0..block_idx.tx_count {
- let tx_idx = reader
- .read_entry(&self.database.tx_index, block_idx.tx_start_idx + i)?
- .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "tx index not found"))?;
- let mut tx_data = vec![0u8; tx_idx.length as usize];
- reader.read_bytes(&self.database.transactions, tx_idx.offset, &mut tx_data)?;
- let transaction: Transaction = deserialize_async(&tx_data).await?;
- tx_hashes_to_remove.push(transaction.hash().0.to_vec());
- // Check for contract deployments to remove
- for call in &transaction.calls {
- if call.data.contract_id == *DEPLOYOOOR_CONTRACT_ID &&
- call.data.data[0] == DeployFunction::DeployV1 as u8
- {
- let params: DeployParamsV1 =
- deserialize_async(&call.data.data[1..]).await?;
- contracts_to_remove.push(ContractId::derive_public(params.public_key));
- }
- }
- }
- }
- let (new_block_blob_len, new_tx_idx_len, new_tx_blob_len) = if new_block_count == 0 {
- (0, 0, 0)
- } else {
- let last_block_idx = reader
- .read_entry(&self.database.block_index, new_block_count - 1)?
- .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "block index not found"))?;
- let new_block_blob_len = last_block_idx.offset + last_block_idx.length;
- let new_tx_idx_len = last_block_idx.tx_start_idx + last_block_idx.tx_count;
- let new_tx_blob_len = if new_tx_idx_len == 0 {
- 0
- } else {
- let last_tx_idx =
- reader.read_entry(&self.database.tx_index, new_tx_idx_len - 1)?.ok_or_else(
- || io::Error::new(io::ErrorKind::NotFound, "tx index not found"),
- )?;
- last_tx_idx.offset + last_tx_idx.length
- };
- (new_block_blob_len, new_tx_idx_len, new_tx_blob_len)
- };
- // Drop the reader before truncating
- drop(reader);
- let mut truncate_tx = self.tapes_db.truncate();
- truncate_tx.drop_fixed_sized_tape(&self.database.block_index, count);
- truncate_tx.drop_fixed_sized_tape(&self.database.difficulty_index, count);
- let tx_idx_to_remove = current_tx_idx_len - new_tx_idx_len;
- truncate_tx.drop_fixed_sized_tape(&self.database.tx_index, tx_idx_to_remove);
- truncate_tx.truncate_blob_tape(&self.database.blocks, new_block_blob_len);
- truncate_tx.truncate_blob_tape(&self.database.transactions, new_tx_blob_len);
- truncate_tx.commit(Persistence::SyncData)?;
- // Atomic sled transaction for removing tx, header indices, and contracts
- (&self.tx_indices, &self.header_indices, &self.contracts)
- .transaction(|(tx_tree, header_tree, contracts_tree)| {
- for tx_hash in &tx_hashes_to_remove {
- tx_tree.remove(tx_hash.as_slice())?;
- }
- for header_hash in &header_hashes_to_remove {
- header_tree.remove(header_hash.as_slice())?;
- }
- for contract_id in &contracts_to_remove {
- contracts_tree.remove(serialize(&contract_id.inner()))?;
- }
- Ok(())
- })
- .map_err(|e: TransactionError<sled::Error>| {
- io::Error::other(format!("sled transaction error: {e}"))
- })?;
- info!(
- target: "explorer::revert_blocks",
- "Reverted {} blocks (new height: {})",
- count,
- if new_block_count == 0 { 0 } else { new_block_count - 1 }
- );
- // Rebuild stats from scratch after reorg
- self.rebuild_stats().await?;
- Ok(())
- }
- /// Revert to a specific height (keep blocks 0..=target_height)
- pub async fn revert_to_height(&self, target_height: u64) -> io::Result<()> {
- let current_height = self.get_height()?.unwrap_or(0);
- if target_height >= current_height {
- return Ok(())
- }
- self.revert_blocks(current_height - target_height).await
- }
- /// Get the current known blockchain height
- pub fn get_height(&self) -> io::Result<Option<u64>> {
- let reader = self.tapes_db.reader();
- let len = reader.fixed_sized_tape_len(&self.database.block_index);
- Ok(len.filter(|&l| l > 0).map(|l| l - 1))
- }
- /// Get the difficulty and cumulative difficulty for a block height
- pub fn get_difficulty(&self, height: u64) -> io::Result<Option<DifficultyIndex>> {
- let reader = self.tapes_db.reader();
- reader.read_entry(&self.database.difficulty_index, height)
- }
- /// Get the block header for a height
- pub async fn get_header(&self, height: u64) -> io::Result<Option<Header>> {
- let reader = self.tapes_db.reader();
- let idx = match reader.read_entry(&self.database.block_index, height)? {
- Some(idx) => idx,
- None => return Ok(None),
- };
- let mut data = vec![0u8; idx.length as usize];
- reader.read_bytes(&self.database.blocks, idx.offset, &mut data)?;
- Ok(Some(deserialize_async(&data).await?))
- }
- /// Get all the transactions in a given block height
- pub async fn get_block_txs(&self, height: u64) -> io::Result<Option<Vec<Transaction>>> {
- let reader = self.tapes_db.reader();
- let block_idx = match reader.read_entry(&self.database.block_index, height)? {
- Some(idx) => idx,
- None => return Ok(None),
- };
- if block_idx.tx_count == 0 {
- return Ok(Some(vec![]))
- }
- // Read all TxIndex entries for this block
- let mut tx_indices = Vec::with_capacity(block_idx.tx_count as usize);
- for i in 0..block_idx.tx_count {
- let tx_idx = reader
- .read_entry(&self.database.tx_index, block_idx.tx_start_idx + i)?
- .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "tx index not found"))?;
- tx_indices.push(tx_idx);
- }
- if tx_indices.is_empty() {
- return Ok(Some(vec![]))
- }
- // Since transactions are stored contiguously, read all data at once
- let first_tx = &tx_indices[0];
- let last_tx = &tx_indices[tx_indices.len() - 1];
- let total_len = (last_tx.offset + last_tx.length - first_tx.offset) as usize;
- // Read all transaction data in one operation
- let mut all_tx_data = vec![0u8; total_len];
- reader.read_bytes(&self.database.transactions, first_tx.offset, &mut all_tx_data)?;
- // Deserialize each transaction from the combined buffer
- let mut txs = Vec::with_capacity(tx_indices.len());
- for tx_idx in &tx_indices {
- let start = (tx_idx.offset - first_tx.offset) as usize;
- let end = start + tx_idx.length as usize;
- txs.push(deserialize_async(&all_tx_data[start..end]).await?);
- }
- Ok(Some(txs))
- }
- /// Get and construct the entire block for a given height.
- pub async fn get_block(&self, height: u64) -> io::Result<Option<BlockInfo>> {
- let header = match self.get_header(height).await? {
- Some(h) => h,
- None => return Ok(None),
- };
- let txs = self.get_block_txs(height).await?.unwrap_or_default();
- // We don't care about displaying the block signature.
- Ok(Some(BlockInfo { header, txs, signature: Signature::dummy() }))
- }
- /// Get basic block info without loading all transactions.
- /// Returns (header, tx_count, total_size) for efficient latest_blocks display.
- pub async fn get_block_summary(&self, height: u64) -> io::Result<Option<(Header, u64, u64)>> {
- let reader = self.tapes_db.reader();
- let block_idx = match reader.read_entry(&self.database.block_index, height)? {
- Some(idx) => idx,
- None => return Ok(None),
- };
- let mut header_data = vec![0u8; block_idx.length as usize];
- reader.read_bytes(&self.database.blocks, block_idx.offset, &mut header_data)?;
- let header: Header = deserialize_async(&header_data).await?;
- // Calculate total size: header + all transactions
- let total_tx_size = if block_idx.tx_count == 0 {
- 0
- } else {
- let first_tx_idx = reader
- .read_entry(&self.database.tx_index, block_idx.tx_start_idx)?
- .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "tx index not found"))?;
- let last_tx_idx = reader
- .read_entry(
- &self.database.tx_index,
- block_idx.tx_start_idx + block_idx.tx_count - 1,
- )?
- .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "tx index not found"))?;
- last_tx_idx.offset + last_tx_idx.length - first_tx_idx.offset
- };
- let total_size = block_idx.length + total_tx_size;
- Ok(Some((header, block_idx.tx_count, total_size)))
- }
- /// Get a transaction by its hash.
- /// Returns the transaction and the block height it belongs to.
- pub async fn get_tx_by_hash(
- &self,
- tx_hash: &[u8; 32],
- ) -> io::Result<Option<(Transaction, u64)>> {
- // Look up the tx_index position from sled
- let tx_idx_pos = match self.tx_indices.get(tx_hash)? {
- Some(pos_bytes) => {
- let bytes: [u8; 8] = pos_bytes.as_ref().try_into().map_err(|_| {
- io::Error::new(io::ErrorKind::InvalidData, "invalid tx index position")
- })?;
- u64::from_le_bytes(bytes)
- }
- None => return Ok(None),
- };
- // Read the TxIndex from tapes
- let reader = self.tapes_db.reader();
- let tx_idx = match reader.read_entry(&self.database.tx_index, tx_idx_pos)? {
- Some(idx) => idx,
- None => return Ok(None),
- };
- // Read the transaction data from the blob tape
- let mut data = vec![0u8; tx_idx.length as usize];
- reader.read_bytes(&self.database.transactions, tx_idx.offset, &mut data)?;
- let transaction: Transaction = deserialize_async(&data).await?;
- Ok(Some((transaction, tx_idx.block_height)))
- }
- /// Get a transaction by its hash string (hex encoded).
- pub async fn get_tx_by_hash_str(
- &self,
- tx_hash_str: &str,
- ) -> io::Result<Option<(Transaction, u64)>> {
- let hash_bytes = hex::decode(tx_hash_str)
- .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid hex string"))?;
- if hash_bytes.len() != 32 {
- return Err(io::Error::new(io::ErrorKind::InvalidInput, "hash must be 32 bytes"));
- }
- let mut tx_hash = [0u8; 32];
- tx_hash.copy_from_slice(&hash_bytes);
- self.get_tx_by_hash(&tx_hash).await
- }
- /// Scan a block's transactions for contract deployments and locks.
- /// Returns (new_contracts, locked_contract_ids)
- async fn scan_contract_calls(
- &self,
- block: &BlockInfo,
- block_height: u64,
- ) -> (Vec<ContractData>, Vec<ContractId>) {
- let mut new_contracts = Vec::new();
- let mut locked_contracts = Vec::new();
- for transaction in &block.txs {
- let tx_hash = *transaction.hash().inner();
- for call in &transaction.calls {
- // Check if this is a call to Deployoor
- if call.data.contract_id != *DEPLOYOOOR_CONTRACT_ID {
- continue;
- }
- let func = call.data.data[0];
- if func == DeployFunction::DeployV1 as u8 {
- let params: DeployParamsV1 =
- deserialize_async(&call.data.data[1..]).await.unwrap();
- let contract_id = ContractId::derive_public(params.public_key);
- info!(
- target: "explorer::scan_contract_calls",
- "Found contract deployment: {} (size: {} bytes)",
- contract_id, params.wasm_bincode.len(),
- );
- new_contracts.push(ContractData {
- contract_id,
- locked: false,
- wasm_size: params.wasm_bincode.len() as u64,
- deploy_block: block_height,
- deploy_tx_hash: tx_hash,
- });
- } else if func == DeployFunction::LockV1 as u8 {
- let params: LockParamsV1 =
- deserialize_async(&call.data.data[1..]).await.unwrap();
- let contract_id = ContractId::derive_public(params.public_key);
- info!(
- target: "explorer::scan_contract_calls",
- "Found contract lock: {}", contract_id,
- );
- locked_contracts.push(contract_id);
- }
- }
- }
- (new_contracts, locked_contracts)
- }
- /// Get a contract by its ID string (base58 encoded).
- pub async fn get_contract(&self, contract_id_str: &str) -> io::Result<Option<ContractData>> {
- let Ok(contract_id) = ContractId::from_str(contract_id_str) else {
- return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid contract ID"))
- };
- match self.contracts.get(serialize_async(&contract_id.inner()).await)? {
- Some(data) => Ok(Some(deserialize_async(&data).await?)),
- None => Ok(None),
- }
- }
- /// List all contracts, optionally filtered by locked status.
- pub async fn list_contracts(
- &self,
- locked_filter: Option<bool>,
- ) -> io::Result<Vec<ContractData>> {
- let mut contracts = Vec::new();
- for result in self.contracts.iter() {
- let (_, value) = result?;
- let contract: ContractData = deserialize_async(&value).await?;
- if let Some(filter) = locked_filter {
- if contract.locked == filter {
- contracts.push(contract);
- }
- } else {
- contracts.push(contract);
- }
- }
- Ok(contracts)
- }
- /// Get the total number of contracts.
- pub fn get_contract_count(&self) -> io::Result<u64> {
- Ok(self.contracts.len() as u64)
- }
- }
- /// Daily statistics aggregate
- #[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
- pub struct DailyStats {
- pub block_count: u64,
- pub user_tx_count: u64, // excluding coinbase
- pub total_size: u64,
- }
- /// Monthly statistics aggregate
- #[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
- pub struct MonthlyStats {
- pub block_count: u64,
- pub total_size: u64,
- }
- impl Explorer {
- /// Update stats for a single block
- pub async fn update_stats_for_block(
- &self,
- timestamp: u64,
- tx_count: u64,
- block_size: u64,
- ) -> io::Result<()> {
- let day = Self::day_from_timestamp(timestamp);
- let (year, month) = Self::year_month_from_timestamp(timestamp);
- let user_tx = tx_count.saturating_sub(1); // exclude coinbase
- // Update daily stats
- let daily_key = format!("daily:{}", day);
- let mut daily = self.get_daily_stats(day).await?.unwrap_or(DailyStats {
- block_count: 0,
- user_tx_count: 0,
- total_size: 0,
- });
- daily.block_count += 1;
- daily.user_tx_count += user_tx;
- daily.total_size += block_size;
- self.stats.insert(daily_key.as_bytes(), serialize_async(&daily).await)?;
- // Update monthly stats
- let monthly_key = format!("monthly:{}:{:02}", year, month);
- let mut monthly = self
- .get_monthly_stats(year, month)
- .await?
- .unwrap_or(MonthlyStats { block_count: 0, total_size: 0 });
- monthly.block_count += 1;
- monthly.total_size += block_size;
- self.stats.insert(monthly_key.as_bytes(), serialize_async(&monthly).await)?;
- Ok(())
- }
- /// Get daily stats for a specific day
- pub async fn get_daily_stats(&self, day: u64) -> io::Result<Option<DailyStats>> {
- let key = format!("daily:{}", day);
- match self.stats.get(key.as_bytes())? {
- Some(data) => Ok(Some(deserialize_async(&data).await?)),
- None => Ok(None),
- }
- }
- /// Get monthly stats for a specific year/month
- pub async fn get_monthly_stats(
- &self,
- year: u32,
- month: u32,
- ) -> io::Result<Option<MonthlyStats>> {
- let key = format!("monthly:{}:{:02}", year, month);
- match self.stats.get(key.as_bytes())? {
- Some(data) => Ok(Some(deserialize_async(&data).await?)),
- None => Ok(None),
- }
- }
- /// Get all daily stats (for graph generation)
- pub async fn get_all_daily_stats(&self) -> io::Result<Vec<(u64, DailyStats)>> {
- let mut result = Vec::new();
- let prefix = b"daily:";
- for item in self.stats.scan_prefix(prefix) {
- let (key, value) = item?;
- let key_str = String::from_utf8_lossy(&key);
- if let Some(day_str) = key_str.strip_prefix("daily:") {
- if let Ok(day) = day_str.parse::<u64>() {
- let stats = deserialize_async(&value).await?;
- result.push((day, stats));
- }
- }
- }
- result.sort_by_key(|(day, _)| *day);
- Ok(result)
- }
- /// Get all monthly stats (for table generation)
- pub async fn get_all_monthly_stats(&self) -> io::Result<Vec<(u32, u32, MonthlyStats)>> {
- let mut result = Vec::new();
- let prefix = b"monthly:";
- for item in self.stats.scan_prefix(prefix) {
- let (key, value) = item?;
- let key_str = String::from_utf8_lossy(&key);
- if let Some(ym_str) = key_str.strip_prefix("monthly:") {
- let parts: Vec<&str> = ym_str.split(':').collect();
- if parts.len() == 2 {
- if let (Ok(year), Ok(month)) =
- (parts[0].parse::<u32>(), parts[1].parse::<u32>())
- {
- let stats = deserialize_async(&value).await?;
- result.push((year, month, stats));
- }
- }
- }
- }
- result.sort_by_key(|(year, month, _)| (*year, *month));
- Ok(result)
- }
- /// Clear all stats (called before rebuilding after reorg)
- pub fn clear_stats(&self) -> io::Result<()> {
- // Clear daily stats
- let daily_keys: Vec<_> =
- self.stats.scan_prefix(b"daily:").filter_map(|r| r.ok().map(|(k, _)| k)).collect();
- for key in daily_keys {
- self.stats.remove(&key)?;
- }
- // Clear monthly stats
- let monthly_keys: Vec<_> =
- self.stats.scan_prefix(b"monthly:").filter_map(|r| r.ok().map(|(k, _)| k)).collect();
- for key in monthly_keys {
- self.stats.remove(&key)?;
- }
- Ok(())
- }
- /// Rebuild all stats from blockchain data
- pub async fn rebuild_stats(&self) -> io::Result<()> {
- info!(target: "explorer::rebuild_stats", "Clearing existing stats...");
- self.clear_stats()?;
- let height = match self.get_height()? {
- Some(h) => h,
- None => return Ok(()), // No blocks yet
- };
- info!(target: "explorer::rebuild_stats", "Rebuilding stats for {} blocks...", height + 1);
- let reader = self.tapes_db.reader();
- for h in 0..=height {
- let block_idx = match reader.read_entry(&self.database.block_index, h)? {
- Some(idx) => idx,
- None => continue,
- };
- // Read header to get timestamp
- let mut header_data = vec![0u8; block_idx.length as usize];
- reader.read_bytes(&self.database.blocks, block_idx.offset, &mut header_data)?;
- let header: Header = deserialize_async(&header_data).await?;
- // Calculate block size
- let tx_size = if block_idx.tx_count == 0 {
- 0
- } else {
- let first_tx_idx = reader
- .read_entry(&self.database.tx_index, block_idx.tx_start_idx)?
- .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "tx index not found"))?;
- let last_tx_idx = reader
- .read_entry(
- &self.database.tx_index,
- block_idx.tx_start_idx + block_idx.tx_count - 1,
- )?
- .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "tx index not found"))?;
- last_tx_idx.offset + last_tx_idx.length - first_tx_idx.offset
- };
- let block_size = block_idx.length + tx_size;
- self.update_stats_for_block(header.timestamp.inner(), block_idx.tx_count, block_size)
- .await?;
- }
- info!(target: "explorer::rebuild_stats", "Stats rebuild complete");
- Ok(())
- }
- /// Get day number from unix timestamp (days since epoch)
- fn day_from_timestamp(timestamp: u64) -> u64 {
- timestamp / 86400
- }
- /// Get year and month from unix timestamp
- fn year_month_from_timestamp(timestamp: u64) -> (u32, u32) {
- // Days since epoch
- let days = timestamp / 86400;
- // Approximate year (will be corrected)
- let mut year = 1970u32;
- let mut remaining_days = days as i64;
- loop {
- let days_in_year = if year.is_multiple_of(4) &&
- (!year.is_multiple_of(100) || year.is_multiple_of(400))
- {
- 366i64
- } else {
- 365i64
- };
- if remaining_days < days_in_year {
- break;
- }
- remaining_days -= days_in_year;
- year += 1;
- }
- // Now find month
- let is_leap =
- year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400));
- let days_in_months: [i64; 12] = if is_leap {
- [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
- } else {
- [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
- };
- let mut month = 1u32;
- for days_in_month in days_in_months.iter() {
- if remaining_days < *days_in_month {
- break;
- }
- remaining_days -= days_in_month;
- month += 1;
- }
- (year, month)
- }
- }
|