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(())
|
|||
|
|
}
|