use std::fs; use std::path::{Path, PathBuf}; use mt_crypto::Hash32; use mt_genesis::ProtocolParams; use crate::clock::meta_dir; use crate::identity::{Identity, NodeError}; pub const NODE_STATE_FILE: &str = "node_state.bin"; const MAGIC: &[u8; 4] = b"mtns"; const VERSION: u8 = 1; const SIZE: usize = 4 + 1 + 1 + 2 + 32 + 32 + 8 + 8 + 8 + 8 + 32; #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(u8)] pub enum NodePhase { Bootstrap = 0, CandidateVdf = 1, Registered = 2, Active = 3, } impl NodePhase { fn from_u8(v: u8) -> Result { match v { 0 => Ok(NodePhase::Bootstrap), 1 => Ok(NodePhase::CandidateVdf), 2 => Ok(NodePhase::Registered), 3 => Ok(NodePhase::Active), other => Err(NodeError::InvalidArguments(format!( "неизвестный node_phase: {other}" ))), } } } pub struct NodeLifecycle { pub phase: NodePhase, pub candidate_seed: Hash32, pub candidate_endpoint: Hash32, pub candidate_progress: u64, pub target_chain_length: u64, pub w_start: u64, pub registration_window: u64, pub nodereg_hash: Hash32, } impl NodeLifecycle { // Автоматическое определение genesis vs candidate per spec Genesis Decree: // если identity.node_pk == params.bootstrap_node_pubkey → узел = bootstrap // node сети, phase = Active immediately (без Candidate VDF, DEV-010); // иначе → standard path: phase = Bootstrap → CandidateVdf на первом окне → // Registered (через apply_noderegistrations_batch) → Active (через // apply_selection_event на ближайшем W % selection_interval == 0). // // Pre-Genesis-ceremony: params.bootstrap_node_pubkey = [0u8; PUBLIC_KEY_SIZE] // (placeholder zeros). Любой реальный узел с identity не совпадёт с zeros; // в этой ветке узел запускается как singleton genesis (legacy local mode) // — после Genesis ceremony эта ветка перестанет применяться (см. // mt_genesis::is_genesis_bootstrap_finalized). pub fn fresh_for(identity: &Identity, params: &ProtocolParams) -> Self { if Self::is_bootstrap_node(identity, params) { Self::fresh_genesis() } else { Self::fresh_candidate() } } pub fn is_bootstrap_node(identity: &Identity, params: &ProtocolParams) -> bool { let bootstrap_pubkey_zeroed = params.bootstrap_node_pubkey.iter().all(|&b| b == 0); if bootstrap_pubkey_zeroed { // Genesis ceremony pending — placeholder zeros pubkey не активирует // production check. Любой узел трактуется как singleton genesis для // local network of one (M5 development phase). return true; } identity.node_pk.as_bytes() == ¶ms.bootstrap_node_pubkey } fn fresh_genesis() -> Self { Self { phase: NodePhase::Active, candidate_seed: [0u8; 32], candidate_endpoint: [0u8; 32], candidate_progress: 0, target_chain_length: 0, w_start: 0, registration_window: 0, nodereg_hash: [0u8; 32], } } fn fresh_candidate() -> Self { Self { phase: NodePhase::Bootstrap, candidate_seed: [0u8; 32], candidate_endpoint: [0u8; 32], candidate_progress: 0, target_chain_length: 0, w_start: 0, registration_window: 0, nodereg_hash: [0u8; 32], } } } pub fn lifecycle_path(data_dir: &Path) -> PathBuf { meta_dir(data_dir).join(NODE_STATE_FILE) } pub fn load_or_init_lifecycle( data_dir: &Path, identity: &Identity, params: &ProtocolParams, ) -> Result { let path = lifecycle_path(data_dir); if !path.exists() { return Ok(NodeLifecycle::fresh_for(identity, params)); } let bytes = fs::read(&path)?; if bytes.len() != SIZE { return Err(NodeError::CorruptedSize { expected: SIZE, actual: bytes.len(), }); } if &bytes[0..4] != MAGIC.as_slice() { return Err(NodeError::InvalidMagic); } if bytes[4] != VERSION { return Err(NodeError::UnsupportedVersion(bytes[4])); } let phase = NodePhase::from_u8(bytes[5])?; let mut candidate_seed = [0u8; 32]; candidate_seed.copy_from_slice(&bytes[8..40]); let mut candidate_endpoint = [0u8; 32]; candidate_endpoint.copy_from_slice(&bytes[40..72]); let candidate_progress = u64::from_le_bytes(bytes[72..80].try_into().unwrap()); let target_chain_length = u64::from_le_bytes(bytes[80..88].try_into().unwrap()); let w_start = u64::from_le_bytes(bytes[88..96].try_into().unwrap()); let registration_window = u64::from_le_bytes(bytes[96..104].try_into().unwrap()); let mut nodereg_hash = [0u8; 32]; nodereg_hash.copy_from_slice(&bytes[104..136]); Ok(NodeLifecycle { phase, candidate_seed, candidate_endpoint, candidate_progress, target_chain_length, w_start, registration_window, nodereg_hash, }) } pub fn save_lifecycle(data_dir: &Path, state: &NodeLifecycle) -> Result<(), NodeError> { fs::create_dir_all(meta_dir(data_dir))?; let path = lifecycle_path(data_dir); let mut buf = vec![0u8; SIZE]; buf[0..4].copy_from_slice(MAGIC); buf[4] = VERSION; buf[5] = state.phase as u8; buf[8..40].copy_from_slice(&state.candidate_seed); buf[40..72].copy_from_slice(&state.candidate_endpoint); buf[72..80].copy_from_slice(&state.candidate_progress.to_le_bytes()); buf[80..88].copy_from_slice(&state.target_chain_length.to_le_bytes()); buf[88..96].copy_from_slice(&state.w_start.to_le_bytes()); buf[96..104].copy_from_slice(&state.registration_window.to_le_bytes()); buf[104..136].copy_from_slice(&state.nodereg_hash); let tmp = path.with_extension("bin.tmp"); fs::write(&tmp, &buf)?; fs::rename(&tmp, &path)?; Ok(()) }