/* 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 .
*/
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,
pub tx_index: FixedSizedTape,
pub difficulty_index: FixedSizedTape,
pub blocks: BlobTape,
pub transactions: BlobTape,
}
impl Explorer {
pub fn open_tapes(db: &Tapes, options: &TapeOpenOptions) -> io::Result {
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| {
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::new();
let mut tx_hashes_to_remove: Vec> = Vec::new();
let mut contracts_to_remove: Vec = 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| {
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