Skip to content

Commit 81091cc

Browse files
authored
Merge pull request #81 from Steake/copilot/implement-state-persistence-rocksdb
Implement RocksDB-backed state persistence for accounts and bonds
2 parents 7ee3933 + 2e3b4aa commit 81091cc

8 files changed

Lines changed: 309 additions & 45 deletions

File tree

crates/bitcell-node/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ base64 = "0.21"
3939

4040
[dev-dependencies]
4141
proptest.workspace = true
42+
tempfile = "3.23.0"

crates/bitcell-node/src/blockchain.rs

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
///! - Block validation including signature, VRF, and transaction verification
66
///! - Transaction indexing for efficient lookups
77
///! - State management with Merkle tree root computation
8-
98
use crate::{Result, MetricsRegistry};
109
use bitcell_consensus::{Block, BlockHeader, Transaction, BattleProof};
1110
use bitcell_crypto::{Hash256, PublicKey, SecretKey};
12-
use bitcell_economics::{COIN, INITIAL_BLOCK_REWARD, HALVING_INTERVAL, MAX_HALVINGS};
11+
use bitcell_economics::{INITIAL_BLOCK_REWARD, HALVING_INTERVAL, MAX_HALVINGS};
1312
use bitcell_state::StateManager;
1413
use std::sync::{Arc, RwLock};
1514
use std::collections::HashMap;
@@ -78,6 +77,54 @@ impl Blockchain {
7877
blockchain
7978
}
8079

