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

345 lines
11 KiB
Rust
Raw Normal View History

// Automated determinism invariants для mt-entry.
// M4 audit prep — node admission flow: NodeRegistration / candidate_vdf_init /
// selection event / Adaptive VDF / nr_sort_key. Любое non-determinism =
// consensus fork. Invariants ловят regression в byte-exact encode,
// hash composition, sort orders, либо state transition apply_*_batch.
use mt_codec::CanonicalEncode;
use mt_crypto::{Hash32, Signature, PUBLIC_KEY_SIZE, SIGNATURE_SIZE};
use mt_entry::{
candidate_vdf_init, compute_expiry_window, compute_node_id, is_selection_window, nodereg_hash,
nr_sort_key, rank_candidates_for_selection, required_vdf_length, selection_slots,
selection_sort_key, NodeRegistration, NODE_REGISTRATION_SIZE, TYPE_NODE_REGISTRATION,
};
use mt_state::{CandidatePool, CandidateRecord, NodeId};
// ---------- Helpers ----------
fn sample_node_registration(seed: u8) -> NodeRegistration {
NodeRegistration {
suite_id: 1,
node_pubkey: [seed; PUBLIC_KEY_SIZE],
operator_account_id: [seed.wrapping_add(1); 32],
proof_endpoint: [seed.wrapping_add(2); 32],
w_start: 100,
vdf_chain_length: 20_160,
signature: Signature::from_array([0u8; SIGNATURE_SIZE]),
}
}
fn sample_candidate(seed: u8, w_start: u64) -> CandidateRecord {
CandidateRecord {
node_id: [seed; 32],
node_pubkey: [seed; PUBLIC_KEY_SIZE],
suite_id: 1,
operator_account_id: [seed.wrapping_add(1); 32],
proof_endpoint: [seed.wrapping_add(2); 32],
w_start,
vdf_chain_length: 20_160,
registration_window: w_start,
expires: w_start + 60_480,
}
}
// ---------- Encoded size matches constant ([I-9] determinism) ----------
#[test]
fn node_registration_encoded_size_constant() {
let nr = sample_node_registration(0xAB);
let mut buf = Vec::new();
nr.encode(&mut buf);
assert_eq!(
buf.len(),
NODE_REGISTRATION_SIZE,
"NodeRegistration encoded size drift: expected {}, got {}",
NODE_REGISTRATION_SIZE,
buf.len()
);
}
#[test]
fn type_node_registration_stable() {
// SSOT [I-10]: type code immutable. Изменение = consensus fork.
assert_eq!(TYPE_NODE_REGISTRATION, 0x11);
}
// ---------- nodereg_hash R2 invariant ----------
#[test]
fn nodereg_hash_deterministic() {
let nr = sample_node_registration(0xAB);
assert_eq!(nodereg_hash(&nr), nodereg_hash(&nr));
}
#[test]
fn nodereg_hash_stable_under_signature_mutation() {
// R2: signature НЕ входит в hash.
let mut a = sample_node_registration(0xAB);
let mut b = a.clone();
b.signature = Signature::from_array([0xFF; SIGNATURE_SIZE]);
assert_eq!(nodereg_hash(&a), nodereg_hash(&b));
a.signature = Signature::from_array([0x42; SIGNATURE_SIZE]);
assert_eq!(nodereg_hash(&a), nodereg_hash(&b));
}
#[test]
fn nodereg_hash_changes_on_field_mutation() {
let base = sample_node_registration(0xAB);
let m_pubkey = sample_node_registration(0xFF);
let m_w_start = {
let mut x = base.clone();
x.w_start = 999;
x
};
let m_vdf = {
let mut x = base.clone();
x.vdf_chain_length = 99999;
x
};
let m_operator = {
let mut x = base.clone();
x.operator_account_id = [0xFF; 32];
x
};
assert_ne!(nodereg_hash(&base), nodereg_hash(&m_pubkey));
assert_ne!(nodereg_hash(&base), nodereg_hash(&m_w_start));
assert_ne!(nodereg_hash(&base), nodereg_hash(&m_vdf));
assert_ne!(nodereg_hash(&base), nodereg_hash(&m_operator));
}
// ---------- compute_node_id determinism ----------
#[test]
fn compute_node_id_deterministic() {
let pubkey = [0xCC; PUBLIC_KEY_SIZE];
assert_eq!(compute_node_id(&pubkey), compute_node_id(&pubkey));
}
#[test]
fn compute_node_id_changes_on_pubkey() {
assert_ne!(
compute_node_id(&[0xAA; PUBLIC_KEY_SIZE]),
compute_node_id(&[0xBB; PUBLIC_KEY_SIZE])
);
}
// ---------- candidate_vdf_init / [I-8] binding ----------
#[test]
fn candidate_vdf_init_deterministic() {
let t_r = [0x11; 32];
let cba = [0x22; 32];
let node_id: NodeId = [0x33; 32];
assert_eq!(
candidate_vdf_init(&t_r, &cba, &node_id),
candidate_vdf_init(&t_r, &cba, &node_id)
);
}
#[test]
fn candidate_vdf_init_changes_on_each_input() {
let t_r = [0x11; 32];
let cba = [0x22; 32];
let node_id: NodeId = [0x33; 32];
let base = candidate_vdf_init(&t_r, &cba, &node_id);
assert_ne!(base, candidate_vdf_init(&[0xFF; 32], &cba, &node_id));
assert_ne!(base, candidate_vdf_init(&t_r, &[0xFF; 32], &node_id));
assert_ne!(base, candidate_vdf_init(&t_r, &cba, &[0xFF; 32]));
}
// ---------- compute_expiry_window ----------
#[test]
fn compute_expiry_window_three_tau2() {
// [C-1] SSOT: candidate_expiry_windows читается из ProtocolParams (60_480 = 3τ₂ at genesis)
let p = mt_genesis::genesis_params();
assert_eq!(p.candidate_expiry_windows, 3 * p.tau2_windows);
assert_eq!(p.candidate_expiry_windows, 60_480);
assert_eq!(compute_expiry_window(100, p), 100 + 60_480);
assert_eq!(compute_expiry_window(0, p), 60_480);
}
// ---------- selection_slots + is_selection_window ----------
#[test]
fn selection_slots_admission_divisor_130() {
// [C-1] SSOT: admission_divisor читается из ProtocolParams (130 at genesis)
let p = mt_genesis::genesis_params();
assert_eq!(p.admission_divisor, 130);
// active = 0 → max(1, 0) = 1 (защита от division/zero edge)
assert_eq!(selection_slots(0, p), 1);
// 130 → 130/130 = 1; max(1, 1) = 1
assert_eq!(selection_slots(130, p), 1);
// 1300 → 10
assert_eq!(selection_slots(1300, p), 10);
// 1% rule sanity: 100k active → 100k/130 ≈ 769 slots
assert_eq!(selection_slots(100_000, p), 100_000 / 130);
}
#[test]
fn selection_slots_at_least_one() {
// max(1, ...) гарантирует ≥ 1 slot для bootstrap (genesis with 1 node)
let p = mt_genesis::genesis_params();
for active in [0u64, 1, 50, 129] {
assert_eq!(selection_slots(active, p), 1, "active={}", active);
}
}
#[test]
fn is_selection_window_every_336() {
// [C-1] SSOT: selection_interval читается из ProtocolParams (336 at genesis)
let p = mt_genesis::genesis_params();
assert_eq!(p.selection_interval, 336);
assert!(!is_selection_window(0, p)); // window 0 — особый случай (нет selection)
assert!(!is_selection_window(1, p));
assert!(!is_selection_window(335, p));
assert!(is_selection_window(336, p));
assert!(!is_selection_window(337, p));
assert!(is_selection_window(672, p));
assert!(is_selection_window(336 * 100, p));
}
// ---------- selection_sort_key + rank_candidates_for_selection ----------
#[test]
fn selection_sort_key_deterministic() {
let t_r = [0x11; 32];
let cba = [0x22; 32];
let node_id: NodeId = [0x33; 32];
assert_eq!(
selection_sort_key(&t_r, &cba, &node_id),
selection_sort_key(&t_r, &cba, &node_id)
);
}
#[test]
fn rank_candidates_canonical_order() {
// Sort by sort_key asc — input order не влияет.
let mut p1 = CandidatePool::new();
p1.insert(sample_candidate(0x10, 100));
p1.insert(sample_candidate(0x20, 100));
p1.insert(sample_candidate(0x30, 100));
let mut p2 = CandidatePool::new();
p2.insert(sample_candidate(0x30, 100));
p2.insert(sample_candidate(0x10, 100));
p2.insert(sample_candidate(0x20, 100));
let r1 = rank_candidates_for_selection(&p1, &[0xAA; 32], &[0xBB; 32]);
let r2 = rank_candidates_for_selection(&p2, &[0xAA; 32], &[0xBB; 32]);
// Same sort_keys derived → same order
let keys1: Vec<Hash32> = r1.iter().map(|(k, _)| *k).collect();
let keys2: Vec<Hash32> = r2.iter().map(|(k, _)| *k).collect();
assert_eq!(keys1, keys2);
// Sorted asc
for w in keys1.windows(2) {
assert!(w[0] <= w[1]);
}
}
// ---------- required_vdf_length / Adaptive VDF ----------
#[test]
fn required_vdf_length_no_active_returns_tau2() {
// Genesis edge case: no active nodes
assert_eq!(required_vdf_length(5, 0, 20_160), 20_160);
}
#[test]
fn required_vdf_length_below_threshold_returns_tau2() {
// pressure_permille ≤ 10 (1%) → base τ₂
// 5 pending / 1000 active = 5 permille (= 0.5%, < 1%)
assert_eq!(required_vdf_length(5, 1000, 20_160), 20_160);
// 10 pending / 1000 active = 10 permille = 1% (boundary, ≤ 10)
assert_eq!(required_vdf_length(10, 1000, 20_160), 20_160);
}
#[test]
fn required_vdf_length_above_threshold_scales() {
// 20 pending / 1000 active = 20 permille (2%, > 10)
// → required = τ₂ × 20 / 10 = τ₂ × 2 = 40_320
assert_eq!(required_vdf_length(20, 1000, 20_160), 40_320);
// 100 pending / 1000 active = 100 permille = 10%
// → required = τ₂ × 100 / 10 = τ₂ × 10
assert_eq!(required_vdf_length(100, 1000, 20_160), 201_600);
}
// ---------- nr_sort_key ----------
#[test]
fn nr_sort_key_deterministic() {
let t_r = [0x11; 32];
let cba = [0x22; 32];
let pubkey = [0x33; PUBLIC_KEY_SIZE];
assert_eq!(
nr_sort_key(&t_r, &cba, &pubkey),
nr_sort_key(&t_r, &cba, &pubkey)
);
}
#[test]
fn nr_sort_key_changes_on_each_input() {
let t_r = [0x11; 32];
let cba = [0x22; 32];
let pubkey = [0x33; PUBLIC_KEY_SIZE];
let base = nr_sort_key(&t_r, &cba, &pubkey);
assert_ne!(base, nr_sort_key(&[0xFF; 32], &cba, &pubkey));
assert_ne!(base, nr_sort_key(&t_r, &[0xFF; 32], &pubkey));
assert_ne!(base, nr_sort_key(&t_r, &cba, &[0xFF; PUBLIC_KEY_SIZE]));
}
// ---------- Cross-distinctness of similar hash compositions ----------
#[test]
fn distinct_domains_for_three_hash_compositions() {
// selection_sort_key, candidate_vdf_init, nr_sort_key все принимают (timechain, cba, identity).
// Domain separators различают:
// selection_sort_key: "mt-selection"
// candidate_vdf_init: "mt-candidate-vdf-init"
// nr_sort_key: "mt-nodereg-sort"
let t_r = [0x11; 32];
let cba = [0x22; 32];
let node_id: NodeId = [0x33; 32];
let pubkey = [0x33; PUBLIC_KEY_SIZE];
let sel = selection_sort_key(&t_r, &cba, &node_id);
let vdf = candidate_vdf_init(&t_r, &cba, &node_id);
let nr = nr_sort_key(&t_r, &cba, &pubkey);
assert_ne!(
sel, vdf,
"selection vs candidate-vdf-init domains must differ"
);
assert_ne!(
vdf, nr,
"candidate-vdf-init vs nodereg-sort domains must differ"
);
assert_ne!(sel, nr, "selection vs nodereg-sort domains must differ");
}
// ---------- Static API invariants ----------
#[test]
fn record_size_positive() {
const _: () = assert!(NODE_REGISTRATION_SIZE > 0);
const _: () = assert!(NODE_REGISTRATION_SIZE == 5344);
}
#[test]
fn admission_divisor_one_percent_cap() {
// 1% / 100 = 1/100; admission_divisor = 130 даёт ≤ ~0.77% per event
// (130 ≥ 100 чтобы slots = 1% или меньше). [C-1] SSOT: значение читается
// из ProtocolParams, не hardcoded const.
let p = mt_genesis::genesis_params();
assert!(p.admission_divisor >= 100);
}
#[test]
fn selection_interval_factors_into_tau2() {
// selection_interval = 336 ≤ τ₂ = 20_160 (60×336 = 20_160). [C-1] SSOT:
// значение читается из ProtocolParams, не hardcoded const.
let p = mt_genesis::genesis_params();
assert!(p.selection_interval < p.tau2_windows);
assert_eq!(p.tau2_windows % p.selection_interval, 0);
}