Прекомпайлы: встроенные программы POA
Прекомпайл — встроенная программа сети по фиксированному адресу. В POA такие программы реализованы модулями Elixir с интерфейсом Chain.Precompile и адресами в диапазоне 0x2D00…. Их устанавливает оператор вместе с кодом узла. Произвольный EVM-байт-код сеть не исполняет.
Фиксированный набор обработчиков упрощает обзор исполняемого кода, но не гарантирует его безопасность. Ошибки обработчиков, проверки доступа и обновления узла остаются частью модели угроз.
Как транзакция находит precompile
Заголовок раздела «Как транзакция находит precompile»У каждой транзакции есть адрес назначения (to). Производитель блоков обрабатывает транзакцию по основному пути: декодирует подписанную транзакцию, проверяет nonce, а затем запрашивает у реестра прекомпайлов, принадлежит ли адрес to зарегистрированному обработчику. Если обработчик найден, происходит диспетчеризация вызова в его функцию execute/3. Если не найден, транзакция обрабатывается как нативный перевод USDC. Обычный перевод между аккаунтами выполняется без прекомпайла.
Сокращённая схема диспетчеризации из lib/chain/block_executor.ex:
defp charge_and_execute(pending, tx, from, to, value, fee, block_number, tx_index, ts) do case to && Chain.Precompiles.Registry.lookup(to) do {:ok, handler} -> cond do Decimal.compare(value, Decimal.new(0)) != :eq -> {:error, :precompile_value_not_supported}
true -> with {:ok, input} <- Crypto.decode_hex_safe(tx.input), true <- byte_size(input) >= 4 do <<selector::binary-4, args::binary>> = input run_precompile_execute(handler, selector, args, input, from, value, ...) else _ -> {:error, :invalid_calldata} end end
_ -> # нативный USDC-перевод: списать с отправителя, зачислить получателю, взять комиссию. ... endendПри совпадении адреса вызывается зарегистрированный обработчик; иначе выполняется нативный перевод. Дополнительная логика сети реализуется в прекомпайлах.
Интерфейс Chain.Precompile
Заголовок раздела «Интерфейс Chain.Precompile»Каждый прекомпайл реализует три колбэка (lib/chain/precompiles/precompile.ex):
defmodule Chain.Precompile do @callback address() :: binary()
@callback execute(selector :: binary(), args :: binary(), context :: Chain.Precompile.Context.t()) :: {:ok, result :: binary(), logs :: list()} | {:revert, reason :: binary()}
@callback read(selector :: binary(), args :: binary()) :: {:ok, abi_encoded :: binary()} | {:revert, reason :: binary()}endaddress/0. Возвращает адрес, по которому доступен данный precompile. Адреса жестко заданы в диапазоне0x2D00…, чтобы весь набор можно было легко перечислить.execute/3. Точка входа для операций, изменяющих состояние. Вызывается внутри транзакции базы данных при формировании блока. Возвращает результат и список логов, которые попадают в tx-receipt, либо кортеж{:revert, reason}для чистого отката изменений.read/2. View-вызов (только для чтения). Используется вeth_callиtriggerconstantcontract. Состояние не изменяет.
Структура context, передаваемая в execute/3, содержит данные, аналогичные msg.sender и msg.value в Solidity, а также высоту блока, timestamp блока и индекс транзакции (tx-index). Это всё, что нужно обработчику, чтобы понять контекст вызова. Лаконичность этого интерфейса — сознательное решение: функцию execute/3, которая может только явно изменять аккаунты и таблицы через локальные модули, проверять (ревьюить) значительно проще, чем полноценную виртуальную машину EVM.
Реестр (lib/chain/precompiles/registry.ex) представляет собой ETS-таблицу, где ключом является адрес:
def lookup(address) when is_binary(address) do case :ets.lookup(@table, address) do [{^address, module}] -> {:ok, module} [] -> :not_found endend
def register(address, handler_module) do GenServer.call(__MODULE__, {:register, address, handler_module})endПри запуске узел загружает активные прекомпайлы. Диспетчеризация использует поиск по адресу в ETS. Новый обработчик требует регистрации и согласованного обновления узлов; его нельзя установить отправкой байт-кода через очередь транзакций.
Пространство имен адресов также имеет значение. Все системные прекомпайлы располагаются в диапазоне 0x2D00…; старший байт 0x2D (ASCII-код символа -, от названия проекта) является зарезервированным префиксом. Вызов eth_getCode возвращает 0x01 для любого существующего адреса в этом диапазоне. Таким образом, кошельки, которые перед вызовом проверяют наличие контракта по адресу через eth_getCode, получают корректный ответ без необходимости реализации полноценной EVM.
HTLC precompile
Заголовок раздела «HTLC precompile»Перейдем от абстракций к коду. HTLC precompile по адресу 0x2D00…0001 — это рабочий код сети, управляющий состоянием (lib/chain/precompiles/htlc.ex).
HTLC (Hashed Time-Locked Contract) блокирует средства под хеш секрета и срок действия. В обычном lock отправитель указывает получателя. Любой, кто знает секрет P, для которого sha256(P) = H, может вызвать claim до истечения срока, но средства всегда получает указанный получатель. Если блокировка не исполнена вовремя, refund возвращает ранее списанные средства отправителю.
У блокировки, созданной через bridge_lock, другая семантика возврата: на стороне POA refund закрывает запись без зачисления средств. Возврат исходных USDC выполняется в Ethereum по правилам его HTLC. Парные блокировки требуют согласованных получателей, сроков и наблюдения за обеими сетями; один HTLC не обеспечивает безопасность всего моста.
defmodule Chain.Precompiles.HTLC do @moduledoc """ Hashed time-locked contract: атомарные свопы без доверенного моста. """ @behaviour Chain.Precompile
@address <<0x2D, 0::144, 0x01>> # 0x2D00…0001
# 4-байтные Keccak-256-селекторы @lock_selector selector("lock(bytes32,address,uint256,uint256)") @claim_selector selector("claim(bytes32,bytes32)") @refund_selector selector("refund(bytes32)") @get_swap_selector selector("getSwap(bytes32)")
defp selector(signature), do: :erlang.binary_part(ExKeccak.hash_256(signature), 0, 4)
@impl true def address, do: @address
@impl true def execute( @lock_selector, <<hash::binary-32, _pad::binary-12, receiver::binary-20, amount::256, deadline_ms::256>>, ctx ) do do_lock(hash, receiver, amount, deadline_ms, ctx) end
def execute(@claim_selector, <<hash::binary-32, preimage::binary-32>>, ctx) do do_claim(hash, preimage, ctx) end
def execute(@refund_selector, <<hash::binary-32>>, ctx) do do_refund(hash, ctx) end
def read(@get_swap_selector, <<hash::binary-32>>), do: do_get_swap(hash)endОбработчик хранит состояние в state.htlc_swaps, проверяет условия каждой операции и изменяет балансы внутри SERIALIZABLE-транзакции базы данных. Корректность этих проверок и их воспроизведение верификатором требуют отдельного ревью.
Мост использует ту же модель прекомпайла по адресу 0x2D00…0003: BridgeRefillMint предоставляет только функцию bridge_lock(...), которая атомарно вставляет запись bridge_mints и привязанный к получателю HTLC, а проверку межсетевого доказательства оставляет верификатору. Подробный процесс описан в статье про мост.
NativeTransferAuth по адресу 0x2D00…0006 — EIP-3009-образный метод для нативного USDC: transferWithAuthorization против state.accounts, не ERC-20. Именно этот asset объявляет x402 на eip155:77. Permit2 и Ethereum mainnet USDC отклоняются.
Границы безопасности HTLC
Заголовок раздела «Границы безопасности HTLC»Хеш, получатель, срок и статус блокировки задают условия claim и refund. Они не устраняют доверие к реализации, обновлениям узлов, межсетевой проверке и хранению активов в Ethereum. Ошибка общего обработчика или управления мостом может затронуть несколько блокировок и весь пул обеспечения; нельзя считать риск ограниченным одним свопом.
Подробная модель доверия, ограничения верификатора и полномочия OperatorVault описаны в статье о мосте.
Компромиссы относительно EVM
Заголовок раздела «Компромиссы относительно EVM»- Установка программ через релиз узла. Пользователь не может добавить новый обработчик транзакцией. Оператор согласует код и его активацию с верификаторами.
- Явное взаимодействие программ. Взаимодействие между прекомпайлами реализуется в коде узла; произвольная композиция пользовательских контрактов не поддерживается.
- Специальные интеграции. Приложения обращаются к ABI конкретного прекомпайла. Совместимость адресов и части RPC не означает совместимость со всем набором инструментов Solidity.
- Контроль ресурсов на уровне узла. Отсутствие EVM и пошагового учёта газа не отменяет проверок размеров входных данных, времени выполнения и нагрузки на БД.
Стратегия безопасности
Заголовок раздела «Стратегия безопасности»При изменении обработчика проверяйте авторизацию, сохранение балансов, повторные вызовы, сроки и откат транзакции. Тесты свойств, анализ типов и модели протокола могут дополнять ревью, но наличие интерфейса @behaviour само по себе не доказывает эти свойства.
Перед выпуском согласуйте версию обработчика, схему БД, высоту активации и правила вычисления корня состояния на производителе и верификаторах. Подтвердите воспроизведение истории и отказ при некорректных входных данных. Публикуйте результаты проверок для конкретного релиза, отдельно от планов на будущую формальную верификацию.
Куда это ведёт
Заголовок раздела «Куда это ведёт»HTLC, BridgeRefillMint и NativeTransferAuth используют эту модель для блокировок, ввода через мост и платежей x402. Их назначение различается; для каждой программы нужны собственные проверки и ограничения.