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]
|
||||
|
||||
## [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
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 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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue