// spec: разделы "Двигатели → TimeChain VDF — осциллятор", "Адаптация D через // participation-ratio feedback", "Cemented bundle aggregate" use mt_codec::{domain, write_u64}; use mt_crypto::{hash, Hash32}; use mt_genesis::ProtocolParams; use mt_state::NodeId; use sha2::{Digest, Sha256}; pub type WindowIndex = u64; // spec: T_r = SHA-256^d(prev). d=0 → no-op (identity). pub fn vdf_step(prev: &Hash32, d: u64) -> Hash32 { let mut current = *prev; for _ in 0..d { current = Sha256::digest(current).into(); } current } pub fn vdf_verify(prev: &Hash32, d: u64, claim: &Hash32) -> bool { &vdf_step(prev, d) == claim } // spec: Adaptive D feedback on τ₂ boundary. Integer form per [I-9]. // median ≥ dead_zone_high (95%) → D × (rate_den + rate_num) / rate_den // median ≤ dead_zone_low (85%) → D × (rate_den - rate_num) / rate_den // else (dead zone) → D unchanged // // median_ratio_permille: 0..=1000 (85% = 850, 95% = 950). Permille для // [I-3]/[I-9] determinism: unsigned integer arithmetic, byte-exact between // implementations. // // Overflow detection через checked_mul: // при D₀ = 252M и max growth 1.03 per τ₂, u64::MAX достигается через // log_1.03(u64::MAX / D₀) ≈ 850 monetary epochs последовательных +3% шагов // (~1.5M лет). Practical horizon недостижим — D флаппает в dead zone. // Halt при overflow = correct behavior (encoded arithmetic horizon), // не attacker-triggered (median_ratio derived канонически из cemented set). pub fn next_d(current_d: u64, median_ratio_permille: u32, params: &ProtocolParams) -> u64 { let low_permille = u32::from(params.participation_dead_zone_low) * 10; let high_permille = u32::from(params.participation_dead_zone_high) * 10; let rate_num = u64::from(params.d_adjustment_rate_num); let rate_den = u64::from(params.d_adjustment_rate_den); if median_ratio_permille >= high_permille { current_d .checked_mul(rate_den + rate_num) .unwrap_or_else(|| { panic!( "next_d overflow при +3% step: current_d = {current_d} × \ (rate_den + rate_num) = {} exceeds u64. Encoded arithmetic \ horizon reached — protocol upgrade required.", rate_den + rate_num ) }) / rate_den } else if median_ratio_permille <= low_permille { // rate_den > rate_num по конструкции (rate_num=3, rate_den=100), вычитание // безопасно. checked_mul защищает от overflow при крупном D. current_d .checked_mul(rate_den - rate_num) .unwrap_or_else(|| { panic!( "next_d overflow при -3% step: current_d = {current_d} × \ (rate_den - rate_num) = {} exceeds u64.", rate_den - rate_num ) }) / rate_den } else { current_d } } // spec: cemented_bundle_aggregate(W) — Правило R3 (aggregate over signer_node_id, не signatures). // Три ветви: // W < 2 → 0x00 × 32 (Genesis) // |cemented| == 0 → SHA-256("mt-bc-aggregate-empty" || W) (fallback) // иначе → SHA-256("mt-bc-aggregate" || concat(sorted node_ids) || W) // // Aggregate строится только над node_ids cemented confirmers + context window_index. // Signatures и content (op_hashes[]/reveal_hashes[]) ИСКЛЮЧЕНЫ из input: // - Ноль grinding surface через σ (signature исключена из hash input; // инвариант сохраняется при любой схеме подписи независимо от // deterministic/randomized свойств) // - Ноль grinding surface через op_hashes[] subset selection confirmer-ом // - [I-8] binding сохранён через quorum emergence S_W pub fn cemented_bundle_aggregate(window: WindowIndex, cemented_node_ids: &[NodeId]) -> Hash32 { let mut w_bytes: Vec = Vec::with_capacity(8); write_u64(&mut w_bytes, window); if window < 2 { return [0u8; 32]; } if cemented_node_ids.is_empty() { return hash(domain::BC_AGGREGATE_EMPTY, &[&w_bytes]); } // Sorted by node_id asc (детерминизм; node_id канонически commit-нут в NodeTable) let mut sorted: Vec = cemented_node_ids.to_vec(); sorted.sort(); // Concat node_ids + context (window_index) → hash с domain separator let mut concat: Vec = Vec::with_capacity(sorted.len() * 32); for id in &sorted { concat.extend_from_slice(id); } hash(domain::BC_AGGREGATE, &[&concat, &w_bytes]) } #[cfg(test)] mod tests { use super::*; // ============ VDF ============ #[test] fn vdf_step_d_zero_is_identity() { let prev = [0xABu8; 32]; assert_eq!(vdf_step(&prev, 0), prev); } #[test] fn vdf_step_d_one_is_single_sha256() { let prev = [0xABu8; 32]; let expected: Hash32 = Sha256::digest(prev).into(); assert_eq!(vdf_step(&prev, 1), expected); } #[test] fn vdf_step_d_ten_chains_ten_hashes() { let prev = [0x01u8; 32]; let result = vdf_step(&prev, 10); // Вычислим вручную 10 итераций и сравним let mut manual: Hash32 = prev; for _ in 0..10 { manual = Sha256::digest(manual).into(); } assert_eq!(result, manual); } #[test] fn vdf_step_deterministic() { let prev = [0xCDu8; 32]; let a = vdf_step(&prev, 100); let b = vdf_step(&prev, 100); assert_eq!(a, b); } #[test] fn vdf_verify_roundtrip() { let prev = [0xEFu8; 32]; let claim = vdf_step(&prev, 50); assert!(vdf_verify(&prev, 50, &claim)); } #[test] fn vdf_verify_rejects_mutated_claim() { let prev = [0x11u8; 32]; let mut claim = vdf_step(&prev, 25); claim[0] ^= 0xFF; assert!(!vdf_verify(&prev, 25, &claim)); } #[test] fn vdf_verify_rejects_wrong_d() { let prev = [0x22u8; 32]; let claim = vdf_step(&prev, 10); assert!(!vdf_verify(&prev, 9, &claim)); assert!(!vdf_verify(&prev, 11, &claim)); } // ============ Adaptive D ============ #[test] fn next_d_median_at_or_above_high_increases() { let params = mt_genesis::genesis_params(); let d = 252_000_000u64; // 950 permille = 95% ровно = boundary high assert_eq!(next_d(d, 950, params), d * 103 / 100); // 1000 = 100% assert_eq!(next_d(d, 1000, params), d * 103 / 100); // 980 assert_eq!(next_d(d, 980, params), d * 103 / 100); } #[test] fn next_d_median_at_or_below_low_decreases() { let params = mt_genesis::genesis_params(); let d = 252_000_000u64; // 850 permille = 85% = boundary low assert_eq!(next_d(d, 850, params), d * 97 / 100); // 0 assert_eq!(next_d(d, 0, params), d * 97 / 100); // 700 assert_eq!(next_d(d, 700, params), d * 97 / 100); } #[test] fn next_d_dead_zone_unchanged() { let params = mt_genesis::genesis_params(); let d = 252_000_000u64; // 900 permille = 90% = середина dead zone assert_eq!(next_d(d, 900, params), d); // 851 = чуть выше low assert_eq!(next_d(d, 851, params), d); // 949 = чуть ниже high assert_eq!(next_d(d, 949, params), d); } #[test] fn next_d_boundary_850_is_decrease() { // spec: "<=" означает включительное сравнение с low let params = mt_genesis::genesis_params(); let d = 252_000_000u64; assert_eq!(next_d(d, 850, params), d * 97 / 100); } #[test] fn next_d_boundary_950_is_increase() { // spec: ">=" означает включительное сравнение с high let params = mt_genesis::genesis_params(); let d = 252_000_000u64; assert_eq!(next_d(d, 950, params), d * 103 / 100); } #[test] fn next_d_precision_on_real_d0() { // Реальное значение D₀ должно точно умножиться на 103/100 let params = mt_genesis::genesis_params(); let d = 252_000_000u64; let increased = next_d(d, 1000, params); assert_eq!(increased, 252_000_000 * 103 / 100); assert_eq!(increased, 259_560_000); let decreased = next_d(d, 0, params); assert_eq!(decreased, 252_000_000 * 97 / 100); assert_eq!(decreased, 244_440_000); } // ============ cemented_bundle_aggregate (SSI R3: aggregate over node_ids) ============ #[test] fn aggregate_window_zero_is_all_zeros() { let ids: Vec = vec![]; assert_eq!(cemented_bundle_aggregate(0, &ids), [0u8; 32]); } #[test] fn aggregate_window_one_is_all_zeros() { let ids: Vec = vec![]; assert_eq!(cemented_bundle_aggregate(1, &ids), [0u8; 32]); } #[test] fn aggregate_empty_set_uses_fallback_with_window() { let ids: Vec = vec![]; let result = cemented_bundle_aggregate(5, &ids); let mut w_bytes: Vec = Vec::with_capacity(8); write_u64(&mut w_bytes, 5u64); let expected = hash(domain::BC_AGGREGATE_EMPTY, &[&w_bytes]); assert_eq!(result, expected); } #[test] fn aggregate_empty_fallback_depends_on_window() { let ids: Vec = vec![]; let r5 = cemented_bundle_aggregate(5, &ids); let r6 = cemented_bundle_aggregate(6, &ids); assert_ne!(r5, r6); } #[test] fn aggregate_single_node_id() { let ids: Vec = vec![[0xAAu8; 32]]; let result = cemented_bundle_aggregate(10, &ids); let concat: Vec = ids[0].to_vec(); let mut w_bytes: Vec = Vec::with_capacity(8); write_u64(&mut w_bytes, 10u64); let expected = hash(domain::BC_AGGREGATE, &[&concat, &w_bytes]); assert_eq!(result, expected); } #[test] fn aggregate_order_independent() { let mk = |order: [u8; 3]| -> Vec { order.iter().map(|b| [*b; 32]).collect() }; let r1 = cemented_bundle_aggregate(10, &mk([0x01, 0x02, 0x03])); let r2 = cemented_bundle_aggregate(10, &mk([0x03, 0x01, 0x02])); let r3 = cemented_bundle_aggregate(10, &mk([0x02, 0x03, 0x01])); assert_eq!(r1, r2); assert_eq!(r2, r3); } #[test] fn aggregate_detects_node_id_change() { let original: Vec = vec![[0x01u8; 32], [0x02u8; 32]]; let mutated: Vec = vec![[0x01u8; 32], [0x03u8; 32]]; // изменён второй let r1 = cemented_bundle_aggregate(10, &original); let r2 = cemented_bundle_aggregate(10, &mutated); assert_ne!(r1, r2); } #[test] fn aggregate_depends_on_window_in_non_empty_branch() { // SSI R3: context (W) включается в input hash всегда, не только в empty-ветви. // Grinding resistance: разные окна дают разный aggregate даже при identical S_W. let ids: Vec = vec![[0x01u8; 32]]; let r5 = cemented_bundle_aggregate(5, &ids); let r100 = cemented_bundle_aggregate(100, &ids); assert_ne!(r5, r100); } #[test] fn aggregate_uses_bc_aggregate_domain() { let ids: Vec = vec![[0xABu8; 32]]; let result = cemented_bundle_aggregate(10, &ids); let concat: Vec = ids[0].to_vec(); let mut w_bytes: Vec = Vec::with_capacity(8); write_u64(&mut w_bytes, 10u64); let expected = hash(domain::BC_AGGREGATE, &[&concat, &w_bytes]); assert_eq!(result, expected); } #[test] fn aggregate_uses_empty_domain_for_empty_set() { let result = cemented_bundle_aggregate(10, &[]); let mut w_bytes: Vec = Vec::with_capacity(8); write_u64(&mut w_bytes, 10u64); let expected = hash(domain::BC_AGGREGATE_EMPTY, &[&w_bytes]); assert_eq!(result, expected); } #[test] fn aggregate_independent_of_signature_type() { // Positive test for SSI R3: cemented_bundle_aggregate API больше не принимает // Signature — grinding surface через σ устранена конструктивно (signature // физически не может попасть в input hash, отсутствует в type сигнатуре функции). // Test проверяет компилируемость API только над &[NodeId]. let ids: Vec = vec![[0x42u8; 32], [0x43u8; 32]]; let _ = cemented_bundle_aggregate(100, &ids); } }