nu-node/src/main.rs
Mukan Erkin 6bc6e90114 feat(genesis): genesis.json seed on first start
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 07:47:16 +03:00

160 lines
4.9 KiB
Rust

mod block_loop;
mod genesis;
mod p2p;
use std::sync::Arc;
use anyhow::Result;
use clap::Parser;
use tokio::sync::{mpsc, Mutex};
use tracing_subscriber::EnvFilter;
use nu_mempool::Mempool;
use nu_rpc::server::RpcServer;
use nu_state::StateDb;
use p2p::{NodeP2pEvent, P2pSender};
use tokio::sync::mpsc as rpc_mpsc;
#[derive(Parser)]
#[command(name = "nu-node", version)]
struct Cli {
/// Single-validator dev mode — no consensus, produces blocks every slot
#[arg(long)]
dev: bool,
/// Act as block-producing validator
#[arg(long)]
validator: bool,
/// Validator address
#[arg(long, default_value = "0xDEV0000000000000000000000000000000000000")]
validator_addr: String,
/// JSON-RPC HTTP bind address
#[arg(long, default_value = "0.0.0.0:9545")]
rpc_addr: String,
/// RocksDB data directory
#[arg(long, default_value = "./data/state")]
db_path: String,
/// Chain identifier
#[arg(long, default_value = "nu-devnet-1")]
chain_id: String,
/// nu-p2p publish API address (empty = P2P disabled)
/// e.g. http://127.0.0.1:30334
#[arg(long)]
p2p_api: Option<String>,
/// Path to genesis.json — applied once on first start if DB is empty
#[arg(long)]
genesis: Option<String>,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
let cli = Cli::parse();
let db = Arc::new(Mutex::new(StateDb::open(&cli.db_path)?));
let mempool = Arc::new(Mutex::new(Mempool::new()));
tracing::info!("State DB opened at {}", cli.db_path);
// Apply genesis if provided and DB is empty
if let Some(ref genesis_path) = cli.genesis {
let db_guard = db.lock().await;
if genesis::needs_seed(&db_guard) {
genesis::apply(&db_guard, genesis_path)?;
} else {
tracing::info!("genesis already applied — skipping");
}
}
// P2P event channel — block_loop and RPC publish, forwarded to nu-p2p via HTTP
let (p2p_sender, rpc_p2p_tx) = if let Some(ref api_url) = cli.p2p_api {
let (block_tx, mut block_rx) = mpsc::channel::<NodeP2pEvent>(128);
let (rpc_tx, mut rpc_rx) = rpc_mpsc::channel::<Vec<u8>>(256);
let publish_url = format!("{}/publish", api_url.trim_end_matches('/'));
let client = reqwest::Client::new();
tokio::spawn(async move {
loop {
tokio::select! {
Some(event) = block_rx.recv() => {
let body = match &event {
NodeP2pEvent::BlockAnnounce { height, hash } => {
tracing::info!(height, hash = %&hash[..8], "p2p: block announce");
serde_json::json!({
"type": "BlockAnnounce",
"data": { "height": height, "hash": hash }
})
}
NodeP2pEvent::TxGossip { raw_tx } => {
tracing::debug!(bytes = raw_tx.len(), "p2p: tx gossip");
serde_json::json!({
"type": "TxGossip",
"data": { "raw_tx": raw_tx }
})
}
};
let _ = client.post(&publish_url).json(&body).send().await;
}
Some(raw) = rpc_rx.recv() => {
tracing::debug!(bytes = raw.len(), "p2p: rpc tx gossip");
let body = serde_json::json!({
"type": "TxGossip",
"data": { "raw_tx": raw }
});
let _ = client.post(&publish_url).json(&body).send().await;
}
else => break,
}
}
});
(Some(P2pSender(block_tx)), Some(rpc_tx))
} else {
(None, None)
};
if cli.dev && cli.validator {
let cfg = block_loop::BlockLoopConfig {
validator_addr: cli.validator_addr.clone(),
chain_id: cli.chain_id.clone(),
dev_mode: cli.dev,
};
tokio::spawn(block_loop::run(
cfg,
Arc::clone(&db),
Arc::clone(&mempool),
p2p_sender.clone(),
));
}
let rpc = RpcServer::with_p2p(
cli.rpc_addr.clone(),
Arc::clone(&db),
Arc::clone(&mempool),
cli.chain_id.clone(),
rpc_p2p_tx,
);
tracing::info!(
chain_id = %cli.chain_id,
rpc_addr = %cli.rpc_addr,
dev = cli.dev,
validator = cli.validator,
p2p = cli.p2p_api.as_deref().unwrap_or("disabled"),
"nu-node ready"
);
rpc.run().await?;
Ok(())
}