Montana's network layer covers transport and discovery between consensus nodes and clients. This specification historically lived as inline sections inside Montana Protocol; it has been split into its own file to separate the layers per the [I-7] minimal-cryptographic-surface principle and to make independent audit easier.
- Transport layer via libp2p (TCP + Noise_PQ XX → Yamux), where Noise_PQ XX is the post-quantum security upgrade replacing the classical TLS 1.3 + Noise XK chain
All time parameters at the network layer (frame rate, padding window, feeler interval, Dandelion timers) are implementation guidance for the node's local network stack. They operate on the node's local clock and are outside the scope of consensus state.
Montana is a personal network. Each node is the participant's personal server. The transport layer is built from this definition: a personal server answers only to participants, a personal messenger hides message timing, personal = affordable to an ordinary person.
All P2P connections are encrypted by Noise_PQ XX (ML-KEM-768 ephemeral KEM on both sides + ML-DSA-65 identity + ChaCha20-Poly1305 AEAD), the production transport handshake. Inbound listeners default to TCP port 8444. The content of the traffic is not accessible to an observer.
A personal server answers only to network participants. After the TLS handshake the client sends an authentication proof. Nodes (registered and invited) sign with the node keypair. Accounts (clients) sign with the account keypair.
1. Signature is valid for the claimed client_pubkey
2. Window slot = current OR previous (window = 2 window_index)
3. Access level — the server checks client_pubkey against three tables in order, the first match determines the level:
-`node_id = SHA-256("mt-node" || client_pubkey)` in Node Table → **full gossip** (client connected with the node keypair)
-`node_id` with `node_pubkey = client_pubkey` in Candidate Pool → **read-only gossip**: receives proposals (candidate connected with the node keypair)
-`account_id = SHA-256("mt-account" || suite_id || client_pubkey)` in Account Table → **connection to a trusted node** (client connected with the account keypair)
- None matched → reject
Conditions 1-2 met + access level from step 3 determined → Noise handshake → Montana's P2P network at the corresponding access level.
Any condition failed → TLS alert `bad_certificate`, close. This is standard server behaviour for mandatory client authentication — there are millions of such servers on the internet (corporate portals, APIs, banking systems).
Replay protection: three-layer defence simultaneously. (a) `server_node_id` binds the proof to a specific recipient — replay against a different server is impossible. (b) The window slot caps the replay window at 2 windows (≤120 seconds at genesis calibration) — the proof becomes invalid after one window. (c) **Session nonce tracking.** The server keeps `used_online_nonces[client_pubkey]` — a set of `online_session_nonce` values used by this client within the current / previous window. On receiving a proof with `online_session_nonce ∈ used_online_nonces[client_pubkey]` → reject (replay within the window slot). Set pruning: entries older than 2 windows are removed (nonce reuse is acceptable after expiry — the window slot is no longer valid). This is a defence-in-depth closure of the MITM-replay class inside the window slot — even if a proof is intercepted, the attacker cannot reuse it against the same server within those 2 windows.
**Verification procedure for an online peer.** On receiving an online IBT advertisement, the server node performs the checks in the following order (mirror of the mesh IBT procedure):
1. Parse the advertisement; extract `client_pubkey`, `online_session_nonce` (32 B), `proof` (3309 B ML-DSA-65 signature).
2. Check that the ML-DSA-65 signature is valid for `client_pubkey` over the message reconstruction `"mt-tunnel-online" || server_node_id || u64_LE(floor(current_window_index / 2)) || online_session_nonce` (current window slot). If it does not match — try with `floor(current_window_index / 2) - 1` (previous slot).
3. Check `server_node_id == local_node_id` (proof bound to this server).
5. Look up `client_pubkey` against three tables in order: Node Table (via `SHA-256("mt-node" || client_pubkey)` lookup) → Candidate Pool → Account Table (via `SHA-256("mt-account" || suite_id || client_pubkey)`). The first match determines the access level. None matched — reject.
6. All checks passed → accept; add `online_session_nonce` to `used_online_nonces[client_pubkey]`; start the Noise handshake; start the P2P session at the access level from step 5.
7. Any check failed → TLS alert `bad_certificate`, close. Silent reject is optional (do not give an attacker feedback about which step failed).
**`used_online_nonces` memory bound.** Set per `client_pubkey`, ephemeral, transport-layer (not consensus state — [I-14] formally does not apply). DoS bound on two levels: (i) per-pubkey set bounded by `MAX_ONLINE_NONCES_PER_PUBKEY = 256` (an attacker with one keypair cannot flood more than 256 nonces per window slot — the handshake rate-limit already caps the real stream below <256/2τ₁);(ii)globalbound`MAX_ONLINE_NONCES_TOTAL = 65 536`(32B×65536≈2MBperserver—acceptablememoryoncommodityhardware[I-5]).Pruningatthewindow-slotboundary:entriesolderthan2windowsareremovedautomatically.Defenceagainstmanydistinctclient_pubkeys:server-sidehandshakerate-limit(seebackpressurerule[B5]inthespec—`max_pending_requests_per_peer`)capsthenew-handshakestreampersource.
Bootstrap exception: genesis bootstrap nodes are hard-coded as `(IP, node_id, pubkey) × 12`. The bootstrap accepts a proof from any valid ML-DSA-65 key (the Account Table is not consulted). To defend against a connection flood, the client attaches a proof-of-work:
Mesh transport (see the «Mesh Transport» subsection below) operates without a fresh `window_index` — the device may be offline for hours or days until the next sync with the internet-side network. The IBT proof in the mesh context uses a **cached**`window_index` — the last known window value from any previous online connection.
**Acceptable staleness bound.** The peer accepts `cached_window_index` in the range `[peer.known_window_index - 7 × τ₁, peer.known_window_index]`. Above `7 × τ₁` the cached value is treated as too stale — the peer rejects the mesh IBT handshake and requires a fresh value through any available channel before continuing.
**Session nonce tracking.** The peer keeps `used_nonces[sender_pubkey]` — a set of `mesh_session_nonce` values used by this sender within the acceptable staleness window. On receiving a proof with `mesh_session_nonce ∈ used_nonces[sender_pubkey]` → reject (replay). Set pruning: entries older than `7 × τ₁` are removed (nonce reuse after expiry is acceptable — the cached_window_index is no longer valid).
**The domain separator MUST be `mt-tunnel-mesh`, not `mt-tunnel-online`.** A separate separator is critical — otherwise an attacker who intercepted an online IBT proof (window slot = `2 × τ₁` replay) could reuse it in the mesh context, where the staleness window is widened to `7 × τ₁`. Cross-context replay is blocked at the domain-separation level.
- Online IBT (separator `mt-tunnel-online`): replay window `2 × τ₁` — narrow, plus per-nonce tracking blocks replay inside the window slot (see Replay protection above).
- Mesh IBT (separator `mt-tunnel-mesh`): replay window widened to `7 × τ₁`, but replay is blocked by per-nonce tracking.
- Cross-context: domain separation makes a proof for one context invalid in the other.
**Verification procedure for a mesh peer.**
1. Parse the advertisement; extract `sender_pubkey`, `mesh_session_nonce`, `proof`.
2. Verify the signature against `sender_pubkey`.
3. Recover `cached_window_index` from the proof message reconstruction (the peer knows sender_pubkey, peer_node_id is known locally; the peer tries the range `[known_window_index - 7 × τ₁, known_window_index]` until a matching value is found; if no match — reject).
A personal messenger hides timing: a continuous stream of frames flows between nodes. Real Montana messages replace padding frames rather than adding to them. An on-network observer cannot distinguish a transfer from a proof of time from silence — everything is the same encrypted frames.
All randomized decisions at the transport layer (stem routing, frame scheduling, nonce generation) use a CSPRNG seeded from the OS entropy pool. A deterministic PRNG derived from node state is forbidden for transport-layer randomness.
Transport obfuscation is orthogonal to consensus. The TimeChain and state machine work over any transport without changes.
#### Post-quantum transport migration (M6 milestone)
**Historical state (pre-M6 closure).** The transport layer used TLS 1.3 with classical X25519 ECDHE for the outer tunnel and Noise XK (Diffie-Hellman over X25519) for the inner peer authentication. Both classical handshakes were vulnerable to store-now-decrypt-later attacks by a future quantum adversary. This exposure has been closed by switching the production transport to Noise_PQ XX (see the next paragraph and the wire format section).
**M6 status — closed.** Production transport is now a single post-quantum handshake: Noise_PQ XX with ML-KEM-768 ephemeral keypairs on both sides of the handshake, ML-DSA-65 identity signatures over the transcript, and ChaCha20-Poly1305 AEAD framing on the established session. The classical TLS 1.3 + Noise XK chain has been removed from the libp2p stack (it provided no security property beyond the IBT-authenticated inner layer; the DPI obfuscation role is preserved by uniform framing on top of Noise_PQ XX). Transport confidentiality is post-quantum end-to-end. PeerId is derived from each peer's ML-DSA-65 identity public key as the SHA-256 multihash (libp2p / IPFS sha2-256 multihash code 0x12), so cryptographic and routing identities are bound to the same key material.
**Multi-confirmer cementing protocol.** The bootstrap genesis cohort operates today in a singleton-cementing regime (the proposer is the sole confirmer because its `chain_length` is dominant after long-running operation; all other operators have `chain_length ≤ 1` from a recent registration). The multi-confirmer cementing protocol formalised below is the normative path for operational regimes in which non-bootstrap operators accumulate non-negligible `chain_length` over many τ₂ epochs.
Protocol steps per window `W`:
1.**Candidate Proposal.** The canonical proposer (per the lottery selection of `W − 2`) constructs `ProposalHeader` with `included_bundles_root` set to `H(my_own_BC)` and broadcasts the header (`MsgType::Proposal`, 0x22).
2.**Follower confirmation.** Each Active operator on receipt of the candidate Proposal (a) verifies the proposer is canonical for `W`, (b) signs its own `BundledConfirmation(W, my_endpoint, my_op_hashes, my_reveal_hashes)` where `my_endpoint = SHA-256("mt-lottery" || T_r(W) || cemented_bundle_aggregate(W-2) || my_node_id || W)`, (c) broadcasts the BC envelope (`MsgType::BundledConfirmation`, 0x20) to the proposer and to all connected peers.
3.**Proposer accumulator.** The proposer maintains a per-window BC accumulator. On each incoming BC, the proposer validates per `validate_bundle` and, if valid, adds the BC to the accumulator. The accumulator window closes when `cemented_sum = Σ node.chain_length` over all collected confirmers reaches `quorum(active_chain_length) = ⌈67 × Σ active_chain_length / 100⌉`, or when one `τ_1` window elapses since the candidate Proposal was broadcast (whichever first).
4.**Cemented Proposal broadcast.** The proposer builds the final ProposalHeader with `included_bundles_root` = sparse Merkle root over the collected BC set, and broadcasts the cemented envelope as `MsgType::Proposal` (0x22). The cemented envelope is extended from the legacy 3722-byte header-only layout to a length-prefixed schema `[header (3722 B)][u16 bundle_count][bundle_count × BC]`, where each BC is variable-length per the standard `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.** Each follower on receipt of the cemented Proposal (a) parses the bundle set, (b) calls `validate_bundle` on each BC with `expected_endpoint = SHA-256("mt-lottery" || T_r(W) || cemented_bundle_aggregate(W-2) || bc.node_id || W)`, (c) builds `ProposalSettle { window_w: W, winner_id, cemented_confirmers: [all signers] }`, (d) calls `apply_proposal` with the multi-confirmer set.
The singleton-cementing path (a single confirmer, used today on the genesis cohort while `chain_length` is dominated by the bootstrap proposer) is the special case of the protocol above with `bundle_count = 1`; the 3722-byte header-only envelope handles that special case. Operational regimes with non-bootstrap operators accumulating `chain_length` require the extended length-prefixed schema and validation of `bundle_count ≥ 1`.
T_r consistency requirement. The follower's `compute_endpoint` requires the canonical `T_r(W)`. Two implementation paths are viable: (i) followers tick the VDF locally in lockstep with wall-clock and cache the per-window `T_r` history during catch-up; (ii) the proposer extends the cemented Proposal envelope with `T_r(W)` (32 B) so followers do not need to compute it locally. Path (ii) is the cleaner architectural choice; it adds 32 B per envelope and removes any drift hazard for followers whose VDF tick is behind the cemented head.
Wire-format and KAT vectors for the cemented Proposal envelope with `bundle_count ≥ 2` are normatively specified in the Network spec section above. Cross-implementation conformance binding for the schema is tracked under milestone M9.
- **Phase 0 — Architecture & scaffolding (closed).** DEV-014 tracker entry in `Code/docs/SPEC_DEVIATIONS.md` documented the migration plan; capability detection was reserved via a `pq_transport_version` wire field in the IBT advertisement.
- **Phase 1 — Noise_PQ handshake implementation (closed).** Custom Noise_PQ handler written outside libp2p's noise upgrade module in `crates/mt-noise-pq`. Wraps the ML-DSA-65 identity signature over a transcript that includes both ephemeral ML-KEM-768 public keys and the ML-KEM-768 ciphertexts. KAT vectors checked into `crates/mt-noise-pq/tests/kat.rs`.
- **Phase 2 — XK → XX redesign (closed).** The initial XK variant required the initiator to know the responder's static ML-KEM-768 public key a priori — incompatible with libp2p's plug-in `with_tcp` auth-upgrade slot which gives the upgrade only the local `libp2p::identity::Keypair` (Ed25519). The XX redesign discovers remote identity during the handshake (ephemeral ML-KEM-768 keypairs on both sides; identity ML-DSA-65 pk transmitted in msg2 / msg3 and authenticated by signature over transcript). New wire format documented in this section.
- **Phase 3 — Classical removal (closed).** The libp2p auth chain `(tls::Config::new, noise::Config::new)` in `mt-net-transport::transport::build_swarm_with_keypair` was replaced with `NoisePqXxConfig`. The transport stack is now `TCP → Noise_PQ XX → Yamux`. Uniform framing layer is preserved (it provides DPI obfuscation orthogonally to the handshake). The `pq_transport_version` field stays reserved-but-unused for future protocol negotiation if multistream-select proves insufficient.
**Verification on the genesis 3-node network.** Each phase is verified on the three production nodes (Moscow, Helsinki, Frankfurt) for ≥24 hours of continuous operation before being declared closed. Phase 1 closure requires byte-exact KAT vectors checked into `mt-conformance` and cross-node handshake success with zero classical fallback during the observation window.
**[I-1] compliance status.** Closed. The entire protocol stack is post-quantum end-to-end: consensus signatures via ML-DSA-65, application-layer encryption via ML-KEM-768, transport handshake via Noise_PQ XX (ML-KEM-768 + ML-DSA-65). No classical Diffie-Hellman remains in the protocol layer.
**Wire format (normative, production XX).** The Noise_PQ XX handshake is exactly three messages. Identifiers in formulas below match the reference implementation in `crates/mt-noise-pq/src/xx_handshake.rs`. The libp2p multistream-select protocol identifier for the handshake is **`/montana/noise-pq-xx/1.0.0`** — this string is the authoritative protocol name used to negotiate the upgrade between two peers.
| msg2 (responder → initiator) | 7533 | `ke_pk_r` (1184 B) ‖ `ct_i` (ML-KEM-768 ct to `ke_pk_i`, 1088 B) ‖ `rs_id_pk` (ML-DSA-65 pk, 1952 B) ‖ `sig_r` (ML-DSA-65 sig, 3309 B) |
| msg3 (initiator → responder) | 6349 | `ct_r` (ML-KEM-768 ct to `ke_pk_r`, 1088 B) ‖ `is_id_pk` (1952 B) ‖ `sig_i` (3309 B) |
Transcript hash (input to both `sig_r` and `sig_i`) is the byte concatenation of msg1 plus the msg2-prefix-without-`sig_r` (for `sig_r`), or msg1 ‖ full-msg2 ‖ msg3-prefix-without-`sig_i` (for `sig_i`). Each signature input is domain-separated with `mt-noise-pq-xx-v1-sig-r` and `mt-noise-pq-xx-v1-sig-i` respectively.
Session keys are derived by domain-separated SHA-256 over the concatenation of both ML-KEM-768 shared secrets and the full transcript:
`sk_i_to_r` and `sk_r_to_i` are 32-byte keys for ChaCha20-Poly1305 AEAD; the AEAD-wrapped byte stream is exposed as `mt_noise_pq::stream::NoisePqStream`.
**PeerId derivation (non-libp2p-standard).** PeerId is derived as the SHA-256 multihash (libp2p / IPFS sha2-256 multihash code 0x12) over the raw byte representation of each peer's ML-DSA-65 identity public key. This is **not** the libp2p-standard PeerId derivation, which is a multihash of the protobuf-encoded `PublicKey` message with one of the libp2p built-in key types (`RSA`, `Ed25519`, `Secp256k1`, `ECDSA`). The non-standard derivation is intentional — Montana uses ML-DSA-65 as the cross-network identity per [I-1], which is not one of libp2p's built-in key types, and the protobuf wrapper would add about fifty bytes of overhead without carrying additional security. Two implications follow: (a) a vanilla libp2p node cannot recover the public key from a Montana PeerId, and (b) Montana nodes do not accept libp2p-standard PeerIds because they have no ML-DSA-65 key to verify them with.
**Legacy XK variant** (`/montana/noise-pq/1.0.0`, `crates/mt-noise-pq/src/lib.rs`) is retained for reference and for KAT continuity but is no longer wired into the libp2p transport. Its wire sizes (msg1 2272 B, msg2 6349 B, msg3 5261 B) and its requirement that the initiator know the responder's static KEM pk a priori made it incompatible with libp2p plug-in. XK is the older form documented here for completeness.
where `ss_rs = mlkem_decap(rs_kem_sk, ct_rs)` and `ss_e = mlkem_decap(ke_sk, ct_e)` on the receiving side respectively, and the corresponding `mlkem_encap(rs_kem_pk) → (ct_rs, ss_rs)` and `mlkem_encap(ke_pk) → (ct_e, ss_e)` on the sending side. Implicit-rejection semantics of FIPS 203 §6.3 are reconciled by the identity-signature check: a maliciously substituted ciphertext yields a different shared secret on the receiver, the transcript master diverges from the sender's, and the identity signature check fails (the receiver returns `BadResponderSignature` or `BadInitiatorSignature`).
**Post-handshake AEAD framing.** After the 3-message handshake completes, every application-layer byte stream between the peers is encrypted with ChaCha20-Poly1305 using the derived directional session keys. Wire format per direction:
The 64-bit nonce counter is monotonic per direction and guaranteed not to overflow at any realistic message rate (2^64 frames at 1 Gbit / s with 64 KiB frames ≈ 2^48 years). Implementations MUST abort the connection (treat as protocol error) if the counter would overflow.
Application-layer messages larger than 65 519 bytes are fragmented by the caller (e.g. by libp2p's stream multiplexer); the AEAD layer makes no provision for in-band fragmentation.
**Capability negotiation.** The primary negotiation mechanism is libp2p multistream-select: each peer advertises the set of upgrade protocols it supports (`/montana/noise-pq/1.0.0` for the Noise_PQ handshake defined here, `/noise` or equivalent for the classical fallback) and the connection negotiates the highest mutually supported one. This is standard libp2p convention and requires no additional wire fields.
A 1-byte `pq_transport_version` field is **reserved but not currently consumed** by the IBT advertisement layout — it is held in reserve for a future explicit out-of-band signal if multistream-select proves insufficient (for example, if some operator policy needs to express «I support Noise_PQ but refuse to fall back to classical»). Values when the field becomes active:
**Cross-implementation conformance.** Reference test vectors in `crates/mt-noise-pq/tests/kat.rs` fix the responder's static ML-KEM-768 keypair seed (`byte_repeat(0x42, 64)`) and the two ML-DSA-65 identity seeds (`byte_repeat(0x77, 32)` for responder, `byte_repeat(0xAA, 32)` for initiator). The 3-message handshake on those fixed inputs must produce byte-identical `sk_i_to_r`, `sk_r_to_i`, and `transcript_hash` across implementations. Per-message wire bytes will differ run to run because the encapsulation step uses fresh OS randomness per FIPS 203 §6.2; only the derived session material is byte-exact deterministic for cross-implementation verification.
The Noise_PQ XX handshake and its uniform-framing layer define the inner transport: the post-quantum session and the DPI-resistant frame stream are invariant across every deployment. The outer carrier — the byte pattern a passive observer sees before the inner handshake begins — is a negotiable profile. A node advertises the profiles it supports; a connecting peer selects the highest-preference profile its local network path permits, established empirically through the reachability-sensing mechanism below.
| Profile | Outer carrier | Observable to a DPI engine | Applicability |
| T0 | none — Noise_PQ XX directly over TCP/8444 | a TCP stream of uniform 1024 B frames | default; lowest latency |
| T1 | TLS 1.3 mimicry — the inner bytes carried inside a TLS record stream whose ClientHello and SNI reproduce a chosen cover host | a TLS 1.3 session to an ordinary HTTPS host | a path that passes TLS but filters raw high ports |
| T2 | HTTP/WebSocket over TLS through a content-delivery network | a TLS session to a large CDN endpoint | a path that filters by destination IP — the CDN address set carries unrelated tenants |
| T3 | a general-purpose pluggable-transport tunnel that carries the inner stream over an external circuit, so the node's own address is absent from the carrier the censor observes | the cover traffic of the chosen pluggable transport | a path performing both address and protocol filtering |
| T4 | mesh radio — BLE / Wi-Fi Direct, see Mesh Transport | radio advertisements at physical range | a complete internet shutdown |
The inner Noise_PQ XX session, its identity authentication through IBT, and the uniform-framing obfuscation are identical under every profile; a profile changes only the outer carrier, never the post-quantum confidentiality or the identity binding. T2 and T3 introduce an external dependency — a CDN tenant, a pluggable-transport bridge — which an operator enables by deliberate configuration; each dependency carries its operational trust note in the Threat Model. Profile negotiation reuses the libp2p multistream-select surface and the reserved `pq_transport_version` field, and leaves the inner handshake wire format unchanged.
Transport profiles are local network-stack behaviour on the node's own clock and sit outside the scope of consensus state.
An open entry with a sequential-SHA-256 barrier makes Sybil nodes expensive: each Sybil = τ₂ windows of sequential SHA-256 (not parallelizable) + a selection event. Peer selection uses diversity constraints from protocol-level data (start_window) and network-level data (/16, ASN).
P2P gossip — only registered and invited nodes (IBT levels 1-2, see Transport Obfuscation → Identity-Bound Tunnel). Accounts (IBT level 3) interact through their trusted node.
Selection: random 50/50 from the «new» and «verified» tables. Bucketing with the node's secret key. No preference by chain_length — selection is uniform.
Every outgoing connection is checked against all four constraints:
```
Network:
/16 — at most 1 outgoing per /16 subnet (IPv4) or /48 (IPv6)
ASN — at most 2 outgoing per autonomous system
Protocol:
start_window — at most 2 outgoing to nodes with start_window inside one τ₂
```
Network constraints: /16 and ASN diversity. The protocol-level constraint start_window is canonically available from the Node Table.
Consequence: a Sybil cluster all registered in one τ₂ → at most 2 of 24 slots. An eclipse requires nodes in 7+ different ASes in 7+ different /16s with registrations in 7+ different τ₂.
The ASN map is loaded at startup. Without the map — fallback to /16.
#### Address manager
Two tables:
- **New** — addresses received via peer exchange and DHT. The node has not yet connected
- **Verified** — addresses to which the node has successfully connected through IBT
Bucketing: `bucket = Hash(secret_key, source_group, addr_group) % N`. Deterministic with the secret key — an attacker cannot predict which bucket their address will fall into.
#### Incoming connections
Up to 32 incoming. On overflow — eviction:
1. Protect 4 with the lowest ping
2. Protect 4 with the most recent useful messages (any valid Montana message the node has not yet seen)
3. Protect up to 8 from different subnets (one from each)
4. Protect 4 with the most recent proposals
5. From the rest — evict from the largest subnet group
#### Anchors
The 2 outgoing connections with the longest uptime are saved every τ₂. On restart after a crash or upgrade — connect to the anchors first, before any random selection from the tables.
Once every 10 τ₁: connect to a random address from «new», perform the IBT handshake (all three verification levels). Success at any level → move to «verified» with the level tag (node / invited / account). Failure → mark or remove.
By behaviour: if a peer has not relayed a single new proposal over τ₂ — replace it. A peer with more than 50% invalid messages in a sliding τ₁ window — disconnect, with a τ₂ reconnection ban. A peer that relays honestly is useful to the network and stays.
Without node_id and node_pubkey the client cannot compute an IBT proof to connect. Peer exchange: at most 100 PeerRecord per message. At most 1 peer-exchange message per τ₁ from each peer.
When a reachability map is available for the node's own vantage (see «Reachability sensing and auto-steering» below), candidate entry points are additionally ranked by their measured reachable fraction on a supported transport profile. The random 50/50 draw between the «new» and «verified» tables, the four diversity constraints, and the absence of chain_length preference are unchanged — reachability ranking selects among candidates that already satisfy those constraints, and the local feeler probe remains authoritative over the map.
Genesis: 12 hard-coded bootstrap nodes `(IP, node_id, pubkey)`. If all 12 IPs are blocked at the country level — a new node cannot enter the network. Five independent discovery channels. One of the five is enough.
**1. Peer exchange.** Every node keeps and forwards a list of active peers to newcomers. Knowing the IP of a single node is enough — a friend, a QR code, a messenger. One live contact = entry to the network.
**2. DHT.** A Kademlia DHT on top of libp2p. Nodes find each other without a central point. Identifiers are randomized — the DHT does not reveal node_id before a Montana connection is established.
**3. Bridge nodes.** Nodes outside the censored jurisdiction, published through out-of-band channels (social networks, messengers, printed QR codes). The IP of a bridge node is unknown to the firewall until it is used.
**4. Encrypted Client Hello (ECH).** Bootstrap via a CDN that supports ECH. The SNI is encrypted — an observer sees the CDN IP but not the target domain. Effective in jurisdictions without active ECH-extension blocking. In jurisdictions that block ECH (China since 2023, Russia since 2024) — this channel is non-functional. For such jurisdictions — channels 1-3 and 5.
**5. Mesh peer exchange.** With no internet access at all (state shutdown, inter-zone connectivity loss, local isolation), the node discovers local peers via mesh transport (Bluetooth LE advertisement, Wi-Fi Direct service discovery). Peer exchange runs at the mesh frame level with `frame_type = 0 (discovery)` — see the «Mesh Transport» and «Store-and-Forward Semantics» subsections. The physical discovery radius is tens of metres; mesh multi-hop forwarding extends the effective radius to hundreds of metres and kilometres with sufficient device density. When at least one device in the mesh network gains internet access — the entire chain synchronizes through it as a single gateway.
Redundancy = resilience. The five channels are independent at the physical-delivery layer (IP internet for 1-4, radio mesh for 5). A state-level block of the internet channel does not affect channel 5 — disabling mesh requires physically suppressing Bluetooth / Wi-Fi on every device, which is practically infeasible.
Filtering is applied per access network: a bootstrap address, port, or transport profile reachable from one operator is filtered on another, and the filter set shifts over time. A static entry list is therefore insufficient — a node learns which entry points are reachable from its own vantage and steers toward them. The mesh measures its own reachability and converges on working paths without operator intervention.
#### Reachability observation
A node or client that completes — or fails — a connection attempt records a reachability observation:
```
ReachabilityObservation:
vantage_class coarse network locator of the observer:
(country_code 2B ISO-3166, asn u32). No finer
locator is recorded; the observation does not
identify the individual observer.
target_ref 32B node_id of the attempted peer
profile u8 transport profile attempted (T0..T4)
outcome u8 0 = reachable (handshake completed),
1 = unreachable (timeout / reset / filtered)
observed_window u32 cached window_index at observation time
```
Observations are transport-layer telemetry on the node's local clock. They enter no state root and form no consensus state. They are bounded and ephemeral: a node retains at most `MAX_OBSERVATIONS_PER_VANTAGE = 256` per `(country_code, asn)` pair — mirroring the `used_online_nonces` per-key bound — and prunes any observation older than `7 × τ₁`, the mesh-IBT staleness bound, so the working set is memory-bounded independently of network size.
#### Reachability map
A node aggregates observations into an advisory map `(vantage_class, target_ref, profile) → reachable_fraction`, where `reachable_fraction` is the count of `outcome = 0` over total observations for that triple. The map propagates as `ReachabilityAdvert` records over peer exchange, under the existing peer-exchange rate limit (at most one peer-exchange message per `τ₁` per peer, at most 100 records per message). The map is advisory: it ranks candidate entry points and never authorizes a connection by itself.
```
ReachabilityAdvert:
country_code 2B ISO-3166-1 alpha-2 of the vantage
asn 4B u32, autonomous system of the vantage
target_ref 32B node_id of the observed peer
profile 1B transport profile observed (T0..T4)
reachable_num u16 corroborating observations with outcome = 0
reachable_den u16 total observations for the triple
observed_window u32 cached window_index of the latest observation
```
**Invariants ReachabilityAdvert:**
-`country_code` is two ASCII letters in the ISO-3166-1 alpha-2 set; any other value drops the record.
-`profile ∈ {0, 1, 2, 3, 4}` (T0..T4); any other value drops the record.
-`reachable_den ≥ 1` and `reachable_num ≤ reachable_den`; otherwise the record drops. `reachable_fraction = reachable_num / reachable_den` is a ranking ratio only and forms no consensus state.
-`observed_window` lies within `[known_window_index − 7 × τ₁, known_window_index]`; a staler value drops the record.
- A `(country_code, asn, target_ref, profile)` triple is acted on only when corroborated by records from at least `REACHABILITY_QUORUM = 3` distinct /16 source groups within that `(country_code, asn)` vantage; the /16 is the diversity unit of the outgoing-connection constraints. A single advert ranks candidates and authorizes no connection.
#### Auto-steering
When selecting an entry point a node consults the map for its own `vantage_class`, prefers candidates with the highest `reachable_fraction` on a supported transport profile, and breaks ties by the round-trip latency of a feeler probe. The node then confirms the chosen candidate by its own connection attempt and IBT handshake; the map informs the choice and the local probe is authoritative, mirroring the feeler discipline of peer selection. On loss of a working entry mid-session the node re-steers to the next corroborated candidate; one active entry carries a hot reserve.
#### Quorum and diversity
A single reported observation does not move the map: a `(vantage_class, target_ref, profile)` triple is treated as reachable only when corroborated by observations from at least `REACHABILITY_QUORUM = 3` distinct /16 source groups — the diversity unit of the outgoing-connection constraints. An adversary reporting false reachability from one position therefore cannot steer honest nodes toward a dead or hostile entry: honest nodes require independent corroboration across network positions and ultimately verify by direct IBT probe.
Reachability sensing, the map, and steering are local network-stack behaviour on the node's own clock and sit outside the scope of consensus state.
Montana's P2P gossip retransmits operations through all nodes. Without protection, the first peer knows the sender's IP. Dandelion++ (Fanti et al. 2018) breaks the IP → operation link by modifying the existing gossip.
**Stem routing.** The stem uses only outgoing connections — incoming connections do not participate. Every 693 windows the node re-selects 2 of its 24 outgoing as `stem_peers` (the stem-set selection period). Inside this 693-window window, the `stem_successor` (forward choice between the 2) rotates every τ₁ — see the Dandelion++ Card for the normative wording. All stem operations in the epoch are routed through one of these 2 (chosen by hash(msg)).
| VDF Reveal | Direct gossip (no stem) | node_id is public in the reveal, anonymity is impossible; IP is hidden by Transport Obfuscation (Noise_PQ XX over TCP/8444 with uniform framing) |
Each stem node insures the next. The τ₁/2 timer runs independently at every hop. If the next hop dropped the message — the current hop notices the operation's absence in gossip and performs fluff itself. Maximum latency = τ₁/2 (one hop), not cumulative.
Dandelion++ requires no external infrastructure. Every Montana node already is a relay — gossip exists; stem adds 2-3 hops in front of it. Latency overhead: milliseconds.
A personal network works when everyone can join. Most home users sit behind NAT — invisible to incoming connections. Without NAT traversal a personal internet = a server club.
**2. DCUtR (hole punching).** Two NAT'd nodes coordinate through a third node with a public IP. Both send outgoing packets — the routers open «holes» for the replies. After coordination — direct connection. Success: 60-70% of cases (TCP). Carrier-grade NAT (mobile operators): ~30%.
**3. Circuit Relay v2 (transit).** If hole punching fails — traffic flows through an outbound peer with a public IP. Relay is not a separate mechanism and not a dedicated server. A relay connection = an ordinary outgoing connection, subject to the same rules: uniform framing, diversity constraints, behavioural rotation. The content is end-to-end encrypted (Noise) — the relay sees participants' IPs but not the content. Metadata is spread across 24 outbound peers from different /16s and ASNs — no single relay sees the full graph.
**Relay limits:** up to 32 concurrent relay connections per node, bandwidth per relay ≤ baseline frame rate (1 KB/s). 32 × 1 KB/s = 32 KB/s ≈ 82 GB/month — acceptable for a home node with a public IP.
**Obligation.** Nodes with public IPs support relay — a personal network works when everyone can join. The reference implementation enables relay when a public IP is detected. Feeler connections check relay support on peers; nodes without relay are marked `no-relay` in the address manager. NAT'd nodes prefer relay-capable peers when picking outgoing connections.
Internet is not always available. State shutdowns (Iran 2019 — a week, Belarus 2020 — days, Myanmar 2021 — months), local outages, isolated zones. Montana keeps working under these conditions through mesh transport over Bluetooth Low Energy and Wi-Fi Direct — devices discover each other in physical radius and forward encrypted Montana messages hop by hop. Mesh does not replace internet transport, it complements it: when connectivity returns, the network automatically converges through a mesh-internet gateway.
**Mesh transport is orthogonal to consensus**, just like internet transport ([the section above](#network-layer)) — the state machine works over any delivery channel without changes.
-`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.
На целевом масштабе сети до ~1B активных аккаунтов (архитектурная цель; один только `AccountRecord` state ≈2.06 TB, fast-sync benchmarks остаются M7 gate) клиент собирает passively-observed pool активных аккаунтов через gossip proposals. Realistic pool size: 10K–100K накопленных за τ₂ observation window.
- **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 механизмы.
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`.
Обязательная нормативная секция для сетевого слоя. Перечисляет 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 (×τ₂ окон 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 узла отправителя |
| **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). |
**Reachability-map poisoning (Censor / Sybil).** A hostile node reports false `ReachabilityObservation` records, claiming a dead or attacker-controlled entry is reachable, to steer honest nodes onto it (connectivity eclipse through steering). Closed by construction: a `(vantage_class, target_ref, profile)` triple is treated as reachable only when corroborated by observations from at least `REACHABILITY_QUORUM = 3` distinct /16 source groups — the diversity unit of the outgoing-connection constraints — and the final decision rests with the local IBT probe, not the map. The map is advisory: poisoning reorders candidate ranking and authorizes no connection. Status: **C**.
**Transport profile external dependencies (Censor).** Profiles T2 and T3 introduce an external carrier the node does not control. T2 routes through a content-delivery network: the provider can revoke the operator's tenant account and withdraw the carrier without notice; the mitigation is the availability of several CDN providers and fallback down the ladder to T1/T0. T3 routes through a pluggable-transport bridge: the bridge address can be enumerated and blocked by the censor, and the bridge operator is a trust party for carrier liveness — never for content, since the inner Noise_PQ XX session is end-to-end; the mitigation is bridge rotation through out-of-band distribution (Censorship-resistant discovery, channel 3) and fallback down the ladder. Neither profile weakens the inner post-quantum confidentiality or the identity binding; the dependency is on carrier availability, not on confidentiality. Status: **P** — availability rests on an external party, confidentiality is closed by construction.
Эти угрозы **не закрываются** сетевым слоем Монтаны 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** — статус: **закрыто**. 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.
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.
| 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 возникают.
Внешний 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)