use crate::storage::bitcoin_core_detection::BitcoinCoreNetwork;
use anyhow::{Context, Result};
use blvm_protocol::serialization::block::deserialize_block_with_witnesses;
use blvm_protocol::{Block, Hash};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use tracing::warn;
const MAGIC_MAINNET: [u8; 4] = [0xF9, 0xBE, 0xB4, 0xD9];
const MAGIC_TESTNET: [u8; 4] = [0x0B, 0x11, 0x09, 0x07];
const MAGIC_REGTEST: [u8; 4] = [0xFA, 0xBF, 0xB5, 0xDA];
const MAGIC_SIGNET: [u8; 4] = [0x0A, 0x03, 0xCF, 0x40];
#[derive(Debug, Clone, Serialize, Deserialize)]
struct BlockLocation {
file_path: PathBuf,
offset: u64,
size: u32,
}
#[derive(Serialize, Deserialize)]
struct BlockIndexCache {
version: u32,
network: String,
blocks_dir: PathBuf,
entries: Vec<(Hash, BlockLocation)>,
}
pub struct BitcoinCoreBlockReader {
blocks_dir: PathBuf,
network: BitcoinCoreNetwork,
block_index: Arc<Mutex<Option<HashMap<Hash, BlockLocation>>>>,
index_cache_path: Option<PathBuf>,
}
impl BitcoinCoreBlockReader {
pub fn new(blocks_dir: &Path, network: BitcoinCoreNetwork) -> Result<Self> {
Self::new_with_cache(blocks_dir, network, None)
}
pub fn new_with_cache(
blocks_dir: &Path,
network: BitcoinCoreNetwork,
cache_dir: Option<&Path>,
) -> Result<Self> {
if !blocks_dir.exists() {
return Err(anyhow::anyhow!(
"Blocks directory does not exist: {:?}",
blocks_dir
));
}
let index_cache_path = cache_dir.map(|dir| {
let network_str = match network {
BitcoinCoreNetwork::Mainnet => "mainnet",
BitcoinCoreNetwork::Testnet => "testnet",
BitcoinCoreNetwork::Regtest => "regtest",
BitcoinCoreNetwork::Signet => "signet",
};
dir.join(format!("block_index_{network_str}.bin"))
});
Ok(Self {
blocks_dir: blocks_dir.to_path_buf(),
network,
block_index: Arc::new(Mutex::new(None)),
index_cache_path,
})
}
fn get_magic(&self) -> &[u8; 4] {
match self.network {
BitcoinCoreNetwork::Mainnet => &MAGIC_MAINNET,
BitcoinCoreNetwork::Testnet => &MAGIC_TESTNET,
BitcoinCoreNetwork::Regtest => &MAGIC_REGTEST,
BitcoinCoreNetwork::Signet => &MAGIC_SIGNET,
}
}
fn build_index(&self) -> Result<HashMap<Hash, BlockLocation>> {
let mut index = HashMap::new();
let entries = std::fs::read_dir(&self.blocks_dir)
.with_context(|| format!("Failed to read blocks directory: {:?}", self.blocks_dir))?;
let mut block_files: Vec<PathBuf> = entries
.filter_map(|entry| {
entry.ok().and_then(|e| {
let path = e.path();
if path.is_file() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.starts_with("blk") && name.ends_with(".dat") {
return Some(path);
}
}
}
None
})
})
.collect();
block_files.sort();
let magic = self.get_magic();
for file_path in block_files {
if let Err(e) = self.scan_block_file(&file_path, magic, &mut index) {
eprintln!("Warning: Failed to scan block file {file_path:?}: {e}");
}
}
Ok(index)
}
fn scan_block_file(
&self,
file_path: &Path,
magic: &[u8; 4],
index: &mut HashMap<Hash, BlockLocation>,
) -> Result<()> {
let mut file = File::open(file_path)
.with_context(|| format!("Failed to open block file: {file_path:?}"))?;
let mut offset = 0u64;
loop {
let block_start = offset;
let mut magic_buf = [0u8; 4];
match file.read_exact(&mut magic_buf) {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
break;
}
Err(e) => return Err(anyhow::anyhow!("Failed to read magic bytes: {}", e)),
}
if magic_buf != *magic {
offset += 1;
file.seek(SeekFrom::Start(offset))?;
continue;
}
offset += 4;
let mut size_buf = [0u8; 4];
file.read_exact(&mut size_buf)
.context("Failed to read block size")?;
let block_size = u32::from_le_bytes(size_buf);
offset += 4;
if block_size > 32 * 1024 * 1024 {
return Err(anyhow::anyhow!(
"Block size too large: {} bytes at offset {}",
block_size,
offset - 4
));
}
if block_size < 80 {
return Err(anyhow::anyhow!(
"Block size {} too small for an 80-byte header at offset {}",
block_size,
block_start
));
}
let mut header_buf = [0u8; 80];
file.read_exact(&mut header_buf)
.context("Failed to read block header")?;
use sha2::{Digest, Sha256};
let first_hash = Sha256::digest(header_buf);
let second_hash = Sha256::digest(first_hash);
let mut block_hash = [0u8; 32];
block_hash.copy_from_slice(&second_hash);
let location = BlockLocation {
file_path: file_path.to_path_buf(),
offset: block_start,
size: block_size,
};
index.insert(block_hash, location);
let remaining = (block_size as u64) - 80;
file.seek(SeekFrom::Current(remaining as i64))
.context("Failed to seek to next block")?;
offset = block_start
.checked_add(8 + block_size as u64)
.ok_or_else(|| anyhow::anyhow!("Block file offset overflow"))?;
}
Ok(())
}
fn get_index(&self) -> Result<Arc<HashMap<Hash, BlockLocation>>> {
let mut index_guard = self.block_index.lock().unwrap();
if index_guard.is_none() {
if let Some(cache_path) = &self.index_cache_path {
if let Ok(index) = self.load_index_from_cache(cache_path) {
*index_guard = Some(index);
} else {
let index = self.build_index()?;
if let Err(e) = self.save_index_to_cache(cache_path, &index) {
warn!("Failed to save block index cache: {e}");
}
*index_guard = Some(index);
}
} else {
*index_guard = Some(self.build_index()?);
}
}
Ok(Arc::new(index_guard.as_ref().unwrap().clone()))
}
fn load_index_from_cache(&self, cache_path: &Path) -> Result<HashMap<Hash, BlockLocation>> {
use std::fs::File;
use std::io::Read;
let mut file = File::open(cache_path)
.with_context(|| format!("Failed to open index cache: {cache_path:?}"))?;
let mut data = Vec::new();
file.read_to_end(&mut data)
.context("Failed to read index cache")?;
let cache: BlockIndexCache = bincode::deserialize(&data)
.map_err(|e| anyhow::anyhow!("Failed to deserialize index cache: {}", e))?;
let network_str = match self.network {
BitcoinCoreNetwork::Mainnet => "mainnet",
BitcoinCoreNetwork::Testnet => "testnet",
BitcoinCoreNetwork::Regtest => "regtest",
BitcoinCoreNetwork::Signet => "signet",
};
if cache.network != network_str {
return Err(anyhow::anyhow!(
"Cache network mismatch: expected {}, got {}",
network_str,
cache.network
));
}
if cache.blocks_dir != self.blocks_dir {
return Err(anyhow::anyhow!(
"Cache blocks directory mismatch: expected {:?}, got {:?}",
self.blocks_dir,
cache.blocks_dir
));
}
Ok(cache.entries.into_iter().collect())
}
fn save_index_to_cache(
&self,
cache_path: &Path,
index: &HashMap<Hash, BlockLocation>,
) -> Result<()> {
use std::fs::File;
use std::io::Write;
if let Some(parent) = cache_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create cache directory: {parent:?}"))?;
}
let network_str = match self.network {
BitcoinCoreNetwork::Mainnet => "mainnet",
BitcoinCoreNetwork::Testnet => "testnet",
BitcoinCoreNetwork::Regtest => "regtest",
BitcoinCoreNetwork::Signet => "signet",
};
let cache = BlockIndexCache {
version: 1,
network: network_str.to_string(),
blocks_dir: self.blocks_dir.clone(),
entries: index.iter().map(|(k, v)| (*k, v.clone())).collect(),
};
let data = bincode::serialize(&cache)
.map_err(|e| anyhow::anyhow!("Failed to serialize index cache: {}", e))?;
let mut file = File::create(cache_path)
.with_context(|| format!("Failed to create index cache: {cache_path:?}"))?;
file.write_all(&data)
.context("Failed to write index cache")?;
Ok(())
}
pub fn read_block(&self, hash: &Hash) -> Result<Option<Block>> {
let index = self.get_index()?;
let location = match index.get(hash) {
Some(loc) => loc,
None => return Ok(None),
};
let mut file = File::open(&location.file_path)
.with_context(|| format!("Failed to open block file: {:?}", location.file_path))?;
file.seek(SeekFrom::Start(location.offset))
.with_context(|| {
format!(
"Failed to seek to block offset {} in file {:?}",
location.offset, location.file_path
)
})?;
let mut magic_buf = [0u8; 4];
file.read_exact(&mut magic_buf)
.context("Failed to read magic bytes")?;
let mut size_buf = [0u8; 4];
file.read_exact(&mut size_buf)
.context("Failed to read block size")?;
let mut block_data = vec![0u8; location.size as usize];
file.read_exact(&mut block_data).with_context(|| {
format!(
"Failed to read block data (size: {}) from file {:?}",
location.size, location.file_path
)
})?;
let (block, _witnesses) = deserialize_block_with_witnesses(&block_data)
.map_err(|e| anyhow::anyhow!("Failed to deserialize block: {}", e))?;
Ok(Some(block))
}
pub fn has_block(&self, hash: &Hash) -> Result<bool> {
let index = self.get_index()?;
Ok(index.contains_key(hash))
}
pub fn block_count(&self) -> Result<usize> {
let index = self.get_index()?;
Ok(index.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::TempDir;
fn create_test_block_file(dir: &Path, filename: &str, blocks: &[&[u8]]) -> Result<()> {
let file_path = dir.join(filename);
let mut file = File::create(&file_path)?;
for block_data in blocks {
file.write_all(&MAGIC_MAINNET)?;
let size = block_data.len() as u32;
file.write_all(&size.to_le_bytes())?;
file.write_all(block_data)?;
}
Ok(())
}
#[test]
fn test_block_reader_creation() {
let temp_dir = TempDir::new().unwrap();
let blocks_dir = temp_dir.path().join("blocks");
std::fs::create_dir_all(&blocks_dir).unwrap();
let reader = BitcoinCoreBlockReader::new(&blocks_dir, BitcoinCoreNetwork::Mainnet);
assert!(reader.is_ok());
let reader = reader.unwrap();
assert_eq!(reader.block_count().unwrap(), 0);
}
#[test]
fn test_block_reader_nonexistent_dir() {
let temp_dir = TempDir::new().unwrap();
let blocks_dir = temp_dir.path().join("nonexistent");
let reader = BitcoinCoreBlockReader::new(&blocks_dir, BitcoinCoreNetwork::Mainnet);
assert!(reader.is_err());
}
}