montana/Монтана-Протокол/Внешний аудит/claude-opus-4-7_2026-04-26_T232707.md

44 KiB
Raw Blame History

Внешний аудит кода Montana — отчёт #2 (incremental, M2 scope)

Аудитор: Claude Opus 4.7 (1M context), модель claude-opus-4-7[1m] Дата проведения: 2026-04-26, T23:27:07 — T00:15:00 (приблизительно) Локация: /Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код/ Тип аудита: incremental — только новые части кода относительно первого аудита Предыдущий отчёт: claude-opus-4-7_2026-04-26_T201805.md


1. Scope этого аудита

AUDIT.md обновлён, расширил scope на M2 state foundation. Этот отчёт покрывает только новые части. Уже закрытое в первом отчёте не повторяю — для M1 (mt-crypto, mt-crypto-native, mt-mnemonic) findings актуальны и из первого отчёта.

Новый scope (этот аудит)

Crate / артефакт Файл Строк (фактически) Назначение
mt-merkle crates/mt-merkle/src/lib.rs 474 Sparse Merkle Tree depth=256 + verify_proof
mt-genesis crates/mt-genesis/src/lib.rs 343 Genesis Decree + ProtocolParams SSOT (4110B)
mt-state crates/mt-state/src/lib.rs 823 AccountTable/NodeTable/CandidatePool + MonetaryState
mt-timechain crates/mt-timechain/src/lib.rs 319 TimeChain VDF + adaptive D + cemented_bundle_aggregate
docs/security-cards.md docs/security-cards.md 298 6 Security Cards для M1 crypto primitives
docs/audit-checklist.md docs/audit-checklist.md 167 Pre-audit self-attestation 11 категорий

Total new code surface: 1959 строк (ровно соответствует AUDIT.md M2 заявлению).

Изменения в уже-аудированных частях

  • mt-codec: crate перенесён mt-timechain → mt-timechain (rename); domain TIMECHAINTIMECHAIN
  • AUDIT.md: существенно расширен (новые секции M2, Spec ↔ Code 16-entries table, Security Cards reference)
  • VERSION.md: spec target обновлён до Montana v33.1.3

Уже покрыто первым аудитом — не повторяю

  • mt-crypto (643 строк) — все unsafe blocks, FFI safety, OpenSSL EVP, Drop+zeroize
  • mt-crypto-native (40+375+56+45 = 516 строк) — Rust binding + C wrapper + build.rs
  • mt-mnemonic (937 строк) — PBKDF2/HKDF/HMAC/wordlist/bit_packing/mnemonic
  • mt-codec base (32 domains) — CanonicalEncode trait + write_u* helpers
  • 51 NIST CAVP fixtures cross-checked byte-exact (ML-DSA-65 + ML-KEM-768 KeyGen + SigGen)

2. Методология (та же, что в первом отчёте)

Доверяю только: исходному коду, тестам, публичным NIST/RFC стандартам, NIST CAVP repository.

Не доверяю ни одному .md файлу в репо — ни AUDIT.md, ни security-cards.md, ни audit-checklist.md, ни VERSION.md, ни ROADMAP.md, ни спеке. Все utterances в этих документах рассматриваются как claims-to-verify.

Single thread / single process соблюдается: .cargo/config.toml содержит jobs = 1 + RUST_TEST_THREADS = 1 (verified из первого аудита).


3. Сильные стороны M2

3.1. Полностью pure-Rust код, ноль unsafe

В отличие от M1 (FFI к OpenSSL), весь M2 написан на чистом Rust без unsafe блоков:

$ grep -rn "unsafe " crates/mt-{merkle,genesis,state,timechain}/src/
(0 hits)

Это существенное упрощение audit surface для M2 layer — нет проблем FFI memory safety, нет необходимости в // SAFETY: комментариях.

3.2. Детерминизм через BTreeMap повсюду

Все state structures используют BTreeMap (не HashMap):

  • mt-merkle::SparseMerkleTree { leaves: BTreeMap<[u8; 32], Hash32> }
  • mt-state::AccountTable { records: BTreeMap<AccountId, AccountRecord> }
  • mt-state::NodeTable { records: BTreeMap<NodeId, NodeRecord> }
  • mt-state::CandidatePool { records: BTreeMap<NodeId, CandidateRecord> }

HashMap имеет non-deterministic iteration order между runs (из-за random hash seed против HashDoS) — это автоматический breaker для consensus determinism. Запрет на HashMap соблюдён.

Тесты *_root_order_independent явно проверяют что вставка в разном порядке даёт одинаковый root (verified).

3.3. Domain separation для всех hash-композиций

Каждый hash-call использует один из 32 domain separators:

  • mt-merkle-leaf / mt-merkle-node (для SMT)
  • mt-state-root (composition)
  • mt-account / mt-node (ID derivation)
  • mt-genesis (Genesis state hash)
  • mt-bc-aggregate / mt-bc-aggregate-empty (cemented bundle)

Новый mt-timechain (b"mt-timechain") заменил старый mt-timechain (b"mt-timechain") при rename.

3.4. Integer-only арифметика в consensus path

Verified: grep -rn "f32\|f64" crates/mt-{merkle,genesis,state,timechain}/src/0 hits (соответствует [I-9]).

MonetaryState::apply_step и next_d используют u64/u128 integer math с явным checked_mul / checked_add для overflow detection.

3.5. Sparse Merkle Tree корректная реализация

mt-merkle правильно реализует SMT depth=256:

  • empty_internal(k) precomputed cache через OnceLock (level 0 = [0;32], level k = internal_hash(empty(k-1), empty(k-1)))
  • compute_subtree_root recursive с splitting on bit
  • prove(key, value) produces inclusion proof + bitmap of present siblings
  • verify_proof(root, proof) reconstructs root from leaf + siblings

Тесты покрывают:

  • empty/single/multi-leaf roots
  • order independence
  • idempotent insert
  • mutated sibling rejection
  • mutated leaf value rejection
  • wrong root rejection
  • absence proof + absence claim of present key (rejection)
  • 100-entry property test

3.6. Genesis Decree фиксирован через singleton

genesis_params() использует OnceLock<ProtocolParams> — singleton инициализируется один раз на программный запуск. Все consumers видят одинаковую копию params.

PARAMS_ENCODED_SIZE = 4110 явная константа, проверяется test'ом против фактического encode().len().

19 mutation cases в encode_detects_field_mutations test — каждая модификация поля даёт другой encoded output.

3.7. State_root композиция явно domain-separated

pub fn compute_state_root(node_root, candidate_root, account_root) -> Hash32 {
    hash(domain::STATE_ROOT, &[node_root, candidate_root, account_root])
}

Tests: state_root_order_matters — перестановка inputs даёт разный hash. state_root_uses_domain_separator — формула совпадает с hash(STATE_ROOT, [r1, r2, r3]).

3.8. cemented_bundle_aggregate с правильным дизайном [I-8]

Три ветви:

  • window < 2[0;32] (genesis)
  • cemented.is_empty()SHA-256(BC_AGGREGATE_EMPTY ‖ window_le)
  • non-empty → SHA-256(BC_AGGREGATE ‖ sorted(node_ids) ‖ window_le)

sorted гарантирует input-order independence — анти-grinding защита через canonical sort.

Signatures и op_hashes excluded — закрывает grinding surface через σ. Comment lines 60-61 правильно объясняет это design rationale.

Window включён в hash (anti-grinding through window):

  • Test aggregate_depends_on_window_in_non_empty_branch — разные window дают разный aggregate даже при identical S_W.

3.9. Adaptive D через permille integer arithmetic

next_d использует permille (×10) для байт-exact integer arithmetic вместо fractional float:

  • low_permille = dead_zone_low × 10 (850 для 85%)
  • high_permille = dead_zone_high × 10 (950 для 95%)
  • Above high: D × (rate_den + rate_num) / rate_den = D × 103 / 100
  • Below low: D × (rate_den - rate_num) / rate_den = D × 97 / 100
  • Dead zone: D unchanged

Tests boundary cases (850, 950, 851, 949, 0, 1000) — все проходят.

3.10. MonetaryState carry-recurrence для precision-preservation

Geometric step-up baseline emission реализован через carry-recurrence:

tmp = R × inflation_num + carry
new_R = tmp / inflation_den
new_carry = tmp mod inflation_den

Это не теряет precision при повторных шагах (downward bias zero). Test monetary_state_carry_invariant_after_n_steps проверяет invariant carry < inflation_den после 100 шагов.

Overflow detection через checked_mul().unwrap_or_else(|| panic!(descriptive)) — explicit halt at arithmetic horizon, не silent corruption.

3.11. 60/60 determinism invariants verified

Прогон тестов в одно ядро:

Crate Unit Determinism Total
mt-merkle 25 (1 ignored) 10 35 (1 ignored)
mt-genesis 24 7 31
mt-state 41 24 65
mt-timechain 19 19 38

Total M2: 165 tests passed, 0 failed, 1 ignored.

3.12. Genesis constants обоснованы академической литературой

Comment в genesis_params (lines 90-95) ссылается на:

Frederick, Loewenstein, O'Donoghue (2002) "Time Discounting and Time Preference: A Critical Review", Journal of Economic Literature 40(2): lower bound observed median individual time preference 2.5%

Это good practice для security-critical константы — академический rationale для pin 41/40.

3.13. Все record types fixed-size encoded

Тип Заявлено Фактически Соответствие
AccountRecord 2059 32+16+2+1+32+4+4+4+1952+4+4+4 = 2059
NodeRecord 2098 32+1952+2+32+8+8+8+48+8 = 2098
CandidateRecord 2082 32+1952+2+32+32+8+8+8+8 = 2082
MonetaryState 24 16+8 = 24
ProtocolParams 4110 layout sum (verified test) = 4110

Каждый размер проверяется в test *_encoded_size.


4. Слабые стороны и Findings (только новые)

Findings нумерованы с префиксом M2- для отделения от первого отчёта. Severity та же шкала: CRITICAL / HIGH / MEDIUM / LOW / INFO.

M2-1 [HIGH] — Bootstrap pubkeys в Genesis всё ещё placeholder

Описание. crates/mt-genesis/src/lib.rs lines 111-114:

bootstrap_account_pubkey: [0u8; PUBLIC_KEY_SIZE],
bootstrap_node_pubkey: [0u8; PUBLIC_KEY_SIZE],
genesis_content_app_id: genesis_app_id(),
genesis_content_data_hash: [0u8; 32],

Также target_zero: [0u8; 32] (line 98) — placeholder.

Test bootstrap_keypairs_finalized (lines 335-342) явно #[ignore = "placeholder pubkeys — unignore after bootstrap keypair finalization"].

Воздействие. Genesis state не финализирован для production deployment:

  • Любой bootstrap actor мог бы попытаться claim ownership (хотя privkey для zero-pubkey не известен — atypical pubkey)
  • target_zero=[0;32] placeholder — Adaptive D начнётся с unrealistic VDF target
  • genesis_content_data_hash=[0;32] — нет привязки к initial content

AUDIT.md заявляет "M1 + M2 layers — READY FOR EXTERNAL AUDIT", но genesis state не финализирован для production.

Severity HIGH: блокер для mainnet deployment, но не blocker для аудита code correctness.

