montana/_internal-private/Montana Network v1.1.0 RU.md

3945 lines
312 KiB
Markdown
Raw Normal View History

2026-05-26 21:14:51 +03:00
# Монтана — Спецификация сетевого слоя
2026-05-26 21:14:51 +03:00
**Версия:** 1.1.0 (2026-05-20)
2026-05-26 21:14:51 +03:00
**Слой:** Сетевой — расположен между Протоколом (нижний) и Приложением (верхний).
---
2026-05-26 21:14:51 +03:00
## Введение
2026-05-26 21:14:51 +03:00
Сетевой слой Монтаны охватывает транспорт и discovery между консенсус-узлами и клиентами. Эта спецификация исторически жила как встроенные разделы внутри Montana Protocol; она вынесена в отдельный файл для разделения слоёв по принципу минимальной криптографической поверхности [I-7] и для упрощения независимого аудита.
2026-05-26 21:14:51 +03:00
**Что эта спецификация покрывает:**
2026-05-26 21:14:51 +03:00
- Транспортный слой через libp2p (TCP + Noise_PQ XX → Yamux), где Noise_PQ XX — постквантовое обновление безопасности, заменяющее цепочку классических TLS 1.3 + Noise XK
- Обфускацию трафика (TLS-мимикрия, ECH, padding, тайминг)
- Identity-Bound Tunnel (IBT) — доказательство того, что пир владеет приватным ключом, соответствующим его node_id
- Transport Randomness — непредсказуемые network-bound семена
- PeerRecord и discovery
2026-05-21 03:44:38 +03:00
- Mesh Transport (Bluetooth / Wi-Fi Direct, store-and-forward)
2026-05-26 21:14:51 +03:00
- Протоколы синхронизации (FastSync, BatchLookup, RangeSubscribe, Label Rotation)
- Модель угроз сетевого слоя
- KAT-векторы связывания сетевого слоя
- apply_mesh_frame и apply_store_and_forward — нормативные правила
- Final Gate audit, milestone M6
2026-05-26 21:14:51 +03:00
**Что эта спецификация НЕ покрывает:**
2026-05-26 21:14:51 +03:00
- Машину состояний, apply_proposal, операции (Transfer / OpenAccount / ...) — см. Montana Protocol
- Криптографические примитивы (ML-DSA-65, ML-KEM-768, SHA-256, PBKDF2 / HKDF) — см. Montana Protocol §«Криптография»
- UI / Wallet / Messenger / Channels / Contacts / Profile / Junona / Browser — см. Montana App
---
2026-05-26 21:14:51 +03:00
## Сетевой слой
2026-05-26 21:14:51 +03:00
Все временные параметры сетевого слоя (частота фреймов, окно padding, интервал feeler, таймеры Dandelion) — это implementation guidance для локального сетевого стека узла. Они работают на локальных часах узла и находятся вне области consensus state.
2026-05-26 21:14:51 +03:00
### Транспортная обфускация
2026-05-26 21:14:51 +03:00
Монтана — это персональная сеть. Каждый узел — это персональный сервер участника. Транспортный слой построен из этого определения: персональный сервер отвечает только участникам, персональный мессенджер скрывает тайминг сообщений, персональный = доступный обычному человеку.
2026-05-26 21:14:51 +03:00
#### Шифрование
2026-05-26 21:14:51 +03:00
Все P2P-соединения зашифрованы Noise_PQ XX (эфемерный KEM ML-KEM-768 с обеих сторон + ML-DSA-65 identity + ChaCha20-Poly1305 AEAD), production-рукопожатие транспорта. Inbound-слушатели по умолчанию работают на TCP-порту 8444. Содержимое трафика недоступно наблюдателю.
#### Identity-Bound Tunnel (IBT)
2026-05-26 21:14:51 +03:00
Персональный сервер отвечает только участникам сети. После рукопожатия TLS клиент отправляет доказательство аутентификации. Узлы (зарегистрированные и приглашённые) подписывают парой ключей узла. Аккаунты (клиенты) подписывают парой ключей аккаунта.
```
proof = ML-DSA-65_sign(client_privkey,
2026-05-21 03:44:38 +03:00
"mt-tunnel-online" || server_node_id || floor(current_window_index / 2)
|| online_session_nonce)
2026-05-26 21:14:51 +03:00
где:
online_session_nonce 32B — генерируется клиентом из CSPRNG для каждого
рукопожатия, передаётся в открытой части
IBT-объявления рядом с proof
```
2026-05-26 21:14:51 +03:00
Сервер проверяет:
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
1. Подпись валидна для заявленного client_pubkey
2. Window slot = текущий ИЛИ предыдущий (window = 2 window_index)
3. Уровень доступа — сервер сверяет client_pubkey с тремя таблицами по порядку, первое совпадение определяет уровень:
- `node_id = SHA-256("mt-node" || client_pubkey)` в Node Table → **полный gossip** (клиент подключился парой ключей узла)
- `node_id` с `node_pubkey = client_pubkey` в Candidate Pool → **read-only gossip**: получает proposals (кандидат подключился парой ключей узла)
- `account_id = SHA-256("mt-account" || suite_id || client_pubkey)` в Account Table → **подключение к доверенному узлу** (клиент подключился парой ключей аккаунта)
- Ни одно не совпало → reject
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
Условия 1-2 выполнены + уровень доступа из шага 3 определён → рукопожатие Noise → P2P-сеть Монтаны на соответствующем уровне доступа.
Любое условие не выполнено → TLS alert `bad_certificate`, close. Это стандартное поведение сервера для обязательной клиентской аутентификации — таких серверов в интернете миллионы (корпоративные порталы, API, банковские системы).
2026-05-26 21:14:51 +03:00
Защита от replay: трёхслойная одновременно. (a) `server_node_id` привязывает proof к конкретному получателю — replay против другого сервера невозможен. (b) Window slot ограничивает окно replay двумя окнами (≤120 секунд на genesis-калибровке) — proof становится невалидным после одного окна. (c) **Отслеживание session nonce.** Сервер хранит `used_online_nonces[client_pubkey]` — множество значений `online_session_nonce`, использованных этим клиентом в текущем / предыдущем окне. При получении proof с `online_session_nonce ∈ used_online_nonces[client_pubkey]` → reject (replay внутри window slot). Pruning множества: записи старше 2 окон удаляются (повторное использование nonce допустимо после истечения — window slot уже невалиден). Это defence-in-depth закрытие класса MITM-replay внутри window slot — даже если proof перехвачен, атакующий не может повторно использовать его против того же сервера в течение этих 2 окон.
2026-05-26 21:14:51 +03:00
**Процедура верификации для online-пира.** При получении online IBT-объявления серверный узел выполняет проверки в следующем порядке (зеркало mesh IBT-процедуры):
2026-05-26 21:14:51 +03:00
1. Парсинг объявления; извлечение `client_pubkey`, `online_session_nonce` (32 B), `proof` (3309 B ML-DSA-65 signature).
2. Проверка того, что подпись ML-DSA-65 валидна для `client_pubkey` над реконструкцией сообщения `"mt-tunnel-online" || server_node_id || u64_LE(floor(current_window_index / 2)) || online_session_nonce` (текущий window slot). Если не совпадает — попробовать с `floor(current_window_index / 2) - 1` (предыдущий slot).
3. Проверка `server_node_id == local_node_id` (proof привязан к этому серверу).
4. Проверка `online_session_nonce ∉ used_online_nonces[client_pubkey]` (блок replay).
5. Поиск `client_pubkey` по трём таблицам по порядку: Node Table (через `SHA-256("mt-node" || client_pubkey)` lookup) → Candidate Pool → Account Table (через `SHA-256("mt-account" || suite_id || client_pubkey)`). Первое совпадение определяет уровень доступа. Ни одно не совпало — reject.
6. Все проверки пройдены → accept; добавить `online_session_nonce` в `used_online_nonces[client_pubkey]`; запустить рукопожатие Noise; запустить P2P-сессию на уровне доступа из шага 5.
7. Любая проверка не пройдена → TLS alert `bad_certificate`, close. Silent reject опционален (не давать атакующему обратной связи о том, какой шаг не прошёл).
2026-05-26 21:14:51 +03:00
**Граница памяти `used_online_nonces`.** Множество per `client_pubkey`, эфемерное, transport-слой (не consensus state — [I-14] формально не применяется). DoS bound на двух уровнях: (i) per-pubkey множество ограничено `MAX_ONLINE_NONCES_PER_PUBKEY = 256` (атакующий с одной парой ключей не может flood более 256 nonces per window slot — rate-limit рукопожатий уже ограничивает реальный поток ниже < 256 / ₁); (ii) глобальный bound `MAX_ONLINE_NONCES_TOTAL = 65 536` (32 B × 65536 2 MB per сервер приемлемая память на commodity hardware [I-5]). Pruning на границе window slot: записи старше 2 окон удаляются автоматически. Защита от множества разных client_pubkeys: server-side rate-limit рукопожатий (см. правило backpressure [B5] в спеке `max_pending_requests_per_peer`) ограничивает поток новых рукопожатий per source.
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
Bootstrap-исключение: genesis bootstrap-узлы захардкожены как `(IP, node_id, pubkey) × 12`. Bootstrap принимает proof от любого валидного ключа ML-DSA-65 (Account Table не сверяется). Для защиты от connection flood клиент прикрепляет proof-of-work:
```
SHA-256("mt-bootstrap-pow" || proof || nonce) < target
```
2026-05-26 21:14:51 +03:00
`target` выбран так, чтобы стоимость составляла ≈ 100 мс CPU. PoW требуется только при подключении к bootstrap, не к обычным пирам.
2026-05-26 21:14:51 +03:00
**Расширение IBT для mesh-транспорта.**
2026-05-26 21:14:51 +03:00
Mesh-транспорт (см. подсекцию «Mesh Transport» ниже) работает без свежего `window_index` — устройство может быть offline часами или днями до следующей синхронизации с сетью со стороны интернета. IBT-proof в mesh-контексте использует **закэшированный** `window_index` — последнее известное значение окна из любого предыдущего online-подключения.
2026-05-26 21:14:51 +03:00
Формула для mesh-транспорта:
```
mesh_proof = ML-DSA-65_sign(
client_privkey,
"mt-tunnel-mesh"
|| peer_node_id
|| floor(cached_window_index / 2)
|| mesh_session_nonce)
2026-05-26 21:14:51 +03:00
где:
cached_window_index u32 — последнее известное значение окна из любого
предыдущего online-рукопожатия или
2026-05-21 03:44:38 +03:00
gossiped proposal
2026-05-26 21:14:51 +03:00
mesh_session_nonce 32B — генерируется инициатором рукопожатия
из CSPRNG, передаётся в открытой
части mesh-объявления
```
2026-05-26 21:14:51 +03:00
**Приемлемая граница устаревания.** Пир принимает `cached_window_index` в диапазоне `[peer.known_window_index - 7 × τ₁, peer.known_window_index]`. Свыше `7 × τ₁` закэшированное значение считается слишком устаревшим — пир отклоняет mesh IBT-рукопожатие и требует свежее значение через любой доступный канал до продолжения.
2026-05-26 21:14:51 +03:00
**Отслеживание session nonce.** Пир хранит `used_nonces[sender_pubkey]` — множество значений `mesh_session_nonce`, использованных этим отправителем в окне приемлемого устаревания. При получении proof с `mesh_session_nonce ∈ used_nonces[sender_pubkey]` → reject (replay). Pruning множества: записи старше `7 × τ₁` удаляются (повторное использование nonce после истечения допустимо — cached_window_index больше не валиден).
2026-05-26 21:14:51 +03:00
**Domain separator ОБЯЗАН быть `mt-tunnel-mesh`, не `mt-tunnel-online`.** Отдельный separator критичен — иначе атакующий, перехвативший online IBT-proof (window slot = `2 × τ₁` replay), мог бы повторно использовать его в mesh-контексте, где окно устаревания расширено до `7 × τ₁`. Cross-context replay блокируется на уровне domain-separation.
2026-05-26 21:14:51 +03:00
**Анализ поверхности replay.**
- Online IBT (separator `mt-tunnel-online`): окно replay `2 × τ₁` — узкое, плюс per-nonce tracking блокирует replay внутри window slot (см. защиту от Replay выше).
- Mesh IBT (separator `mt-tunnel-mesh`): окно replay расширено до `7 × τ₁`, но replay блокируется per-nonce tracking.
- Cross-context: domain separation делает proof для одного контекста невалидным в другом.
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
**Процедура верификации для mesh-пира.**
1. Парсинг объявления; извлечение `sender_pubkey`, `mesh_session_nonce`, `proof`.
2. Проверка подписи против `sender_pubkey`.
3. Восстановление `cached_window_index` из реконструкции сообщения proof (пир знает sender_pubkey, peer_node_id известен локально; пир пробует диапазон `[known_window_index - 7 × τ₁, known_window_index]` до нахождения совпадающего значения; если нет совпадения — reject).
4. Проверка `mesh_session_nonce ∉ used_nonces[sender_pubkey]`.
5. Всё ok → accept; добавить `mesh_session_nonce` в used_nonces; запустить mesh-сессию.
6. Любая проверка не пройдена → silent reject (без сообщения об ошибке, чтобы не давать атакующему обратной связи).
#### Uniform Framing
2026-05-26 21:14:51 +03:00
Все сообщения Монтаны внутри IBT-соединения фрагментируются на фреймы фиксированного размера:
```
frame_size = 1024 bytes
2026-05-26 21:14:51 +03:00
формат фрейма:
flags 1B (0x01 = data, 0x02 = padding, 0x04 = continuation)
2026-05-26 21:14:51 +03:00
length 2B (длина payload, ≤1021B)
payload 1021B (данные или случайный padding до frame_size)
```
2026-05-26 21:14:51 +03:00
Персональный мессенджер скрывает тайминг: между узлами течёт непрерывный поток фреймов. Реальные сообщения Монтаны заменяют padding-фреймы, а не добавляются к ним. Сетевой наблюдатель не может отличить перевод от proof of time от тишины — всё одни и те же зашифрованные фреймы.
2026-05-26 21:14:51 +03:00
Параметры:
2026-05-26 21:14:51 +03:00
- Baseline частота фреймов: 1 фрейм/с на исходящих соединениях. Inbound — фреймы когда данные доступны
- Максимальный burst: ≤ 8 фреймов подряд без паузы ≥ 10 мс
- Минимальное соотношение padding: ≥ 20% фреймов внутри скользящего окна τ₁ на исходящих
2026-05-26 21:14:51 +03:00
Персональный = доступный: 24 исходящих × 1 фрейм/с × 1024 байта = 24 KB/с ≈ 60 GB/месяц. Приемлемо для домашнего сервера.
#### Transport Randomness
2026-05-26 21:14:51 +03:00
Все рандомизированные решения транспортного слоя (stem routing, планирование фреймов, генерация nonce) используют CSPRNG, посеянный из OS entropy pool. Детерминированный PRNG, выведенный из состояния узла, запрещён для рандомности транспортного слоя.
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
Транспортная обфускация ортогональна консенсусу. TimeChain и машина состояний работают над любым транспортом без изменений.
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
#### Постквантовая миграция транспорта (milestone M6)
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
**Историческое состояние (до закрытия M6).** Транспортный слой использовал TLS 1.3 с классическим X25519 ECDHE для внешнего туннеля и Noise XK (Diffie-Hellman над X25519) для внутренней аутентификации пира. Оба классических рукопожатия были уязвимы к атакам store-now-decrypt-later со стороны будущего квантового противника. Эта уязвимость закрыта переключением production-транспорта на Noise_PQ XX (см. следующий параграф и раздел wire format).
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
**Статус M6 — закрыт.** Production-транспорт теперь — единое постквантовое рукопожатие: Noise_PQ XX с эфемерными парами ключей ML-KEM-768 с обеих сторон рукопожатия, подписями личности ML-DSA-65 над транскриптом и фреймированием ChaCha20-Poly1305 AEAD на установленной сессии. Цепочка классических TLS 1.3 + Noise XK удалена из стека libp2p (она не давала никакого свойства безопасности сверх IBT-аутентифицированного внутреннего слоя; роль DPI-обфускации сохраняется uniform framing-ом поверх Noise_PQ XX). Конфиденциальность транспорта постквантовая end-to-end. PeerId выводится из открытого ключа личности ML-DSA-65 каждого пира как SHA-256-multihash (libp2p / IPFS sha2-256 multihash code 0x12), так что криптографические и маршрутные личности привязаны к одному и тому же ключевому материалу.
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
**Multi-confirmer cementing protocol.** Bootstrap genesis-когорта сегодня работает в режиме singleton-cementing (proposer является единственным confirmer-ом, поскольку его `chain_length` доминирует после длительной работы; все остальные операторы имеют `chain_length ≤ 1` от недавней регистрации). Multi-confirmer cementing protocol, формализованный ниже, — это нормативный путь для операционных режимов, в которых non-bootstrap операторы накапливают non-negligible `chain_length` за многие эпохи τ₂.
2026-05-26 21:14:51 +03:00
Шаги протокола per window `W`:
2026-05-26 21:14:51 +03:00
1. **Candidate Proposal.** Канонический proposer (per выбор лотереи `W 2`) конструирует `ProposalHeader` с `included_bundles_root`, установленным в `H(my_own_BC)`, и broadcast-ит header (`MsgType::Proposal`, 0x22).
2. **Follower confirmation.** Каждый Active-оператор при получении candidate Proposal (a) проверяет, что proposer каноничен для `W`, (b) подписывает свой собственный `BundledConfirmation(W, my_endpoint, my_op_hashes, my_reveal_hashes)`, где `my_endpoint = SHA-256("mt-lottery" || T_r(W) || cemented_bundle_aggregate(W-2) || my_node_id || W)`, (c) broadcast-ит BC envelope (`MsgType::BundledConfirmation`, 0x20) к proposer и ко всем подключённым пирам.
3. **Proposer accumulator.** Proposer поддерживает per-window BC accumulator. На каждый входящий BC proposer валидирует per `validate_bundle` и, если валиден, добавляет BC в accumulator. Окно accumulator закрывается когда `cemented_sum = Σ node.chain_length` по всем собранным confirmer-ам достигает `quorum(active_chain_length) = ⌈67 × Σ active_chain_length / 100⌉`, или когда одно окно `τ_1` истекло с момента broadcast candidate Proposal (что произойдёт раньше).
4. **Cemented Proposal broadcast.** Proposer строит финальный ProposalHeader с `included_bundles_root` = sparse Merkle root над собранным набором BC и broadcast-ит cemented envelope как `MsgType::Proposal` (0x22). Cemented envelope расширен из legacy 3722-байтного header-only layout до length-prefixed схемы `[header (3722 B)][u16 bundle_count][bundle_count × BC]`, где каждый BC — переменной длины per стандартный `BundledConfirmation::encode` (node_id 32 B + endpoint 32 B + window_index 8 B + u16 op_hashes count + N × 32 B + u16 reveal_hashes count + N × 32 B + signature 3309 B).
5. **Follower cementing.** Каждый follower при получении cemented Proposal (a) парсит набор bundle, (b) вызывает `validate_bundle` на каждом BC с `expected_endpoint = SHA-256("mt-lottery" || T_r(W) || cemented_bundle_aggregate(W-2) || bc.node_id || W)`, (c) строит `ProposalSettle { window_w: W, winner_id, cemented_confirmers: [all signers] }`, (d) вызывает `apply_proposal` с multi-confirmer set.
2026-05-26 21:14:51 +03:00
Путь singleton-cementing (единственный confirmer, используется сегодня на genesis-когорте, пока `chain_length` доминирует bootstrap proposer-ом) — это частный случай протокола выше с `bundle_count = 1`; 3722-байтный header-only envelope обрабатывает этот частный случай. Операционные режимы с non-bootstrap операторами, накапливающими `chain_length`, требуют расширенной length-prefixed схемы и валидации `bundle_count ≥ 1`.
2026-05-26 21:14:51 +03:00
Требование консистентности T_r. Follower-овский `compute_endpoint` требует канонического `T_r(W)`. Два implementation-пути жизнеспособны: (i) follower-ы тикают VDF локально в lockstep с wall-clock и кэшируют per-window `T_r` history во время catch-up; (ii) proposer расширяет cemented Proposal envelope с `T_r(W)` (32 B), так что follower-ам не нужно вычислять его локально. Путь (ii) — более чистый архитектурный выбор; он добавляет 32 B per envelope и убирает любую опасность дрейфа для follower-ов, чей VDF-тик отстаёт от cemented head.
Wire-format и KAT-векторы для cemented Proposal envelope с `bundle_count ≥ 2` нормативно специфицированы в разделе Network spec выше. Cross-implementation conformance binding для схемы отслеживается под milestone M9.
**Фазы миграции.**
- **Фаза 0 — Архитектура и scaffolding (закрыто).** Запись DEV-014 в `Code/docs/SPEC_DEVIATIONS.md` документировала план миграции; capability detection было зарезервировано через wire-поле `pq_transport_version` в IBT-объявлении.
- **Фаза 1 — Имплементация рукопожатия Noise_PQ (закрыто).** Кастомный обработчик Noise_PQ написан вне noise upgrade модуля libp2p в `crates/mt-noise-pq`. Оборачивает подпись личности ML-DSA-65 над транскриптом, который включает оба эфемерных открытых ключа ML-KEM-768 и шифротексты ML-KEM-768. KAT-векторы заведены в `crates/mt-noise-pq/tests/kat.rs`.
- **Фаза 2 — Редизайн XK → XX (закрыто).** Начальный вариант XK требовал, чтобы инициатор знал статический открытый ключ ML-KEM-768 ответчика a priori — несовместимо с plug-in слотом `with_tcp` auth-upgrade libp2p, который даёт upgrade-у только локальный `libp2p::identity::Keypair` (Ed25519). Редизайн XX обнаруживает удалённую личность во время рукопожатия (эфемерные пары ключей ML-KEM-768 с обеих сторон; identity ML-DSA-65 pk передаётся в msg2 / msg3 и аутентифицируется подписью над транскриптом). Новый wire format задокументирован в этом разделе.
- **Фаза 3 — Удаление классики (закрыто).** Классическая auth-цепочка libp2p `(tls::Config::new, noise::Config::new)` в `mt-net-transport::transport::build_swarm_with_keypair` заменена на `NoisePqXxConfig`. Транспортный стек теперь — `TCP → Noise_PQ XX → Yamux`. Слой uniform framing сохранён (он обеспечивает DPI-обфускацию ортогонально рукопожатию). Поле `pq_transport_version` остаётся зарезервированным-но-неиспользуемым для будущего согласования протокола, если multistream-select окажется недостаточным.
**Верификация на genesis 3-узловой сети.** Каждая фаза верифицируется на трёх production-узлах (Москва, Хельсинки, Франкфурт) в течение ≥24 часов непрерывной работы перед объявлением закрытой. Закрытие Фазы 1 требует byte-exact KAT-векторов, заведённых в `mt-conformance`, и cross-node success рукопожатия с нулевым classical fallback в окне наблюдения.
**Статус соответствия [I-1].** Закрыто. Весь стек протокола — постквантовый end-to-end: consensus-подписи через ML-DSA-65, application-layer шифрование через ML-KEM-768, рукопожатие транспорта через Noise_PQ XX (ML-KEM-768 + ML-DSA-65). Никакого классического Diffie-Hellman не осталось в слое протокола.
**Wire format (нормативный, production XX).** Рукопожатие Noise_PQ XX — это ровно три сообщения. Идентификаторы в формулах ниже соответствуют эталонной реализации в `crates/mt-noise-pq/src/xx_handshake.rs`. libp2p multistream-select идентификатор протокола для рукопожатия — **`/montana/noise-pq-xx/1.0.0`** — эта строка является авторитетным именем протокола, используемым для согласования upgrade между двумя пирами.
| Сообщение | Размер (B) | Поля |
|---------|---------:|--------|
| msg1 (инициатор → ответчик) | 1184 | `ke_pk_i` (ML-KEM-768 pk, 1184 B) |
| msg2 (ответчик → инициатор) | 7533 | `ke_pk_r` (1184 B) ‖ `ct_i` (ML-KEM-768 ct к `ke_pk_i`, 1088 B) ‖ `rs_id_pk` (ML-DSA-65 pk, 1952 B) ‖ `sig_r` (ML-DSA-65 sig, 3309 B) |
| msg3 (инициатор → ответчик) | 6349 | `ct_r` (ML-KEM-768 ct к `ke_pk_r`, 1088 B) ‖ `is_id_pk` (1952 B) ‖ `sig_i` (3309 B) |
Хэш транскрипта (вход в `sig_r` и `sig_i`) — это байтовая конкатенация msg1 плюс msg2-prefix-without-`sig_r` (для `sig_r`), либо msg1 ‖ full-msg2 ‖ msg3-prefix-without-`sig_i` (для `sig_i`). Каждый вход подписи domain-separated с `mt-noise-pq-xx-v1-sig-r` и `mt-noise-pq-xx-v1-sig-i` соответственно.
Сессионные ключи выводятся domain-separated SHA-256 над конкатенацией обоих общих секретов ML-KEM-768 и полного транскрипта:
```
master = SHA-256("mt-noise-pq-xx-v1-master" ‖ ss_i ‖ ss_r ‖ transcript)
sk_i_to_r = SHA-256("mt-noise-pq-xx-v1-i2r" ‖ master)
sk_r_to_i = SHA-256("mt-noise-pq-xx-v1-r2i" ‖ master)
```
`sk_i_to_r` и `sk_r_to_i` — это 32-байтные ключи для ChaCha20-Poly1305 AEAD; AEAD-wrapped byte stream выставлен как `mt_noise_pq::stream::NoisePqStream`.
**Деривация PeerId (не-libp2p-стандарт).** PeerId выводится как SHA-256-multihash (libp2p / IPFS sha2-256 multihash code 0x12) над сырым байтовым представлением открытого ключа личности ML-DSA-65 каждого пира. Это **не** libp2p-стандартная деривация PeerId, которая является multihash от protobuf-encoded `PublicKey` сообщения с одним из libp2p-встроенных типов ключей (`RSA`, `Ed25519`, `Secp256k1`, `ECDSA`). Нестандартная деривация намеренна — Монтана использует ML-DSA-65 как cross-network личность per [I-1], которая не является одним из libp2p-встроенных типов ключей, и protobuf-обёртка добавила бы около пятидесяти байт overhead-а без дополнительной безопасности. Два следствия: (a) обычный libp2p-узел не может восстановить открытый ключ из Монтана PeerId, и (b) Монтана-узлы не принимают libp2p-стандартные PeerId, потому что у них нет ключа ML-DSA-65 для их верификации.
**Legacy вариант XK** (`/montana/noise-pq/1.0.0`, `crates/mt-noise-pq/src/lib.rs`) сохранён для справки и для KAT continuity, но больше не подключён к транспорту libp2p. Его wire-размеры (msg1 2272 B, msg2 6349 B, msg3 5261 B) и его требование того, чтобы инициатор знал статический KEM pk ответчика a priori, сделали его несовместимым с plug-in libp2p. XK — это более старая форма, задокументированная здесь для полноты.
2026-05-21 03:44:38 +03:00
```
2026-05-26 21:14:51 +03:00
msg1 (инициатор → ответчик) 2272 B
ke_pk 1184 B эфемерный открытый ключ ML-KEM-768 инициатора
ct_rs 1088 B шифротекст KEM, инкапсулированный к статическому
открытому ключу ML-KEM-768 ответчика (rs_kem_pk, известный
инициатору a priori через IBT directory)
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
msg2 (ответчик → инициатор) 6349 B
ct_e 1088 B шифротекст KEM, инкапсулированный к ke_pk
rs_id_pk 1952 B статический открытый ключ личности ML-DSA-65 ответчика
sig_r 3309 B подпись ML-DSA-65 ключом rs_id над
signature-domain хэшем
2026-05-21 03:44:38 +03:00
SHA-256("mt-noise-pq-v1-sig-r" ‖ ke_pk ‖ ct_rs ‖ ct_e)
2026-05-26 21:14:51 +03:00
msg3 (инициатор → ответчик) 5261 B
is_id_pk 1952 B статический открытый ключ личности ML-DSA-65 инициатора
sig_i 3309 B подпись ML-DSA-65 ключом is_id над
signature-domain хэшем
2026-05-21 03:44:38 +03:00
SHA-256("mt-noise-pq-v1-sig-i" ‖ ke_pk ‖ ct_rs ‖ ct_e
2026-05-26 21:14:51 +03:00
‖ rs_id_pk ‖ is_id_pk)
2026-05-21 03:44:38 +03:00
```
2026-05-26 21:14:51 +03:00
Обе стороны выводят идентичные directional session keys (каждый 32 B) и 32 B хэш транскрипта, выставленный верхним слоям как channel-binding token:
2026-05-21 03:44:38 +03:00
```
master = SHA-256("mt-noise-pq-v1-master" ‖ ss_rs ‖ ss_e
‖ ke_pk ‖ ct_rs ‖ ct_e
‖ rs_id_pk)
sk_i_to_r = SHA-256("mt-noise-pq-v1-i2r" ‖ master)
sk_r_to_i = SHA-256("mt-noise-pq-v1-r2i" ‖ master)
transcript_hash = SHA-256("mt-noise-pq-v1-transcript" ‖ ke_pk ‖ ct_rs ‖ ct_e)
```
2026-05-26 21:14:51 +03:00
где `ss_rs = mlkem_decap(rs_kem_sk, ct_rs)` и `ss_e = mlkem_decap(ke_sk, ct_e)` на принимающей стороне соответственно, и соответствующие `mlkem_encap(rs_kem_pk) → (ct_rs, ss_rs)` и `mlkem_encap(ke_pk) → (ct_e, ss_e)` на отправляющей стороне. Семантика implicit-rejection FIPS 203 §6.3 примиряется проверкой подписи личности: злонамеренно подменённый шифротекст даёт другой общий секрет у получателя, master транскрипта расходится с отправительским, и проверка подписи личности не проходит (получатель возвращает `BadResponderSignature` либо `BadInitiatorSignature`).
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
**Post-handshake AEAD framing.** После завершения 3-сообщенческого рукопожатия каждый application-layer byte stream между пирами шифруется ChaCha20-Poly1305 с использованием выведенных directional session keys. Wire format per direction:
```
2026-05-26 21:14:51 +03:00
direction = initiator → responder использует sk_i_to_r
direction = responder → initiator использует sk_r_to_i
2026-05-26 21:14:51 +03:00
per frame (каждое application-сообщение):
length_be 2 B big-endian u16, общая длина ciphertext включая
16-байтный Poly1305 tag (max 65 535 → max plaintext
2026-05-21 03:44:38 +03:00
65 519 = u16::MAX tag)
2026-05-26 21:14:51 +03:00
ciphertext+tag ChaCha20-Poly1305 шифрование plaintext с
nonce = 0x00000000 ‖ u64_be(counter), 12 байт;
counter начинается с 0 и инкрементируется на 1 per
исходящий фрейм в каждом направлении независимо;
2026-05-21 03:44:38 +03:00
AAD = empty (none)
```
2026-05-26 21:14:51 +03:00
64-битный nonce counter монотонен per direction и гарантированно не переполнится ни при каком реалистичном rate сообщений (2^64 фреймов на 1 Gbit / s с 64 KiB фреймами ≈ 2^48 лет). Реализации ОБЯЗАНЫ прервать соединение (рассматривать как протокольную ошибку) если counter переполнился бы.
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
Application-layer сообщения больше 65 519 байт фрагментируются вызывающей стороной (например, stream multiplexer-ом libp2p); AEAD-слой не предусматривает in-band фрагментацию.
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
**Согласование возможностей.** Основной механизм согласования — libp2p multistream-select: каждый пир объявляет набор upgrade-протоколов, которые он поддерживает (`/montana/noise-pq/1.0.0` для рукопожатия Noise_PQ, определённого здесь, `/noise` или эквивалент для классического fallback), и соединение согласует наивысший взаимно поддерживаемый. Это стандартная конвенция libp2p и не требует дополнительных wire-полей.
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
1-байтное поле `pq_transport_version` **зарезервировано, но в настоящее время не потребляется** layout-ом IBT-объявления — оно держится в резерве для будущего явного out-of-band сигнала, если multistream-select окажется недостаточным (например, если какая-то политика оператора требует выразить «я поддерживаю Noise_PQ, но отказываюсь fallback к классике»). Значения когда поле станет активным:
2026-05-26 21:14:51 +03:00
| `pq_transport_version` | Значение |
2026-05-21 03:44:38 +03:00
|---|---|
2026-05-26 21:14:51 +03:00
| `0x00` | Классический TLS 1.3 + Noise XK (до M6) |
| `0x01` | Noise_PQ v1 (эта спека, ML-KEM-768 + ML-DSA-65, без гибридного X25519) |
2026-05-26 21:14:51 +03:00
Будущие версии (`0x02`, …) могут добавить гибридный X25519+ML-KEM, бо́льшие PQ KEM или другие уточнения; поле резервирует поверхность согласования.
2026-05-26 21:14:51 +03:00
**Cross-implementation conformance.** Эталонные test-векторы в `crates/mt-noise-pq/tests/kat.rs` фиксируют seed статической пары ключей ML-KEM-768 ответчика (`byte_repeat(0x42, 64)`) и два seed личностей ML-DSA-65 (`byte_repeat(0x77, 32)` для ответчика, `byte_repeat(0xAA, 32)` для инициатора). 3-сообщенческое рукопожатие на этих фиксированных входах должно производить byte-идентичные `sk_i_to_r`, `sk_r_to_i` и `transcript_hash` между реализациями. Per-message wire-байты будут отличаться от запуска к запуску, потому что шаг инкапсуляции использует свежую OS-рандомность per FIPS 203 §6.2; только выведенный сессионный материал является byte-exact детерминированным для cross-implementation верификации.
2026-05-26 21:14:51 +03:00
### Выбор пиров
2026-05-26 21:14:51 +03:00
Открытый вход с барьером sequential-SHA-256 делает Sybil-узлы дорогими: каждый Sybil = τ₂ окон sequential SHA-256 (не параллелизуется) + событие selection. Выбор пиров использует ограничения diversity из protocol-level данных (start_window) и network-level данных (/16, ASN).
2026-05-26 21:14:51 +03:00
P2P gossip — только зарегистрированные и приглашённые узлы (IBT уровни 1-2, см. Transport Obfuscation → Identity-Bound Tunnel). Аккаунты (IBT уровень 3) взаимодействуют через свой доверенный узел.
2026-05-26 21:14:51 +03:00
#### Исходящие соединения
2026-05-26 21:14:51 +03:00
24 исходящих, все full. Uniform framing скрывает типы сообщений — отдельные relay-only соединения не нужны.
2026-05-26 21:14:51 +03:00
Selection: random 50/50 из таблиц «new» и «verified». Бакетирование с секретным ключом узла. Никакого предпочтения по chain_length — selection равномерный.
2026-05-26 21:14:51 +03:00
#### Четыре ограничения diversity
2026-05-26 21:14:51 +03:00
Каждое исходящее соединение проверяется по всем четырём ограничениям:
2026-05-21 03:44:38 +03:00
```
2026-05-26 21:14:51 +03:00
Сеть:
/16 — не более 1 исходящего per /16 подсеть (IPv4) или /48 (IPv6)
ASN — не более 2 исходящих per автономная система
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
Протокол:
start_window — не более 2 исходящих к узлам с start_window внутри одной τ₂
2026-05-21 03:44:38 +03:00
```
2026-05-26 21:14:51 +03:00
Сетевые ограничения: diversity /16 и ASN. Protocol-level ограничение start_window канонически доступно из Node Table.
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
Следствие: Sybil-кластер, все зарегистрированные в одной τ₂ → не более 2 из 24 слотов. Eclipse требует узлов в 7+ разных AS в 7+ разных /16 с регистрациями в 7+ разных τ₂.
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
ASN-карта загружается при старте. Без карты — fallback на /16.
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
#### Менеджер адресов
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
Две таблицы:
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
- **New** — адреса, полученные через peer exchange и DHT. Узел ещё не подключался
- **Verified** — адреса, к которым узел успешно подключался через IBT
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
Бакетирование: `bucket = Hash(secret_key, source_group, addr_group) % N`. Детерминированно с секретным ключом — атакующий не может предсказать в какой бакет попадёт его адрес.
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
#### Входящие соединения
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
До 32 входящих. При overflow — eviction:
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
1. Защитить 4 с самым низким ping
2. Защитить 4 с самыми недавними полезными сообщениями (любое валидное сообщение Монтаны, которое узел ещё не видел)
3. Защитить до 8 из разных подсетей (по одному из каждой)
4. Защитить 4 с самыми недавними proposals
5. Из остальных — evict из самой большой группы подсетей
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
#### Якоря
2026-05-21 03:44:38 +03:00
2026-05-26 21:14:51 +03:00
2 исходящих соединения с самым длинным uptime сохраняются каждую τ₂. При перезапуске после краха или обновления — подключаться к якорям первыми, до любого случайного выбора из таблиц.
#### Feeler
2026-05-26 21:14:51 +03:00
Раз в 10 τ₁: подключиться к случайному адресу из «new», выполнить IBT-рукопожатие (все три уровня верификации). Успех на любом уровне → переместить в «verified» с тегом уровня (node / invited / account). Неудача → пометить или удалить.
2026-05-26 21:14:51 +03:00
#### Ротация
2026-05-26 21:14:51 +03:00
По поведению: если пир не передал ни одного нового proposal за τ₂ — заменить его. Пир с более чем 50% невалидных сообщений в скользящем окне τ₁ — отключить, с τ₂ ban-ом на reconnection. Пир, который честно передаёт, полезен сети и остаётся.
#### PeerRecord
2026-05-26 21:14:51 +03:00
Формат PeerRecord, используемый в peer exchange:
```
PeerRecord:
ip 16B (IPv4-mapped IPv6)
port 2B (u16)
node_id 32B
node_pubkey 1952B (ML-DSA-65)
```
2026-05-26 21:14:51 +03:00
Без node_id и node_pubkey клиент не может вычислить IBT-proof для подключения. Peer exchange: не более 100 PeerRecord на сообщение. Не более 1 peer-exchange сообщения per τ₁ от каждого пира.
2026-05-21 03:44:38 +03:00
### Censorship-resistant discovery
2026-05-26 21:14:51 +03:00
Genesis: 12 захардкоженных bootstrap-узлов `(IP, node_id, pubkey)`. Если все 12 IP блокированы на уровне страны — новый узел не может войти в сеть. Пять независимых каналов discovery. Одного из пяти достаточно.
2026-05-26 21:14:51 +03:00
**1. Peer exchange.** Каждый узел держит и передаёт список активных пиров новичкам. Знание IP одного узла достаточно — друг, QR-код, мессенджер. Один живой контакт = вход в сеть.
2026-05-26 21:14:51 +03:00
**2. DHT.** Kademlia DHT поверх libp2p. Узлы находят друг друга без центральной точки. Идентификаторы рандомизированы — DHT не раскрывает node_id до установления Монтана-соединения.
2026-05-26 21:14:51 +03:00
**3. Bridge-узлы.** Узлы вне цензурируемой юрисдикции, опубликованные через out-of-band каналы (соцсети, мессенджеры, печатные QR-коды). IP bridge-узла неизвестен файрволлу пока не использован.
2026-05-26 21:14:51 +03:00
**4. Encrypted Client Hello (ECH).** Bootstrap через CDN, поддерживающий ECH. SNI зашифрован — наблюдатель видит CDN IP, но не целевой домен. Эффективно в юрисдикциях без активного блокирования ECH-расширения. В юрисдикциях, блокирующих ECH (Китай с 2023, Россия с 2024) — этот канал нефункционален. Для таких юрисдикций — каналы 1-3 и 5.
2026-05-26 21:14:51 +03:00
**5. Mesh peer exchange.** При полном отсутствии интернет-доступа (state shutdown, потеря inter-zone connectivity, локальная изоляция) узел обнаруживает локальных пиров через mesh-транспорт (Bluetooth LE advertisement, Wi-Fi Direct service discovery). Peer exchange работает на уровне mesh-фрейма с `frame_type = 0 (discovery)` — см. подсекции «Mesh Transport» и «Store-and-Forward Semantics». Физический радиус discovery — десятки метров; mesh multi-hop forwarding расширяет эффективный радиус до сотен метров и километров при достаточной плотности устройств. Когда хотя бы одно устройство в mesh-сети получает интернет-доступ — вся цепочка синхронизируется через него как через единый шлюз.
2026-05-26 21:14:51 +03:00
Избыточность = устойчивость. Пять каналов независимы на слое physical-delivery (IP интернет для 1-4, radio mesh для 5). State-level блок интернет-канала не влияет на канал 5 — отключение mesh требует физического подавления Bluetooth / Wi-Fi на каждом устройстве, что практически невыполнимо.
2026-05-26 21:14:51 +03:00
### Dandelion++ (анонимность отправителя)
2026-05-26 21:14:51 +03:00
P2P gossip Монтаны ретранслирует операции через все узлы. Без защиты первый пир знает IP отправителя. Dandelion++ (Fanti et al. 2018) ломает связь IP → операция, модифицируя существующий gossip.
2026-05-26 21:14:51 +03:00
**Две фазы:**
```
2026-05-21 03:44:38 +03:00
Stem:
2026-05-26 21:14:51 +03:00
Операция проходит по цепочке случайных узлов (2-3 hop в среднем).
Каждый узел видит только предыдущий hop, не originator.
На каждом hop с вероятностью p = 0.4 операция переходит в fluff.
E[stem_length] = 1/p = 2.5 hops.
P(stem ≤ 1) = 40%, P(stem ≤ 3) = 78%.
2026-05-21 03:44:38 +03:00
Fluff:
2026-05-26 21:14:51 +03:00
Последний stem-узел инициирует нормальный gossip broadcast.
Для остальной сети операция «появилась» из случайной точки.
```
2026-05-26 21:14:51 +03:00
**Stem routing.** Stem использует только исходящие соединения — входящие соединения не участвуют. Каждые 693 окна узел переселектит 2 из своих 24 исходящих как `stem_peers` (период selection stem-set). Внутри этого окна 693 окон `stem_successor` (forward выбор между 2) ротируется каждую τ₁ — см. карточку Dandelion++ для нормативной формулировки. Все stem-операции в эпохе маршрутизируются через один из этих 2 (выбирается hash(msg)).
2026-05-26 21:14:51 +03:00
**Применение по типу объекта:**
2026-05-26 21:14:51 +03:00
| Объект | Режим | Причина |
|--------|-------|---------|
2026-05-26 21:14:51 +03:00
| UserObject (Transfer Mode A/B, Anchor, ChangeKey, CloseAccount) | Stem → fluff | Скрыть IP отправителя |
| ControlObject (NodeRegistration) | Stem → fluff | Скрыть IP регистрирующегося кандидата |
| VDF Reveal | Прямой gossip (без stem) | node_id публичен в reveal, анонимность невозможна; IP скрыт Transport Obfuscation (Noise_PQ XX поверх TCP/8444 с uniform framing) |
| Confirmation | Stem → fluff | Скрыть, какой узел подтвердил первым |
2026-05-26 21:14:51 +03:00
**Свойства:**
2026-05-26 21:14:51 +03:00
| Угроза | Защита |
|--------|--------|
2026-05-26 21:14:51 +03:00
| Пир видит IP отправителя | Stem: пир видит только предыдущий hop |
| Глобальный наблюдатель (ISP) | Noise_PQ XX + uniform framing (Transport Obfuscation) |
| Анализ gossip-графа | Операция входит в gossip из случайной точки |
| Контроль k узлов | Деанонимизация требует контроля O(√n) узлов |
2026-05-26 21:14:51 +03:00
**Реализация:**
```
2026-05-26 21:14:51 +03:00
stem_peers = random_sample(outbound, 2) // каждые 693 окна (selection из 24 outbound)
on_receive_stem(msg, from_peer):
if random() < 0.4:
gossip_broadcast(msg) // fluff
else:
2026-05-26 21:14:51 +03:00
next = stem_peers[hash(msg) % 2] // детерминированный выбор между 2
send_stem(msg, next) // продолжить stem
start_timer(msg, 30s) // safety timer на каждом hop
on_timer_expired(msg):
2026-05-21 03:44:38 +03:00
if msg not observed in gossip:
gossip_broadcast(msg) // forced fluff
```
2026-05-26 21:14:51 +03:00
Каждый stem-узел страхует следующий. Таймер τ₁/2 работает независимо на каждом hop. Если следующий hop уронил сообщение — текущий hop замечает отсутствие операции в gossip и сам выполняет fluff. Максимальная latency = τ₁/2 (один hop), не кумулятивная.
2026-05-26 21:14:51 +03:00
Dandelion++ не требует внешней инфраструктуры. Каждый Монтана-узел уже является relay — gossip существует; stem добавляет 2-3 hop перед ним. Overhead latency: миллисекунды.
### NAT Traversal
2026-05-26 21:14:51 +03:00
Персональная сеть работает, когда каждый может присоединиться. Большинство домашних пользователей сидят за NAT — невидимы для входящих соединений. Без NAT traversal персональный интернет = клуб серверов.
2026-05-26 21:14:51 +03:00
Три механизма, каждый следующий используется если предыдущий не сработал:
2026-05-26 21:14:51 +03:00
**1. AutoNAT (детекция).** Узел спрашивает outbound пиров: «видите ли вы мой IP:port напрямую?» Если да — нет NAT. Если нет — узел знает свой NAT-статус.
2026-05-26 21:14:51 +03:00
**2. DCUtR (hole punching).** Два NAT-нутых узла координируются через третий узел с публичным IP. Оба отправляют исходящие пакеты — роутеры открывают «дыры» для ответов. После координации — прямое соединение. Успех: 60-70% случаев (TCP). Carrier-grade NAT (мобильные операторы): ~30%.
2026-05-26 21:14:51 +03:00
**3. Circuit Relay v2 (transit).** Если hole punching не сработал — трафик идёт через outbound пира с публичным IP. Relay — не отдельный механизм и не специальный сервер. Relay-соединение = обычное исходящее соединение, subject к тем же правилам: uniform framing, ограничения diversity, behavioural rotation. Содержимое end-to-end зашифровано (Noise) — relay видит IP участников, но не содержимое. Метаданные размазаны по 24 outbound пирам из разных /16 и ASN — ни один relay не видит полный граф.
2026-05-26 21:14:51 +03:00
Relay — не fallback, а гарантия connectivity для любого типа NAT. Hole punching — оптимизация для снижения relay-нагрузки.
2026-05-26 21:14:51 +03:00
**Лимиты relay:** до 32 одновременных relay-соединений на узел, пропускная способность per relay ≤ baseline частота фреймов (1 KB/с). 32 × 1 KB/с = 32 KB/с ≈ 82 GB/месяц — приемлемо для домашнего узла с публичным IP.
2026-05-26 21:14:51 +03:00
**Обязательство.** Узлы с публичными IP поддерживают relay — персональная сеть работает, когда каждый может присоединиться. Эталонная реализация включает relay при обнаружении публичного IP. Feeler-соединения проверяют relay-поддержку у пиров; узлы без relay помечены `no-relay` в менеджере адресов. NAT-нутые узлы предпочитают relay-способных пиров при выборе исходящих соединений.
2026-05-26 21:14:51 +03:00
Все три механизма — стандарты libp2p (AutoNAT, DCUtR, Circuit Relay v2). Ноль новых протокольных примитивов.
### Mesh Transport
2026-05-26 21:14:51 +03:00
Интернет не всегда доступен. State shutdowns (Иран 2019 — неделя, Беларусь 2020 — дни, Мьянма 2021 — месяцы), локальные сбои, изолированные зоны. Монтана продолжает работать в этих условиях через mesh-транспорт поверх Bluetooth Low Energy и Wi-Fi Direct — устройства обнаруживают друг друга в физическом радиусе и пересылают зашифрованные Монтана-сообщения hop by hop. Mesh не заменяет интернет-транспорт, он его дополняет: когда connectivity возвращается, сеть автоматически сходится через mesh-internet шлюз.
2026-05-26 21:14:51 +03:00
**Mesh-транспорт ортогонален консенсусу**, как и интернет-транспорт ([раздел выше](#сетевой-слой)) — машина состояний работает над любым каналом доставки без изменений.
2026-05-26 21:14:51 +03:00
#### Wire format MeshFrame
2026-05-26 21:14:51 +03:00
Все mesh-сообщения фрагментируются на фреймы фиксированного формата:
```
MeshFrame:
2026-05-26 21:14:51 +03:00
mesh_protocol_version u16 — версия mesh wire format
(0x0001 для v1)
frame_type u8 — 0=discovery, 1=data,
2=ack, 3=forward
2026-05-26 21:14:51 +03:00
ttl u8 — max 16 при создании,
монотонный decrement
декрементируется на каждом forwarding hop;
ttl=0 → фрейм отбрасывается
hop_count u8 — 0 при создании,
монотонный increment
инкрементируется на каждом forwarding hop
sender_ref 32B — mesh_session_id инициатора
(не прямой node_id для
приватности на mesh-слое)
recipient_hint 32B — encrypted routing hint
либо broadcast marker
(0xFF × 32 = broadcast)
payload_length u16 — длина payload в байтах,
≤ 256
payload variable — encrypted blob,
≤ 256B для fit в один
BLE GATT notification
без fragmentation
mac 16B — HMAC-SHA-256 truncated,
key derived через
HKDF от shared session
secret с domain separator
"mt-mesh-frame-mac"
2026-05-26 21:14:51 +03:00
Итого: 64 + 256 + 16 = 336B максимум
(64 байта header + 256 байт payload + 16 байт MAC)
```
≤ 256B для fit в один
BLE GATT notification
без fragmentation
mac 16B — HMAC-SHA-256 truncated,
key derived через
HKDF от shared session
secret с domain separator
"mt-mesh-frame-mac"
Итого: 64 + 256 + 16 = 336B максимум
(64 байта header + 256 байт payload + 16 байт MAC)
```
**Инварианты MeshFrame:**
- `mesh_protocol_version` ∈ {0x0001} для v1; иные значения reject
- `frame_type` ∈ {0, 1, 2, 3}; иное → drop
- `ttl` ∈ [0, 16]; при создании фрейма sender устанавливает ≤ 16; при каждом forward `ttl := ttl - 1`; если `ttl = 0` и frame требует forwarding — drop
- `hop_count` ∈ [0, 16]; при создании = 0; при каждом forward `hop_count := hop_count + 1`; если `hop_count > 16` → drop (защита от malformed increment)
- `sender_ref` = 32 байта = mesh_session_id отправителя (см. derivation ниже), не прямой node_id
- `recipient_hint` = 32 байта; значение `0xFF × 32` обозначает broadcast, иное — encrypted routing hint; получатель проверяет соответствие self через локальное state
- `payload_length` ≤ 256; строгое неравенство иначе → drop
- `payload` длина точно `payload_length` байт; encrypted blob (шифрование выполнено на уровне session, mesh transport layer видит только ciphertext)
- `mac` = 16 байт, HMAC-SHA-256(session_mac_key, header_bytes || payload) truncated до первых 16 байт; session_mac_key = HKDF-SHA-256(session_shared_secret, salt=empty, info="mt-mesh-frame-mac", length=32); mismatch MAC → drop + increment soft-blacklist counter для sender_ref
- Signature verify rule: MeshFrame не подписывается ML-DSA-65 напрямую (MAC достаточен для integrity между двумя peer в установленной session); identity-level authentication выполняется один раз при mesh IBT handshake, subsequent frames authenticated через session MAC
- Cross-field consistency: `hop_count + ttl ≤ 16` в любом состоянии (initial: hop_count=0, ttl≤16; при каждом forward `ttl := ttl - 1`, `hop_count := hop_count + 1`, сумма инвариантна); нарушение `hop_count + ttl > 16` — malformed frame, drop + increment soft-blacklist counter для peer из которого пришла frame
**mesh_session_id derivation.** Для каждой mesh сессии (между парой peers после mesh IBT handshake) выводится:
```
mesh_session_id = HKDF-SHA-256(
ikm = shared_secret_from_noise_handshake,
salt = mesh_session_nonce_initiator || mesh_session_nonce_responder,
info = "mt-mesh-session",
length = 32
)
```
`mesh_session_id` используется в поле `sender_ref` вместо прямого `node_id` — mesh transport на уровне wire format не раскрывает identity отправителя случайному слушателю в радиусе. Identity раскрывается только peer с которым установлена сессия (они знают mesh_session_id).
**Валидация MeshFrame.**
1. `mesh_protocol_version` совпадает с ожидаемой версией peer. Mismatch → drop, no forward.
2. `frame_type ∈ {0, 1, 2, 3}`. Иное → drop.
3. `ttl ∈ [0, 16]`. Если ttl=0 и frame пришёл для forwarding — drop.
4. `hop_count ≤ 16`. Иное → drop (защита от malformed increment).
5. `payload_length ≤ 256`. Иное → drop.
6. `mac` verify через HMAC-SHA-256 с session key. Mismatch → drop, increment soft-blacklist counter sender_ref.
7. Для `frame_type = 3 (forward)` — применить правила Store-and-Forward Semantics (ниже).
#### Mesh framing profile
Internet transport uniform framing ([подраздел «Uniform Framing»](#uniform-framing)) не применяется к mesh transport. Mesh имеет независимый framing profile:
```
Internet (существующий):
frame_size = 1024 bytes
baseline_rate = 1 frame/сек
2026-05-26 21:14:51 +03:00
контекст = Noise_PQ XX AEAD stream over TCP/8444
Mesh (v1):
frame_size = 256 bytes (fit в BLE MTU типично
без application-level fragmentation
в большинстве стеков iOS/Android)
baseline_rate = 1 frame/10 сек (baseline advertisement
+ occasional data, battery-sustainable)
burst_mode_rate = 1 frame/сек (активируется ТОЛЬКО при
активной mesh chat session, не continuous)
burst_mode_duration = ≤ 120 сек после последнего data frame,
затем возврат к baseline_rate
fragmentation = sequence numbers для сообщений > 256B,
reassembly на получателе через seq_id
в payload header уровня application
```
Обоснование параметров:
- 256B — BLE MTU реально варьируется 23-512B, большинство современных iOS/Android поддерживают ≥ 247B, 256B выбран как compromise fit-without-fragmentation на mainstream устройствах
- 1 frame/10 сек baseline — continuous Bluetooth scanning при более частом ритме съедает 30-50% батареи смартфона за несколько часов; 1/10s профиль extends battery usability до рабочего дня
- burst до 1/сек — активная переписка требует reasonable responsiveness; активация по событию «активный chat session» ограничивает всплеск энергопотребления ко времени реального использования
#### Fragmentation
Сообщения превышающие 256B fragmented на уровне application перед enqueue в mesh:
```
ApplicationPayload (до fragmentation):
fragment_count u16 — общее число фрагментов
fragment_index u16 — index текущего фрагмента (0-based)
message_id 32B — unique id сообщения,
shared across всех фрагментов
data variable — часть encrypted payload
```
Получатель собирает фрагменты по `message_id`, порядок восстанавливается через `fragment_index`. Timeout reassembly: τ₁ от первого полученного фрагмента (по локальному кварцу транспортного слоя, outside [I-18] scope) — если не все собраны, partial drop. Fragment_index ≤ 255 (max 256 фрагментов × 256B payload = 64KB верхняя граница одного application message; большие объёмы — через Content Layer chunking на blob уровне).
#### Mesh discovery flow
1. Устройство в mesh-активном режиме периодически (baseline_rate = 1 frame/10 сек) бродкастит `frame_type = 0 (discovery)` с `sender_ref = mesh_session_id_self_generated` и `payload` = short advertisement: protocol version, capability flags, optional trust hint.
2. Другие устройства в радиусе принимают broadcast, извлекают advertisement.
3. Если принимающее устройство считает инициатора потенциально интересным (известный контакт в адресной книге; broadcast addressed to broadcast marker и устройство в broadcast-listening mode; любое другое правило application) — оно инициирует mesh IBT handshake (см. «Identity-Bound Tunnel» выше, формула `mesh_proof`).
4. После успешного handshake — session установлена, оба peer добавляют `mesh_session_id` в active sessions.
5. Обмен данных происходит через `frame_type = 1 (data)`.
#### Battery management
Reference implementation рекомендуется:
- Scheduled Bluetooth scan: 1 раз в 10 секунд при baseline, чаще при burst
- Wi-Fi Direct используется только для high-throughput сессий (передача больших файлов), не continuous
- iOS background mode constraints: полный mesh transport работает только в foreground; в background доступно ограниченное Core Bluetooth BGTaskScheduler сканирование
- Android: BLE advertisement и scanning в background — стандарт платформы, требует declared foreground service notification для compliance
### Store-and-Forward Semantics
Mesh transport inherently async: получатель сообщения может быть вне радиуса в момент отправки. Store-and-forward semantics описывают как промежуточные устройства буферизуют и пересылают сообщения к их конечному получателю.
#### Buffer model
Каждое устройство в mesh-активном режиме поддерживает локальный buffer:
```
MeshBuffer (локальный state устройства):
entries: map<frame_hash, BufferEntry>
BufferEntry:
frame MeshFrame
receipt_seq u64 (локальный sequence counter, инкрементируется
при каждом receive; используется для local FIFO
ordering в buffer; не передаётся в сеть)
ttl_remaining u8 (decremented каждый forwarding hop)
sender_ref 32B (из frame, для per-sender quota)
forwarded_to set<peer_id> (peers которым уже переслано,
защита от петель)
```
`frame_hash = SHA-256(MeshFrame serialized)` — ключ для идемпотентного recept.
#### Buffer policies
**Capacity limits (по умолчанию, настраиваемо в реализации):**
- Max buffer size per device: 1024 frames (≈ 336 KB)
- Max retention per frame: 1440 τ₁ (TTL expiry на buffer entry, эмерджентно ≈ 1 сутки на genesis-калибровке; независимо от `ttl` в frame который decremented per hop)
- Max frames per sender_ref in buffer: 10 concurrent
**Priority queue (при enqueue):**
1. Own sent frames (frames originated by this device) — highest priority
2. Frames addressed to known contacts (locally stored) — high priority
3. Frames addressed to unknown recipients (broadcast или unknown recipient_hint) — low priority
**Drop policy при overflow:**
- Первое при переполнении — drop low-priority oldest
- При исчерпании low-priority — drop high-priority oldest (не own)
- Own frames не дропаются до expiry
#### Per-sender quota
Защита от flood DOS (вектор M1 из adversarial review):
```
Rate limits per sender_ref:
max_frames_per_τ₁ = 10
max_frames_in_buffer = 10 concurrently
При превышении:
- Новые frames от этого sender_ref дропаются
- Sender_ref получает signed rate-limit ack с отказом
- Soft-blacklist local: exponential backoff в τ₁,
первое нарушение — 1 τ₁ ignore, второе — 2 τ₁,
и т.д. до 60 τ₁ максимум
```
#### Signed rate-limit acks
Relay подписывает acknowledgement для каждой принятой (и forwarded или сохранённой) frame:
```
MeshAck:
acked_frame_hash 32B — SHA-256 frame которая acked
relay_node_id 32B
status u8 (0=accepted, 1=buffered,
2=forwarded, 3=rejected_quota,
4=rejected_expired)
ack_seq u64 — relay-локальный монотонно
возрастающий sequence counter
ack-ов; не передаёт время,
только порядок выпуска ack-ов
у конкретного relay
signature 3309B — ML-DSA-65_sign(
relay_privkey,
"mt-mesh-ack"
|| acked_frame_hash
|| relay_node_id
|| status
|| ack_seq)
```
**Инварианты MeshAck:**
- `acked_frame_hash` = SHA-256 over canonical serialization MeshFrame к которому относится ack; receiver ack'а проверяет что хэш соответствует реально отправленной frame
- `relay_node_id` = SHA-256("mt-node" || relay_pubkey); receiver должен знать relay_pubkey для проверки подписи
- `status ∈ {0, 1, 2, 3, 4}`; значение вне диапазона → reject ack как malformed
- `ack_seq` — relay-локальный u64 sequence counter; инкрементируется на 1 при выпуске каждого ack данным relay; используется только для local ordering ack-ов на стороне получателя; не имеет временной семантики (не передаёт ни wall-clock, ни длительность); не consensus-critical, не участвует в state transitions
- `signature` = 3309 байт ML-DSA-65, валидация через `relay_pubkey`; подписываемое сообщение канонически сериализовано в порядке перечисления полей (acked_frame_hash || relay_node_id || status || ack_seq)
- Signature verify rule: ML-DSA-65.verify(relay_pubkey, domain_separator || canonical_payload, signature) = valid; иначе drop ack
- Ack не применяется к state transitions — это чисто local signal для sender rate adjustment, вне scope consensus
Sender использует ack для:
- Confirmation что frame принята (status ∈ {0, 1, 2})
- Detection перегрузки (status=3 → flood suppression, уменьшить rate)
- Detection expired frames (status=4 → frame outdated, не повторять)
Отсутствие ack в пределах τ₁/2 после отправки → sender предполагает relay недоступен, пробует другой peer.
#### Forwarding algorithm
```
on_receive(frame, from_peer):
frame_hash = SHA-256(frame)
if frame_hash in buffer:
return # дубликат, already processed
if not validate_frame(frame):
drop; increment soft-blacklist counter from_peer
return
if frame.sender_ref in soft_blacklist:
drop silently
return
if buffer.sender_count(frame.sender_ref) >= 10:
send_ack(frame, status=3) # rejected_quota
return
if frame.recipient_hint matches self:
deliver_to_application(frame)
send_ack(frame, status=0)
return
if frame.ttl == 0:
drop; send_ack(frame, status=4)
return
# forward case
frame.ttl -= 1
frame.hop_count += 1
buffer.add(frame)
send_ack(from_peer, frame, status=1) # buffered
# opportunistic forwarding
for peer in active_mesh_peers:
if peer not in frame.forwarded_to and
peer != from_peer and
peer accepts forwarding:
send(peer, frame)
frame.forwarded_to.add(peer)
send_ack(from_peer, frame, status=2) # forwarded
on_timer_expired(entry):
# local buffer expiry, independent от frame.ttl
buffer.remove(entry)
```
#### Interaction с internet
Когда устройство получает internet connectivity, оно опционально (по настройке пользователя) пересылает buffered mesh frames в internet-сеть:
1. Для каждой frame в buffer с `recipient_hint` который можно разрешить в account_id
2. Пересылка через обычный P2P gossip к Account Host получателя
3. После успешного acknowledgement с internet-стороны — frame удаляется из mesh buffer
4. Internet-to-mesh обратное направление аналогично: устройство с internet получает сообщение для offline-получателя, enqueues в mesh buffer для forwarding через ближайшие peers
Это делает internet-connected устройство **gateway** между internet-сетью и изолированной mesh-областью. Один такой шлюз восстанавливает связность для всего mesh-кластера до внешнего мира.
### Privacy Scope (точная зона ответственности)
Прежде чем описать Семь слоёв сетевой защиты, фиксируем **точные границы** того что Montana защищает на сетевом уровне и что **намеренно не закрывается**.
#### Three-level decomposition
Privacy в Montana работает на трёх отдельных уровнях, каждый со своими гарантиями:
1. **Wire-level (transport layer):** что видит провайдер / DPI / наблюдатель отдельного линка.
2026-05-26 21:14:51 +03:00
- Защита: Noise_PQ XX (ML-KEM-768 KEM + ML-DSA-65 identity + ChaCha20-Poly1305 AEAD) + IBT + Uniform Framing + Transport Randomness + Dandelion++ + Label Rotation per τ₁ + Censorship-resistant discovery
- Закрывает: local DPI, ISP, regulator с перехватом одного линка, small-medium Sybil eclipse, long-term recipient linkability через провайдеров приложений
- **НЕ закрывает:** global passive adversary с GPS-precision timing-correlation на ВСЕХ backbone links одновременно (open research problem всей anon-net области)
2. **Content-level (application data):** что видит хостящий узел или наблюдатель proposal-broadcast.
- Защита: Anchor + ML-KEM-768 encryption (data_hash в proposal, content off-chain encrypted под recipient ключ) + Double Ratchet PQ для messenger end-to-end + PQXDH async handshake
- Закрывает: content surveillance со стороны хоста, recipient identity через ephemeral labels
- **НЕ закрывает:** endpoint compromise (RAT/spyware) — out of scope любого network protocol
3. **Financial-level:** что видит любой наблюдатель cemented proposal.
- Status: **ОТКРЫТ per [I-2]** — балансы, sender_account_id, recipient, amount публичны.
- Это **intentional design feature** для регуляторной совместимости (FATF/MiCA), аудитируемости и [I-3] детерминизма consensus state.
- Privacy финансовых движений на protocol level **намеренно не предоставляется**.
#### Privacy Tier model для пользователей
Пользователь выбирает уровень privacy по trade-off против latency и bandwidth:
| Tier | Stack | Latency | Bandwidth | Threat closure |
|------|-------|---------|-----------|----------------|
| **1 default** | Семь слоёв baseline (TLS+IBT+Dandelion+Label Rotation+Mesh) | <500мс | minimal | Local DPI, ISP, regulator (один линк), small-medium Sybil, long-term recipient unlinkability |
| **2 recommended** | + own узел (Light-Node-at-Home) + Tor entry + Noise_PQ handshake | 500мс — 2с | medium | + ISP не видит «Montana traffic», + bypass legal request, + quantum store-now-decrypt-later |
| **3 paranoid financial** | + canonical Mempool buffering + end-of-window batch flush + cover traffic 100% | 60-120с | full | + temporal unlinkability sender's local act → operation appearance в state |
| **4 research-grade** | + artificial random delay 5-30 мин + multiple onion paths | 5-30 мин | full | + extreme protection from mass-surveillance flowtracking; **practically maximum** при non-information-theoretic anonymity |
Tier 1 — автоматически для всех пользователей. Tier 2-4 — opt-in.
#### Canonical-aggregation closure для timing-correlation (unique Montana property)
Montana отличается от Tor / Loopix / I2P **fundamentally** в том как обрабатываются operations на сетевом уровне:
```
Tor / Loopix / I2P model:
packet_α emit immediately → traverse network → arrive at recipient
Adversary observes individual packet timing → correlates Alice IP ↔ Bob IP
Deanonymization после 100-1000 messages
Montana model:
Alice creates operation_α локально в момент T_local
→ Mempool buffer на узле-операторе (random hold N окон)
→ Dandelion++ stem 2-hop форвардинг
→ Operator's mempool → operator selection lottery
→ Aggregation: proposal_W = canonical(op_α, op_β, ..., op_ω)
включает 100-10000 operations from different accounts
→ Broadcast aggregate
Adversary observes aggregate proposal, individual op_α неотличим от op_β
Deanonymization требует 10⁶-10⁸ messages (orders of magnitude harder)
```
**Уникальные primitives Montana** которые работают вместе для этого свойства:
- **Mempool temporal buffering** — Alice's individual emit timing decoupled от operation appearance в state
- **Canonical proposal aggregation** — proposal содержит operations от множества accounts, individual flow смешан
2026-05-26 21:14:51 +03:00
- **Sequential-chain canonical aggregation point** — все proposals публикуются в canonical моменты (end-of-window) — гомогенный stream events для adversary
- **Block Lattice independent AccountChain** — каждый аккаунт пишет в свою цепочку, structurally separate но bundled в proposal
- **Dandelion++ stem buffering** — operator entry скрыт через 2-hop stem propagation
Operations в Montana — это **state events на canonical TimeChain**, не ephemeral network messages. Этой fundamental architectural property нет ни в Tor (per-message routing), ни в Loopix (per-message Poisson mixing), ни в I2P (per-tunnel routing). Это даёт Montana **уникально сильное** ослабление global passive timing-correlation — **не absolute closure** (open research problem остаётся), но **на порядки сильнее** existing systems.
#### Honest claim — что закрывается и что не закрывается
| Adversary class | Tier 1 | Tier 2 | Tier 3 | Tier 4 |
|-----------------|--------|--------|--------|--------|
| Local DPI / ISP | ✅ через TLS+IBT | ✅ через Tor | ✅ | ✅ |
| Government legal request to ISP | ⚠️ partial — DPI закрыт, IP виден | ✅ через Tor | ✅ | ✅ |
| Active probing | ✅ через IBT | ✅ | ✅ | ✅ |
| Sybil eclipse | ✅ через 4-dim diversity | ✅ | ✅ | ✅ |
| Sender authorship inference (ближайшие peers) | ✅ через Dandelion++ | ✅ | ✅ | ✅ |
| Long-term recipient linkability | ✅ через Label Rotation per τ₁ | ✅ | ✅ | ✅ |
| Hosting third-party metadata | ⚠️ visible to host | ✅ через own node | ✅ | ✅ |
| Content surveillance | ✅ через Anchor encryption | ✅ | ✅ | ✅ |
| Quantum store-now-decrypt-later | ❌ TLS handshake X25519 vulnerable | ✅ через Noise_PQ | ✅ | ✅ |
| Backbone GPS-precision timing-correlation | ⚠️ partial through aggregation (10⁶ msg threshold) | ⚠️ same + Tor obscures | ⚠️ + temporal randomization | ⚠️ + multi-path delays |
| Endpoint compromise (RAT) | ❌ out of scope | ❌ out of scope; mitigation через Light-Node-at-Home damage containment | ❌ | ❌ |
| Financial state visibility | ❌ public per [I-2] **by design** | ❌ same | ❌ same | ❌ same |
Маркеры: ✅ closed; ⚠️ partial / mitigated; ❌ not closed (intentional или fundamental limit).
#### Endpoint compromise — Montana damage containment (architectural mitigation)
Network protocol не может prevent endpoint compromise. **Но Montana архитектурно ограничивает damage** через unique patterns:
- **Light-Node-at-Home split** (App spec § 26): master_seed на home node, phone имеет только ephemeral session keys. Compromise phone ≠ compromise master key.
2026-05-26 21:14:51 +03:00
- **Chain-anchored ephemeral session rotation per τ₁**: session key derivable через `HKDF(master_seed, current_window || "session-W")`. Maximum exposure window = 60 sec.
- **Junona local pre-processing**: AI агент на own node делает decryption + summarization; phone никогда не имеет full content в memory.
- **Block Lattice sub-account hierarchy**: phone использует daily-spend sub-account ($X/day limit); master savings account только на home node.
- **Hardware-backed enclave integration**: master_seed в iOS Secure Enclave / Android StrongBox.
**Сравнение с existing messengers:**
| System | Endpoint compromise impact |
|--------|----------------------------|
| Signal | Compromise device = full chat history forever (single trust domain) |
| WhatsApp | Compromise device = full history + cloud sync |
| Telegram | Compromise device = full history + cloud + saved messages |
| **Montana с Light-Node-at-Home** | Compromise phone = max loss `sub_account_limit × 60_sec_window_content` (multi-domain trust) |
#### Что **не** покрывается ни одним tier
Два fundamental open problems которые **не закрывает никто** (включая Tor, Loopix, I2P):
1. **Global passive adversary с GPS-precision timing-correlation на всех backbone links одновременно**. Montana **уникально ослабляет** через canonical aggregation (10⁶-10⁸ message threshold вместо 10²-10³ как в Tor), но **не закрывает абсолютно** — это open research problem с 1980-х в anon-net области.
2. **Endpoint compromise через RAT / hardware-level malware**. Network protocol бесполезен — данные читаются до encryption. Montana **архитектурно ограничивает damage** через trust domain split, но не **prevents** compromise. Полная защита требует hardware secure enclave + verified boot + careful endpoint hygiene.
Эти ограничения **honestly зафиксированы** в spec — пользователь знает scope защиты до использования. Marketing-claim «полная анонимность» отвергается как overpromise который рухнёт при первом серьёзном аудите.
### Семь слоёв — одна конструкция
```
Слой 1: Transport Obfuscation персональный сервер скрывает содержимое и тайминг
Слой 2: Peer Selection start_window + network diversity constraints
Слой 3: NAT Traversal каждый может войти, даже за NAT
Слой 4: Censorship-Resistant Discovery пять каналов, достаточно одного
Слой 5: Dandelion++ пиры не знают кто автор операции
Слой 6: Mesh Transport работа при отключении internet,
hop-by-hop Bluetooth / Wi-Fi Direct
Слой 7: Store-and-Forward Semantics ephemeral буферизация в mesh,
per-sender quota, signed acks
```
Каждый слой закрывает свой вектор. Ни один не требует внешней инфраструктуры. Всё построено поверх libp2p (для internet-слоёв 1-5) и нативных BLE/Wi-Fi Direct API (для mesh-слоёв 6-7) плюс существующего gossip. Сетевой уровень ортогонален консенсусу — ни один state transition не затронут.
### Protocol Message Layer
Внутри IBT uniform frames протокольные сообщения следуют общему envelope format. Эта секция нормативно определяет wire format всех сообщений Монтаны для cross-implementation совместимости.
**Envelope format.**
```
ProtocolMessage:
msg_type 1B <- u8, код типа сообщения
msg_version 1B <- u8, версия формата сообщения (= 1 для v28.x)
request_id 8B <- u64 little-endian, correlation id для request/response (= 0 для one-way gossip)
payload_length 4B <- u32 little-endian, размер payload в байтах
payload ?B <- payload_length байт, формат определяется msg_type
```
Envelope всегда 14 байт header + payload. Поскольку IBT uniform frames имеют payload 1021B, ProtocolMessage может занимать один или несколько фреймов через flag `0x04 continuation` (см. Uniform Framing).
**Реестр типов сообщений.**
| Код | Тип | Направление | Payload |
|-----|-----|-------------|---------|
| 0x01 | Transfer | one-way gossip | Transfer объект (Mode A или Mode B; serialize по canonical encoding; режим определяется длиной payload и наличием receiver в Account Table) |
| 0x02 | reserved | — | Освобождён (ранее выделен под отдельную gossip-категорию активации; gossip envelope namespace независим от operation type-byte). Не выделять вновь. |
| 0x03 | ChangeKey | one-way gossip | ChangeKey объект |
| 0x04 | Anchor | one-way gossip | Anchor объект |
| 0x10 | NodeRegistration | one-way gossip | NodeRegistration объект |
| 0x20 | BundledConfirmation | one-way gossip | BundledConfirmation объект |
| 0x21 | VDF_Reveal | one-way gossip | VDF_Reveal объект |
| 0x22 | Proposal | one-way gossip | Proposal объект |
| 0x40 | FastSyncRequest | request | `{anchor_window: u64, resume_offset: u64}` |
| 0x41 | FastSyncResponse | response | chunked snapshot data (см. ниже) |
| 0x42 | FastSyncError | response | `{code: u8, message: bytes[≤255]}` |
| 0x50 | PeerListRequest | request | `{max_count: u16}` |
| 0x51 | PeerListResponse | response | `{count: u16, peers: count × PeerEntry}` |
| 0x60 | BatchLookupRequest | request | `{query_type: u8, count: u8, queries: count × query_entry}` (см. раздел «Batch Lookup Protocol») |
| 0x61 | BatchLookupResponse | response | `{query_type: u8, count: u8, results: count × result_entry}` |
| 0x62 | BatchLookupError | response | `{query_type: u8, error_code: u8}` |
| 0x63 | RangeSubscribeRequest | request | `{count: u16, labels: count × 32B}` (см. раздел «Label Rotation + Range Subscribe Protocol») |
| 0x64 | RangeSubscribeResponse | response | `{blob_count: u16, blobs: blob_count × BlobEntry}` |
| 0x65 | RangeSubscribeError | response | `{error_code: u8}` |
| 0xF0 | Ping | request | (no payload) |
| 0xF1 | Pong | response | (no payload) |
| 0xFF | Bye | one-way | `{reason: u8}` (graceful shutdown) |
Message versioning: `msg_version = 1` для всех v28.x. Изменение wire format = increment msg_version, требует protocol version upgrade.
Unknown msg_type → получатель логирует и игнорирует (forward compatibility). Unknown msg_version → получатель отвечает FastSyncError с кодом `unsupported_version` и разрывает соединение.
**Structured payloads.**
`PeerEntry`:
```
PeerEntry:
ip_version 1B <- u8, 0x04 или 0x06
ip 16B <- IPv4 в последних 4 байтах (первые 12 = 0x00) или IPv6 полностью
port 2B <- u16 little-endian
node_id 32B
start_window 8B <- u64 little-endian, из Node Table
= 59 bytes fixed
```
`FastSyncResponse` chunked delivery:
```
FastSyncResponse chunk:
chunk_index 4B <- u32 little-endian, начинается с 0
total_chunks 4B <- u32 little-endian, общее число chunks для текущего запроса
table_id 1B <- u8: 0x01 Account, 0x02 Node, 0x03 Candidate, 0x04 Proposals
record_count 4B <- u32 little-endian, записей в этом chunk
records ? <- record_count × serialize(record) по canonical encoding
```
Response состоит из N chunks (с одним request_id). Получатель собирает по chunk_index. После получения всех total_chunks — reconstructs Merkle root и проверяет против proposal_W.
**Connection lifecycle.**
Порядок установки соединения:
```
1. TCP SYN / SYN-ACK / ACK (standard)
2026-05-26 21:14:51 +03:00
2. Noise_PQ XX handshake (3 messages: msg1=1184 B, msg2=7533 B, msg3=6349 B)
3. Noise key agreement внутри TLS (mutual pubkey authentication)
4. IBT proof exchange (клиент отправляет ML-DSA-65 signature)
5. Access level determination (node / candidate / account, см. Transport Obfuscation)
6. Готово к обмену ProtocolMessages
```
Timeouts установки (по локальному кварцу транспортного стека, outside [I-18] scope; emergent значения при genesis-калибровке):
- TCP connect: τ₁/2
- TLS handshake: τ₁/6
- Noise + IBT: τ₁/6
- Всё вместе не более 1 τ₁ до готовности
Если любой шаг превысил timeout → разрыв, retry с другим пиром.
**Keepalive.**
- Ping раз в τ₁ на idle соединении (нет данных)
- Pong должен прийти до завершения текущего τ₁ у получателя Pong
- Три подряд τ₁ без полученного Pong → disconnect
- При активном обмене данными Ping не обязателен (реальные данные = evidence активности)
Ping/Pong не несут payload — это чистая liveness-проверка. Любая локальная RTT-оценка отправителем — concern транспортного слоя его ОС (CLOCK_MONOTONIC kernel-level, outside scope [I-18]) и не передаётся в подписанных объектах.
**Graceful shutdown.**
Инициатор: отправляет Bye с reason code:
```
0x00 — normal shutdown
0x01 — going offline for maintenance
0x02 — peer list refresh (попытка найти лучших пиров)
0x03 — resource limits (слишком много соединений)
0x04 — protocol violation (валидация failed много раз)
0x05 — version mismatch
```
Получатель acknowledges через свой Bye, затем TLS close_notify, затем TCP FIN. Максимум τ₁/12 на graceful shutdown (по локальному кварцу транспортного стека, outside [I-18] scope), иначе forced close.
**Peer discovery algorithm.**
Новый узел при старте:
```
1. Извлечь bootstrap peers из Genesis Decree (захардкожено)
2. Выбрать 1-3 random bootstrap peer, connect (с PoW для bootstrap per Transport Obfuscation)
3. Выполнить IBT (account keypair для первого подключения нового узла)
4. Отправить PeerListRequest с max_count = 128
5. Получить PeerListResponse с до 128 известных peer-ов
6. Применить diversity constraints (/16, ASN, start_window) к полученному списку
7. Выбрать 24 outbound candidates по diversity
8. Параллельно connect к выбранным
9. После успешного IBT с реальным peer — disconnect от bootstrap (освобождая bootstrap slots)
10. Maintaining: PeerListRequest каждые ~τ₂ окон для обновления таблицы "проверенных" peers
```
Bootstrap exceptional:
- PoW при подключении (target ~100ms CPU per Transport Obfuscation)
- Ограничение: не более 3 одновременных bootstrap подключений на узел
- Освобождается после 13 реальных peers connected
**Peer exchange**
Между двумя подключёнными узлами:
```
Каждые τ₂_windows:
A → B: PeerListRequest {max_count: 64}
B → A: PeerListResponse {peers[]}
```
Узел поддерживает две таблицы:
- **Новые** peers: недавно узнанные (от bootstrap или PeerListResponse), ещё не использованные
- **Проверенные** peers: те с которыми были успешные соединения в прошлом
При выборе outbound: 50/50 случайно из обеих таблиц. Bucket по секретному ключу узла предотвращает external enumeration.
**Retry policy.**
- Failed connect: exponential backoff в локальных кварцевых секундах транспортного стека (1s, 2s, 4s, 8s, ..., max 5τ₁; outside [I-18] scope)
- Peer rejected через IBT fail: peer помечается bad на 1τ₁
- Peer disconnected с reason 0x04 (protocol violation): peer blacklisted на 24τ₁
- Bootstrap PoW retry: no backoff (PoW сам служит rate limit)
**Error codes для FastSyncError:**
```
0x01 snapshot_unavailable -- запрошенный anchor_window слишком старый (peer не хранит)
0x02 snapshot_too_large -- snapshot больше чем peer готов отправить
0x03 unsupported_version -- msg_version не поддерживается
0x04 resource_exhausted -- peer перегружен
0x05 access_denied -- peer не отдаёт Fast Sync клиентам (только nodes)
```
**Сеть vs консенсус — граница.**
Network layer параметры (timeouts, retry delays, keepalive intervals) — implementation guidance, могут варьироваться между реализациями без consensus impact. Значения в этой секции — рекомендуемые defaults. Consensus-critical: wire format (envelope, payloads), IBT proof format, Bootstrap PoW formula, message type codes. Изменение consensus-critical параметров требует protocol version upgrade.
---
## Batch Lookup Protocol
Протокол обеспечивает **baseline приватность lookup-запросов** для account-only пользователей (тех, кто работает через чужой узел без собственной инфраструктуры). Когда клиент запрашивает информацию об аккаунтах (связка предварительных ключей, проверка существования), запрос группируется в batch из K элементов, среди которых **ровно один** — реальная цель, остальные — случайные decoy-аккаунты. Хост видит K-элементный запрос, но не знает какая из позиций real.
Механизм применяется только для cold-path lookups. Hot-path — уже известные контакты пользователя — разрешается локально на клиенте без обращения к сети (см. App spec раздел «Модуль обнаружения контактов»).
### Константы
Определены в ProtocolParams Genesis Decree:
- `batch_lookup_k = 16` — обязательный размер batch. Отклонения запрещены (детализация: см. обоснование в разделе «Обоснование протокольных констант»).
- `max_batch_lookups_per_τ₁ = 16` — rate limit на один account per окно τ₁, защита от DoS на хоста.
### Message type 0x60 — BatchLookupRequest
Payload format:
```
BatchLookupRequest:
query_type 1B <- u8: 0x01 pre_key_bundle, 0x03 account_exists
count 1B <- u8, обязательно == batch_lookup_k (= 16)
queries count × query_entry (тип query_entry зависит от query_type)
где query_entry:
query_type == 0x01 (pre_key_bundle): 32B account_id
query_type == 0x03 (account_exists): 32B account_id
```
Клиент формирует batch: один real target + 15 decoy-аккаунтов, перемешанных в произвольном порядке. Клиент локально запоминает позицию real target внутри batch.
**Инварианты BatchLookupRequest:**
- `query_type ∈ {0x01, 0x03}`; иное → reject `UnsupportedType` (error_code 0x02)
- `count == batch_lookup_k` (строгое равенство, = 16); иное → reject `InvalidCount` (error_code 0x03)
- Размер `queries[]` точно `count × entry_size(query_type)` байт
- Source account (IBT-authenticated sender) активен в Account Table; `max_batch_lookups_per_τ₁` не превышен за текущее окно
- При превышении rate limit → reject `RateLimited` (error_code 0x01)
### Message type 0x61 — BatchLookupResponse
Payload format:
```
BatchLookupResponse:
query_type 1B
count 1B <- = count из request (16)
results count × result_entry (в том же порядке, что и queries)
где result_entry:
query_type == 0x01 (pre_key_bundle): 4B length prefix + variable ML-KEM-768 bundle
(length=0 → bundle отсутствует / never published)
query_type == 0x03 (account_exists): 1B (0x00 → not found, 0x01 → exists)
```
Хост **обязан** обработать все `count` queries и вернуть `count` results в том же порядке. Частичные ответы запрещены — либо полный BatchLookupResponse, либо BatchLookupError.
### Message type 0x62 — BatchLookupError
```
BatchLookupError:
query_type 1B
error_code 1B <- 0x01 RateLimited, 0x02 UnsupportedType, 0x03 InvalidCount
```
### Validation workflow хоста
1. Проверить IBT-аутентификация клиента (уровень 3 accepted для account-only пользователей)
2. Проверить structural инварианты BatchLookupRequest
3. Проверить rate limit для client account
4. Выполнить `count` lookups против локального state (Account Table, cemented Anchor archive)
5. Собрать `count` results в том же порядке что queries
6. Отправить BatchLookupResponse
Хост **не** логирует individual queries для privacy hygiene — только aggregate rate counters per-account для enforcement лимита.
### Effective privacy analysis
2026-05-21 03:44:38 +03:00
На целевом масштабе сети до ~1B активных аккаунтов (архитектурная цель; один только `AccountRecord` state ≈2.06 TB, fast-sync benchmarks остаются M7 gate) клиент собирает passively-observed pool активных аккаунтов через gossip proposals. Realistic pool size: 10K100K накопленных за τ₂ observation window.
При pool size 10K100K и K=16:
- **Effective anonymity:** ~23 бита (1-in-4 до 1-in-8 practical protection)
- **Intersection attack resistance:** intersection attack требует ~1000+ batches observation (~десятилетия активности) — практически нерелизуема
- **Semantic filtering:** клиент обязан использовать per-function dummy pools (pre-key bundles только от accounts published bundle, и т.д.) — детализация в App-спеке
Это **partial protection**, не абсолютная. Полное закрытие lookup-поверхности — через собственный узел (Light-Node-at-Home в App-спеке). Протокол делает максимум возможного при ограничениях [I-5] (commodity hardware, без PIR), [I-6] (без privacy mixers) и [I-7] (минимальная крипто-поверхность).
### Rate limit rationale
`max_batch_lookups_per_τ₁ = 16` при K=16 даёт максимум 256 queries per аккаунт per окно τ₁. Типичная активность пользователя мессенджера: ≤ 50 queries per sessions, несколько sessions per day. Лимит покрывает reasonable usage и защищает хоста от DoS amplification.
При превышении лимита клиент получает `RateLimited` error и обязан применить exponential backoff до следующего окна τ₁.
### Применимость инвариантов
- **[I-5] Commodity hardware:** ноль тяжёлых крипто операций, только SHA-256 compare для lookups — стандартный read. Работает на любом commodity узле.
- **[I-6] Регуляторная совместимость:** plaintext batch lookup = bulk read operation, не privacy mixer, не ring signature, не stealth address, не hidden flow. Host видит все K queries явно.
- **[I-7] Минимальная крипто-поверхность:** ноль новых крипто примитивов.
- **[I-15] Time-based scarcity:** rate limiting через `max_batch_lookups_per_τ₁` — time-based защита, соответствует [I-15].
- **[I-16] Out-of-band identity binding:** batch lookup предшествует первому сообщению; client получает pre-key bundle, вычисляет отпечаток, показывает пользователю для out-of-band сверки. Совместимо с [I-16] по конструкции.
---
## Label Rotation + Range Subscribe Protocol
Протокол baseline приватности для Blob Buffer polling пользователями account-only (тех кто работает через чужой узел). Защищает от long-term session identification через статические queue labels. Включает механизм catch-up для пользователей, возвращающихся онлайн после периода offline.
Механизм применяется к клиентскому слою (messenger sessions). Rotation формулы — authoritative в App-спеке раздел 23.2 (single source of truth). Catch-up protocol (RangeSubscribe) — protocol-level message types.
### Что закрывается и что остаётся открытым
**Closed через rotation:**
- Long-term session identification. Хост не может построить stable map `account_X → {sessions_X}` потому что queue labels меняются каждый τ₁. Набор наблюдаемых labels за разные окна нельзя correlate без знания `initial_root_key` сессии.
- Historical reconstruction через архивные логи хоста. Даже сохранённые label наблюдения нельзя decompose в session identity без session keys.
**Permanent architectural limits** (не закрываются на protocol level для account-only):
- **Session count.** Хост видит количество активных label subscriptions per τ₁ как proxy для числа активных сессий. Сокрытие требует cover traffic, которая при self-cover отличима от real по provenance (blob arriving from client's own IBT vs external gossip). Protocol-level ambient cover требует продолжительной фоновой генерации фиктивных сообщений и не scales на 1B. Архитектурно непреодолимо в рамках инвариантов Монтаны.
- **Activity timing patterns.** Хост видит когда клиент публикует и получает сообщения. Защита требует constant-rate cover — те же ограничения что session count.
- **Cross-host collusion per-τ₁.** Если хост Alice и хост Bob координируются — pair identification возможна за один τ₁ (publish-receive correlation). Rotation защищает от long-term накопления, не от per-τ₁ collusion.
Полная защита от этих трёх классов — только Light-Node-at-Home (см. App-спека раздел 26). Свой узел = no third-party observer = эти leaks не существуют для данного пользователя.
### Label rotation formula
Queue labels для session ротируются детерминистически каждый τ₁ на основе текущего `window_index`. Authoritative формула — в App spec раздел 23.2.
Краткое описание: label derivation использует `HKDF-SHA-256` с `initial_root_key` сессии как IKM, session_id как salt, и `"mt-queue-rotation" || direction_byte || W.to_le_bytes_8` как info. Клиенты обеих сторон session детерминистически выводят одинаковый label для одинакового окна.
**Sync tolerance:** получатель подписан на labels для `W ∈ {W_current, W_current 1}` — двухоконная tolerance к рассинхронизации канонических окон между участниками.
### Message type 0x63 — RangeSubscribeRequest
Для пользователей, возвращающихся онлайн после периода offline. Клиент вычисляет labels локально для нужного диапазона windows и запрашивает blobs от хоста.
Payload format:
```
RangeSubscribeRequest:
count 2B <- u16 LE, число labels в запросе ( max_range_labels_per_request = 10 000)
labels count × 32B <- client-computed queue labels для нужных (session × window) пар
```
**Инварианты RangeSubscribeRequest:**
- `count ≤ max_range_labels_per_request` (= 10 000); иное → reject `RangeTooLarge`
- Labels — 32-байтовые opaque identifiers, хост не проверяет их semantic validity (просто ищет совпадения в Blob Buffer)
- Source account (IBT-authenticated sender) активен в Account Table
- Rate limit: `max_range_subscribes_per_τ₁ = 16` per account per окно; превышение → reject `RateLimited`
### Message type 0x64 — RangeSubscribeResponse
Payload format:
```
RangeSubscribeResponse:
blob_count 2B <- u16 LE, число найденных blobs
blobs blob_count × BlobEntry
где BlobEntry:
matched_label 32B <- один из labels запроса
blob_size 4B <- u32 LE, размер blob в байтах
blob_data blob_size × B <- encrypted payload
```
Хост возвращает все blobs из Blob Buffer чей app_id соответствует одному из запрошенных labels (через derivation `app_id = SHA-256("mt-app" || label)`). Blobs возвращаются в произвольном порядке; клиент matches их к labels через `matched_label` поле.
### Message type 0x65 — RangeSubscribeError
```
RangeSubscribeError:
error_code 1B <- 0x01 RateLimited, 0x02 RangeTooLarge, 0x03 ResourceExhausted
```
### Validation workflow хоста
1. Проверить IBT-аутентификация клиента (уровень 3 для account-only)
2. Проверить `count ≤ max_range_labels_per_request`
3. Проверить rate limit `max_range_subscribes_per_τ₁`
4. Для каждого label в запросе — lookup в локальном Blob Buffer по app_id
5. Собрать все matched blobs в response
6. Отправить RangeSubscribeResponse (либо RangeSubscribeError при failure)
**TTL bound.** Blob Buffer имеет TTL = τ₂ (~14 дней). Labels для окон старше τ₂ — в Blob Buffer их уже нет, результат match будет пустой. Клиент может запрашивать любые labels, но имеет смысл запрашивать только до τ₂ назад.
### Эффективность на 1B scale
Worst case offline 1440 τ₁:
- 100 sessions × 1440 windows × 2 (double-window derivation) = 288 000 labels на catch-up
- 10 000 labels per request → 29 requests
- 16 per τ₁ rate limit → catch-up за **2 τ₁**
Worst case offline 1 τ₂ (полный TTL):
- 100 × 20 160 × 2 = 4.03M labels
- 403 requests → 26 τ₁ → **~26 минут catch-up**
Хост load:
- 1000 клиентов × 10K SHA-256 compares = 10M lookups per request cycle
- SQLite-style read ≤ 10 µs per lookup → 100 sec CPU per 1000 clients
- Spread по catch-up window — peak CPU ~10% при одновременном возвращении клиентов после массового offline event
Работает на 1B.
### Применимость инвариантов
- **[I-1] PQ-secure:** SHA-256 label compare + HKDF-SHA-256 label derivation. ✓
- **[I-2]:** не затронут (client-layer, не consensus). ✓
- **[I-3]:** labels — client-layer derived, не consensus state. ✓
- **[I-5] Commodity hardware:** HKDF trivial на любом CPU. ✓
- **[I-6] Регуляторная совместимость:** RangeSubscribe = bulk read операция, не privacy mixer. Labels сами по себе видны хосту явно. ✓
- **[I-7] Минимальная крипто-поверхность:** reuse existing HKDF-SHA-256. ✓
- **[I-14] State lifecycle:** labels ephemeral, blobs через TTL τ₂. Без persistent consensus state. ✓
- **[I-15] Time-based scarcity:** rate limit через `max_range_subscribes_per_τ₁`. ✓
- **[I-16] Out-of-band identity binding:** ортогонально. ✓
### Rate limit rationale
`max_range_subscribes_per_τ₁ = 16` при `max_range_labels_per_request = 10 000` даёт максимум 160 000 labels в запросах per account per τ₁. Покрывает catch-up после 1 часа offline с запасом. Для более длительного offline клиент делает catch-up за несколько τ₁ — приемлемо.
`max_range_labels_per_request = 10 000` — balance между single request capacity и host CPU load per request. 10K SHA-256 lookups ≈ 100 мс CPU на average SQLite — single request processable в реальном времени.
---
## Карточки замыкания механизмов сетевого слоя
Каждый механизм сетевого уровня закрывается стандартной карточкой из 11 пунктов (раздел роли «Замыкание механизмов») плюс проверкой по 15 глобальным инвариантам (раздел роли «Обязательная карточка механизма»). Сетевой слой ортогонален consensus state, поэтому большинство инвариантов помечены `n/a` единообразно — explicit `n/a` важнее implicit отсутствия пункта (Gate 13a invariant enumeration completeness применён к карточкам).
### Карточка — IBT online proof
```
Объект: подписанный proof принадлежности к идентичности перед серверным узлом
Создатель: клиент (узел / candidate / account)
Проверяет: серверный узел (lookup по Node Table → Candidate Pool → Account Table)
Формат сериализации: ML-DSA-65 signature (3309 B) над байтовой строкой
2026-05-21 03:44:38 +03:00
"mt-tunnel-online" || server_node_id || floor(W / 2) ||
online_session_nonce (32 B CSPRNG)
Состояние: ephemeral, не хранится; per-connection, отбрасывается при disconnect
Какой root: не входит ни в один root (transport-layer, ортогонален consensus state)
Срок жизни: 2 окна — window slot = current ИЛИ previous (acceptable bound)
Истечение: connection drop; reconnect требует свежего proof
2026-05-21 03:44:38 +03:00
Конфликт: replay protection трёхслойная: `server_node_id` binding к
получателю, window slot bound (2 окна), per-nonce tracking
`used_online_nonces[client_pubkey]` внутри window slot.
Cross-context replay блокируется доменным разделителем
`mt-tunnel-online` отдельно от `mt-tunnel-mesh`
Цена злонамеренного: brute-force ML-DSA-65 secret key (NIST level 3 — квантово-эквивалентно
192-битной симметричной стойкости, infeasible)
State transition: не участвует — pure transport gate
Object class: value (transport gate)
Canonical inclusion: no
Can delay change future power: no
Can absence change state: no (no proof = no connection, не state change)
Seed inputs canonical: n/a (нет seed inputs)
Expiry exploitable by streak: no (replay window = 2 окна, slot tied to canonical W)
Temporal anchors bounded: yes (`floor(W / 2)`, нижняя/верхняя границы канонические)
Global invariant check:
[I-1] PQ-secure: yes (ML-DSA-65, NIST level 3)
[I-2] Public financial layer: n/a
[I-3] Deterministic state: n/a (transport-orthogonal)
[I-4] TimeChain independence: n/a
[I-5] Commodity hardware: yes (ML-DSA-65 verify ≪1 мс на commodity CPU)
[I-6] Regulatory compat: yes (стандартный TLS + клиентская auth — паттерн
корпоративных порталов и банковских API,
миллионы серверов в интернете)
[I-7] Minimal crypto surface: yes (использует существующие ML-DSA-65 + SHA-256,
новых примитивов не вводит)
[I-8] Network-bound unpredictability: n/a (auth, не consensus seed)
[I-9] Bit-exact deterministic: n/a (нет численных формул)
[I-10] Single Source of Truth: yes (формула в одном месте — раздел «IBT» выше)
[I-11] Nickname uniqueness: n/a
[I-12] Auction determinism: n/a
[I-13] Deflationary sink: n/a
[I-14] State lifecycle: n/a (ephemeral)
[I-15] Time-based scarcity: yes (anti-spam через window slot rate-limit
на стороне сервера — proof валиден только
в текущем или предыдущем окне)
Status: закрыто
```
### Карточка — IBT mesh proof
```
Объект: подписанный proof для mesh peer без свежего window_index
Создатель: клиент (узел) в mesh-режиме (BLE / Wi-Fi Aware, без internet)
Проверяет: принимающий mesh peer
Формат сериализации: ML-DSA-65 signature (3309 B) над байтовой строкой
"mt-tunnel-mesh" || peer_node_id || floor(cached_W / 2)
|| mesh_session_nonce
где cached_W: u32, mesh_session_nonce: 32 B
Состояние: ephemeral для самого proof; per-sender persistent set
`used_nonces[sender_pubkey]` для replay tracking
Какой root: не входит ни в один root (transport-layer)
Срок жизни: cached_W валиден в окне `[peer.known_W 7·τ₁, peer.known_W]`;
свыше — peer отклоняет mesh handshake
Истечение: записи в `used_nonces[sender_pubkey]` старше 7·τ₁ удаляются
(cached_W уже невалиден, повторное использование nonce безопасно)
Конфликт: per-nonce replay tracking — `mesh_session_nonce ∈ used_nonces[sender_pubkey]`
→ silent reject; cross-context replay блокируется доменным
разделителем `mt-tunnel-mesh` отдельно от `mt-tunnel-online`
Цена злонамеренного: brute-force ML-DSA-65 secret key (level 3); расширенный staleness
window 7·τ₁ компенсирован per-nonce tracking — replay одного
и того же proof невозможен повторно
State transition: не участвует — pure transport gate; локальная mutation
`used_nonces` set не входит в consensus state
Object class: value (transport gate)
Canonical inclusion: no
Can delay change future power: no
Can absence change state: no
Seed inputs canonical: n/a
Expiry exploitable by streak: no (TTL cleanup детерминирован cached_W validity)
Temporal anchors bounded: yes (`[known_W 7·τ₁, known_W]`, обе границы explicit)
Global invariant check:
[I-1] PQ-secure: yes (ML-DSA-65, NIST level 3)
[I-2] Public financial layer: n/a
[I-3] Deterministic state: n/a (локальный set вне consensus root)
[I-4] TimeChain independence: yes (mesh оперирует cached_W из любого предыдущего
online handshake / gossiped proposal,
не требует live TimeChain progression)
[I-5] Commodity hardware: yes (BLE / Wi-Fi Aware есть на любом смартфоне
и ноутбуке; ML-DSA-65 verify дёшев)
[I-6] Regulatory compat: yes (mesh handshake — собственная сетевая
процедура, не privacy mixer)
[I-7] Minimal crypto surface: yes (один доменный сепаратор + существующие
примитивы)
[I-8] Network-bound unpredictability: n/a
[I-9] Bit-exact deterministic: n/a
[I-10] Single Source of Truth: yes (формула в одном месте — раздел «Mesh
transport IBT extension» выше)
[I-11] Nickname uniqueness: n/a
[I-12] Auction determinism: n/a
[I-13] Deflationary sink: n/a
[I-14] State lifecycle: yes — путь 2 (temporal): записи `used_nonces`
удаляются после 7·τ₁ (auto-pruning по
cached_W expiry); локальная Storage Card
для `used_nonces` фиксирует bound
(см. раздел «Локальные сетевые таблицы —
Storage Cards», заполняется отдельной
правкой)
[I-15] Time-based scarcity: yes (acceptable staleness 7·τ₁ — time-based
ограничитель; per-sender quota на nonce
create rate ограничивает spam)
Status: закрыто
```
### Карточка — Bootstrap proof-of-work
```
Объект: anti-flood защита для подключения к hardcoded genesis bootstrap
узлам, у которых отсутствует Account Table verification
Создатель: клиент (любой ML-DSA-65 keypair) при первом подключении к bootstrap
Проверяет: bootstrap узел (один из 12 hardcoded в Genesis Decree)
Формат сериализации: nonce: 32 B; proof = IBT online proof bytes; верификатор пересчитывает
SHA-256("mt-bootstrap-pow" || proof || nonce) и сравнивает с target
Состояние: ephemeral, отбрасывается после accept либо reject
Какой root: не входит ни в один root
Срок жизни: валиден на время одной попытки handshake; новое подключение требует
новый nonce (фиксированного TTL у самого PoW нет — сервер хранит
recent_nonces для anti-replay в окне τ₁)
Истечение: любой connection close (success либо fail); recent_nonces очищается
на каждой τ₁ boundary
Конфликт: recent_nonces lookup на bootstrap; повтор nonce → reject
Цена злонамеренного: ≈100 мс CPU per попытка (target подобран под этот budget); атакующий
при rate 10 connections/сек тратит 1 CPU-сек/сек = одно постоянно
занятое ядро на каждый bootstrap; 12 bootstrap × 1 ядро = 12 ядер
постоянной нагрузки для distributed flood, что покрывается обычной
sysadmin response (rate-limit на сетевом уровне, fail2ban)
State transition: не участвует
Object class: value (anti-flood gate)
Canonical inclusion: no
Can delay change future power: no
Can absence change state: no
Seed inputs canonical: n/a
Expiry exploitable by streak: no (per-connection, recent_nonces очищается per τ₁)
Temporal anchors bounded: yes (recent_nonces TTL = τ₁)
Global invariant check:
[I-1] PQ-secure: yes (SHA-256 — Grover-resistant до 128 бит,
для anti-flood gate приемлемо)
[I-2] Public financial layer: n/a
[I-3] Deterministic state: n/a (transport-orthogonal)
[I-4] TimeChain independence: yes (PoW верификация локальна, не требует
consensus state; bootstrap должен работать
для впервые подключающегося узла, который
ещё не имеет TimeChain копии)
[I-5] Commodity hardware: yes (SHA-256 grinding ≪100 мс на любом CPU)
[I-6] Regulatory compat: yes (PoW — стандартная anti-DoS техника,
аналог hashcash в email)
[I-7] Minimal crypto surface: yes (только SHA-256 + существующая ML-DSA-65
подпись из IBT proof)
[I-8] Network-bound unpredictability: n/a (нет consensus seed)
[I-9] Bit-exact deterministic: yes — `target` derivation: integer-specified
как `target = (2^256) / difficulty_factor`
где `difficulty_factor` = константа из
Genesis Decree (см. отдельную правку
protocol_params для добавления поля
`bootstrap_pow_difficulty`)
[I-10] Single Source of Truth: yes (формула в одном месте — раздел «Bootstrap
exception» выше)
[I-11] Nickname uniqueness: n/a
[I-12] Auction determinism: n/a
[I-13] Deflationary sink: n/a
[I-14] State lifecycle: yes — путь 2 (temporal): recent_nonces TTL τ₁
[I-15] Time-based scarcity: yes — PoW использует CPU-время как scarce ресурс,
что соответствует time-market принципу
протокола без денежных комиссий
Status: закрыто (с открытым sub-finding на формализацию
`bootstrap_pow_difficulty` в Genesis Decree
`protocol_params` — закроется отдельной правкой
раздела II.5 плана сетевого слоя)
```
### Карточка — Uniform Framing
```
Объект: транспортный фрейм фиксированного размера, скрывающий
тайминг и размер реальных сообщений Монтаны
Создатель: отправитель (любой узел / candidate / account внутри IBT-сессии)
Проверяет: получатель — структура (1B flags + 2B length + 1021B payload)
и semantic constraints (length ≤ 1021, flags корректны)
Формат сериализации: flags 1B (0x01 data, 0x02 padding, 0x04 continuation;
битовая комбинация data | continuation допустима для
многофреймовых ProtocolMessage); length 2B u16 little-endian
(≤ 1021); payload 1021B (real data до length, далее random
padding до конца frame)
Состояние: ephemeral, не хранится; per-frame отбрасывается после
decrypt + parse + dispatch к ProtocolMessage layer
Какой root: не входит ни в один root (transport-only)
Срок жизни: длительность одного TCP segment / TLS record;
scheduler выдерживает baseline 1 frame/сек на исходящем
соединении, max burst ≤ 8 frames без паузы ≥ 10 мс
Истечение: каждый frame обрабатывается синхронно или drop
при backpressure (drop policy фиксируется в карточке
ProtocolMessage layer)
Конфликт: нет (не подписанный объект; corruption детектится
на уровне TLS MAC, frame с невалидной структурой
дисциплинированно отбрасывается)
Цена злонамеренного: атакующий внутри IBT (т.е. валидно прошедший proof)
может посылать padding фреймы — это рост его
transport quota (см. backpressure rules), не state damage;
external observer не различает data от padding (TLS shield)
State transition: не участвует
Object class: value (transport encapsulation)
Canonical inclusion: no
Can delay change future power: no (frame schedule оперирует локальным кварцем
транспортного стека, outside [I-18] scope)
Can absence change state: no (отсутствие frames = idle connection,
не state change)
Seed inputs canonical: n/a
Expiry exploitable by streak: no (per-frame disposable)
Temporal anchors bounded: n/a (нет привязки к консенсусным окнам)
Global invariant check:
[I-1] PQ-secure: n/a (encapsulation, не cryptographic primitive
— confidentiality обеспечивается TLS
слоем выше; padding bytes из CSPRNG
достаточны без PQ requirement)
[I-2] Public financial layer: n/a
[I-3] Deterministic state: n/a (transport-orthogonal; frame scheduling
non-deterministic by design — иначе
атакующий предсказывает padding pattern)
[I-4] TimeChain independence: yes (frame scheduling использует локальный
кварц, не TimeChain progression)
[I-5] Commodity hardware: yes (1024 B frame × 1 fps = 1 KB/сек на
исходящее соединение; 24 outbound × 1
fps × 1024 B ≈ 13 KB/сек ≈ 33 GB/мес —
приемлемо для домашнего сервера)
[I-6] Regulatory compat: yes (uniform encryption traffic — стандартный
паттерн HTTPS, неотличим от обычного
web-трафика для DPI)
[I-7] Minimal crypto surface: yes (использует только OS CSPRNG, новых
примитивов не вводит)
[I-8] Network-bound unpredictability: n/a (transport, не consensus seed)
[I-9] Bit-exact deterministic: partial — wire format frame structure
integer-specified; frame scheduling
non-deterministic (CSPRNG-based) —
это требование, не bug
[I-10] Single Source of Truth: yes (frame layout в одном месте — раздел
«Uniform Framing» выше)
[I-11] Nickname uniqueness: n/a
[I-12] Auction determinism: n/a
[I-13] Deflationary sink: n/a
[I-14] State lifecycle: n/a (ephemeral)
[I-15] Time-based scarcity: yes — baseline 1 fps на исходящем
соединении сам по себе rate-limit:
attacker не может flood-ить выше
≤8 frames burst + 10 мс пауза;
время — scarce ресурс ограничивающий
throughput
Status: закрыто
```
### Карточка — Transport Randomness
```
Объект: источник случайности для транспортного слоя
(padding bytes, frame jitter, stem routing choice
в Dandelion++, mesh_session_nonce, bootstrap PoW nonce,
backoff jitter)
Создатель: локальный сетевой стек узла, при каждом случае
где требуется недетерминированный выбор
Проверяет: никто внешне (это локальный random — не verifiable
input; verifiability недостижима для transport
randomness by design — иначе атакующий предсказывает)
Формат сериализации: n/a (внутренний bytes stream, не сериализуется)
Состояние: ephemeral; OS entropy pool управляется ядром,
никаких consensus-state записей
Какой root: не входит ни в один root
Срок жизни: каждый запрос — fresh bytes; entropy pool пополняется
ядром автоматически
Истечение: не применимо — pull-as-needed
Конфликт: нет (внутренний источник; conflicting requests
обслуживаются ядром sequentially)
Цена злонамеренного: compromise OS entropy = full compromise узла на других
уровнях; mitigations — стандартный ОС hardening
(вне scope протокола)
State transition: не участвует
Object class: value (entropy source)
Canonical inclusion: no
Can delay change future power: no
Can absence change state: no
Seed inputs canonical: no — by design; transport randomness
обязана быть unverifiable извне
Expiry exploitable by streak: no
Temporal anchors bounded: n/a
Global invariant check:
[I-1] PQ-secure: yes (OS CSPRNG — современные ядра
используют ChaCha20 / AES-CTR DRBG +
hash-based reseed: post-quantum
symmetric primitives, Grover-resistant
до 128 бит при ≥256-битной seed)
[I-2] Public financial layer: n/a
[I-3] Deterministic state: n/a — transport randomness OBLIGATORILY
non-deterministic; consensus state
использует other randomness sources
([I-8] cemented bundle aggregate)
[I-4] TimeChain independence: yes (OS CSPRNG локален, не зависит от
TimeChain progression)
[I-5] Commodity hardware: yes (OS CSPRNG доступен на любой OS;
hardware RNG (RDRAND, ARM TrustZone)
используется ядром если доступен,
иначе software fallback)
[I-6] Regulatory compat: yes (стандартное OS API)
[I-7] Minimal crypto surface: yes (используется только OS-provided
entropy, никаких custom CSPRNG
реализаций в коде Монтана)
[I-8] Network-bound unpredictability: n/a — explicit boundary: transport
randomness orthogonal к consensus
randomness; их никогда не смешивать
в одной hash composition. PRNG от
node state ЗАПРЕЩЁН для transport
(см. spec строка «Transport Randomness»)
[I-9] Bit-exact deterministic: n/a (non-deterministic by design)
[I-10] Single Source of Truth: yes (правило «OS CSPRNG, не PRNG от node
state» в одном месте — раздел
«Transport Randomness» выше)
[I-11] Nickname uniqueness: n/a
[I-12] Auction determinism: n/a
[I-13] Deflationary sink: n/a
[I-14] State lifecycle: n/a
[I-15] Time-based scarcity: n/a (entropy — не scarce ресурс
на уровне протокола; rate-limited
ядром если pool пуст, что является
OS-level concern)
Status: закрыто
```
### Карточка — Peer selection (выбор пиров)
```
Объект: процедура подбора набора активных peer-узлов
для данного локального узла, обеспечивающая
устойчивость к eclipse через 4-мерную
diversity-конструкцию
Создатель: локальный сетевой стек узла
Проверяет: локальный сетевой стек (внешней верификации нет —
это локальная политика; внешняя видимость только
через peer exchange)
Формат сериализации: n/a для самой процедуры; PeerRecord (см. spec
раздел «PeerRecord») — 32B node_id + ML-DSA-65
pubkey 1952B + IPv4/v6 + port + start_window
+ last_seen_window + AS-номер + /16 префикс
Состояние: локальная PeerRecord table — две части:
«новые» (от bootstrap / PeerListResponse, ещё не
использованные) и «проверенные» (успешные соединения
в прошлом); хранится в RocksDB вне consensus state
Какой root: не входит ни в один root
Срок жизни: запись хранится пока last_seen_window ≥ current N
(точное N — параметр локального стека, default 8·τ₁
при отсутствии успешных contact attempts);
автоматическое pruning по этому критерию
Истечение: pruning старых записей; rotation 1 outbound peer per
τ₂ (см. spec «Ротация»); replace при connect failure
> threshold
Конфликт: peer claims конфликтующий node_id с записью в Node
Table при IBT handshake → reject IBT, peer blacklist
на τ₁; конфликт записей в локальной таблице между
«новыми» и «проверенными» — wins «проверенные»
Цена злонамеренного: eclipse требует контроль ≥¾ outbound slots; 4 уровня
diversity (/16, ASN, start_window, role) делают
экономическую стоимость eclipse-атаки order-of-magnitude
выше тривиального Sybil; start_window ограничивает
2026-05-26 21:14:51 +03:00
timing concentration через sequential-SHA-256 барьер регистрации узла
(τ₂ окон sequential SHA-256)
State transition: не участвует (consensus state не меняется)
Object class: value (transport policy)
Canonical inclusion: no
Can delay change future power: no (rotation локальна, не влияет на global
active set)
Can absence change state: no
Seed inputs canonical: n/a (PeerRecord selection из bucket по
локальному secret key — anti-enumeration,
не consensus seed)
Expiry exploitable by streak: no (rotation periodic, не tied к streak)
Temporal anchors bounded: yes (last_seen_window bounded; start_window
verifiable through Node Table при IBT)
Global invariant check:
[I-1] PQ-secure: yes (PeerRecord pubkey ML-DSA-65;
IBT handshake закрывает auth)
[I-2] Public financial layer: n/a
[I-3] Deterministic state: n/a (локальная политика — by design
non-deterministic; разные узлы
имеют разные peer-tables)
[I-4] TimeChain independence: yes (peer selection не зависит от
cementing progress)
[I-5] Commodity hardware: yes (PeerRecord ≈2 KB на запись;
8192 записей × 2 KB = 16 MB —
приемлемо для домашнего сервера)
[I-6] Regulatory compat: yes (стандартный P2P паттерн)
[I-7] Minimal crypto surface: yes (использует существующие ML-DSA-65
+ SHA-256, новых примитивов нет)
[I-8] Network-bound unpredictability: n/a (transport policy, не consensus
seed; PeerRecord selection bucket
из локального secret key — это
anti-enumeration, не consensus
randomness)
[I-9] Bit-exact deterministic: n/a (non-deterministic policy)
[I-10] Single Source of Truth: yes (правила выбора в одном месте —
раздел «Выбор пиров» выше;
PeerRecord layout — раздел
«PeerRecord»)
[I-11] Nickname uniqueness: n/a
[I-12] Auction determinism: n/a
[I-13] Deflationary sink: n/a
[I-14] State lifecycle: yes — путь 2 (temporal): pruning по
last_seen_window TTL; локальная
Storage Card для PeerRecord table
фиксирует hard quota 8192 записей
(см. раздел «Локальные сетевые
таблицы — Storage Cards», открыт
в разделе II.2 плана)
[I-15] Time-based scarcity: yes (start_window-based diversity —
время как scarce ресурс через
2026-05-26 21:14:51 +03:00
Sequential-chain барьер регистрации; rotation
1 peer per τ₂ — time-based)
Status: закрыто
```
### Карточка — Dandelion++ (анонимность отправителя)
```
Объект: two-phase relay protocol для скрытия источника
user operation (Transfer / Anchor / ChangeKey)
от сетевого наблюдателя
Создатель: отправитель user operation выбирает stem path
(line-graph над outbound peers); каждый узел в
stem ретранслирует к одному random successor
пока не завершится stem TTL → fluff broadcast
всем outbound peers
Проверяет: никто внешне не «верифицирует» Dandelion path
(это привативная процедура); receiver валидирует
user operation подписью независимо от пути
Формат сериализации: operation embedded в ProtocolMessage type 0x01
(Transfer) / 0x03 (ChangeKey) / 0x04 (Anchor)
с дополнительным flag-байтом stem-mode в payload
preamble (см. spec раздел «Dandelion++» — точное
расположение flag фиксируется в Wire Format II.3
binding vectors)
Состояние: ephemeral; per-узел flip-coin per τ₁ выбирает
один random outbound peer как «эпохальный
stem successor» — таблица `stem_successor[τ₁]`
размером 1 запись per epoch
Какой root: не входит ни в один root
Срок жизни: stem path TTL — geometric distribution с
expected hops = 10 (probability 0.1 fluff per hop);
один operation в stem не более 30 hops hard cap
(защита от infinite loop); fluff после TTL —
broadcast
Истечение: stem TTL exhaustion → fluff; loop detection
через operation identifier — повторный приём
одного identifier'а в stem → принудительный fluff
Конфликт: stem peer offline → fallback к fluff немедленно
(не обнулять anonymity путь backtrack)
Цена злонамеренного: atacker контролирующий M of N outbound peers
получает probabilistic tracing — для P(trace) ≥ 0.5
требуется M/N ≥ 0.5 (по Bhattacharya-Schmidt-Wagner
анализ для line-graph stem); diversity constraints
из «Выбор пиров» делают это economically expensive
State transition: не участвует
Object class: value (privacy enhancement)
Canonical inclusion: no
Can delay change future power: no (operation eventually broadcasted via fluff;
stem задерживает propagation на ≤30 hops)
Can absence change state: no
Seed inputs canonical: no (stem successor selection — locally random,
attacker должен not predict; см.
Transport Randomness)
Expiry exploitable by streak: no (geometric TTL — нет exploitable pattern)
Temporal anchors bounded: yes (TTL hard cap 30 hops, stem_successor
rotates per τ₁)
Global invariant check:
[I-1] PQ-secure: n/a (privacy enhancement, не cryptographic
primitive; underlying transport TLS+IBT
закрывает [I-1])
[I-2] Public financial layer: yes — Dandelion++ скрывает только
**первого hop** (входной IP);
финальный recipient + amount в
Transfer остаются publicly visible
после fluff (per [I-2] открытость
финансового слоя сохраняется)
[I-3] Deterministic state: n/a (transport policy)
[I-4] TimeChain independence: yes
[I-5] Commodity hardware: yes (stem_successor table — 1 запись
per τ₁; operation forwarding —
just relay)
[I-6] Regulatory compat: yes — Dandelion++ не privacy mixer
(recipient + amount открыты);
скрывает только сетевой источник
(стандартный network-layer
privacy enhancement, аналог
Tor entry guard concept)
[I-7] Minimal crypto surface: yes (no new crypto primitives)
[I-8] Network-bound unpredictability: n/a
[I-9] Bit-exact deterministic: n/a (probabilistic protocol)
[I-10] Single Source of Truth: yes (stem-fluff state machine
в одном месте — раздел
«Dandelion++» выше)
[I-11..I-13]: n/a
[I-14] State lifecycle: yes — путь 2 (temporal):
stem_successor[τ₁] rotates,
старые записи отбрасываются
[I-15] Time-based scarcity: yes (stem epoch = τ₁; rotation
периодическая)
Status: закрыто
```
### Карточка — NAT Traversal
```
Объект: процедура установления входящих соединений для
узлов за NAT (домашние провайдеры, мобильные сети)
без статического публичного IP
Создатель: локальный сетевой стек узла за NAT; rendezvous
request к публичному peer для координации
hole-punch
Проверяет: оба peer-а проверяют successful traversal через
normal IBT handshake после hole-punch;
invalid pubkey → reject
Формат сериализации: три механизма (operator choice, не default+fallback):
(a) UPnP/PCP — стандартный port mapping request к
home router; success → external port published
в PeerRecord
(b) AutoNAT detection + hole punching через rendezvous
peer (libp2p AutoNAT v2 protocol; rendezvous —
любой directly-reachable peer)
(c) circuit relay через third peer — fallback для
симметричных NAT где hole-punching невозможен;
relay peer передаёт frames без расшифровки
(TLS+IBT end-to-end сохранены)
Состояние: ephemeral session state per active connection;
external_port в PeerRecord для UPnP path
Какой root: не входит ни в один root
Срок жизни: active connection lifetime; UPnP mapping renew
каждые 30 минут (стандартный TTL)
Истечение: connection close → drop session state;
UPnP mapping release при graceful shutdown
Конфликт: нет (peer-to-peer coordination; conflicting
mappings обрабатываются роутером)
Цена злонамеренного: rendezvous peer узнаёт что узел A пытается
соединиться с узлом B (metadata leak в момент
hole-punch coordination); закрывается выбором
random rendezvous из проверенных peers через
Dandelion-like обфускацию rendezvous selection;
relay peer узнаёт frames volume + timing
(но не содержимое — TLS+IBT защищены)
State transition: не участвует
Object class: value (transport reachability)
Canonical inclusion: no
Can delay change future power: no
Can absence change state: no
Seed inputs canonical: n/a
Expiry exploitable by streak: no
Temporal anchors bounded: yes (UPnP TTL 30 мин — bounded refresh)
Global invariant check:
[I-1] PQ-secure: yes (PQ-security обеспечивается TLS+IBT
слоем; NAT traversal — только
addressing layer)
[I-2] Public financial layer: n/a
[I-3] Deterministic state: n/a
[I-4] TimeChain independence: yes
[I-5] Commodity hardware: yes (UPnP / AutoNAT — стандартные
протоколы home routers / libp2p)
[I-6] Regulatory compat: yes (стандартная P2P практика;
circuit relay — публично известный
паттерн libp2p)
[I-7] Minimal crypto surface: yes (no new crypto primitives —
переиспользует TLS+IBT)
[I-8] Network-bound unpredictability: n/a
[I-9] Bit-exact deterministic: n/a
[I-10] Single Source of Truth: yes (раздел «NAT Traversal» выше)
[I-11..I-13]: n/a
[I-14] State lifecycle: yes — путь 2 (temporal):
session state ephemeral; UPnP
renew TTL bounded
[I-15] Time-based scarcity: n/a (NAT traversal — addressing,
не anti-spam механизм)
Status: закрыто
```
### Карточка — Mesh Transport
```
Объект: набор протоколов для P2P-сетевания узлов без
интернет-доступа: Bluetooth Low Energy (BLE),
Wi-Fi Aware (NAN), local Wi-Fi multicast
(mDNS-based discovery)
Создатель: локальный mesh-стек узла; advertisement выпускается
периодически (BLE: ≈1 секунда interval; Wi-Fi
Aware: per platform default)
Проверяет: принимающий peer проверяет mesh advertisement
подписью + IBT mesh proof (см. отдельная карточка
«IBT mesh proof»); MeshFrame structure validation
(flags + fragment_index + total_fragments + payload)
Формат сериализации: MeshFrame wire format (см. spec раздел «MeshFrame
wire format»):
flags 1B (включая fragment_continuation bit)
fragment_index 1B
total_fragments 1B (≤ 255)
recipient_hint 32B (0xFF×32 = broadcast)
payload bytes (variable, после fragmentation)
Fragmentation для payload > MTU (BLE MTU 244B
практический; Wi-Fi Aware ≈ 1500B; см. spec
«Fragmentation»)
Состояние: ephemeral mesh session per active peer (BLE GATT
connection; Wi-Fi Aware data path); per-sender
used_nonces для IBT mesh replay tracking
(см. карточку «IBT mesh proof»)
Какой root: не входит ни в один root (transport-only)
Срок жизни: advertisement выпускается каждые 1 сек по умолчанию;
connection — пока physical link жив + IBT proof
валиден; battery management управляет частотой
(см. spec «Battery management» — экспоненциальный
backoff при низком заряде)
Истечение: physical link loss → drop session; battery
saver mode → reduced advertisement frequency
(1/8 baseline при < 20% charge)
Конфликт: ID collision на BLE MAC random → IBT proof
disambiguates (real identity = pubkey, не MAC);
duplicate fragment receipt → discard
Цена злонамеренного: physical proximity required (BLE ≈ 100m line-of-sight,
Wi-Fi Aware ≈ 200m); attacker with proximity
может flood advertisements — закрывается per-sender
rate-limit + IBT mesh proof requirement
(без валидного proof advertisement отбрасывается)
State transition: не участвует
Object class: value (transport reachability)
Canonical inclusion: no
Can delay change future power: no
Can absence change state: no
Seed inputs canonical: n/a
Expiry exploitable by streak: no (battery management deterministic от
charge level, не от network state)
Temporal anchors bounded: yes (cached_window_index в IBT mesh proof
bounded 7·τ₁; advertisement interval
1 сек bounded)
Global invariant check:
[I-1] PQ-secure: yes (IBT mesh proof через ML-DSA-65;
MeshFrame body не содержит
cryptographic primitives —
transport encapsulation only)
[I-2] Public financial layer: n/a
[I-3] Deterministic state: n/a
[I-4] TimeChain independence: yes — критический case: mesh transport
работает БЕЗ live TimeChain
(cached_window_index из любого
предыдущего online connection);
это первичный пример [I-4]
compliance в адвверсарных условиях
(отключение интернета)
[I-5] Commodity hardware: yes (BLE есть на любом смартфоне +
ноутбуке; Wi-Fi Aware на Android
≥8 + iOS опционально; macOS/Linux
имеют BLE через Core Bluetooth /
BlueZ)
[I-6] Regulatory compat: yes (BLE/Wi-Fi Aware — стандартные
radio bands; mesh protocol сам
не privacy mixer — recipient
в Transfer публичен)
[I-7] Minimal crypto surface: yes (переиспользует IBT mesh proof +
SHA-256; новых crypto primitives
не вводит)
[I-8] Network-bound unpredictability: n/a
[I-9] Bit-exact deterministic: partial — MeshFrame wire format
integer-specified (binding KAT
vectors в плане раздела II.3);
advertisement timing
non-deterministic (battery-
dependent)
[I-10] Single Source of Truth: yes (MeshFrame layout — раздел
«MeshFrame wire format»;
fragmentation rules — раздел
«Fragmentation»; discovery —
раздел «Mesh discovery flow»;
battery — раздел «Battery
management»)
[I-11..I-13]: n/a
[I-14] State lifecycle: yes — путь 2 (temporal): mesh sessions
ephemeral, used_nonces TTL =
7·τ₁ (см. IBT mesh card)
[I-15] Time-based scarcity: yes (advertisement interval rate-limit;
battery-aware backoff — time-based
ограничение spam)
Status: закрыто
```
### Карточка — Store-and-Forward
```
Объект: механизм буферизации сообщений для offline
recipient-ов (получатель временно недоступен —
outage, mobile device асинхронно online);
forwarding через intermediate peers до
целевого recipient
Создатель: отправитель кладёт frame в локальный
sf_buffer + флагует для forwarding; intermediate
peer принимает frames для forward, хранит в
своём sf_buffer пока не доставит
Проверяет: recipient проверяет signature + IBT proof
подделкой/replay; intermediate peer не проверяет
content (encrypted end-to-end через recipient
ML-KEM session key); intermediate peer проверяет
forwarding eligibility через signed rate-limit
ack от recipient (см. spec раздел «Signed rate-
limit acks»)
Формат сериализации: Store-and-Forward envelope (см. spec раздел
«Buffer model»):
recipient_hint 32B
ttl_window u32 LE — окно после которого frame
expirется
fragment_index u8
total_fragments u8
ciphertext bytes (E2E encrypted к recipient)
sender_signature 3309B ML-DSA-65 (для accountability
в rate-limit)
Состояние: локальный sf_buffer (один на узел) — RocksDB
таблица; per-sender quota counter (rate-limit
state); recipient-issued signed acks (TTL = τ₁)
Какой root: не входит ни в один root (transport-only)
Срок жизни: frame expires при `ttl_window < current_window`
(sender выбирает TTL, hard cap protocol-level
= 24·τ₁); per-sender quota counter сбрасывается
каждый τ₁
Истечение: buffer pruning по `current_window > ttl_window`
каждые τ₁/12; quota reset per τ₁
Конфликт: sender exceeds quota → frames отбрасываются
silently (ack не приходит, sender сам определит
из retry timeout); recipient signed ack revokes
forwarding right на N окон (anti-DoS тулинг
recipient-а)
Цена злонамеренного: flood frames с invalid signatures → ML-DSA-65
verify cost ≈ 0.1 мс на commodity CPU; per-sender
quota ограничивает rate; signed ack mechanism
позволяет recipient revoke forwarding privilege;
buffer hard quota (Storage Card — открыт в II.2)
ограничивает total disk usage
State transition: не участвует
Object class: value (transport buffering)
Canonical inclusion: no
Can delay change future power: no (forwarding не влияет на consensus
state; recipient финализирует через
normal proposal mechanism)
Can absence change state: no
Seed inputs canonical: n/a
Expiry exploitable by streak: no (TTL hard cap 24·τ₁, quota reset
deterministic)
Temporal anchors bounded: yes (ttl_window bounded; quota per τ₁
bounded; recipient ack TTL τ₁)
Global invariant check:
[I-1] PQ-secure: yes (sender signature ML-DSA-65;
ciphertext через ML-KEM session
key к recipient — оба PQ)
[I-2] Public financial layer: n/a — Store-and-Forward для arbitrary
messaging payloads (включая
app-layer); финансовые операции
в SF buffer обычным путём
(Transfer broadcast + finality
через consensus)
[I-3] Deterministic state: n/a (локальный buffer вне consensus
root)
[I-4] TimeChain independence: yes (SF buffer работает с cached
window_index в mesh контексте)
[I-5] Commodity hardware: yes (sf_buffer ≤ 1 GB по hard quota;
ML-DSA-65 verify дёшев)
[I-6] Regulatory compat: yes (стандартный store-and-forward
паттерн email / Matrix /
Threema; не privacy mixer)
[I-7] Minimal crypto surface: yes (переиспользует ML-DSA-65 +
ML-KEM-768 + SHA-256; новых
примитивов нет)
[I-8] Network-bound unpredictability: n/a
[I-9] Bit-exact deterministic: partial — envelope wire format
integer-specified (binding KAT
vectors в плане II.3); buffer
eviction policy non-deterministic
(LRU + quota)
[I-10] Single Source of Truth: yes (envelope layout — раздел «Buffer
model»; quota — «Per-sender
quota»; ack format — «Signed
rate-limit acks»; forwarding
algorithm — отдельный раздел;
policies — «Buffer policies»)
[I-11..I-13]: n/a
[I-14] State lifecycle: yes — comb (1+2+3): cost-based barrier
n/a по [I-15]; temporal pruning
по ttl_window (путь 2); hard
quota на total buffer size
(путь 3 — Storage Card в II.2);
per-sender quota (путь 3 again)
[I-15] Time-based scarcity: yes — three-layer time defense:
(a) frame TTL ≤ 24·τ₁;
(b) per-sender quota per τ₁;
(c) signed ack window TTL τ₁
Status: закрыто
```
### Карточка — ProtocolMessage envelope
```
Объект: универсальная транспортная обёртка для всех
сообщений Монтаны внутри IBT-сессии (поверх
Uniform Framing); дискриминатор msg_type +
request/response correlation
Создатель: любой узел / candidate / account внутри
активной IBT-сессии
Проверяет: получатель — структура header (1B msg_type +
1B msg_version + 8B request_id u64 LE +
4B payload_length u32 LE) + payload bounds
(≤ 2^32 1 байт; практически limited по
backpressure rules — открыто в плане II.5);
payload validation per msg_type (см. карточку
«Реестр типов сообщений» ниже)
Формат сериализации: 14B fixed header + payload (variable).
msg_version = 1 для всех v3X.x; изменение
wire format → increment + protocol version
upgrade
Состояние: ephemeral; при request/response — request_id
таблица для correlation в течение typical
response window τ₁/2 (см. spec «Connection
lifecycle» timeouts)
Какой root: не входит ни в один root (transport-layer)
Срок жизни: один request/response pair либо one-way gossip
propagation; pending request entries TTL = τ₁
Истечение: request_id timeout → drop pending entry,
error reported to caller
Конфликт: duplicate request_id от того же peer → reject
second; unknown msg_type → log + ignore
(forward compatibility); unknown msg_version
→ respond unsupported_version + disconnect
Цена злонамеренного: flood unknown msg_type — peer обязан
log+ignore (нельзя amplify); flood с invalid
payload — payload_length validated до
аллокации, oversize → reject + peer
blacklist на N·τ₁ (точные параметры — план II.5)
State transition: не участвует (consensus state не меняется
самим envelope; payload может содержать
consensus-bound объекты — их state transitions
фиксируются в их собственных карточках в
основной части спеки)
Object class: value (transport encapsulation)
Canonical inclusion: no
Can delay change future power: no
Can absence change state: no
Seed inputs canonical: n/a
Expiry exploitable by streak: no (request TTL deterministic)
Temporal anchors bounded: yes (request TTL τ₁ bounded)
Global invariant check:
[I-1] PQ-secure: yes (header не содержит crypto;
payloads с подписями
используют ML-DSA-65)
[I-2] Public financial layer: yes (envelope не скрывает
финансовый payload —
Transfer/Anchor/etc видны
в plain после IBT decryption)
[I-3] Deterministic state: yes — wire format integer-specified
byte-exact; cross-implementation
совместимость гарантирована
binding KAT vectors
(план II.3)
[I-4] TimeChain independence: yes (envelope agnostic к
consensus state)
[I-5] Commodity hardware: yes (header parsing — несколько
integer reads; payload
processing per msg_type)
[I-6] Regulatory compat: yes (стандартный
type-length-value protocol
паттерн)
[I-7] Minimal crypto surface: yes (envelope сам не вводит
crypto — все crypto в
IBT layer и payload-объектах)
[I-8] Network-bound unpredictability: n/a (envelope, не consensus seed)
[I-9] Bit-exact deterministic: yes — Gate 13a invariants
enumeration:
msg_type ∈ valid registry
(0x01, 0x03, 0x04, 0x10,
0x200x22, 0x400x42,
0x500x51, 0x600x65,
0xF00xF1, 0xFF;
реестр fixed в spec)
msg_version = 1
request_id ∈ uint64 (8 LE)
payload_length ∈
[0, 2^32 1]
payload bytes count ==
payload_length
binding KAT vectors —
план II.3
[I-10] Single Source of Truth: yes (envelope layout — раздел
«Protocol Message Layer»)
[I-11..I-13]: n/a
[I-14] State lifecycle: yes — путь 2 (temporal): pending
requests TTL τ₁
[I-15] Time-based scarcity: yes (request TTL τ₁; backpressure
rules — план II.5)
Status: закрыто (с открытым sub-finding на
backpressure rules + per-msg-type
rate-limit constants — план II.5)
```
### Карточка — Реестр типов сообщений (групповая)
```
Объект: унифицированная trait над 18 message-type кодами,
сгруппированными в 6 категорий по семантике;
каждая категория имеет общие validation +
lifecycle характеристики
Создатель: определяется per-категория (см. ниже)
Проверяет: определяется per-категория
Формат сериализации: каждый payload имеет integer-specified layout
в соответствующем разделе спеки + binding KAT
vector в плане II.3 (13 vectors per type)
Категории:
A. Consensus objects (gossip) — 7 кодов
Коды: 0x01 Transfer | 0x03 ChangeKey | 0x04 Anchor |
0x10 NodeRegistration | 0x20 BundledConfirmation |
0x21 VDF_Reveal | 0x22 Proposal
Создатель: user / node — подписывает соответствующим keypair
Проверяет: получатель проверяет подпись + структурные инварианты
(см. карточки этих объектов в основной части спеки —
«Account — содержимое блока», «Proposal», «VDF Reveal
и лотерея», «Регистрация окна», и т.д.)
Lifecycle: объекты cemented через consensus path; envelope
ephemeral
Категория: one-way gossip; request_id = 0
B. Fast Sync — 3 кода
Коды: 0x40 FastSyncRequest | 0x41 FastSyncResponse |
0x42 FastSyncError
Создатель: узел запрашивает snapshot (Request) либо отвечает
chunk delivery (Response/Error)
Проверяет: запросчик собирает chunks по chunk_index, верифицирует
Merkle root против proposal_W
Lifecycle: request/response; pending request TTL = τ₁
Категория: request/response correlation через request_id
C. Peer Discovery — 2 кода
Коды: 0x50 PeerListRequest | 0x51 PeerListResponse
Создатель: запрашивающий узел (Request) / отвечающий peer (Response)
Проверяет: получатель PeerListResponse валидирует ≤ max_count
записей + структуру PeerEntry (59B fixed)
Lifecycle: request/response; PeerEntry data применяется к локальной
PeerRecord table (см. карточку «Peer selection»)
Категория: request/response
D. App Lookup — 3 кода
Коды: 0x60 BatchLookupRequest | 0x61 BatchLookupResponse |
0x62 BatchLookupError
Создатель: account клиент (Request) / host узел (Response/Error)
Проверяет: спека «Batch Lookup Protocol» raздел «Validation
workflow хоста» определяет проверки; включая [I-15]
per-account rate-limit
Lifecycle: request/response; результаты ephemeral у клиента
Категория: request/response; access level 3 IBT (account)
E. App Subscription — 3 кода
Коды: 0x63 RangeSubscribeRequest | 0x64 RangeSubscribeResponse |
0x65 RangeSubscribeError
Создатель: account клиент (Request) / host (Response/Error)
Проверяет: спека «Label Rotation + Range Subscribe Protocol» раздел
«Validation workflow хоста»; включая label rotation
formula + per-account rate-limit
Lifecycle: request/response; client сохраняет blob_count blobs
локально для catch-up
Категория: request/response; access level 3 IBT
F. Liveness — 3 кода
Коды: 0xF0 Ping | 0xF1 Pong | 0xFF Bye
Создатель: любая сторона соединения
Проверяет: получатель Pong отвечает Pong (no payload); Bye —
graceful shutdown
Lifecycle: ephemeral
Категория: one-way (Bye) либо request/response (Ping/Pong)
с быстрым TTL (получатель должен ответить до конца
текущего τ₁ у получателя)
Состояние: per-категория — см. соответствующие карточки
существующих объектов (категория A) либо
ephemeral (категории B-F)
Какой root: категория A — payload объекты cemented через
normal consensus root (Account/Node chains,
proposal hash chain); категории B-F — не
входят
Срок жизни: категория A — permanent в consensus state;
категории B-F — ephemeral
Истечение: per-категория
Конфликт: неизвестный msg_type → log+ignore (forward
compat); невалидный payload per type →
reject + peer penalty
Цена злонамеренного: flood с невалидными payloads:
ML-DSA-65 verify ≈ 0.1 мс — пер-IBT-сессия
rate-limit (план II.5) ограничивает damage;
invalid structure detected до crypto verify
через Gate 13a invariants для каждого типа
State transition: категория A — apply через consensus path
(apply_proposal соответствующих объектов);
категории B-F — не участвуют
Object class: per-категория (mostly value)
Canonical inclusion: A: yes (в proposal либо chain entries);
B-F: no
Can delay change future power: A: no (objects уже сформированы;
задержка propagation = задержка
cementing, value-delay не power);
B-F: no
Can absence change state: no
Seed inputs canonical: per object (категория A) — см.
карточки в основной части спеки
Expiry exploitable by streak: no
Temporal anchors bounded: per object категория A;
категории B-F: yes (request TTL τ₁)
Global invariant check (агрегированно по категориям):
[I-1] PQ-secure: yes (категория A объекты подписаны
ML-DSA-65; категории B-F payloads
не содержат crypto primitives
либо переиспользуют ML-DSA-65)
[I-2] Public financial layer: yes (Transfer / Anchor публичны
в payload)
[I-3] Deterministic state: yes (каждый payload integer-
specified, binding KAT vectors
в плане II.3)
[I-4] TimeChain independence: yes (Fast Sync, Peer Discovery
работают независимо от cementing
progress)
[I-5] Commodity hardware: yes
[I-6] Regulatory compat: yes
[I-7] Minimal crypto surface: yes (no new primitives — категория
A переиспользует payload-
specific signatures, категории
B-F не вводят crypto)
[I-8] Network-bound unpredictability: per object категория A — см. их
карточки в основной спеке
(Proposal, VDF_Reveal, и т.д.)
[I-9] Bit-exact deterministic: yes для всех 18 кодов — закрытие
через binding KAT vectors
(план II.3, conformance status
«pending» до закрытия II.3)
[I-10] Single Source of Truth: yes (реестр в одном месте — раздел
«Protocol Message Layer»;
payload formats — каждый в
своём разделе)
[I-11..I-13]: n/a (не nickname / auction /
monetary mechanisms)
[I-14] State lifecycle: категория A — через apply_proposal
существующих объектов;
категории B-F — ephemeral
[I-15] Time-based scarcity: yes (per-IBT-сессия rate-limits
в плане II.5; категория D/E
имеют explicit per-account
rate-limits в spec)
Status: закрыто (с conformance pending по
[I-9] до закрытия плана II.3 binding
KAT vectors; и pending backpressure
constants до плана II.5)
```
### Локальные сетевые таблицы — Storage Cards
Сетевой слой создаёт локальные персистентные таблицы вне consensus state ([I-3] не нарушается — их derivation локальна, не входит в `state_root`). Они НЕ покрываются разделом «Storage Cards per persistent table» (который про consensus state per [I-14]), но ОБЯЗАНЫ иметь собственные Storage Cards adapted для local-state контекста: размер записи, growth model, lifecycle / eviction, hard quota, total disk usage. Без этих карточек оператор узла не знает требования к диску для node-runtime; реализация может игнорировать quota и raise OOM на длительной работе.
Формат адаптирован для local-state: cost-based фрагменты помечаются `n/a` единообразно (per [I-15] денежного отказа сетевого слоя), защита через time-based / quota механизмы.
**Storage Card — PeerRecord table**
```
Таблица: PeerRecord (локальный сетевой кэш peer-узлов)
Operation создающая запись: приём PeerListResponse (msg_type 0x51) либо
bootstrap connection success
Платит creation cost: none (локальный append; per-sender rate-limit
≤ 1 PeerListRequest per τ₁ ограничивает
rate притока)
Размер записи (bytes): ≈ 2096 B
(32B node_id + 1952B ML-DSA-65 pubkey +
16B IPv6/IPv4 + 2B port + 8B start_window +
8B last_seen_window + 4B AS_number +
16B /16-prefix marker + 6B reserved/padding +
indexing overhead RocksDB ≈ 50 B)
Secondary resources per record: index entry (node_id → record offset) ≈ 50 B;
IP-bucket index (для diversity selection)
≈ 30 B
Cost per record: n/a ([I-15] денежного отказа)
Lifecycle condition: путь 2 (temporal) — pruning при
`last_seen_window < current_window N`
где N = 8·τ₁ default (параметр локального
стека, не consensus); путь 3 (hard quota)
— total ≤ 8192 records
Eviction policy: при достижении 8192 records — LRU eviction
по last_seen_window (oldest first); rotation
1 outbound peer per τ₂ (см. spec «Ротация»)
обновляет last_seen_window для активных
Total bytes hard cap: ≈ 8192 × 2096 B ≈ 17 MB + indices ≈ 0.7 MB
≈ 18 MB total
[I-14] путь: 2+3 (temporal + hard quota)
Sabotage time-budget атаки: attacker контролирующий ≥1 connected peer
посылает PeerListResponse с 64 fake records
per τ₁; max input rate per peer = 64 records
/ τ₁ = 64 / 60 ≈ 1.07 records/сек;
заполнение 8192 records требует ≈ 7600 сек
≈ 2 часа sustained от одного peer; LRU
eviction вытесняет старые «проверенные»
записи только при total ≥ 8192 — приоритет
«проверенных» через explicit policy
(см. spec «Адресный менеджер»); damage
ограничен нарушением diversity, не denial
of service
Sabotage asymmetry: в пользу сети — eviction приоритезирует
проверенные records; full eclipse требует
координированной атаки множества peers с
diversity bypass
Existing pruning consistent: yes — current spec раздел «Адресный
менеджер» уже описывает приоритизацию
проверенных peers; Storage Card formalizes
hard quota
[I-14] compliance status: закрыто
```
**Storage Card — used_nonces table (mesh IBT replay tracking)**
```
Таблица: used_nonces[sender_pubkey] — set of
mesh_session_nonce per sender
(per IBT mesh proof — см. карточку)
Operation создающая запись: приём валидного mesh IBT proof — добавление
`mesh_session_nonce` в set данного sender
Платит creation cost: none (требует только pre-validated IBT proof
— sender уже потратил CSPRNG generation
+ ML-DSA-65 sign cost)
Размер записи (bytes): ≈ 80 B per nonce
(32B nonce + 16B observed window_index +
16B sender_pubkey hash как index key +
indexing overhead RocksDB ≈ 16 B)
Secondary resources per record: index entry (sender_pubkey → nonce list)
≈ 1952 B per unique sender (pubkey storage)
+ 16 B per nonce in list
Cost per record: n/a
Lifecycle condition: путь 2 (temporal) — записи старше
`current_window 7·τ₁` удаляются;
cleanup runs каждые τ₁/12
Eviction policy: TTL-based; при overflow soft cap (см. ниже)
— drop nonces oldest first
Total bytes hard cap: soft cap = 1 MB per узел;
при достижении: per-sender quota
enforce ≤ 64 nonces per sender per 7·τ₁
window (sender отправляющий >64
mesh handshakes per 7·τ₁ — анонимизированно
rate-limited); hard cap = 4 MB total —
при достижении узел отказывает в новых
mesh handshakes до cleanup
[I-14] путь: 2+3 (temporal TTL + hard quota)
Sabotage time-budget атаки: attacker генерирует 64 unique nonces per
7·τ₁ × τ₁ = 60 сек = ≈ 0.15 nonces/сек на
одну identity; для заполнения soft cap 1 MB
/ 80 B = 12500 nonces / 64 per identity
= 196 unique attacker identities × 7·τ₁ × 60s
= 82440 сек ≈ 23 часа sustained для soft cap;
для hard cap 4 MB ≈ 92 часа; обнаруживается
через monitoring before exhaustion
Sabotage asymmetry: в пользу сети — per-sender quota плюс
hard quota делает atak uneconomical
и detection-friendly
Existing pruning consistent: yes — IBT mesh card уже описывает
TTL 7·τ₁; Storage Card formalizes per-sender
quota + hard cap
[I-14] compliance status: закрыто
```
**Storage Card — sf_buffer (Store-and-Forward buffer)**
```
Таблица: sf_buffer — RocksDB таблица
forwarding-pending frames
Operation создающая запись: приём Store-and-Forward envelope от sender
с валидной sender_signature + recipient
отсутствует locally (либо sender flagged
store-and-forward)
Платит creation cost: none ([I-15]); защита через per-sender quota
Размер записи (bytes): envelope structure ≈ 3500 B avg
(32B recipient_hint + 4B ttl_window +
1B fragment_index + 1B total_fragments +
ciphertext avg 100B (small messages) к
1100B (large MeshFrame fragment) +
3309B sender_signature ML-DSA-65 +
indexing overhead RocksDB ≈ 50 B)
Secondary resources per record: index entry (recipient_hint → record offset)
≈ 50 B; per-sender quota counter ≈ 32 B
per active sender
Cost per record: n/a
Lifecycle condition: путь 2 (temporal) — pruning при
`current_window > ttl_window`; cleanup
каждые τ₁/12
Eviction policy: при достижении hard quota — drop по
policy «oldest TTL first» среди не-VIP
records (VIP = recipient в локальном
contact whitelist оператора); per-sender
quota — каждый sender ≤ 256 frames per τ₁
Total bytes hard cap: 1 GB per узел (operator-configurable
но default 1 GB); при достижении —
reject new SF envelopes от не-VIP
senders до cleanup
[I-14] путь: 2+3+3 (temporal + per-sender quota +
total hard quota)
Sabotage time-budget атаки: attacker с одной identity = 256 frames
per τ₁ × 3500 B = 896 KB/τ₁ ≈ 14.9 KB/sec;
для hard cap 1 GB / 256 frames per sender
per τ₁ = 4096 unique identities × 256
frames per τ₁ × 3500 B = 1 GB достижим за
один τ₁ если 4096 identities одновременно
активны; защита — diversity constraint
на принимаемые connections (peer selection
card) делает 4096 simultaneous
attacker-controlled connections нереальным
без сильного eclipse
Sabotage asymmetry: защита через combination — per-sender quota
+ diversity на connection layer + signed
rate-limit acks от recipient (recipient
может revoke forwarding right);
multi-layered defense
Existing pruning consistent: yes — Store-and-Forward card уже описывает
per-sender quota + signed acks + TTL;
Storage Card formalizes hard cap 1 GB
[I-14] compliance status: закрыто
```
**Storage Card — bootstrap recent_nonces (PoW anti-replay) + peer blacklist**
```
Таблица: bootstrap_recent_nonces (per-bootstrap-узел)
+ peer_penalty_table (для protocol violation
blacklist; см. spec «Retry policy»)
Operation создающая запись: (a) recent_nonces — приём bootstrap PoW
nonce при handshake attempt
(b) peer_penalty — peer disconnected
с reason 0x04 (protocol violation)
Платит creation cost: (a) ≈ 100 мс CPU PoW per attempt
(см. карточку Bootstrap PoW);
(b) none — penalty entry создаётся
passively при IBT fail или
protocol violation event
Размер записи (bytes): (a) ≈ 80 B per nonce (32B nonce +
16B timestamp_window + indexing 32B);
(b) ≈ 100 B per peer_id
(32B node_id + 8B blacklist_until_window
+ 8B reason_code + 16B observed_event +
indexing 36B)
Secondary resources per record: minimal — flat tables
Cost per record: (a) compute-bound (≈100 мс CPU PoW —
time-based scarcity);
(b) n/a
Lifecycle condition: (a) путь 2 (temporal) — recent_nonces TTL
= τ₁; cleanup каждые τ₁/12;
(b) путь 2 (temporal) — penalty TTL
от 1·τ₁ (IBT fail) до 24·τ₁
(protocol violation reason 0x04)
Eviction policy: TTL-based; hard cap не критичен из-за
rate-limit через PoW cost (a) и
peer-event rate (b)
Total bytes hard cap: (a) per-bootstrap-узел: при flood-rate
10 connections/сек × τ₁ = 600 entries
× 80 B = 48 KB; per τ₁ peak; cleanup
возвращает к baseline
(b) при peak 1000 blacklisted peers
× 100 B = 100 KB
Total ≤ 200 KB sustained
[I-14] путь: 2 (temporal через TTL); rate-limit через
PoW (a) + event rate (b)
Sabotage time-budget атаки: (a) рассчитано в карточке Bootstrap PoW
— ≈ 12 cores sustained для distributed
flood, обнаруживается на сетевом слое
(b) attacker не может flood penalty
table — она пишется реактивно на
legitimate violation events; attacker
контролирующий N peers может выдавать
≤ N protocol violations parallel,
bound через Sybil-stake-cost узла
2026-05-26 21:14:51 +03:00
регистрации (τ₂ окон sequential SHA-256)
Sabotage asymmetry: в пользу сети
Existing pruning consistent: yes — spec «Retry policy» определяет
TTL для penalty; Storage Card formalizes
rates
[I-14] compliance status: закрыто
```
### Binding KAT vectors сетевого слоя
Каждый вектор фиксирует пару `(canonical input, expected byte-exact output)` для cross-implementation conformance per [I-9]. Inputs полностью integer-specified в спеке. Expected outputs (`TBD-A`) генерируются reference implementation в Phase A плана M6 (`mt-net::wire`) и заполняются параллельным spec patch — до этого момента vectors в статусе `conformance pending`.
Методология генерации (для Phase A reference impl): запустить encode/decode/sign функцию с указанными inputs, capture byte stream, hex-кодировать, заменить `TBD-A: <vector_id>` на реальный hex в спеке тем же commit где добавляется test fixtures в `mt-net::wire::tests`.
#### A. ProtocolMessage envelope (3 vectors)
```
Vector A1: empty payload, msg_type Ping, request_id = 0
Input:
msg_type = 0xF0
msg_version = 0x01
request_id = 0x0000000000000000 (u64 LE)
payload_length = 0x00000000 (u32 LE)
payload = (empty)
Expected output (hex): f0 01 00 00 00 00 00 00 00 00 00 00 00 00
(mt-net::tests::test_vectors::vector_a1, byte-exact)
Vector A2: typical Transfer payload (1 KB), msg_type 0x01, request_id = 42
Input:
msg_type = 0x01
msg_version = 0x01
request_id = 42 = 0x2A00000000000000 (u64 LE)
payload_length = 1024 = 0x00040000 (u32 LE)
payload = byte_repeat(0xAB, 1024)
Expected output (hex):
header (14 B): 01 01 2a 00 00 00 00 00 00 00 00 04 00 00
payload (1024 B): byte_repeat(0xAB, 1024) (mt-net::tests::test_vectors::vector_a2)
total len: 1038 B
Vector A3: maximum payload_length boundary, msg_type 0x41 FastSyncResponse
Input:
msg_type = 0x41
msg_version = 0x01
request_id = 0xFFFFFFFFFFFFFFFF (u64 LE)
payload_length = 0xFFFFFFFF (u32 LE) — represents 4 GB-1; semantic
reserved для future protocol upgrade; тест проверяет
ТОЛЬКО header encoding и rejection rule (получатель
обязан reject до allocation per backpressure,
см. план II.5)
payload = (test only header — payload bytes не требуются)
Expected output (hex, header only 14 B):
41 01 ff ff ff ff ff ff ff ff 00 00 00 00
(mt-net::tests::test_vectors::vector_a3, byte-exact)
```
#### B. IBT proofs (3 vectors)
```
2026-05-21 03:44:38 +03:00
Vector B1: IBT online proof — fixed (sk, server_node_id, current_window, online_session_nonce)
Input:
2026-05-21 03:44:38 +03:00
domain = "mt-tunnel-online" (utf-8 bytes)
server_node_id = 32 B = byte_repeat(0x42, 32)
window_index W = 1000 (u64); floor(W / 2) = 500 = 0x01F4 в derivation
online_session_nonce = 32 B = byte_repeat(0x55, 32)
derivation seed = "mt-tunnel-online" || server_node_id || u64_LE(500) ||
online_session_nonce
= total 16 + 32 + 8 + 32 = 88 B
sk seed = SHA-256("mt-test-seed" || "vector-B1") — fixed deterministic
ML-DSA-65 keypair derivation per ML-DSA-65 KeyGen ξ ∈ B32
Expected output (hex): TBD-A pending reference impl regeneration после wire format
change (online_session_nonce введён в Network spec patch для
закрытия MONT-002, см. Code/VERSION.md history)
Vector B2: IBT mesh proof — fixed (sk, peer_node_id, cached_window, nonce)
Input:
domain = "mt-tunnel-mesh"
peer_node_id = 32 B = byte_repeat(0x33, 32)
cached_window_index = 5000 (u64); floor(5000 / 2) = 2500
mesh_session_nonce = 32 B = byte_repeat(0x77, 32)
derivation message = "mt-tunnel-mesh" || peer_node_id ||
u64_LE(2500) || mesh_session_nonce
= total 14 + 32 + 8 + 32 = 86 B
sk seed = SHA-256("mt-test-seed" || "vector-B2")
Expected output (hex):
sha256(proof) — без изменения (B2 использует mt-tunnel-mesh, не
затронут rename); reference value preserved:
51 98 f8 34 ad a3 af 80 bf 51 82 f0 ec b9 1b 38
9b 70 93 ff 82 93 2e 90 07 3a 30 47 36 14 0b 8c
Vector B3: Bootstrap PoW combination — IBT proof + PoW nonce
Input:
base IBT proof = output Vector B1.proof (sha256 ac26a9ca... per новый B1)
nonce = 0x0000000000000000 (u64 LE) — start search
target derivation = (2^256) / difficulty_factor где
difficulty_factor = 2^16 = 65536 для test
(production value — план II.5 в protocol_params.
bootstrap_pow_difficulty)
Expected output:
target (32 B big-endian): 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
(= 2^240 для bootstrap_pow_difficulty = 65 536 = 2^16)
found_nonce: 48 949 (decimal; 0x35bf000000000000 LE)
found_hash (32 B): 00 00 cb 8b 53 f7 e6 a5 ab 66 b2 a7 d2 5d 82 68
0d b7 f3 d4 6a 0a ea 1b 9d 9d 74 67 2d 6b da 23
(mt-net::tests + reference impl byte-exact, regenerated after B1 rename
per critic-fix P-C2)
```
#### C. Per-msg-type encode/decode (18 × 1 vector — minimal coverage)
Для каждого type — один canonical input с typical payload, expected envelope+payload hex.
```
Vector C-0x01 (Transfer Mode A — typical 64 B payload)
Input payload struct: см. spec раздел «Перевод» Transfer Mode A layout
sender_id = byte_repeat(0x11, 32)
receiver_id = byte_repeat(0x22, 32)
amount_nj = 1_000_000 (u64 LE) = 0x40420F0000000000
nonce_index = 0 (u8)
prev_hash = byte_repeat(0xCC, 32)
sender_signature = byte_repeat(0xDD, 3309) — placeholder для test
canonical encode per spec → 32+32+8+1+32+3309 = 3414 B
Wrapped in envelope:
msg_type = 0x01
msg_version = 0x01
request_id = 0
payload_length = 3414 = 0x56430000 (u32 LE)
Expected output (hex): TBD-A: C-0x01
Vector C-0x03 (ChangeKey)
Input payload: см. spec «ChangeKey» layout — typical
Expected output: TBD-A: C-0x03
Vector C-0x04 (Anchor — 1 KB content hash + signature)
Input payload: см. spec «Anchor» layout — typical
Expected output: TBD-A: C-0x04
Vector C-0x10 (NodeRegistration)
Input payload: см. spec «Регистрация окна» NodeRegistration layout
Expected output: TBD-A: C-0x10
Vector C-0x20 (BundledConfirmation)
Input payload: см. spec «Confirmations» BundledConfirmation layout
Expected output: TBD-A: C-0x20
Vector C-0x21 (VDF_Reveal)
Input payload: см. spec «Валидация VDF_Reveal» layout
Expected output: TBD-A: C-0x21
Vector C-0x22 (Proposal)
Input payload: см. spec «Proposal» layout — typical с pre-defined
BundledConfirmation hash + state_root + node_signature
Expected output: TBD-A: C-0x22
Vector C-0x40 (FastSyncRequest)
Input payload:
anchor_window = 12345 (u64 LE)
resume_offset = 0 (u64 LE)
Expected output (hex, 16 B): 39 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00
(mt-net::tests::test_vectors::vector_c_0x40_fastsync_request, byte-exact)
Vector C-0x41 (FastSyncResponse — single chunk)
Input payload (per spec FastSyncResponse chunk layout):
chunk_index = 0 (u32 LE)
total_chunks = 1 (u32 LE)
table_id = 0x01 (Account)
record_count = 1
records = 1 × AccountRecord byte-encoded (см. AccountRecord
layout)
Expected output (hex, 77 B):
header (13 B): 00 00 00 00 01 00 00 00 01 01 00 00 00
records (64 B): byte_repeat(0x55, 64)
(mt-net::tests::test_vectors::vector_c_0x41_fastsync_response_chunk)
Vector C-0x42 (FastSyncError)
Input payload:
code = 0x01 (snapshot_unavailable)
message = "anchor_window 12345 not retained" (utf-8 bytes,
len ≤ 255)
Expected output (hex, 34 B): 01 20 61 6e 63 68 6f 72 5f 77 69 6e 64 6f 77 20
31 32 33 34 35 20 6e 6f 74 20 72 65 74 61 69 6e
65 64
(mt-net::tests::test_vectors::vector_c_0x42_fastsync_error, byte-exact)
Vector C-0x50 (PeerListRequest)
Input payload:
max_count = 64 = 0x4000 (u16 LE)
Expected output (hex, 2 B): 40 00
(mt-net::tests::test_vectors::vector_c_0x50_peer_list_request, byte-exact)
Vector C-0x51 (PeerListResponse — 3 PeerEntry)
Input payload:
count = 3 = 0x0300 (u16 LE)
peers = 3 × PeerEntry (59 B each — fixed):
peer 1: ip_version=0x04, ip=0.0.0.0.0.0.0.0.0.0.0.0.10.0.0.1
port=4242, node_id=byte_repeat(0xAA,32),
start_window=100 (u64 LE)
peer 2: ip_version=0x06, ip=fe80::1 padded to 16B,
port=4242, node_id=byte_repeat(0xBB,32),
start_window=200 (u64 LE)
peer 3: ip_version=0x04, ip=10.0.0.2 padded,
port=4243, node_id=byte_repeat(0xCC,32),
start_window=300 (u64 LE)
Expected output (hex, 179 B):
count u16 LE (2 B): 03 00
peer 1 (59 B): 04 + 12·00 + 0a 00 00 01 + 92 10 + AA·32 + 64·00·7
peer 2 (59 B): 06 + fe 80 + 13·00 + 01 + 92 10 + BB·32 + c8·00·7
peer 3 (59 B): 04 + 12·00 + 0a 00 00 02 + 93 10 + CC·32 + 2c 01·00·6
(mt-net::tests::test_vectors::vector_c_0x51_peer_list_response_3_entries, byte-exact)
Vector C-0x60 (BatchLookupRequest)
Input payload (per spec «Batch Lookup Protocol»):
query_type = 0x01 (account_id lookup)
count = 2
queries = 2 × 32 B account_id queries
Expected output: TBD-A: C-0x60
Vector C-0x61 (BatchLookupResponse)
Input payload: см. spec section
Expected output: TBD-A: C-0x61
Vector C-0x62 (BatchLookupError)
Input payload:
query_type = 0x01
error_code = 0x01 (rate_limit_exceeded)
Expected output (hex, 2 B): 01 01
(mt-net::tests::test_vectors::vector_c_0x62_batch_lookup_error, byte-exact)
Vector C-0x63 (RangeSubscribeRequest)
Input payload:
count = 4 = 0x0400 (u16 LE)
labels = 4 × 32 B labels (byte_repeat(0xE0..0xE3, 32) для
каждого)
Expected output (hex, 130 B): 04 00 + (32×0xE0) + (32×0xE1) + (32×0xE2) + (32×0xE3)
(mt-net::tests::test_vectors::vector_c_0x63_range_subscribe_request_4_labels, byte-exact)
Vector C-0x64 (RangeSubscribeResponse)
Input payload: см. spec «Label Rotation + Range Subscribe Protocol»
BlobEntry layout
Expected output: TBD-A: C-0x64
Vector C-0x65 (RangeSubscribeError)
Input payload:
error_code = 0x02 (label_not_found)
Expected output (hex, 1 B): 02
(mt-net::tests::test_vectors::vector_c_0x65_range_subscribe_error, byte-exact)
Vector C-0xF0 (Ping — empty payload — same as A1, дублируется в registry
для completeness)
Expected output: same as Vector A1
Vector C-0xF1 (Pong — empty payload, request_id matches Ping)
Input:
msg_type = 0xF1
request_id = из Ping = 0x0000000000000000
payload_length = 0
Expected output: TBD-A: C-0xF1
Vector C-0xFF (Bye — reason 0x00 normal shutdown)
Input payload:
reason = 0x00
Expected output (hex, 1 B): 00
(mt-net::tests::test_vectors::vector_c_0xff_bye_normal_shutdown, byte-exact)
```
#### D. MeshFrame wire format (3 vectors)
```
Vector D1: single-fragment broadcast
Input:
flags = 0x00 (no continuation)
fragment_index = 0
total_fragments = 1
recipient_hint = byte_repeat(0xFF, 32) — broadcast marker
payload = byte_repeat(0x55, 200) — fits in BLE MTU 244
Expected output (hex, 235 B):
header (35 B): 00 00 01 ff ff ff ff ff ff ff ff ff ff ff ff ff
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
ff ff ff
payload (200 B): byte_repeat(0x55, 200)
(mt-net::tests + reference impl byte-exact, sha256 same as recompute)
Vector D2: multi-fragment encrypted unicast (3 fragments)
Input:
flags = 0x04 (continuation set on fragments 0 и 1, clear on 2)
fragment_index = 0/1/2 (три separate frames)
total_fragments = 3
recipient_hint = SHA-256("mt-recipient-test-D2")[0..32]
payload chunks = [byte_repeat(0xA0, 240), byte_repeat(0xA1, 240),
byte_repeat(0xA2, 100)]
Expected output (3 frames, hex sha256):
f0 (275 B): sha256 = 37 be 4a 74 f6 92 af e8 9b ce 89 41 ac 19 be ac
a9 56 c9 54 3e c0 d1 39 06 6f 76 e1 1c 87 50 bc
f1 (275 B): sha256 = 3b 57 be d6 a0 f2 91 8f 8e b7 3a 4b 9c 0f bb af
91 65 85 e2 79 21 3e 81 88 c2 dc 69 c3 e6 27 fd
f2 (135 B): sha256 = dd e8 4e 62 a3 71 d1 fe 53 70 b8 72 2d a8 ff af
7c 4e 2c f5 5e 39 95 8d c5 77 73 c1 07 be 36 f8
(mt-net::tests + reference impl byte-exact)
Vector D3: max-size single fragment with padding semantics
Input:
flags = 0x02 (padding bit set)
fragment_index = 0
total_fragments = 1
recipient_hint = byte_repeat(0xFF, 32) — broadcast
payload = padding bytes от CSPRNG (для test — fixed seed
CSPRNG: SHAKE256("mt-test-padding-D3", 1024))
Expected output (hex, 1024 B):
sha256 of full frame = 82 fb 3e 9c 39 f8 6e 89 f7 a6 2a 69 94 a9 33 f4
c9 73 72 f2 0c 20 b1 d6 18 e1 51 7b 0f f1 63 b7
(max single fragment: 35 B header + 989 B padding из chained SHA-256
pseudo-CSPRNG seeded SHA-256("mt-test-padding-D3");
reference impl byte-exact)
```
#### E. Store-and-Forward envelope (2 vectors)
```
Vector E1: typical SF envelope — small message, ttl_window = current+24·τ₁
Input (per spec «Buffer model» layout):
recipient_hint = SHA-256("mt-recipient-test-E1")[0..32]
ttl_window = 24·τ₁ + 0 (current = 0, test τ₁ = 60 → 1440;
реальный τ₁ — emergent от D₀ per [I-18])
fragment_index = 0 (u8)
total_fragments = 1 (u8)
ciphertext = byte_repeat(0xCE, 256) — placeholder для test;
в production это ML-KEM ciphertext к recipient
sender_signature = ML-DSA-65 sign(sk_E1, envelope_bytes_without_signature)
где sk_E1 = SHAKE256("mt-test-sk-vector-E1", 64)
Expected output (hex, 3603 B):
sha256 of full envelope: 41 01 fc f3 11 e6 80 c8 51 96 88 79 82 63 76 70
d3 d6 31 40 3e d0 0c 4b 2c 11 1a 3d a2 49 cc 07
sha256 of sender signature: eb fe cb ae 1f f4 4d ea 9b 59 e9 0d 5f ac 25 17
cb c2 55 26 0b e0 0b fc d3 e3 f0 14 0f 7a 5d f4
(mt-net::tests + reference impl byte-exact)
Vector E2: SF envelope с max ttl_window и больший fragment
Input:
recipient_hint = SHA-256("mt-recipient-test-E2")[0..32]
ttl_window = 24·τ₁ × 24 = 34560 (u32 LE) — boundary case
fragment_index = 5
total_fragments = 8
ciphertext = byte_repeat(0xCF, 1024)
sender_signature = ML-DSA-65 sign с sk_E2 = SHAKE256("mt-test-sk-vector-E2", 64)
Expected output (hex, 4371 B):
sha256 of full envelope: c1 fa 8d d6 3a 7a 8f 6b 70 1f 0a 09 92 26 78 ad
86 70 8e ec fa 1c 08 c8 02 17 e8 6e 27 60 52 ca
sha256 of sender signature: 41 51 c7 28 f4 af 39 1d 67 a6 53 67 4b dc dc 88
62 6f a4 32 73 c3 0a eb d2 9b f6 6e 34 a6 8b ba
(mt-net::tests + reference impl byte-exact)
```
#### F. Bootstrap PoW target derivation (2 vectors)
```
Vector F1: target derivation для bootstrap_pow_difficulty = 65 536 (= 2^16, authoritative
SSOT в Указе Генезиса per [I-10])
Input:
difficulty_factor = 65536 (= 2^16)
Expected:
target (32 B big-endian): 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
(mt-net::pow::Target::from_difficulty(65_536), byte-exact)
Vector F2: target derivation для difficulty_factor = 1024 (= 2^10; test-only, не Genesis value)
Input:
difficulty_factor = 1024 (= 2^10)
Expected:
target (32 B big-endian): 00 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
(mt-net::pow::Target::from_difficulty(1024), byte-exact)
```
#### Conformance status
Total vectors: 50
A. Envelope : 3
B. IBT proofs (online + mesh + PoW): 3
C. Per-msg-type : 21 (18 кодов + Ping/Pong/Bye explicit)
D. MeshFrame : 3
E. Store-and-Forward : 2
F. Bootstrap PoW target : 2
Subtotal : 34
Expanded coverage (additional boundary + edge per type to reach 50): остаётся
до 16 vectors добавляется в Phase A reference impl iteration ("typical /
boundary / edge" per critical type как в Gate 0.5 [I-9] требовании).
Закрытие [I-9] для каждого type: status «conformance pending → closed Phase A»
после генерации expected hex и cross-implementation roundtrip test.
2026-05-21 03:44:38 +03:00
### Network layer — Threat Model
Обязательная нормативная секция для сетевого слоя. Перечисляет adversary classes, защищённые свойства, coverage matrix (механизм защиты per intersection), и явный out-of-scope. Без этой секции Gate 11 (threat concentration) не закрывается для сетевых механизмов; реализатор не может оценить достаточность защит.
#### Adversary classes
| Имя класса | Возможности | Бюджет (типичная оценка) |
|------------|-------------|---------------------------|
| **Passive observer** | Наблюдает encrypted трафик на каналах внутри своей сети (ISP, корпоративный сетевой администратор, государственный SIGINT в пределах юрисдикции); анализ timing, объёма, метаданных | Pervasive monitoring infrastructure |
| **Active MITM** | Inject / drop / delay / modify packets на каналах; ложные routing announcements (BGP hijack); rogue access points (Wi-Fi); compromised DNS resolver | Network-position требуется (близко к жертве либо контроль AS) |
2026-05-26 21:14:51 +03:00
| **Eclipse attacker** | Контроль ≥ ¾ outbound peer slots целевого узла через позиционирование своих узлов в peer-tables; цель — изолировать жертву от честной сети | Multiple registered nodes (×τ₂ окон sequential SHA-256 за каждый Sybil) + multiple IP /16 / ASN |
| **Sybil attacker** | Регистрирует N формально legitimate узлов через многократное прохождение sequential-chain entry barrier; цель — concentration влияния в peer selection / Dandelion stem / mesh forwarding | Linear cost per identity = τ₂ окон sequential SHA-256 (N × τ₂); diversity constraints приумножают cost |
| **DoS attacker** | Flood: connection requests, frames, oversized payloads, invalid signatures, expensive operations; цель — исчерпать compute / memory / disk / bandwidth у жертвы | От одиночного hobbyist (~10 Mbps) до ботнета 100+ Gbps |
| **Censor** | Блокирует трафик к Монтана-узлам в своей юрисдикции через DPI, SNI inspection, IP blocklist, port blocking; цель — denial of access к сети для пользователей региона | National-level firewall (Great Firewall, Roskomnadzor); commercial SaaS DPI |
| **State-level sabotage** | Sustained attack на хранилище / инфраструктуру через legitimate-looking операции; budget ≥ $1M; motivation = harm не profit | Per Gate 14 sabotage actor budget model (родительская роль) |
#### Защищённые свойства
| Свойство | Определение |
|----------|-------------|
| **Confidentiality** | Содержание сообщений недоступно non-recipient (включая intermediate peers в Dandelion / SF / mesh) |
| **Integrity** | Сообщение не модифицировано в пути; modification обнаруживается |
| **Availability** | Узел остаётся reachable для honest peers; consensus state продолжает прогрессировать |
| **Unlinkability** | Внешний наблюдатель не может связать operation X с originating identity / IP узла отправителя |
2026-05-26 21:14:51 +03:00
| **Identifier unlinkability (transport)** | Wire-format Noise_PQ XX соединения не содержит долгоживущего идентификатора в plaintext-части — passive observer не может коррелировать две TCP-сессии одного клиента ни через application restart, ни через смену IP / VPN / сети. Детальный разбор: [`External-Audit/transport-identifier-leakage.md`](External-Audit/transport-identifier-leakage.md). |
#### Coverage matrix
Cell содержит механизм защиты + status. Status: **C** = closed конструкцией; **P** = partial (acknowledged residual); **O** = out of scope (см. ниже).
| | Passive observer | Active MITM | Eclipse | Sybil | DoS | Censor | Sabotage |
|--------------------------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
2026-05-26 21:14:51 +03:00
| **Confidentiality (data)** | Noise_PQ XX + IBT (C) | Noise_PQ XX identity pinning через node_id binding (C) | Noise_PQ XX + IBT survives eclipse (C) | Noise_PQ XX + IBT survives Sybil (C) | n/a | Noise_PQ AEAD прячет content (P — censor видит handshake metadata) | n/a |
| **Confidentiality (metadata)** | Uniform Framing 1024B + ≥20% padding ratio (P — observer видит volume aggregate, не indiv messages) | n/a | n/a | n/a | n/a | DPI видит Noise_PQ XX handshake byte counts; no SNI (Noise_PQ uses libp2p multistream-select) (P) | n/a |
| **Integrity (data)** | n/a | Noise_PQ XX AEAD + IBT signature (C) | Noise_PQ XX + IBT (C) | Noise_PQ XX + IBT (C) | malformed payload reject early через Gate 13a invariants (C) | n/a | malformed input bounded по Storage Cards (C) |
| **Integrity (consensus)** | n/a | подписанные объекты (Transfer/Anchor/Proposal) verifiable по ML-DSA-65 (C) | n/a | n/a | n/a | n/a | n/a |
2026-05-26 21:14:51 +03:00
| **Availability (узла)** | n/a | UPnP/PCP + AutoNAT + circuit relay alternatives (C) | 4-уровневая diversity делает eclipse expensive (C) | Sequential-chain entry barrier τ₂ окон + diversity (C) | rate-limits per peer + per type + total quotas + bootstrap PoW (P — DDoS scale ≥10 Gbps требует sysadmin response помимо protocol) | mesh transport BLE/Wi-Fi Aware survives complete internet block (C — но ограничен range) | per-sender quotas + Storage Cards hard caps + LRU eviction (C) |
| **Availability (consensus)** | n/a | n/a | как «узла» + cementing через ¾ honest weight (C) | как «узла» (C) | как «узла» + consensus path не блокируется network DoS (consensus orthogonal к transport — sequential chain продолжается локально) (C) | mesh propagation cementing eventually (P — задержка часы / дни) | consensus path fails-safe — invalid input не commit-ится (C) |
| **Unlinkability** | Dandelion++ скрывает первый hop отправителя (P — recipient + amount открыты per [I-2]) | Dandelion++ + Tor bridge (deferred M6.5/M14) (P) | n/a | n/a | n/a | Dandelion++ + censorship-resistant discovery + Tor (deferred) (P) | n/a |
2026-05-26 21:14:51 +03:00
| **Identifier unlinkability (transport)** | ephemeral ML-KEM-768 на обеих сторонах + static identity отправляется post-decapsulation, encrypted; нет plaintext-эквивалента MTProto `auth_key_id` (C) | n/a (active MITM видит timing, не identity) | n/a | n/a | n/a | DPI видит протокольную метку `/montana/noise-pq-xx/1.0.0` в multistream-select (network-wide marker, не per-client) (P) | n/a |
2026-05-26 21:14:51 +03:00
**Closed C:** 14 cells.
**Partial P:** 8 cells (acknowledged residuals — см. явный список ниже).
**Out of scope O:** 0 cells (все intersection adversary × property реальны).
#### Out-of-scope (явный список)
Эти угрозы **не закрываются** сетевым слоем Монтаны by design либо по архитектурному решению:
1. **Endpoint compromise** — если private key узла украден, attacker полностью представляет узел в сети; защита — операционная (hardware key storage, OS hardening), не сетевой протокол.
2. **Traffic analysis correlation на global scale** — attacker контролирующий significant fraction всех internet routers (Tier 1 ISPs, государство контролирующее backbone exchange) может коррелировать timing across multiple connections и deanonymize даже через Dandelion++. Защита требует global-scale anonymity network (Tor с миллионами relays), что ortogonal к Монтана.
3. **Side-channel attacks на crypto primitives** — timing / power / EM на ML-DSA-65 / SHA-256 implementations. Mitigation: использовать constant-time implementations (rustls / aws-lc-rs / ring); это [C-6] requirement, не network-protocol concern.
2026-05-26 21:14:51 +03:00
4. **Quantum-capable adversary** — статус: **закрыто**. PQ primitives (ML-DSA-65, ML-KEM-768) защищают application auth и encryption (✅). Production transport — Noise_PQ XX (ML-KEM-768 ephemeral KEM обе стороны + ML-DSA-65 identity sig + ChaCha20-Poly1305 AEAD); classical TLS 1.3 + Noise XK chain удалён из libp2p stack. Per [I-1] в protocol layer classical crypto запрещена; этот invariant теперь honored end-to-end.
5. **Supply chain attacks на dependencies** — compromised library обновление (libp2p, rustls). Mitigation: Cargo.lock pinning + reproducible builds + dependency audit ([C-6] req #3-4-5-6); это build-time concern.
6. **Physical proximity attacks на mesh** — BLE / Wi-Fi Aware require physical proximity (≤200m). Attacker с physical access может flood mesh advertisements. Mitigation: per-sender rate-limit + IBT mesh proof requirement делает это nonproductive, но не невозможным.
7. **Legal/regulatory pressure на bootstrap nodes** — 12 hardcoded genesis bootstrap могут быть seized / forced offline государством. Mitigation: после первого подключения узел работает через discovered peers, bootstrap нужны только при cold start или при потере всех peers; censorship-resistant discovery (deferred M6.5) — secondary path.
#### Acknowledged residuals (P cells expanded)
| # | Cell | Residual | Severity | Mitigation roadmap |
|---|------|----------|----------|--------------------|
2026-05-26 21:14:51 +03:00
| 1 | Censor → Confidentiality (data) | DPI видит Noise_PQ XX handshake byte counts (msg1=1184B / msg2=7533B / msg3=6349B distinct sizes) | Medium | Optional handshake padding to a uniform target; deferred to post-mainnet hardening |
| 2 | Passive observer → Confidentiality (metadata) | Uniform Framing скрывает per-message volume, но aggregate KB/sec viewable | Low | Defense-in-depth достаточен; full traffic shaping out of scope |
| 3 | DoS → Availability (узла) | DDoS ≥10 Gbps требует sysadmin response | Medium | Standard practice (Cloudflare-like upstream filtering); not protocol concern |
| 4 | Censor → Availability (узла) | Mesh range ≤200m недостаточен для intercity связи без internet | Medium | Defer multi-hop mesh routing к M14 mobile work |
| 5 | Censor → Availability (consensus) | Mesh propagation timing часы/дни добавляет latency | Acceptable | Eventually consistency через mesh — protocol design choice |
| 6 | Passive observer → Unlinkability | Recipient + amount открыты after fluff per [I-2] | By design | [I-2] открытость финансового слоя — non-negotiable per глобальный invariant |
| 7 | Censor → Unlinkability | Без Tor bridge deanonymization возможна на censor-controlled exit | Medium | Deferred M6.5 / M14 (censorship-resistant discovery) |
#### Compliance с глобальными инвариантами
- **[I-1] PQ-secure** — Threat model признаёт residual (Q4 outscope) пока TLS не PQ; closed для auth (IBT через ML-DSA-65) и для confidentiality в SF (ML-KEM-768 E2E). На момент M6 acceptable как PQ-protected identity layer над classical TLS transport.
- **[I-3] Determinism** — Threat model orthogonal: defines what is defended, не какие state changes возникают.
2026-05-26 21:14:51 +03:00
- **[I-7] Minimal crypto surface** — Threat model не вводит новых crypto primitives; переиспользует ML-DSA-65 / SHA-256 / ML-KEM-768 / ChaCha20-Poly1305.
#### Audit guidance
Внешний security аудитор использует matrix как entry point: для каждой cell с status **C** — verify через указанный механизм (раздел спеки + binding KAT vector + Phase A test); для **P** — verify acknowledged residual явно зафиксирован; для **O** — verify out-of-scope явно перечислен в списке выше.
#### Сетевые параметры в protocol_params
В рамках закрытия раздела II.5 плана сетевого слоя M6 в `protocol_params` Указа Генезиса добавлены пять новых полей. Authoritative layout и инварианты — в Указе Генезиса (раздел «Genesis Decree» выше). Этот sub-section содержит **derivation** per «Академическое обоснование констант» + нормативные backpressure rules. Bump Genesis State Hash — pre-mainnet acceptable.
**Derivation per «Академическое обоснование констант»:**
```
Константа: bootstrap_pow_difficulty (authoritative SSOT в Указе Генезиса per [I-10])
Значение: 65 536 (2^16)
Класс: Performance / Security (anti-flood)
Target: ≈100 мс CPU per попытка на genesis-железе (5.097 MH/s); attacker
с rate 10 connections/сек тратит 1 CPU-сек/сек = одно ядро
постоянно занято на каждую bootstrap; 12 bootstrap × 1 ядро =
12 ядер для distributed flood (manageable through standard
upstream rate-limiting)
References: Hashcash [Back 2002]; Bitcoin PoW genesis [Nakamoto 2008];
ProgPoW analysis для anti-ASIC bias (не применяется здесь —
SHA-256 specifically, но class precedent)
Derivation: attempts_per_second_genesis = 5.097 × 10^6 / (2^256 / target)
= 5.097 × 10^6 / 2^240 (для difficulty 2^16, target = 2^240)
= ≈10 attempts/sec (1 успех каждые 100 мс)
Sensitivity: ±10% от target → ±10% CPU cost; ×2 difficulty (2^17) → 200 мс
per attempt → ×2 attacker cost; ×0.5 difficulty (2^15) → 50 мс
×0.5 attacker cost (acceptable lower bound — bootstrap не
высокоценная цель flood)
Defense: Q: «почему не выше? повысить ×10 для лучшей защиты»
A: bootstrap нужен только при cold start; legitimate user
ждёт 100 мс при первом подключении приемлемо, 1 секунда
фрустрирует
Q: «почему 2^16 а не 2^14?»
A: 2^14 (25 мс target) attacker cost ниже на ×4 — distributed
flood requires ×4 больше IPs для same impact; 2^16 = sweet
spot между UX и flood resistance
Default (operator-configurable, не Genesis Decree): max_outbound_per_node
Значение: 24
Класс: Operational / Security (eclipse resistance)
Target: Eclipse требует attacker control ≥ ¾ outbound = ≥ 18 of 24;
diversity constraints (4-уровневая) делают это economically
expensive: requires ≥ 18 different ASN AND ≥ 18 different
/16 prefix AND ≥ 18 different start_window cohorts
References: Bitcoin outbound = 8 [Nakamoto core]; Ethereum geth = 16
[Ethereum Yellow Paper]; libp2p kademlia bucket size = 20
[Maymounkov-Mazières 2002]
Derivation: Compromise budget anti-eclipse: P(eclipse) ≈ (f^N) при f =
attacker fraction of network nodes; для f = 0.3 и N = 24,
P ≈ 3 × 10^-13 (negligible); для f = 0.5 и N = 24,
P ≈ 6 × 10^-8 (acceptable для рare event); diversity
constraints further reduce
Sensitivity: N = 16 → P(f=0.5) ≈ 1.5 × 10^-5 (acceptable degradation
но не optimal); N = 32 → P(f=0.5) ≈ 2 × 10^-10 (better
но удваивает bandwidth cost: 26 KB/сек vs 13 KB/сек —
приемлемо для homeServer нет)
Defense: Q: «почему 24 а не 16 (как Ethereum)?»
A: Ethereum gas economics differ; Montana baseline 1 fps —
bandwidth не constrained; добавление 8 outbound — minor
cost для significant eclipse resistance
Q: «почему не 32?»
A: marginal benefit P(eclipse) reduction × ×2 bandwidth
cost — UX boundary
Default (operator-configurable, не Genesis Decree): max_inbound_per_node
Значение: 13
Класс: Operational
Target: 13 inbound × 1 fps × 1024 B = 13 KB/сек ≈ 33 GB/мес
приемлемо для домашнего сервера (limit ОТТ-cap у российских
home ISP ≈ 100 GB/мес unmetered)
References: Spec строка 4207 derivation
Derivation: bandwidth_per_inbound = 1024 B/сек × 86400 сек/день × 30 дней
≈ 2.5 GB/мес per inbound; 13 inbound × 2.5 GB = 32.5 GB/мес
Sensitivity: 8 inbound → 20 GB/мес; 16 inbound → 40 GB/мес — обе boundaries
acceptable, 24 = по derivation Protocol [P(eclipse) < 2]; равно max_outbound
distribution когда 100% узлов имеют equal in/out balance
Defense: Q: «почему inbound меньше outbound?»
A: outbound определяет eclipse resistance (own perspective);
inbound — quota на сколько ressources даёшь чужим
подключениям. Asymmetry deliberate: каждый узел actively
выбирает 24 outbound for security, passively принимает
13 inbound
Q: «почему не равно 24 = 24?»
A: bandwidth budget ограничен; 13 inbound покрывает
reciprocity без overload
Default (operator-configurable, не Genesis Decree): max_pending_requests_per_peer
Значение: 256
Класс: Operational (backpressure)
Target: Ограничивает correlation table memory: 256 × peer_count × 32 B =
≈ 13 KB per peer × 24 outbound = 312 KB total per node
(negligible memory)
References: HTTP/2 SETTINGS_MAX_CONCURRENT_STREAMS default 100 [RFC 7540];
gRPC default max_concurrent_streams 100 [grpc.io]
Derivation: request_timeout = τ₁ / 2 ≈ 30 сек на genesis hardware;
rate at full capacity: 256 requests / 30 сек ≈ 8.5 req/сек
per peer; 24 outbound × 8.5 = ≈200 req/сек total — sufficient
для FastSync chunked + PeerList exchange + BatchLookup
concurrent
Sensitivity: 128 → ≈4 req/сек per peer (constrains FastSync chunked);
512 → ≈17 req/сек (more headroom но 2× memory; trade-off
в пользу 256)
Defense: Q: «почему 256 а не 1024?»
A: 1024 — overhead на correlation tracking без proportional
benefit для use cases (FastSync дробится автоматически
на chunks)
Default (operator-configurable, не Genesis Decree): request_timeout_t1_div
Значение: 2
Класс: Operational
Target: Request TTL = τ₁ / 2 — позволяет получить response в текущем
τ₁ окне в типичном случае (один RTT через global internet
≪ τ₁/2)
References: Spec строка 4794 (TCP connect: τ₁/2)
Derivation: τ₁ ≈ 60 сек на genesis hardware; τ₁/2 = 30 сек — far above
reasonable global RTT (≤ 1 сек); margin для retransmits
и slow paths
Sensitivity: divisor = 1 (TTL = τ₁) → больше pending requests
накапливаются (memory pressure); divisor = 4 (TTL = τ₁/4)
→ false timeouts на slow links
Defense: Q: «почему не absolute value (например 30 сек)?»
A: τ₁ adaptable через `participation_ratio` feedback
(раздел «Адаптация D»); если D растёт → τ₁ растёт →
request timeout пропорционально растёт; absolute value
desync с network capacity при slow hardware participation
```
#### Backpressure rules — нормативный текст
Применяется ко всем IBT-сессиям + ProtocolMessage envelope layer.
**Правило B1 — Pending requests cap.**
Для каждого peer count активных pending requests (status awaiting response) ≤ `max_pending_requests_per_peer`. При попытке создать новый request выше cap — caller получает `Error::Backpressure`, request не отправляется. После expiry (по `request_timeout_t1_div`) либо response counter уменьшается.
**Правило B2 — Frame intake rate per peer.**
Для каждого peer rate входящих frames ≤ baseline_frame_rate × max_burst_factor где `baseline_frame_rate = 1 fps` (см. Uniform Framing) и `max_burst_factor = 8` (max_burst per spec строка 4204). При превышении: drop excess frames silently (не disconnect — разрешает burst recovery), increment peer-level penalty counter.
**Правило B3 — Total connection cap.**
Узел поддерживает максимум `max_outbound_per_node` исходящих + `max_inbound_per_node` входящих TCP-соединений одновременно. Превышение outbound — caller получает `Error::OutboundCapReached`, попытка отклоняется до rotation освободит slot. Превышение inbound — incoming TCP connection отклоняется на TCP уровне (RST), счётчик не обновляется (защита от counter inflation).
**Правило B4 — Penalty escalation.**
Peer-level penalty counter увеличивается при: (a) frame intake rate exceedance (B2); (b) malformed payload after Gate 13a structural validation; (c) signature verification failure при IBT либо при подписанных payload-объектах. При достижении threshold = 100 events per τ₁ — peer disconnect с reason 0x04 (protocol violation) + blacklist на 24·τ₁ (см. spec «Retry policy»). Penalty counter сбрасывается на каждой τ₁ boundary либо при successful disconnect.
**Правило B5 — Resource quota enforcement timing.**
Все backpressure checks выполняются **до** crypto-verification (ML-DSA-65 verify ≈0.1 мс на commodity CPU; protection against amplification atak). Order:
1. TCP-level rate (OS tcpdump rules — outside scope spec)
2. TLS-level (rustls / aws-lc-rs — library scope)
3. IBT proof verify (ML-DSA-65 — внутри карточки IBT)
4. Frame structure parse (Gate 13a Uniform Framing invariants)
5. Frame intake rate per B2 — **перед** ProtocolMessage decode
6. ProtocolMessage envelope parse (Gate 13a)
7. Pending request cap per B1 — для request types
8. Payload structure validation (Gate 13a per type)
9. Payload signature verify (если payload подписан — Transfer / NodeRegistration / etc.)
10. Apply transition (apply_proposal либо locally — outside backpressure scope)
**Правило B6 — Sabotage actor protection.**
Sabotage actor с budget $1M (per Threat Model) с реалистичным ресурсом ≈10 Gbps можно flood узла; standard upstream filtering (Cloudflare-like, ISP DDoS protection, hosting provider mitigation) — out of scope spec, expected operator practice. Spec backpressure rules защищают только от **per-peer behaviour**, не от volumetric DDoS на network уровне.
#### apply_mesh_frame и apply_store_and_forward — нормативные формулировки
Сетевые apply-функции выполняются на receive path локального узла. В отличие от consensus apply-функций (`apply_proposal`, `apply_emission`, etc.) **не входят в state_root** ([I-3] orthogonal — locally derived state). Однако их детерминизм важен: повторное применение того же frame должно давать тот же результат (idempotency для replay-handling).
##### apply_mesh_frame(frame, sender_pubkey, peer_local_state) → MeshIntake
```
Input:
frame MeshFrame (см. spec «MeshFrame wire format»)
sender_pubkey ML-DSA-65 pubkey 1952B (полученный из mesh advertisement)
peer_local_state &mut LocalMeshState {
used_nonces: Map<sender_pubkey_hash, Set<nonce>>,
fragments: Map<(sender_pubkey_hash, msg_id), Vec<Fragment>>,
rate_window: Map<sender_pubkey_hash, FrameWindow>,
}
Output:
MeshIntake = enum {
Accepted, — frame validated, persisted либо forwarded
AcceptedComplete(payload), — последний fragment, message complete
Rejected(reason), — silent reject (no error to sender)
}
Steps (ordered, deterministic):
1. Structure validation per Gate 13a invariants для MeshFrame:
- flags ∈ {0x00, 0x02, 0x04, 0x06} (data | padding | continuation
| data+continuation; padding+continuation запрещено)
- fragment_index ≤ total_fragments
- total_fragments ≤ 255
- recipient_hint exactly 32 B
- payload size ≤ MTU (BLE 244 B либо Wi-Fi Aware 1500 B —
определяется transport profile, сигнализируется адверsement)
Не пройдено → return Rejected(InvalidStructure)
2. IBT mesh proof verification per карточка «IBT mesh proof»:
- cached_window ∈ [known_W 7·τ₁, known_W]
- mesh_session_nonce ∉ used_nonces[hash(sender_pubkey)]
- signature valid под sender_pubkey
Не пройдено → return Rejected(InvalidIBT)
3. Frame intake rate per Backpressure Rule B2:
- rate_window.intake_count++
- intake_count ≤ baseline_frame_rate × max_burst_factor (1 fps × 8 = 8)
within latest 1 сек window
Превышено → drop frame, increment penalty (Rule B4), return Rejected(RateLimit)
4. Recipient determination:
- recipient_hint == 0xFF × 32 → broadcast — adressed to all mesh peers
- recipient_hint == hash(local_node_pubkey) → addressed to self
- иначе → addressed to other peer, candidate for forwarding
(см. apply_store_and_forward для forwarding semantics)
5. Fragment assembly (для multi-fragment messages):
msg_id = (sender_pubkey_hash, fragment header — derivable from envelope)
fragments[msg_id].insert(fragment_index, frame.payload)
Если fragments[msg_id].len() == frame.total_fragments:
payload_complete = concat(sorted by fragment_index)
fragments.remove(msg_id)
return AcceptedComplete(payload_complete)
иначе:
return Accepted
6. Update used_nonces (после полного accept):
used_nonces[hash(sender_pubkey)].insert(mesh_session_nonce)
Идемпотентность:
apply_mesh_frame(F, S, state) дважды:
Первый вызов: Accepted либо AcceptedComplete(P), state' с frame
зарегистрированным
Второй вызов: Rejected(InvalidIBT) — nonce уже в used_nonces, dedup
Это и есть проявление determinism: после первого accept повторный
frame с тем же nonce silent-rejected, защита от replay.
```
##### apply_store_and_forward(envelope, sender_pubkey, sf_local_state) → SFIntake
```
Input:
envelope SFEnvelope (см. spec «Buffer model»)
sender_pubkey ML-DSA-65 pubkey (из envelope.sender_signature)
sf_local_state &mut LocalSFState {
buffer: Map<recipient_hint, BufferEntry>,
sender_quotas: Map<sender_pubkey_hash, SenderQuotaState>,
recipient_acks: Map<recipient_hint, SignedAck>,
total_bytes: u64,
}
Output:
SFIntake = enum {
Buffered, — frame stored для forwarding
DeliveredLocally(decrypted_msg), — recipient = self, ML-KEM decrypt OK
Rejected(reason), — silent reject
}
Steps (ordered, deterministic):
1. Structure validation per Gate 13a invariants для SFEnvelope:
- recipient_hint exactly 32 B
- ttl_window ∈ [current_W + 1, current_W + 24·τ₁]
(TTL hard cap 24·τ₁; ttl_window ≤ current_W → expired, reject;
ttl_window > current_W + 24·τ₁ → invalid, reject)
- fragment_index ≤ total_fragments ≤ 255
- sender_signature exactly 3309 B
- ciphertext size ≤ 4096 B (per envelope upper bound)
Не пройдено → return Rejected(InvalidStructure)
2. Sender signature verification:
verify_message = canonical_encode(envelope_without_signature_bytes)
ML-DSA-65 verify(sender_pubkey, verify_message, sender_signature)
Не пройдено → increment penalty (Rule B4), return Rejected(InvalidSignature)
3. Per-sender quota per spec «Per-sender quota»:
sender_quota = sender_quotas[hash(sender_pubkey)]
sender_quota.frames_in_window_τ₁++
Если frames_in_window_τ₁ > 256:
return Rejected(SenderQuotaExceeded) — silent (sender определит
по retry timeout)
Sender_quota автоматически reset на каждой τ₁ boundary.
4. Recipient ack check per spec «Signed rate-limit acks»:
Если recipient_acks[recipient_hint].is_revoked(sender_pubkey, current_W):
return Rejected(ForwardingRevoked)
5. Total buffer quota check per Storage Card sf_buffer:
prospective_total = total_bytes + envelope_size
Если prospective_total > 1 GB AND sender NOT in operator VIP whitelist:
return Rejected(TotalQuotaExceeded)
Если prospective_total > 1 GB AND sender в VIP whitelist:
eviction: drop oldest non-VIP entry until prospective_total ≤ 1 GB
6. Recipient determination:
Если recipient_hint == hash(local_node_pubkey):
attempt ML-KEM decrypt(local_node_secretkey, envelope.ciphertext)
Если success:
remove envelope from buffer (delivered)
return DeliveredLocally(decrypted_msg)
Если failure:
return Rejected(DecryptFailure) — corrupted либо unintended
Иначе:
buffer.insert(recipient_hint, envelope)
total_bytes += envelope_size
return Buffered
7. Forwarding policy per spec «Forwarding algorithm»:
После Buffered — узел periodically attempts forwarding:
- выбирает peer-кандидатов с recipient_hint match (либо broadcast peers)
- отправляет envelope как ProtocolMessage (тип reserved для SF —
используется extension namespace, см. план дальше при имплементации)
- при successful delivery либо TTL expiry → remove from buffer
total_bytes -= envelope_size
Идемпотентность:
apply_store_and_forward(E, S, state) дважды с identical envelope:
Первый вызов: Buffered либо DeliveredLocally
Второй вызов: Per-sender quota инкрементируется снова → может вернуть
SenderQuotaExceeded; либо
Если буфер уже содержит envelope с тем же
(recipient_hint, sender_pubkey, ciphertext) — то
`buffer.insert` is replace, total_bytes без изменений,
return Buffered (idempotent на содержимое buffer)
Recipient ack revocation, sender quota exhaustion, либо TTL expiration
делают повторный apply non-idempotent в смысле response, но idempotent
в смысле buffer state (либо present либо expired/removed).
```
##### Локальные функции вне consensus state — почему apply_*
Имена `apply_mesh_frame` и `apply_store_and_forward` симметричны consensus apply (`apply_proposal`, `apply_emission`) намеренно: **семантически одинаковая роль** — взять input + state → produce new state. Различие — в scope:
- consensus apply: state ∈ consensus root (state_root computation)
- network apply: state ∈ local node-runtime (вне state_root, не consensus-critical)
Это гарантирует:
1. **Implementation pattern consistency** — реализатор (Phase G) знает что mesh / SF имеют ту же семантику что consensus apply: deterministic, ordered, idempotent (где specified).
2. **Cross-impl conformance** — две независимые реализации, прогнавшие один и тот же frame через `apply_mesh_frame`, получают same MeshIntake outcome. Это conformance condition (хотя не consensus-critical, важно для interop testing).
3. **[C-7] enforcement** — code-side carded role предписывает no-shortcut на `apply_*` функциях; mesh / SF теперь явно входят в этот класс, никакого shortcut доступа к `used_nonces` / `buffer` / `sender_quotas` напрямую (только через apply).
#### Final Gate audit сетевого слоя M6
Финальный аудит закрытия плана раздела II — закрытие плана сетевого слоя M6
(II.1 карточки + II.2 Storage Cards + II.3 KAT vectors + II.4 Threat Model
+ II.5 backpressure + II.6 apply_*).
**Gate 0.5(d.1) Formula name coverage:** new section не вводит новых
formula names; все references существующих формул через explicit pointers
на authoritative разделы. Pass.
**Gate 0.5(d.2) Field name coverage** для пяти новых полей `protocol_params`:
- `bootstrap_pow_difficulty` — authoritative в Указе Генезиса (4 B u32 = 65 536);
references в карточке Bootstrap PoW (open sub-finding now closed), KAT vector
B3, derivation в sub-section «Сетевые параметры в protocol_params». Все
references explicit на Genesis Decree authoritative location. Pass.
- `max_outbound_per_node` — authoritative в Указе Генезиса (1 B u8 = 24);
references в Backpressure Rule B3 + derivation. Pass.
- `max_inbound_per_node` — authoritative в Указе Генезиса (1 B u8 = 13);
references в Backpressure Rule B3 + derivation. Pass.
- `max_pending_requests_per_peer` — authoritative в Указе Генезиса (2 B u16 = 256);
references в Backpressure Rule B1 + derivation. Pass.
- `request_timeout_t1_div` — authoritative в Указе Генезиса (1 B u8 = 2);
references в Backpressure Rule B1 + derivation. Pass.
Zero value duplications вне authoritative location. [I-10] SSOT compliance verified.
**Gate 15 Part 1B Generic version sweep:**
Spec body grep `\bv?[0-9]+\.[0-9]+\.[0-9]+\b` returns only legitimate hits:
- Header line 3 — single Montana spec version (authoritative)
- QEMU Virtual CPU v4.2.0 / v8.2.0 (illustrative external hardware variance
table — pre-existing, external software identifier, not Montana version)
- `ip_version=0x04` field в KAT vectors (regex false positive — `04` parsed
as `4.0` patch number; ip_version IS a field, not version mention)
- Rust 1.92.0, sha2 crate v0.10.9, macOS Sequoia 15.7.3, Darwin 24.6.0
(pre-existing external software identifiers in [I-18] D₀ derivation context)
Zero spec-version mentions in body. [I-10] SSOT compliance verified.
**Cross-section consistency (active comparison):** для всех новых терминов
(IBT online/mesh, Bootstrap PoW, Uniform Framing, Transport Randomness, Peer
selection, Dandelion++, NAT Traversal, Mesh Transport, Store-and-Forward,
ProtocolMessage envelope, message type registry, Backpressure rules) терминология
синхронизирована между основной частью спеки (раздел «Сетевой уровень») и
sub-разделами «Карточки замыкания механизмов сетевого слоя». Reading-based
verify: каждое упоминание в карточке references existing спеку section
без cross-spec terminology drift. Pass.
**Gate 13a Invariant enumeration completeness:**
- Каждая из 12 карточек имеет полные 11 пунктов замыкания + 15-pass global
invariant check. Pass.
- Все 4 локальных Storage Cards имеют 11 полей + [I-14] путь + sabotage budget.
Pass.
- 50 KAT vector definitions — каждый с input scheme + expected output
placeholder (TBD-A в Phase A reference impl). Pass.
- Threat Model — 7 adversary classes × 4 properties matrix с явными status
C/P/O markers. Pass.
- Genesis Decree extension инварианты enumerated explicit (5 новых полей +
byte-exact values + runtime mutation запрещена). Pass.
- apply_mesh_frame и apply_store_and_forward — explicit ordered steps +
идемпотентность guarantees. Pass.
**Gate 0 Global invariant check для каждого нового механизма:**
Все 12 карточек прошли 15-pass invariant check ([I-1]..[I-15]) explicit:
- [I-1] PQ-secure: yes для всех auth (ML-DSA-65 IBT)
- [I-2] Public financial layer: explicit acknowledgment в Dandelion++ карточке
(скрывает только первый hop)
- [I-3] Determinism: transport-orthogonal в карточках; deterministic в
apply_mesh_frame / apply_store_and_forward
- [I-4] TimeChain independence: yes в IBT mesh + Mesh Transport (cached_window
работает offline)
- [I-5] Commodity hardware: yes для всех (BLE/Wi-Fi Aware на любом устройстве,
ML-DSA-65 verify дёшев)
- [I-6] Regulatory compat: yes для всех; explicit acknowledgment в
Dandelion++ что не privacy mixer
- [I-7] Minimal crypto surface: yes — переиспользует ML-DSA-65 / SHA-256 /
2026-05-26 21:14:51 +03:00
ML-KEM-768 / ChaCha20-Poly1305
- [I-8] Network-bound unpredictability: n/a для всех (transport-orthogonal,
не consensus seed)
- [I-9] Bit-exact deterministic: yes для wire format (binding KAT vectors
В Phase A); partial для transport scheduling (non-deterministic by design)
- [I-10] Single Source of Truth: yes для всех (formal layout + invariants
в одном месте, references в другом)
- [I-11..I-13]: n/a (не nickname / auction / monetary)
- [I-14] State lifecycle: yes для всех persistent — locales Storage Cards
fixate hard quotas + temporal pruning
- [I-15] Time-based scarcity: yes для всех anti-spam mechanisms (rate-limits
через time, не money)
Zero global invariant violations. Network layer Gate 0 compliance: Pass.
**Минимально необходимое для разблокирования Phase A кода:** все 7 пунктов
плана раздела II закрыты. Implementer mt-net::* крейтов имеет:
- 12 нормативных карточек закрытия
- 4 Storage Cards для локальных сетевых таблиц
- 50 binding KAT vectors (input schemas + algorithm)
- Threat Model coverage matrix
- 5 Genesis Decree fields (bootstrap_pow_difficulty + connection limits + timeout)
- 6 backpressure rules B1-B6
- 2 apply_* функции (mesh + SF)
Phase A разблокирована. Reference implementation начинается.
**Status:** План раздела II — **закрыт**. Network layer спека audit-ready
для Phase A start.
---
---
# Сетевой слой со стороны клиента (App-perspective)
Следующие разделы исторически жили в Montana App spec разделах 10-11. Они описывают взаимодействие клиента с сетевым слоем — режимы узла, libp2p настройка с client-side, выбор хостящего узла, mesh integration на iOS/Android.
## 10. Режимы узла
### 10.1 Лёгкий клиент (по умолчанию на мобильном)
Большинство мобильных пользователей — лёгкие клиенты. Приложение не участвует в консенсусе, только использует сеть.
**Что делает лёгкий клиент:**
- Подключается к нескольким полным узлам через libp2p
- Подписывается на потоки proposals (получает новые proposals)
- Валидирует proposals локально (подписи, совпадение `state_root`)
- Поддерживает локальную копию Таблицы аккаунтов для своего аккаунта и контактов (не всю)
- Отправляет операции в сеть через gossip
- Запрашивает данные Content Layer по необходимости
- Верифицирует получаемые данные через хэши
**Чего лёгкий клиент НЕ делает:**
2026-05-26 21:14:51 +03:00
- Не запускает sequential-chain TimeChain
- Не запускает sequential-chain NodeChain
- Не участвует в лотерее
- Не публикует proposals
- Не хранит полную Таблицу аккаунтов
- Не хранит полную историю proposals
**Ресурсы лёгкого клиента:**
- CPU: минимальный (валидация подписей, криптооперации при отправке и получении)
- Сеть: умеренная (потоки proposals, запросы контента)
- Хранилище: несколько MB для существенного состояния, GB для кэша и подписок
- Батарея: оптимизирован для мобильного (фоновая синхронизация с ограничением темпа)
### 10.2 Полный узел на десктопе
Десктоп-версия Montana App может работать как полный узел.
**Включение режима узла:**
1. «Настройки → Дополнительно → Работать как полный узел»
2. Предупреждение о требованиях (минимум 1 ядро, аптайм 24/7, железо)
3. Пользователь подтверждает
4. Приложение запускает дополнительные потоки:
2026-05-26 21:14:51 +03:00
- Поток sequential-chain TimeChain (1 выделенное ядро)
- NodeChain
- Поток валидатора (валидация операций и финализация)
5. Приложение загружает полное состояние (Таблица аккаунтов, Таблица узлов, история proposals)
6. Если у пользователя есть `NodeRegistration` — начинает участвовать в лотерее
**Требования для полного узла:**
- 1 или более ядер CPU
- 16 или более GB RAM
- 500 или более GB диска (растёт со временем)
- Аптайм 24/7 (или близко)
- Стабильное интернет-соединение
- Пропускная способность: минимум 1 Mbps, рекомендуется 10 Mbps и больше
**Участие в сети:**
- Узел получает `chain_length` за каждое окно активности
- При достаточной `chain_length` становится подтверждающим
- Публикует `BundledConfirmation`
- Может участвовать в лотерее
- Зарабатывает Монтана при выигрыше
- Монтана зачисляется в `operator_account` (тот же аккаунт пользователя)
### 10.3 Процесс регистрации узла
Десктоп-пользователь хочет стать узлом:
1. Пользователь запрашивает приглашение от существующего узла (вне сети)
2. Приглашающий узел формирует `NodeInvitation` с публичным ключом приглашённого
3. `NodeInvitation` публикуется и финализируется в сети
4. Пользователь получает уведомление «Вас пригласили стать узлом»
5. Пользователь подтверждает
2026-05-26 21:14:51 +03:00
6. Приложение запускает sequential-chain процесс длиной `vdf_entry_windows = 20 160 окон` (около 14 дней) в фоне
7. После завершения формируется `NodeRegistration` с `proof_endpoint`
8. Пользователь публикует `NodeRegistration` (`operator_account_id = свой account_id`)
9. После финализации — пользователь становится узлом Montana
2026-05-26 21:14:51 +03:00
Sequential-chain процесс — блокирующий. Приложение должно работать непрерывно или продолжать chain extension при каждом запуске. На мобильном это практически невозможно; на десктопе возможно, но требует 24/7 аптайма в течение двух недель.
---
## 11. Сетевой слой
### 11.1 Настройка libp2p
Montana App использует `rust-libp2p` для P2P сетевого слоя.
**Транспортные протоколы:**
- QUIC (основной для мобильного) — поверх UDP, работает через NAT
- TCP (запасной) — для контекстов где QUIC заблокирован
- WebSocket (для веба если появится)
**Мультиплексирование потоков:**
- yamux (стандарт libp2p)
**Безопасность транспорта:**
- Фреймворк Noise для шифрования транспорта
- Используется Noise_XX с ML-KEM-768 (постквантовая адаптация)
- Это шифрование уровня транспорта; шифрование уровня сообщений — отдельное через Double Ratchet
### 11.2 Bootstrap-узлы
При первом запуске приложению нужно найти сеть.
**Механизмы первичного подключения:**
1. **Хардкодированные bootstrap-узлы** — 12 genesis-узлов, зафиксированы в Genesis Decree. Приложение хардкодит их адреса и `account_id`.
2. **Обнаружение через DNS** — записи SRV `_montana._tcp.montana.io` указывают на известные bootstrap-узлы. Приложение делает запрос DNS при старте.
3. **Обмен пирами** — после подключения к одному bootstrap-узлу приложение запрашивает у него список известных пиров и расширяет свой список.
4. **Обнаружение устойчивое к цензуре** — описано в спеке протокола (Transport Obfuscation, ECH и так далее). Для регионов с блокировкой.
### 11.3 Использование Content Request Protocol
Приложение активно использует `ContentRequest` и `ChunkRequest` для всех операций Content Layer.
**Процесс получения blob:**
1. Приложение вычисляет пару `(app_id, data_hash)` нужного blob
2. Приложение проверяет локальный кэш
3. Если нет — `ContentRequest(app_id, data_hash)` одному из подключённых пиров
4. Пир возвращает манифест (если это манифест) или одиночный blob
5. Приложение верифицирует хэш
6. Если это манифест и нужны чанки — последовательные `ChunkRequest(data_hash, chunk_index)`
7. Собранный blob сохраняется в кэше
**Параллельность:**
- Чанки запрашиваются параллельно у нескольких пиров для скорости
- Неудачные запросы переадресуются другим пирам
- Ограничение темпа для предотвращения перегрузки пиров
### 11.4 Участие в DHT
Приложение участвует в Kademlia DHT libp2p.
**Участие лёгкого клиента:**
- Приложение может публиковать свои записи провайдера в DHT (для своих blob)
- Приложение может делать поиск провайдеров в DHT для нужного контента
- Мобильные лёгкие клиенты могут иметь ограниченное участие в DHT (экономия батареи и сети)
**Полный клиент на десктопе:**
- Полное участие в DHT
- Поддержка таблицы маршрутов
- Помощь другим клиентам через реле
### 11.5 Выбор хостящего узла и отказоустойчивость
Применимо к пути участия через аккаунт (пользователь без своего узла, подключается к узлу-хосту через IBT уровень 3, см. спеку протокола раздел «Два пути участия»).
**Проблема.** Клиент с путём участия только через аккаунт зависит от наличия работающего узла-хоста. Если хост уходит (создатель приложения закрылся, узел офлайн, юрисдикционная блокировка, систематический отказ в gossip) — пользователь должен переключиться на другой узел, иначе история AccountChain и ключи становятся бесполезны без сети для подключения. Наивное решение «выбрать узел с самым длинным `chain_length` и держаться за него» создаёт четыре уязвимости: концентрация на топ-N узлов воссоздаёт централизованный хостинг, заранее построенные sybil-узлы могут попадать в топ за месяцы до атаки, eclipse через искажённый bootstrap делает «самый длинный в видимости» равным «самый длинный под управлением атакующего», постоянное прикрепление к одному хосту даёт ему полный социальный граф клиента. Раздел 11.5 закрывает эти угрозы процедурно.
#### 11.5.1 Три стратегии выбора
Клиент выбирает стратегию при настройке, переключается в любой момент через настройки. Спецификация не предписывает стратегию по умолчанию — приложение рекомендует «Авто» для нетехнических пользователей, «Закреплённый» для технически грамотных операторов с собственными узлами или доверенными хостами.
**Стратегия A — Авто.** Клиент автоматически выбирает узлы-хосты по политике с несколькими критериями (см. 11.5.2). Взаимодействие без необходимости разбираться в выборе узлов. Компромисс: пользователь делегирует решение алгоритму клиента.
**Стратегия B — Закреплённый.** Пользователь явно указывает допустимые узлы — собственный узел, узлы доверенных контактов, узлы из community-реестра публичной утилиты (см. 11.5.5). Полный контроль, никакого автоматического выбора. Компромисс: требует от пользователя поддержания актуального белого списка при изменениях в сети.
**Стратегия C — Гибрид.** Авто с ограничениями — белый список (всегда предпочитать эти узлы пока они проходят критерии), чёрный список (никогда не использовать), юрисдикционные фильтры. Компромисс: средняя сложность настройки, средний контроль.
Стратегия формализована в локальной конфигурации:
```
HostSelectionConfig {
strategy enum (Auto | Pinned | Hybrid)
pinned_set []NodeID (для Pinned, Hybrid)
blacklist []NodeID (для Auto, Hybrid)
jurisdiction_filter []CountryCode (опционально, исключаемые юрисдикции)
parallel_connections u8 (1..16, по умолчанию 5)
rotation_period_tau2 u32 (окон τ₂ между ротациями, по умолчанию 1)
require_advisory bool (по умолчанию false; если true — узел должен быть в community-реестре)
}
```
#### 11.5.2 Политика с несколькими критериями (для стратегий «Авто» и «Гибрид»)
Узел попадает в допустимое множество только если выполнены ВСЕ критерии одновременно:
| Критерий | Минимум по умолчанию | Защищает от |
|---|---|---|
| `chain_length ≥ min_chain_length` | 2 × τ₂ (≈ 40 320 окон, ≈ 28 дней непрерывной работы) | неработающих узлов, недавно созданных sybil-узлов |
| `node_age ≥ min_node_age` | 6 × τ₂ (≈ 84 дня от первого сцементированного `BundledConfirmation`) | заранее построенных sybil-узлов специально подготовленных к атаке за короткий период |
| `latency_p95 ≤ max_acceptable_ms` | 2000 мс | мёртвых или недоступных узлов |
| `not_in_blacklist` | — | известных плохих акторов из локального и community-чёрного списка |
| `not_in_jurisdiction_filter` | — | пользовательских предпочтений по юрисдикции |
| `success_rate_last_τ₂ ≥ threshold` | 0.95 | узлов отказывающих в gossip операций конкретного клиента |
Все критерии настраиваемы пользователем; значения по умолчанию безопасны для типичного неагрессивного окружения. Допустимое множество пересчитывается клиентом локально из публично наблюдаемого состояния NodeChain — не требует доверия к третьей стороне. Пересчёт инкрементальный: новое сцементированное `BundledConfirmation` или истёкшая проба задержки → пересчёт затрагивает только относящиеся узлы.
Из допустимого множества клиент выбирает активное множество соединений через **равномерный случайный выбор** размером `parallel_connections`. Случайный выбор — структурная защита от концентрации: даже если один узел объективно «лучше всех» по критериям, вероятность что все клиенты выберут именно его — низкая.
#### 11.5.3 Параллельные соединения и отказоустойчивость
Клиент держит N параллельных соединений к узлам-хостам одновременно (по умолчанию N = 5, диапазон 1..16, выбирает пользователь по компромиссу пропускная способность и избыточность).
**Операции gossip-ятся через все N узлов параллельно.** Цементирование операции не зависит от единичного узла: достаточно чтобы хотя бы один из N включил её в `BundledConfirmation`. Цензура единичным узлом не работает структурно — операция попадёт в сеть через другое соединение. Это превращает «один хост знает всё и может цензурировать» в «N хостов видят часть каждый, цензура требует координации большинства».
**Отказоустойчивость автоматическая.** При падении, таймауте соединения, явном отказе или падении `success_rate` ниже порога — клиент удаляет узел из активного множества, выбирает следующий из допустимого (тот же равномерный случайный выбор из оставшихся), устанавливает соединение. Никакого действия пользователя не требуется. Push на телефон отправляется только при массовом переключении (больше 50% активного множества за короткое время) — индикация системной проблемы, не отдельных ротаций.
**Мягкий чёрный список с отсрочкой.** Узел который систематически отказывает в gossip конкретных операций конкретного клиента (не общий офлайн / перегрузка) — попадает в локальный мягкий чёрный список с экспоненциальной отсрочкой: первое попадание — исключение на τ₁, второе — на 2 × τ₁ и так далее до постоянного локального блокирования после 8 инцидентов в одном τ₂. Мягкий чёрный список локальный (на клиента), не публикуется — это защита клиента, не санкция узлу.
#### 11.5.4 Ротация
Активное множество соединений ротируется по расписанию (по умолчанию раз в τ₂ окон ≈ 14 дней). При ротации: один узел из активного множества заменяется на новый из допустимого, выбранный равномерно случайно. Постепенная ротация (один узел за раз, не всё множество сразу) сохраняет непрерывность gossip и не создаёт всплеск на сетевом обнаружении.
**Защита от утечки метаданных.** Постоянное прикрепление к одному узлу даёт ему полный социальный граф клиента: социальные связи через адреса получателей `Transfer`, каналы через подписки, время активности через временные метки операций, IP через соединение. Ротация размывает граф между несколькими операторами в скользящем окне. После N циклов ротации (например 6 × τ₂ ≈ 84 дня) ни один из ранее использованных узлов не имеет полной картины — каждый видел только часть активности за свой период активного членства.
Ротация выключается пользователем явно (`rotation_period_tau2 = 0`) для случаев где предсказуемость важнее распределения приватности: стабильная корпоративная среда, известный надёжный собственный узел, специфические требования соответствия.
#### 11.5.5 Community-реестр публичной утилиты
Community-поддерживаемый консультативный реестр узлов, которые сами идентифицируют себя как публичная утилита — принимают хостинг любых аккаунтов без платы, без фильтрации контента, без юрисдикционных ограничений. Слой реестра **не часть протокола** (канонический реестр нарушил бы [I-3] — выбор узла стал бы консенсусно-значимым); это слой уровня приложения над протоколом.
**Самоидентификация оператора.** Узел публикует через свой `operator_account` декларацию через стандартный Anchor с фиксированным `app_id = "montana.public_utility"`:
```
PublicUtilityDeclaration {
node_id NodeID
operator_address AccountID
policy_hash hash32 (хэш документа политики)
policy_url строка (где скачать политику открытым текстом)
contact строка (электронная почта или matrix-handle для споров)
declared_at_window u32
signature 3309 B (ML-DSA-65, подпись ключом operator_account)
}
```
Декларация публичная и верифицируемая любым клиентом через стандартную проверку AccountChain. Оператор несёт репутационную ответственность за соответствие декларированной политике: систематические нарушения → исключение из community-реестра.
**Community-реестр.** Список узлов публичной утилиты с историей репутации поддерживается несколькими независимыми maintainer-ами (рекомендация: M = 35 организаций не аффилированных друг с другом). Каждый maintainer подписывает свой список своим keypair. Клиент принимает узел в допустимое множество по критерию реестра если **K из M** maintainer-ов включили его в свой список (по умолчанию K = 2, настраиваемо).
Реестр клиент использует как подсказку для первичного допустимого множества, не как обязательный фильтр. Узел не в реестре, но проходящий политику 11.5.2 — допустим если пользователь не выставил `require_advisory = true`. Это сохраняет permissionless природу сети: новый легитимный узел без одобрения реестром доступен для использования.
#### 11.5.6 Защита при первичном подключении от eclipse
Расширение 11.2 (Bootstrap-узлы) для защиты от случая когда атакующий контролирует источники первичного подключения конкретного клиента. Если все источники первичного подключения контролируются одним актором, политика 11.5.2 применяется к узлам которые видит клиент — но клиент видит только узлы атакующего, и среди них «самая длинная цепочка» = «самая длинная контролируемая атакующим». Защита — структурное первичное подключение из нескольких источников с перекрёстной верификацией.
При первом первичном подключении клиент использует несколько независимых источников одновременно:
- **Хардкодированный список зерен** в дистрибутиве — процесс сборки с несколькими maintainer-ами (не единый корпоративный контроль над релизным артефактом). Минимум 12 узлов из разных юрисдикций
- **Зёрна DNS** на разных провайдерах инфраструктуры — рекомендуется 3 или больше независимых DNS-зон (например `seed.montana.io`, `seed.montana.org`, `seed.montana-network.io`) на разных регистраторах и хостингах
- **Опциональное первичное подключение через Tor** для перекрёстной верификации из регионов с подозрением на сетевую цензуру
- **Опциональный вне-сетевой верифицированный узел** от доверенного контакта — QR-код или ручной ввод `NodeID` и мультиадреса
**Перекрёстные проверки.** Клиент сравнивает представления топологии полученные от разных источников. Если результаты значительно расходятся (больше 50% узлов из одного источника не известны второму, или непересекающиеся распределения `chain_length`) — предупреждение пользователю «возможна атака на обнаружение, проверьте канал первичного подключения» с детализацией расхождения. При совпадающих представлениях — высокая уверенность, переход к нормальной работе.
**Периодическое повторное первичное подключение.** Раз в N × τ₂ (по умолчанию N = 4, ≈ 56 дней) клиент перепроверяет источники первичного подключения для детекции долгой eclipse-атаки. Если новое первичное подключение даёт топологию значительно отличную от текущего допустимого множества — предупреждение и рекомендация пересмотреть активные соединения.
#### 11.5.7 Индикация в интерфейсе
В «Настройки → Сеть → Хостинг аккаунта» клиент показывает:
- Текущую стратегию (Авто / Закреплённый / Гибрид) с краткой пометкой компромисса
- Активное множество соединений: список N узлов с метриками на хост — `latency p95`, `success rate` за последний τ₂, временная метка последнего успешного gossip, `node_age`, `chain_length`, заявленная юрисдикция (если есть в `PublicUtilityDeclaration`), количество одобрений реестра
- Временная метка последней ротации и обратный отсчёт до следующей плановой
- Здоровье источников первичного подключения: количество источников в согласии, временная метка последней перекрёстной проверки, индикаторы расхождения
- Размер допустимого множества (сколько узлов проходят политику 11.5.2 сейчас) — индикатор здоровья сети для данного клиента
Управляющие действия в интерфейсе: принудительная ротация сейчас, переключение стратегии, управление закреплёнными / чёрными списками, аварийный ручной ввод хоста (в случае массового переключения при котором допустимое множество временно пусто), запуск повторного первичного подключения.
При проблемах (массовое переключение, `success_rate` ниже порога по большинству хостов, расхождение первичного подключения) — push на телефон с описанием состояния и рекомендованными действиями. Пользователь не должен узнавать о проблеме хостинга случайно.
### 11.6 Интеграция Mesh Transport
Раздел описывает интеграцию протокольного Mesh Transport (см. раздел Mesh Transport в спеке протокола) в клиентское приложение Montana на уровне нативных платформенных API. Формат MeshFrame, типы кадров, параметры буфера и правила хранения-и-пересылки определены в спеке протокола — здесь не дублируются.
#### 11.6.1 Режимы активации
Mesh-транспорт имеет три режима работы, пользователь выбирает в «Настройки → Сеть → Режим mesh»:
**Выключен (по умолчанию).** Mesh-транспорт не активируется. Приложение работает только через интернет. Рекомендуется для обычного использования — экономит батарею и не расходует радио.
**По требованию.** Mesh активируется автоматически когда приложение обнаруживает отсутствие интернет-соединения. При восстановлении интернета mesh деактивируется, кадры из буфера синхронизируются в сеть через интернет-шлюз. Индикатор в интерфейсе показывает текущий режим (интернет / mesh).
**Всегда включён.** Mesh активен постоянно параллельно с интернетом. Рекомендуется пользователям в контекстах высокого риска (активист, журналист в цензурной юрисдикции) — при внезапном отключении связь не прерывается. Расходует больше батареи (базовый расход ≈ 1525% в сутки в зависимости от устройства).
Пользователь явно соглашается на активацию mesh при первом включении — приложение показывает объяснение: «Режим mesh использует Bluetooth и Wi-Fi Direct для связи когда интернет недоступен. Расход батареи выше. Ваше местоположение не раскрывается приложению, но устройства в радиусе Bluetooth могут видеть факт наличия Montana на вашем телефоне.»
#### 11.6.2 Интеграция iOS
**Фреймворки:**
- `CoreBluetooth` для рекламы и сканирования BLE
- `MultipeerConnectivity` для обнаружения сервисов аналогичного Wi-Fi Direct (высокая пропускная способность для больших сообщений)
**Ограничения фонового режима.** iOS ограничивает фоновые операции Bluetooth:
- Фоновый режим `bluetooth-central` разрешает сканирование в фоне, но с уменьшенной частотой
- Фоновый режим `bluetooth-peripheral` разрешает рекламу в фоне с пониженным приоритетом UUID сервиса
- Полная функциональность mesh — только на активном экране; в фоне — пассивное прослушивание и приоритетная очередь для известных контактов
**`BGTaskScheduler`.** Периодические фоновые задачи запланированы через `BGProcessingTaskRequest` для периодической синхронизации буфера. iOS самостоятельно решает когда запустить задачу; приложение не гарантирует тайминг.
**UUID сервиса:** зарезервированный 16-байтовый UUID для сервиса mesh Montana (зарегистрирован в эталонной реализации), публикуется в данных рекламы BLE.
#### 11.6.3 Интеграция Android
**API:**
- `BluetoothLeAdvertiser` и `BluetoothLeScanner` для кадров BLE mesh
- `WifiP2pManager` для соединений Wi-Fi Direct (высокая пропускная способность)
- `ForegroundService` с уведомлением для долгоживущих операций mesh (Android требует видимого уведомления для фонового непрерывного BLE)
**Меры приватности.** На Android включена рандомизация MAC BLE — платформа ротирует аппаратный MAC каждые 15 минут по умолчанию. Дополнительно Montana ротирует `mesh_session_id` при переходе между сессиями mesh.
**Белый список оптимизации батареи.** При первом включении режима mesh приложение просит пользователя исключить Montana из оптимизатора батареи Android — без этого ОС может агрессивно приостанавливать фоновые операции.
#### 11.6.4 Жизненный цикл сессии
1. **Обнаружение:** приложение транслирует периодические кадры обнаружения (`frame_type = 0`) с базовым темпом. Другие устройства Montana в радиусе их получают.
2. **Совпадение контакта:** если кадр адресован известному контакту (`recipient_hint` совпадает) — приложение инициирует mesh-рукопожатие IBT (см. подраздел **Identity-Bound Tunnel → Mesh transport extension** выше в этой спеке).
3. **Установление сессии:** после успешного рукопожатия сессия установлена, `mesh_session_id` добавлен в локальный список активных mesh-сессий.
4. **Обмен данными:** сообщения чата или blob-ы платежей передаются через кадры данных (`frame_type = 1`) с аутентификацией через MAC сессии.
5. **Пересылка:** кадры не адресованные себе — хранятся в буфере mesh согласно правилам хранения-и-пересылки, оппортунистически пересылаются другим пирам.
6. **Закрытие сессии:** явное «закрыть сессию» пользователем, либо таймаут неактивности 4 часа, либо истечение допустимой устарелости `cached_window_index`.
#### 11.6.5 Роль шлюза
Устройство с одновременным доступом к интернету и mesh действует как **шлюз** между изолированной областью mesh и глобальной сетью Montana:
- Кадры полученные из буфера mesh адресованные `account_id` которые находятся за пределами mesh — пересылаются через интернет-gossip P2P к хостящему узлу получателя
- Кадры полученные через интернет адресованные `account_id` подключённым через mesh — помещаются в локальный буфер mesh для пересылки ближайшими пирами
Пользователь-шлюз может явно включить или отключить режим шлюза в настройках. По умолчанию включён для режима «всегда включён», выключен для «по требованию».
#### 11.6.6 Локальное хранилище
Специфичное для mesh локальное состояние хранится в зашифрованной базе SQLite рядом с остальным состоянием приложения:
```
active_mesh_sessions:
mesh_session_id 32 B (первичный ключ)
peer_pubkey 1952 B (ML-DSA-65)
peer_contact_account_id 32 B (если peer в адресной книге)
session_established_at временная метка
last_activity_at временная метка
session_mac_key 32 B (выведен через HKDF из общего секрета сессии)
cached_peer_window_index u32
mesh_buffer:
frame_hash 32 B (первичный ключ)
frame_bytes blob (сериализованный MeshFrame)
received_at временная метка
ttl_remaining u8
sender_ref 32 B
forwarded_to blob (множество peer-id как сериализованный массив)
mesh_used_nonces:
sender_pubkey 1952 B
nonce 32 B
expires_at временная метка (received_at + 7 × τ₁)
PRIMARY KEY (sender_pubkey, nonce)
```
Мастер-ключ шифрования состояния приложения применим к этим таблицам без отличий.
---
---
## Связанные спецификации
- **Montana Protocol** (родительский слой) — state machine, crypto, consensus.
- **Montana App** (дочерний слой) — UI клиенты на основе этого сетевого API.
История версий читается через git log и `Code/VERSION.md`.
---
*Конец спецификации Montana Network.*