feat(nu-node): initial Faz 0 scaffold

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mukan Erkin TÖRÜK 2026-04-24 00:00:26 +03:00
commit 5430c34d9e
38 changed files with 902 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
target/
*.rs.bk
.env
data/

51
CLAUDE.md Normal file
View file

@ -0,0 +1,51 @@
# nu-node — CLAUDE.md
L1 node implementasyonu. Consensus, mempool, state, block üretimi, RPC.
## Crate Yapısı
| Crate | Sorumluluk |
|-------|-----------|
| `nu-consensus` | PoN validator seçim, slot sistemi, PoN skoru, oylama scheduler |
| `nu-mempool` | Bekleyen tx havuzu; öncelik: Critical > High > Normal |
| `nu-state` | AccountState, StoryNodeState, NftState; RocksDB backend |
| `nu-block` | BlockBuilder (Merkle root), BlockVerifier |
| `nu-rpc` | JSON-RPC HTTP server + WebSocket subscription |
| `nu-vm` | Tx executor, ödül dağıtımı, slashing |
## Kritik Kurallar
- `unwrap()` / `expect()` sadece test kodunda. Production'da `?` veya açık hata.
- State'e dokunan her fonksiyon: önce **tam doğrulama**, sonra mutation.
- Slashing ve ödül dağıtımı **atomik** — başarısız olursa state değişmez.
- `BURN_WALLET` ve `DEV_WALLET` adresleri env variable; hardcode yasak.
## Sabitler (nu-consensus/src/types.rs)
```
SLOT_DURATION_MS = 6_000
ROUND_SIZE = 21
MIN_VALIDATOR_STAKE = 1_000 NUT
PoN: start=1.0, max=1.8, min=0.5
win=+0.02, lose=-0.05, honest_block=+0.01
WHALE_CAP = 5% of total weight
```
## Geliştirme
```bash
# Devnet (tek validator, consensus devre dışı)
cargo run --bin nu-node -- --dev --validator
# Testleri çalıştır
cargo test
# Lint
cargo fmt && cargo clippy
```
## Faz Durumu
- **Faz 0 (şu an):** Scaffold tamam. Modül sınırları tanımlı, tipler yazıldı.
- **Faz 1:** Tx execution (TokenTransfer önce), tek validator devnet, JSON-RPC server.
- **Faz 2:** PoN consensus tam implementasyon, slashing, 3+ validator.

47
Cargo.toml Normal file
View file

@ -0,0 +1,47 @@
[workspace]
members = [
".",
"crates/nu-consensus",
"crates/nu-mempool",
"crates/nu-state",
"crates/nu-block",
"crates/nu-rpc",
"crates/nu-vm",
]
resolver = "2"
[workspace.dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
thiserror = "1"
tracing = "1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
rocksdb = "0.22"
sha2 = "0.10"
hex = "0.4"
uuid = { version = "1", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] }
[package]
name = "nu-node"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "nu-node"
path = "src/main.rs"
[dependencies]
tokio.workspace = true
serde.workspace = true
anyhow.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
nu-consensus = { path = "crates/nu-consensus" }
nu-mempool = { path = "crates/nu-mempool" }
nu-state = { path = "crates/nu-state" }
nu-block = { path = "crates/nu-block" }
nu-rpc = { path = "crates/nu-rpc" }
nu-vm = { path = "crates/nu-vm" }

View file

@ -0,0 +1,12 @@
[package]
name = "nu-block"
version = "0.1.0"
edition = "2021"
[dependencies]
serde.workspace = true
anyhow.workspace = true
thiserror.workspace = true
sha2.workspace = true
hex.workspace = true
nu-state = { path = "../nu-state" }

View file