80+
/// Create new blockchain with persistent storage
81+
///
82+
/// This method initializes the blockchain with RocksDB-backed state storage.
83+
/// State will be persisted to disk and restored across node restarts.
84+
///
85+
/// # Arguments
86+
/// * `secret_key` - Node's secret key for signing
87+
/// * `metrics` - Metrics registry
88+
/// * `data_path` - Path to the data directory for persistent storage
89+
pub fn with_storage(
90+
secret_key: Arc<SecretKey>,
91+
metrics: MetricsRegistry,
92+
data_path: &std::path::Path,
93+
) -> std::result::Result<Self, String> {
94+
// Create storage manager
95+
let storage_path = data_path.join("state");
96+
let storage = Arc::new(
97+
bitcell_state::StorageManager::new(&storage_path)
98+
.map_err(|e| format!("Failed to create storage: {}", e))?
99+
);
100+
101+
// Create state manager with storage
102+
let state = StateManager::with_storage(storage)
103+
.map_err(|e| format!("Failed to initialize state: {:?}", e))?;
104+
105+
let genesis = Self::create_genesis_block(&secret_key);
106+
let genesis_hash = genesis.hash();
107+
108+
let mut blocks = HashMap::new();
109+
blocks.insert(GENESIS_HEIGHT, genesis);
110+
111+
let blockchain = Self {
112+
height: Arc::new(RwLock::new(GENESIS_HEIGHT)),
113+
latest_hash: Arc::new(RwLock::new(genesis_hash)),
114+
blocks: Arc::new(RwLock::new(blocks)),
115+
tx_index: Arc::new(RwLock::new(HashMap::new())),
116+
state: Arc::new(RwLock::new(state)),
117+
metrics: metrics.clone(),
118+
secret_key,
119+
};
120+
121+
// Initialize metrics
122+
blockchain.metrics.set_chain_height(GENESIS_HEIGHT);
123+
blockchain.metrics.set_sync_progress(100);
124+
125+
Ok(blockchain)
126+
}
127+
81128
/// Create genesis block
82129
fn create_genesis_block(secret_key: &SecretKey) -> Block {
83130
let header = BlockHeader {
@@ -518,6 +565,40 @@ mod tests {
518565
// Test reward becomes 0 after 64 halvings
519566
assert_eq!(Blockchain::calculate_block_reward(HALVING_INTERVAL * 64), 0);
520567
}
568+
569+
#[test]
570+
fn test_blockchain_with_persistent_storage() {
571+
use tempfile::TempDir;
572+
573+
let temp_dir = TempDir::new().unwrap();
574+
let data_path = temp_dir.path();
575+
let sk = Arc::new(SecretKey::generate());
576+
let pubkey = [1u8; 33];
577+
578+
// Create blockchain with storage and modify state
579+
{
580+
let metrics = MetricsRegistry::new();
581+
let blockchain = Blockchain::with_storage(sk.clone(), metrics, data_path).unwrap();
582+
583+
// Add an account to state
584+
let mut state = blockchain.state.write().unwrap();
585+
state.update_account(pubkey, bitcell_state::Account {
586+
balance: 1000,
587+
nonce: 5,
588+
});
589+
}
590+
591+
// Recreate blockchain from same storage and verify persistence
592+
{
593+
let metrics = MetricsRegistry::new();
594+
let blockchain = Blockchain::with_storage(sk, metrics, data_path).unwrap();
595+
596+
let state = blockchain.state.read().unwrap();
597+
let account = state.get_account_owned(&pubkey).expect("Account should persist");
598+
assert_eq!(account.balance, 1000);
599+
assert_eq!(account.nonce, 5);
600+
}
601+
}
521602

522603
#[test]
523604
fn test_vrf_block_production_and_validation() {

crates/bitcell-node/src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ pub struct NodeConfig {
1414
/// Block production interval in seconds.
1515
/// Defaults to 10 seconds for testing. Use 600 (10 minutes) for production.
1616
pub block_time_secs: u64,
17+
/// Data directory for persistent storage. If None, uses in-memory storage only.
18+
pub data_dir: Option<std::path::PathBuf>,
1719
}
1820

1921
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -33,6 +35,7 @@ impl Default for NodeConfig {
3335
bootstrap_nodes: vec![],
3436
key_seed: None,
3537
block_time_secs: 10, // Default to 10 seconds for testing
38+
data_dir: None, // Default to in-memory storage for testing
3639
}
3740
}
3841
}

crates/bitcell-node/src/main.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
//! BitCell node binary
22
33
use bitcell_node::{NodeConfig, ValidatorNode, MinerNode};
4-
use bitcell_crypto::SecretKey;
54
use clap::{Parser, Subcommand};
65
use std::path::PathBuf;
76

@@ -81,18 +80,18 @@ async fn main() {
8180
let cli = Cli::parse();
8281

8382
match cli.command {
84-
Commands::Validator { port, rpc_port, data_dir: _, enable_dht, bootstrap, key_seed, key_file, private_key } => {
83+
Commands::Validator { port, rpc_port, data_dir, enable_dht, bootstrap, key_seed, key_file, private_key } => {
8584
println!("🌌 BitCell Validator Node");
8685
println!("=========================");
8786

8887
let mut config = NodeConfig::default();
8988
config.network_port = port;
9089
config.enable_dht = enable_dht;
9190
config.key_seed = key_seed.clone();
91+
config.data_dir = data_dir;
9292
if let Some(bootstrap_node) = bootstrap {
9393
config.bootstrap_nodes.push(bootstrap_node);
9494
}
95-
// TODO: Use data_dir
9695

9796
// Resolve secret key
9897
let secret_key = match bitcell_node::keys::resolve_secret_key(
@@ -122,7 +121,13 @@ async fn main() {
122121
// Or we can modify NodeConfig to hold the secret key? No, NodeConfig is serializable.
123122

124123
// Let's update ValidatorNode::new to take the secret key as an argument.
125-
let mut node = ValidatorNode::with_key(config, secret_key.clone());
124+
let mut node = match ValidatorNode::with_key(config, secret_key.clone()) {
125+
Ok(node) => node,
126+
Err(e) => {
127+
eprintln!("Error initializing validator node: {}", e);
128+
std::process::exit(1);
129+
}
130+
};
126131

127132
// Start metrics server on port + 2 to avoid conflict with P2P port (30333) and RPC port (30334)
128133
let metrics_port = port + 2;
@@ -162,14 +167,15 @@ async fn main() {
162167
tokio::signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
163168
println!("\nShutting down...");
164169
}
165-
Commands::Miner { port, rpc_port, data_dir: _, enable_dht, bootstrap, key_seed, key_file, private_key } => {
170+
Commands::Miner { port, rpc_port, data_dir, enable_dht, bootstrap, key_seed, key_file, private_key } => {
166171
println!("⛏️ BitCell Miner Node");
167172
println!("======================");
168173

169174
let mut config = NodeConfig::default();
170175
config.network_port = port;
171176
config.enable_dht = enable_dht;
172177
config.key_seed = key_seed.clone();
178+
config.data_dir = data_dir;
173179
if let Some(bootstrap_node) = bootstrap {
174180
config.bootstrap_nodes.push(bootstrap_node);
175181
}
@@ -190,7 +196,13 @@ async fn main() {
190196

191197
println!("Miner Public Key: {:?}", secret_key.public_key());
192198

193-
let mut node = MinerNode::with_key(config, secret_key.clone());
199+
let mut node = match MinerNode::with_key(config, secret_key.clone()) {
200+
Ok(node) => node,
201+
Err(e) => {
202+
eprintln!("Error initializing miner node: {}", e);
203+
std::process::exit(1);
204+
}
205+
};
194206

195207
let metrics_port = port + 2;
196208

@@ -228,14 +240,15 @@ async fn main() {
228240
tokio::signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
229241
println!("\nShutting down...");
230242
}
231-
Commands::FullNode { port, rpc_port, data_dir: _, enable_dht, bootstrap, key_seed, key_file, private_key } => {
243+
Commands::FullNode { port, rpc_port, data_dir, enable_dht, bootstrap, key_seed, key_file, private_key } => {
232244
println!("🌍 BitCell Full Node");
233245
println!("====================");
234246

235247
let mut config = NodeConfig::default();
236248
config.network_port = port;
237249
config.enable_dht = enable_dht;
238250
config.key_seed = key_seed.clone();
251+
config.data_dir = data_dir;
239252
if let Some(bootstrap_node) = bootstrap {
240253
config.bootstrap_nodes.push(bootstrap_node);
241254
}
@@ -257,7 +270,13 @@ async fn main() {
257270
println!("Full Node Public Key: {:?}", secret_key.public_key());
258271

259272
// Reuse ValidatorNode for now as FullNode logic is similar (just no voting)
260-
let mut node = ValidatorNode::with_key(config, secret_key.clone());
273+
let mut node = match ValidatorNode::with_key(config, secret_key.clone()) {
274+
Ok(node) => node,
275+
Err(e) => {
276+
eprintln!("Error initializing full node: {}", e);
277+
std::process::exit(1);
278+
}
279+
};
261280

262281
let metrics_port = port + 2;
263282

crates/bitcell-node/src/miner.rs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
///! Miner node implementation
2-
1+
//! Miner node implementation
32
use crate::{NodeConfig, Result, MetricsRegistry, Blockchain, TransactionPool, NetworkManager};
43
use bitcell_crypto::SecretKey;
54
use bitcell_ca::{Glider, GliderPattern};
6-
use bitcell_state::StateManager;
75
use std::sync::Arc;
86
use bitcell_consensus::Transaction;
97

108
/// Miner node
119
pub struct MinerNode {
1210
pub config: NodeConfig,
1311
pub secret_key: Arc<SecretKey>,
14-
pub state: StateManager,
1512
pub glider_strategy: GliderPattern,
1613
pub metrics: MetricsRegistry,
1714
pub blockchain: Blockchain,
@@ -20,25 +17,38 @@ pub struct MinerNode {
2017
}
2118

2219
impl MinerNode {
23-
pub fn new(config: NodeConfig, secret_key: SecretKey) -> Self {
20+
pub fn new(config: NodeConfig, secret_key: SecretKey) -> crate::Result<Self> {
2421
Self::with_key(config, Arc::new(secret_key))
2522
}
2623

27-
pub fn with_key(config: NodeConfig, secret_key: Arc<SecretKey>) -> Self {
24+
pub fn with_key(config: NodeConfig, secret_key: Arc<SecretKey>) -> crate::Result<Self> {
2825
let metrics = MetricsRegistry::new();
29-
let blockchain = Blockchain::new(secret_key.clone(), metrics.clone());
26+
27+
// Create blockchain with or without persistent storage based on config
28+
let blockchain = if let Some(ref data_path) = config.data_dir {
29+
// Ensure data directory exists
30+
std::fs::create_dir_all(data_path)
31+
.map_err(|e| crate::Error::Config(format!("Failed to create data directory: {}", e)))?;
32+
33+
println!("📦 Using persistent storage at: {}", data_path.display());
34+
Blockchain::with_storage(secret_key.clone(), metrics.clone(), data_path)
35+
.map_err(|e| crate::Error::Config(format!("Failed to initialize blockchain with storage: {}", e)))?
36+
} else {
37+
println!("⚠️ Using in-memory storage (data will not persist)");
38+
Blockchain::new(secret_key.clone(), metrics.clone())
39+
};
40+
3041
let network = Arc::new(NetworkManager::new(secret_key.public_key(), metrics.clone()));
3142

32-
Self {
43+
Ok(Self {
3344
config,
3445
secret_key,
35-
state: StateManager::new(),
3646
glider_strategy: GliderPattern::Standard,
3747
metrics,
3848
blockchain,
3949
tx_pool: TransactionPool::default(),
4050
network,
41-
}
51+
})
4252
}
4353

4454
pub async fn start(&mut self) -> Result<()> {
@@ -162,15 +172,15 @@ mod tests {
162172
fn test_miner_creation() {
163173
let config = NodeConfig::default();
164174
let sk = SecretKey::generate();
165-
let miner = MinerNode::new(config, sk);
175+
let miner = MinerNode::new(config, sk).unwrap();
166176
assert_eq!(miner.glider_strategy, GliderPattern::Standard);
167177
}
168178

169179
#[test]
170180
fn test_glider_generation() {
171181
let config = NodeConfig::default();
172182
let sk = SecretKey::generate();
173-
let miner = MinerNode::new(config, sk);
183+
let miner = MinerNode::new(config, sk).unwrap();
174184
let glider = miner.generate_glider();
175185
assert_eq!(glider.pattern, GliderPattern::Standard);
176186
}

crates/bitcell-node/src/validator.rs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
33
use crate::{NodeConfig, Result, MetricsRegistry, Blockchain, TransactionPool};
44
use bitcell_consensus::Block;
5-
use bitcell_state::StateManager;
65
use bitcell_network::PeerManager;
76
use bitcell_crypto::SecretKey;
87
use std::sync::Arc;
@@ -15,7 +14,6 @@ const MAX_TXS_PER_BLOCK: usize = 1000;
1514
/// Validator node
1615
pub struct ValidatorNode {
1716
pub config: NodeConfig,
18-
pub state: StateManager,
1917
pub peers: PeerManager,
2018
pub metrics: MetricsRegistry,
2119
pub blockchain: Blockchain,
@@ -26,7 +24,7 @@ pub struct ValidatorNode {
2624
}
2725

2826
impl ValidatorNode {
29-
pub fn new(config: NodeConfig) -> Self {
27+
pub fn new(config: NodeConfig) -> crate::Result<Self> {
3028
let secret_key = if let Some(seed) = &config.key_seed {
3129
println!("Generating validator key from seed: {}", seed);
3230
let hash = bitcell_crypto::Hash256::hash(seed.as_bytes());
@@ -37,23 +35,36 @@ impl ValidatorNode {
3735
Self::with_key(config, secret_key)
3836
}
3937

40-
pub fn with_key(config: NodeConfig, secret_key: Arc<SecretKey>) -> Self {
38+
pub fn with_key(config: NodeConfig, secret_key: Arc<SecretKey>) -> crate::Result<Self> {
4139
let metrics = MetricsRegistry::new();
42-
let blockchain = Blockchain::new(secret_key.clone(), metrics.clone());
40+
41+
// Create blockchain with or without persistent storage based on config
42+
let blockchain = if let Some(ref data_path) = config.data_dir {
43+
// Ensure data directory exists
44+
std::fs::create_dir_all(data_path)
45+
.map_err(|e| crate::Error::Config(format!("Failed to create data directory: {}", e)))?;
46+
47+
println!("📦 Using persistent storage at: {}", data_path.display());
48+
Blockchain::with_storage(secret_key.clone(), metrics.clone(), data_path)
49+
.map_err(|e| crate::Error::Config(format!("Failed to initialize blockchain with storage: {}", e)))?
50+
} else {
51+
println!("⚠️ Using in-memory storage (data will not persist)");
52+
Blockchain::new(secret_key.clone(), metrics.clone())
53+
};
54+
4355
let tournament_manager = Arc::new(crate::tournament::TournamentManager::new(metrics.clone()));
4456
let network = Arc::new(crate::network::NetworkManager::new(secret_key.public_key(), metrics.clone()));
4557

46-
Self {
58+
Ok(Self {
4759
config,
48-
state: StateManager::new(),
4960
peers: PeerManager::new(),
5061
metrics,
5162
blockchain,
5263
tx_pool: TransactionPool::default(),
5364
secret_key,
5465
tournament_manager,
5566
network,
56-
}
67+
})
5768
}
5869

5970
pub async fn start(&mut self) -> Result<()> {
@@ -276,7 +287,9 @@ mod tests {
276287
#[test]
277288
fn test_validator_creation() {
278289
let config = NodeConfig::default();
279-
let node = ValidatorNode::new(config);
280-
assert_eq!(node.state.accounts.len(), 0);
290+
let node = ValidatorNode::new(config).unwrap();
291+
let state = node.blockchain.state();
292+
let state_guard = state.read().unwrap();
293+
assert_eq!(state_guard.accounts.len(), 0);
281294
}
282295
}

0 commit comments

Comments
 (0)