176 lines
6.1 KiB
Rust
176 lines
6.1 KiB
Rust
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<Self, NodeError> {
|
||
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<NodeLifecycle, NodeError> {
|
||
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(())
|
||
}
|