@ -0,0 +1,64 @@
use crate::types::{Block, BlockHeader, RawTransaction};
use anyhow::Result;
pub struct BlockBuilder {
height: u64,
prev_block_hash: String,
slot: u32,
validator_addr: String,
transactions: Vec<RawTransaction>,
}
impl BlockBuilder {
pub fn new(height: u64, prev_block_hash: String, slot: u32, validator_addr: String) -> Self {
Self { height, prev_block_hash, slot, validator_addr, transactions: vec![] }
}
pub fn add_tx(&mut self, tx: RawTransaction) {
self.transactions.push(tx);
}
pub fn build(self, state_root: String, validator_sig: Vec<u8>) -> Result<Block> {
let tx_root = compute_merkle_root(&self.transactions);
let receipts_root = String::new(); // computed after execution
let header = BlockHeader {
height: self.height,
prev_block_hash: self.prev_block_hash,
timestamp: now_ms(),
validator_addr: self.validator_addr,
validator_sig,
tx_root,
state_root,
receipts_root,
slot: self.slot,
};
Ok(Block { header, transactions: self.transactions })
}
}
fn compute_merkle_root(txs: &[RawTransaction]) -> String {
use sha2::{Digest, Sha256};
if txs.is_empty() {
return "0".repeat(64);
}
let mut hashes: Vec<Vec<u8>> = txs
.iter()
.map(|t| Sha256::digest(t.tx_id.as_bytes()).to_vec())
.collect();
while hashes.len() > 1 {
hashes = hashes.chunks(2).map(|pair| {
let mut h = Sha256::new();
h.update(&pair[0]);
h.update(pair.get(1).unwrap_or(&pair[0]));
h.finalize().to_vec()
}).collect();
}
hex::encode(&hashes[0])
}
fn now_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as i64
}

View file

@ -0,0 +1,3 @@
pub mod builder;
pub mod verifier;
pub mod types;

View file

