use std::ffi::CString;
use libbitcoinkernel_sys::{
btck_BlockHash, btck_ChainstateManager, btck_ChainstateManagerOptions, btck_block_read,
btck_block_spent_outputs_read, btck_chainstate_manager_create, btck_chainstate_manager_destroy,
btck_chainstate_manager_get_active_chain, btck_chainstate_manager_get_block_tree_entry_by_hash,
btck_chainstate_manager_import_blocks, btck_chainstate_manager_options_create,
btck_chainstate_manager_options_destroy, btck_chainstate_manager_options_set_wipe_dbs,
btck_chainstate_manager_options_set_worker_threads_num,
btck_chainstate_manager_options_update_block_tree_db_in_memory,
btck_chainstate_manager_options_update_chainstate_db_in_memory,
btck_chainstate_manager_process_block,
};
use crate::{
ffi::{
c_helpers,
sealed::{AsPtr, FromMutPtr, FromPtr},
},
Block, BlockHash, BlockSpentOutputs, BlockTreeEntry, KernelError,
};
use super::{Chain, Context};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcessBlockResult {
NewBlock,
Duplicate,
Rejected,
}
impl ProcessBlockResult {
pub fn is_new_block(&self) -> bool {
matches!(self, Self::NewBlock)
}
pub fn is_duplicate(&self) -> bool {
matches!(self, Self::Duplicate)
}
pub fn is_rejected(&self) -> bool {
matches!(self, Self::Rejected)
}
}
pub struct ChainstateManager {
inner: *mut btck_ChainstateManager,
}
unsafe impl Send for ChainstateManager {}
unsafe impl Sync for ChainstateManager {}
impl ChainstateManager {
pub fn builder(
context: &Context,
data_dir: &str,
blocks_dir: &str,
) -> Result<ChainstateManagerBuilder, KernelError> {
ChainstateManagerBuilder::new(context, data_dir, blocks_dir)
}
pub fn new(
context: &Context,
data_dir: &str,
blocks_dir: &str,
) -> Result<ChainstateManager, KernelError> {
ChainstateManagerBuilder::new(context, data_dir, blocks_dir)?.build()
}
pub fn process_block(&self, block: &Block) -> ProcessBlockResult {
let mut new_block: i32 = 0;
let accepted = unsafe {
btck_chainstate_manager_process_block(self.inner, block.as_ptr(), &mut new_block)
};
let is_accepted = c_helpers::success(accepted);
let is_new = c_helpers::enabled(new_block);
match (is_accepted, is_new) {
(true, true) => ProcessBlockResult::NewBlock,
(true, false) => ProcessBlockResult::Duplicate,
(false, _) => ProcessBlockResult::Rejected,
}
}
pub fn import_blocks(&self) -> Result<(), KernelError> {
let result = unsafe {
btck_chainstate_manager_import_blocks(
self.inner,
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
)
};
match c_helpers::success(result) {
true => Ok(()),
false => Err(KernelError::Internal(
"Failed to import blocks.".to_string(),
)),
}
}
pub fn read_block_data(&self, entry: &BlockTreeEntry) -> Result<Block, KernelError> {
let inner = unsafe { btck_block_read(self.inner, entry.as_ptr()) };
if inner.is_null() {
return Err(KernelError::Internal("Failed to read block.".to_string()));
}
Ok(unsafe { Block::from_ptr(inner) })
}
pub fn read_spent_outputs(
&self,
entry: &BlockTreeEntry,
) -> Result<BlockSpentOutputs, KernelError> {
let inner = unsafe { btck_block_spent_outputs_read(self.inner, entry.as_ptr()) };
if inner.is_null() {
return Err(KernelError::Internal(
"Failed to read undo data.".to_string(),
));
}
Ok(unsafe { BlockSpentOutputs::from_ptr(inner) })
}
pub fn active_chain(&self) -> Chain<'_> {
let ptr = unsafe { btck_chainstate_manager_get_active_chain(self.inner) };
unsafe { Chain::from_ptr(ptr) }
}
pub fn get_block_tree_entry(&self, block_hash: &BlockHash) -> Option<BlockTreeEntry<'_>> {
let ptr = unsafe {
btck_chainstate_manager_get_block_tree_entry_by_hash(
self.inner,
block_hash as *const _ as *const btck_BlockHash,
)
};
if ptr.is_null() {
None
} else {
Some(unsafe { BlockTreeEntry::from_ptr(ptr) })
}
}
}
impl Drop for ChainstateManager {
fn drop(&mut self) {
unsafe {
btck_chainstate_manager_destroy(self.inner);
}
}
}
pub struct ChainstateManagerBuilder {
inner: *mut btck_ChainstateManagerOptions,
}
impl ChainstateManagerBuilder {
pub fn new(context: &Context, data_dir: &str, blocks_dir: &str) -> Result<Self, KernelError> {
let c_data_dir = CString::new(data_dir)?;
let c_blocks_dir = CString::new(blocks_dir)?;
let inner = unsafe {
btck_chainstate_manager_options_create(
context.as_ptr(),
c_data_dir.as_ptr(),
c_data_dir.as_bytes().len(),
c_blocks_dir.as_ptr(),
c_blocks_dir.as_bytes().len(),
)
};
if inner.is_null() {
return Err(KernelError::Internal(
"Failed to create chainstate manager options.".to_string(),
));
}
Ok(Self { inner })
}
pub fn worker_threads(self, worker_threads: i32) -> Self {
unsafe {
btck_chainstate_manager_options_set_worker_threads_num(self.inner, worker_threads);
}
self
}
pub fn wipe_db(
self,
wipe_block_tree: bool,
wipe_chainstate: bool,
) -> Result<Self, KernelError> {
let result = unsafe {
btck_chainstate_manager_options_set_wipe_dbs(
self.inner,
c_helpers::to_c_bool(wipe_block_tree),
c_helpers::to_c_bool(wipe_chainstate),
)
};
if c_helpers::success(result) {
Ok(self)
} else {
Err(KernelError::InvalidOptions(
"Wiping the block tree without also wiping the chainstate is currently unsupported"
.to_string(),
))
}
}
pub fn block_tree_db_in_memory(self, block_tree_db_in_memory: bool) -> Self {
unsafe {
btck_chainstate_manager_options_update_block_tree_db_in_memory(
self.inner,
c_helpers::to_c_bool(block_tree_db_in_memory),
);
}
self
}
pub fn chainstate_db_in_memory(self, chainstate_db_in_memory: bool) -> Self {
unsafe {
btck_chainstate_manager_options_update_chainstate_db_in_memory(
self.inner,
c_helpers::to_c_bool(chainstate_db_in_memory),
);
}
self
}
pub fn build(self) -> Result<ChainstateManager, KernelError> {
let inner = unsafe { btck_chainstate_manager_create(self.inner) };
if inner.is_null() {
return Err(KernelError::Internal(
"Failed to create chainstate manager.".to_string(),
));
}
Ok(ChainstateManager { inner })
}
}
impl Drop for ChainstateManagerBuilder {
fn drop(&mut self) {
unsafe {
btck_chainstate_manager_options_destroy(self.inner);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ChainType, ContextBuilder};
use tempdir::TempDir;
fn create_test_context() -> Context {
ContextBuilder::new()
.chain_type(ChainType::Regtest)
.build()
.unwrap()
}
fn create_test_dirs() -> (TempDir, String, String) {
let temp_dir = TempDir::new("test_chainman").unwrap();
let data_dir = temp_dir.path().to_str().unwrap().to_string();
let blocks_dir = format!("{}/blocks", data_dir);
(temp_dir, data_dir, blocks_dir)
}
#[test]
fn test_chainstate_manager_options_new() {
let context = create_test_context();
let (_temp_dir, data_dir, blocks_dir) = create_test_dirs();
let builder = ChainstateManagerBuilder::new(&context, &data_dir, &blocks_dir);
assert!(builder.is_ok());
}
#[test]
fn test_chainstate_manager_options_invalid_path() {
let context = create_test_context();
let invalid_path = "test\0path";
let blocks_dir = "blocks";
let builder = ChainstateManagerBuilder::new(&context, invalid_path, blocks_dir);
assert!(builder.is_err());
}
#[test]
fn test_wipe_block_tree_without_chainstate_fails() {
let context = create_test_context();
let (_temp_dir, data_dir, blocks_dir) = create_test_dirs();
let result = ChainstateManagerBuilder::new(&context, &data_dir, &blocks_dir)
.unwrap()
.wipe_db(true, false);
assert!(result.is_err());
if let Err(e) = result {
assert!(matches!(e, KernelError::InvalidOptions(_)));
}
}
#[test]
fn test_chainstate_manager_creation() {
let context = create_test_context();
let (_temp_dir, data_dir, blocks_dir) = create_test_dirs();
let chainman = ChainstateManagerBuilder::new(&context, &data_dir, &blocks_dir)
.unwrap()
.block_tree_db_in_memory(true)
.chainstate_db_in_memory(true)
.wipe_db(false, true)
.unwrap()
.worker_threads(4)
.build();
assert!(chainman.is_ok());
}
#[test]
fn test_process_block_result_new_block() {
let result = ProcessBlockResult::NewBlock;
assert_eq!(result, ProcessBlockResult::NewBlock);
assert!(result.is_new_block());
assert!(!result.is_duplicate());
assert!(!result.is_rejected());
}
#[test]
fn test_process_block_result_duplicate() {
let result = ProcessBlockResult::Duplicate;
assert_eq!(result, ProcessBlockResult::Duplicate);
assert!(!result.is_new_block());
assert!(result.is_duplicate());
assert!(!result.is_rejected());
}
#[test]
fn test_process_block_result_rejected() {
let result = ProcessBlockResult::Rejected;
assert_eq!(result, ProcessBlockResult::Rejected);
assert!(!result.is_new_block());
assert!(!result.is_duplicate());
assert!(result.is_rejected());
}
#[test]
fn test_process_block_result_match() {
let result = ProcessBlockResult::NewBlock;
let message = match result {
ProcessBlockResult::NewBlock => "new",
ProcessBlockResult::Duplicate => "duplicate",
ProcessBlockResult::Rejected => "rejected",
};
assert_eq!(message, "new");
}
#[test]
fn test_process_block_result_equality() {
assert_eq!(ProcessBlockResult::NewBlock, ProcessBlockResult::NewBlock);
assert_ne!(ProcessBlockResult::NewBlock, ProcessBlockResult::Rejected);
assert_ne!(ProcessBlockResult::Duplicate, ProcessBlockResult::Rejected);
}
#[test]
fn test_process_block_result_debug() {
let result = ProcessBlockResult::NewBlock;
let debug_str = format!("{:?}", result);
assert_eq!(debug_str, "NewBlock");
}
}