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

736 lines
44 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Внешний аудит кода 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](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 `TIMECHAIN``TIMECHAIN`
- **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
```rust
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:
```rust
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:
```rust
/// 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)`:
```rust
#[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:
```rust
#[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`:
```rust
// 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:
```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.
### 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`:
```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 байта каждый)
- `tree: SparseMerkleTree { leaves: BTreeMap<[u8; 32], Hash32> }` leaf hashes (32 байта каждый)
Memory overhead: ~2× для каждого entry.
При insert:
```rust
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:
```rust
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 после рефакторинга TIMECHAINTIMECHAIN, который не добавил новый 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)
3. **M2-2** Обновить comment `pin 30/29` в mt-state на актуальный 41/40
4. **M2-3** Добавить binding test vectors apply_step с production pin 41/40
5. **M2-13** Verify design intent: должна ли MonetaryState влиять на compute_state_root?
6. **DOC-1** Sync security-cards.md line numbers с фактическим кодом
7. **DOC-3** Per-primitive Pass 17 checks вместо template inheritance в security-cards
### Приоритет «закрыть для документационного качества» (LOW)
8. **M2-4** Заменить `assert!(level <= TREE_DEPTH)` на `Result` или `debug_assert!`
9. **M2-7** Добавить `checked_mul` в `next_d` для consistency с MonetaryState
10. **M2-8** Spec reference comment для `chain_length_checkpoints[6]`
11. **M2-12** Sync AUDIT.md "33 domains" с фактическими 32
### Приоритет «infrastructure improvement» (INFO)
12. **M2-5** Lazy caching в SparseMerkleTree::root для production scale
13. **M2-9** Документировать ownership invariant между BTreeMap и SMT
14. **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](claude-opus-4-7_2026-04-26_T201805.md) (первый отчёт, M1 scope)