Перейти к содержимому

Прекомпайлы: встроенные программы POA

Прекомпайл — встроенная программа сети по фиксированному адресу. В POA такие программы реализованы модулями Elixir с интерфейсом Chain.Precompile и адресами в диапазоне 0x2D00…. Их устанавливает оператор вместе с кодом узла. Произвольный EVM-байт-код сеть не исполняет.

Фиксированный набор обработчиков упрощает обзор исполняемого кода, но не гарантирует его безопасность. Ошибки обработчиков, проверки доступа и обновления узла остаются частью модели угроз.

У каждой транзакции есть адрес назначения (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-перевод: списать с отправителя, зачислить получателю, взять комиссию.
...
end
end

При совпадении адреса вызывается зарегистрированный обработчик; иначе выполняется нативный перевод. Дополнительная логика сети реализуется в прекомпайлах.

Каждый прекомпайл реализует три колбэка (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()}
end
  • address/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
end
end
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 по адресу 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 отклоняются.

Хеш, получатель, срок и статус блокировки задают условия claim и refund. Они не устраняют доверие к реализации, обновлениям узлов, межсетевой проверке и хранению активов в Ethereum. Ошибка общего обработчика или управления мостом может затронуть несколько блокировок и весь пул обеспечения; нельзя считать риск ограниченным одним свопом.

Подробная модель доверия, ограничения верификатора и полномочия OperatorVault описаны в статье о мосте.

  • Установка программ через релиз узла. Пользователь не может добавить новый обработчик транзакцией. Оператор согласует код и его активацию с верификаторами.
  • Явное взаимодействие программ. Взаимодействие между прекомпайлами реализуется в коде узла; произвольная композиция пользовательских контрактов не поддерживается.
  • Специальные интеграции. Приложения обращаются к ABI конкретного прекомпайла. Совместимость адресов и части RPC не означает совместимость со всем набором инструментов Solidity.
  • Контроль ресурсов на уровне узла. Отсутствие EVM и пошагового учёта газа не отменяет проверок размеров входных данных, времени выполнения и нагрузки на БД.

При изменении обработчика проверяйте авторизацию, сохранение балансов, повторные вызовы, сроки и откат транзакции. Тесты свойств, анализ типов и модели протокола могут дополнять ревью, но наличие интерфейса @behaviour само по себе не доказывает эти свойства.

Перед выпуском согласуйте версию обработчика, схему БД, высоту активации и правила вычисления корня состояния на производителе и верификаторах. Подтвердите воспроизведение истории и отказ при некорректных входных данных. Публикуйте результаты проверок для конкретного релиза, отдельно от планов на будущую формальную верификацию.

HTLC, BridgeRefillMint и NativeTransferAuth используют эту модель для блокировок, ввода через мост и платежей x402. Их назначение различается; для каждой программы нужны собственные проверки и ограничения.