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

338 lines
9.1 KiB
Rust
Raw Permalink 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.

// Automated determinism invariants для mt-state.
// M2 batch 2 audit prep — state layer = consensus state foundation;
// любое non-determinism = consensus fork. Эти invariants ловят regression
// если refactor случайно ломает determinism / SSOT / сериализацию.
use mt_codec::CanonicalEncode;
use mt_crypto::PUBLIC_KEY_SIZE;
use mt_state::{
compute_state_root, derive_account_id, derive_node_id, is_active, AccountRecord, AccountTable,
CandidatePool, CandidateRecord, NodeRecord, NodeTable, ACCOUNT_RECORD_SIZE,
CANDIDATE_RECORD_SIZE, NODE_RECORD_SIZE, WINNER_CLASS_NODE,
};
fn sample_account(id_byte: u8) -> AccountRecord {
AccountRecord {
account_id: [id_byte; 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(id_byte: u8) -> NodeRecord {
NodeRecord {
node_id: [id_byte; 32],
node_pubkey: [0x33; PUBLIC_KEY_SIZE],
suite_id: 1,
operator_account_id: [0x77; 32],
start_window: 1000,
chain_length: 42,
chain_length_snapshot: 40,
chain_length_checkpoints: [0u64; 6],
last_confirmation_window: 5000,
}
}
fn sample_candidate(id_byte: u8) -> CandidateRecord {
CandidateRecord {
node_id: [id_byte; 32],
node_pubkey: [0x44; PUBLIC_KEY_SIZE],
suite_id: 1,
operator_account_id: [0x55; 32],
proof_endpoint: [0x66; 32],
w_start: 500,
vdf_chain_length: 100,
registration_window: 500,
expires: 5000,
}
}
// ---------- Derivation determinism ----------
#[test]
fn derive_account_id_deterministic() {
let pk = [0xAAu8; PUBLIC_KEY_SIZE];
let a = derive_account_id(1, &pk);
let b = derive_account_id(1, &pk);
assert_eq!(a, b);
}
#[test]
fn derive_account_id_changes_on_suite_id() {
let pk = [0xAAu8; PUBLIC_KEY_SIZE];
let a = derive_account_id(1, &pk);
let b = derive_account_id(2, &pk);
assert_ne!(a, b, "suite_id должен влиять на account_id");
}
#[test]
fn derive_account_id_changes_on_pubkey() {
let a = derive_account_id(1, &[0xAA; PUBLIC_KEY_SIZE]);
let b = derive_account_id(1, &[0xBB; PUBLIC_KEY_SIZE]);
assert_ne!(a, b);
}
#[test]
fn derive_node_id_deterministic() {
let pk = [0xCCu8; PUBLIC_KEY_SIZE];
let a = derive_node_id(&pk);
let b = derive_node_id(&pk);
assert_eq!(a, b);
}
#[test]
fn derive_account_node_id_different() {
// Same pubkey → different account_id и node_id (разные domain separators).
let pk = [0xDD; PUBLIC_KEY_SIZE];
let aid = derive_account_id(1, &pk);
let nid = derive_node_id(&pk);
assert_ne!(
aid, nid,
"account_id и node_id должны различаться даже от того же pubkey"
);
}
// ---------- Encoded sizes match constants ([I-9] determinism) ----------
#[test]
fn account_record_encoded_size_constant() {
let mut buf = Vec::new();
sample_account(0xAA).encode(&mut buf);
assert_eq!(
buf.len(),
ACCOUNT_RECORD_SIZE,
"AccountRecord encoded size drift: expected {}, got {}",
ACCOUNT_RECORD_SIZE,
buf.len()
);
}
#[test]
fn node_record_encoded_size_constant() {
let mut buf = Vec::new();
sample_node(0x11).encode(&mut buf);
assert_eq!(
buf.len(),
NODE_RECORD_SIZE,
"NodeRecord encoded size drift: expected {}, got {}",
NODE_RECORD_SIZE,
buf.len()
);
}
#[test]
fn candidate_record_encoded_size_constant() {
let mut buf = Vec::new();
sample_candidate(0x22).encode(&mut buf);
assert_eq!(
buf.len(),
CANDIDATE_RECORD_SIZE,
"CandidateRecord encoded size drift: expected {}, got {}",
CANDIDATE_RECORD_SIZE,
buf.len()
);
}
// ---------- Encoding determinism ----------
#[test]
fn account_record_encoding_deterministic() {
let r = sample_account(0xCC);
let mut a = Vec::new();
let mut b = Vec::new();
r.encode(&mut a);
r.encode(&mut b);
assert_eq!(a, b);
}
// ---------- Table determinism (BTreeMap canonical sort) ----------
#[test]
fn account_table_root_deterministic_across_insertion_orders() {
let r1 = sample_account(0x01);
let r2 = sample_account(0x02);
let r3 = sample_account(0x03);
let mut t_forward = AccountTable::new();
t_forward.insert(r1.clone());
t_forward.insert(r2.clone());
t_forward.insert(r3.clone());
let mut t_reverse = AccountTable::new();
t_reverse.insert(r3.clone());
t_reverse.insert(r2.clone());
t_reverse.insert(r1.clone());
assert_eq!(
t_forward.root(),
t_reverse.root(),
"AccountTable root зависит от insertion order — нарушение [I-3] determinism"
);
}
#[test]
fn node_table_root_deterministic_across_insertion_orders() {
let n1 = sample_node(0x10);
let n2 = sample_node(0x20);
let n3 = sample_node(0x30);
let mut t1 = NodeTable::new();
t1.insert(n1.clone());
t1.insert(n2.clone());
t1.insert(n3.clone());
let mut t2 = NodeTable::new();
t2.insert(n3.clone());
t2.insert(n1.clone());
t2.insert(n2.clone());
assert_eq!(t1.root(), t2.root());
}
#[test]
fn candidate_pool_root_deterministic_across_insertion_orders() {
let c1 = sample_candidate(0x40);
let c2 = sample_candidate(0x50);
let mut p1 = CandidatePool::new();
p1.insert(c1.clone());
p1.insert(c2.clone());
let mut p2 = CandidatePool::new();
p2.insert(c2.clone());
p2.insert(c1.clone());
assert_eq!(p1.root(), p2.root());
}
// ---------- compute_state_root determinism + dependence ----------
#[test]
fn compute_state_root_deterministic() {
let n = [0x11u8; 32];
let c = [0x22u8; 32];
let a = [0x33u8; 32];
assert_eq!(
compute_state_root(&n, &c, &a),
compute_state_root(&n, &c, &a)
);
}
#[test]
fn compute_state_root_changes_on_each_root() {
let n = [0x11u8; 32];
let c = [0x22u8; 32];
let a = [0x33u8; 32];
let base = compute_state_root(&n, &c, &a);
assert_ne!(base, compute_state_root(&[0x00u8; 32], &c, &a));
assert_ne!(base, compute_state_root(&n, &[0x00u8; 32], &a));
assert_ne!(base, compute_state_root(&n, &c, &[0x00u8; 32]));
}
#[test]
fn compute_state_root_order_sensitive() {
// Order аргументов влияет — node_root vs candidate_root vs account_root
// нельзя поменять местами.
let a = [0x11u8; 32];
let b = [0x22u8; 32];
let c = [0x33u8; 32];
assert_ne!(
compute_state_root(&a, &b, &c),
compute_state_root(&b, &a, &c),
"compute_state_root должен быть order-sensitive"
);
}
// ---------- is_active boundary ----------
#[test]
fn is_active_window_zero_correct_with_saturating_sub() {
// current_window < last_confirmation_window → saturating_sub = 0 → active
let n = NodeRecord {
last_confirmation_window: 100,
..sample_node(0x99)
};
assert!(is_active(&n, 50, 1000));
}
#[test]
fn is_active_at_2_tau2_boundary_inclusive() {
let n = NodeRecord {
last_confirmation_window: 1000,
..sample_node(0x88)
};
let tau2 = 100;
// current - last = 200 = 2×τ₂ — inclusive, active
assert!(is_active(&n, 1200, tau2));
// current - last = 201 — beyond, inactive
assert!(!is_active(&n, 1201, tau2));
}
// ---------- Static API invariants ----------
#[test]
fn winner_class_node_is_one() {
// SSOT [I-10]: WINNER_CLASS_NODE = 1 константа.
// Если кто-то случайно меняет — этот test fails.
assert_eq!(WINNER_CLASS_NODE, 1);
}
#[test]
fn record_sizes_positive() {
// Compile-time const checks — defensive sanity, не runtime check.
const _: () = assert!(ACCOUNT_RECORD_SIZE > 0);
const _: () = assert!(NODE_RECORD_SIZE > 0);
const _: () = assert!(CANDIDATE_RECORD_SIZE > 0);
}
#[test]
fn empty_tables_have_consistent_empty_root() {
let a = AccountTable::new();
let b = AccountTable::new();
assert_eq!(
a.root(),
b.root(),
"Empty AccountTable roots должны совпадать"
);
let n1 = NodeTable::new();
let n2 = NodeTable::new();
assert_eq!(n1.root(), n2.root());
let c1 = CandidatePool::new();
let c2 = CandidatePool::new();
assert_eq!(c1.root(), c2.root());
}
#[test]
fn account_table_remove_inverse_of_insert() {
let r = sample_account(0xEE);
let id = r.account_id;
let mut t = AccountTable::new();
let empty_root = t.root();
t.insert(r);
assert!(t.contains(&id));
let after_insert_root = t.root();
assert_ne!(empty_root, after_insert_root);
t.remove(&id);
assert!(!t.contains(&id));
assert_eq!(
t.root(),
empty_root,
"Insert + remove должно возвращать root к empty state"
);
}