648 lines
19 KiB
Rust
648 lines
19 KiB
Rust
// 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);
|
||
}
|
||
}
|