From 6bc6e90114fdc92d408762226bacc735fbb8c2eb7e747717a3313358c4282797 Mon Sep 17 00:00:00 2001 From: Mukan Erkin Date: Sat, 25 Apr 2026 07:47:16 +0300 Subject: [PATCH] feat(genesis): genesis.json seed on first start Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 9 ++- genesis.json | 31 +++++++++++ src/genesis.rs | 145 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 15 +++++ 4 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 genesis.json create mode 100644 src/genesis.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1094528..0f169a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,14 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) ## [Unreleased] -## [0.10.0] — 2026-04-24 +## [0.11.0] — 2026-04-25 + +### Added +- `src/genesis.rs` — `GenesisConfig` deserializer; `needs_seed()` + `apply()`: accounts, validators, genesis story root node, NFT#0 tek seferde DB'ye yazılır; `genesis:applied` anahtarı ile tekrar seed engellenir +- `--genesis ` CLI flag — node ilk başlatıldığında DB boşsa `genesis.json`'ı otomatik uygular +- `genesis.json` örnek dosyası + +## [0.10.0] — 2026-04-25 ### Added - `block_loop`: her slot başında süresi dolan ban'lar kaldırılır (`unban_expired_validators`) diff --git a/genesis.json b/genesis.json new file mode 100644 index 0000000..643a32b --- /dev/null +++ b/genesis.json @@ -0,0 +1,31 @@ +{ + "chain_id": "nu-devnet-1", + "genesis_time": 1745539200000, + "dev_wallet": "0xDEV0000000000000000000000000000000000000", + "burn_wallet": "0xBURN000000000000000000000000000000000000", + "accounts": [ + { + "address": "0xDEV0000000000000000000000000000000000000", + "balance": 52000000, + "staked": 5000000, + "pon_score": 1.4 + } + ], + "validators": [ + { + "address": "0xDEV0000000000000000000000000000000000000", + "stake": 5000000, + "pon_score": 1.4, + "is_active": true + } + ], + "genesis_story": { + "story_id": "genesis", + "root_node": { + "node_id": "0", + "author": "0xDEV0000000000000000000000000000000000000", + "content_hash": "bafkreigenesisrootnode000000000000000000000000000000000000000", + "submitted_at": 1745539200000 + } + } +} diff --git a/src/genesis.rs b/src/genesis.rs new file mode 100644 index 0000000..17f156d --- /dev/null +++ b/src/genesis.rs @@ -0,0 +1,145 @@ +use anyhow::{Context, Result}; +use serde::Deserialize; + +use nu_state::{ + account::AccountState, + story_node::{NodeStatus, StoryNodeState}, + NftState, StateDb, ValidatorState, +}; + +#[derive(Debug, Deserialize)] +pub struct GenesisConfig { + pub chain_id: String, + pub genesis_time: i64, + pub dev_wallet: String, + pub burn_wallet: String, + pub accounts: Vec, + pub validators: Vec, + pub genesis_story: GenesisStory, +} + +#[derive(Debug, Deserialize)] +pub struct GenesisAccount { + pub address: String, + pub balance: u64, + pub staked: u64, + pub pon_score: f64, +} + +#[derive(Debug, Deserialize)] +pub struct GenesisValidator { + pub address: String, + pub stake: u64, + pub pon_score: f64, + pub is_active: bool, +} + +#[derive(Debug, Deserialize)] +pub struct GenesisStory { + pub story_id: String, + pub root_node: GenesisNode, +} + +#[derive(Debug, Deserialize)] +pub struct GenesisNode { + pub node_id: String, + pub author: String, + pub content_hash: String, + pub submitted_at: i64, +} + +/// Returns true if this is a fresh (empty) database that needs seeding. +pub fn needs_seed(db: &StateDb) -> bool { + db.get::("genesis:applied") + .ok() + .flatten() + .is_none() +} + +/// Apply genesis config to an empty StateDb. +pub fn apply(db: &StateDb, path: &str) -> Result<()> { + let raw = std::fs::read_to_string(path) + .with_context(|| format!("cannot read genesis file: {path}"))?; + let cfg: GenesisConfig = serde_json::from_str(&raw) + .with_context(|| "invalid genesis.json format")?; + + // Accounts + for ga in &cfg.accounts { + let account = AccountState { + address: ga.address.clone(), + balance: ga.balance, + staked: ga.staked, + pon_score: ga.pon_score, + nonce: 0, + locked: 0, + locked_until: 0, + nft_ids: vec![cfg.genesis_story.root_node.node_id.clone()], + }; + db.put(&format!("account:{}", ga.address), &account)?; + } + + // Validators + for gv in &cfg.validators { + let vs = ValidatorState { + address: gv.address.clone(), + stake: gv.stake, + pon_score: gv.pon_score, + is_active: gv.is_active, + last_block: 0, + slash_count: 0, + skip_count: 0, + consecutive_blocks: 0, + ban_until_slot: 0, + }; + db.put(&format!("validator:{}", gv.address), &vs)?; + } + + // Genesis story root node — approved immediately, no voting + let node = &cfg.genesis_story.root_node; + let story_id = &cfg.genesis_story.story_id; + let node_state = StoryNodeState { + node_id: node.node_id.clone(), + temp_id: node.node_id.clone(), + story_id: story_id.clone(), + parent_id: String::new(), + author: node.author.clone(), + content_hash: node.content_hash.clone(), + status: NodeStatus::Approved, + submitted_at: node.submitted_at, + vote_open_at: 0, + vote_end_at: 0, + votes: vec![], + nft_id: node.node_id.clone(), + entry_fee: 0, + }; + db.put(&format!("node:{}", node.node_id), &node_state)?; + + // Genesis NFT for root node + let nft = NftState { + nft_id: node.node_id.clone(), + node_id: node.node_id.clone(), + owner: node.author.clone(), + collection_id: String::new(), + depth: 0, + lineage: vec![], + minted_at: cfg.genesis_time, + }; + db.put(&format!("nft:{}", node.node_id), &nft)?; + + // Mark genesis as applied so we never re-seed + db.put("genesis:applied", &serde_json::json!({ + "chain_id": cfg.chain_id, + "applied_at": cfg.genesis_time, + "story_id": story_id, + "root_node_id": node.node_id, + }))?; + + tracing::info!( + chain_id = %cfg.chain_id, + story_id = %story_id, + root_node = %node.node_id, + "genesis state applied" + ); + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 5c29b74..13841ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod block_loop; +mod genesis; mod p2p; use std::sync::Arc; @@ -46,6 +47,10 @@ struct Cli { /// e.g. http://127.0.0.1:30334 #[arg(long)] p2p_api: Option, + + /// Path to genesis.json — applied once on first start if DB is empty + #[arg(long)] + genesis: Option, } #[tokio::main] @@ -61,6 +66,16 @@ async fn main() -> Result<()> { tracing::info!("State DB opened at {}", cli.db_path); + // Apply genesis if provided and DB is empty + if let Some(ref genesis_path) = cli.genesis { + let db_guard = db.lock().await; + if genesis::needs_seed(&db_guard) { + genesis::apply(&db_guard, genesis_path)?; + } else { + tracing::info!("genesis already applied — skipping"); + } + } + // P2P event channel — block_loop and RPC publish, forwarded to nu-p2p via HTTP let (p2p_sender, rpc_p2p_tx) = if let Some(ref api_url) = cli.p2p_api { let (block_tx, mut block_rx) = mpsc::channel::(128);