348 lines
17 KiB
Markdown
348 lines
17 KiB
Markdown
|
|
# Spec Patch: opcode `0x06 VpnHeartbeat`
|
|||
|
|
|
|||
|
|
**Целевая версия спеки:** Montana Protocol v35.25.1 → **v35.26.0**
|
|||
|
|
**Дата проекта патча:** 2026-05-19
|
|||
|
|
**Применимый раздел:** §11 «Account operations», §13 «apply_proposal»
|
|||
|
|
**Зависимые инварианты:** [I-1] PQ-secure, [I-3] Determinism, [I-9] Bit-exact arithmetic, [I-13] Deflationary sink, [I-14] State lifecycle
|
|||
|
|
**Связанные роли:** `Protocol/CLAUDE.md` v4.30.0+ (архитектор), `Protocol/CRITIC.md` v3.13.0+ (критик)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Намерение патча
|
|||
|
|
|
|||
|
|
Закрытие глобальной слабости в текущей реализации Montana App + VPN-balance coordinator:
|
|||
|
|
|
|||
|
|
1. **Heartbeat начисление Ɉ происходит вне консенсуса.** Текущий Rust coordinator на узле Moscow ведёт `state.json` авторитарно, не реплицируется через TimeChain, не cemented. F-4 из аудит-пакета `Android/Внешний-аудит/07-Известные-ограничения.md`.
|
|||
|
|
|
|||
|
|
2. **Sybil-resistance работает только через Ed25519 TOFU pinning.** Это нарушает [I-1] PQ-secure (Shor на curve25519), хотя сейчас acceptable как Phase 2 stopgap. CF-Phase2-1.
|
|||
|
|
|
|||
|
|
3. **Migration к полному консенсусу требует нового opcode и operation type.**
|
|||
|
|
|
|||
|
|
Этот патч описывает opcode `0x06 VpnHeartbeat` — нормативно. После принятия:
|
|||
|
|
- `mt-account` реализует `apply_vpn_heartbeat`
|
|||
|
|
- `montana-node` принимает в mempool, cement через BundledConfirmation
|
|||
|
|
- AccountRecord.balance обновляется через canonical apply pipeline
|
|||
|
|
- Координатор Moscow становится **indexer** для быстрого чтения, не source-of-truth
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §1. Type byte registry — расширение
|
|||
|
|
|
|||
|
|
Текущее распределение (Protocol v35.25.0 §11.1):
|
|||
|
|
|
|||
|
|
| Byte | Operation | Класс |
|
|||
|
|
|------|-----------|-------|
|
|||
|
|
| `0x01` | _reserved (anchor-fee sponsor)_ | — |
|
|||
|
|
| `0x02` | Transfer | value |
|
|||
|
|
| `0x03` | ChangeKey | power |
|
|||
|
|
| `0x04` | Anchor | value |
|
|||
|
|
| `0x05` | NicknameBid | value (allocation) |
|
|||
|
|
|
|||
|
|
После v35.26.0:
|
|||
|
|
|
|||
|
|
| Byte | Operation | Класс |
|
|||
|
|
|------|-----------|-------|
|
|||
|
|
| `0x01` | _reserved_ | — |
|
|||
|
|
| `0x02` | Transfer | value |
|
|||
|
|
| `0x03` | ChangeKey | power |
|
|||
|
|
| `0x04` | Anchor | value |
|
|||
|
|
| `0x05` | NicknameBid | value (allocation) |
|
|||
|
|
| **`0x06`** | **`VpnHeartbeat`** | **value (operator-credit)** |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §2. Operation layout
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
operation VpnHeartbeat ::= {
|
|||
|
|
type_byte: u8 = 0x06
|
|||
|
|
sender_account_id: AccountId (20B) // адрес владельца кошелька-кредитуемого
|
|||
|
|
window_index: u64 (8B LE) // окно консенсуса в котором heartbeat валиден
|
|||
|
|
exit_node_id: NodeId (32B) // public key валидатора-exit (узел VPN-каскада)
|
|||
|
|
duration_ms: u32 (4B LE) // intervalo миллисекунд с предыдущего heartbeat
|
|||
|
|
operator_signature: Signature (666B) // FN-DSA-512 подпись sender-а над canonical_preimage
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Размер:** `1 + 20 + 8 + 32 + 4 + 666 = 731 bytes`.
|
|||
|
|
|
|||
|
|
### §2.1 Canonical preimage для подписи
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
domain_separator = ASCII("mt-vpn-heartbeat-v1") (19B)
|
|||
|
|
preimage = domain_separator
|
|||
|
|
|| sender_account_id (20B)
|
|||
|
|
|| window_index_le8 (8B)
|
|||
|
|
|| exit_node_id (32B)
|
|||
|
|
|| duration_ms_le4 (4B)
|
|||
|
|
preimage_length = 19 + 20 + 8 + 32 + 4 = 83 bytes
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`operator_signature = FN-DSA-512.Sign(sender_secret_key, preimage)`.
|
|||
|
|
|
|||
|
|
### §2.2 Инварианты VpnHeartbeat
|
|||
|
|
|
|||
|
|
1. **VH-1 type byte:** `type_byte == 0x06`.
|
|||
|
|
2. **VH-2 sender exists:** `AccountTable[sender_account_id]` существует и `account_chain_length ≥ 1`.
|
|||
|
|
3. **VH-3 window bounds:** `current_window − 1 ≤ window_index ≤ current_window` (heartbeat принимается только в текущем или предыдущем окне).
|
|||
|
|
4. **VH-4 exit-node validity:** `NodeTable[exit_node_id]` существует и имеет `is_active = true` и `node_role ∈ {exit_node, dual_role}`.
|
|||
|
|
5. **VH-5 signature verify:** `FN-DSA-512.Verify(AccountTable[sender_account_id].suite_pubkey, preimage, operator_signature) == true`.
|
|||
|
|
6. **VH-6 duration upper bound:** `duration_ms ≤ MAX_HEARTBEAT_DURATION_MS = 30 000` ms (защита от backdate flooding).
|
|||
|
|
7. **VH-7 duration lower bound:** `duration_ms ≥ MIN_HEARTBEAT_DURATION_MS = 4 000` ms (защита от heartbeat spam внутри окна).
|
|||
|
|
8. **VH-8 monotonic nonce:** `duration_ms` накопляется через `AccountTable[sender].vpn_credited_seconds_in_window[window_index]` — heartbeat принимается только если суммарная сумма за окно ≤ `WINDOW_DURATION_MS = τ₁`.
|
|||
|
|
9. **VH-9 exit-node cooldown:** `NodeTable[exit_node_id].last_heartbeat_processed_window ≥ window_index − 2` (защита от использования давно-offline exit-узлов).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §3. apply_vpn_heartbeat
|
|||
|
|
|
|||
|
|
### §3.1 Шаги применения
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
apply_vpn_heartbeat(state, op):
|
|||
|
|
// Шаг 1. Валидация инвариантов VH-1..VH-9.
|
|||
|
|
require all VH-1..VH-9 pass
|
|||
|
|
|
|||
|
|
// Шаг 2. Вычисление credit.
|
|||
|
|
rate_nj_per_ms = RATE_NJ_PER_MILLISECOND = 1 // 0.001 Ɉ/sec = 1 nɈ/ms
|
|||
|
|
credit_nj = op.duration_ms × rate_nj_per_ms // integer arithmetic per [I-9]
|
|||
|
|
|
|||
|
|
// Шаг 3. State mutation:
|
|||
|
|
state.account_table[op.sender_account_id].balance_nj += credit_nj
|
|||
|
|
state.account_table[op.sender_account_id].vpn_credited_seconds_x1000_in_window[op.window_index] += op.duration_ms
|
|||
|
|
state.node_table[op.exit_node_id].vpn_traffic_served_seconds += op.duration_ms / 1000
|
|||
|
|
|
|||
|
|
// Шаг 4. Emission supply update:
|
|||
|
|
state.supply_nj += credit_nj // эмиссия Ɉ за VPN-работу
|
|||
|
|
|
|||
|
|
// Шаг 5. Chain length:
|
|||
|
|
state.account_table[op.sender_account_id].account_chain_length += 1
|
|||
|
|
state.account_table[op.sender_account_id].frontier_hash = H_canon(op)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### §3.2 Возможные результаты
|
|||
|
|
|
|||
|
|
- **OK:** state mutated as above.
|
|||
|
|
- **Err::AccountNotFound:** инвариант VH-2 нарушен.
|
|||
|
|
- **Err::SignatureInvalid:** VH-5.
|
|||
|
|
- **Err::WindowOutOfBounds:** VH-3.
|
|||
|
|
- **Err::ExitNodeOffline:** VH-4 / VH-9.
|
|||
|
|
- **Err::DurationOutOfBounds:** VH-6 / VH-7.
|
|||
|
|
- **Err::WindowQuotaExceeded:** VH-8.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §4. Эмиссия Ɉ за VPN — экономический анализ
|
|||
|
|
|
|||
|
|
### §4.1 Параметры
|
|||
|
|
|
|||
|
|
| Параметр | Значение | Обоснование |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `RATE_NJ_PER_MILLISECOND` | `1 nɈ/ms` (= `0.001 Ɉ/sec`) | Соответствует текущему MVP-coordinator. Закрепляется как initial baseline. |
|
|||
|
|
| `MIN_HEARTBEAT_DURATION_MS` | `4 000 ms` | Rate-limit per identity per heartbeat (см. Pass 18 critic). |
|
|||
|
|
| `MAX_HEARTBEAT_DURATION_MS` | `30 000 ms` | Защита от backdate flooding. |
|
|||
|
|
| `WINDOW_DURATION_MS` | `τ₁ = 30 000 ms` (один TimeChain window) | Используется как cap quota per window. |
|
|||
|
|
| `MAX_HEARTBEATS_PER_WINDOW_PER_SENDER` | `WINDOW_DURATION_MS / MIN_HEARTBEAT_DURATION_MS = 7` | Pre-mainnet, может быть скорректировано. |
|
|||
|
|
|
|||
|
|
### §4.2 Maximum emission rate per validator
|
|||
|
|
|
|||
|
|
Допущения: один honest sender накапливает `WINDOW_DURATION_MS = 30 000 ms = 30 sec` credit per τ₁.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
emission_per_sender_per_τ₁ = 30 000 nɈ = 0.03 Ɉ
|
|||
|
|
emission_per_sender_per_day = 0.03 × (86400/30) = 86.4 Ɉ/day
|
|||
|
|
emission_per_sender_per_year = 31536 Ɉ/year (theoretical maximum)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### §4.3 Sybil-attack defense
|
|||
|
|
|
|||
|
|
Sybil-attacker создаёт N fake accounts. Каждый требует:
|
|||
|
|
- Открытие AccountRecord через первый Transfer (закрытие через [I-14] cost barrier — отдельный механизм)
|
|||
|
|
- FN-DSA-512 signature на каждый heartbeat (computational cost не критичный, hardware-asymmetry attack)
|
|||
|
|
- Активный VPN session на exit-узле (network bandwidth cost > VPN credit при честных rate)
|
|||
|
|
|
|||
|
|
**Деривация:** atacker экономика выгодна только если `revenue_per_account_per_day > cost_of_VPN_bandwidth_per_account_per_day`. Учитывая что VPN-bandwidth сам стоит более чем 0.001 Ɉ/sec (текущий TC price + cloud bandwidth pricing), attack экономически нерентабельна.
|
|||
|
|
|
|||
|
|
### §4.4 Соответствие [I-13] Deflationary sink
|
|||
|
|
|
|||
|
|
VpnHeartbeat — **value operation**, не burn. Эмиссия Ɉ за работу validator-а покрывается **существующими** механизмами sink ([I-13]):
|
|||
|
|
- NicknameBid burn
|
|||
|
|
- Anchor fee burn
|
|||
|
|
- ChangeKey re-issuance cost
|
|||
|
|
|
|||
|
|
Баланс между emission (VpnHeartbeat) и burn (NicknameBid + Anchor) — open параметр для economic equilibrium (см. Pass 22 critic — equilibrium analysis).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §5. AccountRecord — расширение поля
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
AccountRecord ::= {
|
|||
|
|
... // existing fields per v35.25
|
|||
|
|
vpn_credited_seconds_x1000: u64 // суммарно за всю жизнь аккаунта
|
|||
|
|
vpn_credited_seconds_x1000_in_window: u32 // per current window (reset per τ₁)
|
|||
|
|
vpn_last_window_index: u64 // последнее окно где аккаунт активен
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Дополнительно 24 байта на AccountRecord.**
|
|||
|
|
|
|||
|
|
### §5.1 NodeRecord — расширение поля
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
NodeRecord ::= {
|
|||
|
|
...
|
|||
|
|
vpn_traffic_served_seconds: u64 // суммарно за всю жизнь узла
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Дополнительно 8 байт на NodeRecord.**
|
|||
|
|
|
|||
|
|
### §5.2 [I-14] State lifecycle compliance
|
|||
|
|
|
|||
|
|
`vpn_credited_seconds_x1000_in_window` сбрасывается на каждой границе τ₁. Это **не persistent growth** — поле bounded, переиспользуется. ✅
|
|||
|
|
|
|||
|
|
`vpn_traffic_served_seconds` растёт линейно с активностью узла. Для exit-узлов это **expected behavior** (per [I-14] путь cost-based: NODE_REGISTRATION_STAKE покрывает storage cost для NodeRecord). ✅
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §6. Test vectors (binding для conformance)
|
|||
|
|
|
|||
|
|
### §6.1 Vector V6-1: канонический preimage
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
sender_account_id = 2f8714b236118011647ec51d0ca6ad40d286bec7 (20B)
|
|||
|
|
window_index = 1779120000 (u64 LE)
|
|||
|
|
exit_node_id = b17dd919772d4268a7249b866b92d12b... (32B placeholder)
|
|||
|
|
duration_ms = 5000 (u32 LE)
|
|||
|
|
|
|||
|
|
preimage_hex = 6d742d76706e2d68656172746265 6174 2d 76 31 [domain]
|
|||
|
|
| 2f8714b236118011647ec51d0ca6 ad40 d2 86 bec7 [sender]
|
|||
|
|
| 00ed2bc564010000 [window le8]
|
|||
|
|
| b17dd919772d4268a7249b866b92 d12b ... [exit_id]
|
|||
|
|
| 88130000 [duration le4]
|
|||
|
|
preimage_size = 83 bytes
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
(Конкретный hex значения вычисляются при первом deploy implementation — это placeholder.)
|
|||
|
|
|
|||
|
|
### §6.2 Vector V6-2: balance delta integer arithmetic
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
duration_ms = 5000 ms
|
|||
|
|
rate_nj_per_ms = 1
|
|||
|
|
credit_nj = 5000
|
|||
|
|
|
|||
|
|
Pre-state: account.balance_nj = 0
|
|||
|
|
Post-state: account.balance_nj = 5000
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### §6.3 Vector V6-3: rejected — duration слишком большая
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
duration_ms = 35000
|
|||
|
|
Expected: Err::DurationOutOfBounds (VH-6 violation, 35000 > MAX = 30000)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §7. Migration path Phase 2 → Phase 3
|
|||
|
|
|
|||
|
|
### §7.1 Координатор Moscow остаётся
|
|||
|
|
|
|||
|
|
После принятия opcode `0x06` координатор `mt-vpn-balance.service` продолжает работать как **indexer**:
|
|||
|
|
- Принимает heartbeat от Android клиента через legacy REST API (Ed25519 signed).
|
|||
|
|
- Транслирует в `VpnHeartbeat` opcode → sends в локальный montana-node mempool.
|
|||
|
|
- Опционально: возвращает клиенту transaction hash как proof что operation попала в mempool.
|
|||
|
|
|
|||
|
|
### §7.2 Android client изменения
|
|||
|
|
|
|||
|
|
После принятия opcode:
|
|||
|
|
- BIP39 seed → FN-DSA-512 keypair (вместо Ed25519). Требует Falcon JNI bridge.
|
|||
|
|
- Heartbeat body расширяется: добавляется `signature` (FN-DSA-512, 666B) и поля `window_index`, `exit_node_id`, `duration_ms`.
|
|||
|
|
- Координатор Moscow становится транспортом, не authority.
|
|||
|
|
|
|||
|
|
### §7.3 Backwards-compatible переходный период
|
|||
|
|
|
|||
|
|
В переходный период:
|
|||
|
|
- Координатор принимает **обе** формы heartbeat: legacy (Ed25519) и новую (FN-DSA-512).
|
|||
|
|
- Legacy heartbeats не cemented в TimeChain — only credited через координатор.
|
|||
|
|
- Pre-mainnet принцип: при запуске mainnet legacy heartbeats **прекращают** работать, требуется update приложения.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §8. Adversarial gates check (Gate 0..15)
|
|||
|
|
|
|||
|
|
### Gate 0 — Global invariants
|
|||
|
|
|
|||
|
|
- [I-1] PQ-secure: ✅ FN-DSA-512 (Falcon, lattice-based, NIST PQ winner)
|
|||
|
|
- [I-2] Public financial layer: ✅ balance + duration_ms public
|
|||
|
|
- [I-3] Determinism: ✅ integer arithmetic only, no floats
|
|||
|
|
- [I-4] TimeChain independence: ✅ VpnHeartbeat зависит от TimeChain (window_index), не наоборот
|
|||
|
|
- [I-5] Commodity hardware: ✅ FN-DSA-512 на ARM64 ~5-10ms sign, приемлемо
|
|||
|
|
- [I-6] Regulatory compat: ✅ public balance, no privacy mixer
|
|||
|
|
- [I-7] Minimal crypto surface: ✅ переиспользует FN-DSA-512 уже в спеке
|
|||
|
|
- [I-8] Network-bound unpredictability: **N/A** (VpnHeartbeat не использует seed/lottery)
|
|||
|
|
- [I-9] Bit-exact arithmetic: ✅ integer-only, test vectors §6
|
|||
|
|
- [I-13] Deflationary sink: ✅ эмиссия покрывается existing burn механизмами
|
|||
|
|
- [I-14] State lifecycle: ✅ window-window поле сбрасывается, persistent поля cost-bounded
|
|||
|
|
|
|||
|
|
### Gate 1 — Control plane separation
|
|||
|
|
|
|||
|
|
`VpnHeartbeat` — value operation (двигает balance, эмиссию). Не power. ✅
|
|||
|
|
|
|||
|
|
### Gate 2 — Temporal anchor audit
|
|||
|
|
|
|||
|
|
`window_index` ограничен `current_window ± 1` (VH-3). Нет precompute attack. ✅
|
|||
|
|
|
|||
|
|
### Gate 9 — Expiry math
|
|||
|
|
|
|||
|
|
Heartbeat не имеет expiry. Quota window-based (VH-8). ✅
|
|||
|
|
|
|||
|
|
### Gate 10 — Hardware asymmetry
|
|||
|
|
|
|||
|
|
VpnHeartbeat не использует canonical seed → grinding-attack не применим. ✅
|
|||
|
|
|
|||
|
|
### Gate 13 — Invariant enumeration
|
|||
|
|
|
|||
|
|
§2.2 содержит exhaustive list VH-1..VH-9. ✅
|
|||
|
|
|
|||
|
|
### Gate 14 — State lifecycle
|
|||
|
|
|
|||
|
|
§5.2 — все persistent поля либо bounded, либо cost-protected. ✅
|
|||
|
|
|
|||
|
|
### Gate 15 — Post-edit completeness
|
|||
|
|
|
|||
|
|
При принятии патча требуется:
|
|||
|
|
- Удалить упоминания координатора Moscow как authoritative в Android `Внешний-аудит/05-Состояние-и-хранилище.md`
|
|||
|
|
- Обновить mt-account реализацию: добавить apply_vpn_heartbeat
|
|||
|
|
- Обновить mt-conformance test vectors
|
|||
|
|
- Bump VERSION.md и Code/VERSION.md
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §9. Roadmap implementation
|
|||
|
|
|
|||
|
|
| Этап | Длительность | Описание |
|
|||
|
|
|------|--------------|----------|
|
|||
|
|
| **M-VPN-2 Phase A** | 1 week | Spec patch finalized + critic 22-pass audit |
|
|||
|
|
| **M-VPN-2 Phase B** | 2 weeks | `mt-account::apply_vpn_heartbeat` + 50+ unit tests |
|
|||
|
|
| **M-VPN-2 Phase C** | 1 week | `montana-node` accepts opcode в mempool + cemented |
|
|||
|
|
| **M-VPN-2 Phase D** | 1 week | `mt-conformance` test vectors из §6 |
|
|||
|
|
| **M-VPN-3 Phase A** | 1 week | Android pqcrypto-falcon JNI bridge research + prototype |
|
|||
|
|
| **M-VPN-3 Phase B** | 2 weeks | Production Android FN-DSA-512 integration |
|
|||
|
|
| **M-VPN-3 Phase C** | 1 week | Coordinator Moscow translates legacy → opcode |
|
|||
|
|
| **M-VPN-4** | 1 week | Migration cutover (legacy heartbeats deprecated) |
|
|||
|
|
|
|||
|
|
**Total:** ~9 weeks. Зависит от availability `pqcrypto-falcon` Android bindings.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §10. Status
|
|||
|
|
|
|||
|
|
**Spec patch:** draft pending critic review.
|
|||
|
|
**Implementation:** **TODO** (M-VPN-2/3 milestones not started).
|
|||
|
|
**Acknowledged dependency:** Falcon-512 Android JNI is non-trivial — alternative migration path = SLH-DSA (SPHINCS+) если pqcrypto-falcon не пригоден.
|
|||
|
|
|
|||
|
|
После принятия патча архитектором + критиком — bump спеки v35.25.0 → v35.26.0 с заменой §11.1 type byte registry и добавлением §11.2-VpnHeartbeat.
|