montana/Montana-Protocol/Code/crates/mt-timechain/src/lib.rs

348 lines
13 KiB
Rust
Raw Normal View History

// 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<u8> = 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<NodeId> = cemented_node_ids.to_vec();
sorted.sort();
// Concat node_ids + context (window_index) → hash с domain separator
let mut concat: Vec<u8> = 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<NodeId> = vec![];
assert_eq!(cemented_bundle_aggregate(0, &ids), [0u8; 32]);
}
#[test]
fn aggregate_window_one_is_all_zeros() {
let ids: Vec<NodeId> = vec![];
assert_eq!(cemented_bundle_aggregate(1, &ids), [0u8; 32]);
}
#[test]
fn aggregate_empty_set_uses_fallback_with_window() {
let ids: Vec<NodeId> = vec![];
let result = cemented_bundle_aggregate(5, &ids);
let mut w_bytes: Vec<u8> = 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<NodeId> = 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<NodeId> = vec![[0xAAu8; 32]];
let result = cemented_bundle_aggregate(10, &ids);
let concat: Vec<u8> = ids[0].to_vec();
let mut w_bytes: Vec<u8> = 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<NodeId> { 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<NodeId> = vec![[0x01u8; 32], [0x02u8; 32]];
let mutated: Vec<NodeId> = 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<NodeId> = 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<NodeId> = vec![[0xABu8; 32]];
let result = cemented_bundle_aggregate(10, &ids);
let concat: Vec<u8> = ids[0].to_vec();
let mut w_bytes: Vec<u8> = 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<u8> = 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<NodeId> = vec![[0x42u8; 32], [0x43u8; 32]];
let _ = cemented_bundle_aggregate(100, &ids);
}
}