// spec, разделы "BundledConfirmation", "VDF Reveal и лотерея", "Signed scope, identity и aggregation" (Правила R1, R2) use mt_codec::{domain, write_bytes, write_u16, write_u64, CanonicalEncode}; use mt_crypto::{ hash, suite_id_from_u16, verify, Hash32, PublicKey, Signature, SuiteId, SIGNATURE_SIZE, }; use mt_state::{NodeId, NodeTable}; // spec v33.1.5+: window_index унифицирован на u64 (8B LE) во всех M4 структурах // (BundledConfirmation, VdfReveal, ProposalHeader). Старый u32 (4B) убран как // architectural smell — единый тип для одного концептуально-единого field // устраняет cross-struct cast и расхождение в hash composition. pub const BUNDLE_FIXED_OVERHEAD: usize = 32 + 32 + 8 + 2 + 2 + SIGNATURE_SIZE; pub const REVEAL_SIZE: usize = 32 + 8 + 32 + SIGNATURE_SIZE; // Early-warning threshold для bundle hash counts (closure M-M4-1 partial, // per внешний аудит claude-opus-4-7-1m_2026-04-28_T2023). // // u16 length prefix даёт hard cap 65 535 hashes; на 1B пользователей при // ~1000 узлах одно окно может приближаться к этому пределу. Threshold = 50% // (32 768) — early signal для оператора / mt-telemetry чтобы M6+ spec-patch // u16→u32 был запланирован ДО реального достижения hard reject. // // Lib остаётся silent (no eprintln, [I-3] / [C-6] cleanliness): caller // (montana-node либо mt-telemetry F-5) применяет should_warn_*_count() в // собственном logging path. validate_bundle продолжает hard reject ровно на // u16::MAX — early-warning не меняет consensus semantics. pub const HASH_COUNT_EARLY_WARNING_THRESHOLD: usize = 32_768; #[inline] pub fn should_warn_op_hashes_count(count: usize) -> bool { count >= HASH_COUNT_EARLY_WARNING_THRESHOLD } #[inline] pub fn should_warn_reveal_hashes_count(count: usize) -> bool { count >= HASH_COUNT_EARLY_WARNING_THRESHOLD } #[derive(Clone, Debug, Eq, PartialEq)] pub struct BundledConfirmation { pub node_id: NodeId, pub endpoint: Hash32, pub window_index: u64, pub op_hashes: Vec, pub reveal_hashes: Vec, pub signature: Signature, } impl BundledConfirmation { // spec: Правило R1 — signed_scope = canonical_bytes без поля signature (last SIGNATURE_SIZE bytes) // // Wire format: длины op_hashes/reveal_hashes как u16 LE prefixes (per spec). // Caller responsibility: validate_bundle проверяет op_hashes.len() ≤ u16::MAX // ДО encode (M4-1 closure). debug_assert catch silent truncation если caller // обходит validate_bundle в debug builds; в release полагается на validate // gating (signature verify косвенно catches encode mismatch). pub fn encode_signed_scope(&self, buf: &mut Vec) { debug_assert!( self.op_hashes.len() <= u16::MAX as usize, "BundledConfirmation.op_hashes.len() = {} > u16::MAX; \ caller обязан validate_bundle ДО encode (M4-1 invariant)", self.op_hashes.len() ); debug_assert!( self.reveal_hashes.len() <= u16::MAX as usize, "BundledConfirmation.reveal_hashes.len() = {} > u16::MAX; \ caller обязан validate_bundle ДО encode (M4-1 invariant)", self.reveal_hashes.len() ); write_bytes(buf, &self.node_id); write_bytes(buf, &self.endpoint); write_u64(buf, self.window_index); write_u16(buf, self.op_hashes.len() as u16); for h in &self.op_hashes { write_bytes(buf, h); } write_u16(buf, self.reveal_hashes.len() as u16); for h in &self.reveal_hashes { write_bytes(buf, h); } } } impl CanonicalEncode for BundledConfirmation { fn encode(&self, buf: &mut Vec) { self.encode_signed_scope(buf); write_bytes(buf, self.signature.as_bytes()); } } // spec: Правило R2 — bundle_hash = SHA-256("mt-bundle" || signed_scope(bundle)) pub fn bundle_hash(bc: &BundledConfirmation) -> Hash32 { let mut scope = Vec::new(); bc.encode_signed_scope(&mut scope); hash(domain::BUNDLE, &[&scope]) } #[derive(Clone, Debug, Eq, PartialEq)] pub enum BundleError { UnknownNode, UnsupportedSuite, OpsOutOfOrder, RevealsOutOfOrder, WrongEndpoint, InvalidSignature, // M4-1 closure: encode_signed_scope использует write_u16 для длины // op_hashes/reveal_hashes (spec wire format). При len > u16::MAX = 65535 // cast `as u16` молча truncates → silent encode/decode mismatch. Защита // через signature verify косвенная (mismatch → wrong scope hash → fail); // explicit invariant ловит это до encode/sign и даёт чёткий error path. TooManyOps, TooManyReveals, } // spec: "op_hashes[] ascending lexicographic" + "reveal_hashes[] ascending lexicographic" fn is_strictly_ascending(items: &[Hash32]) -> bool { items.windows(2).all(|w| w[0] < w[1]) } // spec, раздел "BundledConfirmation" — валидация перед inclusion в cemented set. // Проверки: (a) node зарегистрирован и подписывает ML-DSA-65, // (b) endpoint == T_r текущего окна (caller вычисляет canonical T_r), // (c) op_hashes[] / reveal_hashes[] строго возрастают, // (d) signature верифицируется против NodeTable[node_id].node_pubkey над signed_scope. pub fn validate_bundle( bc: &BundledConfirmation, node_table: &NodeTable, expected_endpoint: &Hash32, ) -> Result<(), BundleError> { let node = node_table .get(&bc.node_id) .ok_or(BundleError::UnknownNode)?; match suite_id_from_u16(node.suite_id) { Some(SuiteId::Mldsa65) => {}, None => return Err(BundleError::UnsupportedSuite), } if bc.endpoint != *expected_endpoint { return Err(BundleError::WrongEndpoint); } // M4-1 closure: explicit caps на длину Vec'ов перед encode (write_u16 cast). // Защита от silent truncation; reject ДО signature verify чтобы не тратить // ML-DSA-65 verify cycles на guaranteed-broken bundle. // M4-1 hard limit: wire format кодирует длину как u16 LE → ≤ 65535 hashes. // SCALE NOTE (M6+ scaling concern): на 1B активных пользователей при // ~1000 узлах одно окно может содержать > 100K операций per node → // bundle с op_hashes > 65535 не поместится в текущий wire format. // Эскалация требует spec-patch на u16→u32 length prefix. if bc.op_hashes.len() > u16::MAX as usize { return Err(BundleError::TooManyOps); } if bc.reveal_hashes.len() > u16::MAX as usize { return Err(BundleError::TooManyReveals); } if !is_strictly_ascending(&bc.op_hashes) { return Err(BundleError::OpsOutOfOrder); } if !is_strictly_ascending(&bc.reveal_hashes) { return Err(BundleError::RevealsOutOfOrder); } let mut scope = Vec::new(); bc.encode_signed_scope(&mut scope); let pk = PublicKey::from_array(node.node_pubkey); if !verify(&pk, &scope, &bc.signature) { return Err(BundleError::InvalidSignature); } Ok(()) } // spec, раздел "VDF Reveal и лотерея" (строки 920-928) #[derive(Clone, Debug, Eq, PartialEq)] pub struct VdfReveal { pub node_id: NodeId, pub window_index: u64, pub endpoint: Hash32, pub signature: Signature, } impl VdfReveal { // spec: Правило R1 — signed_scope = canonical_bytes без поля signature (last SIGNATURE_SIZE bytes) pub fn encode_signed_scope(&self, buf: &mut Vec) { write_bytes(buf, &self.node_id); write_u64(buf, self.window_index); write_bytes(buf, &self.endpoint); } } impl CanonicalEncode for VdfReveal { fn encode(&self, buf: &mut Vec) { self.encode_signed_scope(buf); write_bytes(buf, self.signature.as_bytes()); } } // spec: Правило R2 — reveal_hash = SHA-256("mt-vdf-reveal" || signed_scope(reveal)) pub fn reveal_hash(reveal: &VdfReveal) -> Hash32 { let mut scope = Vec::new(); reveal.encode_signed_scope(&mut scope); hash(domain::VDF_REVEAL, &[&scope]) } // spec, "VDF Reveal и лотерея" — endpoint formula: // endpoint_node(W) = SHA-256("mt-lottery" || T_r(W) || cemented_bundle_aggregate(W-2) || node_id || window_index) // window_index encoded as u64 LE (8B) consistent с layout field (spec v33.1.5+ unified). pub fn compute_endpoint( t_r: &Hash32, cemented_bundle_aggregate_w_minus_2: &Hash32, node_id: &NodeId, window_index: u64, ) -> Hash32 { let mut w_le = Vec::with_capacity(8); write_u64(&mut w_le, window_index); hash( domain::LOTTERY, &[t_r, cemented_bundle_aggregate_w_minus_2, node_id, &w_le], ) } #[derive(Clone, Debug, Eq, PartialEq)] pub enum RevealError { UnknownNode, UnsupportedSuite, WrongWindow, WrongEndpoint, InvalidSignature, } // spec, "Валидация VDF_Reveal" (строки 1020-1026) — правила 1, 2, 3, 5. // Правило 4 (weighted_ticket < target) — в Phase C (Node lottery). pub fn validate_reveal( reveal: &VdfReveal, node_table: &NodeTable, t_r: &Hash32, cemented_bundle_aggregate_w_minus_2: &Hash32, current_window: u64, ) -> Result<(), RevealError> { let node = node_table .get(&reveal.node_id) .ok_or(RevealError::UnknownNode)?; match suite_id_from_u16(node.suite_id) { Some(SuiteId::Mldsa65) => {}, None => return Err(RevealError::UnsupportedSuite), } if reveal.window_index != current_window { return Err(RevealError::WrongWindow); } let expected_endpoint = compute_endpoint( t_r, cemented_bundle_aggregate_w_minus_2, &reveal.node_id, reveal.window_index, ); if reveal.endpoint != expected_endpoint { return Err(RevealError::WrongEndpoint); } let mut scope = Vec::new(); reveal.encode_signed_scope(&mut scope); let pk = PublicKey::from_array(node.node_pubkey); if !verify(&pk, &scope, &reveal.signature) { return Err(RevealError::InvalidSignature); } Ok(()) } // ============ Phase C: Node lottery (per [I-9]) ============ // spec, раздел "Класс 1: узлы" + "Integer log algorithm (per [I-9])". // Conformance status: closed (binding coefficients B0..B3 + 5 test vectors в спеке). // spec: seniority_term = min(chain_length / 13, chain_length_snapshot). // Целочисленное деление unsigned u64 (truncation toward zero): chain_length < 13 // ⇒ seniority_term = 0 (первые 13 окон после регистрации). // Делитель 13 — derivation: target T_cap = 3 × T_year ≈ 1 577 880 окон, // snapshot_max = 6τ₂ = 120 960, divisor = 1 577 880 / 120 960 ≈ 13. // Structural reuse: совпадает с EMISSION_moneta = 13 Ɉ per window. pub fn seniority_term(chain_length: u64, chain_length_snapshot: u64) -> u64 { (chain_length / 13).min(chain_length_snapshot) } // spec: lottery_weight = chain_length_snapshot + seniority_term. // Инвариант DS-2: при chain_length_snapshot ≥ 1 (что гарантировано для active // узлов через pruning/active_predicate chain) → lottery_weight ≥ 1. // Overflow: snapshot ≤ 120960 (6τ₂), seniority ≤ snapshot, sum ≤ 241920 ⇒ safe u64. pub fn lottery_weight(chain_length: u64, chain_length_snapshot: u64) -> u64 { chain_length_snapshot + seniority_term(chain_length, chain_length_snapshot) } // LN(2) в Q64.64: ln(2) × 2^64 ≈ 12786308645202655659 (truncated toward zero). // spec binding constant LN2_Q64. const LN2_Q64: u128 = 0xB172_17F7_D1CF_79AB; // spec binding coefficients (halved polynomial form, все unsigned u64 Q64). // p(y) = log2(1+y) × 2^64 ≈ (B0 + y·(B1 - y·(B2_ABS - y·B3))) << 1 // a1 > 1 не поместился бы в u64 при полном Q64 — halved form обходит через // division by 2 и финальный left shift. const B0: u64 = 0x0014_E086_EC98_2D63; const B1: u64 = 0xB59D_DDE5_2A69_D000; const B2_ABS: u64 = 0x49DF_5C3B_FD9C_EC00; const B3: u64 = 0x1441_7E56_D333_1800; // spec: log2_q64(endpoint) → Q64.64 u128 representation of log2(2^256 / endpoint). // Monotonic: меньший endpoint → больший log2_q64. // endpoint == 0 (вероятность SHA-collision) клипируется до u128::MAX. // // Binding: degree-3 Remez minimax polynomial, максимальная ошибка 2^-10.62. // [I-8] reconciliation: approximation error grinding advantage bounded через // cemented_bundle_aggregate(W-2) в endpoint formula — см. спеку «Integer log // algorithm» раздел «[I-8] reconciliation». pub fn log2_q64(endpoint: &Hash32) -> u128 { // Count leading zeros of u256 big-endian. let mut leading: u32 = 0; let mut all_zero = true; for b in endpoint.iter() { if *b == 0 { leading += 8; } else { leading += b.leading_zeros(); all_zero = false; break; } } if all_zero { // endpoint == 0, log2(∞), saturate return u128::MAX; } // int_part = leading_zeros = log2(2^256 / endpoint) integer part (floor). let int_part = leading as u128; // Extract u128 mantissa in [2^127, 2^128) from big-endian u256. // MSB position in [0, 255] = 255 - leading. // Split endpoint в две u128 halves (high = bits 128..255, low = bits 0..127). // M4-LOW-3 closure: unwrap_or([0; 16]) вместо expect — структурно // unreachable (slice [0..16] от [u8; 32] всегда длиной 16, try_into для // [u8; 16] не fails), но absolute panic-free guarantee предпочтительнее // controlled panic при protocol invariant breach. На impossible path // fallback даёт all-zeros half, log2 returns deterministic 0 (вместо // unrecoverable panic). External audit T141253 [LOW]. let mut hi_bytes = [0u8; 16]; hi_bytes.copy_from_slice(&endpoint[0..16]); let e_hi = u128::from_be_bytes(hi_bytes); let mut lo_bytes = [0u8; 16]; lo_bytes.copy_from_slice(&endpoint[16..32]); let e_lo = u128::from_be_bytes(lo_bytes); let msb_position: u32 = 255 - leading; // ∈ [0, 255] let mantissa: u128 = if msb_position >= 128 { // e_hi != 0. Right-shift u256 by (msb_position - 127) ∈ [1, 128]. let shift = msb_position - 127; if shift < 128 { (e_hi << (128 - shift)) | (e_lo >> shift) } else { // shift == 128: low 128 bits of (e_hi:e_lo >> 128) = e_hi e_hi } } else { // e_hi == 0. msb in e_lo at bit msb_position ≤ 127. // Left-shift e_lo by (127 - msb_position) ∈ [0, 127]. let shift = 127 - msb_position; e_lo << shift }; // x_q64 = y × 2^64 где y = mantissa/2^127 - 1 ∈ [0, 1). let x_q64 = ((mantissa - (1u128 << 127)) >> 63) as u64; // Unsigned Horner evaluation half_p(y) = B0 + y·(B1 - y·(B2_ABS - y·B3)). // Все intermediate shifts u64; intermediate invariants доказаны non-negative в спеке. let t1 = (((B3 as u128) * (x_q64 as u128)) >> 64) as u64; // y·B3 ≤ B3 < B2_ABS debug_assert!(t1 <= B2_ABS); let t2 = B2_ABS - t1; let t3 = (((t2 as u128) * (x_q64 as u128)) >> 64) as u64; debug_assert!(t3 <= B1); let t4 = B1 - t3; let t5 = (((t4 as u128) * (x_q64 as u128)) >> 64) as u64; let half_p = B0 + t5; // < 2^63 + ε let frac_q64 = (half_p as u128) << 1; // ≈ log2(1+y) × 2^64 // log2(2^256/e) = (leading + 1) - log2(1+y). При y→1 minimax approximation // может незначительно превысить 1 (minimax equal positive/negative errors) — // saturating_sub обеспечивает результат ≥ 0 (log2 от числа ≥ 1 не отрицательно). ((int_part + 1) << 64).saturating_sub(frac_q64) } // spec: ticket = ln_q64(endpoint) = -ln(endpoint/2^256) × 2^64. // Computed as log2_q64 × LN2_Q64 / 2^64. // Monotonic: меньший endpoint → больший ln_q64. pub fn ln_q64(endpoint: &Hash32) -> u128 { let log2 = log2_q64(endpoint); if log2 == u128::MAX { return u128::MAX; // endpoint == 0 saturates through } // log2 < 2^72 (256 × 2^64 bound); LN2 < 2^64. // Product fits in u192; need >> 64. Split log2 в high/low 64-bit halves. let log2_high = (log2 >> 64) as u64; let log2_low = log2 as u64; // term_high = log2_high × LN2 (u64 × u128 → u128, LN2 < 2^64 ⇒ safe) let term_high = (log2_high as u128) * LN2_Q64; // term_low = (log2_low × LN2) >> 64 let term_low = ((log2_low as u128) * LN2_Q64) >> 64; term_high.saturating_add(term_low) } // spec, раздел "Класс 1: узлы" integer form. // weighted_ticket_node = ln_q64(endpoint) / (lottery_weight as u128). // Precondition: chain_length_snapshot ≥ 1 (DS-2 invariant; caller enforces). // Integer division toward zero (unsigned u128 / u128). pub fn weighted_ticket_node( endpoint: &Hash32, chain_length: u64, chain_length_snapshot: u64, ) -> u128 { let w = lottery_weight(chain_length, chain_length_snapshot); // DS-2 gate: w ≥ 1. Если нарушено — protocol violation (spec строка 924). // Здесь защищаемся от panic: при w == 0 возвращаем u128::MAX (неверный // winner in argmin, но не crash — caller должен validate до вызова). if w == 0 { return u128::MAX; } ln_q64(endpoint) / (w as u128) } // ============ Phase D: Account lottery УДАЛЕНА ============ // // spec Sovereignty Ladder: лотерея single-class. compute_account_endpoint, // weighted_ticket_account, AccountLotteryError, validate_account_participation, // domain `mt-account-lottery`, 4 binding test vectors A1-A4 удалены. // ============ Phase E: Winner determination (argmin) ============ // spec, раздел "Определение winner-а (Lookback Leadership)" (строки ~1017-1056): // winner_{W-1} = argmin(weighted_ticket) среди cemented VDF_Reveal nodes + accounts. // // [C-1] SSOT: единый источник WINNER_CLASS_NODE — mt-state. // Лотерея single-class: только узлы (spec Sovereignty Ladder); // account lottery удалена, значение `2` зарезервировано для будущих расширений. pub use mt_state::WINNER_CLASS_NODE; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Candidate { pub ticket: u128, // weighted_ticket Q64.64 (u128) pub class: u8, // WINNER_CLASS_NODE (единственное valid значение текущей схемы) pub id: [u8; 32], // node_id } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Winner { pub class: u8, pub id: [u8; 32], pub ticket: u128, } // spec, "argmin(weighted_ticket)" — минимизация по основному ключу. // Tie-breaking (spec ambiguity, canonical rule введён здесь): // 1. ticket ascending // 2. class ascending (Node=1 < Account=2) — node preference при tie // 3. id lex ascending (32-byte byte-wise compare) // Probability ticket-tie ~ 2^-128; правило нужно для byte-exact determinism. pub fn determine_winner(candidates: &[Candidate]) -> Option { candidates .iter() .min_by(|a, b| { a.ticket .cmp(&b.ticket) .then_with(|| a.class.cmp(&b.class)) .then_with(|| a.id.cmp(&b.id)) }) .map(|c| Winner { class: c.class, id: c.id, ticket: c.ticket, }) } // spec, раздел "Определение winner-а" + "fallback cascade" (строка 1052): // fallback_proposer_W = second_min, third_min, etc. // Возвращает sorted vector кандидатов по той же canonical rule. // Caller берёт [0] для winner, [1] для fallback_1, и т.д. pub fn sorted_candidates_for_fallback(candidates: &[Candidate]) -> Vec { let mut sorted: Vec = candidates.to_vec(); sorted.sort_by(|a, b| { a.ticket .cmp(&b.ticket) .then_with(|| a.class.cmp(&b.class)) .then_with(|| a.id.cmp(&b.id)) }); sorted } // ============ Phase F: Quorum calculation ============ // spec, раздел "Confirmation cutoff" (строка 1433 + integer form): // quorum(W) = ⌈0.67 × active_chain_length(W)⌉ (real-valued commentary) // quorum(W) = (67 × active_chain_length + 99) / 100 (integer, authoritative [I-9]) // Unsigned u64. spec bound active ≤ 10^14: 67 × 10^14 + 99 < 2^63 — safe. // // M4-LOW-5 closure: saturating_mul/saturating_add защищают от u64::MAX // overflow (active_chain_length > u64::MAX/67 ≈ 2.7×10^17, нерелевантно // практически но defense-in-depth: на overflow возвращаем graceful // u64::MAX/100 вместо silent wrap либо panic в debug build). pub fn quorum(active_chain_length: u64) -> u64 { let scaled = 67u64.saturating_mul(active_chain_length); scaled.saturating_add(99) / 100 } // spec: объект cemented когда cemented_sum ≥ quorum(W). pub fn is_cemented(cemented_sum: u64, active_chain_length: u64) -> bool { cemented_sum >= quorum(active_chain_length) } #[cfg(test)] mod tests { use super::*; use mt_crypto::{keypair, sign, SECRET_KEY_SIZE, SIGNATURE_SIZE}; use mt_state::{derive_node_id, NodeRecord}; #[test] fn early_warning_threshold_is_half_u16_max() { // 32 768 = ровно 50% от u16::MAX (65 535) + 1; даёт ~33K headroom // оператору перед hard reject в validate_bundle. assert_eq!(HASH_COUNT_EARLY_WARNING_THRESHOLD, 32_768); assert!(HASH_COUNT_EARLY_WARNING_THRESHOLD < u16::MAX as usize); } #[test] fn warn_helpers_trigger_at_threshold_boundary() { assert!(!should_warn_op_hashes_count(0)); assert!(!should_warn_op_hashes_count( HASH_COUNT_EARLY_WARNING_THRESHOLD - 1 )); assert!(should_warn_op_hashes_count( HASH_COUNT_EARLY_WARNING_THRESHOLD )); assert!(should_warn_op_hashes_count(u16::MAX as usize)); assert!(!should_warn_reveal_hashes_count(0)); assert!(!should_warn_reveal_hashes_count( HASH_COUNT_EARLY_WARNING_THRESHOLD - 1 )); assert!(should_warn_reveal_hashes_count( HASH_COUNT_EARLY_WARNING_THRESHOLD )); } fn make_node( pubkey: [u8; mt_crypto::PUBLIC_KEY_SIZE], start_window: u64, ) -> (NodeId, NodeRecord) { let node_id = derive_node_id(&pubkey); let rec = NodeRecord { node_id, node_pubkey: pubkey, suite_id: SuiteId::Mldsa65 as u16, operator_account_id: [0x11; 32], start_window, chain_length: 1, chain_length_snapshot: 1, chain_length_checkpoints: [1; 6], last_confirmation_window: start_window, }; (node_id, rec) } fn build_signed_bc( sk: &mt_crypto::SecretKey, node_id: NodeId, endpoint: Hash32, window_index: u64, op_hashes: Vec, reveal_hashes: Vec, ) -> BundledConfirmation { let mut bc = BundledConfirmation { node_id, endpoint, window_index, op_hashes, reveal_hashes, signature: Signature::from_array([0u8; SIGNATURE_SIZE]), }; let mut scope = Vec::new(); bc.encode_signed_scope(&mut scope); bc.signature = sign(sk, &scope).expect("sign BundledConfirmation scope"); bc } #[test] fn encode_matches_spec_layout() { let bc = BundledConfirmation { node_id: [0xAA; 32], endpoint: [0xBB; 32], window_index: 0x0102030405060708, op_hashes: vec![[0x11; 32], [0x22; 32]], reveal_hashes: vec![[0x33; 32]], signature: Signature::from_array([0x44; SIGNATURE_SIZE]), }; let mut buf = Vec::new(); bc.encode(&mut buf); let expected_size = BUNDLE_FIXED_OVERHEAD + 32 * 2 + 32; assert_eq!(buf.len(), expected_size); // node_id: 0..32 assert_eq!(&buf[0..32], &[0xAA; 32]); // endpoint: 32..64 assert_eq!(&buf[32..64], &[0xBB; 32]); // window_index u64 LE: 64..72 (spec v33.1.5+ — было u32 4B до v33.1.5) assert_eq!( &buf[64..72], &[0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01] ); // op_count LE: 72..74 = 2 assert_eq!(&buf[72..74], &[0x02, 0x00]); // op_hashes: 74..106 (first), 106..138 (second) assert_eq!(&buf[74..106], &[0x11; 32]); assert_eq!(&buf[106..138], &[0x22; 32]); // reveal_count LE: 138..140 = 1 assert_eq!(&buf[138..140], &[0x01, 0x00]); // reveal_hashes: 140..172 assert_eq!(&buf[140..172], &[0x33; 32]); // signature: last SIGNATURE_SIZE bytes (3309 для ML-DSA-65) assert_eq!(&buf[172..172 + SIGNATURE_SIZE], &[0x44; SIGNATURE_SIZE]); } #[test] fn encode_empty_bundle_fixed_overhead() { let bc = BundledConfirmation { node_id: [0; 32], endpoint: [0; 32], window_index: 0, op_hashes: vec![], reveal_hashes: vec![], signature: Signature::from_array([0u8; SIGNATURE_SIZE]), }; let mut buf = Vec::new(); bc.encode(&mut buf); assert_eq!(buf.len(), BUNDLE_FIXED_OVERHEAD); } #[test] fn signed_scope_excludes_signature() { let bc = BundledConfirmation { node_id: [0xAA; 32], endpoint: [0xBB; 32], window_index: 7, op_hashes: vec![[0x01; 32]], reveal_hashes: vec![], signature: Signature::from_array([0xCC; SIGNATURE_SIZE]), }; let mut scope = Vec::new(); bc.encode_signed_scope(&mut scope); let mut full = Vec::new(); bc.encode(&mut full); assert_eq!(full.len(), scope.len() + SIGNATURE_SIZE); assert_eq!(&full[..scope.len()], scope.as_slice()); assert_eq!(&full[scope.len()..], &[0xCC; SIGNATURE_SIZE]); } #[test] fn signed_scope_same_for_different_signatures() { // R2 свойство: identifier стабилен независимо от схемы подписи. let mut bc1 = BundledConfirmation { node_id: [0xAA; 32], endpoint: [0xBB; 32], window_index: 7, op_hashes: vec![], reveal_hashes: vec![], signature: Signature::from_array([0x00; SIGNATURE_SIZE]), }; let mut scope1 = Vec::new(); bc1.encode_signed_scope(&mut scope1); bc1.signature = Signature::from_array([0xFF; SIGNATURE_SIZE]); let mut scope2 = Vec::new(); bc1.encode_signed_scope(&mut scope2); assert_eq!(scope1, scope2); } #[test] fn bundle_hash_domain_mt_bundle() { let bc = BundledConfirmation { node_id: [0x01; 32], endpoint: [0x02; 32], window_index: 1, op_hashes: vec![], reveal_hashes: vec![], signature: Signature::from_array([0x00; SIGNATURE_SIZE]), }; let mut scope = Vec::new(); bc.encode_signed_scope(&mut scope); let expected = hash(b"mt-bundle", &[&scope]); assert_eq!(bundle_hash(&bc), expected); } #[test] fn bundle_hash_stable_across_resign() { // R2: identifier не зависит от signature let mut bc = BundledConfirmation { node_id: [0x01; 32], endpoint: [0x02; 32], window_index: 1, op_hashes: vec![[0xAB; 32]], reveal_hashes: vec![], signature: Signature::from_array([0x00; SIGNATURE_SIZE]), }; let h1 = bundle_hash(&bc); bc.signature = Signature::from_array([0xCD; SIGNATURE_SIZE]); let h2 = bundle_hash(&bc); assert_eq!(h1, h2); } #[test] fn bundle_hash_changes_with_content() { let mut bc = BundledConfirmation { node_id: [0x01; 32], endpoint: [0x02; 32], window_index: 1, op_hashes: vec![], reveal_hashes: vec![], signature: Signature::from_array([0u8; SIGNATURE_SIZE]), }; let h1 = bundle_hash(&bc); bc.op_hashes.push([0xAB; 32]); let h2 = bundle_hash(&bc); assert_ne!(h1, h2); } #[test] fn validate_accepts_valid_bundle() { let (pk, sk) = keypair(); let (node_id, rec) = make_node(*pk.as_bytes(), 10); let mut nt = NodeTable::new(); nt.insert(rec); let endpoint = [0x55; 32]; let bc = build_signed_bc( &sk, node_id, endpoint, 42, vec![[0x01; 32], [0x02; 32]], vec![[0x10; 32]], ); assert_eq!(validate_bundle(&bc, &nt, &endpoint), Ok(())); } #[test] fn validate_accepts_empty_ops_and_reveals() { let (pk, sk) = keypair(); let (node_id, rec) = make_node(*pk.as_bytes(), 1); let mut nt = NodeTable::new(); nt.insert(rec); let endpoint = [0x77; 32]; let bc = build_signed_bc(&sk, node_id, endpoint, 5, vec![], vec![]); assert_eq!(validate_bundle(&bc, &nt, &endpoint), Ok(())); } #[test] fn validate_rejects_unknown_node() { let (pk, sk) = keypair(); let (node_id, _rec) = make_node(*pk.as_bytes(), 1); let nt = NodeTable::new(); // пустой let endpoint = [0; 32]; let bc = build_signed_bc(&sk, node_id, endpoint, 1, vec![], vec![]); assert_eq!( validate_bundle(&bc, &nt, &endpoint), Err(BundleError::UnknownNode) ); } #[test] fn validate_rejects_unsupported_suite() { let (pk, sk) = keypair(); let (node_id, mut rec) = make_node(*pk.as_bytes(), 1); rec.suite_id = 0xFFFF; // не Mldsa65 let mut nt = NodeTable::new(); nt.insert(rec); let endpoint = [0; 32]; let bc = build_signed_bc(&sk, node_id, endpoint, 1, vec![], vec![]); assert_eq!( validate_bundle(&bc, &nt, &endpoint), Err(BundleError::UnsupportedSuite) ); } #[test] fn validate_rejects_wrong_endpoint() { let (pk, sk) = keypair(); let (node_id, rec) = make_node(*pk.as_bytes(), 1); let mut nt = NodeTable::new(); nt.insert(rec); let endpoint = [0xAA; 32]; let expected = [0xBB; 32]; let bc = build_signed_bc(&sk, node_id, endpoint, 1, vec![], vec![]); assert_eq!( validate_bundle(&bc, &nt, &expected), Err(BundleError::WrongEndpoint) ); } #[test] fn validate_rejects_unsorted_ops() { let (pk, sk) = keypair(); let (node_id, rec) = make_node(*pk.as_bytes(), 1); let mut nt = NodeTable::new(); nt.insert(rec); let endpoint = [0; 32]; let bc = build_signed_bc( &sk, node_id, endpoint, 1, vec![[0x02; 32], [0x01; 32]], // обратный порядок vec![], ); assert_eq!( validate_bundle(&bc, &nt, &endpoint), Err(BundleError::OpsOutOfOrder) ); } #[test] fn validate_rejects_duplicate_ops() { let (pk, sk) = keypair(); let (node_id, rec) = make_node(*pk.as_bytes(), 1); let mut nt = NodeTable::new(); nt.insert(rec); let endpoint = [0; 32]; // strict ascending — дубликаты отклоняются let bc = build_signed_bc( &sk, node_id, endpoint, 1, vec![[0x01; 32], [0x01; 32]], vec![], ); assert_eq!( validate_bundle(&bc, &nt, &endpoint), Err(BundleError::OpsOutOfOrder) ); } #[test] fn validate_rejects_unsorted_reveals() { let (pk, sk) = keypair(); let (node_id, rec) = make_node(*pk.as_bytes(), 1); let mut nt = NodeTable::new(); nt.insert(rec); let endpoint = [0; 32]; let bc = build_signed_bc( &sk, node_id, endpoint, 1, vec![], vec![[0x99; 32], [0x11; 32]], ); assert_eq!( validate_bundle(&bc, &nt, &endpoint), Err(BundleError::RevealsOutOfOrder) ); } #[test] fn validate_rejects_duplicate_reveals() { let (pk, sk) = keypair(); let (node_id, rec) = make_node(*pk.as_bytes(), 1); let mut nt = NodeTable::new(); nt.insert(rec); let endpoint = [0; 32]; let bc = build_signed_bc( &sk, node_id, endpoint, 1, vec![], vec![[0x05; 32], [0x05; 32]], ); assert_eq!( validate_bundle(&bc, &nt, &endpoint), Err(BundleError::RevealsOutOfOrder) ); } #[test] fn validate_rejects_bad_signature() { let (pk, sk) = keypair(); let (node_id, rec) = make_node(*pk.as_bytes(), 1); let mut nt = NodeTable::new(); nt.insert(rec); let endpoint = [0; 32]; let mut bc = build_signed_bc(&sk, node_id, endpoint, 1, vec![], vec![]); // испортить подпись let mut sig_bytes = *bc.signature.as_bytes(); sig_bytes[0] ^= 0xFF; sig_bytes[100] ^= 0xAA; bc.signature = Signature::from_array(sig_bytes); assert_eq!( validate_bundle(&bc, &nt, &endpoint), Err(BundleError::InvalidSignature) ); } #[test] fn validate_rejects_signature_from_different_key() { let (pk, _sk) = keypair(); let (_other_pk, other_sk) = keypair(); let (node_id, rec) = make_node(*pk.as_bytes(), 1); let mut nt = NodeTable::new(); nt.insert(rec); let endpoint = [0; 32]; // подписано other_sk, но NodeTable имеет pk let bc = build_signed_bc(&other_sk, node_id, endpoint, 1, vec![], vec![]); assert_eq!( validate_bundle(&bc, &nt, &endpoint), Err(BundleError::InvalidSignature) ); } #[test] fn encode_determinism() { let bc = BundledConfirmation { node_id: [0x01; 32], endpoint: [0x02; 32], window_index: 42, op_hashes: vec![[0x03; 32]], reveal_hashes: vec![[0x04; 32]], signature: Signature::from_array([0x05; SIGNATURE_SIZE]), }; let mut a = Vec::new(); bc.encode(&mut a); let mut b = Vec::new(); bc.encode(&mut b); assert_eq!(a, b); } #[test] fn encoded_counts_are_le() { let bc = BundledConfirmation { node_id: [0; 32], endpoint: [0; 32], window_index: 0, op_hashes: vec![[0; 32]; 0x1234], reveal_hashes: vec![[0; 32]; 0x0056], signature: Signature::from_array([0; SIGNATURE_SIZE]), }; let mut buf = Vec::new(); bc.encode(&mut buf); // spec v33.1.5+: window_index 8B → op_count at offset 72..74 assert_eq!(&buf[72..74], &[0x34, 0x12]); // reveal_count at offset 74 + 0x1234*32 = 74 + 149504 = 149578 let reveal_count_offset = 74 + 0x1234 * 32; assert_eq!( &buf[reveal_count_offset..reveal_count_offset + 2], &[0x56, 0x00] ); } #[test] fn encoded_window_index_is_le() { let bc = BundledConfirmation { node_id: [0; 32], endpoint: [0; 32], window_index: 0xDEADBEEFCAFEBABE, op_hashes: vec![], reveal_hashes: vec![], signature: Signature::from_array([0; SIGNATURE_SIZE]), }; let mut buf = Vec::new(); bc.encode(&mut buf); // spec v33.1.5+: window_index u64 LE 8B at offset 64..72 assert_eq!( &buf[64..72], &[0xBE, 0xBA, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE] ); } #[test] fn bundle_hash_is_32_bytes() { let bc = BundledConfirmation { node_id: [0; 32], endpoint: [0; 32], window_index: 0, op_hashes: vec![], reveal_hashes: vec![], signature: Signature::from_array([0; SIGNATURE_SIZE]), }; let h = bundle_hash(&bc); assert_eq!(h.len(), 32); } #[test] fn bundle_fixed_overhead_value() { // spec v33.1.5+: 32 (node_id) + 32 (endpoint) + 8 (window_index u64) + 2 (op_count) + 2 (reveal_count) // + 3309 (signature ML-DSA-65) = 3385 assert_eq!(BUNDLE_FIXED_OVERHEAD, 3385); } #[test] fn secret_key_size_constant_available() { // Sanity: SECRET_KEY_SIZE импортирован и совпадает (ML-DSA-65 expanded = 4032) assert_eq!(SECRET_KEY_SIZE, 4032); } // -------- VdfReveal (Phase B) -------- fn build_signed_reveal( sk: &mt_crypto::SecretKey, node_id: NodeId, window_index: u64, endpoint: Hash32, ) -> VdfReveal { let mut r = VdfReveal { node_id, window_index, endpoint, signature: Signature::from_array([0u8; SIGNATURE_SIZE]), }; let mut scope = Vec::new(); r.encode_signed_scope(&mut scope); r.signature = sign(sk, &scope).expect("sign Reveal scope"); r } #[test] fn reveal_encode_matches_spec_layout() { let r = VdfReveal { node_id: [0xAA; 32], window_index: 0x0102030405060708, endpoint: [0xBB; 32], signature: Signature::from_array([0x44; SIGNATURE_SIZE]), }; let mut buf = Vec::new(); r.encode(&mut buf); assert_eq!(buf.len(), REVEAL_SIZE); // spec v33.1.5+: 32 (node_id) + 8 (window u64 LE) + 32 (endpoint) + 3309 (signature ML-DSA-65) = 3381 assert_eq!(REVEAL_SIZE, 3381); // node_id: 0..32 assert_eq!(&buf[0..32], &[0xAA; 32]); // window_index u64 LE: 32..40 (spec v33.1.5+ — было u32 4B до v33.1.5) assert_eq!( &buf[32..40], &[0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01] ); // endpoint: 40..72 assert_eq!(&buf[40..72], &[0xBB; 32]); // signature: 72..72+SIGNATURE_SIZE assert_eq!(&buf[72..72 + SIGNATURE_SIZE], &[0x44; SIGNATURE_SIZE]); } #[test] fn reveal_signed_scope_excludes_signature() { let r = VdfReveal { node_id: [0x11; 32], window_index: 42, endpoint: [0x22; 32], signature: Signature::from_array([0xCC; SIGNATURE_SIZE]), }; let mut scope = Vec::new(); r.encode_signed_scope(&mut scope); let mut full = Vec::new(); r.encode(&mut full); assert_eq!(full.len(), scope.len() + SIGNATURE_SIZE); assert_eq!(&full[..scope.len()], scope.as_slice()); assert_eq!(scope.len(), 32 + 8 + 32); } #[test] fn reveal_hash_domain_mt_vdf_reveal() { let r = VdfReveal { node_id: [0x01; 32], window_index: 7, endpoint: [0x02; 32], signature: Signature::from_array([0u8; SIGNATURE_SIZE]), }; let mut scope = Vec::new(); r.encode_signed_scope(&mut scope); let expected = hash(b"mt-vdf-reveal", &[&scope]); assert_eq!(reveal_hash(&r), expected); } #[test] fn reveal_hash_stable_across_resign() { let mut r = VdfReveal { node_id: [0x01; 32], window_index: 7, endpoint: [0x02; 32], signature: Signature::from_array([0u8; SIGNATURE_SIZE]), }; let h1 = reveal_hash(&r); r.signature = Signature::from_array([0xFF; SIGNATURE_SIZE]); let h2 = reveal_hash(&r); assert_eq!(h1, h2); } #[test] fn endpoint_formula_matches_spec() { // spec v33.1.5+: endpoint = SHA-256("mt-lottery" || T_r || cba(W-2) || node_id || window_index(8B LE)) let t_r: Hash32 = [0x10; 32]; let cba: Hash32 = [0x20; 32]; let node_id: NodeId = [0x30; 32]; let window_index: u64 = 0xDEADBEEFCAFEBABE; let got = compute_endpoint(&t_r, &cba, &node_id, window_index); let mut w_le = Vec::new(); mt_codec::write_u64(&mut w_le, window_index); let expected = hash(b"mt-lottery", &[&t_r, &cba, &node_id, &w_le]); assert_eq!(got, expected); } #[test] fn endpoint_changes_with_each_input() { let t_r: Hash32 = [0x10; 32]; let cba: Hash32 = [0x20; 32]; let node_id: NodeId = [0x30; 32]; let w: u64 = 5; let base = compute_endpoint(&t_r, &cba, &node_id, w); let alt_t_r = [0x11; 32]; let alt_cba = [0x21; 32]; let alt_node = [0x31; 32]; assert_ne!(compute_endpoint(&alt_t_r, &cba, &node_id, w), base); assert_ne!(compute_endpoint(&t_r, &alt_cba, &node_id, w), base); assert_ne!(compute_endpoint(&t_r, &cba, &alt_node, w), base); assert_ne!(compute_endpoint(&t_r, &cba, &node_id, w + 1), base); } #[test] fn validate_reveal_accepts_valid() { let (pk, sk) = keypair(); let (node_id, rec) = make_node(*pk.as_bytes(), 1); let mut nt = NodeTable::new(); nt.insert(rec); let t_r = [0x55; 32]; let cba = [0x66; 32]; let w: u64 = 100; let endpoint = compute_endpoint(&t_r, &cba, &node_id, w); let r = build_signed_reveal(&sk, node_id, w, endpoint); assert_eq!(validate_reveal(&r, &nt, &t_r, &cba, w), Ok(())); } #[test] fn validate_reveal_rejects_unknown_node() { let (pk, sk) = keypair(); let (node_id, _rec) = make_node(*pk.as_bytes(), 1); let nt = NodeTable::new(); let t_r = [0; 32]; let cba = [0; 32]; let w = 1; let endpoint = compute_endpoint(&t_r, &cba, &node_id, w); let r = build_signed_reveal(&sk, node_id, w, endpoint); assert_eq!( validate_reveal(&r, &nt, &t_r, &cba, w), Err(RevealError::UnknownNode) ); } #[test] fn validate_reveal_rejects_unsupported_suite() { let (pk, sk) = keypair(); let (node_id, mut rec) = make_node(*pk.as_bytes(), 1); rec.suite_id = 0xFFFF; let mut nt = NodeTable::new(); nt.insert(rec); let t_r = [0; 32]; let cba = [0; 32]; let w = 1; let endpoint = compute_endpoint(&t_r, &cba, &node_id, w); let r = build_signed_reveal(&sk, node_id, w, endpoint); assert_eq!( validate_reveal(&r, &nt, &t_r, &cba, w), Err(RevealError::UnsupportedSuite) ); } #[test] fn validate_reveal_rejects_wrong_window() { let (pk, sk) = keypair(); let (node_id, rec) = make_node(*pk.as_bytes(), 1); let mut nt = NodeTable::new(); nt.insert(rec); let t_r = [0; 32]; let cba = [0; 32]; let w: u64 = 10; let endpoint = compute_endpoint(&t_r, &cba, &node_id, w); let r = build_signed_reveal(&sk, node_id, w, endpoint); // current_window = 11, reveal.window_index = 10 assert_eq!( validate_reveal(&r, &nt, &t_r, &cba, 11), Err(RevealError::WrongWindow) ); } #[test] fn validate_reveal_rejects_wrong_endpoint() { let (pk, sk) = keypair(); let (node_id, rec) = make_node(*pk.as_bytes(), 1); let mut nt = NodeTable::new(); nt.insert(rec); let t_r = [0x11; 32]; let cba = [0x22; 32]; let w: u64 = 5; // заявленный endpoint — произвольный, не равен compute_endpoint let bogus_endpoint = [0xEE; 32]; let r = build_signed_reveal(&sk, node_id, w, bogus_endpoint); assert_eq!( validate_reveal(&r, &nt, &t_r, &cba, w), Err(RevealError::WrongEndpoint) ); } #[test] fn validate_reveal_rejects_bad_signature() { let (pk, sk) = keypair(); let (node_id, rec) = make_node(*pk.as_bytes(), 1); let mut nt = NodeTable::new(); nt.insert(rec); let t_r = [0x11; 32]; let cba = [0x22; 32]; let w: u64 = 5; let endpoint = compute_endpoint(&t_r, &cba, &node_id, w); let mut r = build_signed_reveal(&sk, node_id, w, endpoint); let mut sig_bytes = *r.signature.as_bytes(); sig_bytes[0] ^= 0xFF; sig_bytes[200] ^= 0xAA; r.signature = Signature::from_array(sig_bytes); assert_eq!( validate_reveal(&r, &nt, &t_r, &cba, w), Err(RevealError::InvalidSignature) ); } #[test] fn validate_reveal_rejects_signature_from_different_key() { let (pk, _sk) = keypair(); let (_other_pk, other_sk) = keypair(); let (node_id, rec) = make_node(*pk.as_bytes(), 1); let mut nt = NodeTable::new(); nt.insert(rec); let t_r = [0; 32]; let cba = [0; 32]; let w: u64 = 1; let endpoint = compute_endpoint(&t_r, &cba, &node_id, w); // подпись от other_sk, но NodeTable указывает на pk let r = build_signed_reveal(&other_sk, node_id, w, endpoint); assert_eq!( validate_reveal(&r, &nt, &t_r, &cba, w), Err(RevealError::InvalidSignature) ); } #[test] fn reveal_encode_determinism() { let r = VdfReveal { node_id: [0x42; 32], window_index: 99, endpoint: [0x77; 32], signature: Signature::from_array([0xAA; SIGNATURE_SIZE]), }; let mut a = Vec::new(); r.encode(&mut a); let mut b = Vec::new(); r.encode(&mut b); assert_eq!(a, b); } #[test] fn reveal_size_constant_value() { // spec v33.1.5+: window_index u64 8B (было u32 4B до v33.1.5) assert_eq!(REVEAL_SIZE, 32 + 8 + 32 + SIGNATURE_SIZE); assert_eq!(REVEAL_SIZE, 3381); } // ============ Phase C: Node lottery ============ #[test] fn seniority_below_13_is_zero() { // chain_length < 13 → seniority_term = 0 (integer div truncation) for cl in [0u64, 1, 5, 12] { assert_eq!(seniority_term(cl, 1000), 0); } } #[test] fn seniority_at_13_is_one() { assert_eq!(seniority_term(13, 1000), 1); } #[test] fn seniority_capped_by_snapshot() { // chain_length / 13 может превысить snapshot → cap на snapshot let cl = 130_000u64; let snap = 100u64; // 130_000 / 13 = 10_000, min(10_000, 100) = 100 assert_eq!(seniority_term(cl, snap), 100); } #[test] fn lottery_weight_sum_of_components() { // chain_length = 26 → chain_length / 13 = 2 // snapshot = 10 → seniority_term = min(2, 10) = 2 // lottery_weight = 10 + 2 = 12 assert_eq!(lottery_weight(26, 10), 12); } #[test] fn lottery_weight_ds2_floor_at_snapshot_one() { // DS-2: snapshot ≥ 1 ⇒ lottery_weight ≥ 1 // chain_length = 0 → seniority = 0 → weight = snapshot assert_eq!(lottery_weight(0, 1), 1); } #[test] fn lottery_weight_new_node_13_windows() { // Первые 13 окон: chain_length < 13, seniority = 0, weight = snapshot // Per spec: "первые 13 окон после регистрации lottery_weight = snapshot" assert_eq!(lottery_weight(1, 1), 1); assert_eq!(lottery_weight(7, 7), 7); assert_eq!(lottery_weight(12, 12), 12); } #[test] fn lottery_weight_max_2x_snapshot() { // Spec: "максимальное преимущество старожила ≈ 2x относительно новичка" // seniority capped by snapshot ⇒ lottery_weight ≤ 2 × snapshot let snap = 120_960u64; // 6τ₂ при τ₂ = 20 160 let max_cl = u64::MAX; let w = lottery_weight(max_cl, snap); assert!(w <= 2 * snap); assert_eq!(w, 2 * snap); // saturated cap } #[test] fn log2_q64_zero_endpoint_saturates() { assert_eq!(log2_q64(&[0u8; 32]), u128::MAX); } #[test] fn log2_q64_one_endpoint_max_log() { // endpoint = 1 (only LSB set) → log2(2^256/1) = 256 exactly. // poly3 approximation at y=0: frac = 2·B0 ≈ 2^49 (minimax equioscillating // error at endpoints, not zero). Binding output: (leading+1)·2^64 - 2·B0. let mut e = [0u8; 32]; e[31] = 1; let log2 = log2_q64(&e); let two_b0 = 2u128 * 0x0014_E086_EC98_2D63u128; // 2·B0 = frac at y=0 assert_eq!(log2, (256u128 << 64) - two_b0); } #[test] fn log2_q64_full_bits_minimal() { // endpoint = 0xFF..FF (all bits set), msb at bit 255 // leading_zeros = 0, y ≈ 1 - ε // Real log2(2^256 / (2^256 - 1)) ≈ 0 (very small positive) // Линейная аппроксимация: frac = 2^64 - x_q64 где x_q64 ≈ 2^64 - 1 // ⇒ frac ≈ 1 Q64 (near zero в real units) let e = [0xFFu8; 32]; let log2 = log2_q64(&e); assert_eq!(log2 >> 64, 0); // frac в младшей области — near-zero в real units let frac = log2 & ((1u128 << 64) - 1); assert!(frac < (1u128 << 4)); // менее 2^-60 real — очень близко к нулю } #[test] fn log2_q64_monotonic_descending_in_endpoint() { // endpoint_a < endpoint_b ⇒ log2_q64(a) >= log2_q64(b) let mut prev = u128::MAX; for i in [1u8, 2, 5, 10, 20, 50, 100, 200] { let mut e = [0u8; 32]; e[0] = i; // top byte only let v = log2_q64(&e); assert!(v <= prev, "log2 not monotonic decreasing: i={i}"); prev = v; } } #[test] fn log2_q64_determinism() { let e = [0x55u8; 32]; let a = log2_q64(&e); let b = log2_q64(&e); assert_eq!(a, b); } #[test] fn ln_q64_zero_endpoint_saturates() { assert_eq!(ln_q64(&[0u8; 32]), u128::MAX); } #[test] fn ln_q64_monotonic_descending() { // Same property as log2_q64: smaller endpoint → larger ln let mut prev = u128::MAX; for i in [1u8, 2, 5, 10, 20, 50, 100, 200] { let mut e = [0u8; 32]; e[0] = i; let v = ln_q64(&e); assert!(v <= prev); prev = v; } } #[test] fn ln_q64_determinism() { let e = [0xABu8; 32]; assert_eq!(ln_q64(&e), ln_q64(&e)); } #[test] fn ln_q64_equals_log2_times_ln2() { // ln(x) = log2(x) × ln(2) ; в Q64: ln_q64 = (log2_q64 × LN2_Q64) >> 64 let e = [0x77u8; 32]; let log2 = log2_q64(&e); let ln_direct = ln_q64(&e); // Reference computation let log2_high = (log2 >> 64) as u64; let log2_low = log2 as u64; let term_high = (log2_high as u128) * LN2_Q64; let term_low = ((log2_low as u128) * LN2_Q64) >> 64; let ln_expected = term_high.saturating_add(term_low); assert_eq!(ln_direct, ln_expected); } // ============ Binding test vectors (spec v29.12.0, Integer log algorithm) ============ #[test] fn ln_q64_binding_tv1_boundary_low() { // TV1 — endpoint = 0x00..01 (smallest non-zero) → largest ln let e = { let mut e = [0u8; 32]; e[31] = 1; e }; assert_eq!(ln_q64(&e), 0x00000000000000b171fb06bb5b60c961u128); } #[test] fn ln_q64_binding_tv2_msb_only() { // TV2 — endpoint = 2^255 → log2 = 1 → ticket ≈ LN2_Q64 let mut e = [0u8; 32]; e[0] = 0x80; assert_eq!(ln_q64(&e), 0x0000000000000000b15526e15db6980cu128); } #[test] fn ln_q64_binding_tv3_typical() { // TV3 — typical dense pattern let e = [ 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, ]; assert_eq!(ln_q64(&e), 0x00000000000000004f60bd6fe6504646u128); } #[test] fn ln_q64_binding_tv4_near_max() { // TV4 — endpoint = 2^256-1 → log2(2^256/e) ≈ 0 → ticket = 0 let e = [0xFFu8; 32]; assert_eq!(ln_q64(&e), 0u128); } #[test] fn ln_q64_binding_tv5_peak_error_region() { // TV5 — peak-error region (y ≈ 0.84, attacker-favorable peak of equioscillation) let e = [ 0xeb, 0x85, 0x1e, 0xb8, 0x51, 0xeb, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; assert_eq!(ln_q64(&e), 0x000000000000000015756c980b547a82u128); } // Binding test vectors: weighted_ticket_node (spec v29.12.0, Лотерея узлов) // Все используют ln_q64 = 0x4f60bd6fe6504646 от TV3 endpoint. fn tv3_endpoint() -> [u8; 32] { [ 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, ] } #[test] fn weighted_ticket_node_binding_n1_typical() { // chain=1000, snap=500, N=13: seniority = min(1000/13=76, 500) = 76 // weight = 500 + 76 = 576 let t = weighted_ticket_node(&tv3_endpoint(), 1000, 500); assert_eq!(t, 0x000000000000000000234770A382CE58u128); } #[test] fn weighted_ticket_node_binding_n2_boundary() { // DS-2 floor: chain_length=1, snapshot=1 → weight=1 let t = weighted_ticket_node(&tv3_endpoint(), 1, 1); assert_eq!(t, 0x00000000000000004F60BD6FE6504646u128); } #[test] fn weighted_ticket_node_binding_n3_seniority_cap() { // chain_length/13 = 76923 но capped by snapshot=10 → seniority=10, weight=20 let t = weighted_ticket_node(&tv3_endpoint(), 1_000_000, 10); assert_eq!(t, 0x000000000000000003F80978CB840383u128); } #[test] fn weighted_ticket_node_binding_n4_max_boundary() { // chain_length=u64::MAX, snapshot=120960 (6τ₂ актуальный snapshot_max) // → seniority=120960 (capped), weight=241920 let t = weighted_ticket_node(&tv3_endpoint(), u64::MAX, 120_960); assert_eq!(t, 0x000000000000000000001580E0B1AED0u128); } #[test] fn weighted_ticket_node_binding_n5_seniority_threshold() { // chain_length=13 exactly: seniority_term = 13/13 = 1, weight = 1+1 = 2 // Это первое целое значение chain_length дающее seniority_term ≥ 1. let t = weighted_ticket_node(&tv3_endpoint(), 13, 1); assert_eq!(t, 0x000000000000000027B05EB7F3282323u128); } // spec Sovereignty Ladder: binding vectors A1-A4 (weighted_ticket_account) // удалены. Account lottery не существует в текущей схеме. #[test] fn weighted_ticket_node_determinism() { let e = [0x42u8; 32]; assert_eq!( weighted_ticket_node(&e, 1000, 500), weighted_ticket_node(&e, 1000, 500) ); } #[test] fn weighted_ticket_node_decreases_with_weight() { // Больший lottery_weight → меньший weighted_ticket (при том же endpoint). // Больше веса — больше шанс победить (меньше ticket в argmin). let e = [0x33u8; 32]; let low_w = weighted_ticket_node(&e, 0, 100); let high_w = weighted_ticket_node(&e, 0, 10000); assert!(high_w < low_w); } #[test] fn weighted_ticket_node_monotonic_in_endpoint() { // При одинаковом weight: меньший endpoint → больший ticket. let snap = 1000u64; let cl = 10_000u64; let mut prev = u128::MAX; for i in [1u8, 10, 50, 100, 200, 255] { let mut e = [0u8; 32]; e[0] = i; let v = weighted_ticket_node(&e, cl, snap); assert!(v <= prev); prev = v; } } #[test] fn weighted_ticket_node_zero_weight_saturates() { // Нарушение DS-2: snapshot = 0 → weight = 0 → защитный u128::MAX let e = [0x55u8; 32]; assert_eq!(weighted_ticket_node(&e, 0, 0), u128::MAX); } #[test] fn weighted_ticket_node_integer_div_toward_zero() { // Явно проверить что integer div, не rounding let mut e = [0u8; 32]; e[0] = 0x80; // endpoint = 2^255 let w: u64 = 3; let ticket = ln_q64(&e); let expected = ticket / (w as u128); // integer div assert_eq!(weighted_ticket_node(&e, 0, w), expected); } #[test] fn log2_q64_boundary_msb_at_127() { // msb at bit 127 (границу между hi/lo halves) — edge case мантиссы let mut e = [0u8; 32]; e[16] = 0x80; // bit 127 set в low half let v = log2_q64(&e); // leading = 128, y = 0, total = 129·2^64 - 2·B0 (minimax poly3 error at y=0) let two_b0 = 2u128 * 0x0014_E086_EC98_2D63u128; assert_eq!(v, (129u128 << 64) - two_b0); } #[test] fn log2_q64_boundary_msb_at_128() { // msb at bit 128 — первый бит high half let mut e = [0u8; 32]; e[15] = 0x01; // bit 128 set let v = log2_q64(&e); // leading = 127, y = 0, total = 128·2^64 - 2·B0 (minimax poly3 error at y=0) let two_b0 = 2u128 * 0x0014_E086_EC98_2D63u128; assert_eq!(v, (128u128 << 64) - two_b0); } #[test] fn ln2_q64_constant_value() { // LN2_Q64 ≈ ln(2) × 2^64 // ln(2) ≈ 0.693147... × 2^64 ≈ 0xB17217F7D1CF79AB assert_eq!(LN2_Q64, 0xB172_17F7_D1CF_79AB); } // ============ Phase E: Winner determination ============ fn cand(ticket: u128, class: u8, id_byte: u8) -> Candidate { Candidate { ticket, class, id: [id_byte; 32], } } #[test] fn winner_empty_candidates_returns_none() { assert_eq!(determine_winner(&[]), None); } #[test] fn winner_single_candidate() { let c = cand(100, WINNER_CLASS_NODE, 0x11); let w = determine_winner(&[c]).unwrap(); assert_eq!(w.class, WINNER_CLASS_NODE); assert_eq!(w.id, [0x11; 32]); assert_eq!(w.ticket, 100); } #[test] fn winner_picks_minimum_ticket() { let a = cand(500, WINNER_CLASS_NODE, 0x11); let b = cand(100, WINNER_CLASS_NODE, 0x22); let c = cand(300, WINNER_CLASS_NODE, 0x33); let w = determine_winner(&[a, b, c]).unwrap(); assert_eq!(w.id, [0x22; 32]); assert_eq!(w.ticket, 100); } #[test] fn winner_node_account_mixed_min_wins() { // Account с меньшим ticket побеждает над node let node = cand(500, WINNER_CLASS_NODE, 0x11); let acc = cand(100, WINNER_CLASS_NODE, 0x22); let w = determine_winner(&[node, acc]).unwrap(); assert_eq!(w.class, WINNER_CLASS_NODE); assert_eq!(w.ticket, 100); } #[test] fn winner_tie_breaker_class_node_preferred() { // Same ticket: Node (class=1) < Account (class=2) → Node wins let node = cand(100, WINNER_CLASS_NODE, 0xFF); let acc = cand(100, WINNER_CLASS_NODE, 0x00); let w = determine_winner(&[acc, node]).unwrap(); assert_eq!(w.class, WINNER_CLASS_NODE); } #[test] fn winner_tie_breaker_id_lex_ascending() { // Same ticket, same class: id lex asc let a = cand(100, WINNER_CLASS_NODE, 0x22); let b = cand(100, WINNER_CLASS_NODE, 0x11); let w = determine_winner(&[a, b]).unwrap(); assert_eq!(w.id, [0x11; 32]); // lex меньший } #[test] fn winner_deterministic_on_permutation() { let c1 = cand(500, WINNER_CLASS_NODE, 0x11); let c2 = cand(100, WINNER_CLASS_NODE, 0x22); let c3 = cand(300, WINNER_CLASS_NODE, 0x33); let w1 = determine_winner(&[c1, c2, c3]).unwrap(); let w2 = determine_winner(&[c3, c2, c1]).unwrap(); let w3 = determine_winner(&[c2, c3, c1]).unwrap(); assert_eq!(w1, w2); assert_eq!(w2, w3); } #[test] fn sorted_candidates_fallback_order() { let c1 = cand(500, WINNER_CLASS_NODE, 0x11); let c2 = cand(100, WINNER_CLASS_NODE, 0x22); let c3 = cand(300, WINNER_CLASS_NODE, 0x33); let sorted = sorted_candidates_for_fallback(&[c1, c2, c3]); assert_eq!(sorted[0].ticket, 100); // winner assert_eq!(sorted[1].ticket, 300); // fallback_1 assert_eq!(sorted[2].ticket, 500); // fallback_2 } #[test] fn sorted_candidates_empty() { let sorted = sorted_candidates_for_fallback(&[]); assert!(sorted.is_empty()); } #[test] fn sorted_candidates_stable_on_permutation() { let c1 = cand(500, WINNER_CLASS_NODE, 0x11); let c2 = cand(100, WINNER_CLASS_NODE, 0x22); let c3 = cand(300, WINNER_CLASS_NODE, 0x33); let s1 = sorted_candidates_for_fallback(&[c1, c2, c3]); let s2 = sorted_candidates_for_fallback(&[c3, c2, c1]); assert_eq!(s1, s2); } #[test] fn winner_with_u128_max_tickets() { // Защитный u128::MAX при DS-2/DS-3 violation let good = cand(1000, WINNER_CLASS_NODE, 0x11); let bad = cand(u128::MAX, WINNER_CLASS_NODE, 0x22); let w = determine_winner(&[good, bad]).unwrap(); assert_eq!(w.ticket, 1000); } // ============ Phase F: Quorum ============ #[test] fn quorum_spec_test_vectors() { // Spec v29.8.0 binding test vectors (P3): assert_eq!(quorum(1), 1); assert_eq!(quorum(100), 67); assert_eq!(quorum(149), 100); assert_eq!(quorum(150), 101); assert_eq!(quorum(1000), 670); } #[test] fn quorum_zero_active_is_zero() { // Edge case — no active nodes assert_eq!(quorum(0), 0); } #[test] fn quorum_monotonic_non_decreasing() { let mut prev = 0u64; for x in [0u64, 1, 10, 100, 1000, 10_000, 100_000] { let q = quorum(x); assert!(q >= prev); prev = q; } } #[test] fn quorum_large_no_overflow() { // 10^14 — max bound в спеке. 67 × 10^14 + 99 = 6.7e15 < 2^63. let big = 100_000_000_000_000u64; let q = quorum(big); // ~67% of big assert!(q > big / 2); assert!(q < big); } #[test] fn is_cemented_at_exact_quorum() { // cemented_sum == quorum → cemented (>=) assert!(is_cemented(67, 100)); } #[test] fn is_cemented_below_quorum() { assert!(!is_cemented(66, 100)); } #[test] fn is_cemented_above_quorum() { assert!(is_cemented(100, 100)); } #[test] fn is_cemented_zero_active_zero_cemented() { // quorum(0) = 0, cemented_sum = 0 ≥ 0 → cemented // Это эдж кейс — в реальности active=0 halt liveness, не consensus assert!(is_cemented(0, 0)); } }