Compare commits
5 commits
015e521ae2
...
c22c0e0278
| Author | SHA256 | Date | |
|---|---|---|---|
| c22c0e0278 | |||
| ec355b40d2 | |||
| 3711bc450c | |||
| 3eb361df36 | |||
| 142264191c |
7 changed files with 268 additions and 28 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -7,6 +7,22 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.7.0] — 2026-04-24
|
||||
|
||||
### Added
|
||||
- `nu-vm/executor.rs` — `execute_nft_transfer`: sahiplik + koleksiyon kontrolü, owner güncelleme, hesap nft_ids sync
|
||||
- `nu-vm/executor.rs` — `execute_collection_claim`: `validate_lineage()` ile geçerli prefix-extension yolu doğrulama; tüm NFT'lere `collection_id` atar
|
||||
- `nu-vm/engine.rs` — `NftTransfer` ve `CollectionClaim` varyantları bağlandı; `NftMint` kullanıcı tx'i değil (hata döner)
|
||||
|
||||
## [0.6.0] — 2026-04-24
|
||||
|
||||
### Added
|
||||
- `block_loop.rs` — `BlockLoopConfig.dev_mode`; rotation check: non-dev modda `slot_producer()` çağrılır, sıra başka validatörde ise slot atlanır
|
||||
- `block_loop.rs` — `load_validator_set(db)`: her slot başında `"validator:"` prefix'i taranarak canlı `ValidatorSet` oluşturulur
|
||||
- `block_loop.rs` — `update_validator_pon(db, address)`: dürüst blok sonrası PoN skoru ve `consecutive_blocks` güncellenir
|
||||
- `nu-consensus/src/validator_set.rs` — `slot_producer()`, `update()`, `get()`, `active_count()`, `Default` impl; `schedule()` filtresine `is_banned()` eklendi
|
||||
- `nu-rpc/src/handlers.rs` — `nu_getValidator`: verilen adres için `ValidatorState` döner, kayıt yoksa null
|
||||
|
||||
## [0.5.0] — 2026-04-24
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -11,8 +11,32 @@ impl ValidatorSet {
|
|||
}
|
||||
|
||||
pub fn register(&mut self, record: ValidatorRecord) {
|
||||
if !self.validators.iter().any(|v| v.address == record.address) {
|
||||
self.validators.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, address: &str, f: impl FnOnce(&mut ValidatorRecord)) {
|
||||
if let Some(v) = self.validators.iter_mut().find(|v| v.address == address) {
|
||||
f(v);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, address: &str) -> Option<&ValidatorRecord> {
|
||||
self.validators.iter().find(|v| v.address == address)
|
||||
}
|
||||
|
||||
pub fn active_count(&self) -> usize {
|
||||
self.validators.iter().filter(|v| v.is_active).count()
|
||||
}
|
||||
|
||||
/// Returns the expected block producer for the given slot.
|
||||
/// In single-validator dev mode this always returns that validator.
|
||||
pub fn slot_producer(&self, slot: u32, prev_block_hash: &str) -> Option<String> {
|
||||
let schedule = self.schedule(slot, prev_block_hash);
|
||||
let idx = (slot as usize) % schedule.len().max(1);
|
||||
schedule.into_iter().nth(idx)
|
||||
}
|
||||
|
||||
/// Weighted shuffle seeded by slot + prev_block_hash; filters ineligible validators.
|
||||
pub fn schedule(&self, slot: u32, prev_block_hash: &str) -> Vec<String> {
|
||||
|
|
@ -23,9 +47,13 @@ impl ValidatorSet {
|
|||
.iter()
|
||||
.filter(|v| v.is_active && v.stake >= MIN_VALIDATOR_STAKE)
|
||||
.filter(|v| v.consecutive_blocks < MAX_CONSECUTIVE_BLOCKS)
|
||||
.filter(|v| !v.is_banned(slot))
|
||||
.collect();
|
||||
|
||||
// Weighted shuffle: higher pon_score → higher probability of early position
|
||||
if candidates.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
Self::weighted_shuffle(&mut candidates, &seed);
|
||||
|
||||
candidates
|
||||
|
|
@ -43,7 +71,6 @@ impl ValidatorSet {
|
|||
}
|
||||
|
||||
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)
|
||||
|
|
@ -53,3 +80,9 @@ impl ValidatorSet {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ValidatorSet {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::{
|
|||
types::{JsonRpcRequest, JsonRpcResponse},
|
||||
};
|
||||
use nu_block::types::{Block, RawTransaction};
|
||||
use nu_state::account::AccountState;
|
||||
use nu_state::{account::AccountState, ValidatorState};
|
||||
|
||||
pub async fn dispatch(req: JsonRpcRequest, state: &AppState) -> JsonRpcResponse {
|
||||
match req.method.as_str() {
|
||||
|
|
@ -13,6 +13,7 @@ pub async fn dispatch(req: JsonRpcRequest, state: &AppState) -> JsonRpcResponse
|
|||
"nu_getAccount" => handle_get_account(&req, state).await,
|
||||
"nu_sendRawTx" => handle_send_raw_tx(&req, state).await,
|
||||
"nu_getBlock" => handle_get_block(&req, state).await,
|
||||
"nu_getValidator" => handle_get_validator(&req, state).await,
|
||||
"nu_getTx" => not_implemented(&req, "nu_getTx"),
|
||||
"nu_getStory" => not_implemented(&req, "nu_getStory"),
|
||||
"nu_getNode" => not_implemented(&req, "nu_getNode"),
|
||||
|
|
@ -110,6 +111,21 @@ async fn handle_get_block(req: &JsonRpcRequest, state: &AppState) -> JsonRpcResp
|
|||
}
|
||||
}
|
||||
|
||||
async fn handle_get_validator(req: &JsonRpcRequest, state: &AppState) -> JsonRpcResponse {
|
||||
let address = match req.params.get(0).and_then(|v| v.as_str()) {
|
||||
Some(a) => a.to_string(),
|
||||
None => return JsonRpcResponse::err(req.id.clone(), -32602, "Missing address param".into()),
|
||||
};
|
||||
|
||||
let key = format!("validator:{address}");
|
||||
let db = state.db.lock().await;
|
||||
match db.get::<ValidatorState>(&key) {
|
||||
Ok(Some(vs)) => JsonRpcResponse::ok(req.id.clone(), serde_json::to_value(vs).unwrap()),
|
||||
Ok(None) => JsonRpcResponse::ok(req.id.clone(), serde_json::Value::Null),
|
||||
Err(e) => JsonRpcResponse::err(req.id.clone(), -32000, e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn not_implemented(req: &JsonRpcRequest, method: &str) -> JsonRpcResponse {
|
||||
JsonRpcResponse::err(req.id.clone(), -32000, format!("{method} not implemented yet"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ use nu_block::types::{Block, RawTransaction, TxPayload, TxReceipt};
|
|||
use nu_state::StateAccessor;
|
||||
|
||||
use crate::executor::{
|
||||
execute_node_approve, execute_node_reject, execute_node_submit,
|
||||
execute_stake_op, execute_token_transfer, execute_validator_register,
|
||||
execute_collection_claim, execute_node_approve, execute_node_reject, execute_node_submit,
|
||||
execute_nft_transfer, execute_stake_op, execute_token_transfer, execute_validator_register,
|
||||
execute_vote_cast, execute_vote_register, execute_voting_open,
|
||||
ExecutionContext,
|
||||
};
|
||||
|
|
@ -68,11 +68,15 @@ fn execute_tx(
|
|||
TxPayload::NodeReject { node_id } => {
|
||||
execute_node_reject(&ctx, node_id)
|
||||
}
|
||||
// Faz 2 later
|
||||
TxPayload::NftMint { .. }
|
||||
| TxPayload::NftTransfer { .. }
|
||||
| TxPayload::CollectionClaim { .. } => {
|
||||
Err(crate::errors::VmError::Unknown(format!("{} not yet implemented", payload_name(&tx.payload))))
|
||||
TxPayload::NftTransfer { nft_id, to } => {
|
||||
execute_nft_transfer(&ctx, &tx.sender, tx.nonce, tx.fee, nft_id, to)
|
||||
}
|
||||
TxPayload::CollectionClaim { nft_ids } => {
|
||||
execute_collection_claim(&ctx, &tx.sender, tx.nonce, tx.fee, nft_ids)
|
||||
}
|
||||
// Minted automatically via NodeApprove — not user-submitted
|
||||
TxPayload::NftMint { .. } => {
|
||||
Err(crate::errors::VmError::Unknown("NftMint not a user tx".into()))
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -335,6 +335,109 @@ pub fn execute_node_approve(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Transfer an NFT from sender to recipient.
|
||||
pub fn execute_nft_transfer(
|
||||
ctx: &ExecutionContext,
|
||||
sender: &str,
|
||||
nonce: u64,
|
||||
fee: u64,
|
||||
nft_id: &str,
|
||||
to: &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 mut nft = ctx.state.get_nft(nft_id)
|
||||
.ok_or_else(|| VmError::Unknown(format!("NFT {nft_id} not found")))?;
|
||||
|
||||
if nft.owner != sender {
|
||||
return Err(VmError::Unknown(format!("sender does not own NFT {nft_id}")));
|
||||
}
|
||||
if !nft.collection_id.is_empty() {
|
||||
return Err(VmError::Unknown("NFT is part of a collection — cannot transfer".into()));
|
||||
}
|
||||
|
||||
let balance = ctx.state.get_balance(sender);
|
||||
if balance < fee {
|
||||
return Err(VmError::InsufficientBalance { need: fee, have: balance });
|
||||
}
|
||||
|
||||
nft.owner = to.to_string();
|
||||
ctx.state.set_nft(&nft);
|
||||
|
||||
// Update NFT id lists on both accounts
|
||||
let mut sender_acct = ctx.state.get_account(sender);
|
||||
sender_acct.nft_ids.retain(|id| id != nft_id);
|
||||
sender_acct.balance -= fee;
|
||||
sender_acct.nonce += 1;
|
||||
ctx.state.set_account(&sender_acct);
|
||||
|
||||
let mut recipient_acct = ctx.state.get_account(to);
|
||||
recipient_acct.nft_ids.push(nft_id.to_string());
|
||||
ctx.state.set_account(&recipient_acct);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Claim an exclusive collection from a valid lineage of NFTs.
|
||||
/// All NFTs must be owned by sender and form a valid prefix-extension path.
|
||||
pub fn execute_collection_claim(
|
||||
ctx: &ExecutionContext,
|
||||
sender: &str,
|
||||
nonce: u64,
|
||||
fee: u64,
|
||||
nft_ids: &[String],
|
||||
) -> Result<(), VmError> {
|
||||
use nu_state::nft::validate_lineage;
|
||||
|
||||
let expected_nonce = ctx.state.get_nonce(sender);
|
||||
if nonce != expected_nonce {
|
||||
return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce });
|
||||
}
|
||||
if nft_ids.is_empty() {
|
||||
return Err(VmError::Unknown("nft_ids cannot be empty".into()));
|
||||
}
|
||||
if !validate_lineage(nft_ids) {
|
||||
return Err(VmError::Unknown("NFT ids do not form a valid lineage".into()));
|
||||
}
|
||||
|
||||
let balance = ctx.state.get_balance(sender);
|
||||
if balance < fee {
|
||||
return Err(VmError::InsufficientBalance { need: fee, have: balance });
|
||||
}
|
||||
|
||||
// Validate ownership and free status
|
||||
let mut nfts = Vec::with_capacity(nft_ids.len());
|
||||
for id in nft_ids {
|
||||
let nft = ctx.state.get_nft(id)
|
||||
.ok_or_else(|| VmError::Unknown(format!("NFT {id} not found")))?;
|
||||
if nft.owner != sender {
|
||||
return Err(VmError::Unknown(format!("sender does not own NFT {id}")));
|
||||
}
|
||||
if !nft.collection_id.is_empty() {
|
||||
return Err(VmError::Unknown(format!("NFT {id} already in a collection")));
|
||||
}
|
||||
nfts.push(nft);
|
||||
}
|
||||
|
||||
// collection_id = last nft_id in path (the deepest node)
|
||||
let collection_id = nft_ids.last().unwrap().clone();
|
||||
|
||||
for mut nft in nfts {
|
||||
nft.collection_id = collection_id.clone();
|
||||
ctx.state.set_nft(&nft);
|
||||
}
|
||||
|
||||
let mut account = ctx.state.get_account(sender);
|
||||
account.balance -= fee;
|
||||
account.nonce += 1;
|
||||
ctx.state.set_account(&account);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Auto-tx: reject node, burn entry fee, unlock stakes.
|
||||
pub fn execute_node_reject(
|
||||
ctx: &ExecutionContext,
|
||||
|
|
|
|||
|
|
@ -7,9 +7,13 @@ use nu_block::{
|
|||
builder::BlockBuilder,
|
||||
types::{Block, RawTransaction, TxPayload},
|
||||
};
|
||||
use nu_consensus::slot::current_slot;
|
||||
use nu_consensus::{
|
||||
slot::current_slot,
|
||||
types::ValidatorRecord,
|
||||
validator_set::ValidatorSet,
|
||||
};
|
||||
use nu_mempool::Mempool;
|
||||
use nu_state::StateDb;
|
||||
use nu_state::{story_node::StoryNodeState, ValidatorState, StateDb};
|
||||
use nu_vm::execute_block;
|
||||
|
||||
use crate::p2p::P2pSender;
|
||||
|
|
@ -21,6 +25,8 @@ const SCHEDULER_ADDR: &str = "0x0000000000000000000000000000SCHEDULER";
|
|||
pub struct BlockLoopConfig {
|
||||
pub validator_addr: String,
|
||||
pub chain_id: String,
|
||||
/// dev mode: skip rotation check, always produce
|
||||
pub dev_mode: bool,
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
|
|
@ -36,6 +42,7 @@ pub async fn run(
|
|||
tracing::info!(
|
||||
chain_id = %config.chain_id,
|
||||
validator = %config.validator_addr,
|
||||
dev_mode = config.dev_mode,
|
||||
"block loop started"
|
||||
);
|
||||
|
||||
|
|
@ -45,6 +52,28 @@ pub async fn run(
|
|||
let slot = current_slot();
|
||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||
|
||||
// Load validator set from DB
|
||||
let validator_set = {
|
||||
let db_guard = db.lock().await;
|
||||
load_validator_set(&db_guard)
|
||||
};
|
||||
|
||||
// Rotation check: in non-dev mode skip if not our slot
|
||||
if !config.dev_mode {
|
||||
let expected = validator_set.slot_producer(slot, &prev_hash);
|
||||
match &expected {
|
||||
Some(addr) if addr != &config.validator_addr => {
|
||||
tracing::debug!(slot, expected = %addr, "not our slot — skipping");
|
||||
continue;
|
||||
}
|
||||
None => {
|
||||
tracing::warn!(slot, "no eligible validator — skipping");
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Scheduler: inject auto-txs for pending nodes
|
||||
{
|
||||
let db_guard = db.lock().await;
|
||||
|
|
@ -93,9 +122,7 @@ pub async fn run(
|
|||
{
|
||||
let mut pool = mempool.lock().await;
|
||||
for r in &result.receipts {
|
||||
if r.success {
|
||||
pool.remove(&r.tx_id);
|
||||
}
|
||||
if r.success { pool.remove(&r.tx_id); }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +133,8 @@ pub async fn run(
|
|||
if let Err(e) = db_guard.put(&format!("block:{height}"), &block) {
|
||||
tracing::error!(height, "failed to persist block: {e}");
|
||||
}
|
||||
// Update validator PoN: honest block produced
|
||||
update_validator_pon(&db_guard, &config.validator_addr);
|
||||
}
|
||||
|
||||
if let Some(ref sender) = p2p {
|
||||
|
|
@ -119,6 +148,7 @@ pub async fn run(
|
|||
height,
|
||||
applied = result.applied,
|
||||
failed = result.failed,
|
||||
validator = %config.validator_addr,
|
||||
"block produced"
|
||||
);
|
||||
|
||||
|
|
@ -126,13 +156,51 @@ pub async fn run(
|
|||
}
|
||||
}
|
||||
|
||||
/// Scans pending nodes and produces VotingOpen / NodeApprove / NodeReject auto-txs.
|
||||
fn load_validator_set(db: &StateDb) -> ValidatorSet {
|
||||
let mut set = ValidatorSet::new();
|
||||
let records: Vec<ValidatorState> = db.scan_prefix("validator:");
|
||||
for vs in records {
|
||||
set.register(ValidatorRecord {
|
||||
address: vs.address,
|
||||
stake: vs.stake,
|
||||
pon_score: vs.pon_score,
|
||||
is_active: vs.is_active,
|
||||
last_block: vs.last_block,
|
||||
slash_count: vs.slash_count,
|
||||
skip_count: vs.skip_count,
|
||||
consecutive_blocks: vs.consecutive_blocks,
|
||||
ban_until_slot: vs.ban_until_slot,
|
||||
});
|
||||
}
|
||||
set
|
||||
}
|
||||
|
||||
fn update_validator_pon(db: &StateDb, address: &str) {
|
||||
use nu_consensus::pon_score::update_on_honest_block;
|
||||
if let Some(mut vs) = db.get::<ValidatorState>(&format!("validator:{address}")).ok().flatten() {
|
||||
let mut record = ValidatorRecord {
|
||||
address: vs.address.clone(),
|
||||
stake: vs.stake,
|
||||
pon_score: vs.pon_score,
|
||||
is_active: vs.is_active,
|
||||
last_block: vs.last_block,
|
||||
slash_count: vs.slash_count,
|
||||
skip_count: vs.skip_count,
|
||||
consecutive_blocks: vs.consecutive_blocks,
|
||||
ban_until_slot: vs.ban_until_slot,
|
||||
};
|
||||
update_on_honest_block(&mut record);
|
||||
vs.pon_score = record.pon_score;
|
||||
vs.consecutive_blocks += 1;
|
||||
vs.skip_count = 0;
|
||||
let _ = db.put(&format!("validator:{address}"), &vs);
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_scheduler_txs(db: &StateDb, now_ms: i64) -> Vec<RawTransaction> {
|
||||
use nu_state::story_node::StoryNodeState;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
let mut txs = Vec::new();
|
||||
|
||||
let nodes: Vec<StoryNodeState> = db.scan_prefix("node_temp:");
|
||||
|
||||
for node in nodes {
|
||||
|
|
@ -140,10 +208,9 @@ fn generate_scheduler_txs(db: &StateDb, now_ms: i64) -> Vec<RawTransaction> {
|
|||
Some(TxPayload::VotingOpen { node_id: node.temp_id.clone() })
|
||||
} else if node.ready_for_finalization(now_ms) {
|
||||
if node.is_approved() {
|
||||
let canonical_id = node.temp_id.clone(); // simplified — real impl derives from tree depth
|
||||
Some(TxPayload::NodeApprove {
|
||||
temp_id: node.temp_id.clone(),
|
||||
canonical_id,
|
||||
canonical_id: node.temp_id.clone(),
|
||||
})
|
||||
} else {
|
||||
Some(TxPayload::NodeReject { node_id: node.temp_id.clone() })
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ async fn main() -> Result<()> {
|
|||
let cfg = block_loop::BlockLoopConfig {
|
||||
validator_addr: cli.validator_addr.clone(),
|
||||
chain_id: cli.chain_id.clone(),
|
||||
dev_mode: cli.dev,
|
||||
};
|
||||
tokio::spawn(block_loop::run(
|
||||
cfg,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue