Корень состояния: независимая проверка блоков
Производитель блоков POA вычисляет корень состояния (state root) после исполнения каждого блока и включает его в хеш блока. Верификатор повторно исполняет транзакции на своей копии предыдущего состояния и сравнивает результат.
Эта проверка выявляет несовпадение состояния и исполнения. Она сама по себе не гарантирует доступность сети, отсутствие разных ветвей у производителя или независимый консенсус о выборе ветви.
Что входит в state root
Заголовок раздела «Что входит в state root»Текущая реализация Chain.StateRoot использует версию состава 9. В хеш входят восемнадцать компонентов по 32 байта в следующем порядке:
state_root = keccak256( accounts_root || htlc_root || precompiles_root || bridge_mints_root || bridge_halt_consensus_root || bridge_governors_state_root || bridge_unhalt_pending_root || authorization_current_producer_root || authorization_forks_root || authorization_accepted_root || chain_params_root || x402_authorization_nonces_root || confidential_accounts_root || confidential_lane_total_root || confidential_nonces_root || confidential_inbound_notes_root || confidential_frozen_recipients_root || confidential_note_cap_exempt_root)Слоты 1–12 и слот 14 — это байты version 8 для тех же компонентов. Слот 13 — дерево конфиденциальных счетов. Слот 15 — дерево конфиденциальных nonce. Слоты 16–18 — список входящих заметок, список замороженных получателей и список исключений из лимита заметок.
Конфиденциальные счета и nonce — это разреженные деревья Меркла фиксированной глубины 256. Бит 0 — старший бит первого байта ключа. Бит 0 выбирает левого ребёнка, бит 1 — правого. Ключ счёта — keccak256 от сырых 20 байт адреса. Ключ nonce — keccak256 от сырых 20 байт from, соединённых с 32 байтами nonce. empty_node(256) — 32 нулевых байта. Для d от 255 до 0 empty_node(d) — это keccak256 от empty_node(d+1), соединённого с самим собой. empty_node(255) равен ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5. Корень пустого дерева — empty_node(0), а не нулевое слово из 32 байт. Значение листа — это хеш строки version 8, и второй раз он не хешируется. Лист счёта — keccak256 от адреса, соединённого с 32 байтами commitment. Лист nonce — keccak256 от from, nonce и used_at_block как знакового 64-битного целого. Внутренний узел — keccak256 от левого ребёнка, соединённого с правым. Отсутствующий ребёнок — empty_node(d+1).
Пустой hash_list — 32 нулевых байта. Непустой hash_list — keccak256 от сцепленных 32-байтных хешей строк. Замороженные получатели и исключения из лимита — это hash_list от keccak256 сырых 20 байт адреса, адреса отсортированы по возрастанию. Входящие заметки не входят ни в одно дерево. Их список — hash_list от keccak256(from, nonce, to, c_v и created_block как знаковое 64-битное целое), порядок (from, nonce). inserted_at в хеш не входит. Сумма полосы — keccak256 десятичного кодирования суммы, либо нуля, если строки нет.
На переходе к version 9 precompile 0007 остаётся выключенным, поэтому список входящих заметок, state.confidential_note_cap_exempt и state.confidential_frozen_recipients пусты. Повторное воспроизведение истории через этот переход не поддерживается. Узел синхронизируется из резервной копии, снятой после перехода. В этой копии есть created_block, инкрементальные корни счетов и nonce, пустой список исключений и пустой список замороженных получателей.
Основные группы состояния:
accounts: адреса, балансы и nonce. Баланс хранится как неотрицательное целое число micro-USDC. База данных отклоняет любую запись, после которой баланс стал бы отрицательным, дробным или бесконечным, поэтому такой блок никогда не фиксируется. Это ограничение относится к схеме, а не к состоянию: оно не хешируется, и его добавление не изменило ни одного state root.htlc_swaps: блокировки, получатели, сроки и статусы.precompiles: реестр обработчиков и параметры активации.bridge_mints: исходные Ethereum-события, привязки HTLC и блок наблюдения.- Состояние остановки моста, управляющих ролей и ожидающих возобновлений.
- Текущий производитель, авторизации форков и принятые тикеты.
chain_params: параметры идентичности сети.x402_authorization_nonces: использованные авторизации платежей; этот компонент препятствует повторному исполнению.- Конфиденциальные commitment счетов и использованные конфиденциальные nonce — деревья, описанные выше.
- Сумма конфиденциальной полосы, входящие заметки, замороженные получатели и исключения из лимита.
Поля и порядок строк заданы в lib/chain/state_root.ex. Корни таблиц строятся из канонически закодированных хешей строк; одинаковые числовые значения Decimal дают одинаковые байты. Это описание состава, а не замена спецификации побайтового кодирования.
Кеш blocks_tip исключён: корень вычисляется до обновления указателя на последний блок. Кеш конфиденциальных корней и его контрольные точки не входят в прообраз. Локальные метрики и операционные данные также не становятся состоянием консенсуса автоматически.
При изменении состава, кодирования или сортировки требуется согласованное обновление производителя и верификаторов. Проверка state_root_version должна соответствовать версии кода на всех узлах; несовпадение останавливает обработку с :state_root_version_mismatch. Узел берёт это значение из переменной окружения CHAIN_STATE_ROOT_VERSION при запуске, поэтому изменение вступает в силу после перезапуска; без переменной узел эту проверку не выполняет.
Как block hash фиксирует state root
Заголовок раздела «Как block hash фиксирует state root»Хеш каждого блока формируется так:
block_hash = keccak256( block_number (8 байт) || parent_hash (32 байта) || timestamp (8 байт) || tx_root (32 байта, хеш всех tx hash в порядке выполнения) || state_root (32 байта, корень отсортированных хешей, описанный выше))Любое изменение баланса, статуса HTLC или регистрации прекомпайла без соответствующей валидной транзакции изменяет state root, затем block hash, а затем и цепочку parent_hash для всех последующих блоков. Одно несанкционированное изменение строки каскадно приводит к легко обнаруживаемому разрыву цепи.
Что верификатор с этим делает
Заголовок раздела «Что верификатор с этим делает»В составе сети работает независимый клиент-верификатор (verifier). Для каждого блока он выполняет следующие шаги:
- Получает блок (заголовок + сырые подписанные транзакции) от upstream-узла.
- Повторно выполняет транзакции на своей локальной копии предыдущего состояния.
- Вычисляет собственный state root.
- Сравнивает его с корнем, заявленным производителем блоков.
- При совпадении: фиксирует блок в базе и предоставляет проверенное состояние кошелькам и RPC-клиентам.
- При несовпадении: откатывает транзакцию, логирует критическое предупреждение и прекращает обслуживание запросов.
Верификатор работает как отдельный BEAM-узел со своей собственной базой данных. У него нет прямого доступа к хранилищу производителя. Клиенты могут читать проверенное состояние через RPC верификатора; отправка транзакций использует отдельный публичный RPC. Несколько верификаторов могут работать независимо друг от друга. Запустить собственный верификатор может любой желающий.
Верификатор, успешно зафиксировавший блок, ретранслирует его через свой собственный block feed. Другие верификаторы могут подписаться на этот поток вместо потока производителя. Таким образом, верификаторы могут выстраиваться в цепочку или древовидную структуру:
Producer ──▶ Верификатор A ──▶ Верификатор C ▶ Верификатор B ──▶ Верификатор DКаждый верификатор самостоятельно проверяет блок, независимо от источника. Модель доверия остаётся неизменной: сначала проверь, потом обслуживай.
Как блоки и транзакции перемещаются между узлами
Заголовок раздела «Как блоки и транзакции перемещаются между узлами»Производитель и верификатор работают как отдельные BEAM-узлы и обмениваются блоками через Erlang distribution. Для этого канала оператор должен настроить TLS и закрытый сетевой доступ. Сам выбор Erlang distribution не включает шифрование автоматически.
Блоки (от производителя к верификаторам): после фиксации (коммита) каждого блока в базе данных производитель публикует событие через конвейер GenStage. GenStage — это библиотека Elixir для организации потоков producer-consumer с поддержкой backpressure. Каждый подписанный верификатор получает каждый блок (используется broadcast fan-out, а не round-robin). Если верификатор отстает или отключается, производитель буферизирует до 1000 блоков; более старые блоки удаляются из буфера.
При запуске (или после разрыва соединения) верификатор сначала синхронизирует историю (catch-up), а затем подписывается на поток новых блоков. Он запрашивает у upstream-узла текущий указатель (tip), затем скачивает блоки пакетами, начиная со своего последнего известного блока, и проверяет каждый из них. Как только синхронизация завершена, верификатор переключается на live-подписку GenStage. Этот механизм работает одинаково независимо от того, является ли upstream-узлом производитель или другой верификатор.
Событие блока содержит заголовок и сырые подписанные транзакции в порядке исполнения. Verifier восстанавливает отправителя Ethereum из подписанного RLP и повторно проверяет подпись Tron по protobuf и заявленному отправителю.
Отправка транзакций: публичный wallet RPC принимает подписанные транзакции через интерфейс записи производителя. Отдельный verifier работает только на чтение: eth_sendRawTransaction и отправка Tron отклоняются в verifier mode. Для отправки используйте публичный RPC POA.
Публичный edge открывает только разрешённые RPC и API. Закрытые порты узлов и администрирования остаются за сетевой границей. Верификатор только для чтения и публичный RPC для записи — разные роли развёртывания.
Дополнительные верификаторы могут подключаться к первому верификатору вместо прямого соединения с производителем. Каждый верификатор ретранслирует проверенные блоки, поэтому топология сети может представлять собой звезду, цепочку или дерево в зависимости от схемы развёртывания.
Текущий подход и масштабирование
Заголовок раздела «Текущий подход и масштабирование»Текущая реализация использует метод sorted-hash: извлечение всех строк из каждой таблицы состояния, сортировка по первичному ключу, хеширование и конкатенация. Вычислительная сложность составляет O(n) на блок по всему объему состояния. Производительность зависит от размера состояния и оборудования; её нужно измерять для конкретного развёртывания.
По мере роста объема состояния планируется миграция на инкрементальное дерево Меркла (Merkle tree), которое обновляет только те строки, которые изменились в текущем блоке. В этом случае вычислительная стоимость на блок снизится до O(k log n), где k — количество измененных строк. Подобная миграция является изменением правил консенсуса: потребуется версионированное обновление (rollout), чтобы каждый верификатор вычислял идентичный корень для одного и того же блока.
Почему это важно для cross-chain мостов
Заголовок раздела «Почему это важно для cross-chain мостов»Мост использует корень состояния для фиксации строк bridge_mints вместе с остальным состоянием. Верификатор дополнительно проверяет Ethereum-события через настроенный RPC. Корень связывает результат с блоком, но не проверяет Ethereum сам по себе: достоверность источника межсетевых данных остаётся отдельной границей доверия. См. Мост.