nu-node/crates/nu-vm/src/executor.rs

363 lines
12 KiB
Rust

use crate::errors::VmError;
use crate::rewards::{community_split, genesis_split, SHELL_PER_NUT, NODE_REWARD_NUT};
use nu_state::{
nft::NftState,
story_node::{NodeStatus, StoryNodeState, WeightedVote},
validator::ValidatorState,
StateAccessor,
};
pub struct ExecutionContext<'a> {
pub state: &'a dyn StateAccessor,
pub block_height: u64,
pub now_ms: i64,
}
// entry_fee = 25% of reward = 25 NUT = 25_000 Shell
const ENTRY_FEE_SHELL: u64 = (NODE_REWARD_NUT / 4) * SHELL_PER_NUT;
// vote_stake_lock = 10% of reward = 10 NUT = 10_000 Shell
const VOTE_LOCK_SHELL: u64 = (NODE_REWARD_NUT / 10) * SHELL_PER_NUT;
pub fn execute_token_transfer(
ctx: &ExecutionContext,
sender: &str,
to: &str,
amount: u64,
fee: u64,
nonce: u64,
) -> Result<(), VmError> {
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 });
}
let recipient_balance = ctx.state.get_balance(to);
ctx.state.set_balance(sender, balance - total);
ctx.state.set_balance(to, recipient_balance + amount);
ctx.state.inc_nonce(sender);
Ok(())
}
/// Validates entry fee, creates a Pending StoryNodeState.
/// Canonical node_id is assigned at approval — temp_id used until then.
pub fn execute_node_submit(
ctx: &ExecutionContext,
sender: &str,
nonce: u64,
fee: u64,
story_id: &str,
parent_node_id: &str,
content_hash: &str,
temp_id: &str,
) -> Result<(), VmError> {
let expected_nonce = ctx.state.get_nonce(sender);
if nonce != expected_nonce {
return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce });
}
let total = ENTRY_FEE_SHELL.checked_add(fee).ok_or(VmError::Unknown("overflow".into()))?;
let balance = ctx.state.get_balance(sender);
if balance < total {
return Err(VmError::InsufficientBalance { need: total, have: balance });
}
// Deduct entry fee + tx fee
ctx.state.set_balance(sender, balance - total);
ctx.state.inc_nonce(sender);
let node = StoryNodeState {
node_id: String::new(), // assigned at approval
temp_id: temp_id.to_string(),
story_id: story_id.to_string(),
parent_id: parent_node_id.to_string(),
author: sender.to_string(),
content_hash: content_hash.to_string(),
status: NodeStatus::Pending,
submitted_at: ctx.now_ms,
vote_open_at: 0,
vote_end_at: 0,
votes: Vec::new(),
nft_id: String::new(),
entry_fee: ENTRY_FEE_SHELL,
};
ctx.state.set_node(&node);
Ok(())
}
/// Locks stake so the voter can cast a vote on the given node.
/// Stake is released when the voting period ends (approval or rejection).
pub fn execute_vote_register(
ctx: &ExecutionContext,
sender: &str,
nonce: u64,
fee: u64,
node_temp_id: &str,
) -> Result<(), VmError> {
let expected_nonce = ctx.state.get_nonce(sender);
if nonce != expected_nonce {
return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce });
}
let node = ctx.state.get_node_by_temp(node_temp_id)
.ok_or_else(|| VmError::NodeNotFound(node_temp_id.to_string()))?;
if node.status != NodeStatus::VotingOpen {
return Err(VmError::NodeNotVoting);
}
// Check not already registered
if node.votes.iter().any(|v| v.voter == sender) {
return Err(VmError::AlreadyVoted);
}
let total = VOTE_LOCK_SHELL.checked_add(fee).ok_or(VmError::Unknown("overflow".into()))?;
let balance = ctx.state.get_balance(sender);
if balance < total {
return Err(VmError::InsufficientBalance { need: total, have: balance });
}
ctx.state.set_balance(sender, balance - fee);
ctx.state.lock_stake(sender, VOTE_LOCK_SHELL, node.vote_end_at);
ctx.state.inc_nonce(sender);
Ok(())
}
/// Records the voter's choice. Weight computed from staked + PoN score.
/// Validates voter was registered (has a pending vote entry).
pub fn execute_vote_cast(
ctx: &ExecutionContext,
sender: &str,
nonce: u64,
fee: u64,
node_temp_id: &str,
approve: bool,
) -> Result<(), VmError> {
let expected_nonce = ctx.state.get_nonce(sender);
if nonce != expected_nonce {
return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce });
}
let node = ctx.state.get_node_by_temp(node_temp_id)
.ok_or_else(|| VmError::NodeNotFound(node_temp_id.to_string()))?;
if node.status != NodeStatus::VotingOpen {
return Err(VmError::NodeNotVoting);
}
// Voter must have registered (stake locked) — check no existing approve/reject cast
let already_cast = node.votes.iter().any(|v| v.voter == sender && v.weight > 0.0);
if already_cast {
return Err(VmError::AlreadyVoted);
}
let balance = ctx.state.get_balance(sender);
if balance < fee {
return Err(VmError::InsufficientBalance { need: fee, have: balance });
}
let account = ctx.state.get_account(sender);
let weight = (account.staked as f64).sqrt() * account.pon_score;
ctx.state.set_balance(sender, balance - fee);
ctx.state.inc_nonce(sender);
ctx.state.record_vote(node_temp_id, WeightedVote { voter: sender.to_string(), approve, weight });
Ok(())
}
/// Stake or unstake NUT tokens.
pub fn execute_stake_op(
ctx: &ExecutionContext,
sender: &str,
nonce: u64,
fee: u64,
stake: bool,
amount: u64,
) -> Result<(), VmError> {
let expected_nonce = ctx.state.get_nonce(sender);
if nonce != expected_nonce {
return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce });
}
let mut account = ctx.state.get_account(sender);
let total_fee_deduct = fee;
if stake {
let total = amount.checked_add(total_fee_deduct).ok_or(VmError::Unknown("overflow".into()))?;
if account.balance < total {
return Err(VmError::InsufficientBalance { need: total, have: account.balance });
}
account.balance -= total;
account.staked += amount;
} else {
if account.balance < total_fee_deduct {
return Err(VmError::InsufficientBalance { need: total_fee_deduct, have: account.balance });
}
if account.staked < amount {
return Err(VmError::InsufficientBalance { need: amount, have: account.staked });
}
account.balance -= total_fee_deduct;
account.staked -= amount;
account.balance += amount;
}
account.nonce += 1;
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(())
}