diff --git a/crates/bitcell-node/Cargo.toml b/crates/bitcell-node/Cargo.toml index fd14b8f..d5aa685 100644 --- a/crates/bitcell-node/Cargo.toml +++ b/crates/bitcell-node/Cargo.toml @@ -39,3 +39,4 @@ base64 = "0.21" [dev-dependencies] proptest.workspace = true +tempfile = "3.23.0" diff --git a/crates/bitcell-node/src/blockchain.rs b/crates/bitcell-node/src/blockchain.rs index e8af0ea..2419c4e 100644 --- a/crates/bitcell-node/src/blockchain.rs +++ b/crates/bitcell-node/src/blockchain.rs @@ -5,11 +5,10 @@ ///! - Block validation including signature, VRF, and transaction verification ///! - Transaction indexing for efficient lookups ///! - State management with Merkle tree root computation - use crate::{Result, MetricsRegistry}; use bitcell_consensus::{Block, BlockHeader, Transaction, BattleProof}; use bitcell_crypto::{Hash256, PublicKey, SecretKey}; -use bitcell_economics::{COIN, INITIAL_BLOCK_REWARD, HALVING_INTERVAL, MAX_HALVINGS}; +use bitcell_economics::{INITIAL_BLOCK_REWARD, HALVING_INTERVAL, MAX_HALVINGS}; use bitcell_state::StateManager; use std::sync::{Arc, RwLock}; use std::collections::HashMap; @@ -78,6 +77,54 @@ impl Blockchain { blockchain } + /// Create new blockchain with persistent storage + /// + /// This method initializes the blockchain with RocksDB-backed state storage. + /// State will be persisted to disk and restored across node restarts. + /// + /// # Arguments + /// * `secret_key` - Node's secret key for signing + /// * `metrics` - Metrics registry + /// * `data_path` - Path to the data directory for persistent storage + pub fn with_storage( + secret_key: Arc, + metrics: MetricsRegistry, + data_path: &std::path::Path, + ) -> std::result::Result { + // Create storage manager + let storage_path = data_path.join("state"); + let storage = Arc::new( + bitcell_state::StorageManager::new(&storage_path) + .map_err(|e| format!("Failed to create storage: {}", e))? + ); + + // Create state manager with storage + let state = StateManager::with_storage(storage) + .map_err(|e| format!("Failed to initialize state: {:?}", e))?; + + let genesis = Self::create_genesis_block(&secret_key); + let genesis_hash = genesis.hash(); + + let mut blocks = HashMap::new(); + blocks.insert(GENESIS_HEIGHT, genesis); + + let blockchain = Self { + height: Arc::new(RwLock::new(GENESIS_HEIGHT)), + latest_hash: Arc::new(RwLock::new(genesis_hash)), + blocks: Arc::new(RwLock::new(blocks)), + tx_index: Arc::new(RwLock::new(HashMap::new())), + state: Arc::new(RwLock::new(state)), + metrics: metrics.clone(), + secret_key, + }; + + // Initialize metrics + blockchain.metrics.set_chain_height(GENESIS_HEIGHT); + blockchain.metrics.set_sync_progress(100); + + Ok(blockchain) + } + /// Create genesis block fn create_genesis_block(secret_key: &SecretKey) -> Block { let header = BlockHeader { @@ -518,6 +565,40 @@ mod tests { // Test reward becomes 0 after 64 halvings assert_eq!(Blockchain::calculate_block_reward(HALVING_INTERVAL * 64), 0); } + + #[test] + fn test_blockchain_with_persistent_storage() { + use tempfile::TempDir; + + let temp_dir = TempDir::new().unwrap(); + let data_path = temp_dir.path(); + let sk = Arc::new(SecretKey::generate()); + let pubkey = [1u8; 33]; + + // Create blockchain with storage and modify state + { + let metrics = MetricsRegistry::new(); + let blockchain = Blockchain::with_storage(sk.clone(), metrics, data_path).unwrap(); + + // Add an account to state + let mut state = blockchain.state.write().unwrap(); + state.update_account(pubkey, bitcell_state::Account { + balance: 1000, + nonce: 5, + }); + } + + // Recreate blockchain from same storage and verify persistence + { + let metrics = MetricsRegistry::new(); + let blockchain = Blockchain::with_storage(sk, metrics, data_path).unwrap(); + + let state = blockchain.state.read().unwrap(); + let account = state.get_account_owned(&pubkey).expect("Account should persist"); + assert_eq!(account.balance, 1000); + assert_eq!(account.nonce, 5); + } + } #[test] fn test_vrf_block_production_and_validation() { diff --git a/crates/bitcell-node/src/config.rs b/crates/bitcell-node/src/config.rs index 7d73cb9..969f6d2 100644 --- a/crates/bitcell-node/src/config.rs +++ b/crates/bitcell-node/src/config.rs @@ -14,6 +14,8 @@ pub struct NodeConfig { /// Block production interval in seconds. /// Defaults to 10 seconds for testing. Use 600 (10 minutes) for production. pub block_time_secs: u64, + /// Data directory for persistent storage. If None, uses in-memory storage only. + pub data_dir: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -33,6 +35,7 @@ impl Default for NodeConfig { bootstrap_nodes: vec![], key_seed: None, block_time_secs: 10, // Default to 10 seconds for testing + data_dir: None, // Default to in-memory storage for testing } } } diff --git a/crates/bitcell-node/src/main.rs b/crates/bitcell-node/src/main.rs index b8c5227..a100284 100644 --- a/crates/bitcell-node/src/main.rs +++ b/crates/bitcell-node/src/main.rs @@ -1,7 +1,6 @@ //! BitCell node binary use bitcell_node::{NodeConfig, ValidatorNode, MinerNode}; -use bitcell_crypto::SecretKey; use clap::{Parser, Subcommand}; use std::path::PathBuf; @@ -81,7 +80,7 @@ async fn main() { let cli = Cli::parse(); match cli.command { - Commands::Validator { port, rpc_port, data_dir: _, enable_dht, bootstrap, key_seed, key_file, private_key } => { + Commands::Validator { port, rpc_port, data_dir, enable_dht, bootstrap, key_seed, key_file, private_key } => { println!("🌌 BitCell Validator Node"); println!("========================="); @@ -89,10 +88,10 @@ async fn main() { config.network_port = port; config.enable_dht = enable_dht; config.key_seed = key_seed.clone(); + config.data_dir = data_dir; if let Some(bootstrap_node) = bootstrap { config.bootstrap_nodes.push(bootstrap_node); } - // TODO: Use data_dir // Resolve secret key let secret_key = match bitcell_node::keys::resolve_secret_key( @@ -122,7 +121,13 @@ async fn main() { // Or we can modify NodeConfig to hold the secret key? No, NodeConfig is serializable. // Let's update ValidatorNode::new to take the secret key as an argument. - let mut node = ValidatorNode::with_key(config, secret_key.clone()); + let mut node = match ValidatorNode::with_key(config, secret_key.clone()) { + Ok(node) => node, + Err(e) => { + eprintln!("Error initializing validator node: {}", e); + std::process::exit(1); + } + }; // Start metrics server on port + 2 to avoid conflict with P2P port (30333) and RPC port (30334) let metrics_port = port + 2; @@ -162,7 +167,7 @@ async fn main() { tokio::signal::ctrl_c().await.expect("Failed to listen for Ctrl+C"); println!("\nShutting down..."); } - Commands::Miner { port, rpc_port, data_dir: _, enable_dht, bootstrap, key_seed, key_file, private_key } => { + Commands::Miner { port, rpc_port, data_dir, enable_dht, bootstrap, key_seed, key_file, private_key } => { println!("⛏️ BitCell Miner Node"); println!("======================"); @@ -170,6 +175,7 @@ async fn main() { config.network_port = port; config.enable_dht = enable_dht; config.key_seed = key_seed.clone(); + config.data_dir = data_dir; if let Some(bootstrap_node) = bootstrap { config.bootstrap_nodes.push(bootstrap_node); } @@ -190,7 +196,13 @@ async fn main() { println!("Miner Public Key: {:?}", secret_key.public_key()); - let mut node = MinerNode::with_key(config, secret_key.clone()); + let mut node = match MinerNode::with_key(config, secret_key.clone()) { + Ok(node) => node, + Err(e) => { + eprintln!("Error initializing miner node: {}", e); + std::process::exit(1); + } + }; let metrics_port = port + 2; @@ -228,7 +240,7 @@ async fn main() { tokio::signal::ctrl_c().await.expect("Failed to listen for Ctrl+C"); println!("\nShutting down..."); } - Commands::FullNode { port, rpc_port, data_dir: _, enable_dht, bootstrap, key_seed, key_file, private_key } => { + Commands::FullNode { port, rpc_port, data_dir, enable_dht, bootstrap, key_seed, key_file, private_key } => { println!("🌍 BitCell Full Node"); println!("===================="); @@ -236,6 +248,7 @@ async fn main() { config.network_port = port; config.enable_dht = enable_dht; config.key_seed = key_seed.clone(); + config.data_dir = data_dir; if let Some(bootstrap_node) = bootstrap { config.bootstrap_nodes.push(bootstrap_node); } @@ -257,7 +270,13 @@ async fn main() { println!("Full Node Public Key: {:?}", secret_key.public_key()); // Reuse ValidatorNode for now as FullNode logic is similar (just no voting) - let mut node = ValidatorNode::with_key(config, secret_key.clone()); + let mut node = match ValidatorNode::with_key(config, secret_key.clone()) { + Ok(node) => node, + Err(e) => { + eprintln!("Error initializing full node: {}", e); + std::process::exit(1); + } + }; let metrics_port = port + 2; diff --git a/crates/bitcell-node/src/miner.rs b/crates/bitcell-node/src/miner.rs index eee7b2a..f361ff8 100644 --- a/crates/bitcell-node/src/miner.rs +++ b/crates/bitcell-node/src/miner.rs @@ -1,9 +1,7 @@ -///! Miner node implementation - +//! Miner node implementation use crate::{NodeConfig, Result, MetricsRegistry, Blockchain, TransactionPool, NetworkManager}; use bitcell_crypto::SecretKey; use bitcell_ca::{Glider, GliderPattern}; -use bitcell_state::StateManager; use std::sync::Arc; use bitcell_consensus::Transaction; @@ -11,7 +9,6 @@ use bitcell_consensus::Transaction; pub struct MinerNode { pub config: NodeConfig, pub secret_key: Arc, - pub state: StateManager, pub glider_strategy: GliderPattern, pub metrics: MetricsRegistry, pub blockchain: Blockchain, @@ -20,25 +17,38 @@ pub struct MinerNode { } impl MinerNode { - pub fn new(config: NodeConfig, secret_key: SecretKey) -> Self { + pub fn new(config: NodeConfig, secret_key: SecretKey) -> crate::Result { Self::with_key(config, Arc::new(secret_key)) } - pub fn with_key(config: NodeConfig, secret_key: Arc) -> Self { + pub fn with_key(config: NodeConfig, secret_key: Arc) -> crate::Result { let metrics = MetricsRegistry::new(); - let blockchain = Blockchain::new(secret_key.clone(), metrics.clone()); + + // Create blockchain with or without persistent storage based on config + let blockchain = if let Some(ref data_path) = config.data_dir { + // Ensure data directory exists + std::fs::create_dir_all(data_path) + .map_err(|e| crate::Error::Config(format!("Failed to create data directory: {}", e)))?; + + println!("📦 Using persistent storage at: {}", data_path.display()); + Blockchain::with_storage(secret_key.clone(), metrics.clone(), data_path) + .map_err(|e| crate::Error::Config(format!("Failed to initialize blockchain with storage: {}", e)))? + } else { + println!("⚠️ Using in-memory storage (data will not persist)"); + Blockchain::new(secret_key.clone(), metrics.clone()) + }; + let network = Arc::new(NetworkManager::new(secret_key.public_key(), metrics.clone())); - Self { + Ok(Self { config, secret_key, - state: StateManager::new(), glider_strategy: GliderPattern::Standard, metrics, blockchain, tx_pool: TransactionPool::default(), network, - } + }) } pub async fn start(&mut self) -> Result<()> { @@ -162,7 +172,7 @@ mod tests { fn test_miner_creation() { let config = NodeConfig::default(); let sk = SecretKey::generate(); - let miner = MinerNode::new(config, sk); + let miner = MinerNode::new(config, sk).unwrap(); assert_eq!(miner.glider_strategy, GliderPattern::Standard); } @@ -170,7 +180,7 @@ mod tests { fn test_glider_generation() { let config = NodeConfig::default(); let sk = SecretKey::generate(); - let miner = MinerNode::new(config, sk); + let miner = MinerNode::new(config, sk).unwrap(); let glider = miner.generate_glider(); assert_eq!(glider.pattern, GliderPattern::Standard); } diff --git a/crates/bitcell-node/src/validator.rs b/crates/bitcell-node/src/validator.rs index 6233c52..7f0a4fc 100644 --- a/crates/bitcell-node/src/validator.rs +++ b/crates/bitcell-node/src/validator.rs @@ -2,7 +2,6 @@ use crate::{NodeConfig, Result, MetricsRegistry, Blockchain, TransactionPool}; use bitcell_consensus::Block; -use bitcell_state::StateManager; use bitcell_network::PeerManager; use bitcell_crypto::SecretKey; use std::sync::Arc; @@ -15,7 +14,6 @@ const MAX_TXS_PER_BLOCK: usize = 1000; /// Validator node pub struct ValidatorNode { pub config: NodeConfig, - pub state: StateManager, pub peers: PeerManager, pub metrics: MetricsRegistry, pub blockchain: Blockchain, @@ -26,7 +24,7 @@ pub struct ValidatorNode { } impl ValidatorNode { - pub fn new(config: NodeConfig) -> Self { + pub fn new(config: NodeConfig) -> crate::Result { let secret_key = if let Some(seed) = &config.key_seed { println!("Generating validator key from seed: {}", seed); let hash = bitcell_crypto::Hash256::hash(seed.as_bytes()); @@ -37,15 +35,28 @@ impl ValidatorNode { Self::with_key(config, secret_key) } - pub fn with_key(config: NodeConfig, secret_key: Arc) -> Self { + pub fn with_key(config: NodeConfig, secret_key: Arc) -> crate::Result { let metrics = MetricsRegistry::new(); - let blockchain = Blockchain::new(secret_key.clone(), metrics.clone()); + + // Create blockchain with or without persistent storage based on config + let blockchain = if let Some(ref data_path) = config.data_dir { + // Ensure data directory exists + std::fs::create_dir_all(data_path) + .map_err(|e| crate::Error::Config(format!("Failed to create data directory: {}", e)))?; + + println!("📦 Using persistent storage at: {}", data_path.display()); + Blockchain::with_storage(secret_key.clone(), metrics.clone(), data_path) + .map_err(|e| crate::Error::Config(format!("Failed to initialize blockchain with storage: {}", e)))? + } else { + println!("⚠️ Using in-memory storage (data will not persist)"); + Blockchain::new(secret_key.clone(), metrics.clone()) + }; + let tournament_manager = Arc::new(crate::tournament::TournamentManager::new(metrics.clone())); let network = Arc::new(crate::network::NetworkManager::new(secret_key.public_key(), metrics.clone())); - Self { + Ok(Self { config, - state: StateManager::new(), peers: PeerManager::new(), metrics, blockchain, @@ -53,7 +64,7 @@ impl ValidatorNode { secret_key, tournament_manager, network, - } + }) } pub async fn start(&mut self) -> Result<()> { @@ -276,7 +287,9 @@ mod tests { #[test] fn test_validator_creation() { let config = NodeConfig::default(); - let node = ValidatorNode::new(config); - assert_eq!(node.state.accounts.len(), 0); + let node = ValidatorNode::new(config).unwrap(); + let state = node.blockchain.state(); + let state_guard = state.read().unwrap(); + assert_eq!(state_guard.accounts.len(), 0); } } diff --git a/crates/bitcell-state/src/lib.rs b/crates/bitcell-state/src/lib.rs index cfdc159..2de046e 100644 --- a/crates/bitcell-state/src/lib.rs +++ b/crates/bitcell-state/src/lib.rs @@ -79,20 +79,18 @@ impl StateManager { Ok(manager) } - /// Get account + /// Get account (returns reference to cached value) + /// + /// Note: This only checks the in-memory cache. For guaranteed up-to-date values + /// that may exist only in storage, use get_account_owned() instead. pub fn get_account(&self, pubkey: &[u8; 33]) -> Option<&Account> { - // Check in-memory cache first - if let Some(account) = self.accounts.get(pubkey) { - return Some(account); - } - - // If we have storage, try loading from disk - // Note: This returns None because we can't return a reference to a temporary - // In production, we'd need to update the cache or use a different pattern - None + self.accounts.get(pubkey) } - /// Get account (with storage fallback, returns owned value) + /// Get account with storage fallback (returns owned value) + /// + /// This method checks both the in-memory cache and storage backend, + /// ensuring that persisted state is accessible even if not yet cached. pub fn get_account_owned(&self, pubkey: &[u8; 33]) -> Option { // Check in-memory cache first if let Some(account) = self.accounts.get(pubkey) { @@ -102,6 +100,12 @@ impl StateManager { // Fallback to storage if available if let Some(storage) = &self.storage { if let Ok(Some(account)) = storage.get_account(pubkey) { + if tracing::enabled!(tracing::Level::TRACE) { + tracing::trace!( + pubkey = %hex::encode(&pubkey), + "Loaded account from storage (cache miss)" + ); + } return Some(account); } } @@ -132,12 +136,18 @@ impl StateManager { self.recompute_root(); } - /// Get bond state + /// Get bond state (returns reference to cached value) + /// + /// Note: This only checks the in-memory cache. For guaranteed up-to-date values + /// that may exist only in storage, use get_bond_owned() instead. pub fn get_bond(&self, pubkey: &[u8; 33]) -> Option<&BondState> { self.bonds.get(pubkey) } - /// Get bond state (with storage fallback, returns owned value) + /// Get bond state with storage fallback (returns owned value) + /// + /// This method checks both the in-memory cache and storage backend, + /// ensuring that persisted state is accessible even if not yet cached. pub fn get_bond_owned(&self, pubkey: &[u8; 33]) -> Option { // Check in-memory cache first if let Some(bond) = self.bonds.get(pubkey) { @@ -147,6 +157,12 @@ impl StateManager { // Fallback to storage if available if let Some(storage) = &self.storage { if let Ok(Some(bond)) = storage.get_bond(pubkey) { + if tracing::enabled!(tracing::Level::TRACE) { + tracing::trace!( + pubkey = %hex::encode(&pubkey), + "Loaded bond from storage (cache miss)" + ); + } return Some(bond); } } @@ -276,6 +292,7 @@ impl Default for StateManager { #[cfg(test)] mod tests { use super::*; + use tempfile::TempDir; #[test] fn test_state_manager() { @@ -292,4 +309,77 @@ mod tests { let retrieved = sm.get_account(&pubkey).unwrap(); assert_eq!(retrieved.balance, 1000); } + + #[test] + fn test_state_manager_with_storage() { + let temp_dir = TempDir::new().unwrap(); + let storage = Arc::new(StorageManager::new(temp_dir.path()).unwrap()); + let pubkey = [1u8; 33]; + + // Create state manager with storage and add an account + { + let mut sm = StateManager::with_storage(storage.clone()).unwrap(); + let account = Account { + balance: 1000, + nonce: 5, + }; + sm.update_account(pubkey, account); + } + + // Create new state manager with same storage and verify persistence + { + let sm = StateManager::with_storage(storage).unwrap(); + let retrieved = sm.get_account_owned(&pubkey).unwrap(); + assert_eq!(retrieved.balance, 1000); + assert_eq!(retrieved.nonce, 5); + } + } + + #[test] + fn test_bond_persistence_with_storage() { + let temp_dir = TempDir::new().unwrap(); + let storage = Arc::new(StorageManager::new(temp_dir.path()).unwrap()); + let miner_id = [42u8; 33]; + + // Create state manager with storage and add a bond + { + let mut sm = StateManager::with_storage(storage.clone()).unwrap(); + let bond = BondState { + amount: 5000, + status: BondStatus::Active, + locked_epoch: 10, + }; + sm.update_bond(miner_id, bond); + } + + // Create new state manager with same storage and verify persistence + { + let sm = StateManager::with_storage(storage).unwrap(); + let retrieved = sm.get_bond_owned(&miner_id).unwrap(); + assert_eq!(retrieved.amount, 5000); + assert_eq!(retrieved.locked_epoch, 10); + assert!(retrieved.is_active()); + } + } + + #[test] + fn test_state_manager_get_or_create_account() { + let mut sm = StateManager::new(); + let pubkey = [3u8; 33]; + + // Account doesn't exist yet + assert!(sm.get_account(&pubkey).is_none()); + assert!(sm.get_account_owned(&pubkey).is_none()); + + // Create account + let account = Account { + balance: 500, + nonce: 0, + }; + sm.update_account(pubkey, account); + + // Now it exists + assert!(sm.get_account(&pubkey).is_some()); + assert_eq!(sm.get_account_owned(&pubkey).unwrap().balance, 500); + } } diff --git a/crates/bitcell-state/src/storage.rs b/crates/bitcell-state/src/storage.rs index 94a9284..67a21a0 100644 --- a/crates/bitcell-state/src/storage.rs +++ b/crates/bitcell-state/src/storage.rs @@ -382,4 +382,51 @@ mod tests { storage.store_header(42, b"hash", b"header").unwrap(); assert_eq!(storage.get_latest_height().unwrap(), Some(42)); } + + #[test] + fn test_account_persistence() { + let temp_dir = TempDir::new().unwrap(); + let pubkey = [42u8; 33]; + let account = Account { balance: 1000, nonce: 5 }; + + // Store account + { + let storage = StorageManager::new(temp_dir.path()).unwrap(); + storage.store_account(&pubkey, &account).unwrap(); + } + + // Reopen storage and verify persistence + { + let storage = StorageManager::new(temp_dir.path()).unwrap(); + let retrieved = storage.get_account(&pubkey).unwrap().unwrap(); + assert_eq!(retrieved.balance, 1000); + assert_eq!(retrieved.nonce, 5); + } + } + + #[test] + fn test_bond_persistence() { + let temp_dir = TempDir::new().unwrap(); + let miner_id = [99u8; 33]; + let bond = BondState { + amount: 5000, + status: crate::BondStatus::Active, + locked_epoch: 10, + }; + + // Store bond + { + let storage = StorageManager::new(temp_dir.path()).unwrap(); + storage.store_bond(&miner_id, &bond).unwrap(); + } + + // Reopen storage and verify persistence + { + let storage = StorageManager::new(temp_dir.path()).unwrap(); + let retrieved = storage.get_bond(&miner_id).unwrap().unwrap(); + assert_eq!(retrieved.amount, 5000); + assert_eq!(retrieved.locked_epoch, 10); + assert!(retrieved.is_active()); + } + } }