@ -0,0 +1,37 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockHeader {
pub height: u64,
pub prev_block_hash: String,
pub timestamp: i64,
pub validator_addr: String,
pub validator_sig: Vec<u8>,
pub tx_root: String,
pub state_root: String,
pub receipts_root: String,
pub slot: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawTransaction {
pub tx_id: String,
pub sender: String,
pub nonce: u64,
pub fee: u64,
pub sig: Vec<u8>,
pub payload: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Block {
pub header: BlockHeader,
pub transactions: Vec<RawTransaction>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxReceipt {
pub tx_id: String,
pub success: bool,
pub error: String,
}

View file

@ -0,0 +1,13 @@
use crate::types::Block;
use anyhow::{bail, Result};
pub fn verify_block(block: &Block, expected_prev_hash: &str) -> Result<()> {
if block.header.prev_block_hash != expected_prev_hash {
bail!("prev_block_hash mismatch");
}
if block.header.validator_sig.is_empty() {
bail!("missing validator signature");
}
// TODO: secp256k1 signature verification against validator_addr
Ok(())
}

View file

@ -0,0 +1,14 @@
[package]
name = "nu-consensus"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio.workspace = true
serde.workspace = true
anyhow.workspace = true
thiserror.workspace = true
tracing.workspace = true
sha2.workspace = true
hex.workspace = true
chrono.workspace = true

View file

@ -0,0 +1,5 @@
pub mod types;
pub mod scheduler;
pub mod validator_set;
pub mod pon_score;
pub mod slot;

View file

@ -0,0 +1,19 @@
use crate::types::*;
pub fn update_on_vote(record: &mut ValidatorRecord, won: bool) {
let delta = if won { PON_SCORE_WIN } else { PON_SCORE_LOSE };
record.pon_score = (record.pon_score + delta).clamp(PON_SCORE_MIN, PON_SCORE_MAX);
}
pub fn update_on_honest_block(record: &mut ValidatorRecord) {
record.pon_score = (record.pon_score + PON_SCORE_HONEST_BLOCK)
.clamp(PON_SCORE_MIN, PON_SCORE_MAX);
}
pub fn voting_weight(record: &ValidatorRecord, nft_depth_bonus: f64) -> f64 {
(record.stake as f64).sqrt() * record.pon_score * (1.0 + nft_depth_bonus)
}
pub fn apply_whale_cap(weight: f64, total_weight: f64) -> f64 {
weight.min(total_weight * WHALE_CAP_RATIO)
}

View file

@ -0,0 +1,33 @@
// Voting cycle scheduler — produces NodeApprove/NodeReject/VotingOpen auto-transactions.
// Triggered by the block producer at slot boundaries.
use anyhow::Result;
pub enum SchedulerEvent {
VotingOpen { temp_id: String },
NodeApprove { temp_id: String },
NodeReject { temp_id: String },
}
pub trait PendingNodeStore {
fn nodes_ready_for_voting(&self, now_ms: i64) -> Vec<(String, i64)>; // (temp_id, submitted_at)
fn nodes_ready_for_finalization(&self, now_ms: i64) -> Vec<(String, bool)>; // (temp_id, approved)
}
pub fn tick(store: &dyn PendingNodeStore, now_ms: i64) -> Result<Vec<SchedulerEvent>> {
let mut events = vec![];
for (temp_id, _) in store.nodes_ready_for_voting(now_ms) {
events.push(SchedulerEvent::VotingOpen { temp_id });
}
for (temp_id, approved) in store.nodes_ready_for_finalization(now_ms) {
if approved {
events.push(SchedulerEvent::NodeApprove { temp_id });
} else {
events.push(SchedulerEvent::NodeReject { temp_id });
}
}
Ok(events)
}

View file

@ -0,0 +1,14 @@
use crate::types::SLOT_DURATION_MS;
use std::time::{SystemTime, UNIX_EPOCH};
pub fn current_slot() -> u32 {
let ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
(ms / SLOT_DURATION_MS) as u32
}
pub fn slot_start_ms(slot: u32) -> u64 {
slot as u64 * SLOT_DURATION_MS
}

View file

@ -0,0 +1,38 @@
use serde::{Deserialize, Serialize};
pub const SLOT_DURATION_MS: u64 = 6_000;
pub const ROUND_SIZE: u32 = 21;
pub const MAX_CONSECUTIVE_BLOCKS: u32 = 5;
pub const MIN_VALIDATOR_STAKE: u64 = 1_000_000_000; // 1000 NUT in micro-units
pub const PON_SCORE_INIT: f64 = 1.0;
pub const PON_SCORE_MAX: f64 = 1.8;
pub const PON_SCORE_MIN: f64 = 0.5;
pub const PON_SCORE_WIN: f64 = 0.02;
pub const PON_SCORE_LOSE: f64 = -0.05;
pub const PON_SCORE_HONEST_BLOCK: f64 = 0.01;
pub const WHALE_CAP_RATIO: f64 = 0.05;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidatorRecord {
pub address: String,
pub stake: u64,
pub pon_score: f64,
pub is_active: bool,
pub last_block: u64,
pub slash_count: u32,
pub consecutive_blocks: u32,
}
impl ValidatorRecord {
pub fn new(address: String, stake: u64) -> Self {
Self {
address,
stake,
pon_score: PON_SCORE_INIT,
is_active: true,
last_block: 0,
slash_count: 0,
consecutive_blocks: 0,
}
}
}

View file

@ -0,0 +1,55 @@
use crate::types::{ValidatorRecord, MAX_CONSECUTIVE_BLOCKS, MIN_VALIDATOR_STAKE};
use sha2::{Digest, Sha256};
pub struct ValidatorSet {
pub validators: Vec<ValidatorRecord>,
}
impl ValidatorSet {
pub fn new() -> Self {
Self { validators: vec![] }
}
pub fn register(&mut self, record: ValidatorRecord) {
self.validators.push(record);
}
/// Weighted shuffle seeded by slot + prev_block_hash; filters ineligible validators.
pub fn schedule(&self, slot: u32, prev_block_hash: &str) -> Vec<String> {
let seed = Self::make_seed(slot, prev_block_hash);
let mut candidates: Vec<&ValidatorRecord> = self
.validators
.iter()
.filter(|v| v.is_active && v.stake >= MIN_VALIDATOR_STAKE)
.filter(|v| v.consecutive_blocks < MAX_CONSECUTIVE_BLOCKS)
.collect();
// Weighted shuffle: higher pon_score → higher probability of early position
Self::weighted_shuffle(&mut candidates, &seed);
candidates
.into_iter()
.take(21)
.map(|v| v.address.clone())
.collect()
}
fn make_seed(slot: u32, prev_hash: &str) -> [u8; 32] {
let mut h = Sha256::new();
h.update(slot.to_le_bytes());
h.update(prev_hash.as_bytes());
h.finalize().into()
}
fn weighted_shuffle(candidates: &mut Vec<&ValidatorRecord>, seed: &[u8; 32]) {
// Fisher-Yates with pon_score-weighted random keys derived from seed
for i in (1..candidates.len()).rev() {
let key = u64::from_le_bytes(seed[..8].try_into().unwrap())
.wrapping_add(i as u64)
.wrapping_mul(candidates[i].pon_score.to_bits());
let j = (key as usize) % (i + 1);
candidates.swap(i, j);
}
}
}

View file

@ -0,0 +1,12 @@
[package]
name = "nu-mempool"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio.workspace = true
serde.workspace = true
anyhow.workspace = true
thiserror.workspace = true
tracing.workspace = true
chrono.workspace = true

View file

@ -0,0 +1,5 @@
pub mod pool;
pub mod priority;
pub mod types;
pub use pool::Mempool;

View file

@ -0,0 +1,68 @@
use crate::types::*;
use crate::priority::TxPriority;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct PendingTx {
pub tx_id: String,
pub sender: String,
pub fee: u64,
pub priority: TxPriority,
pub received_at: i64, // Unix epoch ms
pub raw: Vec<u8>,
}
pub struct Mempool {
txs: Vec<PendingTx>,
sender_counts: HashMap<String, usize>,
}
impl Mempool {
pub fn new() -> Self {
Self {
txs: vec![],
sender_counts: HashMap::new(),
}
}
pub fn insert(&mut self, tx: PendingTx) -> bool {
if self.txs.len() >= MEMPOOL_MAX_TX {
return false;
}
let count = self.sender_counts.entry(tx.sender.clone()).or_insert(0);
if *count >= MAX_TX_PER_SENDER && tx.priority == TxPriority::Normal {
return false;
}
*count += 1;
self.txs.push(tx);
true
}
pub fn select_for_block(&mut self, max_tx: usize, now_ms: i64) -> Vec<PendingTx> {
self.evict_expired(now_ms);
let mut sorted = self.txs.clone();
sorted.sort_by(|a, b| {
b.priority.cmp(&a.priority)
.then(b.fee.cmp(&a.fee))
});
sorted.into_iter().take(max_tx).collect()
}
pub fn remove(&mut self, tx_id: &str) {
if let Some(pos) = self.txs.iter().position(|t| t.tx_id == tx_id) {
let tx = self.txs.remove(pos);
if let Some(c) = self.sender_counts.get_mut(&tx.sender) {
*c = c.saturating_sub(1);
}
}
}
fn evict_expired(&mut self, now_ms: i64) {
self.txs.retain(|t| now_ms - t.received_at < MEMPOOL_TTL_MS);
}
pub fn len(&self) -> usize {
self.txs.len()
}
}

View file

@ -0,0 +1,9 @@
// NodeApprove and VoteCast transactions are prioritized over regular transfers.
// Within same priority tier, higher fee wins.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum TxPriority {
Normal = 0,
High = 1, // VoteCast, VoteRegister
Critical = 2, // NodeApprove, VotingOpen, NodeReject (auto-scheduler)
}

View file

@ -0,0 +1,3 @@
pub const MEMPOOL_MAX_TX: usize = 10_000;
pub const MEMPOOL_TTL_MS: i64 = 3_600_000; // 1 hour
pub const MAX_TX_PER_SENDER: usize = 5;

11
crates/nu-rpc/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "nu-rpc"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio.workspace = true
serde.workspace = true
serde_json.workspace = true
anyhow.workspace = true
tracing.workspace = true

View file

@ -0,0 +1,28 @@
// Method dispatch — each handler maps to a nu.rpc.api method name.
// Handlers are stubs; implementation wired in Faz 1.
use crate::types::{JsonRpcRequest, JsonRpcResponse};
pub fn dispatch(req: JsonRpcRequest) -> JsonRpcResponse {
match req.method.as_str() {
"nu_getBlock" => stub(&req, "nu_getBlock"),
"nu_getTx" => stub(&req, "nu_getTx"),
"nu_getAccount" => stub(&req, "nu_getAccount"),
"nu_getStory" => stub(&req, "nu_getStory"),
"nu_getNode" => stub(&req, "nu_getNode"),
"nu_getNft" => stub(&req, "nu_getNft"),
"nu_listStories" => stub(&req, "nu_listStories"),
"nu_listPendingVotes" => stub(&req, "nu_listPendingVotes"),
"nu_sendRawTx" => stub(&req, "nu_sendRawTx"),
"nu_chainInfo" => stub(&req, "nu_chainInfo"),
_ => JsonRpcResponse::err(req.id, -32601, "Method not found".into()),
}
}
fn stub(req: &JsonRpcRequest, method: &str) -> JsonRpcResponse {
JsonRpcResponse::err(
req.id.clone(),
-32000,
format!("{method} not implemented yet"),
)
}

3
crates/nu-rpc/src/lib.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod server;
pub mod handlers;
pub mod types;

View file

@ -0,0 +1,19 @@
// HTTP + WebSocket server skeleton — wired in Faz 1 with axum or hyper.
// POST /rpc → JSON-RPC dispatch
// WS /ws → event subscription stream
pub struct RpcServer {
pub bind_addr: String,
}
impl RpcServer {
pub fn new(bind_addr: impl Into<String>) -> Self {
Self { bind_addr: bind_addr.into() }
}
pub async fn run(self) -> anyhow::Result<()> {
tracing::info!("RPC server listening on {}", self.bind_addr);
// TODO Faz 1: axum router, /rpc POST handler, /ws upgrade
Ok(())
}
}

View file

@ -0,0 +1,33 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub method: String,
pub params: serde_json::Value,
pub id: serde_json::Value,
}
#[derive(Debug, Serialize)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
pub result: Option<serde_json::Value>,
pub error: Option<JsonRpcError>,
pub id: serde_json::Value,
}
#[derive(Debug, Serialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
}
impl JsonRpcResponse {
pub fn ok(id: serde_json::Value, result: serde_json::Value) -> Self {
Self { jsonrpc: "2.0".into(), result: Some(result), error: None, id }
}
pub fn err(id: serde_json::Value, code: i32, message: String) -> Self {
Self { jsonrpc: "2.0".into(), result: None, error: Some(JsonRpcError { code, message }), id }
}
}

View file

@ -0,0 +1,13 @@
[package]
name = "nu-state"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio.workspace = true
serde.workspace = true
serde_json.workspace = true
anyhow.workspace = true
thiserror.workspace = true
tracing.workspace = true
rocksdb.workspace = true

View file

@ -0,0 +1,23 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AccountState {
pub address: String,
pub balance: u64, // NUT micro-units
pub nonce: u64,
pub staked: u64,
pub locked: u64,
pub locked_until: i64, // Unix epoch ms; 0 = not locked
pub pon_score: f64,
pub nft_ids: Vec<String>,
}
impl AccountState {
pub fn new(address: String) -> Self {
Self { address, pon_score: 1.0, ..Default::default() }
}
pub fn available_balance(&self) -> u64 {
self.balance.saturating_sub(self.locked)
}
}

33
crates/nu-state/src/db.rs Normal file
View file

@ -0,0 +1,33 @@
use anyhow::Result;
use rocksdb::{DB, Options};
use serde::{de::DeserializeOwned, Serialize};
pub struct StateDb {
db: DB,
}
impl StateDb {
pub fn open(path: &str) -> Result<Self> {
let mut opts = Options::default();
opts.create_if_missing(true);
Ok(Self { db: DB::open(&opts, path)? })
}
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
match self.db.get(key.as_bytes())? {
Some(bytes) => Ok(Some(serde_json::from_slice(&bytes)?)),
None => Ok(None),
}
}
pub fn put<T: Serialize>(&self, key: &str, value: &T) -> Result<()> {
let bytes = serde_json::to_vec(value)?;
self.db.put(key.as_bytes(), bytes)?;
Ok(())
}
pub fn delete(&self, key: &str) -> Result<()> {
self.db.delete(key.as_bytes())?;
Ok(())
}
}

View file

@ -0,0 +1,6 @@
pub mod account;
pub mod story_node;
pub mod nft;
pub mod db;
pub use db::StateDb;

View file

@ -0,0 +1,29 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NftState {
pub nft_id: String,
pub node_id: String,
pub owner: String,
pub collection_id: String, // empty until CollectionClaim
pub depth: u32,
pub lineage: Vec<String>, // node_ids root → this
pub minted_at: i64,
}
/// Validates that nft_ids form a valid lineage (each id is a numeric prefix extension of the previous).
pub fn validate_lineage(nft_ids: &[String]) -> bool {
if nft_ids.is_empty() {
return false;
}
for window in nft_ids.windows(2) {
let parent = &window[0];
let child = &window[1];
// child must start with parent (e.g. "1" → "11" → "115")
// and child must be exactly one digit longer
if !child.starts_with(parent.as_str()) || child.len() != parent.len() + 1 {
return false;
}
}
true
}

View file

@ -0,0 +1,47 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum NodeStatus {
Pending,
VotingOpen,
Approved,
Rejected,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WeightedVote {
pub voter: String,
pub approve: bool,
pub weight: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoryNodeState {
pub node_id: String, // canonical; empty until approved
pub temp_id: String, // client UUID
pub story_id: String,
pub parent_id: String,
pub author: String,
pub content_hash: String,
pub status: NodeStatus,
pub submitted_at: i64,
pub vote_open_at: i64,
pub vote_end_at: i64,
pub votes: Vec<WeightedVote>,
pub nft_id: String, // set on approval
pub entry_fee: u64,
}
impl StoryNodeState {
pub fn approve_votes_weight(&self) -> f64 {
self.votes.iter().filter(|v| v.approve).map(|v| v.weight).sum()
}
pub fn reject_votes_weight(&self) -> f64 {
self.votes.iter().filter(|v| !v.approve).map(|v| v.weight).sum()
}
pub fn is_approved(&self) -> bool {
self.approve_votes_weight() > self.reject_votes_weight()
}
}

12
crates/nu-vm/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "nu-vm"
version = "0.1.0"
edition = "2021"
[dependencies]
serde.workspace = true
serde_json.workspace = true
anyhow.workspace = true
thiserror.workspace = true
tracing.workspace = true
nu-state = { path = "../nu-state" }

View file

@ -0,0 +1,23 @@
use thiserror::Error;
#[derive(Debug, Error)]
pub enum VmError {
#[error("insufficient balance: need {need}, have {have}")]
InsufficientBalance { need: u64, have: u64 },
#[error("invalid nonce: expected {expected}, got {got}")]
InvalidNonce { expected: u64, got: u64 },
#[error("invalid signature")]
InvalidSignature,
#[error("nft not owned by sender")]
NftNotOwned,
#[error("nft already claimed in another collection")]
NftAlreadyClaimed,
#[error("invalid lineage path")]
InvalidLineage,
#[error("stake below minimum")]
StakeTooLow,
#[error("double spend detected")]
DoubleSpend,
#[error("unknown: {0}")]
Unknown(String),
}

View file

@ -0,0 +1,43 @@
// Transaction executor — validates then mutates state atomically.
// Rule: validate fully before any state mutation; on error, state is unchanged.
use crate::errors::VmError;
pub struct ExecutionContext<'a> {
pub state: &'a mut dyn StateAccessor,
pub block_height: u64,
pub now_ms: i64,
}
pub trait StateAccessor {
fn get_balance(&self, address: &str) -> u64;
fn get_nonce(&self, address: &str) -> u64;
fn set_balance(&mut self, address: &str, balance: u64);
fn inc_nonce(&mut self, address: &str);
}
pub fn execute_token_transfer(
ctx: &mut ExecutionContext,
sender: &str,
to: &str,
amount: u64,
fee: u64,
nonce: u64,
) -> Result<(), VmError> {
// Validate
let expected_nonce = ctx.state.get_nonce(sender);
if nonce != expected_nonce {
return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce });
}
let balance = ctx.state.get_balance(sender);
let total = amount.checked_add(fee).ok_or(VmError::Unknown("overflow".into()))?;
if balance < total {
return Err(VmError::InsufficientBalance { need: total, have: balance });
}
// Mutate
ctx.state.set_balance(sender, balance - total);
ctx.state.set_balance(to, ctx.state.get_balance(to) + amount);
ctx.state.inc_nonce(sender);
Ok(())
}

4
crates/nu-vm/src/lib.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod executor;
pub mod rewards;
pub mod slashing;
pub mod errors;

View file

@ -0,0 +1,27 @@
pub const NODE_REWARD_TOTAL: u64 = 100_000_000; // 100 NUT in micro-units
pub struct RewardSplit {
pub voters: u64, // 50%
pub author: u64, // 25%
pub burn: u64, // 25%
}
pub fn community_split() -> RewardSplit {
RewardSplit {
voters: NODE_REWARD_TOTAL / 2,
author: NODE_REWARD_TOTAL / 4,
burn: NODE_REWARD_TOTAL / 4,
}
}
pub fn genesis_split() -> u64 {
NODE_REWARD_TOTAL // 100% to dev wallet
}
pub fn entry_fee(reward: u64) -> u64 {
reward / 4 // 25%
}
pub fn vote_stake_lock(reward: u64) -> u64 {
reward / 10 // 10%
}

View file

@ -0,0 +1,25 @@
use nu_state::account::AccountState;
pub struct SlashResult {
pub amount_cut: u64,
pub to_burn: u64,
pub to_reporter: u64,
}
pub fn slash_double_sign(validator: &mut AccountState) -> SlashResult {
let cut = validator.staked / 10; // 10%
validator.staked = validator.staked.saturating_sub(cut);
split_slash(cut)
}
pub fn slash_invalid_block(validator: &mut AccountState) -> SlashResult {
let cut = validator.staked / 20; // 5%
validator.staked = validator.staked.saturating_sub(cut);
split_slash(cut)
}
fn split_slash(cut: u64) -> SlashResult {
let to_burn = cut / 2;
let to_reporter = cut - to_burn;
SlashResult { amount_cut: cut, to_burn, to_reporter }
}

17
src/main.rs Normal file
View file

@ -0,0 +1,17 @@
use anyhow::Result;
use tracing_subscriber::EnvFilter;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
tracing::info!("nu-node starting...");
// TODO Faz 1: parse CLI args (--dev, --validator, --rpc-port, --db-path)
// TODO Faz 1: init StateDb, Mempool, ValidatorSet, RpcServer
// TODO Faz 1: run consensus loop
Ok(())
}