Single source of truth для всех известных отклонений реализации от спеки Montana. Введён v1.13.0 роли архитектора кода ([C-10] Mandatory deviation tracker).
Каждый `// SPEC DEVIATION DEV-NNN: ...` в коде ссылается на конкретный entry ниже. Pre-commit hook (`scripts/pre-commit.sh`) сверяет количество.
Закрытые `DEV-N` оставляются в файле как историческая запись со`Status: закрыто (commit <sha>)`.
**Что делает код:** `vdf_chain_length=0` (либо user-provided), без проверки `≥ τ₂`, обходит `apply_noderegistrations_batch` через ручной `CandidatePool::insert`
**Closure path:** реализовать candidate VDF phase в `start.rs` — узел тикает VDF до `vdf_chain_length ≥ τ₂_windows`, после чего автоматически формирует NodeRegistration с правильным `vdf_chain_length` и вызывает `apply_noderegistrations_batch` через canonical pipeline
**Closure cost:** ~14 дней wall-clock на M-class Mac (физика VDF, не код) + ~4 часа кода
**Status:** закрыто (commit `fb204ef` mt-local-node: byte-exact rewrite через canonical apply_proposal)
**Что делает код:** `candidate_vdf_init(&[0u8; 32], &[0u8; 32], &node_id)` — timechain_value и cba как zeros (placeholder)
**Severity:** блокер mainnet ([I-8] violation — нет canonical unpredictable-offline binding)
**Closure path:** на момент формирования NodeRegistration использовать **реальные**`timechain.t_r` и `cemented_bundle_aggregate(W_start - 2, &cemented_node_ids_at_W_start_minus_2)` из локального state узла
**Closure cost:** ~1 час кода после DEV-001 closure
**Status:** закрыто (commit `fb204ef` mt-local-node: byte-exact rewrite через canonical apply_proposal)
---
## DEV-003: Лотерея отсутствует — winner = первый node по lex
**Что делает код:** `state.nodes.iter().next()` — первый узел по `node_id` lex order, **без формирования VDF_Reveal, без endpoint, без weighted_ticket**
**Closure path:** реализовать per окно: формирование `VDF_Reveal` (`mt_lottery::VdfReveal`) с`endpoint = SHA-256("mt-lottery" || T_r || cba || node_id || W LE)`, подпись `node_sk`; вычисление `weighted_ticket_node` через `mt_lottery::weighted_ticket_node`; для singleton — единственный кандидат — argmin тривиален и corretct **через каноничный API**
**Closure cost:** ~6 часов кода
**Status:** закрыто (commit `fb204ef` mt-local-node: byte-exact rewrite через canonical apply_proposal)
---
## DEV-004: BundledConfirmation никогда не формируется
**Spec quote:** «chain_length инкрементируется при cemented `BundledConfirmation`», quorum = `(67 × X + 99) / 100` от active_chain_length
**Что делает код:** `chain_length += 1` напрямую без формирования BC, без подписи `op_hashes/reveal_hashes`, без quorum cementing
**Severity:** блокер mainnet (chain_length грубо инкрементируется на основании несуществующего правила)
**Closure path:** формирование `mt_lottery::BundledConfirmation`с`op_hashes[]` (от Account Table cemented operations) + `reveal_hashes[]` (от cemented VDF_Reveal предыдущего окна) + подпись `node_sk`; cementing через quorum (для singleton — сам себе 100%, проверка через `mt_lottery::is_cemented`)
**Closure cost:** ~8 часов кода
**Status:** закрыто (commit `fb204ef` mt-local-node: byte-exact rewrite через canonical apply_proposal)
---
## DEV-005: ProposalHeader не формируется, Step 4 apply_proposal обойдён
**Что делает код:** напрямую `account.balance += 13_000_000_000` минуя `apply_emission`; ProposalHeader не формируется, `archive_proposal` не вызывается
**Что делает код:** state_root recompute не существует. Singleton mode — узел сам себе proposer и validator, но cross-check всё равно необходим для регулярного self-verification (защита от corruption диска / памяти)
**Severity:** средний (singleton не имеет 2 узлов для cross-check, но self-verification обязателен)
**Closure path:** после формирования `ProposalHeader.state_root`, повторно вычислить `compute_state_root(account_root, node_root, candidate_root)` независимо и сверить byte-exact; mismatch → panic (corruption detected)
**Closure cost:** ~1 час кода
**Status:** закрыто (commit `fb204ef` mt-local-node: byte-exact rewrite через canonical apply_proposal)
**Spec section:** «Адаптация D через participation-ratio feedback»
**Spec quote:** «D адаптируется на границе τ₂ через каноническое chain observation»
**Что делает код:** `timechain.current_d` фиксируется на `D₀=252M`, `next_d` не вызывается
**Severity:** блокер mainnet для long-running узла (>14 дней)
**Closure path:** хранить `participation_history: Vec<u32>` (permille per окно) в timechain state; на каждой τ₂ boundary вычислить median + `next_d(current_d, median, params)`; обновить `timechain.current_d`; для singleton: participation_ratio = всегда 1000 → median=1000 → каждые τ₂ D × 1.03
**Closure cost:** ~3 часа кода
**Status:** закрыто (commit `fb204ef` mt-local-node: byte-exact rewrite через canonical apply_proposal)
---
## DEV-008: selection_event с zeros в advance.rs vs реальный T_r в start.rs
**Что делает код:** `let placeholder = [0u8; 32]` для обоих `t_r` и `cba`. Silent divergence между моими же командами — `start.rs` использует реальный `timechain.t_r`, `advance.rs` использует zeros. Один и тот же state, разные seeds → разные ranking → разные winners для multi-candidate.
**Severity:** блокер mainnet (silent divergence между путями исполнения)
**Closure path:** удалить `advance.rs` целиком — для byte-exact spec нет «быстрой симуляции», есть только реальное исполнение
**Closure cost:** ~10 минут (удаление файла + dispatch update)
**Status:** закрыто (commit `fb204ef` mt-local-node: byte-exact rewrite через canonical apply_proposal)
---
## DEV-009: apply_proposal целиком обойдён — все steps реализованы ручным insert/update
**Что делает код:** напрямую модифицирует `AccountTable`/`NodeTable`/`CandidatePool` вне любого `apply_proposal`. Каждое окно — это ad-hoc набор shortcut'ов, не каноническая state transition.
**Severity:** блокер mainnet (silent divergence реализации от спеки на per-окно basis)
**Closure path:** заменить ad-hoc на canonical `apply_proposal` pipeline через `mt_account::apply_proposal(&mut account_table, &mut node_table, &mut candidate_pool, &proposal_input, params)`. Singleton mode формирует валидный `ProposalInput` для каждого окна и вызывает canonical pipeline.
**Closure cost:** ~16 часов кода (зависит от DEV-001..DEV-006)
**Status:** закрыто (commit `fb204ef` mt-local-node: byte-exact rewrite через canonical apply_proposal)
---
## DEV-011: hardware calibration initial D под target window time
**Crate:** `montana-node`
**File:line:** `crates/montana-node/src/commands/start.rs` функция
`calibrate_d_for_target_window` + первый запуск start
**Spec quote:** «`cemented_sum = Σ chain_length узлов чьи BundledConfirmation попали в included_bundles`. Объект cemented когда `cemented_sum ≥ quorum(active_chain_length)`, где `quorum = (67 × active + 99) / 100`.» (mt-consensus/src/lib.rs:327, mt-lottery/src/lib.rs:503-510)
**Что делает код:** Active-фаза в start.rs формирует proposal где my_node — единственный confirmer (`included_bundles = {my_bundle}`, cemented_sum = my_node.chain_length). Это корректно ТОЛЬКО когда `state.nodes == {my_node}` (1 узел в NodeTable, мой собственный). В multi-node NodeTable my_node.chain_length <quorum(Σ_chain_length)→`is_cemented`возвращаетfalse→узелпадаетс`singleton cementing: cemented=X, active=Y, quorum=Z`.ГардDEV-012добавляетпроверку`state.nodes.len() == 1 && state.nodes.contains(&my_node)`;принеудачепропускаетproposal-блок(break'active_arm),непадает.
**Severity:** блокер mainnet (M9 Phase 2 = apply_proposal от peer-ов не реализован, multi-node консенсус не работает)
**Closure path:** реализовать M9 Phase 2 — drain incoming Proposal envelope (start.rs:160-169), validate через `mt_consensus::validate_acceptance`, `mt_account::apply_proposal` для cemented set от proposer-а, рекомпьют state_root, sync `current_window` + `state.nodes[].chain_length` из peer Proposal. После этого Frankfurt/Helsinki как followers догоняют moscow без необходимости producить собственные singleton-proposal.
**Closure cost:** ~3-5 дней wall-clock на implementation + integration test (e2e_three_peer_apply_proposal)
**Status:** открыто
**Прецедент:** Frankfurt узел стал Active при genesis bootstrap (registration_window=45916, start_window=46032, chain_length=1) и сразу попал в multi-node ситуацию (state.nodes = {msk, fra}). 4 790 рестартов montana-node за 24 часа с ошибкой `singleton cementing: cemented=1, active=25767, quorum=17264` — msk имел chain_length=25766 в state Frankfurt'а (получено через P2P-синхронизацию), fra собственный chain_length=1. Гард предотвращает crash loop; узел остаётся в Active phase, продолжает heartbeat к peer-ам, ждёт M9 Phase 2.