feat(genesis): genesis.json seed on first start
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5e00c6f090
commit
6bc6e90114
4 changed files with 199 additions and 1 deletions
|
|
@ -7,7 +7,14 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
### Added
|
||||||
- `block_loop`: her slot başında süresi dolan ban'lar kaldırılır (`unban_expired_validators`)
|
- `block_loop`: her slot başında süresi dolan ban'lar kaldırılır (`unban_expired_validators`)
|
||||||
|
|
|
||||||
31
genesis.json
Normal file
31
genesis.json
Normal 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
145
src/genesis.rs
Normal 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(())
|
||||||
|
}
|
||||||
15
src/main.rs
15
src/main.rs
|
|
@ -1,4 +1,5 @@
|
||||||
mod block_loop;
|
mod block_loop;
|
||||||
|
mod genesis;
|
||||||
mod p2p;
|
mod p2p;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -46,6 +47,10 @@ struct Cli {
|
||||||
/// e.g. http://127.0.0.1:30334
|
/// e.g. http://127.0.0.1:30334
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
p2p_api: Option<String>,
|
p2p_api: Option<String>,
|
||||||
|
|
||||||
|
/// Path to genesis.json — applied once on first start if DB is empty
|
||||||
|
#[arg(long)]
|
||||||
|
genesis: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
@ -61,6 +66,16 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
tracing::info!("State DB opened at {}", cli.db_path);
|
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
|
// 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 (p2p_sender, rpc_p2p_tx) = if let Some(ref api_url) = cli.p2p_api {
|
||||||
let (block_tx, mut block_rx) = mpsc::channel::<NodeP2pEvent>(128);
|
let (block_tx, mut block_rx) = mpsc::channel::<NodeP2pEvent>(128);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue