//! Synor blockchain node daemon. //! //! This is the main entry point for running a Synor node. #![allow(dead_code)] use std::path::PathBuf; use std::sync::Arc; use clap::{Parser, Subcommand}; use tracing::{error, info}; use synord::config::NodeConfig; use synord::node::SynorNode; use synord::services::StorageService; /// Synor blockchain node daemon. #[derive(Parser)] #[command(name = "synord")] #[command(version, about = "Synor blockchain node daemon", long_about = None)] struct Cli { /// Configuration file path #[arg(short, long, default_value = "synord.toml")] config: PathBuf, /// Data directory #[arg(short, long, env = "SYNOR_DATA_DIR")] data_dir: Option, /// Network to connect to #[arg(short, long, default_value = "mainnet")] network: String, /// Log level #[arg(long, default_value = "info")] log_level: String, /// Enable JSON logging #[arg(long)] json_logs: bool, #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum Commands { /// Run the node Run { /// Enable mining #[arg(long)] mine: bool, /// Mining address for block rewards #[arg(long)] coinbase: Option, /// Number of mining threads (0 = auto) #[arg(long, default_value = "0")] mining_threads: usize, /// RPC bind address #[arg(long, default_value = "127.0.0.1")] rpc_host: String, /// RPC port #[arg(long, default_value = "16110")] rpc_port: u16, /// WebSocket port #[arg(long, default_value = "16111")] ws_port: u16, /// P2P bind address #[arg(long, default_value = "0.0.0.0")] p2p_host: String, /// P2P port #[arg(long, default_value = "16100")] p2p_port: u16, /// Seed nodes to connect to #[arg(long)] seeds: Vec, }, /// Initialize a new node Init { /// Network (mainnet, testnet, devnet) #[arg(long, default_value = "mainnet")] network: String, /// Force overwrite existing config #[arg(long)] force: bool, }, /// Import blocks from file Import { /// Path to blocks file path: PathBuf, /// Skip verification #[arg(long)] no_verify: bool, }, /// Export blocks to file Export { /// Output path path: PathBuf, /// Start height #[arg(long, default_value = "0")] from: u64, /// End height (0 = latest) #[arg(long, default_value = "0")] to: u64, }, /// Show node version and info Version, } #[tokio::main] async fn main() { let cli = Cli::parse(); // Initialize logging init_logging(&cli.log_level, cli.json_logs); info!( version = env!("CARGO_PKG_VERSION"), "Starting Synor node daemon" ); // Run command let result = match cli.command { Some(Commands::Run { mine, coinbase, mining_threads, rpc_host, rpc_port, ws_port, p2p_host, p2p_port, seeds, }) => { run_node( cli.config, cli.data_dir, cli.network, mine, coinbase, mining_threads, rpc_host, rpc_port, ws_port, p2p_host, p2p_port, seeds, ) .await } Some(Commands::Init { network, force }) => init_node(cli.data_dir, network, force).await, Some(Commands::Import { path, no_verify }) => { import_blocks(cli.config, cli.data_dir, path, no_verify).await } Some(Commands::Export { path, from, to }) => { export_blocks(cli.config, cli.data_dir, path, from, to).await } Some(Commands::Version) => { print_version(); Ok(()) } None => { // Default to run run_node( cli.config, cli.data_dir, cli.network, false, None, 0, "127.0.0.1".to_string(), 16110, 16111, "0.0.0.0".to_string(), 16100, vec![], ) .await } }; if let Err(e) = result { error!("Node error: {}", e); std::process::exit(1); } } /// Initialize logging. fn init_logging(level: &str, json: bool) { use tracing_subscriber::{fmt, prelude::*, EnvFilter}; let filter = EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new(level)); let subscriber = tracing_subscriber::registry().with(filter); if json { subscriber .with(fmt::layer().json()) .init(); } else { subscriber .with(fmt::layer().with_target(true)) .init(); } } /// Run the node. async fn run_node( config_path: PathBuf, data_dir: Option, network: String, mine: bool, coinbase: Option, mining_threads: usize, rpc_host: String, rpc_port: u16, ws_port: u16, p2p_host: String, p2p_port: u16, seeds: Vec, ) -> anyhow::Result<()> { // Load or create config let config = NodeConfig::load_or_default(&config_path, &network)?; // Override with CLI args let config = config .with_data_dir(data_dir) .with_mining(mine, coinbase, mining_threads) .with_rpc(&rpc_host, rpc_port, ws_port) .with_p2p(&p2p_host, p2p_port, seeds); info!( network = %config.network, data_dir = %config.data_dir.display(), "Node configuration loaded" ); // Create and start node let node = SynorNode::new(config).await?; let node = Arc::new(node); // Start all services node.start().await?; info!("Synor node is running"); // Wait for shutdown signal wait_for_shutdown().await; info!("Shutting down..."); node.stop().await?; info!("Node stopped gracefully"); Ok(()) } /// Initialize a new node with genesis block. async fn init_node( data_dir: Option, network: String, force: bool, ) -> anyhow::Result<()> { use synor_consensus::genesis::ChainConfig; use synor_storage::{BlockBody, ChainState}; use synor_types::{BlockId, Network}; let data_dir = data_dir.unwrap_or_else(default_data_dir); // Check if already initialized let genesis_marker = data_dir.join("chainstate").join("GENESIS"); if genesis_marker.exists() && !force { anyhow::bail!( "Node already initialized at {}. Use --force to reinitialize.", data_dir.display() ); } // Parse network let net = match network.as_str() { "mainnet" => Network::Mainnet, "testnet" => Network::Testnet, "devnet" => Network::Devnet, _ => anyhow::bail!("Unknown network: {}. Use mainnet, testnet, or devnet.", network), }; info!(network = %network, "Initializing node..."); // Get chain config with genesis block let chain_config = ChainConfig::for_network(net); info!( genesis_hash = %hex::encode(chain_config.genesis_hash.as_bytes()), "Using genesis block" ); // Create directories std::fs::create_dir_all(&data_dir)?; std::fs::create_dir_all(data_dir.join("blocks"))?; std::fs::create_dir_all(data_dir.join("chainstate"))?; std::fs::create_dir_all(data_dir.join("contracts"))?; std::fs::create_dir_all(data_dir.join("keys"))?; // Create and save node config let config = NodeConfig::for_network(&network)? .with_data_dir(Some(data_dir.clone())); let config_path = data_dir.join("synord.toml"); config.save(&config_path)?; info!("Created configuration file"); // Initialize storage let storage = StorageService::new(&config).await?; storage.start().await?; info!("Initialized storage"); // Store genesis block header storage.put_header(&chain_config.genesis.header).await?; info!("Stored genesis header"); // Store genesis block body let genesis_hash = chain_config.genesis_hash; let body = BlockBody { transaction_ids: chain_config.genesis.body.transactions .iter() .map(|tx| tx.txid()) .collect(), }; storage.put_block_body(&genesis_hash, &body).await?; info!("Stored genesis block body"); // Store genesis transactions for tx in &chain_config.genesis.body.transactions { storage.put_transaction(tx).await?; } info!( tx_count = chain_config.genesis.body.transactions.len(), "Stored genesis transactions" ); // Set genesis hash in metadata let genesis_id = BlockId::from_bytes(*genesis_hash.as_bytes()); storage.set_genesis(&genesis_id).await?; info!("Set genesis hash"); // Set initial tips (just genesis) storage.set_tips(&[genesis_id]).await?; info!("Set initial tips"); // Initialize chain state let chain_state = ChainState { max_blue_score: 0, total_blocks: 1, daa_score: 0, difficulty_bits: chain_config.initial_difficulty, total_work: vec![0; 32], }; storage.set_chain_state(&chain_state).await?; info!("Initialized chain state"); // Create genesis marker file std::fs::write(&genesis_marker, hex::encode(genesis_hash.as_bytes()))?; // Stop storage storage.stop().await?; info!( path = %data_dir.display(), network = %network, genesis = %hex::encode(genesis_hash.as_bytes()), "Node initialized successfully" ); println!(); println!("Synor node initialized!"); println!(); println!(" Network: {}", network); println!(" Data dir: {}", data_dir.display()); println!(" Genesis: {}", hex::encode(genesis_hash.as_bytes())); println!(); println!("Chain parameters:"); println!(" Block time: {} ms", chain_config.target_block_time_ms); println!(" GHOSTDAG K: {}", chain_config.ghostdag_k); println!(" Initial reward: {} SYNOR", chain_config.initial_reward / 100_000_000); println!(" Halving interval: {} blocks", chain_config.halving_interval); println!(); println!("To start the node:"); println!(" synord run --network {}", network); println!(); Ok(()) } /// Import blocks from file. async fn import_blocks( config_path: PathBuf, data_dir: Option, path: PathBuf, no_verify: bool, ) -> anyhow::Result<()> { use std::fs::File; use std::io::{BufReader, Read}; let config = NodeConfig::load_or_default(&config_path, "mainnet")?; let config = config.with_data_dir(data_dir); info!( path = %path.display(), verify = !no_verify, "Importing blocks" ); // Open the import file let file = File::open(&path)?; let mut reader = BufReader::new(file); // Read file header (magic + version) let mut magic = [0u8; 8]; reader.read_exact(&mut magic)?; if &magic != b"SYNBLKS\x01" { anyhow::bail!("Invalid block export file format"); } // Initialize storage let storage = Arc::new(StorageService::new(&config).await?); storage.start().await?; let mut imported = 0u64; let mut errors = 0u64; // Read blocks until EOF loop { // Read block length let mut len_buf = [0u8; 4]; match reader.read_exact(&mut len_buf) { Ok(_) => {} Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break, Err(e) => return Err(e.into()), } let block_len = u32::from_le_bytes(len_buf) as usize; if block_len == 0 { break; } // Read block data let mut block_bytes = vec![0u8; block_len]; reader.read_exact(&mut block_bytes)?; // Deserialize block data (hash + header + body) let block_data: synord::services::BlockData = match borsh::from_slice(&block_bytes) { Ok(b) => b, Err(e) => { error!("Failed to deserialize block: {}", e); errors += 1; continue; } }; // Store the block if let Err(e) = storage.put_block(&block_data).await { error!(hash = hex::encode(&block_data.hash[..8]), "Failed to store block: {}", e); errors += 1; } else { imported += 1; if imported % 1000 == 0 { info!("Imported {} blocks...", imported); } } } storage.stop().await?; info!( imported = imported, errors = errors, "Block import complete" ); Ok(()) } /// Export blocks to file. async fn export_blocks( config_path: PathBuf, data_dir: Option, path: PathBuf, from: u64, to: u64, ) -> anyhow::Result<()> { use std::fs::File; use std::io::{BufWriter, Write}; let config = NodeConfig::load_or_default(&config_path, "mainnet")?; let config = config.with_data_dir(data_dir); info!( path = %path.display(), from = from, to = to, "Exporting blocks" ); // Initialize storage let storage = Arc::new(StorageService::new(&config).await?); storage.start().await?; // Get tips to start walking backwards through the DAG let tips = storage.get_tips().await?; if tips.is_empty() { anyhow::bail!("No tips found - is the node initialized?"); } // Open output file let file = File::create(&path)?; let mut writer = BufWriter::new(file); // Write file header (magic + version) writer.write_all(b"SYNBLKS\x01")?; let mut exported = 0u64; let mut errors = 0u64; // Walk backwards from tips through the DAG // Export blocks with blue_score in [from, to] range let mut seen = std::collections::HashSet::new(); let mut to_visit: Vec<[u8; 32]> = tips.into_iter().map(|h| *h.as_bytes()).collect(); while let Some(hash) = to_visit.pop() { if seen.contains(&hash) { continue; } seen.insert(hash); if let Ok(Some(block_data)) = storage.get_block(&hash).await { // Parse header to check blue score (used as height in DAG) let header: synor_types::BlockHeader = match borsh::from_slice(&block_data.header) { Ok(h) => h, Err(e) => { error!("Failed to parse header: {}", e); errors += 1; continue; } }; let blue_score = header.blue_score.value(); // Only export blocks within the specified blue score range if blue_score >= from && blue_score <= to { // Serialize the block data let serialized = borsh::to_vec(&block_data)?; // Write length + data writer.write_all(&(serialized.len() as u32).to_le_bytes())?; writer.write_all(&serialized)?; exported += 1; if exported % 1000 == 0 { info!("Exported {} blocks...", exported); } } // Add parents to visit (walk backwards through DAG) // Only continue if we haven't gone below the 'from' threshold if blue_score > from { for parent in &header.parents { to_visit.push(*parent.as_bytes()); } } } } // Write terminator writer.write_all(&0u32.to_le_bytes())?; writer.flush()?; storage.stop().await?; info!( exported = exported, errors = errors, path = %path.display(), "Block export complete" ); Ok(()) } /// Print version information. fn print_version() { println!("synord {}", env!("CARGO_PKG_VERSION")); println!(); println!("Build info:"); println!(" Rust version: {}", rustc_version()); println!(" Target: {}", std::env::consts::ARCH); println!(" OS: {}", std::env::consts::OS); println!(); println!("Network parameters:"); println!(" Max supply: 70,000,000 SYNOR"); println!(" Block time: ~1 second (DAG)"); println!(" Algorithm: kHeavyHash PoW"); println!(" Consensus: GHOSTDAG"); } fn rustc_version() -> &'static str { option_env!("RUSTC_VERSION").unwrap_or("unknown") } /// Get default data directory. fn default_data_dir() -> PathBuf { dirs::data_dir() .unwrap_or_else(|| PathBuf::from(".")) .join("synor") } /// Wait for shutdown signal. async fn wait_for_shutdown() { #[cfg(unix)] { use tokio::signal::unix::{signal, SignalKind}; let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM"); let mut sigint = signal(SignalKind::interrupt()).expect("Failed to register SIGINT"); tokio::select! { _ = sigterm.recv() => { info!("Received SIGTERM"); } _ = sigint.recv() => { info!("Received SIGINT"); } } } #[cfg(windows)] { tokio::signal::ctrl_c().await.expect("Failed to listen for Ctrl+C"); info!("Received Ctrl+C"); } }