feat(genesis): genesis.json seed on first start

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mukan Erkin TÖRÜK 2026-04-25 07:47:16 +03:00
parent 5e00c6f090
commit 6bc6e90114
4 changed files with 199 additions and 1 deletions

View file

@ -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 <path>` 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`)

31
genesis.json Normal file
View file

@ -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
}
}
}

145
src/genesis.rs Normal file
View file

@ -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<GenesisAccount>,
pub validators: Vec<GenesisValidator>,
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::<serde_json::Value>("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(())
}

View file

@ -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<String>,
/// Path to genesis.json — applied once on first start if DB is empty
#[arg(long)]
genesis: Option<String>,
}
#[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::<NodeP2pEvent>(128);