feat(nu-node): single-validator block production loop
- nu-vm/engine.rs: execute_block runs all txs in a block against StateDb; TokenTransfer fully applied, other variants return "not implemented" receipt - StateAccessor trait: set_balance/inc_nonce now take &self (RocksDB interior mutability) - src/block_loop.rs: tokio task that produces a block each slot (6s) in dev mode; drains mempool, executes txs, removes successful ones, persists block to RocksDB - StateDb wrapped in Arc<Mutex> — block loop holds write lock per block, RPC holds read lock for nu_getAccount - main.rs: spawns block_loop when --dev --validator flags are set Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fd829ba1dd
commit
265097375a
11 changed files with 241 additions and 40 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
|
@ -808,7 +808,9 @@ name = "nu-node"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"hex",
|
||||||
"nu-block",
|
"nu-block",
|
||||||
"nu-consensus",
|
"nu-consensus",
|
||||||
"nu-mempool",
|
"nu-mempool",
|
||||||
|
|
@ -816,6 +818,8 @@ dependencies = [
|
||||||
"nu-state",
|
"nu-state",
|
||||||
"nu-vm",
|
"nu-vm",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|
@ -855,6 +859,7 @@ name = "nu-vm"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"nu-block",
|
||||||
"nu-state",
|
"nu-state",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,10 @@ anyhow.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
|
hex.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
nu-consensus = { path = "crates/nu-consensus" }
|
nu-consensus = { path = "crates/nu-consensus" }
|
||||||
nu-mempool = { path = "crates/nu-mempool" }
|
nu-mempool = { path = "crates/nu-mempool" }
|
||||||
nu-state = { path = "crates/nu-state" }
|
nu-state = { path = "crates/nu-state" }
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use nu_state::account::AccountState;
|
||||||
pub async fn dispatch(req: JsonRpcRequest, state: &AppState) -> JsonRpcResponse {
|
pub async fn dispatch(req: JsonRpcRequest, state: &AppState) -> JsonRpcResponse {
|
||||||
match req.method.as_str() {
|
match req.method.as_str() {
|
||||||
"nu_chainInfo" => handle_chain_info(&req, state),
|
"nu_chainInfo" => handle_chain_info(&req, state),
|
||||||
"nu_getAccount" => handle_get_account(&req, state),
|
"nu_getAccount" => handle_get_account(&req, state).await,
|
||||||
"nu_sendRawTx" => handle_send_raw_tx(&req, state).await,
|
"nu_sendRawTx" => handle_send_raw_tx(&req, state).await,
|
||||||
"nu_getBlock" => not_implemented(&req, "nu_getBlock"),
|
"nu_getBlock" => not_implemented(&req, "nu_getBlock"),
|
||||||
"nu_getTx" => not_implemented(&req, "nu_getTx"),
|
"nu_getTx" => not_implemented(&req, "nu_getTx"),
|
||||||
|
|
@ -33,14 +33,15 @@ fn handle_chain_info(req: &JsonRpcRequest, state: &AppState) -> JsonRpcResponse
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_get_account(req: &JsonRpcRequest, state: &AppState) -> JsonRpcResponse {
|
async fn handle_get_account(req: &JsonRpcRequest, state: &AppState) -> JsonRpcResponse {
|
||||||
let address = match req.params.get(0).and_then(|v| v.as_str()) {
|
let address = match req.params.get(0).and_then(|v| v.as_str()) {
|
||||||
Some(a) => a.to_string(),
|
Some(a) => a.to_string(),
|
||||||
None => return JsonRpcResponse::err(req.id.clone(), -32602, "Missing address param".into()),
|
None => return JsonRpcResponse::err(req.id.clone(), -32602, "Missing address param".into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let key = format!("account:{address}");
|
let key = format!("account:{address}");
|
||||||
match state.db.get::<AccountState>(&key) {
|
let db = state.db.lock().await;
|
||||||
|
match db.get::<AccountState>(&key) {
|
||||||
Ok(Some(account)) => {
|
Ok(Some(account)) => {
|
||||||
JsonRpcResponse::ok(req.id.clone(), serde_json::to_value(account).unwrap())
|
JsonRpcResponse::ok(req.id.clone(), serde_json::to_value(account).unwrap())
|
||||||
}
|
}
|
||||||
|
|
@ -55,23 +56,16 @@ fn handle_get_account(req: &JsonRpcRequest, state: &AppState) -> JsonRpcResponse
|
||||||
async fn handle_send_raw_tx(req: &JsonRpcRequest, state: &AppState) -> JsonRpcResponse {
|
async fn handle_send_raw_tx(req: &JsonRpcRequest, state: &AppState) -> JsonRpcResponse {
|
||||||
let raw_value = match req.params.get(0) {
|
let raw_value = match req.params.get(0) {
|
||||||
Some(v) => v.clone(),
|
Some(v) => v.clone(),
|
||||||
None => {
|
None => return JsonRpcResponse::err(req.id.clone(), -32602, "Missing tx param".into()),
|
||||||
return JsonRpcResponse::err(req.id.clone(), -32602, "Missing tx param".into())
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let tx: RawTransaction = match serde_json::from_value(raw_value) {
|
let tx: RawTransaction = match serde_json::from_value(raw_value) {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return JsonRpcResponse::err(
|
return JsonRpcResponse::err(req.id.clone(), -32602, format!("Invalid tx format: {e}"))
|
||||||
req.id.clone(),
|
|
||||||
-32602,
|
|
||||||
format!("Invalid tx format: {e}"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Basic structural validation before touching mempool
|
|
||||||
if tx.tx_id.is_empty() {
|
if tx.tx_id.is_empty() {
|
||||||
return JsonRpcResponse::err(req.id.clone(), -32602, "tx_id is required".into());
|
return JsonRpcResponse::err(req.id.clone(), -32602, "tx_id is required".into());
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +73,7 @@ async fn handle_send_raw_tx(req: &JsonRpcRequest, state: &AppState) -> JsonRpcRe
|
||||||
return JsonRpcResponse::err(req.id.clone(), -32602, "sender is required".into());
|
return JsonRpcResponse::err(req.id.clone(), -32602, "sender is required".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let tx_id = tx.tx_id.clone();
|
let tx_id = tx.tx_id.clone();
|
||||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||||
|
|
||||||
let mut pool = state.mempool.lock().await;
|
let mut pool = state.mempool.lock().await;
|
||||||
|
|
@ -89,14 +83,10 @@ async fn handle_send_raw_tx(req: &JsonRpcRequest, state: &AppState) -> JsonRpcRe
|
||||||
}
|
}
|
||||||
|
|
||||||
if pool.insert(tx, now_ms) {
|
if pool.insert(tx, now_ms) {
|
||||||
tracing::info!("tx accepted into mempool: {tx_id} (pool size: {})", pool.len());
|
tracing::info!("tx accepted: {tx_id} (pool size: {})", pool.len());
|
||||||
JsonRpcResponse::ok(req.id.clone(), json!({ "tx_id": tx_id }))
|
JsonRpcResponse::ok(req.id.clone(), json!({ "tx_id": tx_id }))
|
||||||
} else {
|
} else {
|
||||||
JsonRpcResponse::err(
|
JsonRpcResponse::err(req.id.clone(), -32000, "mempool full or sender limit reached".into())
|
||||||
req.id.clone(),
|
|
||||||
-32000,
|
|
||||||
"mempool full or sender limit reached".into(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@ use crate::{
|
||||||
types::{JsonRpcRequest, JsonRpcResponse},
|
types::{JsonRpcRequest, JsonRpcResponse},
|
||||||
};
|
};
|
||||||
use nu_mempool::Mempool;
|
use nu_mempool::Mempool;
|
||||||
use nu_state::db::StateDb;
|
use nu_state::StateDb;
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db: Arc<StateDb>,
|
pub db: Arc<Mutex<StateDb>>,
|
||||||
pub mempool: Arc<Mutex<Mempool>>,
|
pub mempool: Arc<Mutex<Mempool>>,
|
||||||
pub chain_id: String,
|
pub chain_id: String,
|
||||||
}
|
}
|
||||||
|
|
@ -30,9 +30,9 @@ pub struct RpcServer {
|
||||||
impl RpcServer {
|
impl RpcServer {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
bind_addr: impl Into<String>,
|
bind_addr: impl Into<String>,
|
||||||
db: Arc<StateDb>,
|
db: Arc<Mutex<StateDb>>,
|
||||||
mempool: Arc<Mutex<Mempool>>,
|
mempool: Arc<Mutex<Mempool>>,
|
||||||
chain_id: String,
|
chain_id: String,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
bind_addr: bind_addr.into(),
|
bind_addr: bind_addr.into(),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
use crate::{account::AccountState, db::StateDb};
|
use crate::{account::AccountState, db::StateDb};
|
||||||
|
|
||||||
|
/// Read/write access to account state.
|
||||||
|
/// `set_balance` and `inc_nonce` take `&self` because StateDb uses RocksDB's
|
||||||
|
/// interior mutability — writes do not require exclusive access at the Rust level.
|
||||||
pub trait StateAccessor {
|
pub trait StateAccessor {
|
||||||
fn get_balance(&self, address: &str) -> u64;
|
fn get_balance(&self, address: &str) -> u64;
|
||||||
fn get_nonce(&self, address: &str) -> u64;
|
fn get_nonce(&self, address: &str) -> u64;
|
||||||
fn set_balance(&mut self, address: &str, balance: u64);
|
fn set_balance(&self, address: &str, balance: u64);
|
||||||
fn inc_nonce(&mut self, address: &str);
|
fn inc_nonce(&self, address: &str);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StateAccessor for StateDb {
|
impl StateAccessor for StateDb {
|
||||||
|
|
@ -26,7 +29,7 @@ impl StateAccessor for StateDb {
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_balance(&mut self, address: &str, balance: u64) {
|
fn set_balance(&self, address: &str, balance: u64) {
|
||||||
let key = format!("account:{address}");
|
let key = format!("account:{address}");
|
||||||
let mut account = self
|
let mut account = self
|
||||||
.get::<AccountState>(&key)
|
.get::<AccountState>(&key)
|
||||||
|
|
@ -37,7 +40,7 @@ impl StateAccessor for StateDb {
|
||||||
let _ = self.put(&key, &account);
|
let _ = self.put(&key, &account);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inc_nonce(&mut self, address: &str) {
|
fn inc_nonce(&self, address: &str) {
|
||||||
let key = format!("account:{address}");
|
let key = format!("account:{address}");
|
||||||
let mut account = self
|
let mut account = self
|
||||||
.get::<AccountState>(&key)
|
.get::<AccountState>(&key)
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,4 @@ anyhow.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
nu-state = { path = "../nu-state" }
|
nu-state = { path = "../nu-state" }
|
||||||
|
nu-block = { path = "../nu-block" }
|
||||||
|
|
|
||||||
73
crates/nu-vm/src/engine.rs
Normal file
73
crates/nu-vm/src/engine.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
use nu_block::types::{Block, RawTransaction, TxPayload, TxReceipt};
|
||||||
|
use nu_state::StateAccessor;
|
||||||
|
|
||||||
|
use crate::executor::{execute_token_transfer, ExecutionContext};
|
||||||
|
|
||||||
|
pub struct BlockResult {
|
||||||
|
pub receipts: Vec<TxReceipt>,
|
||||||
|
pub applied: u32,
|
||||||
|
pub failed: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes all transactions in a block against the given state.
|
||||||
|
/// Each tx is validated and applied atomically; failures produce a receipt
|
||||||
|
/// but do not roll back already-applied txs.
|
||||||
|
pub fn execute_block(
|
||||||
|
block: &Block,
|
||||||
|
state: &dyn StateAccessor,
|
||||||
|
now_ms: i64,
|
||||||
|
) -> BlockResult {
|
||||||
|
let mut receipts = Vec::with_capacity(block.transactions.len());
|
||||||
|
let mut applied = 0u32;
|
||||||
|
let mut failed = 0u32;
|
||||||
|
|
||||||
|
for tx in &block.transactions {
|
||||||
|
let receipt = execute_tx(tx, state, block.header.height, now_ms);
|
||||||
|
if receipt.success { applied += 1; } else { failed += 1; }
|
||||||
|
receipts.push(receipt);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockResult { receipts, applied, failed }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_tx(
|
||||||
|
tx: &RawTransaction,
|
||||||
|
state: &dyn StateAccessor,
|
||||||
|
block_height: u64,
|
||||||
|
now_ms: i64,
|
||||||
|
) -> TxReceipt {
|
||||||
|
let ctx = ExecutionContext { state, block_height, now_ms };
|
||||||
|
|
||||||
|
let result = match &tx.payload {
|
||||||
|
TxPayload::TokenTransfer { to, amount } => {
|
||||||
|
execute_token_transfer(&ctx, &tx.sender, to, *amount, tx.fee, tx.nonce)
|
||||||
|
}
|
||||||
|
// Remaining variants return "not yet implemented" — applied in later Faz 1 tasks
|
||||||
|
other => {
|
||||||
|
let name = payload_name(other);
|
||||||
|
Err(crate::errors::VmError::Unknown(format!("{name} execution not yet implemented")))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(()) => TxReceipt { tx_id: tx.tx_id.clone(), success: true, error: String::new() },
|
||||||
|
Err(e) => TxReceipt { tx_id: tx.tx_id.clone(), success: false, error: e.to_string() },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn payload_name(payload: &TxPayload) -> &'static str {
|
||||||
|
match payload {
|
||||||
|
TxPayload::TokenTransfer { .. } => "TokenTransfer",
|
||||||
|
TxPayload::NodeSubmit { .. } => "NodeSubmit",
|
||||||
|
TxPayload::VoteRegister { .. } => "VoteRegister",
|
||||||
|
TxPayload::VoteCast { .. } => "VoteCast",
|
||||||
|
TxPayload::NftTransfer { .. } => "NftTransfer",
|
||||||
|
TxPayload::CollectionClaim { .. } => "CollectionClaim",
|
||||||
|
TxPayload::StakeOp { .. } => "StakeOp",
|
||||||
|
TxPayload::ValidatorRegister { .. }=> "ValidatorRegister",
|
||||||
|
TxPayload::NodeApprove { .. } => "NodeApprove",
|
||||||
|
TxPayload::NftMint { .. } => "NftMint",
|
||||||
|
TxPayload::NodeReject { .. } => "NodeReject",
|
||||||
|
TxPayload::VotingOpen { .. } => "VotingOpen",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,13 +2,13 @@ use crate::errors::VmError;
|
||||||
use nu_state::StateAccessor;
|
use nu_state::StateAccessor;
|
||||||
|
|
||||||
pub struct ExecutionContext<'a> {
|
pub struct ExecutionContext<'a> {
|
||||||
pub state: &'a mut dyn StateAccessor,
|
pub state: &'a dyn StateAccessor,
|
||||||
pub block_height: u64,
|
pub block_height: u64,
|
||||||
pub now_ms: i64,
|
pub now_ms: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_token_transfer(
|
pub fn execute_token_transfer(
|
||||||
ctx: &mut ExecutionContext,
|
ctx: &ExecutionContext,
|
||||||
sender: &str,
|
sender: &str,
|
||||||
to: &str,
|
to: &str,
|
||||||
amount: u64,
|
amount: u64,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
pub mod engine;
|
||||||
pub mod executor;
|
pub mod executor;
|
||||||
pub mod rewards;
|
pub mod rewards;
|
||||||
pub mod slashing;
|
pub mod slashing;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
|
|
||||||
|
pub use engine::{execute_block, BlockResult};
|
||||||
|
|
|
||||||
109
src/block_loop.rs
Normal file
109
src/block_loop.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tokio::time::{interval, Duration};
|
||||||
|
|
||||||
|
use nu_block::{builder::BlockBuilder, types::Block};
|
||||||
|
use nu_consensus::slot::current_slot;
|
||||||
|
use nu_mempool::Mempool;
|
||||||
|
use nu_state::StateDb;
|
||||||
|
use nu_vm::execute_block;
|
||||||
|
|
||||||
|
const MAX_TX_PER_BLOCK: usize = 500;
|
||||||
|
const BLOCK_INTERVAL_MS: u64 = 6_000; // one slot
|
||||||
|
|
||||||
|
pub struct BlockLoopConfig {
|
||||||
|
pub validator_addr: String,
|
||||||
|
pub chain_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(
|
||||||
|
config: BlockLoopConfig,
|
||||||
|
db: Arc<Mutex<StateDb>>,
|
||||||
|
mempool: Arc<Mutex<Mempool>>,
|
||||||
|
) {
|
||||||
|
let mut ticker = interval(Duration::from_millis(BLOCK_INTERVAL_MS));
|
||||||
|
let mut height: u64 = 1;
|
||||||
|
let mut prev_hash = "0".repeat(64);
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
chain_id = %config.chain_id,
|
||||||
|
validator = %config.validator_addr,
|
||||||
|
"block loop started"
|
||||||
|
);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
ticker.tick().await;
|
||||||
|
|
||||||
|
let slot = current_slot();
|
||||||
|
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||||
|
|
||||||
|
let pending = {
|
||||||
|
let mut pool = mempool.lock().await;
|
||||||
|
pool.select_for_block(MAX_TX_PER_BLOCK, now_ms)
|
||||||
|
};
|
||||||
|
|
||||||
|
if pending.is_empty() {
|
||||||
|
tracing::debug!(slot, height, "no pending txs — skipping block");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut builder = BlockBuilder::new(
|
||||||
|
height,
|
||||||
|
prev_hash.clone(),
|
||||||
|
slot,
|
||||||
|
config.validator_addr.clone(),
|
||||||
|
);
|
||||||
|
for p in &pending {
|
||||||
|
builder.add_tx(p.tx.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let block = match builder.build("0".repeat(64), vec![]) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(slot, height, "block build error: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = {
|
||||||
|
let db_guard = db.lock().await;
|
||||||
|
execute_block(&block, &*db_guard, now_ms)
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut pool = mempool.lock().await;
|
||||||
|
for r in &result.receipts {
|
||||||
|
if r.success {
|
||||||
|
pool.remove(&r.tx_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let block_key = format!("block:{height}");
|
||||||
|
{
|
||||||
|
let db_guard = db.lock().await;
|
||||||
|
if let Err(e) = db_guard.put(&block_key, &block) {
|
||||||
|
tracing::error!(height, "failed to persist block: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_hash = block_hash(&block);
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
slot,
|
||||||
|
height,
|
||||||
|
applied = result.applied,
|
||||||
|
failed = result.failed,
|
||||||
|
"block produced"
|
||||||
|
);
|
||||||
|
|
||||||
|
height += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_hash(block: &Block) -> String {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
let data = serde_json::to_vec(&block.header).unwrap_or_default();
|
||||||
|
hex::encode(Sha256::digest(&data))
|
||||||
|
}
|
||||||
33
src/main.rs
33
src/main.rs
|
|
@ -1,3 +1,5 @@
|
||||||
|
mod block_loop;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
@ -12,14 +14,18 @@ use nu_state::StateDb;
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "nu-node", version)]
|
#[command(name = "nu-node", version)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
/// Run in single-validator dev mode (no consensus required)
|
/// Single-validator dev mode — no consensus, produces blocks every slot
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
dev: bool,
|
dev: bool,
|
||||||
|
|
||||||
/// Act as validator in this session
|
/// Act as block-producing validator
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
validator: bool,
|
validator: bool,
|
||||||
|
|
||||||
|
/// Validator address (required when --validator is set)
|
||||||
|
#[arg(long, default_value = "0xDEV0000000000000000000000000000000000000")]
|
||||||
|
validator_addr: String,
|
||||||
|
|
||||||
/// JSON-RPC HTTP bind address
|
/// JSON-RPC HTTP bind address
|
||||||
#[arg(long, default_value = "0.0.0.0:9545")]
|
#[arg(long, default_value = "0.0.0.0:9545")]
|
||||||
rpc_addr: String,
|
rpc_addr: String,
|
||||||
|
|
@ -41,15 +47,20 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
if cli.dev {
|
let db = Arc::new(Mutex::new(StateDb::open(&cli.db_path)?));
|
||||||
tracing::info!("Starting in --dev mode (single validator, consensus disabled)");
|
|
||||||
}
|
|
||||||
|
|
||||||
let db = Arc::new(StateDb::open(&cli.db_path)?);
|
|
||||||
tracing::info!("State DB opened at {}", cli.db_path);
|
tracing::info!("State DB opened at {}", cli.db_path);
|
||||||
|
|
||||||
let mempool = Arc::new(Mutex::new(Mempool::new()));
|
let mempool = Arc::new(Mutex::new(Mempool::new()));
|
||||||
|
|
||||||
|
// Spawn block production loop in dev mode
|
||||||
|
if cli.dev && cli.validator {
|
||||||
|
let cfg = block_loop::BlockLoopConfig {
|
||||||
|
validator_addr: cli.validator_addr.clone(),
|
||||||
|
chain_id: cli.chain_id.clone(),
|
||||||
|
};
|
||||||
|
tokio::spawn(block_loop::run(cfg, Arc::clone(&db), Arc::clone(&mempool)));
|
||||||
|
}
|
||||||
|
|
||||||
let rpc = RpcServer::new(
|
let rpc = RpcServer::new(
|
||||||
cli.rpc_addr.clone(),
|
cli.rpc_addr.clone(),
|
||||||
Arc::clone(&db),
|
Arc::clone(&db),
|
||||||
|
|
@ -58,9 +69,11 @@ async fn main() -> Result<()> {
|
||||||
);
|
);
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"nu-node ready — chain_id={} rpc={}",
|
chain_id = %cli.chain_id,
|
||||||
cli.chain_id,
|
rpc_addr = %cli.rpc_addr,
|
||||||
cli.rpc_addr
|
dev = cli.dev,
|
||||||
|
validator = cli.validator,
|
||||||
|
"nu-node ready"
|
||||||
);
|
);
|
||||||
|
|
||||||
rpc.run().await?;
|
rpc.run().await?;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue