feat(nu-node): initial Faz 0 scaffold
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
5430c34d9e
38 changed files with 902 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
target/
|
||||||
|
*.rs.bk
|
||||||
|
.env
|
||||||
|
data/
|
||||||
51
CLAUDE.md
Normal file
51
CLAUDE.md
Normal 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
47
Cargo.toml
Normal 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" }
|
||||||
12
crates/nu-block/Cargo.toml
Normal file
12
crates/nu-block/Cargo.toml
Normal 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" }
|
||||||
64
crates/nu-block/src/builder.rs
Normal file
64
crates/nu-block/src/builder.rs
Normal 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
|
||||||
|
}
|
||||||
3
crates/nu-block/src/lib.rs
Normal file
3
crates/nu-block/src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod builder;
|
||||||
|
pub mod verifier;
|
||||||
|
pub mod types;
|
||||||
37
crates/nu-block/src/types.rs
Normal file
37
crates/nu-block/src/types.rs
Normal 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,
|
||||||
|
}
|
||||||
13
crates/nu-block/src/verifier.rs
Normal file
13
crates/nu-block/src/verifier.rs
Normal 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(())
|
||||||
|
}
|
||||||
14
crates/nu-consensus/Cargo.toml
Normal file
14
crates/nu-consensus/Cargo.toml
Normal 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
|
||||||
5
crates/nu-consensus/src/lib.rs
Normal file
5
crates/nu-consensus/src/lib.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod types;
|
||||||
|
pub mod scheduler;
|
||||||
|
pub mod validator_set;
|
||||||
|
pub mod pon_score;
|
||||||
|
pub mod slot;
|
||||||
19
crates/nu-consensus/src/pon_score.rs
Normal file
19
crates/nu-consensus/src/pon_score.rs
Normal 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)
|
||||||
|
}
|
||||||
33
crates/nu-consensus/src/scheduler.rs
Normal file
33
crates/nu-consensus/src/scheduler.rs
Normal 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)
|
||||||
|
}
|
||||||
14
crates/nu-consensus/src/slot.rs
Normal file
14
crates/nu-consensus/src/slot.rs
Normal 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
|
||||||
|
}
|
||||||
38
crates/nu-consensus/src/types.rs
Normal file
38
crates/nu-consensus/src/types.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
crates/nu-consensus/src/validator_set.rs
Normal file
55
crates/nu-consensus/src/validator_set.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
crates/nu-mempool/Cargo.toml
Normal file
12
crates/nu-mempool/Cargo.toml
Normal 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
|
||||||
5
crates/nu-mempool/src/lib.rs
Normal file
5
crates/nu-mempool/src/lib.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod pool;
|
||||||
|
pub mod priority;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
pub use pool::Mempool;
|
||||||
68
crates/nu-mempool/src/pool.rs
Normal file
68
crates/nu-mempool/src/pool.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
9
crates/nu-mempool/src/priority.rs
Normal file
9
crates/nu-mempool/src/priority.rs
Normal 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)
|
||||||
|
}
|
||||||
3
crates/nu-mempool/src/types.rs
Normal file
3
crates/nu-mempool/src/types.rs
Normal 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
11
crates/nu-rpc/Cargo.toml
Normal 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
|
||||||
28
crates/nu-rpc/src/handlers.rs
Normal file
28
crates/nu-rpc/src/handlers.rs
Normal 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
3
crates/nu-rpc/src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod server;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod types;
|
||||||
19
crates/nu-rpc/src/server.rs
Normal file
19
crates/nu-rpc/src/server.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
33
crates/nu-rpc/src/types.rs
Normal file
33
crates/nu-rpc/src/types.rs
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
13
crates/nu-state/Cargo.toml
Normal file
13
crates/nu-state/Cargo.toml
Normal 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
|
||||||
23
crates/nu-state/src/account.rs
Normal file
23
crates/nu-state/src/account.rs
Normal 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
33
crates/nu-state/src/db.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
6
crates/nu-state/src/lib.rs
Normal file
6
crates/nu-state/src/lib.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
pub mod account;
|
||||||
|
pub mod story_node;
|
||||||
|
pub mod nft;
|
||||||
|
pub mod db;
|
||||||
|
|
||||||
|
pub use db::StateDb;
|
||||||
29
crates/nu-state/src/nft.rs
Normal file
29
crates/nu-state/src/nft.rs
Normal 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
|
||||||
|
}
|
||||||
47
crates/nu-state/src/story_node.rs
Normal file
47
crates/nu-state/src/story_node.rs
Normal 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
12
crates/nu-vm/Cargo.toml
Normal 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" }
|
||||||
23
crates/nu-vm/src/errors.rs
Normal file
23
crates/nu-vm/src/errors.rs
Normal 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),
|
||||||
|
}
|
||||||
43
crates/nu-vm/src/executor.rs
Normal file
43
crates/nu-vm/src/executor.rs
Normal 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
4
crates/nu-vm/src/lib.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod executor;
|
||||||
|
pub mod rewards;
|
||||||
|
pub mod slashing;
|
||||||
|
pub mod errors;
|
||||||
27
crates/nu-vm/src/rewards.rs
Normal file
27
crates/nu-vm/src/rewards.rs
Normal 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%
|
||||||
|
}
|
||||||
25
crates/nu-vm/src/slashing.rs
Normal file
25
crates/nu-vm/src/slashing.rs
Normal 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
17
src/main.rs
Normal 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(())
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue