196 lines
5.7 KiB
Rust
196 lines
5.7 KiB
Rust
|
|
// Automated determinism invariants для mt-timechain.
|
|||
|
|
// M2 batch 3 audit prep — TimeChain VDF + Adaptive D + cemented_bundle_aggregate
|
|||
|
|
// = критическая консенсус-поверхность с защитой [I-8] Network-Bound
|
|||
|
|
// Unpredictability. Любой non-determinism = consensus fork.
|
|||
|
|
|
|||
|
|
use mt_genesis::genesis_params;
|
|||
|
|
use mt_state::NodeId;
|
|||
|
|
use mt_timechain::{cemented_bundle_aggregate, next_d, vdf_step, vdf_verify};
|
|||
|
|
|
|||
|
|
// ---------- VDF determinism ----------
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn vdf_step_zero_iterations_identity() {
|
|||
|
|
let prev = [0x42u8; 32];
|
|||
|
|
assert_eq!(
|
|||
|
|
vdf_step(&prev, 0),
|
|||
|
|
prev,
|
|||
|
|
"vdf_step(prev, 0) должна быть identity (no-op)"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn vdf_step_deterministic() {
|
|||
|
|
let prev = [0x33u8; 32];
|
|||
|
|
let a = vdf_step(&prev, 100);
|
|||
|
|
let b = vdf_step(&prev, 100);
|
|||
|
|
assert_eq!(a, b);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn vdf_step_changes_with_iterations() {
|
|||
|
|
let prev = [0x55u8; 32];
|
|||
|
|
let a = vdf_step(&prev, 1);
|
|||
|
|
let b = vdf_step(&prev, 2);
|
|||
|
|
let c = vdf_step(&prev, 3);
|
|||
|
|
assert_ne!(a, b);
|
|||
|
|
assert_ne!(b, c);
|
|||
|
|
assert_ne!(a, c);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn vdf_step_changes_with_prev_input() {
|
|||
|
|
assert_ne!(vdf_step(&[0x00u8; 32], 50), vdf_step(&[0xFFu8; 32], 50));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn vdf_verify_accepts_correct_chain() {
|
|||
|
|
let prev = [0x77u8; 32];
|
|||
|
|
let claim = vdf_step(&prev, 42);
|
|||
|
|
assert!(vdf_verify(&prev, 42, &claim));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn vdf_verify_rejects_wrong_claim() {
|
|||
|
|
let prev = [0x88u8; 32];
|
|||
|
|
let claim = vdf_step(&prev, 50);
|
|||
|
|
let bad_claim = [0x99u8; 32];
|
|||
|
|
// Positive + negative checks
|
|||
|
|
assert!(vdf_verify(&prev, 50, &claim));
|
|||
|
|
assert!(!vdf_verify(&prev, 50, &bad_claim));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn vdf_verify_rejects_wrong_iteration_count() {
|
|||
|
|
let prev = [0xAAu8; 32];
|
|||
|
|
let claim = vdf_step(&prev, 100);
|
|||
|
|
assert!(vdf_verify(&prev, 100, &claim));
|
|||
|
|
assert!(!vdf_verify(&prev, 99, &claim));
|
|||
|
|
assert!(!vdf_verify(&prev, 101, &claim));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn vdf_chain_composition() {
|
|||
|
|
// vdf_step(vdf_step(prev, A), B) == vdf_step(prev, A+B)
|
|||
|
|
let prev = [0xBBu8; 32];
|
|||
|
|
let a = 30u64;
|
|||
|
|
let b = 70u64;
|
|||
|
|
let intermediate = vdf_step(&prev, a);
|
|||
|
|
let chained = vdf_step(&intermediate, b);
|
|||
|
|
let direct = vdf_step(&prev, a + b);
|
|||
|
|
assert_eq!(chained, direct, "VDF chain composition non-associative");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- next_d Adaptive D feedback ----------
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn next_d_dead_zone_unchanged() {
|
|||
|
|
let params = genesis_params();
|
|||
|
|
let current_d = 252_000_000u64;
|
|||
|
|
// dead_zone middle (90%) → unchanged
|
|||
|
|
let result = next_d(current_d, 900, params);
|
|||
|
|
assert_eq!(
|
|||
|
|
result, current_d,
|
|||
|
|
"next_d in dead zone должен быть unchanged"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn next_d_above_high_threshold_increases() {
|
|||
|
|
let params = genesis_params();
|
|||
|
|
let current_d = 252_000_000u64;
|
|||
|
|
let result = next_d(current_d, 1000, params); // 100% > high (95%)
|
|||
|
|
assert!(
|
|||
|
|
result > current_d,
|
|||
|
|
"next_d above high threshold должен расти"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn next_d_below_low_threshold_decreases() {
|
|||
|
|
let params = genesis_params();
|
|||
|
|
let current_d = 252_000_000u64;
|
|||
|
|
let result = next_d(current_d, 0, params); // 0% < low (85%)
|
|||
|
|
assert!(
|
|||
|
|
result < current_d,
|
|||
|
|
"next_d below low threshold должен уменьшаться"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn next_d_deterministic() {
|
|||
|
|
let params = genesis_params();
|
|||
|
|
let current_d = 252_000_000u64;
|
|||
|
|
for ratio in [0u32, 500, 850, 900, 950, 1000] {
|
|||
|
|
let a = next_d(current_d, ratio, params);
|
|||
|
|
let b = next_d(current_d, ratio, params);
|
|||
|
|
assert_eq!(a, b, "next_d non-deterministic at ratio {ratio}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- cemented_bundle_aggregate determinism + [I-8] ----------
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn cba_window_zero_returns_genesis_zero() {
|
|||
|
|
let nodes: Vec<NodeId> = vec![[0x11; 32], [0x22; 32]];
|
|||
|
|
assert_eq!(cemented_bundle_aggregate(0, &nodes), [0u8; 32]);
|
|||
|
|
assert_eq!(cemented_bundle_aggregate(1, &nodes), [0u8; 32]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn cba_empty_cemented_returns_empty_marker() {
|
|||
|
|
let result = cemented_bundle_aggregate(100, &[]);
|
|||
|
|
// Must not be all-zeros (genesis case) AND deterministic
|
|||
|
|
assert_ne!(result, [0u8; 32]);
|
|||
|
|
assert_eq!(cemented_bundle_aggregate(100, &[]), result);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn cba_changes_on_window() {
|
|||
|
|
let nodes: Vec<NodeId> = vec![[0x33; 32]];
|
|||
|
|
assert_ne!(
|
|||
|
|
cemented_bundle_aggregate(100, &nodes),
|
|||
|
|
cemented_bundle_aggregate(101, &nodes)
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn cba_changes_on_node_ids() {
|
|||
|
|
let nodes_a: Vec<NodeId> = vec![[0x11; 32]];
|
|||
|
|
let nodes_b: Vec<NodeId> = vec![[0x22; 32]];
|
|||
|
|
assert_ne!(
|
|||
|
|
cemented_bundle_aggregate(100, &nodes_a),
|
|||
|
|
cemented_bundle_aggregate(100, &nodes_b)
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn cba_deterministic() {
|
|||
|
|
let nodes: Vec<NodeId> = vec![[0x11; 32], [0x22; 32], [0x33; 32]];
|
|||
|
|
let a = cemented_bundle_aggregate(500, &nodes);
|
|||
|
|
let b = cemented_bundle_aggregate(500, &nodes);
|
|||
|
|
assert_eq!(a, b);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn cba_canonical_sort_independence_от_порядка_входа() {
|
|||
|
|
// Spec: sorted by node_id asc — input order не должен влиять.
|
|||
|
|
let nodes_forward: Vec<NodeId> = vec![[0x11; 32], [0x22; 32], [0x33; 32]];
|
|||
|
|
let nodes_reverse: Vec<NodeId> = vec![[0x33; 32], [0x22; 32], [0x11; 32]];
|
|||
|
|
assert_eq!(
|
|||
|
|
cemented_bundle_aggregate(500, &nodes_forward),
|
|||
|
|
cemented_bundle_aggregate(500, &nodes_reverse),
|
|||
|
|
"cemented_bundle_aggregate input order должен быть нерелевантен (canonical sort)"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn cba_empty_vs_nonempty_distinct() {
|
|||
|
|
let with_nodes: Vec<NodeId> = vec![[0x11; 32]];
|
|||
|
|
assert_ne!(
|
|||
|
|
cemented_bundle_aggregate(100, &[]),
|
|||
|
|
cemented_bundle_aggregate(100, &with_nodes),
|
|||
|
|
"empty cemented set должен давать разный CBA от non-empty"
|
|||
|
|
);
|
|||
|
|
}
|