diff --git a/CHANGELOG.md b/CHANGELOG.md index d1a413b..f5ec060 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) ## [Unreleased] +## [0.5.0] — 2026-04-24 + +### Added +- `nu-consensus/src/slashing.rs` — `slash_double_sign` (%10 stake, PoN→0.5, 30 slot ban), `slash_invalid_block` (%5 stake, PoN→0.7), `record_skip`/`record_block`, `try_unban` +- `nu-consensus/src/types.rs` — `ValidatorRecord`'a `skip_count`, `ban_until_slot` alanları; `MIN_VALIDATOR_STAKE` Shell cinsine güncellendi (1_000_000) +- `nu-state/src/validator.rs` — `ValidatorState` struct (on-chain validator kayıt) +- `nu-state/src/db.rs` — `scan_prefix` — prefix ile tüm kayıtları iterate eder +- `nu-state/src/accessor.rs` — `get_nft/set_nft`, `get_validator/set_validator` trait metodları +- `nu-vm/executor.rs` — `execute_validator_register`, `execute_voting_open`, `execute_node_approve` (NFT mint + ödül dağıtımı), `execute_node_reject` (entry fee yakım + stake unlock) +- `block_loop.rs` — her slot'ta `generate_scheduler_txs` çalışır; Pending→VotingOpen ve VotingOpen→Approved/Rejected auto-tx'leri mempool'a eklenir +- `DEV_WALLET` env variable desteği — hardcode kaldırıldı + ## [0.4.0] — 2026-04-24 ### Added diff --git a/Cargo.lock b/Cargo.lock index f69eeed..80fc1b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -860,6 +860,7 @@ version = "0.1.0" dependencies = [ "anyhow", "nu-block", + "nu-consensus", "nu-state", "serde", "serde_json", diff --git a/crates/nu-state/src/accessor.rs b/crates/nu-state/src/accessor.rs index b6a1ba0..7b23395 100644 --- a/crates/nu-state/src/accessor.rs +++ b/crates/nu-state/src/accessor.rs @@ -1,7 +1,9 @@ use crate::{ account::AccountState, db::StateDb, + nft::NftState, story_node::{StoryNodeState, WeightedVote}, + validator::ValidatorState, }; pub trait StateAccessor { @@ -31,7 +33,15 @@ pub trait StateAccessor { fn get_node(&self, node_id: &str) -> Option; fn set_node(&self, node: &StoryNodeState); - // --- Helpers used by multiple executors --- + // --- NFT --- + fn get_nft(&self, nft_id: &str) -> Option; + fn set_nft(&self, nft: &NftState); + + // --- Validator --- + fn get_validator(&self, address: &str) -> Option; + fn set_validator(&self, v: &ValidatorState); + + // --- Composite helpers --- fn lock_stake(&self, address: &str, amount: u64, until_ms: i64) { let mut a = self.get_account(address); a.locked = a.locked.saturating_add(amount); @@ -63,35 +73,44 @@ pub trait StateAccessor { impl StateAccessor for StateDb { fn get_account(&self, address: &str) -> AccountState { - let key = format!("account:{address}"); - self.get::(&key) + self.get::(&format!("account:{address}")) .ok() .flatten() .unwrap_or_else(|| AccountState::new(address.to_string())) } fn set_account(&self, account: &AccountState) { - let key = format!("account:{}", account.address); - let _ = self.put(&key, account); + let _ = self.put(&format!("account:{}", account.address), account); } fn get_node_by_temp(&self, temp_id: &str) -> Option { - let key = format!("node_temp:{temp_id}"); - self.get::(&key).ok().flatten() + self.get::(&format!("node_temp:{temp_id}")).ok().flatten() } fn get_node(&self, node_id: &str) -> Option { - let key = format!("node:{node_id}"); - self.get::(&key).ok().flatten() + self.get::(&format!("node:{node_id}")).ok().flatten() } fn set_node(&self, node: &StoryNodeState) { - // Index by temp_id (always set) and canonical node_id (set after approval) - let temp_key = format!("node_temp:{}", node.temp_id); - let _ = self.put(&temp_key, node); + let _ = self.put(&format!("node_temp:{}", node.temp_id), node); if !node.node_id.is_empty() { - let canon_key = format!("node:{}", node.node_id); - let _ = self.put(&canon_key, node); + let _ = self.put(&format!("node:{}", node.node_id), node); } } + + fn get_nft(&self, nft_id: &str) -> Option { + self.get::(&format!("nft:{nft_id}")).ok().flatten() + } + + fn set_nft(&self, nft: &NftState) { + let _ = self.put(&format!("nft:{}", nft.nft_id), nft); + } + + fn get_validator(&self, address: &str) -> Option { + self.get::(&format!("validator:{address}")).ok().flatten() + } + + fn set_validator(&self, v: &ValidatorState) { + let _ = self.put(&format!("validator:{}", v.address), v); + } } diff --git a/crates/nu-state/src/lib.rs b/crates/nu-state/src/lib.rs index 661f21b..504077e 100644 --- a/crates/nu-state/src/lib.rs +++ b/crates/nu-state/src/lib.rs @@ -1,9 +1,12 @@ pub mod accessor; pub mod account; -pub mod story_node; -pub mod nft; pub mod db; +pub mod nft; +pub mod story_node; +pub mod validator; pub use accessor::StateAccessor; pub use db::StateDb; +pub use nft::NftState; pub use story_node::{NodeStatus, StoryNodeState, WeightedVote}; +pub use validator::ValidatorState; diff --git a/crates/nu-state/src/validator.rs b/crates/nu-state/src/validator.rs new file mode 100644 index 0000000..6513360 --- /dev/null +++ b/crates/nu-state/src/validator.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValidatorState { + pub address: String, + pub stake: u64, + pub pon_score: f64, + pub is_active: bool, + pub last_block: u64, + pub slash_count: u32, + pub skip_count: u32, + pub consecutive_blocks: u32, + pub ban_until_slot: u32, +} + +impl ValidatorState { + pub fn new(address: String, stake: u64) -> Self { + Self { + address, + stake, + pon_score: 1.0, + is_active: true, + last_block: 0, + slash_count: 0, + skip_count: 0, + consecutive_blocks: 0, + ban_until_slot: 0, + } + } +} diff --git a/crates/nu-vm/Cargo.toml b/crates/nu-vm/Cargo.toml index 6670f3d..e1b11c5 100644 --- a/crates/nu-vm/Cargo.toml +++ b/crates/nu-vm/Cargo.toml @@ -9,5 +9,6 @@ serde_json.workspace = true anyhow.workspace = true thiserror.workspace = true tracing.workspace = true -nu-state = { path = "../nu-state" } -nu-block = { path = "../nu-block" } +nu-state = { path = "../nu-state" } +nu-block = { path = "../nu-block" } +nu-consensus = { path = "../nu-consensus" } diff --git a/crates/nu-vm/src/engine.rs b/crates/nu-vm/src/engine.rs index e730995..f5a27e9 100644 --- a/crates/nu-vm/src/engine.rs +++ b/crates/nu-vm/src/engine.rs @@ -2,8 +2,10 @@ use nu_block::types::{Block, RawTransaction, TxPayload, TxReceipt}; use nu_state::StateAccessor; use crate::executor::{ - execute_node_submit, execute_stake_op, execute_token_transfer, execute_vote_cast, - execute_vote_register, ExecutionContext, + execute_node_approve, execute_node_reject, execute_node_submit, + execute_stake_op, execute_token_transfer, execute_validator_register, + execute_vote_cast, execute_vote_register, execute_voting_open, + ExecutionContext, }; pub struct BlockResult { @@ -13,12 +15,15 @@ pub struct BlockResult { } pub fn execute_block(block: &Block, state: &dyn StateAccessor, now_ms: i64) -> BlockResult { + let dev_wallet = std::env::var("DEV_WALLET") + .unwrap_or_else(|_| "0xDEV0000000000000000000000000000000000000".to_string()); + let mut receipts = Vec::with_capacity(block.transactions.len()); - let mut applied = 0u32; - let mut failed = 0u32; + let mut applied = 0u32; + let mut failed = 0u32; for tx in &block.transactions { - let receipt = execute_tx(tx, state, block.header.height, now_ms); + let receipt = execute_tx(tx, state, block.header.height, now_ms, &dev_wallet); if receipt.success { applied += 1; } else { failed += 1; } receipts.push(receipt); } @@ -31,6 +36,7 @@ fn execute_tx( state: &dyn StateAccessor, block_height: u64, now_ms: i64, + dev_wallet: &str, ) -> TxReceipt { let ctx = ExecutionContext { state, block_height, now_ms }; @@ -39,10 +45,7 @@ fn execute_tx( execute_token_transfer(&ctx, &tx.sender, to, *amount, tx.fee, tx.nonce) } TxPayload::NodeSubmit { story_id, parent_node_id, content_hash, temp_id, entry_fee: _ } => { - execute_node_submit( - &ctx, &tx.sender, tx.nonce, tx.fee, - story_id, parent_node_id, content_hash, temp_id, - ) + execute_node_submit(&ctx, &tx.sender, tx.nonce, tx.fee, story_id, parent_node_id, content_hash, temp_id) } TxPayload::VoteRegister { node_id, stake_lock: _ } => { execute_vote_register(&ctx, &tx.sender, tx.nonce, tx.fee, node_id) @@ -53,16 +56,23 @@ fn execute_tx( TxPayload::StakeOp { stake, amount } => { execute_stake_op(&ctx, &tx.sender, tx.nonce, tx.fee, *stake, *amount) } - // Faz 2: consensus-driven auto txs - TxPayload::NodeApprove { .. } - | TxPayload::NftMint { .. } - | TxPayload::NodeReject { .. } - | TxPayload::VotingOpen { .. } - | TxPayload::ValidatorRegister { .. } + TxPayload::ValidatorRegister { stake } => { + execute_validator_register(&ctx, &tx.sender, tx.nonce, tx.fee, *stake) + } + TxPayload::VotingOpen { node_id } => { + execute_voting_open(&ctx, node_id) + } + TxPayload::NodeApprove { temp_id, canonical_id } => { + execute_node_approve(&ctx, temp_id, canonical_id, dev_wallet) + } + TxPayload::NodeReject { node_id } => { + execute_node_reject(&ctx, node_id) + } + // Faz 2 later + TxPayload::NftMint { .. } | TxPayload::NftTransfer { .. } | TxPayload::CollectionClaim { .. } => { - let name = payload_name(&tx.payload); - Err(crate::errors::VmError::Unknown(format!("{name} not yet implemented"))) + Err(crate::errors::VmError::Unknown(format!("{} not yet implemented", payload_name(&tx.payload)))) } }; diff --git a/crates/nu-vm/src/executor.rs b/crates/nu-vm/src/executor.rs index ba9af9d..207372b 100644 --- a/crates/nu-vm/src/executor.rs +++ b/crates/nu-vm/src/executor.rs @@ -1,8 +1,10 @@ use crate::errors::VmError; -use crate::rewards::{SHELL_PER_NUT, NODE_REWARD_NUT}; +use crate::rewards::{community_split, genesis_split, SHELL_PER_NUT, NODE_REWARD_NUT}; use nu_state::{ - StateAccessor, + nft::NftState, story_node::{NodeStatus, StoryNodeState, WeightedVote}, + validator::ValidatorState, + StateAccessor, }; pub struct ExecutionContext<'a> { @@ -207,3 +209,155 @@ pub fn execute_stake_op( ctx.state.set_account(&account); Ok(()) } + +/// Register as a validator — locks stake, creates ValidatorState. +pub fn execute_validator_register( + ctx: &ExecutionContext, + sender: &str, + nonce: u64, + fee: u64, + stake: u64, +) -> Result<(), VmError> { + use nu_consensus::types::MIN_VALIDATOR_STAKE; + + let expected_nonce = ctx.state.get_nonce(sender); + if nonce != expected_nonce { + return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce }); + } + if stake < MIN_VALIDATOR_STAKE { + return Err(VmError::StakeTooLow); + } + + let mut account = ctx.state.get_account(sender); + let total = stake.checked_add(fee).ok_or(VmError::Unknown("overflow".into()))?; + if account.balance < total { + return Err(VmError::InsufficientBalance { need: total, have: account.balance }); + } + + account.balance -= total; + account.staked += stake; + account.nonce += 1; + ctx.state.set_account(&account); + + let record = ValidatorState::new(sender.to_string(), stake); + ctx.state.set_validator(&record); + Ok(()) +} + +const BURN_WALLET: &str = "0x000000000000000000000000000000000000DEAD"; +const GENESIS_STORY: &str = "genesis"; + +/// Auto-tx: transition node from Pending → VotingOpen. +pub fn execute_voting_open( + ctx: &ExecutionContext, + node_id: &str, +) -> Result<(), VmError> { + let mut node = ctx.state.get_node_by_temp(node_id) + .ok_or_else(|| VmError::NodeNotFound(node_id.to_string()))?; + + if node.status != NodeStatus::Pending { + return Err(VmError::Unknown("node not in Pending state".into())); + } + + let voting_period_ms: i64 = 3 * 24 * 60 * 60 * 1_000; + node.status = NodeStatus::VotingOpen; + node.vote_open_at = ctx.now_ms; + node.vote_end_at = ctx.now_ms + voting_period_ms; + ctx.state.set_node(&node); + Ok(()) +} + +/// Auto-tx: approve node, mint NFT, distribute rewards. +pub fn execute_node_approve( + ctx: &ExecutionContext, + temp_id: &str, + canonical_id: &str, + dev_wallet: &str, +) -> Result<(), VmError> { + let mut node = ctx.state.get_node_by_temp(temp_id) + .ok_or_else(|| VmError::NodeNotFound(temp_id.to_string()))?; + + if node.status != NodeStatus::VotingOpen { + return Err(VmError::NodeNotVoting); + } + + node.status = NodeStatus::Approved; + node.node_id = canonical_id.to_string(); + node.nft_id = canonical_id.to_string(); + ctx.state.set_node(&node); + + // Mint NFT + let depth = canonical_id.len() as u32; + let nft = NftState { + nft_id: canonical_id.to_string(), + node_id: canonical_id.to_string(), + owner: node.author.clone(), + collection_id: String::new(), + depth, + lineage: vec![], + minted_at: ctx.now_ms, + }; + ctx.state.set_nft(&nft); + ctx.state.add_nft(&node.author, canonical_id); + + // Unlock voter stakes + update PoN scores + let winning_approve = node.is_approved(); + let total_weight: f64 = node.votes.iter().map(|v| v.weight).sum(); + let mut voter_reward_pool = 0u64; + + if node.story_id == GENESIS_STORY { + let reward = genesis_split(); + let dw_bal = ctx.state.get_balance(dev_wallet); + ctx.state.set_balance(dev_wallet, dw_bal + reward); + } else { + let split = community_split(); + voter_reward_pool = split.voters; + + // Author reward + let author_bal = ctx.state.get_balance(&node.author); + ctx.state.set_balance(&node.author, author_bal + split.author); + + // Burn + let burn_bal = ctx.state.get_balance(BURN_WALLET); + ctx.state.set_balance(BURN_WALLET, burn_bal + split.burn); + } + + // Distribute voter rewards + unlock stakes + for vote in &node.votes { + ctx.state.unlock_stake(&vote.voter, VOTE_LOCK_SHELL); + if node.story_id != GENESIS_STORY && vote.approve == winning_approve && total_weight > 0.0 { + let share = (vote.weight / total_weight * voter_reward_pool as f64) as u64; + let bal = ctx.state.get_balance(&vote.voter); + ctx.state.set_balance(&vote.voter, bal + share); + } + } + + Ok(()) +} + +/// Auto-tx: reject node, burn entry fee, unlock stakes. +pub fn execute_node_reject( + ctx: &ExecutionContext, + node_id: &str, +) -> Result<(), VmError> { + let mut node = ctx.state.get_node_by_temp(node_id) + .ok_or_else(|| VmError::NodeNotFound(node_id.to_string()))?; + + if node.status != NodeStatus::VotingOpen { + return Err(VmError::NodeNotVoting); + } + + node.status = NodeStatus::Rejected; + ctx.state.set_node(&node); + + // Burn entry fee + let burn_bal = ctx.state.get_balance(BURN_WALLET); + ctx.state.set_balance(BURN_WALLET, burn_bal + node.entry_fee); + + // Unlock all voter stakes + for vote in &node.votes { + ctx.state.unlock_stake(&vote.voter, VOTE_LOCK_SHELL); + } + + Ok(()) +}