montana/Монтана-Протокол/Код/crates/mt-state/src/lib.rs

648 lines
19 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// spec, разделы "Состояние сети" + "Consensus encoding layer"
use std::collections::BTreeMap;
use mt_codec::{
domain, write_bytes, write_u128, write_u16, write_u32, write_u64, write_u8, CanonicalEncode,
};
use mt_crypto::{hash, Hash32, PUBLIC_KEY_SIZE};
use mt_merkle::SparseMerkleTree;
pub type AccountId = [u8; 32];
pub type NodeId = [u8; 32];
// spec: AccountRecord layout — см. раздел "Account — содержимое блока".
pub const ACCOUNT_RECORD_SIZE: usize = 2059;
pub const NODE_RECORD_SIZE: usize = 2098;
pub const CANDIDATE_RECORD_SIZE: usize = 2082;
// spec, "Proposal header" layout: winner_class единственное valid значение = 1 (Node).
// Константа WINNER_CLASS_NODE сохранена для apply_emission и mt-lottery::Candidate.class.
pub const WINNER_CLASS_NODE: u8 = 1;
// spec: Account Table (запись на аккаунт) — 2059 bytes fixed
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AccountRecord {
pub account_id: AccountId,
pub balance: u128,
pub suite_id: u16,
pub is_node_operator: bool,
pub frontier_hash: Hash32,
pub op_height: u32,
pub account_chain_length: u32,
pub account_chain_length_snapshot: u32,
pub current_pubkey: [u8; PUBLIC_KEY_SIZE],
pub creation_window: u32,
pub last_op_window: u32,
pub last_activation_window: u32,
}
impl CanonicalEncode for AccountRecord {
fn encode(&self, buf: &mut Vec<u8>) {
write_bytes(buf, &self.account_id);
write_u128(buf, self.balance);
write_u16(buf, self.suite_id);
write_u8(buf, self.is_node_operator as u8);
write_bytes(buf, &self.frontier_hash);
write_u32(buf, self.op_height);
write_u32(buf, self.account_chain_length);
write_u32(buf, self.account_chain_length_snapshot);
write_bytes(buf, &self.current_pubkey);
write_u32(buf, self.creation_window);
write_u32(buf, self.last_op_window);
write_u32(buf, self.last_activation_window);
}
}
// spec: Node Table (запись на узел) — 2098 bytes fixed (см. NODE_RECORD_SIZE)
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NodeRecord {
pub node_id: NodeId,
pub node_pubkey: [u8; PUBLIC_KEY_SIZE],
pub suite_id: u16,
pub operator_account_id: AccountId,
pub start_window: u64,
pub chain_length: u64,
pub chain_length_snapshot: u64,
// spec, раздел "Состояние сети → Node tier checkpoints" — 6 snapshot
// значений `chain_length` зафиксированных на границах τ₂-окон tier
// confirmation. Используется selection event для weighted lottery: tier
// weight рассчитывается через серию snapshots, а не моментальное значение
// (защита от грайнинга через мгновенный chain_length boost).
pub chain_length_checkpoints: [u64; 6],
pub last_confirmation_window: u64,
}
impl CanonicalEncode for NodeRecord {
fn encode(&self, buf: &mut Vec<u8>) {
write_bytes(buf, &self.node_id);
write_bytes(buf, &self.node_pubkey);
write_u16(buf, self.suite_id);
write_bytes(buf, &self.operator_account_id);
write_u64(buf, self.start_window);
write_u64(buf, self.chain_length);
write_u64(buf, self.chain_length_snapshot);
for checkpoint in &self.chain_length_checkpoints {
write_u64(buf, *checkpoint);
}
write_u64(buf, self.last_confirmation_window);
}
}
// spec: Candidate Pool (запись на кандидата) — 2082 bytes fixed (см. CANDIDATE_RECORD_SIZE)
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CandidateRecord {
pub node_id: NodeId,
pub node_pubkey: [u8; PUBLIC_KEY_SIZE],
pub suite_id: u16,
pub operator_account_id: AccountId,
pub proof_endpoint: Hash32,
pub w_start: u64,
pub vdf_chain_length: u64,
pub registration_window: u64,
pub expires: u64,
}
impl CanonicalEncode for CandidateRecord {
fn encode(&self, buf: &mut Vec<u8>) {
write_bytes(buf, &self.node_id);
write_bytes(buf, &self.node_pubkey);
write_u16(buf, self.suite_id);
write_bytes(buf, &self.operator_account_id);
write_bytes(buf, &self.proof_endpoint);
write_u64(buf, self.w_start);
write_u64(buf, self.vdf_chain_length);
write_u64(buf, self.registration_window);
write_u64(buf, self.expires);
}
}
// BTreeMap + SparseMerkleTree, детерминированный порядок (HashMap запрещён спекой)
#[derive(Default, Clone)]
pub struct AccountTable {
records: BTreeMap<AccountId, AccountRecord>,
tree: SparseMerkleTree,
}
impl AccountTable {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, id: &AccountId) -> Option<&AccountRecord> {
self.records.get(id)
}
pub fn insert(&mut self, record: AccountRecord) {
let key = record.account_id;
let mut buf = Vec::with_capacity(ACCOUNT_RECORD_SIZE);
record.encode(&mut buf);
self.tree.insert(key, &buf);
self.records.insert(key, record);
}
pub fn remove(&mut self, id: &AccountId) -> Option<AccountRecord> {
self.tree.remove(id);
self.records.remove(id)
}
pub fn contains(&self, id: &AccountId) -> bool {
self.records.contains_key(id)
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn root(&self) -> Hash32 {
self.tree.root()
}
pub fn iter(&self) -> impl Iterator<Item = &AccountRecord> {
self.records.values()
}
}
#[derive(Default, Clone)]
pub struct NodeTable {
records: BTreeMap<NodeId, NodeRecord>,
tree: SparseMerkleTree,
}
impl NodeTable {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, id: &NodeId) -> Option<&NodeRecord> {
self.records.get(id)
}
pub fn insert(&mut self, record: NodeRecord) {
let key = record.node_id;
let mut buf = Vec::with_capacity(NODE_RECORD_SIZE);
record.encode(&mut buf);
self.tree.insert(key, &buf);
self.records.insert(key, record);
}
pub fn remove(&mut self, id: &NodeId) -> Option<NodeRecord> {
self.tree.remove(id);
self.records.remove(id)
}
pub fn contains(&self, id: &NodeId) -> bool {
self.records.contains_key(id)
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn root(&self) -> Hash32 {
self.tree.root()
}
pub fn iter(&self) -> impl Iterator<Item = &NodeRecord> {
self.records.values()
}
}
#[derive(Default, Clone)]
pub struct CandidatePool {
records: BTreeMap<NodeId, CandidateRecord>,
tree: SparseMerkleTree,
}
impl CandidatePool {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, id: &NodeId) -> Option<&CandidateRecord> {
self.records.get(id)
}
pub fn insert(&mut self, record: CandidateRecord) {
let key = record.node_id;
let mut buf = Vec::with_capacity(CANDIDATE_RECORD_SIZE);
record.encode(&mut buf);
self.tree.insert(key, &buf);
self.records.insert(key, record);
}
pub fn remove(&mut self, id: &NodeId) -> Option<CandidateRecord> {
self.tree.remove(id);
self.records.remove(id)
}
pub fn contains(&self, id: &NodeId) -> bool {
self.records.contains_key(id)
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn root(&self) -> Hash32 {
self.tree.root()
}
pub fn iter(&self) -> impl Iterator<Item = &CandidateRecord> {
self.records.values()
}
}
// spec: state_root = SHA-256("mt-state-root"
// || node_root
// || candidate_root
// || account_root)
pub fn compute_state_root(
node_root: &Hash32,
candidate_root: &Hash32,
account_root: &Hash32,
) -> Hash32 {
hash(
domain::STATE_ROOT,
&[node_root, candidate_root, account_root],
)
}
// spec: account_id = SHA-256("mt-account" || suite_id || pubkey)
pub fn derive_account_id(suite_id: u16, pubkey: &[u8; PUBLIC_KEY_SIZE]) -> AccountId {
hash(domain::ACCOUNT, &[&suite_id.to_le_bytes(), pubkey])
}
// spec: node_id = SHA-256("mt-node" || node_pubkey)
pub fn derive_node_id(node_pubkey: &[u8; PUBLIC_KEY_SIZE]) -> NodeId {
hash(domain::NODE, &[node_pubkey])
}
// spec: active(node, W) = (W - node.last_confirmation_window) <= 2 × τ₂_windows
pub fn is_active(node: &NodeRecord, current_window: u64, tau2_windows: u64) -> bool {
current_window.saturating_sub(node.last_confirmation_window) <= 2 * tau2_windows
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_account() -> AccountRecord {
AccountRecord {
account_id: [0xAA; 32],
balance: 1_000_000_000_000u128,
suite_id: 1,
is_node_operator: false,
frontier_hash: [0xBB; 32],
op_height: 5,
account_chain_length: 10,
account_chain_length_snapshot: 9,
current_pubkey: [0xCC; PUBLIC_KEY_SIZE],
creation_window: 100,
last_op_window: 200,
last_activation_window: 0,
}
}
fn sample_node() -> NodeRecord {
NodeRecord {
node_id: [0x11; 32],
node_pubkey: [0x22; PUBLIC_KEY_SIZE],
suite_id: 1,
operator_account_id: [0x33; 32],
start_window: 50,
chain_length: 100,
chain_length_snapshot: 80,
chain_length_checkpoints: [10, 20, 30, 40, 50, 60],
last_confirmation_window: 150,
}
}
fn sample_candidate() -> CandidateRecord {
CandidateRecord {
node_id: [0x44; 32],
node_pubkey: [0x55; PUBLIC_KEY_SIZE],
suite_id: 1,
operator_account_id: [0x66; 32],
proof_endpoint: [0x77; 32],
w_start: 10,
vdf_chain_length: 20_160,
registration_window: 30_000,
expires: 90_480,
}
}
#[test]
fn account_record_encoded_size() {
let mut buf = Vec::new();
sample_account().encode(&mut buf);
assert_eq!(buf.len(), ACCOUNT_RECORD_SIZE);
assert_eq!(ACCOUNT_RECORD_SIZE, 2059);
}
#[test]
fn node_record_encoded_size() {
let mut buf = Vec::new();
sample_node().encode(&mut buf);
assert_eq!(buf.len(), NODE_RECORD_SIZE);
assert_eq!(NODE_RECORD_SIZE, 2098);
}
#[test]
fn candidate_record_encoded_size() {
let mut buf = Vec::new();
sample_candidate().encode(&mut buf);
assert_eq!(buf.len(), CANDIDATE_RECORD_SIZE);
assert_eq!(CANDIDATE_RECORD_SIZE, 2082);
}
#[test]
fn account_record_field_order() {
let a = sample_account();
let mut buf = Vec::new();
a.encode(&mut buf);
// First 32 bytes = account_id
assert_eq!(&buf[..32], &a.account_id);
// Next 16 bytes = balance LE
assert_eq!(&buf[32..48], &a.balance.to_le_bytes());
// Next 2 bytes = suite_id LE
assert_eq!(&buf[48..50], &a.suite_id.to_le_bytes());
// Next 1 byte = is_node_operator as u8
assert_eq!(buf[50], 0u8); // false
}
#[test]
fn account_record_is_node_operator_true_encodes_one() {
let mut a = sample_account();
a.is_node_operator = true;
let mut buf = Vec::new();
a.encode(&mut buf);
assert_eq!(buf[50], 1u8);
}
#[test]
fn node_record_field_order() {
let n = sample_node();
let mut buf = Vec::new();
n.encode(&mut buf);
// First 32 = node_id
assert_eq!(&buf[..32], &n.node_id);
// Next PUBLIC_KEY_SIZE (1952 для ML-DSA-65) = node_pubkey
assert_eq!(&buf[32..32 + PUBLIC_KEY_SIZE], &n.node_pubkey);
}
#[test]
fn candidate_record_field_order() {
let c = sample_candidate();
let mut buf = Vec::new();
c.encode(&mut buf);
assert_eq!(&buf[..32], &c.node_id);
assert_eq!(&buf[32..32 + PUBLIC_KEY_SIZE], &c.node_pubkey);
}
#[test]
fn encoding_deterministic() {
let a = sample_account();
let mut b1 = Vec::new();
a.encode(&mut b1);
let mut b2 = Vec::new();
a.encode(&mut b2);
assert_eq!(b1, b2);
}
#[test]
fn derive_account_id_deterministic() {
let pk = [0xAB; PUBLIC_KEY_SIZE];
let id1 = derive_account_id(1, &pk);
let id2 = derive_account_id(1, &pk);
assert_eq!(id1, id2);
}
#[test]
fn derive_account_id_formula() {
let pk = [0xAB; PUBLIC_KEY_SIZE];
let expected = hash(domain::ACCOUNT, &[&1u16.to_le_bytes(), &pk]);
assert_eq!(derive_account_id(1, &pk), expected);
}
#[test]
fn derive_account_id_differs_by_suite() {
let pk = [0xAB; PUBLIC_KEY_SIZE];
assert_ne!(derive_account_id(1, &pk), derive_account_id(2, &pk));
}
#[test]
fn derive_node_id_formula() {
let pk = [0xCD; PUBLIC_KEY_SIZE];
let expected = hash(domain::NODE, &[&pk]);
assert_eq!(derive_node_id(&pk), expected);
}
#[test]
fn account_table_insert_get() {
let mut t = AccountTable::new();
assert!(t.is_empty());
let a = sample_account();
let id = a.account_id;
t.insert(a.clone());
assert!(t.contains(&id));
assert_eq!(t.get(&id), Some(&a));
assert_eq!(t.len(), 1);
}
#[test]
fn account_table_remove() {
let mut t = AccountTable::new();
let a = sample_account();
t.insert(a.clone());
let removed = t.remove(&a.account_id);
assert_eq!(removed, Some(a));
assert!(t.is_empty());
}
#[test]
fn account_table_root_changes_on_insert() {
let mut t = AccountTable::new();
let empty_root = t.root();
t.insert(sample_account());
assert_ne!(t.root(), empty_root);
}
#[test]
fn account_table_root_stable_on_idempotent_insert() {
let mut t = AccountTable::new();
let a = sample_account();
t.insert(a.clone());
let r1 = t.root();
t.insert(a);
let r2 = t.root();
assert_eq!(r1, r2);
}
#[test]
fn account_table_root_order_independent() {
let mut a1 = sample_account();
a1.account_id = [0x01; 32];
let mut a2 = sample_account();
a2.account_id = [0x02; 32];
let mut a3 = sample_account();
a3.account_id = [0x03; 32];
let mut t1 = AccountTable::new();
t1.insert(a1.clone());
t1.insert(a2.clone());
t1.insert(a3.clone());
let mut t2 = AccountTable::new();
t2.insert(a3);
t2.insert(a1);
t2.insert(a2);
assert_eq!(t1.root(), t2.root());
}
#[test]
fn node_table_insert_get_root() {
let mut t = NodeTable::new();
let empty_root = t.root();
let n = sample_node();
t.insert(n.clone());
assert_eq!(t.get(&n.node_id), Some(&n));
assert_ne!(t.root(), empty_root);
}
#[test]
fn candidate_pool_insert_get_root() {
let mut p = CandidatePool::new();
let empty_root = p.root();
let c = sample_candidate();
p.insert(c.clone());
assert_eq!(p.get(&c.node_id), Some(&c));
assert_ne!(p.root(), empty_root);
}
#[test]
fn state_root_deterministic() {
let node_root = [0x01; 32];
let cand_root = [0x02; 32];
let acct_root = [0x03; 32];
let a = compute_state_root(&node_root, &cand_root, &acct_root);
let b = compute_state_root(&node_root, &cand_root, &acct_root);
assert_eq!(a, b);
}
#[test]
fn state_root_detects_node_root_mutation() {
let a = compute_state_root(&[0x01; 32], &[0x02; 32], &[0x03; 32]);
let b = compute_state_root(&[0xFF; 32], &[0x02; 32], &[0x03; 32]);
assert_ne!(a, b);
}
#[test]
fn state_root_detects_candidate_root_mutation() {
let a = compute_state_root(&[0x01; 32], &[0x02; 32], &[0x03; 32]);
let b = compute_state_root(&[0x01; 32], &[0xFF; 32], &[0x03; 32]);
assert_ne!(a, b);
}
#[test]
fn state_root_detects_account_root_mutation() {
let a = compute_state_root(&[0x01; 32], &[0x02; 32], &[0x03; 32]);
let b = compute_state_root(&[0x01; 32], &[0x02; 32], &[0xFF; 32]);
assert_ne!(a, b);
}
#[test]
fn state_root_order_matters() {
let r1 = [0x01; 32];
let r2 = [0x02; 32];
let r3 = [0x03; 32];
assert_ne!(
compute_state_root(&r1, &r2, &r3),
compute_state_root(&r2, &r1, &r3)
);
assert_ne!(
compute_state_root(&r1, &r2, &r3),
compute_state_root(&r3, &r2, &r1)
);
}
#[test]
fn state_root_uses_domain_separator() {
let r1 = [0x01; 32];
let r2 = [0x02; 32];
let r3 = [0x03; 32];
let expected = hash(domain::STATE_ROOT, &[&r1, &r2, &r3]);
assert_eq!(compute_state_root(&r1, &r2, &r3), expected);
}
#[test]
fn is_active_same_window() {
let mut n = sample_node();
n.last_confirmation_window = 100;
assert!(is_active(&n, 100, 20_160));
}
#[test]
fn is_active_within_2_tau2() {
let mut n = sample_node();
let tau2 = 20_160u64;
n.last_confirmation_window = 100;
assert!(is_active(&n, 100 + 2 * tau2, tau2)); // ровно на границе
assert!(is_active(&n, 100 + tau2, tau2));
}
#[test]
fn is_active_beyond_2_tau2_false() {
let mut n = sample_node();
let tau2 = 20_160u64;
n.last_confirmation_window = 100;
assert!(!is_active(&n, 100 + 2 * tau2 + 1, tau2));
}
#[test]
fn is_active_bootstrap_at_genesis() {
// bootstrap узел имеет last_confirmation_window = 0
// В окне 0 active должен быть true
let mut n = sample_node();
n.last_confirmation_window = 0;
assert!(is_active(&n, 0, 20_160));
}
#[test]
fn account_table_default_equals_new() {
let t1 = AccountTable::new();
let t2 = AccountTable::default();
assert_eq!(t1.root(), t2.root());
}
#[test]
fn empty_tables_have_same_empty_root() {
let a = AccountTable::new();
let n = NodeTable::new();
let c = CandidatePool::new();
// Все три используют SparseMerkleTree — empty root одинаков
assert_eq!(a.root(), n.root());
assert_eq!(n.root(), c.root());
}
#[test]
fn pubkey_size_matches_crypto_constant() {
// Связка: наши record types используют PUBLIC_KEY_SIZE из mt-crypto
// ML-DSA-65 pubkey = 1952 B (FIPS 204 level 3)
assert_eq!(PUBLIC_KEY_SIZE, 1952);
}
}