montana/Монтана-Протокол/Архив/Montana v25.0.0.md

218 KiB
Raw Blame History

Montana — Спецификация протокола

Версия: 25.0.0 (2026-04-11)

Определение

Montana — цифровой стандарт времени как реляционная структура. Сеть независимых VDF-осцилляторов, конституирующих каноническую последовательность событий через последовательное хэширование и консенсус между узлами. Montana производит каноническую структуру отношений между хэш-событиями, индексированных window_index. Каждое канонически зарегистрированное окно = 60 Ɉ (TimeCoin).

Montana Time — реляционная структура, конституируемая последовательным хэшированием в рамках VDF и канонической упорядоченностью, устанавливаемой консенсусом между узлами. Внутри этой структуры время в протоколе существует как последовательность канонических событий.

Montana — самодостаточная система отсчёта: каноническая последовательность событий, которую внешние системы могут наблюдать и использовать как reference frame для своих нужд.

Основная функция — каноническая временная координата (window_index). Вторичная — передача ценности.

Консенсус: Proof of Time (PoT) — четыре цепочки. TimeChain: глобальная каноническая цепь (D последовательных SHA-256 = одно окно). NodeChain: персональная цепочка узла (доказательство присутствия в каждом окне). AccountChain: счётчик окон активности аккаунта. AccountTable: состояние счёта. Влияние узла = длина его NodeChain. Протокол и есть структура отношений между событиями, оцифрованная и криптографически верифицируемая.

Genesis: symbolic window 0. Перевод window_index в любые внешние time scales является задачей клиентского слоя.

Генезис-фраза: «Кто контролирует прошлое, контролирует будущее. Кто контролирует настоящее, контролирует прошлое.» — Оруэлл, 1984

Протокол не имеет on-chain governance. Эволюция протокола проходит через Bitcoin-style процесс: открытые предложения (MIPs — Montana Improvement Proposals) публикуются в Content Layer как рекомендации, реализации выпускают новые версии, операторы узлов выбирают какую версию запускать. Fork resolution детерминирован через chain_length большинство. См. раздел «Эволюция протокола».


Четыре решённые проблемы

1. Каноническая временная координата

Проблема. Существующие системы измерения времени (NTP, GPS, PTP) измеряют физическое время через доверенную инфраструктуру. Компрометация сервера NTP или отключение спутника GPS нарушает временную шкалу для всех зависимых систем. Использование таких систем в консенсусе протокола создаёт subjective input в consensus state.

Решение. Реляционная временная структура — сеть независимых VDF-осцилляторов, производящая каноническую последовательность событий через собственную работу. Каждый узел вычисляет цепочку событий автономно через последовательное SHA-256 хэширование. Результат детерминирован и верифицируем любым участником из canonical inputs.

Свойства. Montana Time обладает четырьмя свойствами:

  • Монотонность. window_index строго возрастает. VDF последователен — каждый хэш зависит от предыдущего. Канонический порядок событий однозначен.
  • Детерминизм. Все честные узлы согласны bit-exact на структуру событий — window_index, T_r, state_root. Каждое поле consensus state объективно вычислимо всеми узлами.
  • Верифицируемость. Любой может пересчитать VDF и проверить каждое событие последовательности.
  • Независимость. Каждый узел считает самостоятельно, опираясь только на canonical inputs протокола.

Montana и NTP/GPS/PTP — системы разных типов. NTP и GPS измеряют физическое время через внешние источники. Montana производит каноническую последовательность событий через собственную работу VDF и consensus.

2. Неплутократический консенсус

Проблема. В Proof of Work влияние пропорционально вычислительному бюджету. В Proof of Stake — капиталу. В обоих случаях безопасность сети является функцией концентрации ресурсов, приобретаемых на рынке.

Решение. Proof of Time — механизм консенсуса, в котором влияние узла определяется исключительно длительностью его непрерывного присутствия в сети, измеренной в подписанных временных окнах. Вес узла = длина его NodeChain (количество окон, в которых узел криптографически доказал своё присутствие).

Свойства.

  • Время — единственный ресурс, который нельзя приобрести, передать, делегировать или сконцентрировать
  • Два участника, запустившие узлы одновременно, имеют равный вес независимо от капитала
  • Стоимость атаки на консенсус выражается не в валюте, а во времени, и растёт линейно с возрастом сети

3. Window-based эмиссия

Проблема. Денежная политика фиатных валют определяется решениями комитетов и непредсказуема. Денежная политика Bitcoin предсказуема, но дефляционна — фиксированный потолок supply создаёт ожидание роста цены и подавляет использование как средства обмена.

Решение. Window-based эмиссия — денежная политика, в которой количество новых единиц за одно каноническое окно фиксировано и неизменно на всём горизонте существования протокола. Одно окно Montana Time порождает 60 единиц TimeCoin.

Свойства.

  • Supply после окна W = 60 × (W + 1) Ɉ
  • Эмиссия линейна по window_index — инфляция монотонно убывает и асимптотически стремится к нулю
  • Эмиссия не контролируется ни одним участником, комитетом или голосованием
  • Денежная политика полностью определена единственной константой (60 Ɉ за окно) и не может быть изменена после генезиса
  • Физическая скорость выпуска в SI-секундах определяется скоростью hardware сети и остаётся свойством клиентского слоя, вне scope консенсуса

4. Эволюция без on-chain governance

Проблема. On-chain governance — голосования, советы, формальные процедуры изменения правил внутри протокола — вводит subjective компоненты в consensus state и создаёт постоянную атакуемую поверхность. Любая структура которая голосует становится мишенью: компрометация моделей, подкуп участников, юрисдикционное давление. Чем формальнее governance, тем чётче определена цель атаки.

Решение. Bitcoin-style эволюция. Протокол не имеет on-chain governance вообще. Изменения протокола публикуются как открытые предложения (MIPs — Montana Improvement Proposals) в Content Layer. Реализации (узловое ПО) выпускают новые версии с реализованными MIPs. Операторы узлов сами выбирают какую версию запускать. Fork resolution полностью детерминирован: при расхождении правил сеть разделяется на цепочки, каждая со своим chain_length, и узлы следуют за той цепочкой которая длиннее по их собственным правилам валидации. Никаких голосований внутри консенсуса. Никаких советов с правом подписи изменений. Никаких полей governance в state.

Свойства.

  • Consensus state не содержит ни одного subjective поля связанного с governance — нет attack surface
  • Последнее слово реально у узлов: оператор каждого узла самостоятельно решает что запускать, без посредников
  • MIPs существуют как тексты в Content Layer (anchor + контент), любой может опубликовать, любой может верифицировать авторство и timestamp
  • Экспертные советы (AI Council, Core Council) допустимы как advisory — публикуют рекомендации, обзоры, анализ безопасности; их подписи не имеют binding эффекта на consensus
  • Захват advisory совета не даёт контроля над протоколом — он даёт только возможность опубликовать рекомендацию, которую узлы могут проигнорировать
  • История эволюции навсегда в Content Layer через anchor — каждый MIP и каждое обсуждение timestamped в TimeChain

Следствие: цифровой reference frame времени без человека-посредника

Четыре решённые проблемы порождают уникальную возможность. Любой документ, событие, состояние может быть записано в Montana с математически доказуемой привязкой к канонической позиции в последовательности событий (window_index). Anchor — 32 байта, навсегда. Ни одна существующая система не предоставляет координату времени, которая одновременно децентрализована, неплутократична, привязана к детерминированной денежной политике, свободна от on-chain governance и не зависит от внешних физических эталонов. Montana — не блокчейн с функцией timestamping. Montana — reference frame времени с функцией передачи ценности. Внешние системы могут наблюдать последовательность окон Montana и строить собственные переводы в свои локальные стандарты — этот перевод является задачей наблюдателя, не протокола.

Bitcoin убрал доверие к деньгам — Montana следует тому же принципу для всех слоёв. Ни один человек не может в одиночку изменить протокол. Ни одна группа разработчиков. Ни одна корпорация. Ни один совет. Изменения существуют только как открытые предложения и реализации, которые операторы узлов выбирают запускать или нет.


Глобальные инварианты протокола

Глобальный инвариант — свойство, которое протокол обязан сохранять во всех своих компонентах. Нарушение в одной части = нарушение во всём протоколе. Глобальные инварианты не имеют исключений и не подлежат локальному trade-off.

[I-1] Постквантовая безопасность. Все криптографические примитивы устойчивы к квантовому компьютеру. Допустимо: SHA-256 (Grover ослабляет до 128-bit, приемлемо), FN-DSA-512 (Falcon, lattice), ML-KEM (Kyber), ML-DSA (Dilithium), STARK (hash-based ZK), lattice commitments. Запрещено: ECDLP, RSA, классический Diffie-Hellman, Pedersen commitments на эллиптических кривых, Bulletproofs, Schnorr/EdDSA.

[I-2] Открытость финансового слоя. Балансы, суммы переводов, отправители, получатели — публичны. Приватность данных приложения — через Anchor (хэш в сети, контент у владельца зашифрованным).

[I-3] Детерминизм consensus state. Любое состояние, входящее в consensus root, объективно вычислимо одинаково всеми узлами.

Corollary I-3.a. Любой механизм, результат которого в consensus state или в protocol-level behavior (mempool prioritization, gossip ordering, fork-choice, peer scoring) зависит от измерения физического мира — астрономического, геофизического, атомного, биологического или любого другого — отклоняется по нарушению I-3. Corollary применяется независимо от точности модели измерения.

[I-4] Независимость TimeChain от Account state. TimeChain продвигается из canonical inputs без зависимости от состояния Account Table. Зависимости однонаправленные: TimeChain → NodeChain → AccountChain → AccountTable.

[I-5] Реализуемость без специализированного оборудования. Все примитивы имеют production-ready open-source реализации, работающие на commodity CPU узла без TEE, без обязательного GPU, без обязательного ASIC.

[I-6] Регуляторная совместимость. Протокол опирается на механизмы, совместимые с FATF/AML/MiCA/ETF. Запрещено: privacy mixers на уровне протокола, anonymous addresses, hidden flows, ring signatures, stealth addresses.

[I-7] Минимальная криптографическая поверхность. Каждый новый примитив требует обоснования закрытием конкретного механизма. Дублирование функциональности через два разных примитива запрещено.

Language firewall

В нормативном тексте спецификации Montana допустимые термины для описания протокольных объектов, счётчиков, периодов или интервалов: window, tick, epoch, cycle — определённые через window counts. Термины физического времени (second, minute, hour, day, week, month, year, tropical, synodic, sidereal) применяются только в advisory контекстах клиентского слоя и при описании внешних систем (Bitcoin block interval, NTP, GPS).


Montana Time

VDF — цифровой осциллятор в собственных единицах. D последовательных SHA-256 = одно окно τ₁ Montana. Число D представляет канонический объём работы, конституирующий единицу Montana Time.

TimeChain — глобальная каноническая цепь, поддерживаемая сетью узлов. Каждый узел вычисляет её независимо через последовательное хэширование. Результат детерминирован bit-exact — одни входные данные дают одну каноническую последовательность.

Токен — каноническая регистрация одного окна Montana Time. Протокол производит канонические окна и регистрирует каждое из них как 60 Ɉ.

Определение Montana Time

montana_time(W) := W

Единственное каноническое определение времени в протоколе. Всё остальное — производные или advisory вычисления клиентского слоя.

Одно окно = D последовательных SHA-256 итераций от предыдущего canonical anchor. D фиксируется в Genesis Decree и может адаптироваться runtime-ом через participation-ratio feedback (см. раздел «Адаптация D»).

Четыре свойства

  • Монотонность. window_index строго возрастает. VDF последователен — каждый хэш зависит от предыдущего.
  • Детерминизм. Все честные узлы согласны bit-exact на window_index, T_r, state_root. Каждое поле consensus state объективно вычислимо всеми узлами.
  • Верифицируемость. Любой может пересчитать VDF и проверить каждое событие последовательности.
  • Независимость. Каждый узел вычисляет канон сам, опираясь только на canonical inputs протокола.

Montana и NTP/GPS/PTP — системы разных типов. NTP и GPS измеряют физическое время через внешние источники. Montana производит каноническую последовательность событий через собственную работу VDF и consensus.

Гранулярность

Атом Montana Time — одна SHA-256 итерация. Окно Montana Time — D атомов. Произвольный интервал — N окон. Все три уровня выражены в канонических числах, на которые bit-exact согласны все узлы.

Физическая длительность одной итерации зависит от hardware узла (наносекунды — десятки наносекунд на commodity CPU). Физическая длительность окна зависит от скорости железа узла и от участия сети. Физическая длительность — свойство конкретного наблюдателя, выводимое на клиентском слое.

Time Oracle

Canonical window_index каждого proposal — верифицируемая координата события. Внешние системы используют Montana Time как reference frame:

  • Timestamping. H(document) привязанный к window_index = криптографическое доказательство существования в позиции W канонической последовательности.
  • Ordering. Два события, привязанные к разным window_index, имеют доказуемый канонический порядок.
  • Anchoring. Внешний протокол якорится в Montana Time для независимой верификации порядка событий.

Перевод window_index → физическое время в любых внешних стандартах (UTC, TAI, GPS Time) является задачей клиентского слоя. Montana производит каноническую последовательность окон; внешний наблюдатель выбирает собственный метод привязки window_index к своим локальным временным единицам.

TimeChain хранится навсегда. Канонические координаты верифицируемы любым узлом в любой момент.


Криптография

Два примитива с разделёнными ролями:

  • SHA-256 — консенсус (TimeChain, NodeChain), адреса, Merkle-деревья, хэширование
  • FN-DSA-512 (Falcon-512, выбран в финальном раунде NIST PQC selection, июль 2022; forthcoming FIPS 206; reference implementation production-ready) — подписи операций аккаунтов и proposals узлов

SHA-256 обеспечивает квантовую устойчивость консенсуса: алгоритм Гровера сокращает безопасность с 256 до 128 бит. FN-DSA-512 обеспечивает математическую постквантовую устойчивость подписей на основе NTRU-решёток. Других криптографических примитивов в протоколе нет — финансовый слой публичен, приватность данных обеспечивается на уровне приложений через Anchor.

Подписи — FN-DSA-512

Подпись на NTRU-решётках (Falcon-512). Stateless, многоразовая. Публичный ключ закрепляется за аккаунтом при создании и используется для всех последующих операций.

Компонент Размер
Приватный ключ 1 281B
Публичный ключ 897B
Подпись (padded) 666B

Поле suite_id в формате блока обеспечивает миграцию подписи без изменения модели состояния. Активация новой схемы требует protocol upgrade. Активная схема на момент запуска: FN-DSA-512.

Адреса

Формат: mt + Base58(account_id + checksum).

Account_id = SHA-256("mt-account" || suite_id || pubkey). Стабильный идентификатор аккаунта. Смена ключа или схемы подписи выполняется через ChangeKey без изменения account_id — account_id привязан к первому pubkey, а текущий ключ хранится в состоянии аккаунта.

Инвариант derivation. Проверка account_id == SHA-256("mt-account" || suite_id || pubkey) происходит один раз при settle OpenAccount (apply at window close). После этого account_id — каноничный ключ записи, формула не пересчитывается. Доказательство derivation навсегда сохранено в proposal с финализированным OpenAccount. Любой аудитор может replay из proposal history. Original_pubkey не дублируется в Account Table — integrity гарантируется неизменностью proposal chain.

Поле suite_id в Account Table — current (мутируется ChangeKey синхронно с current_pubkey), используется для верификации текущих подписей. Original suite_id зафиксирован только в исторической OpenAccount записи в proposal chain.


Account Chain (Block Lattice)

Каждый аккаунт имеет собственную цепочку операций. Перевод — одна операция в цепочке отправителя. Зачисление получателю — детерминированно после финализации. Цепочки аккаунтов полностью независимы.

Реестр типов объектов

UserObjects:
  0x01  OpenAccount
  0x02  Transfer
  0x03  ChangeKey
  0x04  Anchor

ControlObjects:
  0x10  NodeInvitation
  0x11  NodeRegistration

Типы операций

Универсальная форма операции:

type      (1B)  | prev_hash (32B) | payload (variable) | signature (666B)

Все операции — этот шаблон. prev_hash связывает операции в цепочку аккаунта. signature — FN-DSA-512 владельца. payload зависит от типа. Все non-OpenAccount операции начинают payload с sender (32B account_id) — узел проверяет Account Table[sender].frontier_hash == prev_hash и signature валиден для current_pubkey за O(1).

OpenAccount — создание аккаунта (один раз). Единственная операция где prev_hash = 0x00...00:

type       1B   <- 0x01 OpenAccount
prev_hash 32B   <- 0x00...00
payload  899B   <- suite_id (2B) || pubkey (897B FN-DSA-512)
signature 666B
Итого: ~1 598 B

account_id = SHA-256("mt-account" || suite_id || pubkey) — детерминирован, не хранится в payload.

Transfer — публичный перевод:

type       1B   <- 0x02 Transfer
prev_hash 32B
payload   80B   <- sender (32B) || link (32B receiver) || amount (16B u128 nɈ)
signature 666B
Итого:    ~779 B

sender — account_id отправителя, явно. Узел проверяет Account Table[sender].frontier_hash == prev_hash за O(1).

Открытые поля: отправитель (через frontier index по prev_hash), получатель, сумма, баланс после операции (через Account Table). Псевдонимность на уровне account_id, как Bitcoin. Финансовая приватность — задача приложений (микшеры, payment channels), не протокола.

ChangeKey — смена ключа или схемы подписи:

type       1B   <- 0x03 ChangeKey
prev_hash 32B
payload  931B   <- sender (32B) || new_suite_id (2B) || new_pubkey (897B)
signature 666B  <- подписано старым ключом
Итого: ~1 630 B

Anchor — криптографический якорь (привязка данных ко времени):

type       1B   <- 0x04 Anchor
prev_hash 32B
payload   96B   <- sender (32B) || app_id (32B) || data_hash (32B)
signature 666B
Итого:    ~795 B

Anchor не перемещает средства и не требует комиссии. Единственная операция — запись data_hash в цепочку аккаунта с привязкой к timechain_value окна финализации. Приватность данных приложения обеспечивается тем что в сеть попадает только хэш — содержимое хранится у владельца зашифрованным.

Верификация баланса

Открытое арифметическое сравнение. Узел проверяет:

sender != receiver
amount > 0
sender.balance >= amount

sender != receiver запрещает self-transfer — иначе атакующий мог бы наращивать account_chain_length каждое окно через no-op переводы себе.

При settle (apply at window close):

sender.balance   -= amount
receiver.balance += amount

Баланс обновляется не при cement (~0.3 сек), а в конце окна при батчевом apply. Между cement и settle операция необратима но баланс ещё не изменён. Никаких proofs, никакой криптографии помимо подписи и хэша.

Anti-inflation

Чеканка из воздуха невозможна через локальный инвариант на каждом state transition.

Per-user-operation invariant. Каждое применение пользовательской операции обязано удовлетворять Σ delta_balance == 0:

Transfer:    sender.balance -= amount, receiver.balance += amount  → Σ = 0
OpenAccount: новый аккаунт с balance = 0                            → Σ = 0
ChangeKey:   только обновление current_pubkey                       → Σ = 0
Anchor:      только запись data_hash                                → Σ = 0

Per-proposal invariant. Каждый финализированный proposal окна τ₁ обязан удовлетворять delta_supply == +60 Ɉ:

apply_proposal step 2 (TimeCoin emission):
  если winner_class = Node:    operator_account.balance += 60_000_000_000 nɈ
  если winner_class = Account: winner_account.balance   += 60_000_000_000 nɈ
  
delta_supply за proposal = +60_000_000_000 nɈ ровно один раз

O(1) проверка на каждое state transition. Глобальный инвариант Σ balance == 60 Ɉ × (window_index + 1) истинен по индукции от genesis при условии что каждый переход поддерживает per-operation invariant.

genesis state (аксиома): window_index не определён, supply = 0,       Σ balance = 0
первое окно:             window_index = 0, supply = 60 Ɉ,              Σ balance = 60 Ɉ
окно k:                  window_index = k, supply = 60 × (k+1) Ɉ,      Σ balance = 60 × (k+1) Ɉ

Никаких откатов cemented операций не требуется — каждое cemented локально валидно по конструкции.

τ₂ sanity check. Дополнительная проверка раз в τ₂: пересчёт Σ balance по всей Account Table и сравнение с 60 Ɉ × (window_index + 1). Не load-bearing для финализации — служит для обнаружения багов реализации. Расхождение = немедленная остановка узла, дамп state для расследования.

Перевод

Перевод на несуществующий account_id — отклоняется. Получатель обязан существовать в Account Table до получения перевода.

TimeCoin

Победитель τ₁ регистрирует одно окно Montana Time: 60 Ɉ. При финализации proposal окна:

если winner_class = Node:    operator_account.balance += 60_000_000_000 nɈ
если winner_class = Account: winner_account.balance   += 60_000_000_000 nɈ

Атомарное обновление баланса. Узел получает награду через привязанный operator_account (зафиксирован при NodeRegistration). Никаких отдельных coinbase-структур, никаких отдельных таблиц эмиссии. Зачисление есть состояние Account Table.

Публичное (верифицируемо всеми):
  TimeCoin:     60 Ɉ за окно (константа)
  Supply audit: supply(window_index) = 60_000_000_000 × (window_index + 1) nɈ
  Winner:       winner_id в proposal header
  Все балансы:  Account Table
  Все переводы: цепочки операций аккаунтов
  VDF:          TimeChain values, NodeChain endpoints, подписи

Псевдонимность на уровне account_id (как Bitcoin). Финансовая приватность — задача приложений: микшеры, payment channels, off-chain settlements. Не задача протокола.

Двойная трата

Каждый аккаунт имеет одну цепочку. Две операции с одним prev_hash = equivocation.

Без конфликта: операция → узлы валидируют → публикуют confirmation → quorum → cemented (необратимо, ~0.3 сек). Баланс обновляется при settle (apply at window close).

При конфликте (equivocation):

  1. Узел получает операцию X с prev_hash = H. Узел уже видел операцию Y с prev_hash = H, Y ≠ X. Форк обнаружен. Обе операции помечаются как equivocated.
  2. Если одна операция уже cemented (quorum до обнаружения конфликта) — cemented необратимо. Вторая отклоняется.
  3. Если ни одна не cemented — узлы продолжают собирать confirmations для обеих. Если одна набирает quorum → cemented, вторая отклоняется.
  4. Если через 10 окон ни одна не набрала quorum → обе отклоняются окончательно. Аккаунт продолжает с последней cemented операции. Владелец отправляет новую операцию.

Equivocation создаётся только владельцем аккаунта (требуется подпись). Третья сторона не может создать equivocation для чужого аккаунта. Стимул: двойная трата = потеря обеих операций.

Антиспам

Ноль комиссий — антиспам через время. Право на операцию = доказанное время существования аккаунта.

Приоритет операции

account_age = current_window - creation_window
priority(op) = account_age × windows_since_last_op

account_age — возраст аккаунта в окнах. Растёт линейно. Некупуемый. windows_since_last_op — окна с последней операции аккаунта. Сбрасывается при каждой операции. Спамер обнуляет приоритет с каждой операцией — самонаказание.

При переполнении ёмкости сети — операции с наименьшим приоритетом ожидают следующего окна.

Бакеты по account_age

Изоляция спама. Каждый аккаунт может опубликовать максимум одну операцию за окно τ₁ (dependency rule). При переполнении сети (больше операций в мемпуле чем пропускная способность окна) — бакеты определяют приоритет включения. Round-robin по бакетам: одна операция из бакета 0, одна из бакета 1, ..., по кругу. Спам в бакете 0 не вытесняет операции из бакетов 1-3.

Бакет 0:  account_age < 4τ₂
Бакет 1:  account_age 4τ₂ — 16τ₂
Бакет 2:  account_age 16τ₂ — 64τ₂
Бакет 3:  account_age 64τ₂+

Границы бакетов = 4^N × τ₂. Все аккаунты: максимум 1 операция за τ₁. Бакет определяет приоритет при переполнении, не потолок TPS.

Новый аккаунт — бакет 0 с момента создания. 1 операция в минуту. Вход без ожидания: получил перевод → сразу можешь отправить.

Throughput на аккаунт

1 операция за τ₁ (одно окно). Это консервативно по сравнению с Bitcoin-style блок-ориентированными сетями. Один Anchor содержит Merkle root от произвольного количества записей — throughput данных не ограничен. Для высокочастотных переводов — payment channels или application-level batching.

Спамер с 1000 новых аккаунтов: 1000 операций за τ₁ в бакете 0. Бакет 0 получает 1/4 от round-robin. Изолирован. Аккаунты в бакетах 1-3 не замечают.


Состояние сети

Глобальное состояние = Account Table + Node Table + Invitation Table.

Account Table (запись на аккаунт):
  account_id              32B     <- = SHA-256("mt-account" || suite_id || pubkey)
  balance                 16B     <- u128 nɈ, открыт
  suite_id                 2B
  is_node_operator         1B     <- 1 если аккаунт привязан как operator узла; исключён из лотереи аккаунтов
  frontier_hash           32B     <- хэш последней операции в цепочке
  op_height                4B     <- количество операций в цепочке
  account_chain_length     4B     <- количество уникальных окон τ₁ с операцией (длина AccountChain), live
  account_chain_length_snapshot 4B <- snapshot account_chain_length на последнюю τ₂ boundary, используется лотереей
  current_pubkey         897B     <- FN-DSA-512
  creation_window          4B     <- окно создания аккаунта (OpenAccount)
  last_op_window           4B     <- окно последней операции (для приоритета)

Node Table (запись на узел):
  node_id                          32B     <- SHA-256("mt-node" || node_pubkey), верифицируемо
  node_pubkey                     897B
  suite_id                          2B
  operator_account_id              32B     <- account_id куда зачисляется TimeCoin при победе узла; неизменен после регистрации
  start_window                      8B     <- u64, окно регистрации (первое окно NodeChain)
  chain_length                      8B     <- u64, число окон с cemented BundledConfirmation узла; инкрементируется в apply at window close
  last_confirmation_window          8B     <- u64, window_index последнего окна с cemented BundledConfirmation

Invitation Table (запись на приглашение):
  invitation_id                    32B     <- SHA-256("mt-invitation" || inviter_node_id || invited_pubkey || invite_window)
  inviter_node_id                  32B     <- node_id пригласившего узла
  invited_pubkey                  897B     <- FN-DSA-512 публичный ключ приглашённого
  invited_node_id                  32B     <- = SHA-256("mt-node" || invited_pubkey), derived
  invite_window                     8B     <- u64, окно apply NodeInvitation
  invite_expires                    8B     <- u64, invite_window + 21 160 (1 000 окон запас сверх 20 160 окон VDF)

Active node predicate (derived). Узел считается активным если опубликовал cemented BundledConfirmation за последние 2τ₂:

active(node, W) = (W - node.last_confirmation_window) <= 2 × τ₂_windows

Predicate не хранится в state — вычисляется из last_confirmation_window и текущего window_index. Применяется в quorum, confirmation_threshold, лотерее, валидации NodeRegistration.

Quota predicate (derived). Количество одновременно активных приглашений узла:

invites_active(N) = |{ inv ∈ Invitation Table : inv.inviter_node_id == N }|

Не хранится в state — вычисляется сканированием Invitation Table (≤ 10³ записей при росте сети, миллисекунды). Используется при валидации NodeInvitation: пригласитель допускается если invites_active(N) < quota(bucket(N.chain_length)).

Семантика «одновременно активных» закрывает атаку на τ₂ boundary: записи expired/consumed удаляются из таблицы, slot освобождается, новое приглашение создаётся. Узел не может удвоить квоту через границу epoch. Пропускная способность узла-инвайтера определяется частотой completion циклов (20 160 окон на одно VDF), не границей epoch τ₂.

Invitation как объект первого класса. Каждое приглашение — независимая запись с собственным lifecycle (created → expired или created → consumed by NodeRegistration). Узел-инвайтер может держать N параллельных приглашений ограниченных только квотой бакета. Single-slot конфликтов нет конструкцией.

State Root

Merkle-дерево глобального состояния. Три подкорня обновляются при применении операций (apply_proposal и apply at window close):

state_root = SHA-256("mt-state-root" || node_root || invitation_root || account_root)

node_root:       Merkle root Node Table, обновляется при apply NodeRegistration,
                 chain_length increment (apply step 3.5), pruning узлов на τ₂.
invitation_root: Merkle root Invitation Table, обновляется при apply NodeInvitation
                 (создание), apply NodeRegistration (удаление), apply step 3 (expiry),
                 cascade при pruning узла-инвайтера.
account_root:    Merkle root Account Table, обновляется батчем при apply at window
                 close (все cemented операции окна применяются к state, затем
                 account_root пересчитывается).

Все три root соответствуют settled state (после apply at window close). Они не обновляются на каждую индивидуальную cemented операцию (нет "live root"), а применяются батчами, обеспечивая единый и верифицируемый state_root.
Порядок node_root → invitation_root → account_root отражает направление
зависимостей: узлы существуют первичны, приглашения связывают существующих
с будущими узлами, аккаунты — финансовый слой над всем.
Domain separator `mt-state-root` отличён от `mt-merkle-node`
(используемого для внутренних узлов Merkle tree) — hash spaces не пересекаются.

Структура Account Table Root:

Sparse Merkle tree глубины 256, индексированный по account_id:

leaf_hash(account)        = SHA-256("mt-merkle-leaf" || serialize(account_record))
internal(left, right)     = SHA-256("mt-merkle-node" || left || right)
empty_leaf                = 0x00 × 32

account_root = root of sparse Merkle tree over Account Table

Обновление одного аккаунта пересчитывает ровно log₂(N) хэшей пути от листа к корню — для N=10⁹ аккаунтов это 30 SHA-256 вычислений (~60 µs CPU).

Структура Node Table Root: аналогично, sparse Merkle tree по node_id. Размер сети ≤ 10⁵ узлов → пути ~17 хэшей.

Структура Invitation Table Root: sparse Merkle tree глубины 256, индексированный по invitation_id. Размер ≤ 10³ записей при росте сети → пути ~10 хэшей. Empty root = 0x00 × 32.

Каждый узел в Node Table — участник сети. Узел существует в таблице = участвует.

Все sort keys фиксированной длины. Побайтовое лексикографическое сравнение. Две реализации с одинаковыми данными строят одинаковое дерево и получают одинаковый State Root.

State Root коммитится в заголовке каждого proposal τ₁. account_root, node_root и invitation_root соответствуют settled state после apply at window close — все cemented операции окна W применены к таблицам перед сборкой proposal.

Inclusion proof

Любой cemented аккаунт может предоставить доказательство существования в state:

proof = Merkle path длиной log₂(N) (~30 хэшей для N=10⁹)
verify(proof, account_record, account_root):
  reconstruct path bottom-up; compare с account_root

Доказательство верифицируется против account_root любого финализированного proposal начиная с окна когда состояние было обновлено. Не нужны архивы операций — текущее состояние самодостаточно.

Pruning

На τ₂ boundary применяется pruning неактивных аккаунтов:

Удалить все записи Account Table где:
  balance == 0                                            <- нулевой баланс
  AND last_op_window + 4τ₂ <= current_window              <- нет активности 4τ₂ (56 дней)
  AND is_node_operator == 0                               <- не привязан как operator узла
  AND нет cemented NodeRegistration в control_set         <- нет pending привязки
      ожидающего apply, ссылающегося на этот account_id

Пустой аккаунт без активности 56 дней — удаляется, кроме:

  • Operator-аккаунтов уже зарегистрированных узлов (is_node_operator == 1)
  • Аккаунтов на которые ссылается cemented NodeRegistration ожидающий apply

Без второго исключения возможна race: NodeRegistration cemented (operator валиден), pruning применился до apply этого NodeRegistration → аккаунт удалён → apply отклонён. Защита: pruning не трогает аккаунты, на которые есть cemented pending registration.

Каждое удаление пересчитывает соответствующий путь в Merkle tree (logarithmic). Pruning детерминирован, автоматичен, каноничен.

Recovery semantics. Воссоздание pruned аккаунта через новый OpenAccount с тем же ключом создаёт новую цепочку: frontier_hash начинается заново, op_height сбрасывается в 1, account_chain_length = 0. Старые prev_hash references на цепочку до pruning отклоняются — цепочка удалена из текущего state. История переводов до pruning не восстанавливается из текущего Account Table, но навсегда сохранена в proposals. Восстановление истории возможно через scan архива proposals.


Двигатели

Четыре цепочки с односторонним потоком зависимостей: TimeChain → NodeChain → AccountChain → AccountTable.

TimeChain — глобальные часы (ход времени). NodeChain — машинное присутствие узла (непрерывное VDF). AccountChain — человеческое присутствие аккаунта (дискретные операции). AccountTable — состояние счёта.

TimeChain VDF — осциллятор

Первичный продукт протокола. Непрерывная последовательная SHA-256 цепочка — цифровой осциллятор Montana Time:

T_r = SHA-256^D(T_{r-1})

D — количество последовательных хэшей за одно окно τ₁. Каждый хэш — один тик осциллятора. D хэшей — одно колебание. TimeChain продвигается по расписанию окон. Для фиксированного индекса r значение T_r совпадает у всех честных узлов. Каждый узел вычисляет TimeChain независимо — результат детерминирован.

TimeChain не зависит от состояния, транзакций и поведения отдельных узлов. Даже при отказе всего Account слоя часы продолжают тикать.

NodeChain — персональная цепочка узла

Криптографическое доказательство присутствия конкретного node_id при каждом тике часов. Якорится в TimeChain каждое окно:

S_{i,s,0}   = SHA-256(S_{i,s-1,m} || T_s || node_id_i)
S_{i,s,j+1} = SHA-256(S_{i,s,j})    для j = 0..m-1

Три компонента seed: предыдущий endpoint (непрерывность цепочки), значение TimeChain (протокольное время), node_id (идентичность). m последовательных хэшей за окно — одно звено NodeChain.

Инициализация: для первого окна нового узла предыдущий endpoint отсутствует. NodeChain init привязан к каноническим данным proposal в котором NodeInvitation финализирован:

S_{i,0,0} = SHA-256("mt-nodechain-init" || control_root || timechain_value || node_id_i)

control_root и timechain_value из proposal header окна финализации Invitation. Оба канонические (не зависят от субъективного user_set). Предвычисление VDF невозможно — timechain_value неизвестен до закрытия окна. Grinding surface = ноль. Верифицируем любым узлом.

NodeChain зависит от TimeChain. TimeChain не зависит от NodeChain.

AccountChain — персональная цепочка аккаунта

Криптографическое доказательство присутствия конкретного account_id в дискретных моментах. Каждое звено — финализированная операция аккаунта (Transfer, OpenAccount, ChangeKey, Anchor). Linking через prev_hash (хэш предыдущей операции в цепочке аккаунта). Якорится в TimeChain через timechain_value момента финализации каждой операции.

Длина AccountChain — количество окон τ₁ в которых аккаунт имел cemented операцию:

account_chain_length(account, W) = | { w : w <= W, аккаунт имел cemented операцию в окне w } |

Dependency rule ограничивает аккаунт одной операцией за окно τ₁ — поэтому длина AccountChain совпадает с числом окон активности. Поле account_chain_length хранится в Account Table, обновляется при apply операции:

on_operation_applied(operation, window W):
  account = operation.account_id
  account.account_chain_length += 1
  account.last_op_window = W
  account.op_height += 1

Параллелизм с NodeChain:

Свойство NodeChain AccountChain
Источник node_pubkey account_pubkey
Идентификатор node_id account_id
Тип присутствия машинное человеческое
Ритм непрерывный (каждое окно VDF) дискретный (окно с операцией)
Длина chain_length (окна) account_chain_length (окна)
Единица длины окно τ₁ окно τ₁
Накопление автоматически с каждым окном через активность пользователя
Якорь во времени timechain_value каждое окно timechain_value окна с операцией
Защита от подделки VDF необратим подпись FN-DSA-512
Linking endpoint предыдущего звена prev_hash предыдущей операции
Защита от Sybil 20 160 окон VDF + invite slot накопление окон требует работы в каждом окне

Узел доказывает присутствие непрерывной работой машины в каждом окне. Аккаунт доказывает присутствие активным использованием сети — каждая операция фиксирует одно окно человеческого бытия на временной шкале Montana. Оба механизма математически верифицируемы, оба производят запись на одной шкале времени.

AccountChain зависит от TimeChain напрямую — каждая операция привязана к timechain_value момента финализации. AccountChain не зависит от NodeChain по построению — цепочка аккаунта существует независимо от того какой узел победил в окне финализации.

VDF Reveal и лотерея

В лотерее участвуют два класса субъектов: узлы (через NodeChain) и аккаунты (через AccountChain). Каждый класс производит ticket взвешенный по длине своей цепочки. Lowest weighted_ticket из объединённого множества кандидатов выигрывает 60 Ɉ.

Класс 1: узлы

После закрытия окна τ₁ каждый узел вычисляет свой ticket:

ticket_node = -ln(endpoint_node / 2^256)
weighted_ticket_node = ticket_node / chain_length

Если weighted_ticket_node < target — узел кандидат и публикует VDF_Reveal:

VDF_Reveal:
  node_id          32B
  window_index      4B     <- индекс τ₁
  endpoint         32B     <- S_{i,s,m}
  start_window      4B     <- окно начала NodeChain (для верификации)
  signature       666B     <- FN-DSA-512, подписано node_pubkey
Итого:       ~738B

Класс 2: аккаунты

Аккаунт автоматически становится кандидатом если у него есть финализированная операция в окне W. Никакой отдельный reveal не публикуется — операция уже cemented в сети, endpoint вычисляется детерминированно любым узлом.

operation_for_lottery(account, W) = единственная cemented операция аккаунта в окне W
                                     (dependency rule: максимум одна)

endpoint_account(W) = SHA-256(
  "mt-account-lottery" ||
  account_id ||
  hash(operation_for_lottery) ||
  timechain_value(W)
)

account_length_at_lottery = account.account_chain_length_snapshot

ticket_account = -ln(endpoint_account / 2^256)
weighted_ticket_account = ticket_account / account_length_at_lottery

account_chain_length_snapshot обновляется на каждой τ₂ boundary копией текущего account_chain_length. Между τ₂ boundaries snapshot frozen — все узлы используют одно значение, лотерея детерминирована. Live account_chain_length продолжает расти от cemented операций, но не используется лотереей до следующего τ₂.

Если weighted_ticket_account < target — аккаунт кандидат. Аккаунт без операции в окне W не участвует в лотерее этого окна.

Исключение operator-аккаунтов. Аккаунт с is_node_operator = 1 исключён из лотереи аккаунтов даже если у него есть операция в окне. Узел получает вес через NodeChain (через свой node_id); operator_account только хранит TimeCoin. Двойной счёт исключён конструкцией. Оператор узла, желающий участвовать в лотерее аккаунтов, использует отдельный персональный аккаунт без NodeRegistration привязки.

Защита от grinding: dependency rule ограничивает аккаунт одной операцией за окно — один лотерейный билет, нет выбора «лучшей» операции. timechain_value(W) известен только после закрытия окна — endpoint не предсказуем заранее.

Победитель окна

candidates = node_candidates  account_candidates
winner = argmin(weighted_ticket(c) for c in candidates)
winner получает TIME_RECORD = 60 Ɉ

Формирование Proposal (Proposer Logic): Роли winner (получатель 60 Ɉ) и proposer (узел, собирающий и подписывающий proposal) строго разделены:

  • winner_id определяется абсолютным минимумом среди всех weighted_tickets (и узлов, и аккаунтов). Он забирает награду.
  • proposer_node_id всегда назначается узлу-кандидату с минимальным weighted_ticket в этом окне (возможно совпадает с победителем, если победил узел).

Узел-proposer собирает proposal без дополнительной награды (это обязанность валидатора). Proposal содержит winner_id победившего аккаунта (или узла).

Если в окне нет узлов-кандидатов и есть аккаунты-кандидаты: победитель выбирается среди аккаунтов. Proposal формирует ближайший активный узел (fallback к узлу с lowest ticket, даже если он не успел с VDF), без дополнительной награды.

Калибровка target

Target калиброван на ~12 кандидатов за окно (включая обоих классов). Калибровка на τ₂:

target_new = target_old × (12 / actual_candidates_per_window)
actual_candidates_per_window = total_candidates_за_τ₂ / 20 160

Трафик reveal за окно: ~12 узловых reveals × 738B ≈ 8.9 KB. Аккаунты не публикуют отдельные reveals — их операции уже в gossip.

Валидация reveal узла

  1. Подпись FN-DSA-512 соответствует node_pubkey из Node Table
  2. window_index = только что закрытый τ₁
  3. node_id существует в Node Table
  4. weighted_ticket < target
  5. endpoint верифицируем: пересчёт NodeChain VDF от предыдущего endpoint

Валидация участия аккаунта

  1. account_id существует в Account Table
  2. account_chain_length_snapshot > 0
  3. У аккаунта есть хотя бы одна cemented операция в окне W
  4. operation_for_lottery определена детерминированно
  5. weighted_ticket_account < target

Account — содержимое блока

Приём, верификация объектов и формирование набора. Два класса объектов:

UserObjects — пользовательские операции:

Тип Описание Валидация
Transfer Публичный перевод FN-DSA-512 подпись, prev_hash, sender != receiver, amount > 0, sender.balance >= amount, получатель существует
OpenAccount Создание аккаунта FN-DSA-512 подпись, prev_hash = 0, account_id = SHA-256("mt-account"
ChangeKey Смена ключа FN-DSA-512 подпись старым ключом, new_pubkey
Anchor Якорь данных ко времени FN-DSA-512 подпись, prev_hash, app_id = 32B, data_hash = 32B

ControlObjects — объекты управляющие составом сети:

Тип Описание Валидация
NodeInvitation Приглашение нового узла FN-DSA-512 подпись пригласившего, активный inviter, invites_active < квота бакета, invited_pubkey не зарегистрирован
NodeRegistration Регистрация узла FN-DSA-512 подпись, node_id уникален, proof_endpoint верифицируем через VDF, приглашение существует

Каждый узел валидирует объекты обоих классов локально при получении. Валидные объекты ретранслируются по P2P.

Все объекты — UserObjects и ControlObjects — финализируются (cemented) одинаково: через 67% active_chain_length подтверждения в BundledConfirmation. Cemented status объективен и одинаков для всех узлов. Дискреция победителя над включением ControlObjects = ноль.

Proposal

Proposal содержит control_set и метаданные окна. UserObjects применяются к Account Table батчем при settle (apply at window close); в proposal они не повторяются. ControlObjects применяются к Node Table в apply_proposal step 1 в детерминированном порядке.

control_set(proposal окна W) определён формулой:

control_set = {
  ControlObject c :
    c.cemented_window > previous_proposal.window
    AND c.cemented_window <= W
}

сортировка: (cemented_window asc, op_hash lex asc)

Где previous_proposal.window — окно предыдущего финализированного proposal в цепочке. Множество детерминировано: cemented_window — каноническое поле объекта (известно всем узлам через BundledConfirmation), op_hash — детерминирован.

Победитель обязан включить весь control_set целиком. Пропуск или добавление лишнего ControlObject = невалидный proposal = fallback. Каждый узел независимо вычисляет ожидаемый control_set по той же формуле и сравнивает с proposer's set.

Форки аккаунтов (две операции с одним prev_hash) разрешаются голосованием узлов весом chain_length. 67% active_chain_length за одну операцию → побеждает (см. раздел «Двойная трата»).

Закрытие окна (Quorum-driven Finalization)

TimeChain W   →  VDF candidates publish endpoints  →  BundledConfirmations
                                                              │
                                                              ▼
                                              ┌─────────────────────┐
                                              │ quorum threshold    │
                                              │ 67% active_chain_len│
                                              └──────────┬──────────┘
                                                         │ reached
                                                         ▼
                                                Window W finalized
                                                Determine winner
                                                Assemble proposal
                                                Proceed to W+1
  • Quorum event. Окно W канонически закрыто, когда суммарный chain_length узлов, подписавших BundledConfirmation для окна W, достигает quorum(W) = ⌈0.67 × active_chain_length(W)⌉. Этот event детерминирован и виден всем узлам bit-exact через один и тот же cemented set.
  • Late confirmations. BundledConfirmation окна W, полученные после достижения quorum event, игнорируются. Cemented set окна W становится каноническим в момент quorum event и не изменяется.
  • VDF_Reveal. VDF_Reveal окна W принимаются всеми узлами до момента quorum event. После — игнорируются. VDF_Reveal-ы, включённые в хотя бы одну из cemented BundledConfirmation, считаются каноническими кандидатами лотереи окна W.
  • ControlObjects. ControlObjects не имеют отдельного cutoff — они попадают в control_set следующего proposal по моменту cement.

Закрытие окна — event-driven. Триггер закрытия — достижение quorum event в канонических cemented sets. Окно закрывается, когда сеть канонически собрала достаточно подписей.

После quorum event: каждый узел независимо определяет winner_id = argmin(weighted_ticket) среди канонических кандидатов окна W, proposer_node_id = узел-кандидат с минимальным weighted_ticket. Исключительно этот узел-proposer собирает и подписывает proposal от имени всей сети.

Свойство темпа сети. Сеть продвигается со скоростью медианного активного набора узлов, не со скоростью быстрейшего. Quorum требует подписей большинства по chain_length — быстрейший узел ждёт, пока достаточно других успеет, медленнейший узел пропускает окно и догоняет позже. Hardware progress ускоряет сеть естественно, но только когда ускоряется медиана.

Proposer

Proposer (proposer_node_id) собирает proposal:

  • control_set: все cemented ControlObjects в окнах (previous_proposal.window, W] (формула выше). Свобода = ноль.
  • State Root snapshot: account_root, node_root и invitation_root после apply at window close (все cemented операции и control objects окна W применены батчем)

Свобода proposer: ноль. control_set детерминирован формулой. State root вычисляется после батчевого apply — каждый узел независимо применяет один и тот же cemented set и получает один и тот же результат.

Proposal с пропущенным cemented ControlObject, добавленным non-cemented ControlObject, неверным порядком или неверным state_root отклоняется, переход ко второму месту.

Финальность proposal

Финальность proposal = подпись proposer_node_id на proposal header (верифицируемая против Node Table[proposer_node_id].node_pubkey) + независимая верифицируемость состояния.

  1. Proposer (proposer_node_id) публикует подписанный proposal header + control_set
  2. Каждый узел проверяет window_index == prev_proposal.window_index + 1, protocol_version >= prev_proposal.protocol_version и protocol_version <= local_max_supported_version
  3. Каждый узел независимо вычисляет ожидаемый control_set по формуле и сравнивает с proposer's
  4. Каждый узел применяет control_set + TimeCoin детерминированно в порядке (cemented_window asc, op_hash lex asc)
  5. Каждый узел сравнивает вычисленный state_root с заявленным в proposal
  6. Совпадает — proposal принят
  7. Не совпадает — proposal отклонён, fallback на второе место

Финальность операций аккаунтов — отдельный процесс через подтверждения (67% active_chain_length), не через proposal.

Proposal header:

Proposal header:
  prev_proposal_hash    32B
  window_index           8B    <- u64, индекс окна τ₁ с genesis; == prev_proposal.window_index + 1
  protocol_version       4B    <- u32, активная версия протокола на момент window_index
  control_root          32B    <- Merkle root control_set (каноничен)
  node_root             32B    <- Merkle root Node Table (обновляется каждое окно)
  invitation_root       32B    <- Merkle root Invitation Table
  account_root          32B    <- Merkle root Account Table после apply at window close
  state_root            32B    <- SHA-256("mt-state-root" || node_root || invitation_root || account_root)
  timechain_value       32B
  winner_class           1B    <- 1=Node, 2=Account
  winner_endpoint       32B    <- NodeChain endpoint winner-а (если winner_class=1)
                                  иначе endpoint_account вычисляемый
  winner_id             32B    <- получатель TimeCoin: node_id (winner_class=1)
                                  или account_id (winner_class=2)
  proposer_node_id      32B    <- узел собравший и подписавший proposal;
                                  если winner_class=1, proposer_node_id == winner_id;
                                  если winner_class=2, proposer_node_id = node-кандидат
                                  с минимальным weighted_ticket (или fallback)
  target                 8B    <- текущий target лотереи
  fallback_depth         1B    <- 1 = первое место, 2+ = fallback
  signature            666B    <- FN-DSA-512, подпись header Node Table[proposer_node_id].node_pubkey

Все поля proposal header канонически вычислимы bit-exact из предыдущего state и cemented set окна W. Каждое поле имеет источником либо canonical state, либо детерминированную функцию от canonical state.

Разделение ролей winner_id и proposer_node_id. Это два независимых поля с разными назначениями:

  • winner_id — получатель TimeCoin. Аккаунт или узел, выигравший лотерею окна. Используется только в apply_proposal step 2 для зачисления 60 Ɉ.
  • proposer_node_id — узел ответственный за сборку и публикацию proposal. Подписывает header своим node_pubkey. Верификация подписи proposal — против Node Table[proposer_node_id].node_pubkey, всегда.

Когда winner_class = Account, winner — это аккаунт без node_pubkey, физически не способный подписать proposal. Подписывает всегда узел-proposer (ближайший по weighted_ticket). TimeCoin при этом получает winner (аккаунт), proposer не получает дополнительной награды.

Инварианты Proposal header:

  • window_index == prev_proposal.window_index + 1 (монотонность, шаг 1)
  • protocol_version >= prev_proposal.protocol_version (не убывает; изменяется только через software upgrade узла, см. раздел «Эволюция протокола»)
  • protocol_version <= local_max_supported_version (узел обязан отклонить proposal с protocol_version которую его реализация не поддерживает; принятие неизвестной версии = принятие непроверяемых правил = нарушение безопасности)

Cemented window объекта — window_index proposal-а в котором BundledConfirmation с этим объектом достиг quorum. Определён детерминированно для каждого cemented объекта.

Settled window объекта — window_index proposal-а в котором объект был применён к state:

  • Для UserObjects: settled_window = cemented_window (apply batch at window close того же окна). Следующая операция от того же sender возможна в окне cemented_window + 1 (dependency rule)
  • Для ControlObjects: settled_window = window_index первого proposal где объект попал в control_set (обычно cemented_window + 1)

Fallback: если primary proposer_node_id не опубликовал валидный proposal до момента, когда сеть уже видит BundledConfirmation-ы для окна W+1 от fallback_depth_threshold узлов (минимум 10% active_chain_length), proposal окна W считается отсутствующим, и роль proposer переходит к следующему узлу-кандидату по weighted_ticket. Fallback event канонически определён через наблюдение сетью попыток продвижения к следующему окну — это event-driven сигнал, не физический таймер.

При fallback меняется только proposer_node_id; winner_id и распределение TimeCoin всегда сохраняются (60 Ɉ всё равно уходит изначальному победительному ticket-у). Новый proposer_node_id подписывает header своим node_pubkey, fallback_depth инкрементируется.

Полная симметрия fallback: Молчание первого proposer никогда не лишает winner награды — оно только переводит обязанность сборки proposal к следующему узлу. Независимо от того, является ли winner узлом или аккаунтом, награда привязана к лотерейному билету и гарантирована, если хотя бы один узел в сети соберёт валидный proposal через механизм fallback.

Непрерывность VDF

VDF следующего окна вычисляется непрерывно, не ожидая завершения финализации предыдущего. TimeChain для окна N+1 детерминирован — каждый узел вычисляет его независимо. NodeChain для окна N+1 стартует сразу после закрытия окна N, используя собственный endpoint текущего окна и новое значение TimeChain. Reveal phase и финализация происходят параллельно с началом VDF следующего окна.

Confirmations (финализация операций и control objects)

Confirmers — узлы с chain_length >= confirmation_threshold. Подтверждают все валидные объекты окна (UserObjects + ControlObjects) от имени сети.

active_chain_length(W) = Σ node.chain_length
                         для node ∈ Node Table : active(node, W)

confirmation_threshold(W) = active_chain_length(W) / 100
~100 confirmers при любом размере сети.

Только активные узлы (cemented BundledConfirmation за последние 2τ₂) учитываются. Мёртвый вес исключён конструкцией. Сканирование Node Table для вычисления active_chain_length — O(|Node Table|) ≤ 10⁵ записей, миллисекунды.

Confirmer собирает все валидные объекты за окно (без разделения на классы) и публикует один BundledConfirmation:

BundledConfirmation:
  node_id         32B
  endpoint        32B     <- текущий NodeChain endpoint (доказывает chain_length)
  window_index     4B
  op_count         2B
  op_hashes[]     op_count × 32B    <- хэши UserObjects и ControlObjects вместе
  signature       666B

Один BundledConfirmation per (node_id, window_index). Повторный отклоняется. Endpoint верифицируем: пересчёт m хэшей от предыдущего известного endpoint данного узла. node.chain_length хранится в Node Table и инкрементируется в apply_proposal шаг 3.5 для каждого узла с cemented BundledConfirmation в окне W. Endpoint BundledConfirmation верифицирует вычисление VDF за соответствующее окно.

Объект финализирован (cemented) когда подтверждения от confirmers с суммарным chain_length > quorum. Cemented — необратимо. Типичное время: ~0.3 секунды. Это правило применяется одинаково к UserObjects и ControlObjects: cemented status объективен и каноничен для всех узлов.

Confirmation cutoff (детерминизм cemented set). Confirmer обязан опубликовать BundledConfirmation окна W до момента T_W_close + R/2 (половина reveal окна). BundledConfirmation окна W, полученные после этого момента, игнорируются при вычислении cemented set окна W. Cutoff делает множество cemented operations окна W объективным и одинаковым у всех узлов: любой узел собирает confirmations за окно W до T_W_close + R/2 → независимо вычисляет один и тот же cemented set.

Dependency rule (детерминизм apply). Одно правило: confirmer подтверждает операцию только если все её зависимости разрешены из settled state окна W-1.

Операция валидна для inclusion в BundledConfirmation окна W если:
  1. prev_hash == Account Table[sender].frontier_hash
     на момент settled state конца окна W-1
  2. Для Transfer: receiver существует в Account Table
     на момент settled state конца окна W-1
  3. sender.balance >= amount (для Transfer)
     на момент settled state конца окна W-1

Settled state конца окна W-1 — результат apply_proposal окна W-1 — одинаков у всех узлов (детерминированная функция от cemented set W-1 и предыдущего state). Confirmer проверяет каждую операцию против этого глобально единого состояния. Никаких bundle-local цепочек, никакого mempool order.

Следствие: одна операция на аккаунт за окно τ₁. Вторая операция от того же sender имеет prev_hash = H(первой операции), но первая ещё не settled (settled = конец текущего окна W). Confirmer отклоняет вторую. Она пройдёт в окне W+1 когда первая settled. Throughput на аккаунт: 1 операция за окно. Это достаточно для всех бытовых сценариев; для высокочастотных — batching через Anchor (один Anchor содержит Merkle root тысяч записей).

Cross-account зависимости сериализуются через окна — создание аккаунта OpenAccount в окне W, получение перевода в окне W+1.

quorum(W) = ⌈0.67 × active_chain_length(W)⌉

Объект cemented когда суммарный chain_length confirmers подтвердивших объект через BundledConfirmation окна W ≥ quorum(W). Активный набор детерминирован — все узлы вычисляют active_chain_length(W) независимо из state Node Table и получают одно и то же значение.

Если active_chain_length падает ниже минимума жизнеспособности (теоретически возможно при массовом offline) — финализация останавливается до восстановления активности. Halt by liveness, не by safety: вернувшиеся узлы возобновляют работу с последнего cemented state.

Трафик confirmations: ~100 bundles × ~4 KB ≈ 400 KB за окно. Стабильно при любом масштабе.

Узлы-наблюдатели (chain_length < threshold) получают bundles, верифицируют endpoint и подписи, подсчитывают quorum, применяют cemented операции. Не публикуют confirmations.

State transition

Два параллельных процесса обновления состояния:

Применение операций по window close. Cemented операции окна W буферизуются до confirmation cutoff (T_W_close + R/2). После cutoff множество cemented операций детерминировано. На момент T_W_close + R (момент сборки proposal) все cemented операции окна W применяются батчем в детерминированном порядке:

Порядок apply: по op_hash lex asc

Каждый аккаунт имеет максимум одну cemented операцию в окне W (dependency rule). Порядок между аккаунтами — лексикографически по op_hash. Детерминирован, вычислим независимо каждым узлом.

Apply каждой операции:

Transfer:     sender.balance -= amount
              receiver.balance += amount
              sender.frontier_hash = H(operation)
              update_merkle_path(sender)
              update_merkle_path(receiver)

OpenAccount:  создать запись в Account Table (balance = 0, pubkey, frontier_hash = H(op))
              insert_merkle_leaf(new_account)

ChangeKey:    account.current_pubkey = new_pubkey
              account.suite_id = new_suite_id
              account.frontier_hash = H(operation)
              update_merkle_path(account)

Anchor:       записать data_hash в цепочку аккаунта (frontier_hash обновлён)
              update_merkle_path(account)

После каждой операции: account_root = current root.

При apply каждой операции обновляется AccountChain length signer-аккаунта (подписавшего операцию):

on_operation_applied(operation, window W):
  signer = operation.sender   # account_id из payload
  signer.account_chain_length += 1
  signer.last_op_window = W
  signer.op_height += 1
  # Получатель Transfer не получает обновления chain_length —
  # пассивное получение не считается активностью.

Dependency rule: один аккаунт = одна операция за окно τ₁. Каждая cemented операция = +1 к account_chain_length = одно окно присутствия.

State transition в proposal: при settle (apply at window close) применяется атомарно:

apply_proposal(state, proposal) -> state':

  Шаг 1: применить control_set в порядке (cemented_window asc, op_hash lex asc).
    NodeInvitation:   создать запись в Invitation Table:
                        invitation_id      = SHA-256("mt-invitation" || inviter_node_id || invited_pubkey || W_p)
                        inviter_node_id    = inviter_node_id
                        invited_pubkey     = invited_pubkey
                        invited_node_id    = SHA-256("mt-node" || invited_pubkey)
                        invite_window      = W_p
                        invite_expires     = W_p + 21 160
                      Node Table инвайтера не изменяется.
    NodeRegistration: найти запись inv в Invitation Table по (inviter_node_id, invited_node_id == node_id),
                      проверить inv.invite_expires > W_p,
                      создать запись в Node Table (start_window = W_p, chain_length = 0,
                      last_confirmation_window = 0, operator_account_id зафиксирован),
                      установить is_node_operator = 1 у operator-аккаунта,
                      удалить запись inv из Invitation Table.

  Шаг 2: применить TimeCoin победителя.
    Если winner_class = 1 (Node): operator_account = Node Table[winner_id].operator_account_id
                                  operator_account.balance += 60_000_000_000 nɈ
    Если winner_class = 2 (Account): Account Table[winner_id].balance += 60_000_000_000 nɈ
                                     Proposer (proposer_node_id) формирует proposal без награды.

  Шаг 3: обработать expiry.
    Все записи inv ∈ Invitation Table где inv.invite_expires <= current_window:
      удалить inv из Invitation Table, обновить invitation_root.

  Шаг 3.5: обновить chain_length активных узлов.
    Для каждого узла N с cemented BundledConfirmation в окне W:
      N.chain_length += 1
      N.last_confirmation_window = W
      update_merkle_path(N) в node_root
    Множество узлов с cemented BundledConfirmation в окне W детерминировано
    (cemented status объективен) — все узлы применяют один и тот же набор обновлений.

  Шаг 4: node_root, invitation_root и account_root уже отражают все cemented изменения
         (incremental Merkle update произошёл при каждом state transition).
         state_root = SHA-256("mt-state-root" || node_root || invitation_root || account_root).

Порядок детерминирован. Каждый узел применяет одни и те же шаги и получает один и тот же state_root.

AccountTable зависит от TimeChain, NodeChain и AccountChain. Обратных зависимостей нет.

С ростом TPS сети дополнительные ядра подключаются для верификации операций. Минимум для валидатора: 3 логических ядра (TimeChain + NodeChain + Account). Один узел = 3 ядра. 50 ядер = 16 узлов. Верификация операций аккаунтов полностью параллелизуется — цепочки аккаунтов независимы.

Приглашение и регистрация

Два уровня входа в сеть. Узлы участвуют в консенсусе — приглашение + 20 160 окон VDF. Аккаунты держат и переводят средства — создаются явно через OpenAccount, без приглашений.

Генезис: 12 узлов в разных локациях (hardcoded, аналог bootstrap nodes в Bitcoin).

Genesis State — аксиома сети. Не результат операций, не финализация. Начальное состояние, существующее до того как любая операция возможна:

Genesis State (до первого окна, supply = 0):

  Account Table = 12 хардкодированных записей:
    account_id      = SHA-256("mt-account" || suite_id || pubkey_i)
    balance         = 0
    suite_id        = 0x0001 (FN-DSA-512)
    is_node_operator = 1
    current_pubkey  = pubkey_i (founder i)
    frontier_hash   = SHA-256("mt-genesis" || account_id)
    op_height       = 0
    account_chain_length = 0
    account_chain_length_snapshot = 0
    creation_window = 0
    (остальные поля = 0)

  Node Table = 12 хардкодированных записей:
    node_id                  = SHA-256("mt-node" || node_pubkey_i)
    node_pubkey              = node_pubkey_i (founder i)
    suite_id                 = 0x0001
    operator_account_id      = account_id_i (i-я запись Account Table)
    start_window             = 0
    chain_length             = 0
    last_confirmation_window = 0

  Invitation Table = ∅ (пустая на genesis)

  Genesis NodeChain init для каждого genesis-узла:
    nodechain_init_i = SHA-256("mt-nodechain-genesis" || node_id_i)
    
    Первое звено NodeChain в окне 0: S_{i,0,0} = nodechain_init_i.
    Дальнейшие звенья по обычной формуле S_{i,s+1,0} = SHA-256(S_{i,s,m} || T_{s+1} || node_id_i).

  genesis_account_root    = sparse Merkle root над 12 записями Account Table
  genesis_node_root       = sparse Merkle root над 12 записями Node Table
  genesis_invitation_root = 0x00 × 32 (пустая sparse Merkle tree)
  genesis_state_root      = SHA-256("mt-state-root" || genesis_node_root || genesis_invitation_root || genesis_account_root)

  protocol_params (каноническая сериализация, little-endian, фиксированная длина полей):
    D₀                             (8B)   начальное значение D TimeChain VDF (2^32)
    m₀                             (8B)   начальное значение m NodeChain VDF
    τ₂_windows                     (8B)   число окон в τ₂ (20 160)
    timecoin_per_window            (16B)  60_000_000_000 nɈ (u128)
    target₀                        (32B)  начальный target лотереи
    confirmation_quorum_num        (1B)   67
    confirmation_quorum_den        (1B)   100
    participation_dead_zone_low    (2B)   85 (соответствует 0.85 × 100)
    participation_dead_zone_high   (2B)   95 (соответствует 0.95 × 100)
    d_adjustment_rate_num          (2B)   2 (+2% / -2% за τ₂)
    d_adjustment_rate_den          (2B)   100
    invite_expiry_windows          (8B)   21 160
    pruning_idle_windows           (8B)   80 640 (4τ₂)
    founders_account_pubkeys       (12 × 897B = 10 764 B)
    founders_node_pubkeys          (12 × 897B = 10 764 B)
    genesis_content_app_id         (32B)  = SHA-256("mt-app" || "montana")
    genesis_content_data_hash      (32B)  хэш манифеста книги Montana v1.0 (хардкодировано)
  
  Genesis State Hash = SHA-256(genesis_state_root || protocol_params)

12 пар (pubkey, node_pubkey) основателей публикуются в Genesis Decree вместе с протокольными параметрами и финальным Genesis State Hash. Genesis Decree immutable — закреплён в коде каждой реализации и не может быть изменён без согласованного software upgrade всех узлов. genesis_content_data_hash обновляется аналогично — через новую версию ПО, не через runtime механизм.

Первое окно τ₁ после генезиса — window_index = 0, protocol_version = 1. Один из 12 узлов выигрывает лотерею → его operator_account.balance += 60_000_000_000 nɈ → supply = 60 Ɉ. Per-operation invariant начинает действовать с этого момента.

Mandatory content replication. Каждый узел Montana обязан хранить текущую версию книги Montana как persistent blob по (genesis_content_app_id, genesis_content_data_hash). При Fast Sync новый узел загружает genesis content как часть обязательной начальной синхронизации (см. раздел Fast Sync).

Приглашение узла (NodeInvitation)

Вход узла в консенсус. Приглашение + 20 160 окон VDF + регистрация. Квота определяет максимум одновременно активных приглашений узла, по chain_length бакету:

Бакет 0:  chain_length 1-4 τ₂          1 одновременное приглашение
Бакет 1:  chain_length 4-16 τ₂         4 одновременных приглашения
Бакет 2:  chain_length 16-64 τ₂        16 одновременных приглашений
Бакет 3:  chain_length 64+ τ₂          64 одновременных приглашения

Границы бакетов = 4^N × τ₂. Квота = 4^N одновременно активных. Та же формула что и для антиспам аккаунтов. Slot освобождается при completion (NodeRegistration cemented) или expiry (21 160 окон) — invites_active(N) уменьшается, узел может создать новое приглашение.

NodeInvitation:
  type               1B   <- 0x10 NodeInvitation
  inviter_node_id   32B
  invited_pubkey   897B     <- публичный ключ приглашённого узла
  signature        666B     <- подписано inviter node_pubkey
Итого:         ~1 596B

NodeInvitation — ControlObject. Не содержит start_window — определяется при apply (окно proposal в который invitation включена в control_set).

Валидация:

  1. Подпись валидна для inviter node_pubkey из Node Table
  2. inviter существует в Node Table и active(inviter, current_window)
  3. invites_active(inviter) < quota(bucket(inviter.chain_length))
  4. invited node_id = SHA-256("mt-node" || invited_pubkey) не существует в Node Table
  5. Не существует записи в Invitation Table с тем же (inviter_node_id, invited_pubkey) в текущей τ₂-эпохе (защита от повторного приглашения того же ключа)

Жизненный цикл NodeInvitation:

  1. Опубликована в окне W₀
  2. Cemented в окне W_c через 67% active_chain_length confirmations (~0.3 сек)
  3. Включена в control_set proposal окна W_p ≥ W_c (первый proposal после cement)
  4. Применена в apply_proposal step 1 окна W_p — создание новой записи в Invitation Table

При apply (proposal P окна W_p) создаётся запись Invitation Table:

inv.invitation_id      = SHA-256("mt-invitation" || inviter_node_id || invited_pubkey || W_p)
inv.inviter_node_id    = inviter_node_id
inv.invited_pubkey     = invited_pubkey
inv.invited_node_id    = SHA-256("mt-node" || invited_pubkey)
inv.invite_window      = W_p
inv.invite_expires     = W_p + 21 160

Node Table инвайтера не изменяется. Множественные параллельные приглашения от одного узла создают независимые записи Invitation Table — single-slot конфликта нет.

Привязка NodeChain к моменту приглашения

Первое звено NodeChain приглашённого узла привязано к каноническим полям proposal в котором NodeInvitation финализирован:

nodechain_init = SHA-256("mt-nodechain-init" || control_root || timechain_value || node_id)

control_root и timechain_value — канонические поля из proposal header окна финализации. Не зависят от субъективного user_set победителя. Предвычисление VDF невозможно: timechain_value неизвестен до закрытия окна.

Приглашённый узел узнаёт control_root и timechain_value только увидев финализированный proposal → вычисляет nodechain_init → начинает NodeChain с окна W+1.

Регистрация узла

Приглашённый узел после финализации NodeInvitation:

  1. Наблюдает proposal с NodeInvitation → получает control_root и timechain_value
  2. Вычисляет nodechain_init = SHA-256("mt-nodechain-init" || control_root || timechain_value || node_id)
  3. Непрерывно строит NodeChain: 20 160 окон подряд (от W+1 до W+20 160), каждое звено якорится в соответствующий TimeChain
  4. Через 20 160 окон получает proof_endpoint = S_{i,20159,m}
  5. Публикует NodeRegistration
NodeRegistration:
  type                  1B   <- 0x11 NodeRegistration
  suite_id              2B
  node_pubkey         897B     <- FN-DSA-512 ключ узла
  inviter_node_id      32B     <- кто пригласил
  operator_account_id  32B     <- account_id оператора, должен существовать в Account Table
  proof_endpoint       32B     <- S_{i,20159,m} (endpoint после 20 160 окон VDF)
  signature           666B     <- подписано node_pubkey
Итого:            ~1 662 B

NodeRegistration — ControlObject.

Валидация NodeRegistration:

  1. Подпись FN-DSA-512 валидна для node_pubkey
  2. node_id = SHA-256("mt-node" || node_pubkey) уникален (не существует в Node Table)
  3. Существует запись inv ∈ Invitation Table где inv.inviter_node_id == inviter_node_id AND inv.invited_node_id == node_id AND inv.invite_expires > current_window
  4. inv.invite_window + 20 160 < current_window (VDF завершён)
  5. operator_account_id существует в Account Table и is_node_operator == 0
  6. Восстановить control_root и timechain_value из proposal окна inv.invite_window
  7. Вычислить nodechain_init = SHA-256("mt-nodechain-init" || control_root || timechain_value || node_id) из proposal окна inv.invite_window
  8. proof_endpoint верифицируем: пересчёт VDF от nodechain_init через 20 160 окон с якорением в TimeChain значения от inv.invite_window + 1

Верификация: 20 160 сегментов VDF проверяются параллельно. На C ядрах: ~(20 160/C) × t_segment.

Жизненный цикл NodeRegistration:

  1. Опубликована в окне W₀ (после 20 160 окон VDF от приглашения)
  2. Cemented в окне W_c через 67% active_chain_length confirmations
  3. Включена в control_set proposal окна W_p ≥ W_c
  4. Применена в apply_proposal step 1 окна W_p

При apply:

  • Проверить Account Table[operator_account_id].is_node_operator == 0 — если 1, отклонить (другая NodeRegistration с тем же operator уже была применена раньше в порядке (cemented_window, op_hash))
  • Создать запись в Node Table: start_window = W_p, chain_length = 0, last_confirmation_window = 0, operator_account_id зафиксирован
  • Установить is_node_operator = 1 у operator-аккаунта
  • Удалить запись inv из Invitation Table (приглашение consumed)

Истечение приглашения узла

Если NodeRegistration не финализирован до inv.invite_expires (= invite_window + 21 160) — приглашённый не завершил VDF. В apply_proposal шаг 3 запись inv удаляется из Invitation Table автоматически. Slot инвайтера освобождается (invites_active(N) уменьшается на 1), узел может приглашать снова.

Создание аккаунта

Аккаунт не требует приглашений. Пользователь генерирует FN-DSA-512 keypair → вычисляет account_id = SHA-256("mt-account" || suite_id || pubkey) → публикует OpenAccount → операция cemented (~0.3 сек) → запись появляется в Account Table при settle (apply at window close). Адрес существует математически до публикации, но операции к нему отклоняются до settle OpenAccount.

Sybil-барьер для аккаунтов: account_age (возраст аккаунта) определяет квоту операций. Новый аккаунт — бакет 0, максимум 1 операция за τ₁. Рост квоты = время. Пустые аккаунты бесполезны — без баланса ничего не делают.

Скорость роста сети

Узлы: квота приглашений определяется chain_length бакетом (4^N за τ₂). Рост ограничен зрелостью сети:

Генезис (12 узлов, бакет 0):     12 × 1 = 12 приглашений за τ₂
После 4τ₂ (бакет 1):             12 × 4 = 48 приглашений за τ₂
После 16τ₂ (бакет 2):            12 × 16 = 192 приглашения за τ₂
После 64τ₂ (бакет 3):            12 × 64 = 768 приглашений за τ₂

С ростом chain_length инвайтеров барьер приглашений размывается: старые узлы приглашают больше, сеть растёт быстрее. Каждый приглашённый узел проходит 20 160 окон VDF независимо от квоты пригласившего.

Аккаунты: без ограничений. Любой владелец TimeCoin может создать аккаунт любому, переведя средства на новый адрес. Рост пользовательской базы не ограничен протоколом.


Потоковая модель

Операции аккаунтов текут непрерывно. Узел получает операцию → проверяет подпись FN-DSA-512 и баланс (против settled state W-1) → передаёт в P2P gossip. Confirmers (~100 узлов с наибольшим chain_length) собирают операции за окно и публикуют BundledConfirmation.

Операция проходит два состояния:

  • Cemented (~0.3 сек): 67% active_chain_length подтвердили. Операция необратима. Баланс ещё не обновлён.
  • Settled (конец окна, ≤ 60 сек): все cemented операции окна применены к Account Table батчем. Баланс обновлён. state_root зафиксирован в proposal.

Два параллельных процесса:

  • Операции подтверждаются непрерывно через confirmations (cement), применяются батчем в конце окна (settle)
  • Часы тикают по расписанию окон τ₁ (TimeChain, NodeChain, лотерея, TimeCoin)

Кошелёк получателя отображает входящий перевод в два этапа: «confirmed» после cement (~0.3 сек), «settled» после apply at window close (≤ 60 сек). Между cement и settle операция уже необратима — различие только для UX индикации.

Цепочки аккаунтов полностью независимы. Операции разных аккаунтов обрабатываются параллельно без конфликтов.


Временные слои (τ)

τ₁ = 1 window  →  τ₂ = 20 160 windows

Одно окно — τ₁. Всё остальное — производные в window counts.

τ₁ — Окно (D хэшей)

Единственная единица канонического времени протокола. Регистрация одного окна Montana Time и эмиссия.

  • TimeChain продвигается на D хэшей

  • NodeChain продвигается на m хэшей с якорем в текущем T_s

  • Операции аккаунтов подтверждаются непрерывно через confirmations (cement), применяются батчем в конце окна (settle)

  • control_set: все cemented ControlObjects из окон (previous_proposal.window, current_window] (каноничен)

  • Кандидаты (~12) раскрывают NodeChain endpoint через VDF_Reveal

  • Лотерея: ticket_i = -ln(endpoint_i / 2^256), победитель = lowest ticket среди кандидатов

  • Окно канонически закрывается в момент quorum event (67% active_chain_length подписали BundledConfirmation окна W)

  • Proposer (proposer_node_id) публикует подписанный proposal

  • Финальность proposal: подпись proposer_node_id на proposal header. Каждый валидатор применяет control_set + TimeCoin детерминированно и проверяет state_root

  • TimeCoin: регистрация одного окна Montana Time (60 Ɉ) → победителю

  • Supply audit: суммарная эмиссия TimeCoin от генезиса сверяется с supply(window_index) = 60 × (window_index + 1) Ɉ

  • Разрешение форков: приоритет ветки с наибольшим суммарным TimeChain-доказательством

TimeChain safety: компрометация значения TimeChain требует нарушения свойства последовательности SHA-256 VDF.

TimeChain liveness: задержка продвижения TimeChain невозможна — TimeChain вычисляется каждым узлом независимо.

τ₂ — Адаптация (20 160 windows)

  • Адаптация D и m через participation-ratio feedback (см. ниже)
  • Snapshot account_chain_length: для каждого аккаунта account_chain_length_snapshot = account_chain_length. Snapshot используется лотереей аккаунтов в течение следующего τ₂. Детерминированно для всех узлов
  • Pruning Account Table: удаление пустых аккаунтов без активности 4τ₂ (80 640 окон) с обновлением Merkle путей
  • Pruning Node Table: для каждого узла N где (current_window - N.last_confirmation_window) > 8 × τ₂_windows:
    1. Если N.operator_account_id существует в Account Table — установить Account Table[N.operator_account_id].is_node_operator = 0 (operator-аккаунт освобождается, может участвовать в лотерее аккаунтов)
    2. Cascade удаление invitations: удалить все записи inv ∈ Invitation Table где inv.inviter_node_id == N.node_id. Приглашённые этими invitations теряют возможность зарегистрироваться (NodeRegistration не пройдёт валидацию — invitation запись отсутствует)
    3. Удалить запись N из Node Table
    4. Пересчитать node_root и invitation_root
  • Supply audit (sanity check): Σ balance(account) для всех аккаунтов = 60_000_000_000 × (window_index + 1) nɈ
  • Криптографическая амнезия: подписанные proposals сохраняются навсегда — верифицируемая цепочка state commitments. Proposals доказывают что конкретное состояние было закоммичено proposer-узлом; восстановление содержимого состояния требует snapshot или архива
  • Пересчёт параметра D через participation-ratio feedback

Адаптация D через participation-ratio feedback

D адаптируется на границе τ₂ через каноническое chain observation — долю активного chain_length-а, успевшего подписать BundledConfirmation в каждом окне предыдущего τ₂.

Канонический вход:

participation_ratio(W) = cemented_chain_length(W) / active_chain_length(W)

Где cemented_chain_length(W) — суммарный chain_length узлов, чьи BundledConfirmation для окна W попали в cemented set; active_chain_length(W) — суммарный chain_length узлов с active(node, W) = true. Оба числа канонически вычисляются каждым узлом bit-exact из Node Table и cemented sets.

Формула адаптации на τ₂ boundary:

median_ratio = median(participation_ratio(W) for W in последние 20 160 окон)

Если median_ratio >= 0.95:  D_new = D_old × 1.02    (+2%, сеть в комфорте, ускоряемся)
Если median_ratio <= 0.85:  D_new = D_old × 0.98    (-2%, сеть под давлением, замедляемся)
Иначе (dead zone):          D_new = D_old           (zone comfort, D не трогаем)

m_new = m_old × (D_new / D_old)

Семантика.

  • median_ratio >= 0.95: большинство активных узлов легко успевают подписать каждое окно. У сети есть запас производительности — D можно поднять, окно станет чуть длиннее в единицах работы, TimeCoin эмиссия замедляется в физическом времени, но сеть укрепляет запас прочности.
  • median_ratio <= 0.85: значительная часть активных узлов не успевает подписать. Сеть близка к границе жизнеспособности — D нужно уменьшить, окно становится короче в единицах работы, медленные узлы получают шанс догнать медиану.
  • Dead zone 0.85-0.95: естественные колебания, D не адаптируется. Это защита от реактивной волатильности.

Свойства.

  • Канонически детерминировано. participation_ratio вычисляется из canonical cemented sets и Node Table. Два честных узла получают одно и то же значение bit-exact.
  • Опирается только на canonical chain observations. Все входные данные формулы — cemented sets и Node Table, оба детерминированы. Corollary I-3.a соблюдён.
  • Медленная реакция. Adjustment rate ±2% за τ₂ делает стратегическую манипуляцию через withholding confirmations экономически нерациональной: actor-у с 10% chain_length-а для сдвига D на 2x требуется систематически saboтировать свои подписи ~35 эпох (~16 месяцев), теряя все свои TimeCoin награды в этот период.
  • Dead zone защищает от флуктуаций. Случайные колебания participation_ratio в диапазоне 0.85-0.95 не вызывают adaptation.
  • Естественное следование hardware progress. Если железо ускоряется, медианные узлы начинают успевать с запасом, median_ratio поднимается выше 0.95, D растёт, окно нормализуется. Сеть автоматически адаптируется к ожидаемому hardware evolution без явного measurement.
  • Нет stремления к hard fork по дизайну. Continuous adaptation в рамках speech-first принципа устраняет необходимость периодического hard fork как запрограммированного события.

Threat model:

  • Actor с <20% chain_length-а экономически не может сдвинуть median_ratio значимо.
  • Hyperscaler с 15% узлов может систематически снижать median_ratio на ~15%, но только теряя свои награды. При clamp ±2% за τ₂ максимальный сдвиг D за год составляет ±1.02^26 ≈ ±66%, что значительно меньше, чем требуется для fatal disruption при правильном выборе D₀ с запасом.
  • Координированная атака узлов с >50% chain_length эквивалентна атаке на весь консенсус и не рассматривается в рамках локальной защиты participation_ratio.

Genesis parameters:

D₀ = 2^32              (≈ 4.3 × 10⁹, каноническое round number)
m₀ = ⌈D₀ / 12⌉         (пропорция 1:12 от TimeChain VDF к NodeChain VDF)
participation_dead_zone_low  = 0.85
participation_dead_zone_high = 0.95
d_adjustment_rate = 0.02     (±2% за τ₂)

Параметры D₀ и m₀ фиксируются в Genesis Decree. Остальные константы закреплены в протокольных параметрах и могут быть изменены только через protocol version upgrade (software hard fork), не через runtime mechanism.


Консенсус — Proof of Time (PoT)

Четыре цепочки

TimeChain — глобальные часы. Чистая VDF-цепочка T_r = SHA-256^D(T_{r-1}). Первичный продукт протокола. Источник времени и случайности. Продвигается по расписанию окон.

NodeChain — персональная цепочка узла. VDF-цепочка конкретного node_id, якорится в TimeChain каждое окно. Доказывает непрерывную работу узла.

Account — состояние счёта. Операции финализируются непрерывно через подтверждения (67% active_chain_length). ControlObjects включаются в proposal (каноничен).

Зависимости односторонние: TimeChain → NodeChain → AccountChain → AccountTable. Отказ в AccountTable не останавливает часы. Отказ конкретного узла в NodeChain не заражает общий ритм.

Лотерея

Лотерея объединяет два класса участников: узлы (через NodeChain) и аккаунты (через AccountChain). Каждый класс производит weighted ticket по длине своей цепочки. Lowest weighted_ticket из объединённого множества побеждает.

Узлы автоматически участвуют в каждом окне:

ticket_node = -ln(endpoint_node / 2^256)
weighted_ticket_node = ticket_node / chain_length

Аккаунты участвуют в окне с финализированной операцией:

ticket_account = -ln(endpoint_account / 2^256)
weighted_ticket_account = ticket_account / account_chain_length

account_chain_length_snapshot обновляется на τ₂ boundary, frozen до следующей τ₂ boundary. Лотерея использует snapshot — детерминированно для всех узлов.

Если weighted_ticket < target — субъект кандидат. Target калиброван на ~12 кандидатов за окно (включая оба класса). Из кандидатов побеждает lowest weighted_ticket.

Стимул узла: каждое окно с опубликованным BundledConfirmation увеличивает chain_length → увеличивает шанс победы. Пропущенное окно — это окно не входит в chain_length. Узел остаётся в Node Table и продолжает участвовать.

Стимул аккаунта: каждое окно с операцией увеличивает account_chain_length → реальный (хоть и редкий) шанс выиграть 60 Ɉ за активность в Montana.

Победитель τ₁

Победитель определяется после закрытия окна τ₁. Lowest weighted_ticket из всех кандидатов (узлов и аккаунтов) = победитель.

Если победил узел:

  • Записывает TimeChain value
  • Operator account узла получает 60 Ɉ TimeCoin
  • Коммитит State Root
  • Формирует proposal (control_set + State Root + TimeCoin), подписывает node_pubkey

Если победил аккаунт:

  • Аккаунт получает 60 Ɉ TimeCoin (winner_account.balance += 60_000_000_000 nɈ)
  • Proposal формирует узел-кандидат с минимальным weighted_ticket в этом окне (proposer_node)
  • Если в окне нет узлов-кандидатов — proposer выбирается из всех узлов с lowest weighted_ticket (fallback)
  • Proposer не получает дополнительной награды — это его обязанность как ближайшего узла

Финальность proposal — подпись proposer_node_id на proposal header. Верификация — независимый пересчёт state_root.

Верификация

Proposer публикует: {proposer_node_id, NodeChain endpoint, proposal}.

Верификация NodeChain за одно окно: пересчёт m хэшей. Параллелизация по сегментам — время верификации обратно пропорционально числу ядер.

Верификация proposal: независимое применение control_set + TimeCoin и сравнение state_root.

Устойчивость

  • Остановка часов исключена: каждый узел тикает независимо
  • Искажение часов исключено: VDF последователен, результат детерминирован
  • Proposer grinding исключён: control_set каноничен, state transition детерминирован, операции финализируются независимо от победителя
  • Front-running исключён: операции финализируются через подтверждения (~0.3s), не через proposal proposer-а
  • Предвычисление исключено: seed содержит текущее значение TimeChain
  • Replay исключён: TimeChain уникален для каждого τ₁
  • Аппаратное преимущество ограничено: последовательное хэширование масштабируется тактовой частотой, не количеством ядер
  • Sybil-барьер: приглашение (квота по chain_length бакету, 4^N за τ₂) + 20 160 окон VDF + 3 ядра на узел + weighted_ticket в лотерее
  • Цензура операций исключена: операции финализируются через подтверждения узлов, не через победителя
  • Цензура ControlObjects исключена: control_set каноничен, пропуск = fallback
  • Liveness halt операций исключён: финализация через 67% active_chain_length, не зависит от победителя
  • Liveness halt proposals исключён: fallback на следующего кандидата
  • Масштабирование: трафик лотереи ~8.9 KB за окно при любом количестве узлов

Разрешение конфликтов

Двойная операция аккаунта (две операции с одним prev_hash): equivocation. Cemented до обнаружения — необратимо, вторая отклоняется. Не cemented — ожидание quorum 10 окон, затем обе отклоняются. См. раздел «Двойная трата».

Невалидный proposal: валидаторы отклоняют, fallback на следующего кандидата. Победитель теряет TimeCoin за это окно.

Два proposal от одного proposer_node_id в одном окне: оба отклоняются (equivocation), fallback к следующему узлу-кандидату. Если этот узел был winner (winner_class=1), он теряет TimeCoin.


Адреса и переводы

Полный флоу перевода

1. Боб: OpenAccount → cemented (~0.3 сек) → settled (конец окна) →
   account_id зарегистрирован в Account Table (balance = 0)
2. Боб → Алисе: "отправь на mt4ZGfe..." (account_id Боба)
3. Алиса формирует Transfer (в следующем окне после settle OpenAccount Боба):
   type:       0x02
   prev_hash:  хэш её предыдущей settled операции (frontier_hash из settled state W-1)
   payload:    sender (account_id Алисы) || link (account_id Боба) || amount (50_000_000_000 nɈ)
4. Алиса подписывает FN-DSA-512
5. Алиса рассылает операцию узлам сети
6. Каждый узел проверяет (против settled state W-1):
   FN-DSA-512 подпись валидна для current_pubkey Алисы
   prev_hash совпадает с frontier_hash Алисы
   amount > 0
   alice.balance >= amount
   получатель (Боб) существует в Account Table
7. Confirmers публикуют BundledConfirmation, операция распространяется через P2P gossip
8. Cement: 67% active_chain_length подтвердили → операция необратима (~0.3 сек)
   Кошелёк Боба отображает «confirmed»
9. Settle (apply at window close):
   alice.balance -= 50 Ɉ
   bob.balance   += 50 Ɉ
   alice.frontier_hash = H(operation)
   alice.op_height += 1
   alice.account_chain_length += 1
   Кошелёк Боба отображает «settled»

Баланс

Баланс аккаунта — открытое число u128 nɈ в Account Table. Обновляется при settle (apply at window close): исходящий Transfer вычитает amount, входящий зачисляет. Видим всем узлам и через любого верификатора цепочки.

Бэкап = seed (для деривации приватного ключа FN-DSA-512). Восстановление кошелька: ключ выводится из seed, баланс читается из текущего Account Table — никакого локального состояния не требуется.


Эмиссия

Единица

Монета: TimeCoin (тикер: $TimeCoin, символ: Ɉ).

1 Ɉ = 1 000 mɈ = 1 000 000 μɈ = 1 000 000 000 nɈ

Одно окно τ₁ регистрирует одну единицу Montana Time = 60 Ɉ. Число 60 — фиксированная конвенциональная константа протокола эмиссии за окно.

Точность: 9 знаков после запятой. Все расчёты эмиссии в nɈ (целочисленная арифметика, без плавающей точки).

Issuance schedule

Одно окно Montana Time порождает 60 Ɉ. С первого окна и навсегда.

Параметр Значение
Genesis window_index = 0
TIME_RECORD 60 000 000 000 nɈ (60 Ɉ за окно)

Регистрация окна

time_record(window_index) = 60_000_000_000 nɈ

Каждое окно τ₁ регистрирует одно каноническое окно Montana Time = 60 Ɉ. Без халвингов, без фаз, без исключений. Одна константа на весь горизонт существования протокола.

Supply audit

supply(window_index) = 60_000_000_000 × (window_index + 1) nɈ

Одно умножение. Проверяемо каждым узлом в каждом τ₁. O(1).

Инфляция

Supply растёт линейно по window_index. Инфляция снижается асимптотически к нулю — константная эмиссия делится на растущий supply:

После 525 960 окон (≈ эквивалент 1 году на genesis-эпоху hardware):  100%
После 1 051 920 окон:                                                  50%
После 2 629 800 окон:                                                  20%
После 5 259 600 окон:                                                  10%
После 26 298 000 окон:                                                  2%
После 52 596 000 окон:                                                  1%
После 525 960 000 окон:                                               0.1%

Приведённые ориентиры относятся к количеству окон с genesis. Физическая длительность любого количества окон в SI-секундах определяется скоростью hardware сети и выводится на клиентском слое через локальные референсы наблюдателя.

Раннее участие

Эмиссия постоянна: 60 TimeCoin за каждое окно, с первого блока и навсегда. Вероятность победы пропорциональна весу. Узел, работающий дольше, побеждает чаще. Два узла запустившиеся одновременно имеют равные шансы независимо от капитала. Узел запустившийся раньше имеет преимущество — он доказал больше времени.

Стимул для ранних участников встроен в арифметику: не бонусы, не множители — просто больший вес.

Распределение

Победитель окна τ₁ — узел или аккаунт — регистрирует одно окно Montana Time и получает 60 Ɉ TimeCoin в своей цепочке. Одно правило. Неизменно с генезиса.

Узлы и аккаунты конкурируют в единой лотерее. Узлы доминируют статистически из-за непрерывного присутствия — chain_length растёт каждое окно, weighted_ticket систематически ниже. Аккаунты получают долю эмиссии пропорционально своей активности — account_chain_length растёт с каждым окном с операцией. Время — единственный арбитр.

Базовый бюджет: 60 Ɉ/τ₁ (регистрация одного окна Montana Time). Реальный бюджет безопасности в покупательной способности зависит от рынка.

60 Ɉ за окно — каноническая константа эмиссии Montana Time. Покупательная способность определяется рынком, а не протоколом.

Двигатель роста сети

Участие аккаунтов в лотерее создаёт flywheel роста сети:

Активные пользователи в приложениях → AccountChain растёт → шансы в лотерее
        ↓                                                          ↓
Приложения привлекают пользователей             Иногда выигрывают TimeCoin
        ↓                                                          ↓
Разработчики хотят пользователей                Дополнительная мотивация активности
        ↓                                                          ↓
Разработчики запускают узлы Montana             Больше операций в сети
        ↓                                                          ↓
Узлы зарабатывают TimeCoin                      Сеть растёт и децентрализуется
        ↓                                                          ↓
Финансирование развития инфраструктуры → больше пропускной способности → лучше для приложений

Эмиссия 60 Ɉ за окно одна и та же, но финансирует обе стороны одновременно: узлы (поддержание сети) и активные пользователи (использование сети). Они не конкурируют — они взаимно усиливают друг друга. Это циркулярная экономика которой нет в Bitcoin.

Reference Emission Ratio

Reference Emission Ratio (RER) — эталонный коэффициент клиентского слоя, выражающий один BTC в единицах Ɉ через отношение эмиссионных правил Bitcoin и Montana.

RER — коэффициент эмиссионных геометрий, а не рыночная цена, обменный курс или рекомендация для торговли.

Область действия

RER существует только на клиентском слое.

  • Вне state machine
  • Вне proposal header
  • Вне консенсуса
  • Вычисляется локально клиентом из публичных данных Bitcoin

Узлы Montana выполняют консенсус без обращения к RER.

Формула

Пусть n — индекс halving-эпохи Bitcoin (n = 0 для периода 2009-2012).

rer_btc(n) = 12 × 2^n  [Ɉ / BTC]

Вывод:

block_reward_btc(n) = 50 / 2^n  BTC
emission_per_btc_block(n) = 600 × 1 Ɉ per Bitcoin second / block_reward_btc(n)
                          = 600 / (50 / 2^n)
                          = 12 × 2^n  [Ɉ / BTC]

Коэффициент использует историческое соответствие 1 Ɉ ↔ 1 Bitcoin second (600 секунд block interval, 50 BTC reward в первой эпохе) как эталонный масштаб для сравнения двух эмиссионных кривых. Это advisory инструмент клиентского слоя для интеграции с Bitcoin экосистемой.

Интерпретация

RER отвечает на вопрос: сколько единиц Montana соответствует одному BTC в текущей subsidy-эпохе Bitcoin по правилам эмиссии обеих цепей.

Это коэффициент эмиссионных кривых. Bitcoin уменьшает выпуск геометрически через halving. Montana добавляет 60 Ɉ за окно линейно. Из-за этого rer_btc(n) удваивается на каждом halving Bitcoin.

Halving-эпохи

n Период (ориентир) Block reward RER (Ɉ/BTC)
0 2009 — 2012 50 BTC 12
1 2012 — 2016 25 BTC 24
2 2016 — 2020 12.5 BTC 48
3 2020 — 2024 6.25 BTC 96
4 2024 — 2028 3.125 BTC 192
5 2028 — 2032 1.5625 BTC 384
6 2032 — 2036 0.78125 BTC 768
7 2036 — 2040 0.390625 BTC 1 536
8 2040 — 2044 0.1953125 BTC 3 072

Границы эпох определяются Bitcoin block height: каждые 210 000 блоков. Даты — ориентировочные.

Свойства

  • Детерминизм. Любой клиент получает одно и то же значение из Bitcoin block height.
  • Автономность от рынка. Биржи, order book и внешние цены вне формулы.
  • Автономность от governance. Значение зависит только от публичных параметров Bitcoin.
  • Историческая верифицируемость. RER для любой эпохи Bitcoin вычислим ретроспективно.
  • Простота. Одна формула, локальное вычисление.

Область применения

RER описывает отношение эмиссионных кривых Bitcoin и Montana. Рыночная цена, справедливая стоимость, ликвидность, волатильность и комиссионный рынок Bitcoin определяются другими источниками. RER корректно интерпретировать как reference emission ratio, а не как price.

Использование

RER может использоваться кошельками и приложениями для:

  • отображения BTC в единицах Montana по эмиссионной геометрии
  • исторического сравнения halving-эпох
  • UI-индикатора эмиссионного отношения Bitcoin и Montana
  • образовательного слоя, объясняющего различие двух эмиссионных кривых

Пропускная способность

Размер Transfer: ~779 B (открытый перевод, FN-DSA-512 подпись).

Канал узла TPS
10 Mbps ~1 600
100 Mbps ~16 000
1 Gbps ~160 000

Хранение

Состояния операции (UX)

Операция проходит два различимых состояния:

publish ──→ cement (~0.3 сек) ──→ settle (≤ 60 сек)
            "confirmed"          "settled"
  • Cemented (~0.3 сек): 67% active_chain_length подтвердили операцию через BundledConfirmation. Операция необратима и гарантированно будет применена в конце окна. Wallet показывает «confirmed».
  • Settled (≤ 60 сек, в конце окна): все cemented операции окна применены батчем к Account Table в детерминированном порядке. account_root зафиксирован в proposal. Wallet показывает «settled».

Между cement и settle операция уже необратима — настройка двух UI-состояний нужна только для индикации завершённости state transition. Зависимые операции (Transfer на только что созданный аккаунт) сериализуются по окнам через confirmer dependency rule, поэтому cemented операция гарантированно settle-ится.

Модель: глобальное состояние + локальная история

Узлы хранят глобальное состояние (Account Table, Node Table, Invitation Table, proposals). Тела операций аккаунтов хранятся у владельцев. После settle (apply at window close) state transition применён — балансы в таблице обновлены, тело операции сети больше не нужно.

Три уровня участников

Узел (валидатор) — десктоп или сервер, 24/7, минимум 3 ядра (1 узел = 3 ядра, 50 ядер = 16 узлов):

Хранит:
  Account Table              (account_id, balance, frontier_hash, pubkey)
                             + persistent sparse Merkle tree (account_root обновляется при settle)
  Node Table                 (node_id, pubkey, start_window, chain_length)
                             + persistent sparse Merkle tree (node_root обновляется при settle)
  Invitation Table           (invitation_id, inviter, invited, window, expires)
                             + persistent sparse Merkle tree (invitation_root обновляется при settle)
  Proposals                  (навсегда)
  Blob Buffer (ephemeral)    (TTL = τ₂, для кратковременных сообщений)
  Persistent Blob Storage    (TTL = 0, контент app_id на которые узел подписан)
  Genesis Content            (книга Montana, mandatory replication)
  Persistent Blob Index      ((app_id, data_hash) → blob, с флагом is_manifest)

Делает:
  TimeChain VDF              (1 ядро, 24/7)
  NodeChain VDF              (1 ядро, 24/7)
  Валидация операций         (1+ ядро)
  P2P gossip                 (операции, confirmations, reveals, proposals)
  Почтовый ящик              (хранит сообщения для своего владельца пока тот офлайн)
  Content replication        (DHT provide, gossip announce, serving ContentRequest/ChunkRequest)
  Chunk verification         (SHA-256 + Merkle reconstruction для полученных чанков)

Кошелёк (клиент) — телефон, онлайн когда используется:

Хранит:
  Свои ключи            (seed → keypairs, включая encryption key для application layer)
  Свои контакты         (адресная книга: имя → mt-адрес, с локальным override)
  Локальная история     (своя цепочка операций для UX)
  Сообщения             (локальная история переписки, messenger session states)
  Timestamp proofs      (Anchor + BundledConfirmations + proposal headers, локально)
  Подписки              (app_id каналов, книги, профилей на которые подписан)
  Реплики контента      (persistent blobs подписанных app_id по желанию)

Делает:
  Отправка/получение переводов
  Мессенджер (P2P напрямую через libp2p)
  Discovery (через application layer)
  Запрос pubkey и proposals у узлов сети

Доверенный узел — узел друга, семьи, сообщества:

Делает:
  Всё что узел + хранит Blob Buffer для привязанных аккаунтов
  Владелец аккаунта привязывает свой account_id к доверенному узлу
  Узел хранит зашифрованные сообщения (содержимое скрыто)
  Владелец забирает сообщения когда появляется онлайн

Размеры

Участник Данные Размер
Узел (1M аккаунтов) Account Table + Node Table + Invitation Table + Proposals ~2 GB
Узел (10M аккаунтов) Account Table + Node Table + Invitation Table + Proposals ~11 GB
Узел (100M аккаунтов) Account Table + Node Table + Invitation Table + Proposals ~101 GB
Кошелёк (обычный) 100 операций/год + контакты + сообщения ~1 MB
Кошелёк (активный) 10 000 операций/год ~16 MB
Корпорация 1M Anchor/год ~0.8 GB

Потеря данных клиента

Потеря устройства: баланс в Account Table цел и публичен, seed восстанавливает ключи, доступ к аккаунту полностью восстанавливается. Локальная история переводов и сообщений утрачена — но баланс читается из Account Table напрямую. Если есть доверенный узел — зашифрованные сообщения можно восстановить.

Fast Sync (новый узел)

  1. Цепочка proposals от генезиса — проверка TimeChain-цепочки и подписей proposer-узлов (мегабайты)
  2. Snapshot трёх таблиц (Account Table + Node Table + Invitation Table) от пиров на момент окна W (произвольное недавнее окно)
  3. Reconstructed account_root, node_root и invitation_root сравниваются с соответствующими полями из proposal окна W. Все три совпадают → snapshot валиден. Проверка state_root = SHA-256("mt-state-root" || node_root || invitation_root || account_root) — дополнительный integrity check.
  4. Catch-up после окна W до текущего:
    • Запросить cemented UserObjects и применить их батчем к Account Table по алгоритму apply at window close (включая проверку prev_hash и баланса).
    • Запросить cemented ControlObjects (NodeInvitation, NodeRegistration) и применить их к Node Table и Invitation Table в детерминированном порядке.
    • Выполнить incremental update Merkle trees (account_root, node_root, invitation_root) для отражения changes.
    • На каждом промежуточном proposal сверять локальный state_root с заявленным в proposal header
  5. Mandatory genesis content replication. Загрузить книгу Montana через Content Layer:
    • ContentRequest(genesis_content_app_id, genesis_content_data_hash) → manifest
    • Для каждого чанка: ChunkRequest + верификация SHA-256
    • Пересчёт Merkle root манифеста → сравнение с genesis_content_data_hash
    • Без успешной загрузки genesis content Fast Sync считается неполным
  6. Узел синхронизирован и готов к участию

Snapshot привязан к конкретному proposal (settled state после apply at window close). Catch-up дистанция определяется свежестью snapshot, обычно минуты.


Application Layer

Montana — цифровой стандарт времени. Приложения управляют своим состоянием самостоятельно (серверы, базы данных, P2P). Montana хранит только криптографические отпечатки с привязкой ко времени — 32 байта на запись.

Модель приложения на Montana

Приложение Montana — это набор узлов с интерфейсом. Создатель приложения запускает узлы Montana (обычные узлы, тикающие VDF, валидирующие операции, участвующие в консенсусе). Узлы зарабатывают TimeCoin за поддержание сети через лотерею.

Для создателя приложения:

  • Не нужно строить отдельную инфраструктуру безопасности — приватность данных через Anchor (хэш в сети, контент у владельца зашифрованным), антицензура через Transport Obfuscation и Dandelion++, децентрализация через отсутствие центрального сервера получаются бесплатно из протокола
  • Бизнес-модель: эмиссия Montana через узлы создателя. Не реклама, не подписка, не продажа данных
  • Чем больше пользователей в приложении → тем больше операций в сети → тем больше нужно узлов для обслуживания (Blob Buffer, валидация, P2P gossip) → больше узлов = больше шансов в лотерее = больше TimeCoin

Для пользователя:

  • Каждое действие в приложении создаёт операцию в его AccountChain
  • account_chain_length растёт автоматически с каждым окном с операцией
  • Пользователь автоматически участвует в лотерее в каждом окне с операцией — без заявок, без стейкинга, без понимания криптографии
  • Шанс победы зависит от account_chain_length — длинная активная цепочка даёт реальные шансы выиграть 60 Ɉ
  • Ничего не привязано к конкретному приложению — seed принадлежит пользователю, account_id переходит между приложениями без потери истории

Нулевая стоимость переключения приложений. AccountChain пользователя — его собственность. Если приложение закрылось или пользователь хочет уйти — account_id, баланс, история и накопленный account_chain_length остаются. Пользователь продолжает в другом приложении на том же протоколе. Приложения вынуждены конкурировать качеством, а не замком.

Двигатель роста сети через AccountChain

Лотерея Montana объединяет два класса участников: узлы (NodeChain) и аккаунты (AccountChain). Узлы доминируют статистически из-за непрерывного присутствия. Аккаунты получают долю эмиссии через активность пользователей. Эта механика создаёт самоподдерживающийся цикл роста сети — см. раздел "Эмиссия → Двигатель роста сети".

Anchor

Одна операция, данные навсегда привязаны к timechain_value конкретного окна.

Anchor:
  prev_hash              32B
  account_id             32B
  app_id                 32B     <- SHA-256("mt-app" || app_name)
  data_hash              32B     <- Merkle root, H(document), произвольный хэш
  signature             666B
Итого:               ~796B

app_id — детерминированный идентификатор пространства имён. Вычисляется из имени приложения, регистрация не требуется. Позволяет фильтровать, индексировать, строить лёгкие клиенты для конкретного приложения.

Timestamp Proof

Стандартный формат доказательства: документ D существовал не позже момента T.

В модели v20.2.0 операции аккаунтов финализируются через BundledConfirmations узлов-confirmers, не через включение в proposal. Доказательство существования Anchor — набор подписанных подтверждений с суммарным chain_length ≥ quorum.

Proof собирается владельцем Anchor в момент финализации и хранится локально вместе с документом. Сеть не обязана хранить BundledConfirmations долгосрочно — ответственность за сохранение proof лежит на стороне, которой нужно доказать timestamp.

Структура proof:
  1. Документ D и H(D)
  2. Anchor body (prev_hash, account_id, app_id, data_hash, signature)
  3. Если data_hash = MerkleRoot batch'а: Merkle path от H(D) до data_hash
  4. Набор BundledConfirmations за окно W cementing'а Anchor:
     - каждая содержит H(Anchor) в op_hashes[]
     - каждая подписана confirmer node_pubkey
     - каждая содержит endpoint NodeChain confirmer на момент окна W
     - суммарный chain_length confirmers ≥ 67% active_chain_length(W)
  5. Proposal header окна W (содержит timechain_value = T)
  6. Цепочка proposal headers от W до genesis (через prev_proposal_hash)

Верификация любым третьим лицом, без доверия Montana-узлу:
  1. Если есть Merkle path: пересчитать H(D) → data_hash, сравнить с data_hash в Anchor
  2. Проверить FN-DSA-512 подпись на Anchor
  3. Для каждой BundledConfirmation: проверить FN-DSA-512 подпись confirmer
  4. Для каждой confirmation: пересчитать NodeChain endpoint от start_window до окна W,
     подтвердить заявленный chain_length
  5. Суммировать chain_length подтверждающих, проверить ≥ 67% active_chain_length(W)
  6. Из proposal header окна W взять timechain_value = T
  7. Пересчитать TimeChain VDF от proposal окна W до genesis по prev_proposal_hash

Proposals хранятся навсегда — timechain_value(W) и цепочка к genesis всегда доступны. BundledConfirmations хранятся локально владельцем proof. Timestamp proof самодостаточен и верифицируем в любой момент в будущем.

Примеры

Мессенджер. Каждое сообщение хэшируется, цепочка хэшей формирует Merkle root, Merkle root записывается в Anchor раз в минуту или час. Montana хранит 32 байта — доказательство что набор сообщений существовал в конкретный момент. Подделать историю переписки невозможно — хэш не совпадёт.

Архив документов. Компания ежедневно записывает Merkle root документов. Через 10 лет регулятор спрашивает «существовал ли документ X на дату Y». Компания предоставляет документ, Merkle proof и ссылку на proposal. Верификация математическая.

Социальная сеть. Каждый пост привязан к Montana Time через Anchor. Порядок публикаций доказуем. Редактирование не скрывает оригинал — хэш оригинала уже в цепочке.

Экономика

Anchor бесплатен. Тысячи приложений записывающих якоря — утилитарное использование Montana Time. Спрос на токен привязан к утилитарной функции: перевод ценности и запись времени, не спекуляция.

Не нужны смарт-контракты. Не нужен Turing-complete язык. Не нужен газ. Не нужны комиссии.

Phone Discovery и Messenger

Phone discovery и messenger — полностью на Application Layer, не в protocol core. Protocol не знает о телефонах, именах, сообщениях. Application Layer реализует эти функции через Content Layer (см. ниже) и Interop Standards.

Протокол предоставляет:

  • Identity: account_id через OpenAccount
  • Timestamping: Anchor с произвольным data_hash
  • Storage: Blob Buffer для хранения произвольных байт (persistent и ephemeral режимы)
  • Transport: libp2p gossip и Content Request Protocol

Всё остальное — phone discovery, encryption, messaging protocols, profiles — реализуется на уровне приложения. Стандарты совместимости фиксированы в разделе Application Layer Interop Standards.

Content Layer

Content Layer предоставляет механизм хранения и репликации произвольных данных между узлами с привязкой к Anchor. Узлы подписанные на app_id хранят контент этого app_id. Новый узел или узел восстанавливающийся после offline скачивает недостающие блобы у пиров, верифицирует целостность через хэши из Anchor. Целевая задача — децентрализованное облако данных где каждый подписчик является хранителем, а факт существования контента зафиксирован навсегда через Anchor в proposal chain.

Persistent Blob

Blob Buffer получает второй режим хранения:

  • TTL = τ₂ (ephemeral) — кратковременные сообщения, удаляются через τ₂
  • TTL = 0 (persistent) — контент привязанный к Anchor, хранится бессрочно пока узел подписан на соответствующий app_id

Persistent blob индексируется парой (app_id, data_hash) → blob_bytes. Блоб может содержать manifest чанкованного контента (флаг is_manifest = true) или один чанк/целый файл. Размер одного blob ограничен chunk_size. Удаление persistent blob — решение оператора узла через явную отписку от app_id, не автоматически по таймеру.

Chunking Standard

Большие файлы разбиваются на чанки фиксированного размера:

chunk_size = 256 KB (фиксировано протоколом)

chunk formaт: chunk_index (4B, u32) || chunk_data (≤262 144 bytes)
chunk_hash   = SHA-256("mt-content-chunk" || chunk_data)

Manifest содержит метаданные файла и упорядоченный список chunk_hashes:

Manifest {
  version:       u16    (currently 1)
  file_name:     string (UTF-8, length-prefixed, max 256 bytes)
  file_size:     u64
  mime_type:     string (UTF-8, length-prefixed, max 64 bytes)
  chunk_count:   u32
  chunk_hashes:  [32B × chunk_count]
}

Merkle tree строится поверх chunk_hashes. Корень дерева:

data_hash = SHA-256("mt-content-manifest" || canonical_serialization(Manifest))

Этот data_hash записывается в Anchor. Маленький файл (< chunk_size) — один чанк, manifest с chunk_count = 1.

Manifest сохраняется как первый persistent blob по (app_id, data_hash) с флагом is_manifest = true. Индекс узла хранит эту связь, позволяя быстро находить manifest для любого Anchor.

Content Request Protocol

P2P сообщения libp2p для обмена контентом между узлами:

ContentRequest:
  app_id      32B
  data_hash   32B    (data_hash из Anchor — manifest root)

ContentResponse:
  status      1B     (0 = ok, 1 = not_found, 2 = error)
  payload     variable  (serialized Manifest если status = 0)

ChunkRequest:
  data_hash   32B    (data_hash манифеста)
  chunk_index 4B

ChunkResponse:
  status      1B
  chunk_data  variable  (до chunk_size байт)

Процесс верификации при получении:

  1. Получив Manifest: десериализовать, проверить каноническая форма, пересчитать data_hash = SHA-256("mt-content-manifest" || serialization), сравнить с запрошенным
  2. Получив чанк: пересчитать chunk_hash = SHA-256("mt-content-chunk" || chunk_data), сравнить с соответствующим элементом chunk_hashes в manifest
  3. После сбора всех чанков: пересчитать Merkle tree из chunk_hashes, сравнить корень с data_hash из Anchor в proposal
  4. Любое несовпадение — отклонить ответ пира, запросить у другого пира, пометить нечестного пира в local blacklist транспорта

Content Discovery

Два параллельных механизма поиска провайдеров контента:

DHT provide/lookup (Kademlia):

  • Узел хранящий контент app_id публикует запись "я провайдер для app_id X" в Kademlia DHT
  • Запрашивающий узел делает lookup по app_id, получает список провайдеров
  • Подключается к провайдерам, запрашивает контент через ContentRequest/ChunkRequest
  • Стандартный libp2p content routing

Gossip announce:

  • При установлении соединения с новым пиром узел в handshake объявляет список своих app_id (Bloom filter если список большой)
  • Пир запоминает привязку пир → app_id
  • При локальном ContentRequest по app_id которого нет — пересылает запрос пирам объявившим этот app_id

Оба механизма работают параллельно. Узел использует любой рабочий путь. Content Discovery — локальная network state, не входит в consensus.

Genesis Content

Книга Montana — первый и обязательный контент сети, зафиксированный в Genesis Decree:

genesis_content_app_id    = SHA-256("mt-app" || "montana")
genesis_content_data_hash = <хэш манифеста книги Montana v1.0, хардкодировано в Genesis Decree>

Mandatory replication. Каждый узел Montana обязан хранить текущую версию книги Montana как persistent blob. Это часть протокольного определения "узел Montana", не optional подписка. При Fast Sync новый узел загружает genesis content вместе с initial state; без успешной загрузки sync считается неполным.

Обновление книги. Автор публикует новый Anchor в genesis_content_app_id с новым data_hash. Узлы получают новую версию через Content Request Protocol, верифицируют через Merkle reconstruction, заменяют локальную копию. Старые версии остаются доступными через исторические Anchor в proposals навсегда — версии книги историчны и неудаляемы. Обновление значения genesis_content_data_hash в protocol_params возможно только через software upgrade узла (новая версия Rust core с обновлённой константой), как любое изменение Genesis Decree.

Application Layer Interop Standards

Этот раздел фиксирует минимальные стандарты для совместимости между приложениями Montana. Приложения следующие этим стандартам могут взаимодействовать между собой — обмениваться профилями, сообщениями, контентом. Приложения использующие другие форматы работают в изоляции.

Это нормативный раздел: форматы и формулы в нём обязательны для interop-совместимых приложений.

Canonical app_id registry

Фиксированные app_id для стандартных функций приложений:

Функция Формула Назначение
genesis content SHA-256("mt-app" || "montana") Книга Montana и её обновления
profile SHA-256("mt-app" || "profile") Публичные профили пользователей
encryption keys SHA-256("mt-app" || "encryption-keys") Discovery encryption pubkeys
messenger prekeys SHA-256("mt-app" || "messenger-prekeys") Pre-keys bundles для Double Ratchet
phone discovery SHA-256("mt-app" || "phone-discovery") Public mode phone → account lookup

Пользовательские каналы используют формулу SHA-256("mt-app" || channel_name) где channel_name — произвольная строка выбранная создателем канала. Уникальность каналов обеспечивается через уникальность имени; коллизии разрешаются по первому cemented Anchor в данном app_id.

ProfileBlob format

Канонический формат публичного профиля пользователя:

ProfileBlob {
  version       u16    (currently 1)
  display_name  string (UTF-8, length-prefixed, max 64 bytes, may be empty)
  avatar_hash   32B    (ref to image blob by data_hash, или 0x00..00)
  bio           string (UTF-8, length-prefixed, max 256 bytes, may be empty)
  updated_at    u64    (unix timestamp публикации)
}

Сериализация: little-endian, length-prefix для строк (u16 length + bytes).

Публикация профиля:

  1. Serialize ProfileBlob канонически
  2. data_hash = SHA-256("mt-profile" || serialized)
  3. store_blob(app_id_profile, data_hash, serialized)
  4. publish_anchor(app_id_profile, data_hash)

Lookup профиля другого пользователя:

  1. Запросить через Anchor history: все Anchor с app_id = profile и sender = target_account_id
  2. Отсортировать по времени (окно финализации Anchor), взять новейший
  3. fetch_blob(app_id_profile, latest_data_hash)
  4. Deserialize

Profile опционален — пользователь может не публиковать профиль. Приложение должно поддерживать локальные override имён независимо от публичного профиля (пользователь может видеть контакт как "Мама" локально, даже если публичный профиль контакта другой).

Published encryption_pubkey format

Формат блока публикации encryption key пользователя для приёма зашифрованного контента:

EncryptionKeyBlob {
  version            u16    (currently 1)
  algorithm          u16    (1 = ML-KEM-768)
  encryption_pubkey  variable  (1184B для ML-KEM-768)
  published_at       u64    (unix timestamp)
}

Публикация:

  1. Serialize EncryptionKeyBlob
  2. data_hash = SHA-256("mt-encryption-key" || serialized)
  3. store_blob(app_id_encryption_keys, data_hash, serialized)
  4. publish_anchor(app_id_encryption_keys, data_hash)

Lookup encryption key получателя:

  1. Запросить Anchor history: все Anchor с app_id = encryption-keys и sender = target_account_id
  2. Взять новейший (последняя ротация ключа)
  3. fetch_blob(app_id_encryption_keys, latest_data_hash)
  4. Deserialize, извлечь encryption_pubkey

Key rotation. Публикация нового Anchor с новой EncryptionKeyBlob. Старые ключи остаются в proposal history навсегда — старые ciphertexts расшифровываются если владелец сохранил старый seckey.

Messenger pre-keys bundle format

Для инициализации Double Ratchet PQ session с offline получателем. Пользователь публикует pre-keys bundle заранее; отправитель использует его для первого сообщения без ответа получателя.

PreKeyBundle {
  version             u16    (currently 1)
  identity_key        variable (ML-KEM-768 identity pubkey, 1184B)
  signed_prekey       variable (ML-KEM-768 signed pre-key, 1184B)
  prekey_signature    666B   (FN-DSA-512 подпись signed_prekey identity key)
  one_time_prekeys    [variable]  (массив ML-KEM-768 pubkeys, одноразовые)
  published_at        u64
}

Публикация:

  1. Serialize PreKeyBundle
  2. data_hash = SHA-256("mt-prekeys" || serialized)
  3. store_blob(app_id_messenger_prekeys, data_hash, serialized)
  4. publish_anchor(app_id_messenger_prekeys, data_hash)

Refresh. При исчерпании one_time_prekeys (каждое pre-key используется одним отправителем и удаляется) публикуется новый bundle. Получатель должен мониторить использование pre-keys и публиковать fresh bundle заранее.

Phone discovery (public mode)

Опциональная функция для приложений поддерживающих поиск по номеру телефона.

Формула: phone_hash = SHA-256("mt-phone-public" || phone_e164)

Где phone_e164 — номер телефона в формате E.164 (например +79991234567).

Публикация в public mode:

  1. Пользователь явно включил public phone discovery в приложении
  2. Приложение вычисляет phone_hash
  3. data_hash = phone_hash
  4. Persistent blob содержит account_id владельца (32B)
  5. store_blob(app_id_phone_discovery, data_hash, account_id)
  6. publish_anchor(app_id_phone_discovery, data_hash)

Lookup:

  1. Приложение для каждого контакта из адресной книги вычисляет phone_hash
  2. fetch_blob(app_id_phone_discovery, phone_hash) → account_id или not_found
  3. Если найден — контакт в сети Montana

Privacy warning. Public mode подвержен rainbow table attack. Атакующий со списком phone numbers может вычислить phone_hashes и искать совпадения в сети. Это эквивалентно модели WhatsApp. Пользователь выбирает режим осознанно.

Private mode (рекомендованный по умолчанию). Пользователь не публикует phone_hash. Контакты добавляются только через QR-код, ссылку или прямой обмен account_id. Максимальная приватность, ручной ввод контактов.

Эти примитивы не входят в protocol core (core остаётся с SHA-256 + FN-DSA-512). Они рекомендованы для Application Layer encryption и messaging чтобы обеспечить совместимость между приложениями. Приложения использующие другие примитивы работают в изоляции.

Примитив Стандарт Применение
ML-KEM-768 FIPS 203, NIST PQC Key encapsulation для encrypted messaging и file encryption
ChaCha20-Poly1305 RFC 8439 Symmetric AEAD encryption содержимого
HKDF-SHA-256 RFC 5869 Derivation ключей из KEM shared secret

Все три примитива постквантово-безопасны или симметричны (ChaCha20-Poly1305 ослабляется Grover до 128-bit, приемлемо). Production-ready реализации доступны для всех major языков.

Application-level key derivation из seed. Encryption keypair (ML-KEM-768) выводится из того же seed что и account и node keypairs, через отдельный domain separator mt-encryption-key (см. раздел Деривация ключей). Восстановление seed из мнемоники восстанавливает все три keypair одной операцией.

Integration

Три операции для подключения внешних систем к Montana.

Write — запись

Внешняя система формирует Anchor и отправляет в P2P-сеть.

Вход:  app_id (32B) + data_hash (32B) + подпись FN-DSA-512
Выход: Anchor финализирован в окне W через ≥67% active_chain_length
       confirmations с timechain_value T_W

data_hash — произвольный хэш: Merkle root документов, хэш batch'а Rollup, fingerprint состояния. Montana не интерпретирует содержимое — хранит 32 байта с привязкой ко времени.

Read — сбор proof

Внешняя система собирает timestamp proof в момент финализации Anchor.

Вход:  Anchor (только что финализированный)
Выход: Anchor body + BundledConfirmations покрывающие H(Anchor) +
       proposal header окна cementing'а + цепочка proposal headers до genesis

Сбор proof — клиентская задача. После получения BundledConfirmations с суммарным chain_length ≥ quorum клиент сохраняет proof локально. Узлы Montana не обязаны хранить BundledConfirmations долгосрочно — они нужны только для текущего подсчёта quorum.

Verify — верификация

Внешняя система проверяет proof автономно, без доверия к Montana-узлу.

1. Если есть Merkle path: пересчитать H(D) → data_hash в Anchor
2. Проверить FN-DSA-512 подпись на Anchor
3. Для каждой BundledConfirmation в proof:
   a. Проверить FN-DSA-512 подпись confirmer
   b. Пересчитать NodeChain endpoint confirmer от start_window до окна W
   c. Подтвердить заявленный chain_length
4. Суммировать chain_length подтверждающих ≥ 67% active_chain_length(W)
5. Проверить FN-DSA-512 подпись proposer на header окна W
6. Проверить timechain_value(W) пересчётом TimeChain VDF от T_{W-1}
7. Проверить цепочку proposals от W до genesis (prev_proposal_hash)

Шаги 1, 2, 3a, 5: ~O(1) хэш-операций (константное число CPU-циклов). Шаг 3b: пересчёт NodeChain VDF confirmer — m × (W start_window) хэшей, параллелизуется по сегментам. Шаг 6: D хэшей на одном ядре (один сегмент TimeChain VDF). Шаг 7: линейная проверка подписей и хэшей по цепочке proposals от окна W до genesis.

Полная верификация от генезиса: H сегментов TimeChain VDF, каждый независим. На C ядрах: ~(H/C) × D хэшей. TimeChain хранит все промежуточные T_r в proposals — параллелизация полная.


Ключи

Мнемоника и seed

24 слова из словаря BIP-39 (2 048 английских слов). 256 бит энтропии + 8 бит checksum.

mnemonic:  24 слова BIP-39
seed:      PBKDF2-SHA512(mnemonic, "mt-seed", 2048 итераций)

Деривация

seed
├── Аккаунт:   FN-DSA-512 keypair (HMAC-SHA256(seed || "mt-account-key"))
│              → account_id = SHA-256("mt-account" || account_pubkey)
└── Узел:      FN-DSA-512 keypair (HMAC-SHA256(seed || "mt-node-key"))
               → node_id = SHA-256("mt-node" || node_pubkey)

Один seed порождает два FN-DSA-512 keypair. Аккаунт — подпись операций пользователя. Узел — подпись proposals и NodeChain endpoints. Это полный набор секретных материалов protocol core.

account_id и node_id выводятся из публичных ключей, верифицируемы без знания seed.

Application-level keys. Приложения могут выводить дополнительные ключи из того же seed через свои domain separators (не protocol-critical). Рекомендованный стандарт Application Layer определяет encryption keypair через HMAC-SHA256(seed || "mt-app-encryption-key") для ML-KEM-768. Этот ключ не входит в protocol core — его знание не нужно узлам консенсуса. Приложения использующие этот стандарт получают совместимое восстановление: один seed → все ключи (account, node, encryption).

Следствие: любое устройство с seed получает полный доступ к аккаунту. Баланс читается из текущего Account Table — никакого локального состояния не требуется. Бэкап = 24 слова, восстановление мгновенное.


Криптографическая реализация

Primitive layer

Собственная реализация криптографических примитивов запрещена. Только audited библиотеки с constant-time гарантиями и опубликованными test vectors.

Примитив Стандарт Роль
SHA-256 FIPS 180-4 TimeChain, NodeChain, адреса, Merkle-деревья
FN-DSA-512 NIST PQC selection финал (июль 2022), forthcoming FIPS 206, reference implementation production-ready Подписи операций аккаунтов и proposals узлов

Consensus encoding layer

Консенсусно-критическая поверхность: каноническая сериализация, Merkle layout и domain separation. Разная сериализация одного объекта = разный хэш = форк. Требования:

  • Fixed binary encoding для каждого консенсусного объекта
  • Length-prefix кодирование полей, фиксированный endianness (little-endian)
  • Domain separation для всех хэшей:
Домен Контекст
mt-op Хэширование операций аккаунтов
mt-header Хэширование proposal headers
mt-account Деривация account_id
mt-invitation Деривация invitation_id и хэширование NodeInvitation
mt-merkle-leaf Листья Merkle-деревьев
mt-merkle-node Внутренние узлы Merkle-деревьев
mt-state-root Композиция state_root из node_root, invitation_root и account_root
mt-timechain TimeChain VDF seed
mt-nodechain-init NodeChain init seed
mt-confirmation Хэширование async confirmations
mt-app Деривация app_id для Application Layer
mt-node Деривация node_id
mt-genesis Деривация frontier_hash genesis-аккаунтов
mt-nodechain-genesis Деривация nodechain_init для genesis-узлов
mt-seed Salt для PBKDF2 деривации seed из мнемоники
mt-account-key Деривация keypair аккаунта из seed
mt-node-key Деривация keypair узла из seed
mt-account-lottery Endpoint AccountChain для лотереи
mt-content-chunk Хэширование чанков контента в Content Layer
mt-content-manifest Хэширование манифеста чанкованного контента
mt-profile Хэширование ProfileBlob в Application Layer
mt-encryption-key Хэширование EncryptionKeyBlob в Application Layer
mt-app-encryption-key Деривация encryption keypair из seed в Application Layer
mt-prekeys Хэширование PreKeyBundle в Application Layer
mt-phone-public Хэширование phone_hash для public mode phone discovery
mt-tunnel IBT proof подпись при входе на узел
mt-bootstrap-pow Proof-of-work при подключении к bootstrap
  • Альтернативные сериализации запрещены
  • Test vectors для каждого консенсусного объекта
  • Cross-implementation conformance tests перед запуском mainnet

Protocol layer

Собственная реализация поверх криптографического ядра:

Компонент Назначение
Merkle-деревья State Root (из SHA-256 вызовов)
VDF scheduling Управление TimeChain и NodeChain цепочками
State machine Account Table, Node Table, state transitions
P2P gossip Распространение операций, confirmations и proposals

Инфраструктура

Библиотека Назначение
RocksDB Хранение Account Table и операций
libp2p P2P транспорт

Production: Rust.


Сетевой уровень

Transport Obfuscation

Монтана — персональная сеть. Каждый узел — персональный сервер участника. Транспортный слой построен из этого определения: персональный сервер отвечает только участникам, персональный мессенджер скрывает тайминг сообщений, персональный = доступный обычному человеку.

Шифрование

Все P2P-соединения инкапсулированы в TLS 1.3 на порт 443. Noise framework (встроен в libp2p) для аутентификации по публичному ключу узла внутри TLS. Содержимое трафика недоступно наблюдателю.

Identity-Bound Tunnel (IBT)

Персональный сервер отвечает только участникам сети. После TLS handshake клиент отправляет proof аутентификации. Узлы (зарегистрированные и приглашённые) подписывают node keypair. Аккаунты (клиенты) подписывают account keypair.

proof = FN-DSA-512_sign(client_privkey,
          "mt-tunnel" || server_node_id || floor(unix_timestamp / 120))

Сервер проверяет:

  1. Подпись валидна для заявленного client_pubkey
  2. Timestamp slot = текущий ИЛИ предыдущий (окно 4 минуты)
  3. Уровень доступа — сервер проверяет client_pubkey по трём таблицам последовательно, первое совпадение определяет уровень:
    • node_id = SHA-256("mt-node" || client_pubkey) в Node Table → полный gossip (клиент подключился node keypair)
    • invitation_id с invited_pubkey = client_pubkey в Invitation Table → read-only gossip: получает proposals, отправляет только свой NodeRegistration (клиент подключился node keypair)
    • account_id = SHA-256("mt-account" || suite_id || client_pubkey) в Account Table → подключение к доверенному узлу (клиент подключился account keypair)
    • Ни одно не найдено → отказ

Условия 1-2 выполнены + уровень 3 определён → Noise handshake → Montana P2P с соответствующим уровнем доступа. Любое не выполнено → TLS alert bad_certificate, close. Стандартное поведение сервера с обязательной аутентификацией клиента — таких серверов в интернете миллионы (корпоративные порталы, API, банковские системы).

Replay protection: server_node_id привязывает proof к конкретному получателю. Timestamp slot ограничивает окно до 4 минут.

Bootstrap exception: genesis bootstrap nodes хардкодированы как (IP, node_id, pubkey) × 12. Bootstrap принимает proof от любого валидного FN-DSA-512 ключа (Account Table не проверяется). Для защиты от connection flood клиент прилагает proof-of-work:

SHA-256("mt-bootstrap-pow" || proof || nonce) < target

target подбирается чтобы стоимость ≈ 100ms CPU. PoW требуется только при подключении к bootstrap, не к обычным peers.

Uniform Framing

Все Montana-сообщения внутри IBT-соединения фрагментируются на фреймы фиксированного размера:

frame_size = 1024 bytes

frame format:
  flags     1B    (0x01 = data, 0x02 = padding, 0x04 = continuation)
  length    2B    (полезная нагрузка, ≤1021B)
  payload   1021B (данные или random padding до frame_size)

Персональный мессенджер скрывает тайминг: между узлами идёт постоянный поток фреймов. Реальные Montana-сообщения замещают padding-фреймы, не добавляются к ним. Наблюдатель внутри сети не может отличить перевод от доказательства времени от тишины — всё одинаковые зашифрованные фреймы.

Параметры:

  • Baseline frame rate: 1 frame/сек на исходящих соединениях. Входящие — фреймы при наличии данных
  • Maximum burst: ≤ 8 frames подряд без паузы ≥ 10ms
  • Minimum padding ratio: ≥ 20% фреймов в скользящем 60-секундном окне на исходящих

Персональный = доступный: 8 исходящих × 1 frame/сек × 1024 bytes = 8 KB/сек ≈ 20 GB/мес. Приемлемо для домашнего сервера.

Transport Randomness

Все рандомизированные решения транспортного уровня (stem routing, frame scheduling, nonce generation) используют CSPRNG из OS entropy pool. Детерминированный PRNG от node state запрещён для transport-layer randomness.

Transport obfuscation ортогонален консенсусу. TimeChain, NodeChain, state machine работают поверх любого транспорта без изменений.

Peer Selection

Приватная сеть по приглашениям делает sybil-узлы дорогими: каждый sybil = приглашение + 20 160 окон VDF + выделенное ядро CPU. Peer selection использует эту особенность — diversity constraints из протокольных данных (start_window, inviter) дополняют сетевые (/16, ASN).

P2P gossip — только зарегистрированные и приглашённые узлы (уровни 1-2 IBT, см. Transport Obfuscation → Identity-Bound Tunnel). Аккаунты (уровень 3 IBT) взаимодействуют через свой доверенный узел.

Исходящие соединения

8 исходящих, все полные. Uniform framing скрывает типы сообщений — отдельные relay-only соединения не нужны.

Выбор: случайный 50/50 из таблиц «новые» и «проверенные» (модель addrman Bitcoin Core). Бакетирование с секретным ключом узла. Без preference по chain_length — выбор равномерный.

Четыре уровня diversity

Каждый исходящий проверяется по всем четырём constraints:

Сетевые:
  /16  — не более 1 исходящего на /16 подсеть (IPv4) или /48 (IPv6)
  ASN  — не более 2 исходящих на автономную систему

Протокольные:
  start_window — не более 2 исходящих к узлам с start_window в одном τ₂
  inviter      — не более 2 исходящих к узлам приглашённым одним inviter

Сетевые constraints — доказаны 10 годами Bitcoin Core (Heilman et al. 2015 → netgroup diversity → ASMAP). Протокольные constraints — уникальны для приватной сети: start_window и inviter_node_id канонически доступны из Node Table и proposal history.

Следствие: кластер sybil от одного inviter → максимум 2 из 8 слотов. Кластер sybil зарегистрированных в один τ₂ → максимум 2 из 8. Eclipse требует минимум 4 независимых inviter в 4 разных AS в 4 разных /16 с регистрацией в 4 разных τ₂.

ASN-карта загружается при запуске (аналог ASMAP Bitcoin Core). Без карты — fallback на /16.

Адресный менеджер

Две таблицы:

  • Новые — адреса полученные через peer exchange и DHT. Узел ещё не подключался
  • Проверенные — адреса к которым узел успешно подключался через IBT

Бакетирование: bucket = Hash(secret_key, source_group, addr_group) % N. Детерминированно с секретным ключом — атакующий не может предсказать в какой бакет попадёт его адрес.

Входящие соединения

До 32 входящих. При переполнении — вытеснение:

  1. Защитить 4 с наименьшим пингом
  2. Защитить 4 с последними полезными сообщениями (любое валидное Montana-сообщение которое узел ещё не видел)
  3. Защитить до 8 из разных подсетей (по одному от каждой)
  4. Защитить 4 с последними proposals
  5. Из оставшихся — вытеснить из крупнейшей подсетевой группы

Якоря

2 исходящих с наибольшим uptime соединения сохраняются каждые τ₂. При перезапуске после аварии или обновления — подключиться к якорям первым до случайного выбора из таблиц.

Feeler

Каждые 10 минут: подключиться к случайному адресу из «новых», выполнить IBT handshake (все три уровня проверки). Успех на любом уровне → перенести в «проверенные» с пометкой уровня (node / invited / account). Неуспех → пометить или удалить.

Ротация

По поведению: если peer не передал ни одного нового proposal за τ₂ — заменить. Peer с долей невалидных сообщений выше 50% в скользящем τ₁-окне — отключить с запретом переподключения на τ₂. Peer который relay-ит честно — полезен сети, остаётся.

PeerRecord

Формат записи о пире при peer exchange:

PeerRecord:
  ip            16B   (IPv4-mapped IPv6)
  port           2B   (u16)
  node_id       32B
  node_pubkey  897B   (FN-DSA-512)

Без node_id и node_pubkey клиент не может вычислить IBT proof для подключения. Peer exchange: не более 100 PeerRecord за сообщение. Не более 1 peer exchange сообщения в минуту от каждого peer.

Censorship-Resistant Discovery

Генезис: 12 hardcoded bootstrap nodes (IP, node_id, pubkey). Если все 12 IP заблокированы на уровне страны — новый узел не может войти в сеть. Четыре независимых канала обнаружения. Достаточно одного из четырёх.

1. Peer exchange. Каждый узел хранит и передаёт список активных пиров новичкам. Достаточно знать IP одного узла — друг, QR-код, мессенджер. Один живой контакт = вход в сеть.

2. DHT. Kademlia DHT поверх libp2p. Узлы находят друг друга без центральной точки. Идентификаторы рандомизированы — DHT не раскрывает node_id до установления Montana-соединения.

3. Bridge nodes. Узлы за пределами цензурируемой юрисдикции, опубликованные через внеполосные каналы (социальные сети, мессенджеры, печатные QR-коды). IP bridge node неизвестен фаерволу до использования.

4. Encrypted Client Hello (ECH). Bootstrap через CDN с поддержкой ECH. SNI зашифрован — наблюдатель видит IP CDN, но не целевой домен. Эффективен в юрисдикциях без активной блокировки ECH extension. В юрисдикциях блокирующих ECH (Китай с 2023, Россия с 2024) — канал неработоспособен. Для таких юрисдикций — каналы 1-3.

Избыточность = устойчивость. Четыре канала независимы. Блокировка одного не влияет на остальные.

Dandelion++ (анонимность отправителя)

P2P gossip Montana ретранслирует операции через все узлы. Без защиты первый пир знает IP отправителя. Dandelion++ (Fanti et al. 2018) устраняет связь IP → операция модификацией существующего gossip.

Две фазы:

Stem (стебель):
  Операция проходит по цепочке случайных узлов (в среднем 2-3 hop).
  Каждый узел видит только предыдущий hop, не автора.
  На каждом hop с вероятностью p = 0.4 переход в fluff.
  E[stem_length] = 1/p = 2.5 hops.
  P(stem ≤ 1) = 40%, P(stem ≤ 3) = 78%.

Fluff (пух):
  Последний stem-узел запускает обычный gossip.
  Для всей сети операция «появилась» из случайной точки.

Stem routing. Стебель использует только исходящие соединения — входящие не участвуют. Каждые 1440 окон (~24 часа) узел выбирает 2 из 8 исходящих как стебельных (stem epoch = 1 день). Все стебельные операции в эпохе направляются через одного из этих 2 (выбор по hash(msg)).

Применение по типу объекта:

Объект Режим Причина
UserObject (Transfer, Anchor, OpenAccount, ChangeKey) Stem → fluff Скрыть IP отправителя
ControlObject (NodeInvitation, NodeRegistration) Stem → fluff Скрыть IP пригласившего/регистрирующегося
VDF Reveal Прямой gossip (без stem) node_id публичен в reveal, анонимность невозможна
Confirmation Stem → fluff Скрыть какой узел подтвердил первым

VDF Reveal — единственное исключение. Reveal содержит node_id по определению. Связь IP → node_id для внешнего наблюдателя закрыта слоем Transport Obfuscation (TLS 1.3 на порт 443).

Свойства:

Угроза Защита
Пир видит IP отправителя Stem: пир видит только предыдущий hop
Глобальный наблюдатель (ISP) TLS 1.3 + uniform framing (Transport Obfuscation)
Анализ графа gossip Операция входит в gossip из случайной точки
Контроль k узлов Деанонимизация требует контроля O(√n) узлов

Реализация:

stem_peers = random_sample(outbound, 2)    // каждые 1440 окон

on_receive_stem(msg, from_peer):
  if random() < 0.4:
    gossip_broadcast(msg)                  // fluff
  else:
    next = stem_peers[hash(msg) % 2]      // детерминированный выбор из 2
    send_stem(msg, next)                   // продолжить stem
  start_timer(msg, 30s)                   // страховка на каждом hop

on_timer_expired(msg):
  if msg не обнаружен в gossip:
    gossip_broadcast(msg)                  // принудительный fluff

Каждый stem-узел страхует следующий. Таймер 30 секунд на каждом hop независимо. Если следующий hop уронил сообщение — текущий hop обнаруживает отсутствие операции в gossip и делает fluff сам. Максимальная задержка = 30 секунд (один hop), не кумулятивная.

Dandelion++ не требует внешней инфраструктуры. Каждый Montana-узел уже является relay — gossip существует, stem добавляет 2-3 hop перед ним. Latency overhead: миллисекунды.

NAT Traversal

Персональная сеть работает когда каждый может войти. Большинство домашних пользователей за NAT — невидимы для входящих соединений. Без NAT traversal персональный интернет = серверный клуб.

Три механизма, каждый следующий — если предыдущий не сработал:

1. AutoNAT (определение). Узел спрашивает outbound peers: «видишь ли мой IP:port напрямую?» Если да — NAT нет. Если нет — узел знает свой NAT-статус.

2. DCUtR (пробивка). Два NAT-узла координируются через третий узел с публичным IP. Оба отправляют исходящие пакеты — роутеры открывают «дырки» для ответов. После координации — прямое соединение. Успех: 60-70% случаев (TCP). Carrier-grade NAT (мобильные операторы): ~30%.

3. Circuit Relay v2 (транзит). Если пробивка не удалась — трафик идёт через outbound peer с публичным IP. Relay — не отдельный механизм и не выделенный сервер. Relay-соединение = обычное исходящее соединение, подчиняющееся тем же правилам: uniform framing, diversity constraints, ротация по поведению. Содержимое зашифровано конец-в-конец (Noise) — relay видит IP участников но не содержимое. Metadata распределён по 8 outbound peers из разных /16 и ASN — ни один relay не видит полный граф.

Relay — не fallback а гарантия подключения при любом типе NAT. Пробивка — оптимизация для снижения нагрузки на relay.

Лимиты relay: до 32 одновременных relay-соединений на узел, bandwidth per relay ≤ baseline frame rate (1 KB/сек). 32 × 1 KB/сек = 32 KB/сек ≈ 82 GB/мес — приемлемо для домашнего узла с публичным IP.

Обязанность. Узлы с публичным IP поддерживают relay — персональная сеть работает когда каждый может войти. Reference implementation включает relay при обнаружении публичного IP. Feeler-подключения проверяют поддержку relay у peers; узлы без relay помечаются no-relay в адресном менеджере. NAT-узлы предпочитают peers поддерживающие relay при выборе исходящих.

Все три механизма — стандарт libp2p (AutoNAT, DCUtR, Circuit Relay v2). Ноль новых протокольных примитивов.

Пять слоёв — одна конструкция

Слой 1: Transport Obfuscation         персональный сервер скрывает содержимое и тайминг
Слой 2: Peer Selection                 invitation-aware diversity не даёт окружить узел
Слой 3: NAT Traversal                  каждый может войти, даже за NAT
Слой 4: Censorship-Resistant Discovery четыре канала, достаточно одного
Слой 5: Dandelion++                    пиры не знают кто автор операции

Каждый слой закрывает свой вектор. Ни один не требует внешней инфраструктуры. Всё построено поверх libp2p и существующего gossip. Сетевой уровень ортогонален консенсусу — ни один state transition не затронут.


Эволюция протокола

Протокол Montana не имеет on-chain governance. Изменения правил протокола не голосуются внутри консенсуса, не подписываются советами, не хранятся в state. Эволюция проходит по той же модели что Bitcoin: открытые предложения, независимые реализации, добровольный выбор операторов узлов, fork resolution через большинство chain_length.

Принцип

Consensus state Montana содержит только то что необходимо для финансового слоя и хронометража: TimeChain, NodeChain, AccountChain, Account Table, Node Table. Никаких полей governance, никаких советов в state, никаких голосований в реестре операций. Любая попытка ввести on-chain governance вводит subjective компоненты в consensus state и создаёт постоянную атакуемую поверхность — это нарушение глобального инварианта I-3.

Эволюция протокола существует вне consensus state, как социальный и инженерный процесс над Content Layer и репозиториями реализаций.

Жизненный цикл изменения

1. PROPOSAL
   Любой участник публикует MIP (Montana Improvement Proposal)
   как persistent blob в Content Layer:
     app_id   = SHA-256("mt-app" || "mips")
     data_hash = H(текст MIP)
     anchor   = операция Anchor в AccountChain автора
   
   Авторство и timestamp доказуемы через подпись Anchor и
   timechain_value cemented окна. История эволюции навсегда
   в Content Layer + TimeChain.

2. DISCUSSION
   Открытое обсуждение в публичных каналах
   (форумы, репозитории, advisory councils — см. ниже).
   Никаких формальных голосований внутри протокола.

3. IMPLEMENTATION
   Реализации (Rust core и альтернативные клиенты) выпускают
   новые версии узлового ПО с реализованным изменением.
   Каждая версия закрепляется за конкретным protocol_version
   (u32 в Proposal header).

4. ADOPTION
   Операторы узлов самостоятельно выбирают какую версию
   запускать. Никакого on-chain голосования, никакого формального
   activation window. Узлы публикуют proposals со своим protocol_version.

5. FORK RESOLUTION
   При расхождении правил сеть может разделиться на цепочки.
   Каждый узел следует той цепочке которая длиннее по его
   собственным правилам валидации (chain_length majority).
   Меньшинство либо обновляется до правил большинства, либо
   продолжает работать как независимая цепочка (hard fork).

Поле protocol_version

Поле protocol_version (u32) в Proposal header — единственный сигнал эволюции внутри консенсуса. Узел публикует proposals с тем protocol_version который реализован его версией ПО. Инвариант protocol_version >= prev_proposal.protocol_version запрещает откат к более старым правилам внутри одной цепочки.

protocol_version не голосуется и не активируется через governance. Он отражает фактическое состояние реализации узла — что узел реально умеет валидировать. Расхождение protocol_version между honest узлами разрешается естественно через fork choice по chain_length.

Advisory councils

Группы экспертов могут существовать как advisory структуры — публикующие рекомендации, обзоры, анализ безопасности через Content Layer. Их подписи не имеют binding эффекта на consensus, их составы не хранятся в state, их голоса не считаются в state transitions.

Примеры advisory структур (опциональны, не часть протокола):

  • AI Council — модели разных компаний публикуют технические обзоры MIPs
  • Core Council — публичные эксперты публикуют анализ безопасности и социальную координацию

Захват advisory совета не даёт контроля над протоколом — он даёт только возможность опубликовать рекомендацию, которую операторы узлов могут проигнорировать. Это устраняет attack surface governance: нет binding голосования = нет цели для компрометации.

Advisory councils организуются вне протокола (репозитории, форумы, каналы Content Layer). Протокол не знает об их существовании и не выделяет им никаких прав.

Сравнение с Bitcoin

Модель полностью симметрична Bitcoin BIP процессу:

Bitcoin Montana
BIP в репозитории bitcoin/bips MIP в Content Layer
Core devs выпускают bitcoind Core devs выпускают Rust core
Майнеры выбирают версию Узлы выбирают версию
Hash power voting (фактическое) chain_length voting (фактическое)
Hard fork → две цепи Hard fork → две цепи
Нет on-chain governance Нет on-chain governance

Bitcoin работает по этой модели 16 лет без on-chain governance. Это не эксперимент, а проверенная временем архитектура. Montana следует тому же принципу.

Параметрическая адаптация

Параметры D и m адаптируются автоматически на границе τ₂ через participation-ratio feedback (см. раздел «Адаптация D через participation-ratio feedback»). Это не governance. Адаптация детерминирована, опирается только на canonical chain observations (cemented sets, Node Table), не требует голосования, не требует социальной координации, не зависит от измерений физического мира. Формула адаптации и её параметры зафиксированы в Genesis Decree; правка самой формулы требует MIP + новой версии ПО + adoption через chain_length, как и любое другое изменение протокола.

Закрытие окна определяется quorum event в канонических cemented sets. Механизм полностью event-driven и опирается только на canonical state.


Архитектура

  ТЕЛЕФОН / ДЕСКТОП                        УЗЕЛ (десктоп / сервер, 24/7)
┌────────────────────────┐         ┌──────────────────────────────────────┐
│  Кошелёк               │         │                                      │
│  FN-DSA-512 keypair    │         │  TimeChain                           │
│  локальная UX-история  │         │  T_r = SHA-256^D(T_{r-1})            │
│  операций              │         │  каноническая последовательность,    │
│                        │         │  источник случайности                │
│  AccountChain          │         │        │                             │
│  (счётчик окон         │         │        ▼                             │
│   активности)          │         │  NodeChain (per node)                │
│                        │         │  S_{i,w} = SHA-256(S_{i,w-1} || T_w  │
└──────────┬─────────────┘         │            || node_id)               │
           │  операции             │  доказательство присутствия          │
           │  (type|prev_hash|     │  chain_length = окна с BundledConf.  │
           │   payload|FN-DSA-512) │        │                             │
           └──────────────────────▶│        ▼                             │
                confirmations      │  AccountTable                        │
               ◀──────────────────-│  balance: u64 (открыт)               │
                                   │  pubkey, frontier_hash               │
                                   │  account_chain_length                │
                                   │        │                             │
                                   │        ▼                             │
                                   │  Proposals (навсегда)                │
                                   │  control_root, node_root,            │
                                   │  account_root, timechain_value       │
                                   └──────────────────────────────────────┘

Зависимости: TimeChain → NodeChain → AccountTable
Отказ AccountTable не останавливает продвижение TimeChain.
Отказ узла не заражает каноническую последовательность.