feat(consensus): slashing, ValidatorState, scheduler auto-tx, NodeApprove/Reject execution
This commit is contained in:
parent
ef3d18ef56
commit
015e521ae2
8 changed files with 267 additions and 37 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -7,6 +7,18 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
||||||
|
|
||||||
## [Unreleased]
|
## [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<T>` — 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
|
## [0.4.0] — 2026-04-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -860,6 +860,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"nu-block",
|
"nu-block",
|
||||||
|
"nu-consensus",
|
||||||
"nu-state",
|
"nu-state",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
account::AccountState,
|
account::AccountState,
|
||||||
db::StateDb,
|
db::StateDb,
|
||||||
|
nft::NftState,
|
||||||
story_node::{StoryNodeState, WeightedVote},
|
story_node::{StoryNodeState, WeightedVote},
|
||||||
|
validator::ValidatorState,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait StateAccessor {
|
pub trait StateAccessor {
|
||||||
|
|
@ -31,7 +33,15 @@ pub trait StateAccessor {
|
||||||
fn get_node(&self, node_id: &str) -> Option<StoryNodeState>;
|
fn get_node(&self, node_id: &str) -> Option<StoryNodeState>;
|
||||||
fn set_node(&self, node: &StoryNodeState);
|
fn set_node(&self, node: &StoryNodeState);
|
||||||
|
|
||||||
// --- Helpers used by multiple executors ---
|
// --- NFT ---
|
||||||
|
fn get_nft(&self, nft_id: &str) -> Option<NftState>;
|
||||||
|
fn set_nft(&self, nft: &NftState);
|
||||||
|
|
||||||
|
// --- Validator ---
|
||||||
|
fn get_validator(&self, address: &str) -> Option<ValidatorState>;
|
||||||
|
fn set_validator(&self, v: &ValidatorState);
|
||||||
|
|
||||||
|
// --- Composite helpers ---
|
||||||
fn lock_stake(&self, address: &str, amount: u64, until_ms: i64) {
|
fn lock_stake(&self, address: &str, amount: u64, until_ms: i64) {
|
||||||
let mut a = self.get_account(address);
|
let mut a = self.get_account(address);
|
||||||
a.locked = a.locked.saturating_add(amount);
|
a.locked = a.locked.saturating_add(amount);
|
||||||
|
|
@ -63,35 +73,44 @@ pub trait StateAccessor {
|
||||||
|
|
||||||
impl StateAccessor for StateDb {
|
impl StateAccessor for StateDb {
|
||||||
fn get_account(&self, address: &str) -> AccountState {
|
fn get_account(&self, address: &str) -> AccountState {
|
||||||
let key = format!("account:{address}");
|
self.get::<AccountState>(&format!("account:{address}"))
|
||||||
self.get::<AccountState>(&key)
|
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.unwrap_or_else(|| AccountState::new(address.to_string()))
|
.unwrap_or_else(|| AccountState::new(address.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_account(&self, account: &AccountState) {
|
fn set_account(&self, account: &AccountState) {
|
||||||
let key = format!("account:{}", account.address);
|
let _ = self.put(&format!("account:{}", account.address), account);
|
||||||
let _ = self.put(&key, account);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_node_by_temp(&self, temp_id: &str) -> Option<StoryNodeState> {
|
fn get_node_by_temp(&self, temp_id: &str) -> Option<StoryNodeState> {
|
||||||
let key = format!("node_temp:{temp_id}");
|
self.get::<StoryNodeState>(&format!("node_temp:{temp_id}")).ok().flatten()
|
||||||
self.get::<StoryNodeState>(&key).ok().flatten()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_node(&self, node_id: &str) -> Option<StoryNodeState> {
|
fn get_node(&self, node_id: &str) -> Option<StoryNodeState> {
|
||||||
let key = format!("node:{node_id}");
|
self.get::<StoryNodeState>(&format!("node:{node_id}")).ok().flatten()
|
||||||
self.get::<StoryNodeState>(&key).ok().flatten()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_node(&self, node: &StoryNodeState) {
|
fn set_node(&self, node: &StoryNodeState) {
|
||||||
// Index by temp_id (always set) and canonical node_id (set after approval)
|
let _ = self.put(&format!("node_temp:{}", node.temp_id), node);
|
||||||
let temp_key = format!("node_temp:{}", node.temp_id);
|
|
||||||
let _ = self.put(&temp_key, node);
|
|
||||||
if !node.node_id.is_empty() {
|
if !node.node_id.is_empty() {
|
||||||
let canon_key = format!("node:{}", node.node_id);
|
let _ = self.put(&format!("node:{}", node.node_id), node);
|
||||||
let _ = self.put(&canon_key, node);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_nft(&self, nft_id: &str) -> Option<NftState> {
|
||||||
|
self.get::<NftState>(&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<ValidatorState> {
|
||||||
|
self.get::<ValidatorState>(&format!("validator:{address}")).ok().flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_validator(&self, v: &ValidatorState) {
|
||||||
|
let _ = self.put(&format!("validator:{}", v.address), v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
pub mod accessor;
|
pub mod accessor;
|
||||||
pub mod account;
|
pub mod account;
|
||||||
pub mod story_node;
|
|
||||||
pub mod nft;
|
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
pub mod nft;
|
||||||
|
pub mod story_node;
|
||||||
|
pub mod validator;
|
||||||
|
|
||||||
pub use accessor::StateAccessor;
|
pub use accessor::StateAccessor;
|
||||||
pub use db::StateDb;
|
pub use db::StateDb;
|
||||||
|
pub use nft::NftState;
|
||||||
pub use story_node::{NodeStatus, StoryNodeState, WeightedVote};
|
pub use story_node::{NodeStatus, StoryNodeState, WeightedVote};
|
||||||
|
pub use validator::ValidatorState;
|
||||||
|
|
|
||||||
30
crates/nu-state/src/validator.rs
Normal file
30
crates/nu-state/src/validator.rs
Normal file
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,5 +9,6 @@ serde_json.workspace = true
|
||||||
anyhow.workspace = true
|
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" }
|
nu-block = { path = "../nu-block" }
|
||||||
|
nu-consensus = { path = "../nu-consensus" }
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ use nu_block::types::{Block, RawTransaction, TxPayload, TxReceipt};
|
||||||
use nu_state::StateAccessor;
|
use nu_state::StateAccessor;
|
||||||
|
|
||||||
use crate::executor::{
|
use crate::executor::{
|
||||||
execute_node_submit, execute_stake_op, execute_token_transfer, execute_vote_cast,
|
execute_node_approve, execute_node_reject, execute_node_submit,
|
||||||
execute_vote_register, ExecutionContext,
|
execute_stake_op, execute_token_transfer, execute_validator_register,
|
||||||
|
execute_vote_cast, execute_vote_register, execute_voting_open,
|
||||||
|
ExecutionContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct BlockResult {
|
pub struct BlockResult {
|
||||||
|
|
@ -13,12 +15,15 @@ pub struct BlockResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_block(block: &Block, state: &dyn StateAccessor, now_ms: i64) -> 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 receipts = Vec::with_capacity(block.transactions.len());
|
||||||
let mut applied = 0u32;
|
let mut applied = 0u32;
|
||||||
let mut failed = 0u32;
|
let mut failed = 0u32;
|
||||||
|
|
||||||
for tx in &block.transactions {
|
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; }
|
if receipt.success { applied += 1; } else { failed += 1; }
|
||||||
receipts.push(receipt);
|
receipts.push(receipt);
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +36,7 @@ fn execute_tx(
|
||||||
state: &dyn StateAccessor,
|
state: &dyn StateAccessor,
|
||||||
block_height: u64,
|
block_height: u64,
|
||||||
now_ms: i64,
|
now_ms: i64,
|
||||||
|
dev_wallet: &str,
|
||||||
) -> TxReceipt {
|
) -> TxReceipt {
|
||||||
let ctx = ExecutionContext { state, block_height, now_ms };
|
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)
|
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: _ } => {
|
TxPayload::NodeSubmit { story_id, parent_node_id, content_hash, temp_id, entry_fee: _ } => {
|
||||||
execute_node_submit(
|
execute_node_submit(&ctx, &tx.sender, tx.nonce, tx.fee, story_id, parent_node_id, content_hash, temp_id)
|
||||||
&ctx, &tx.sender, tx.nonce, tx.fee,
|
|
||||||
story_id, parent_node_id, content_hash, temp_id,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
TxPayload::VoteRegister { node_id, stake_lock: _ } => {
|
TxPayload::VoteRegister { node_id, stake_lock: _ } => {
|
||||||
execute_vote_register(&ctx, &tx.sender, tx.nonce, tx.fee, node_id)
|
execute_vote_register(&ctx, &tx.sender, tx.nonce, tx.fee, node_id)
|
||||||
|
|
@ -53,16 +56,23 @@ fn execute_tx(
|
||||||
TxPayload::StakeOp { stake, amount } => {
|
TxPayload::StakeOp { stake, amount } => {
|
||||||
execute_stake_op(&ctx, &tx.sender, tx.nonce, tx.fee, *stake, *amount)
|
execute_stake_op(&ctx, &tx.sender, tx.nonce, tx.fee, *stake, *amount)
|
||||||
}
|
}
|
||||||
// Faz 2: consensus-driven auto txs
|
TxPayload::ValidatorRegister { stake } => {
|
||||||
TxPayload::NodeApprove { .. }
|
execute_validator_register(&ctx, &tx.sender, tx.nonce, tx.fee, *stake)
|
||||||
| TxPayload::NftMint { .. }
|
}
|
||||||
| TxPayload::NodeReject { .. }
|
TxPayload::VotingOpen { node_id } => {
|
||||||
| TxPayload::VotingOpen { .. }
|
execute_voting_open(&ctx, node_id)
|
||||||
| TxPayload::ValidatorRegister { .. }
|
}
|
||||||
|
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::NftTransfer { .. }
|
||||||
| TxPayload::CollectionClaim { .. } => {
|
| TxPayload::CollectionClaim { .. } => {
|
||||||
let name = payload_name(&tx.payload);
|
Err(crate::errors::VmError::Unknown(format!("{} not yet implemented", payload_name(&tx.payload))))
|
||||||
Err(crate::errors::VmError::Unknown(format!("{name} not yet implemented")))
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
use crate::errors::VmError;
|
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::{
|
use nu_state::{
|
||||||
StateAccessor,
|
nft::NftState,
|
||||||
story_node::{NodeStatus, StoryNodeState, WeightedVote},
|
story_node::{NodeStatus, StoryNodeState, WeightedVote},
|
||||||
|
validator::ValidatorState,
|
||||||
|
StateAccessor,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ExecutionContext<'a> {
|
pub struct ExecutionContext<'a> {
|
||||||
|
|
@ -207,3 +209,155 @@ pub fn execute_stake_op(
|
||||||
ctx.state.set_account(&account);
|
ctx.state.set_account(&account);
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue