AUDIT.md обновлён, расширил scope на **M2 state foundation**. Этот отчёт покрывает только новые части. Уже закрытое в первом отчёте **не повторяю** — для M1 (mt-crypto, mt-crypto-native, mt-mnemonic) findings актуальны и из первого отчёта.
**Не доверяю** ни одному `.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` блоков:
`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:
`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
### 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.
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.
Также `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
/// Panics: при overflow `r * num + carry > u128::MAX`. Encoded arithmetic
/// horizon при pin 30/29 — около 1 930 monetary epochs. ...
```
Но`genesis_params` имеет:
```rust
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)`:
`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`)—internalinvariant
-`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:
```rust
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.
**Описание.** `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`:
```rust
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 байта каждый)
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.
Я подтвердил что `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-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
- 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 не работает.
**Описание.** Каждая 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.
**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)
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.
**Серверы:** 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
AUDIT.md заявляет "16/16 findings closed" — это относится к internal critic-mode findings, не к external audit findings. **Внешний аудит выявил 36 findings, ни одно из которых не отражено в audit-checklist closure.**