**Слой:** Network — между Protocol (низкий) и App (высокий).
---
## Введение
Сетевой слой Montana — транспорт и discovery между узлами консенсуса и клиентами. Эта спецификация исторически жила inline разделами в Montana Protocol; отделена в собственный файл для разделения слоёв по принципу [I-7] минимальной криптографической поверхности и удобства независимого аудита.
**Что в этой спеке:**
- Транспортный слой через libp2p (TCP+TLS 1.3 + Noise XK)
Все временные параметры сетевого уровня (frame rate, padding window, feeler interval, Dandelion timers) — implementation guidance для локального сетевого стека узла. Они оперируют на локальных часах узла и находятся вне scope consensus state.
### Обфускация транспорта
Монтана — персональная сеть. Каждый узел — персональный сервер участника. Транспортный слой построен из этого определения: персональный сервер отвечает только участникам, персональный мессенджер скрывает тайминг сообщений, персональный = доступный обычному человеку.
#### Шифрование
Все P2P-соединения инкапсулированы в TLS 1.3 на порт 443. Noise framework (встроен в libp2p) для аутентификации по публичному ключу узла внутри TLS. Содержимое трафика недоступно наблюдателю.
#### Identity-Bound Tunnel (IBT)
Персональный сервер отвечает только участникам сети. После TLS handshake клиент отправляет proof аутентификации. Узлы (зарегистрированные и приглашённые) подписывают node keypair. Аккаунты (клиенты) подписывают account keypair.
-`node_id`с`node_pubkey = client_pubkey` в Candidate Pool → **read-only gossip**: получает proposals (кандидат подключился node keypair)
-`account_id = SHA-256("mt-account" || suite_id || client_pubkey)` в Account Table → **подключение к доверенному узлу** (клиент подключился account keypair)
- Ни одно не найдено → отказ
Условия 1-2 выполнены + уровень 3 определён → Noise handshake → P2P-сеть Монтаны с соответствующим уровнем доступа.
Любое не выполнено → TLS alert `bad_certificate`, close. Стандартное поведение сервера с обязательной аутентификацией клиента — таких серверов в интернете миллионы (корпоративные порталы, API, банковские системы).
Replay protection: server_node_id привязывает proof к конкретному получателю. Window slot ограничивает replay window до 2 окон.
Bootstrap exception: genesis bootstrap nodes хардкодированы как `(IP, node_id, pubkey) × 12`. Bootstrap принимает proof от любого валидного ML-DSA-65 ключа (Account Table не проверяется). Для защиты от connection flood клиент прилагает proof-of-work:
`target` подбирается чтобы стоимость ≈ 100ms CPU. PoW требуется только при подключении к bootstrap, не к обычным peers.
**Mesh transport IBT extension.**
Mesh transport (см. подраздел «Mesh Transport» ниже) работает при отсутствии fresh `window_index` — устройство может быть offline часы или дни до следующей синхронизации с internet-сетью. IBT proof в mesh контексте использует **cached**`window_index` — значение последнего известного окна с любого предыдущего online-соединения.
Формула для mesh transport:
```
mesh_proof = ML-DSA-65_sign(
client_privkey,
"mt-tunnel-mesh"
|| peer_node_id
|| floor(cached_window_index / 2)
|| mesh_session_nonce)
где:
cached_window_index u32 — последнее известное окно
от любого предыдущего online
handshake или gossiped proposal
mesh_session_nonce 32B — генерируется инициатором
handshake из CSPRNG, передаётся
в plain части mesh advertisement
```
**Acceptable staleness bound.** Peer принимает `cached_window_index` в диапазоне `[peer.known_window_index - 7 × τ₁, peer.known_window_index]`. Свыше `7 × τ₁` cached значение признаётся слишком старым — peer отклоняет mesh IBT handshake и требует свежее значение через любой доступный канал до продолжения.
**Session nonce tracking.** Peer хранит `used_nonces[sender_pubkey]` — set of `mesh_session_nonce` значений использованных данным sender в пределах acceptable staleness window. При приёме proof с`mesh_session_nonce ∈ used_nonces[sender_pubkey]` → reject (replay). Set pruning: записи старше `7 × τ₁` удаляются (nonce reuse после expiry acceptable, cached_window_index уже невалиден).
**Domain separator обязательно `mt-tunnel-mesh`, не `mt-tunnel-online`.** Отдельный separator критичен — иначе атакующий перехвативший online IBT proof (window slot = `2 × τ₁` replay) мог бы использовать его в mesh контексте где staleness window расширен до `7 × τ₁`. Cross-context replay блокируется на уровне domain separation.
payload 1021B (данные или random padding до frame_size)
```
Персональный мессенджер скрывает тайминг: между узлами идёт постоянный поток фреймов. Реальные сообщения Монтаны замещают padding-фреймы, не добавляются к ним. Наблюдатель внутри сети не может отличить перевод от доказательства времени от тишины — всё одинаковые зашифрованные фреймы.
Параметры:
- Baseline frame rate: 1 frame/сек на исходящих соединениях. Входящие — фреймы при наличии данных
- Maximum burst: ≤ 8 frames подряд без паузы ≥ 10ms
- Minimum padding ratio: ≥ 20% фреймов в скользящем окне τ₁ на исходящих
Все рандомизированные решения транспортного уровня (stem routing, frame scheduling, nonce generation) используют CSPRNG из OS entropy pool. Детерминированный PRNG от node state запрещён для transport-layer randomness.
Transport obfuscation ортогонален консенсусу. TimeChain, state machine работают поверх любого транспорта без изменений.
### Выбор пиров
Открытый вход с VDF-барьером делает sybil-узлы дорогими: каждый sybil = τ₂ окон VDF (sequential SHA-256, не ускоряется параллелизмом) + selection event. Peer selection использует diversity constraints из протокольных данных (start_window) и сетевых (/16, ASN).
P2P gossip — только зарегистрированные и приглашённые узлы (уровни 1-2 IBT, см. Transport Obfuscation → Identity-Bound Tunnel). Аккаунты (уровень 3 IBT) взаимодействуют через свой доверенный узел.
#### Исходящие соединения
24 исходящих, все полные. Uniform framing скрывает типы сообщений — отдельные relay-only соединения не нужны.
Выбор: случайный 50/50 из таблиц «новые» и «проверенные». Бакетирование с секретным ключом узла. Без preference по chain_length — выбор равномерный.
#### Четыре уровня diversity
Каждый исходящий проверяется по всем четырём constraints:
```
Сетевые:
/16 — не более 1 исходящего на /16 подсеть (IPv4) или /48 (IPv6)
ASN — не более 2 исходящих на автономную систему
Протокольные:
start_window — не более 2 исходящих к узлам с start_window в одном τ₂
```
Сетевые constraints: /16 и ASN diversity. Протокольный constraint start_window канонически доступен из Node Table.
Следствие: кластер sybil зарегистрированных в один τ₂ → максимум 2 из 24 слотов. Eclipse требует узлы в 7+ разных AS в 7+ разных /16 с регистрацией в 7+ разных τ₂.
ASN-карта загружается при запуске. Без карты — fallback на /16.
#### Адресный менеджер
Две таблицы:
- **Новые** — адреса полученные через peer exchange и DHT. Узел ещё не подключался
- **Проверенные** — адреса к которым узел успешно подключался через IBT
Бакетирование: `bucket = Hash(secret_key, source_group, addr_group) % N`. Детерминированно с секретным ключом — атакующий не может предсказать в какой бакет попадёт его адрес.
#### Входящие соединения
До 32 входящих. При переполнении — вытеснение:
1. Защитить 4 с наименьшим пингом
2. Защитить 4 с последними полезными сообщениями (любое валидное сообщение Монтаны которое узел ещё не видел)
3. Защитить до 8 из разных подсетей (по одному от каждой)
4. Защитить 4 с последними proposals
5. Из оставшихся — вытеснить из крупнейшей подсетевой группы
#### Якоря
2 исходящих с наибольшим uptime соединения сохраняются каждые τ₂. При перезапуске после аварии или обновления — подключиться к якорям первым до случайного выбора из таблиц.
#### Feeler
Раз в 10 τ₁: подключиться к случайному адресу из «новых», выполнить IBT handshake (все три уровня проверки). Успех на любом уровне → перенести в «проверенные» с пометкой уровня (node / invited / account). Неуспех → пометить или удалить.
#### Ротация
По поведению: если peer не передал ни одного нового proposal за τ₂ — заменить. Peer с долей невалидных сообщений выше 50% в скользящем τ₁-окне — отключить с запретом переподключения на τ₂. Peer который relay-ит честно — полезен сети, остаётся.
#### PeerRecord
Формат записи о пире при peer exchange:
```
PeerRecord:
ip 16B (IPv4-mapped IPv6)
port 2B (u16)
node_id 32B
node_pubkey 1952B (ML-DSA-65)
```
Без node_id и node_pubkey клиент не может вычислить IBT proof для подключения. Peer exchange: не более 100 PeerRecord за сообщение. Не более 1 peer exchange сообщения за τ₁ от каждого peer.
### Цензуроустойчивое обнаружение
Генезис: 12 hardcoded bootstrap nodes `(IP, node_id, pubkey)`. Если все 12 IP заблокированы на уровне страны — новый узел не может войти в сеть. Пять независимых каналов обнаружения. Достаточно одного из пяти.
**1. Peer exchange.** Каждый узел хранит и передаёт список активных пиров новичкам. Достаточно знать IP одного узла — друг, QR-код, мессенджер. Один живой контакт = вход в сеть.
**2. DHT.** Kademlia DHT поверх libp2p. Узлы находят друг друга без центральной точки. Идентификаторы рандомизированы — DHT не раскрывает node_id до установления соединения Монтаны.
**3. Bridge nodes.** Узлы за пределами цензурируемой юрисдикции, опубликованные через внеполосные каналы (социальные сети, мессенджеры, печатные QR-коды). IP bridge node неизвестен фаерволу до использования.
**4. Encrypted Client Hello (ECH).** Bootstrap через CDN с поддержкой ECH. SNI зашифрован — наблюдатель видит IP CDN, но не целевой домен. Эффективен в юрисдикциях без активной блокировки ECH extension. В юрисдикциях блокирующих ECH (Китай с 2023, Россия с 2024) — канал неработоспособен. Для таких юрисдикций — каналы 1-3, 5.
**5. Mesh peer exchange.** При полном отсутствии доступа к internet (государственный shutdown, отключение межзоновой связности, локальная изоляция) узел обнаруживает локальных peers через mesh transport (Bluetooth LE advertisement, Wi-Fi Direct service discovery). Peer exchange работает на уровне mesh frame с типом `frame_type = 0 (discovery)` — см. подраздел «Mesh Transport» и «Store-and-Forward Semantics». Физический радиус обнаружения — десятки метров; mesh multi-hop forwarding расширяет эффективный радиус до сотен метров и километров при достаточной плотности устройств. Когда хотя бы одно устройство в mesh-сети получает доступ к internet — вся цепочка синхронизируется через него как через единый шлюз.
Избыточность = устойчивость. Пять каналов независимы по physical-layer доставке (IP internet для 1-4, radio mesh для 5). Блокировка internet-канала на уровне государства не затрагивает канал 5 — отключить mesh требует подавления Bluetooth/Wi-Fi на каждом устройстве физически, что практически нереализуемо.
### Dandelion++ (анонимность отправителя)
P2P gossip Монтана ретранслирует операции через все узлы. Без защиты первый пир знает IP отправителя. Dandelion++ (Fanti et al. 2018) устраняет связь IP → операция модификацией существующего gossip.
**Две фазы:**
```
Stem (стебель):
Операция проходит по цепочке случайных узлов (в среднем 2-3 hop).
Каждый узел видит только предыдущий hop, не автора.
На каждом hop с вероятностью p = 0.4 переход в fluff.
E[stem_length] = 1/p = 2.5 hops.
P(stem ≤ 1) = 40%, P(stem ≤ 3) = 78%.
Fluff (пух):
Последний stem-узел запускает обычный gossip.
Для всей сети операция «появилась» из случайной точки.
```
**Stem routing.** Стебель использует только исходящие соединения — входящие не участвуют. Каждые 693 окна узел переизбирает 2 из 24 исходящих как `stem_peers` (период выбора множества стебельных). Внутри этого окна 693 окон — `stem_successor` (forward choice одного из 2) ротируется каждое τ₁ — см. Карточку Dandelion++ для нормативной формулировки. Все стебельные операции в эпохе направляются через одного из этих 2 (выбор по hash(msg)).
| Анализ графа gossip | Операция входит в gossip из случайной точки |
| Контроль k узлов | Деанонимизация требует контроля O(√n) узлов |
**Реализация:**
```
stem_peers = random_sample(outbound, 2) // каждые 693 окна (выбор из 24 outbound)
on_receive_stem(msg, from_peer):
if random() <0.4:
gossip_broadcast(msg) // fluff
else:
next = stem_peers[hash(msg) % 2] // детерминированный выбор из 2
send_stem(msg, next) // продолжить stem
start_timer(msg, 30s) // страховка на каждом hop
on_timer_expired(msg):
if msg не обнаружен в gossip:
gossip_broadcast(msg) // принудительный fluff
```
Каждый stem-узел страхует следующий. Таймер τ₁/2 на каждом hop независимо. Если следующий hop уронил сообщение — текущий hop обнаруживает отсутствие операции в gossip и делает fluff сам. Максимальная задержка = τ₁/2 (один hop), не кумулятивная.
Dandelion++ не требует внешней инфраструктуры. Каждый узел Монтаны уже является relay — gossip существует, stem добавляет 2-3 hop перед ним. Latency overhead: миллисекунды.
### NAT Traversal
Персональная сеть работает когда каждый может войти. Большинство домашних пользователей за NAT — невидимы для входящих соединений. Без NAT traversal персональный интернет = серверный клуб.
Три механизма, каждый следующий — если предыдущий не сработал:
**1. AutoNAT (определение).** Узел спрашивает outbound peers: «видишь ли мой IP:port напрямую?» Если да — NAT нет. Если нет — узел знает свой NAT-статус.
**2. DCUtR (пробивка).** Два NAT-узла координируются через третий узел с публичным IP. Оба отправляют исходящие пакеты — роутеры открывают «дырки» для ответов. После координации — прямое соединение. Успех: 60-70% случаев (TCP). Carrier-grade NAT (мобильные операторы): ~30%.
**3. Circuit Relay v2 (транзит).** Если пробивка не удалась — трафик идёт через outbound peer с публичным IP. Relay — не отдельный механизм и не выделенный сервер. Relay-соединение = обычное исходящее соединение, подчиняющееся тем же правилам: uniform framing, diversity constraints, ротация по поведению. Содержимое зашифровано конец-в-конец (Noise) — relay видит IP участников но не содержимое. Metadata распределён по 24 outbound peers из разных /16 и ASN — ни один relay не видит полный граф.
Relay — не fallback а гарантия подключения при любом типе NAT. Пробивка — оптимизация для снижения нагрузки на relay.
**Лимиты relay:** до 32 одновременных relay-соединений на узел, bandwidth per relay ≤ baseline frame rate (1 KB/сек). 32 × 1 KB/сек = 32 KB/сек ≈ 82 GB/мес — приемлемо для домашнего узла с публичным IP.
**Обязанность.** Узлы с публичным IP поддерживают relay — персональная сеть работает когда каждый может войти. Reference implementation включает relay при обнаружении публичного IP. Feeler-подключения проверяют поддержку relay у peers; узлы без relay помечаются `no-relay` в адресном менеджере. NAT-узлы предпочитают peers поддерживающие relay при выборе исходящих.
Все три механизма — стандарт libp2p (AutoNAT, DCUtR, Circuit Relay v2). Ноль новых протокольных примитивов.
### Mesh Transport
Internet не всегда доступен. Государственные shutdown (Иран 2019 — неделя, Беларусь 2020 — дни, Мьянма 2021 — месяцы), локальные сбои, изолированные зоны. Монтана поддерживает работу в таких условиях через mesh transport поверх Bluetooth Low Energy и Wi-Fi Direct — устройства обнаруживают друг друга в физическом радиусе и пересылают encrypted сообщения Монтаны hop-by-hop. Mesh не замещает internet transport, а дополняет: при возвращении связности сеть автоматически конвергирует через mesh-internet шлюз.
**Mesh transport ортогонален консенсусу** так же как internet transport ([раздел выше](#сетевой-уровень)) — state machine работает поверх любого доставочного канала без изменений.
#### MeshFrame wire format
Все mesh-сообщения фрагментируются на фреймы фиксированного формата:
```
MeshFrame:
mesh_protocol_version u16 — версия mesh wire format
-`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:
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:
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 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 / наблюдатель отдельного линка.
- Закрывает: 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.
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 — что закрывается и что не закрывается
| 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 персональный сервер скрывает содержимое и тайминг
Слой 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 всегда 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). Не выделять вновь. |
records ? <-record_count×serialize(record)поcanonicalencoding
```
Response состоит из N chunks (с одним request_id). Получатель собирает по chunk_index. После получения всех total_chunks — reconstructs Merkle root и проверяет против proposal_W.
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.
- 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 на хоста.
Клиент формирует batch: один real target + 15 decoy-аккаунтов, перемешанных в произвольном порядке. Клиент локально запоминает позицию real target внутри batch.
Хост **обязан** обработать все `count` queries и вернуть `count` results в том же порядке. Частичные ответы запрещены — либо полный BatchLookupResponse, либо BatchLookupError.
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
На масштабе сети 1B+ активных пользователей клиент собирает passively-observed pool активных аккаунтов через gossip proposals. Realistic pool size: 10K–100K накопленных за τ₂ observation window.
При pool size 10K–100K и K=16:
- **Effective anonymity:** ~2–3 бита (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 от хоста.
- 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 <-u16LE,числонайденныхblobs
blobs blob_count × BlobEntry
где BlobEntry:
matched_label 32B <-одинизlabelsзапроса
blob_size 4B <-u32LE,размерblobвбайтах
blob_data blob_size × B <-encryptedpayload
```
Хост возвращает все blobs из Blob Buffer чей app_id соответствует одному из запрошенных labels (через derivation `app_id = SHA-256("mt-app" || label)`). Blobs возвращаются в произвольном порядке; клиент matches их к labels через `matched_label` поле.
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
`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) над байтовой строкой
Сетевой слой создаёт локальные персистентные таблицы вне 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 механизмы.
Платит 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 узла
регистрации (τ₂ окон VDF)
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`.
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.
### Сетевой уровень — 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) |
| **Eclipse attacker** | Контроль ≥ ¾ outbound peer slots целевого узла через позиционирование своих узлов в peer-tables; цель — изолировать жертву от честной сети | Multiple registered nodes (×τ₂ окон VDF за каждый Sybil) + multiple IP /16 / ASN |
| **Sybil attacker** | Регистрирует N формально legitimate узлов через многократное прохождение VDF 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 узла отправителя |
#### Coverage matrix
Cell содержит механизм защиты + status. Status: **C** = closed конструкцией; **P** = partial (acknowledged residual); **O** = out of scope (см. ниже).
| | Passive observer | Active MITM | Eclipse | Sybil | DoS | Censor | Sabotage |
Эти угрозы **не закрываются** сетевым слоем Монтаны 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.
4.**Quantum-capable adversary** — статус: **mainnet blocker, plan to closure через Noise_PQ migration**. Текущее состояние: PQ primitives (ML-DSA-65, ML-KEM-768) защищают application auth и encryption (✅), но TLS 1.3 handshake использует classical X25519 ECDHE — vulnerable к store-now-decrypt-later квантовым атакам. **План closure**: replace TLS 1.3 + Noise XK на единый Noise_PQ handshake с ML-KEM-768 как KEM replacement для DH; это удаляет TLS layer целиком (он не даёт ничего сверх IBT-аутентифицированной Noise) и достигает pure-PQ handshake без classical primitives в protocol layer. **Per [I-1]** в protocol layer classical crypto **запрещена** (только в client layer допустима — например browser в Junona к обычному web). Defer to upstream rustls — отвергнут как «acknowledged residual» больше не приемлем; closure через Noise_PQ — обязательный mainnet milestone.
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.
| 1 | Censor → Confidentiality (data) | DPI видит TLS metadata (handshake, certificate, SNI без ECH) | Medium | TLS 1.3 ECH когда rustls поддержит; deferred from M6 baseline |
| 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 |
| 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 возникают.
- **[I-7] Minimal crypto surface** — Threat model не вводит новых crypto primitives; переиспользует ML-DSA-65 / SHA-256 / ML-KEM-768 / TLS 1.3.
#### 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
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.
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).
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)
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)
3.`NodeInvitation` публикуется и финализируется в сети
4. Пользователь получает уведомление «Вас пригласили стать узлом»
5. Пользователь подтверждает
6. Приложение запускает VDF-процесс длиной `vdf_entry_windows = 20 160 окон` (около 14 дней) в фоне
7. После завершения формируется `NodeRegistration`с`proof_endpoint`
8. Пользователь публикует `NodeRegistration` (`operator_account_id = свой account_id`)
9. После финализации — пользователь становится узлом Montana
VDF-процесс — блокирующий. Приложение должно работать непрерывно или продолжать VDF при каждом запуске. На мобильном это практически невозможно; на десктопе возможно, но требует 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.
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 — Гибрид.** Авто с ограничениями — белый список (всегда предпочитать эти узлы пока они проходят критерии), чёрный список (никогда не использовать), юрисдикционные фильтры. Компромисс: средняя сложность настройки, средний контроль.
| `node_age ≥ min_node_age` | 6 × τ₂ (≈ 84 дня от первого сцементированного `BundledConfirmation`) | заранее построенных sybil-узлов специально подготовленных к атаке за короткий период |
| `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 = 3–5 организаций не аффилированных друг с другом). Каждый 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 активен постоянно параллельно с интернетом. Рекомендуется пользователям в контекстах высокого риска (активист, журналист в цензурной юрисдикции) — при внезапном отключении связь не прерывается. Расходует больше батареи (базовый расход ≈ 15–25% в сутки в зависимости от устройства).
Пользователь явно соглашается на активацию 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.