Рекомендация. Сгенерировать реальные bootstrap keypairs через keypair_from_seed с предопределённым seed (например, SHA-256("Montana Genesis bootstrap" + дата запуска). Зафиксировать в Genesis Decree до mainnet. Развернуть #[ignore] тест.

M2-2 [MEDIUM] — Stale comment pin 30/29 в mt-state

Описание. crates/mt-state/src/lib.rs lines 59-60:

/// Panics: при overflow `r * num + carry > u128::MAX`. Encoded arithmetic
/// horizon при pin 30/29 — около 1 930 monetary epochs. ...

Но genesis_params имеет:

inflation_num: 41,
inflation_den: 40,  // pin 41/40 = 2.5% per spec v33+

Comment ссылается на устаревший pin 30/29 (3.45% inflation), но production pin 41/40 (2.5% inflation).

Воздействие.

  • audit-checklist.md line 107 повторяет stale claim "Encoded arithmetic horizon ~1930 monetary epochs (≈ 60 тысяч лет)" — на самом деле под 41/40 horizon составляет ~78 тысяч лет (log_1.025(u128::MAX / (41×13e9)) ≈ 2487 monetary epochs × 524160 windows × 60 sec/window = 78K years)
  • Не security finding (horizon стал больше под текущим pin), но документация и comment рассогласованы с production code

Рекомендация. Обновить comment в mt-state/src/lib.rs:59-60 на актуальный pin 41/40 + рассчитанный horizon 2487 monetary epochs ≈ 78K лет. Также обновить audit-checklist.md §K.

M2-3 [MEDIUM] — Binding test vectors используют OLD pin 30/29

Описание. Тесты MonetaryState::apply_step используют apply_step(30, 29):

#[test]
fn monetary_state_apply_step_first_geometric_step() {
    // spec, binding test vector — переход в эпоху 6 (первый geometric step):
    //   tmp = 13_000_000_000 × 30 + 0 = 390_000_000_000
    //   r_new   = 390_000_000_000 / 29 = 13_448_275_862  (toward zero)
    //   carry   = 390_000_000_000 mod 29 = 2
    let mut s = MonetaryState::genesis(13_000_000_000);
    s.apply_step(30, 29);
    assert_eq!(s.r_baseline_current_moneta, 13_448_275_862);
    assert_eq!(s.carry_current, 2);
}

Под фактический production pin 41/40:

  • tmp = 13e9 × 41 + 0 = 533e9
  • r_new = 533e9 / 40 = 13_325_000_000
  • carry = 533e9 mod 40 = 0

Эти числа НИГДЕ не тестируются. Все binding vectors testing arbitrary parameters (30/29), не production constants.

Воздействие.

  • Apply_step algorithm correctness verified для 30/29
  • Apply_step integration с production constants не verified — regression risk при изменении inflation_num/inflation_den в genesis
  • Если в production будущем меняют pin (gov upgrade) — нужны новые binding vectors, не сразу очевидно что они нужны

Рекомендация. Добавить binding test vector с production constants:

#[test]
fn monetary_state_production_pin_41_40_first_step() {
    let mut s = MonetaryState::genesis(13_000_000_000);
    s.apply_step(41, 40);
    assert_eq!(s.r_baseline_current_moneta, 13_325_000_000);
    assert_eq!(s.carry_current, 0);
}

M2-4 [MEDIUM] — assert! panic'и на user-reachable input в mt-merkle

Описание. crates/mt-merkle/src/lib.rs:

// Line 23
pub fn empty_internal(level: usize) -> Hash32 {
    assert!(level <= TREE_DEPTH, "empty_internal: level > TREE_DEPTH");
    ...
}

// Line 42
fn get_bit(key: &[u8; 32], index: usize) -> u8 {
    assert!(index < 256, "get_bit: index >= 256");
    ...
}

empty_internal — public function. get_bit — private, но вызывается из compute_subtree_root и prove / verify_proof.

Воздействие.

  • empty_internal(level) — public, может быть вызван внешним кодом с level > 256 → panic
  • get_bit всегда вызывается с index < 256 (loop 0..TREE_DEPTH = 256) — internal invariant
  • verify_proof имеет for level in 0..TREE_DEPTH → index in [0, 255] always — safe
  • В CLAUDE.md "Никаких unwrap()/expect() в lib коде. Только в тестах и в случаях где panic означает protocol violation" — assert documents internal invariant правильно (line 22 comment "Panic above 256: programmer error, not a runtime condition")

Severity LOW-MEDIUM: technically reachable через misuse empty_internal (public API) с large level. Не security risk per se, но liveness vulnerability через panic.

Рекомендация. Либо изменить empty_internal сигнатуру на Result<Hash32, ...> либо заменить assert! на debug_assert! (in production builds — silently incorrect, не panic).

M2-5 [LOW] — Performance: O(N×256) на каждый root() call

Описание. mt-merkle::SparseMerkleTree::root() каждый раз делает полную recursive computation через все entries:

pub fn root(&self) -> Hash32 {
    let entries: Vec<_> = self.leaves.iter().map(|(k, v)| (*k, *v)).collect();
    compute_subtree_root(&entries, TREE_DEPTH)
}

Для N entries и 256 levels: O(N × 256) operations per call. Для N = 10000: 2.5M operations per root call. Для N = 1000000: 256M.

Воздействие. Performance, не security. AUDIT.md scope не упоминает SMT performance, hint что caching будет добавлен позже.

Рекомендация. Добавить lazy caching (invalidate on insert/remove, recompute on root call). Не critical для current audit, но warrants for production scale.

M2-6 [LOW] — VDF computation cost O(d) symmetric verify

Описание. mt-timechain::vdf_step(prev, d) делает d итераций SHA-256. Verify через vdf_verify тоже O(d).

Для D₀ = 252_000_000 — это ~252M SHA-256 operations per verify.

Воздействие. Это намеренно (VDF by definition slow), но:

  • Каждый узел делает много verify calls per epoch
  • Symmetric cost prover/verifier — VDF design principle, но традиционные VDFs (Wesolowski, Pietrzak) имеют O(1) verify
  • Sequential SHA-256 без acceleration

Рекомендация. Документировать в spec явный compute budget для verify operations per epoch. Это spec-level concern, не code finding. Опционально: рассмотреть Wesolowski VDF для O(1) verify в будущих versions.

M2-7 [LOW] — next_d без overflow detection

Описание. mt-timechain::next_d:

if median_ratio_permille >= high_permille {
    current_d * (rate_den + rate_num) / rate_den
}

current_d * (rate_den + rate_num)нет checked_mul. При current_d близком к u64::MAX — overflow.

Воздействие.

  • current_d начинается с D₀ = 252_000_000
  • Каждый +3% step grows D на 1.03 factor
  • u64::MAX ≈ 1.84e19
  • D достигнет u64::MAX через log_1.03(1.84e19 / 2.52e8) ≈ 850 monetary epochs ≈ 1.5M years of consecutive +3% steps
  • В реальности оно flapped в dead zone, growth limited
  • Practical horizon: ~1.5M years безопасно

Severity LOW: horizon очень большой, но pattern несимметричен с MonetaryState::apply_step (которое имеет checked_mul).

Рекомендация. Добавить checked_mul().expect("D overflow at horizon") для consistency с MonetaryState::apply_step pattern.

M2-8 [LOW] — chain_length_checkpoints: [u64; 6] без объяснения

Описание. NodeRecord имеет chain_length_checkpoints: [u64; 6] field. Spec reference в комментариях file отсутствует. Назначение 6 checkpoints не очевидно.

Возможные интерпретации:

  • Per-tier confirmations (6 tiers)
  • 6 historical snapshots
  • 6 confirmation thresholds

Воздействие. Audit confusion. Без spec context невозможно проверить semantic correctness — encoder pишет 6 × 8 = 48 байт, decoder ожидает 6 × 8 байт, layout symmetric, но meaning unspecified в коде.

Рекомендация. Добавить comment с spec reference: // spec, раздел "Node tier checkpoints" — 6 checkpoint snapshots для tier-based confirmation. Без spec docs не доверяю.

M2-9 [LOW] — Двойное хранение records в state tables

Описание. Каждый AccountTable / NodeTable / CandidatePool содержит:

  • records: BTreeMap<Id, FullRecord> — full records (2059/2098/2082 байта каждый)
  • tree: SparseMerkleTree { leaves: BTreeMap<[u8; 32], Hash32> } — leaf hashes (32 байта каждый)

Memory overhead: ~2× для каждого entry.

При insert:

self.tree.insert(key, &buf);  // hashes serialized record, stores hash
self.records.insert(key, record);  // stores full record

Если tree.insert succeeds но records.insert panics (out of memory) — таблицы desync. В Rust ownership это unlikely, но possible под severe memory pressure.

Severity LOW: double storage acceptable for clarity vs performance trade-off. Desync atomicity weak в edge cases (OOM).

Рекомендация. Документировать ownership invariant в комментариях. Опционально: использовать Cow / single-source representation.

M2-10 [LOW] — Recursion depth 256 в compute_subtree_root

Описание. compute_subtree_root recursive depth 256. Stack frame ~100 bytes = ~25KB total stack. На default thread stack (8MB Linux) — OK. На stack-limited environments (embedded? Windows default 1MB) может быть marginal.

Воздействие. Production environments — OK. Embedded — concern.

Рекомендация. Опционально: рассмотреть iterative version. Не critical.

M2-11 [INFO] — Inflation pin 41/40 = 2.5% asymptotic

Описание. genesis_params pin inflation_num=41, inflation_den=40.

Math check:

  • gross inflation per monetary epoch = 41/40 = 1.025 (×1.025)
  • net inflation = (41-40)/40 = 1/40 = 0.025 = 2.5% per monetary epoch
  • monetary_epoch_windows = 524_160 windows
  • assuming 60 sec/window: 524_160 × 60 = 31_449_600 sec = 31_449_600 / (365.25 × 86400) = ~365 days = 1 year
  • → 2.5% annual inflation

Test verifies arithmetically:

let asymptotic_ppm = ((p.inflation_num - p.inflation_den) * 1_000_000) / p.inflation_den;
assert_eq!(asymptotic_ppm, 25_000);  // 25_000 ppm = 2.5%

Не finding — math correct, just acknowledge что 2.5% annual (не 2.5% per-window или per-epoch as some readers might interpret).

M2-12 [INFO] — mt-recovery-fingerprint domain — заявление AUDIT.md не верифицировано полностью

Описание. AUDIT.md заявляет:

Domain registry sync (spec v33.1.3 list ↔ code mt-codec const list) (mt-recovery-fingerprint added v33.1.3)

Я подтвердил что RECOVERY_FINGERPRINT: &[u8] = b"mt-recovery-fingerprint" присутствует в mt-codec. Но не доверяю спеке — не могу проверить что spec v33.1.3 действительно содержит этот domain.

Также: AUDIT.md заявляет "33 domains" в codec table line 35. Фактически 32 domains в коде. Расхождение 33 vs 32.

Severity INFO/LOW: документация устарела (либо comment "33" в AUDIT.md unupdated после рефакторинга TIMECHAIN→TIMECHAIN, который не добавил новый domain, а заменил существующий).

M2-13 [LOW] — compute_state_root не включает MonetaryState

Описание. compute_state_root(node_root, candidate_root, account_root) — только три merkle roots. Не включает MonetaryState.

Это означает: state hash меняется при изменении balances в Account Table (через AccountRecord которая содержит balance), но не реагирует напрямую на изменение r_baseline_current_moneta или carry_current в MonetaryState scalar.

Если MonetaryState является глобальным consensus scalar (per AUDIT.md "два глобальных скаляра общего consensus state"), она должна влиять на state_root. Иначе — два узла с разными MonetaryState но одинаковыми Account/Node/Candidate tables будут производить одинаковый state_root → fork undetected.

Воздействие. Высокий potentially, но:

  • MonetaryState updates детерминирована через apply_step(num, den) от Genesis
  • Если все узлы стартуют из Genesis и не миссят monetary_epoch_tick — MonetaryState синхронен у всех
  • Fork detection через state_root mismatch — weakened (MonetaryState не отражена)

Комментарий в spec говорит state_root = SHA-256(STATE_ROOT || node_root || candidate_root || account_root) — current code соответствует.

Severity LOW-MEDIUM: depends on spec design. Если spec явно decided MonetaryState не в state_root — design choice, OK. Если spec хочет включить — bug.

Рекомендация. Verify против spec (которой я не доверяю) — это единственный способ закрыть finding. Opt: добавить MonetaryState bytes в compute_state_root input для defense-in-depth fork detection.

M2-14 [INFO] — Все panic sites документированы и controlled

В audit-checklist §K документирует 3 panic sites в MonetaryState::apply_step:

  • Line 63: assert!(inflation_den > 0)
  • Lines 67-72: overflow r × num
  • Lines 77-81: overflow prod + carry

Все 3 — controlled halts при protocol-invariant violation, не attacker-triggered (inflation_den приходит из Genesis OnceLock; overflow возможен только на arithmetic horizon ~78K лет). Документация correct в design intent.

Также есть 2 assert! в mt-merkle (lines 23, 42) — internal invariants, не documented в audit-checklist §K. Это доп finding M2-4 (см. выше) — audit-checklist incomplete.

Findings из первого аудита, всё ещё не закрытые

Несколько findings из первого отчёта (claude-opus-4-7_2026-04-26_T201805.md) остались открытыми в обновлённом коде:

ID Описание Статус
F-1 (1st) Line counts mt-crypto/src/lib.rs: 568 заявлено, 643 фактически Не закрыт (AUDIT.md повторяет 568)
F-2 (1st) cargo fmt --check FAILS Не verified в этом аудите (могло закрыться)
F-3 (1st) Stale "RustCrypto pure-Rust" в m1_crypto.rs example Не verified
F-4 (1st) 3 unsafe blocks без // SAFETY: (mt-crypto/lib.rs lines 168, 187, 351) Не закрыт (AUDIT.md повторяет "4 unsafe with SAFETY")
F-13 (1st) "13 error codes" semantic ambiguity Не закрыт
F-18 (1st) Total surface 1084 vs фактически 1159 Не закрыт (AUDIT.md повторяет 1084)

AUDIT.md и audit-checklist.md повторяют те же inaccurate claims что были в первой версии. Несмотря на 16 findings заявленных как closed, реальный document drift сохранился.


5. Findings в новых docs

DOC-1 [MEDIUM] — security-cards.md содержит stale line numbers

Описание. docs/security-cards.md Card 1 "Site of construction: crates/mt-crypto/src/lib.rs:122-159".

Из первого аудита фактический range для SecretKey impl: lines 130-161 (or close). С учётом текущих 643 строк (vs 568 claimed) — все line refs in cards may be off-by-many-lines.

Также Card 3 "keypair_from_seed" references line 290-296 для heap Box allocation. Я в первом аудите видел keypair_from_seed начинался на line 217 (фактически), не 290.

Воздействие. Auditor reading docs пытается найти line 290 в файле, видит другой код. Confusion.

Рекомендация. Auto-generate line refs from code (grep -n "fn keypair_from_seed") или удалить hardcoded line numbers, оставив только function names.

DOC-2 [HIGH] — audit-checklist.md повторяет stale audit info

Описание. docs/audit-checklist.md:

  • Line 18 "Layer 1 Rust shim 568 строк" (фактически 643)
  • Line 18 "4 sites: keypair_from_seed line 177, sign line 213, verify line 228, keypair_from_seed_mlkem line 296" (фактически 7 unsafe blocks, lines 224, 267, 282, 365 у тех 4 что имеют SAFETY)
  • Line 21 "Total own audit surface 1084 строк" (фактически 1159)
  • Line 162 "(architect role per CLAUDE.md v1.11.0)" — но в текущей сессии CLAUDE.md уже v1.12.0

Воздействие. Documentation pure drift. AUDIT.md заявляет "documentation accuracy verified through critic review of audit package itself" — но факты противоречат.

Рекомендация. Audit-checklist должен быть auto-generated из code либо verified via CI на каждый commit. Manual maintainance не работает.

DOC-3 [MEDIUM] — security-cards.md "Pass 17 checks 1-8: 8/8 closed" — without explicit listing

Описание. Каждая Card ссылается на "Pass 17 checks 1-8: 8/8 closed". Card 1 явно перечисляет 8:

  1. Constant-time
  2. Memory access
  3. Branch pattern
  4. Zeroization on drop
  5. Library check
  6. Stack hygiene
  7. OS-level mlock
  8. Memory barrier

Cards 2-6 говорят "Pass 17 checks 1-8: 8/8 closed (same as SecretKey)" without re-listing. Cards для разных primitives имеют разные threat models — applying same 8 checks без adaptation может miss primitive-specific concerns.

Например, для verify (Card 6) "Branching on PK bytes: no" — но pk public, branching на public material acceptable. Threat model для verify фундаментально отличается от sign — but Card 6 inherits same 8 checks template.

Воздействие. Cards могут содержать false negatives — checks marked closed without primitive-specific rigor.

Рекомендация. Каждая Card должна явно перечислить applicable Pass 17 checks с primitive-specific reasoning, не reuse template. Card 6 (verify) корректно говорит "Status: closed (no secret material — Security Card minimal)" — этого достаточно. Но Cards 2-5 should explicit list.


6. Spec ↔ Code byte-exact alignment table verification

AUDIT.md table (lines 150-167) заявляет 17 alignments (counted from table). Я verified independently:

AUDIT.md заявление Verified Источник Comment
ML-DSA-65 pubkey 1952B mt-crypto::PUBLIC_KEY_SIZE = 1952 First audit verified
ML-DSA-65 secretkey 4032B mt-crypto::SECRET_KEY_SIZE = 4032 First audit
ML-DSA-65 signature 3309B mt-crypto::SIGNATURE_SIZE = 3309 First audit
ML-DSA-65 seed 32B mt-crypto::KEYPAIR_SEED_SIZE = 32 First audit
ML-KEM-768 ek 1184B mt-crypto::MLKEM_PUBLIC_KEY_SIZE = 1184 First audit
ML-KEM-768 dk 2400B mt-crypto::MLKEM_SECRET_KEY_SIZE = 2400 First audit
ML-KEM-768 seed 64B mt-crypto::MLKEM_SEED_SIZE = 64 First audit
AccountRecord 2059B mt-state::ACCOUNT_RECORD_SIZE = 2059 This audit
NodeRecord 2098B mt-state::NODE_RECORD_SIZE = 2098 This audit
CandidateRecord 2082B mt-state::CANDIDATE_RECORD_SIZE = 2082 This audit
ProposalHeader 3722B ⚠️ Не M1/M2 scope (mt-account/mt-consensus) Не verified
ProtocolParams 4110B mt-genesis::PARAMS_ENCODED_SIZE = 4110 This audit
TREE_DEPTH 256 mt-merkle::TREE_DEPTH = 256 This audit
Inflation pin 41/40 mt-genesis: inflation_num=41, inflation_den=40 This audit
R_GENESIS 13 Ɉ mt-genesis: r_genesis_moneta=13_000_000_000 This audit
monetary_epoch_windows 524_160 mt-genesis: monetary_epoch_windows=524_160 This audit
Domain registry 33 mt-codec actually 32 domains Расхождение

Total: 16/17 verified, 1 расхождение, 1 out of scope.

Расхождение 33 vs 32 domains — AUDIT.md inaccurate (либо не учли rename TIMECHAIN→TIMECHAIN что не добавил новый, а заменил существующий domain).


7. Известные ограничения этого аудита

Те же что в первом отчёте + специфичные для M2:

  1. ProposalHeader 3722B size не verified (out of M1+M2 scope, в mt-account/mt-consensus — M3-M5 audit)
  2. Spec v33.1.3 содержание не проверено (нулевое доверие) — если spec действительно содержит mt-recovery-fingerprint domain, что-то вроде сверки spec ↔ code не verified в этом аудите
  3. MonetaryState production pin 41/40 не имеет binding test vectors — только regression baselines на 30/29 (M2-3)
  4. VDF compute budget не определён формально — perf concern (M2-6)

Остальное — те же ограничения первого отчёта (side-channel, formal verification, audit firm signature).


8. Recommendations по приоритетам

Приоритет «закрыть до production audit firm engagement» (HIGH)

  1. M2-1 — Финализировать bootstrap pubkeys в Genesis Decree, убрать [0;32] placeholders
  2. DOC-2 — Sync audit-checklist.md с фактическим кодом (line counts, unsafe block locations)

Приоритет «закрыть до v1.0 release» (MEDIUM)

  1. M2-2 — Обновить comment pin 30/29 в mt-state на актуальный 41/40
  2. M2-3 — Добавить binding test vectors apply_step с production pin 41/40
  3. M2-13 — Verify design intent: должна ли MonetaryState влиять на compute_state_root?
  4. DOC-1 — Sync security-cards.md line numbers с фактическим кодом
  5. DOC-3 — Per-primitive Pass 17 checks вместо template inheritance в security-cards

Приоритет «закрыть для документационного качества» (LOW)

  1. M2-4 — Заменить assert!(level <= TREE_DEPTH) на Result или debug_assert!
  2. M2-7 — Добавить checked_mul в next_d для consistency с MonetaryState
  3. M2-8 — Spec reference comment для chain_length_checkpoints[6]
  4. M2-12 — Sync AUDIT.md "33 domains" с фактическими 32

Приоритет «infrastructure improvement» (INFO)

  1. M2-5 — Lazy caching в SparseMerkleTree::root для production scale
  2. M2-9 — Документировать ownership invariant между BTreeMap и SMT
  3. M2-10 — Iterative variant compute_subtree_root для embedded support

9. Итоговая оценка уровня безопасности M2

Шкала такая же как в первом отчёте

Оценка Montana M2: 8 / 10

Обоснование оценки 8:

За что ставлю 8 (положительное):

  1. Pure Rust, ноль unsafe — существенно проще audit surface чем M1
  2. BTreeMap, не HashMap повсеместно — детерминизм гарантирован
  3. Domain separation для всех hash-композиций
  4. Integer-only арифметика (no f32/f64) per [I-9]
  5. Sparse Merkle Tree корректно реализован — все edge cases tested
  6. Genesis Decree фиксирован через singleton (OnceLock) с 19-mutation detection
  7. Carry-recurrence для precision-preserving emission
  8. Overflow detection через checked_mul + descriptive panic
  9. cemented_bundle_aggregate: anti-grinding через canonical sort + window binding + signature exclusion
  10. Adaptive D через permille (×10) integer math с dead zone semantics
  11. 60/60 determinism invariants verified — cross-implementation conformance preparation
  12. 17 mutation cases для encoding (Genesis), 24 для state, 19 для timechain
  13. compute_state_root order-sensitive (mutation на каждом из 3 inputs detected)
  14. All record types fixed-size encoded (2059/2098/2082/4110/24)
  15. Все public API expose iter() для deterministic traversal

За что снимаю 2 (отрицательное):

  1. M2-1 HIGH: Bootstrap pubkeys placeholder zeros — Genesis не финализирован
  2. M2-2/M2-3 MEDIUM: Stale comment + binding tests на устаревшем pin 30/29 (production 41/40)
  3. DOC-2 HIGH: audit-checklist.md повторяет stale info (line counts, unsafe locations) — documentation drift не закрывается
  4. M2-4 MEDIUM: Public API panic'и через assert! (empty_internal) — liveness vulnerability через misuse
  5. M2-13 LOW-MEDIUM: MonetaryState не входит в compute_state_root — fork detection weakened
  6. DOC-1 LOW: security-cards.md line numbers устарели после code growth
  7. M2-7 LOW: Inconsistent overflow handling — next_d без checked_mul vs MonetaryState с checked_mul
  8. Findings из первого аудита (F-1, F-4, F-13, F-18) не закрыты в обновлённой документации
  9. Domain registry count расхождение: AUDIT.md "33", фактически "32"
  10. Те же глобальные ограничения первого отчёта: no fuzzing, no formal verification, no side-channel testing, no audit firm signature

Чтобы поднять до 9: закрыть M2-1, M2-3, M2-13, DOC-2 + sync AUDIT.md/security-cards/audit-checklist с реальным кодом.

Чтобы поднять до 10: + audit firm signature + formal verification SMT и MonetaryState + side-channel hardware testing + production-ready Genesis bootstrap keypairs.

Заключение по M2

M2 layer (state foundation) демонстрирует очень хороший дизайн с pure Rust implementation, ноль unsafe blocks, tight discipline относительно детерминизма (BTreeMap, no f64, no SystemTime). Architecture decisions (carry-recurrence, sorted aggregate, integer permille) — solid и sophisticated.

Главные слабости — не в коде, а в документационной discipline:

  • AUDIT.md и audit-checklist.md содержат stale claims которые должны быть auto-verified в CI
  • Test coverage оставляет gap для production constants (binding vectors на 30/29 вместо 41/40)
  • Genesis bootstrap не финализирован — критично для mainnet deployment

Code per se прошёл бы external audit firm review с относительно небольшим количеством blockers. Documentation drift — embarrassing для self-attestation, но fixable за 1-2 сессии.

Я подтверждаю заявление AUDIT.md "M1 + M2 layers — READY FOR EXTERNAL AUDIT" в части code quality, но отвергаю заявление "documentation accuracy verified" — это empirically false.


10. Метаданные воспроизведения

Команды для проверки findings:

cd "/Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код" && cargo test -p mt-merkle -p mt-genesis -p mt-state -p mt-timechain
cd "/Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код" && wc -l crates/mt-{merkle,genesis,state,timechain}/src/lib.rs
cd "/Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код" && grep -E "pub const [A-Z_]+: &\[u8\]" crates/mt-codec/src/lib.rs | wc -l
cd "/Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код" && grep -rn "unsafe " crates/mt-{merkle,genesis,state,timechain}/src/
cd "/Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код" && grep -rn "f32\|f64" crates/mt-{merkle,genesis,state,timechain}/src/
cd "/Users/kh./Python/Ничто/Монтана/Русский/Протокол/Код" && grep -n "pin 30/29\|inflation_num: 41" crates/mt-state/src/lib.rs crates/mt-genesis/src/lib.rs

Среда выполнения:

  • Платформа: Darwin 24.6.0 (macOS), ARM64 (Apple Silicon)
  • rustc: 1.92.0 (Homebrew)
  • cargo: 1.92.0 (Homebrew)
  • .cargo/config.toml: jobs=1, RUST_TEST_THREADS=1 (verified)

Серверы: montana-moscow и montana-frankfurt — доступны но не использованы в этом incremental аудите (audit surface небольшой, локально достаточно).


11. Cross-reference на первый аудит

Этот отчёт дополняет, не заменяет первый аудит. Findings первого аудита (claude-opus-4-7_2026-04-26_T201805.md) актуальны:

  • F-1..F-19 первого аудита — для M1 layer (mt-crypto, mt-crypto-native, mt-mnemonic) — остаются открытыми до их явного закрытия в коде
  • Этот отчёт добавляет M2-1..M2-14 + DOC-1..DOC-3 для нового scope

Total findings between two reports:

  • Первый отчёт: 19 findings (1 HIGH, 4 MEDIUM, 11 LOW, 3 INFO)
  • Этот отчёт: 17 findings (3 HIGH, 7 MEDIUM, 6 LOW, 1 INFO)
  • Combined: 36 findings для M1 + M2

AUDIT.md заявляет "16/16 findings closed" — это относится к internal critic-mode findings, не к external audit findings. Внешний аудит выявил 36 findings, ни одно из которых не отражено в audit-checklist closure.


Аудитор: Claude Opus 4.7 (1M context) Подпись модели: claude-opus-4-7[1m] Дата создания отчёта: 2026-04-26 Идентификатор аудита: claude-opus-4-7_2026-04-26_T232707 Тип: Incremental external audit (M2 scope only) Cross-reference: claude-opus-4-7_2026-04-26_T201805.md (первый отчёт, M1 scope)