- Регистрация
- 23 Авг 2023
- Сообщения
- 3,969
- Реакции
- 0
- Баллы
- 36
Ofline
Если push-модель LayerZero вам уже понятна (отправили сообщение из одной сети и получили его в другой), то следующий шаг — это научиться читать данные из других блокчейнов, не разворачивая контракт в целевой сети и не отправляя два отдельных сообщения «туда-обратно». В LayerZero v2 для этого предусмотрен механизм Omnichain Queries, более известный как lzRead.
О нем далее и поговорим!
Если вы вдруг пропустили третью часть, то можно посмотреть у коллеги тут.
alt text
В статье рассмотрим, как устроен lzRead, из каких контрактов он состоит, как написать и настроить контракт для получения цен из пула Uniswap V3 — с разбором кода и деплоем в Remix.
Терминология:
lzRead позволяет контракту запрашивать и получать состояние из других блокчейнов. В основе лежит идея BQL (Blockchain Query Language) — единый способ формулировать запросы (что читать, из какой сети, на каком блоке/времени), получать и при необходимости обрабатывать ответы.
По шагам:
Архитектура OApp
Чтобы контракт мог отправлять read-запросы и получать ответы, нужно наследоваться от OAppRead.sol. Цепочка наследования:
Контракты простые — имеет смысл просмотреть их перед следующим этапом.
Чтобы реализовать lzRead, нужен контракт, наследующий OAppRead и реализующий три части: формирование запроса, оценка комиссии, обработка ответа. В следующем разделе — пример такого контракта и его методы.
Мы уже написали готовый контракт UniswapV3ObserveRead.sol. Он запрашивает с другой сети (data chain) результат вызова
При деплое передаем пять аргументов:
Внутри сохраняются
Метод собирает read-команду с помощью библиотеки ReadCodecV1 (кодирование и декодирование вызовов).
Цель — закодировать вызов
В массив можно добавить несколько запросов (в т. ч. в разные сети).
Перед отправкой запроса вызываем view-функцию, чтобы узнать, сколько нативного токена (или LZ token) нужно отправить вместе с
Зачем
Возвращается структура
Отправляет сформированную командой
Возвращается
Вызывается протоколом, когда верифицированный ответ доставлен в origin. В
Декодируем payload и эмитим событие
Настройка бывает двух видов: на endpoint (библиотеки send/receive, конфиг ReadLib с executor и требования к DVN) и на OApp (enforced options, при необходимости смена read-канала).
Вызывать настройку на endpoint может только сам OApp или его делегат; настройку на OApp (например
Контракт конфига помогает настроить эти параметры LzReadConfig.sol.
Настраивать может деплоер, сам контракт (вызов от его адреса) или делегат — через
Порядок деплоя и настройки:
В конструкторе OApp уже вызывается
Альтернатива нашему мануальному способу деплою, конфигурации и способу чтению данных является уже репозиторием с уже готовым набором скриптов — LayerZero CLI.
Представим: в нашей сети (origin) Base Sepolia нет цены на токен, которая нужна контракту. Через lzRead можно запросить данные о цене с другой сети — например, с контракта пула Uniswap V3 в сети Ethereum Sepolia, где этот токен уже торгуется.
Ниже — пошаговый порядок: деплой контракта в origin, оценка комиссии через
Можно повторить с указанными ниже адресами или подставить свои из списка, убедившись, что для пары сетей есть Read Path в Read Data Channels и в data chain есть подходящий пул Uniswap V3 с ликвидностью.
Совет: после деплоя сразу делайте Pin contract for current workspace (значок рядом с адресом контракта в Remix), а адреса копируйте — при смене сети развернутые контракты сбрасываются. Чтобы вызвать методы уже развернутого контракта, во вкладке Contract выберите контракт и вставьте его адрес в At Address.
В примере: origin = Base Sepolia, data chain = Ethereum Sepolia.
1.Откройте Remix и добавьте контракты LzReadConfig.sol и UniswapV3ObserveRead.sol.
2.Собираем все данные для деплоя:
Endpoint берем для сети origin с этой таблицы;
EID для data chain берем тут;
targetPoolAddress для target сети можем найти через Uniswap deployments. Убедитесь, что вызов
readChannel находим в этой таблице, указывая сеть origin и сеть target data;
readLib для origin сети находим тут;
libConfigParams для origin сети тоже находим тут. Он включает в себя такие параметры как: executor, requiredDVNCount, optionalDVNCount, optionalDVNThreshold, requiredDVNs, optionalDVNs;
enforced использует уже известный readChannel, msgType = 1, и options, который мы кодируем через специальную созданную тулзу;
alt text
alt text
3.Задеплойте сначала LzReadConfig.sol (аргумент: endpoint), затем UniswapV3ObserveRead.sol (endpoint, readChannel, targetEid, targetPoolAddress, адрес LzReadConfig).
alt text
alt text
4.Далее, на LzReadConfig вызовите
alt text
5.Оцените комиссию:
Вызов quoteObserve в Remix
6.В Remix в поле Value укажите
Value и вызов readObserve
7.После подтверждения транзакции, в сканере по адресу
LayerZero Scan: Response transaction
8.После статуса Delivered в origin будет вызван
Remix: логи события ObserveResult
С lzRead ваш контракт в одной сети может запросить данные в другой и получить ответ обратно — без деплоя контрактов там и без двух отдельных сообщений туда-сюда. Вы формируете запрос, отправляете его по read-каналу и обрабатываете ответ в
Мы с командой делимся мыслями и наблюдениями в своём Telegram-канале. Не всё доходит до статей, что-то проще оформить в пост, чем расписывать длинный текст. Если интересно, чем живём, что обсуждаем и с какими задачами сталкиваемся, то заглядывайте.
О нем далее и поговорим!
Если вы вдруг пропустили третью часть, то можно посмотреть у коллеги тут.
alt text
В статье рассмотрим, как устроен lzRead, из каких контрактов он состоит, как написать и настроить контракт для получения цен из пула Uniswap V3 — с разбором кода и деплоем в Remix.
Терминология:
Исходная сеть (origin chain) — сеть, где развернут ваш контракт, который будет запрашивать данные из другой сети.
Сеть данных (data chain / target chain) — сеть, из которой вы читаете данные.
Endpoint — системный смарт-контракт в каждой сети от LayerZero, через который проходят входящие и исходящие сообщения.
EID (Endpoint ID) — числовой идентификатор сети в протоколе LayerZero.
Read Channel — отдельный канал сообщений именно для чтений; его ID и поддерживаемые пути приведены в таблицах деплоев.
DVN (Decentralized Verifier Network) — сеть верификаторов, подтверждающих корректность ответа.
ReadLib1002 — message-library для чтений; для lzRead нужны совместимые библиотеки и DVN с доступом к архивным нодам.
Как устроен lzRead
lzRead позволяет контракту запрашивать и получать состояние из других блокчейнов. В основе лежит идея BQL (Blockchain Query Language) — единый способ формулировать запросы (что читать, из какой сети, на каком блоке/времени), получать и при необходимости обрабатывать ответы.
По шагам:
Формирование запроса — приложение собирает запрос: какие данные нужны, из какой целевой сети, на каком блоке или времени. Запрос кодируется в стандартную команду по схеме BQL.
Отправка запроса — команда отправляется через Endpoint LayerZero по отдельному read-каналу (не обычному messaging). По каналу явно передается, что это запрос с ожиданием ответа, а не просто смена состояния.
Получение и верификация данных (DVN data fetch and verification) — DVN принимают запрос, забирают данные с архивной ноды требуемой сети и при необходимости применяют off-chain compute: lzMap (преобразование ответов из одной или нескольких сетей) и lzReduce (агрегация нескольких ответов в один). Каждый DVN формирует криптографический хеш результата для проверки целостности. В этой статье мы делаем один запрос в одну сеть, поэтому Compute не настраиваем; как задать lzMap/lzReduce для сценариев с несколькими сетями или агрегацией — можно посмотреть в документации lzRead.
Доставка ответа (Response handling) — после верификации нужным числом DVN Endpoint доставляет итоговый ответ обратно в исходную сеть. Контракт-получатель обрабатывает его в_lzReceive(): декодирует payload и использует полученные данные.
Архитектура контрактов
Архитектура OApp
Чтобы контракт мог отправлять read-запросы и получать ответы, нужно наследоваться от OAppRead.sol. Цепочка наследования:
OAppRead.sol -> OApp.sol
OApp -> OAppReceiver.sol и OAppSender.sol
OAppReceiver, OAppSender -> OAppCore.sol
OAppCore -> Ownable (OpenZeppelin)
Контракты простые — имеет смысл просмотреть их перед следующим этапом.
Чтобы реализовать lzRead, нужен контракт, наследующий OAppRead и реализующий три части: формирование запроса, оценка комиссии, обработка ответа. В следующем разделе — пример такого контракта и его методы.
Пример OApp контракта (UniswapV3ObserveRead.sol)
Мы уже написали готовый контракт UniswapV3ObserveRead.sol. Он запрашивает с другой сети (data chain) результат вызова
observe() у пула Uniswap V3 — накопленные за указанный период значения тика и ликвидности; по ним можно вычислить TWAP (Time-Weighted Average Price) — среднюю цену актива за период без развертывания контракта в сети пула. Ответ доставляется обратно в наш контракт в origin. Контракт наследует OAppRead и OAppOptionsType3.
OAppRead — отправка read-запроса и прием ответа в_lzReceive.
OAppOptionsType3 — библиотека для опций сообщений. Owner задает принудительные опции (enforced) через[B]setEnforcedOptions(EnforcedOptionParam[])[/B]для пар(eid, msgType); они хранятся вenforcedOptions[eid][msgType].[B]combineOptions(eid, msgType, [I]extraOptions)[/I][/B]собирает итоговые опции: объединяет эти enforced с опциями вызывающего — параметромextraOptionsвquoteObserve/readObserve(на стороне executor значения складываются) и передает результат в[I]lzSend[/I]иquote. Для lzRead опции в формате Type3: газ на доставку ответа, размер ответа в байтах (response size) и value для executor; сборка —addExecutorLzReadOption(gas, responseSizeBytes, value). Если enforced уже заданы с достаточным газом и response size, можно вызывать с_extraOptions = 0x, иначе — передать закодированные опции.
При деплое передаем пять аргументов:
Код:
constructor(
address _endpoint,
uint32 _readChannel,
uint32 _targetEid,
address _targetPoolAddress,
address _config // контракт LzReadConfig — деплоим первым
) OAppRead(_endpoint, _config) Ownable(_config) {
READ_CHANNEL = _readChannel;
targetEid = _targetEid;
targetPoolAddress = _targetPoolAddress;
_setPeer(READ_CHANNEL, AddressCast.toBytes32(address(this)));
}
_endpoint — адрес Endpoint в сети деплоя (origin). Берется из Chains для выбранной origin-сети.
_readChannel — идентификатор read-канала. Берется из таблицы по паре origin и data chain.
_targetEid — EID целевой сети (откуда читаем пул). Берется из Chains для выбранной data chain.
_targetPoolAddress — адрес пула Uniswap V3 в data chain.
config — адрес контракта LzReadConfig.sol (деплоить первым). Передаётся в OAppRead и в Ownable: конфиг сразу становится владельцем OApp, адрес на OApp не хранится. После деплоя на OApp вызовите[I]setDelegate([/I]config). Позже (от владельца конфига): сменить делегата —setOAppDelegate(oapp, delegate); задать/сменить read-канал —setOAppReadChannel(oapp, channelId, active)(active = false чтобы отключить приём); передать владение —transferOAppOwnership(oapp, newOwner).
Внутри сохраняются
READ_CHANNEL, targetEid, targetPoolAddress и вызывается [I]setPeer(READ[/I]CHANNEL, AddressCast.toBytes32(address(this))) — так мы говорим протоколу, что ответы по этому read-каналу доставлять на этот контракт.Формирование запроса на чтение
Метод собирает read-команду с помощью библиотеки ReadCodecV1 (кодирование и декодирование вызовов).
Цель — закодировать вызов
observe(secondsAgos) на пуле Uniswap V3.
Код:
function getCmd(uint32[] calldata secondsAgos) public view returns (bytes memory) {
bytes memory callData =
abi.encodeWithSelector(IUniswapV3PoolObserve.observe.selector, secondsAgos);
EVMCallRequestV1[] memory req = new EVMCallRequestV1[](1);
req[0] = EVMCallRequestV1({
appRequestLabel: 1, // метка запроса
targetEid: targetEid, // EID сети, откуда читать данные
isBlockNum: false, // читать по времени (true = по номеру блока)
blockNumOrTimestamp: uint64(block.timestamp), // таймстамп, на котором будет считывание данных
confirmations: 15, // сколько подтверждений блока нужно
to: targetPoolAddress, // адрес контракта в data chain
callData: callData // закодированный метод который будет вызван для получения информации
});
return ReadCodecV1.encode(0, req); // версия 0, один запрос без Compute
}
secondsAgos — массив «сколько секунд назад» дляobserve; например[3600,0]— данные за последний час и «сейчас».
В массив можно добавить несколько запросов (в т. ч. в разные сети).
Оценка комиссии за запрос
Перед отправкой запроса вызываем view-функцию, чтобы узнать, сколько нативного токена (или LZ token) нужно отправить вместе с
readObserve.
Код:
function quoteObserve(
uint32[] calldata secondsAgos, // те же, что пойдут в readObserve
bytes calldata _extraOptions, // закодированные опции для executor
bool _payInLzToken // true = платить в LZ token, false = в нативном токене сети
) external view returns (MessagingFee memory fee);
Зачем
[B][I]payInLzToken[/I][/B]: вы заранее говорите, чем будете платить при вызове readObserve — нативным токеном сети или LZ token. В fee возвращаются оба значения (nativeFee и lzTokenFee); используйте то, что соответствует вашему выбору: при false смотрите fee.nativeFee и передаете эту сумму в msg.value в readObserve, при true — fee.lzTokenFee, а оплата LZ token идет через механизм протокола (approve + списание). В статье дальше предполагается оплата нативным токеном (payInLzToken = false).Возвращается структура
fee (поля nativeFee, lzTokenFee).Отправка read-запроса: readObserve
Отправляет сформированную командой
getCmd(secondsAgos) read-запрос в read-канал. Вызывать payable, с msg.value >= fee.nativeFee (значение из quoteObserve).
Код:
function readObserve(
uint32[] calldata secondsAgos, // массив для observe, например [3600, 0]
bytes calldata _extraOptions // те же опции, что в quoteObserve (см. выше; при тесте 0x)
) external payable returns (MessagingReceipt memory receipt);
Возвращается
MessagingReceipt (например, для отслеживания в сканере). Ответ придет асинхронно в _lzReceive.Получение ответа: _lzReceive
Вызывается протоколом, когда верифицированный ответ доставлен в origin. В
_message приходят закодированные возвращаемые значения observe(): два массива (int56[] tickCumulatives, uint160[] secondsPerLiquidityCumulativeX128s).
Код:
function _lzReceive(
Origin calldata, // метаданные сообщения (srcEid, sender, nonce)
bytes32, // guid сообщения
bytes calldata _message, // ответ: abi-encoded (int56[], uint160[])
address, // executor
bytes calldata
) internal override {
(int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) =
abi.decode(_message, (int56[], uint160[]));
emit ObserveResult(tickCumulatives, secondsPerLiquidityCumulativeX128s);
}
Декодируем payload и эмитим событие
ObserveResult — по нему можно убедиться, что данные пришли.Конфигурация приложения
Настройка бывает двух видов: на endpoint (библиотеки send/receive, конфиг ReadLib с executor и требования к DVN) и на OApp (enforced options, при необходимости смена read-канала).
Вызывать настройку на endpoint может только сам OApp или его делегат; настройку на OApp (например
setEnforcedOptions) — только владелец OApp. По этой причине, на UniswapV3ObserveRead.sol в конструкторе назначаем владельцем и делегатом наш контракт конфига.Контракт конфига помогает настроить эти параметры LzReadConfig.sol.
Настраивать может деплоер, сам контракт (вызов от его адреса) или делегат — через
setDelegate(address _delegate) из OAppCore.sol.Порядок деплоя и настройки:
Задеплоить LzReadConfig.sol с одним аргументом:_endpoint(адрес Endpoint для сети из таблицы).
Задеплоить UniswapV3ObserveRead.sol, передав в аргументы тот же адрес endpoint и адрес только что задеплоенного контракта конфига.
Одним вызовом настроить и endpoint, и OApp — метод конфигаconfigureFull(_oapp, [I]readChannel, [/I]readLib, [I]libConfig, [/I]receiveGracePeriod, _enforced)задаёт на endpoint библиотеки send/receive и конфиг ReadLib, а на OApp — enforced options (газ и размер ответа для lzRead). Аргументы:
_oapp — адрес задеплоенного OApp (UniswapV3ObserveRead). Берёте из Remix после деплоя.
_readChannel — идентификатор read-канала для пары сетей (origin → data chain). Берётся из таблицы Read Data Channels по вашей сети и целевой.
_readLib — адрес библиотеки Read (например ReadLib1002). Тоже из той же таблицы для вашей сети.
_libConfig — конфиг ReadLib на endpoint:(executor, requiredDVNCount, optionalDVNCount, optionalDVNThreshold, requiredDVNs[], optionalDVNs[]). Адреса executor, requiredDVNs и optionalDVNs и массив требуемых DVNs вы можете выбрать из таблицы.
_receiveGracePeriod — задержка активации receive-библиотеки в секундах; обычно 0 (сразу).
_enforced — принудительные опции для lzRead: структура из трех полей. eid (uint32) = тот же readChannel; msgType (uint16) = 1 для lzRead; options (bytes) — закодированные опции (gas, размер ответа в байтах, value).
Поле options: К сожалению в библиотеках layerZero нет доступа к вспомогательному методу кодировки этих параметров, поэтому я создал для вас тулзу кодировки, которой вы можете воспользоваться для удобства. Скачиваем файл тулзы и открываем в браузере.
В конструкторе OApp уже вызывается
[I]setPeer(READCHANNEL, ...)[/I]. Менять read-канал или отключать приём ответов можно через конфиг: владелец конфига вызывает [I]LzReadConfig.setOAppReadChannel(адрес OApp, channelId, active)[/I] (active = false чтобы отключить).Альтернатива нашему мануальному способу деплою, конфигурации и способу чтению данных является уже репозиторием с уже готовым набором скриптов — LayerZero CLI.
Практика
Представим: в нашей сети (origin) Base Sepolia нет цены на токен, которая нужна контракту. Через lzRead можно запросить данные о цене с другой сети — например, с контракта пула Uniswap V3 в сети Ethereum Sepolia, где этот токен уже торгуется.
Ниже — пошаговый порядок: деплой контракта в origin, оценка комиссии через
quoteObserve, отправка read-запроса через readObserve, проверка транзакции в LayerZero Scan и проверка результата (событие ObserveResult).Можно повторить с указанными ниже адресами или подставить свои из списка, убедившись, что для пары сетей есть Read Path в Read Data Channels и в data chain есть подходящий пул Uniswap V3 с ликвидностью.
Совет: после деплоя сразу делайте Pin contract for current workspace (значок рядом с адресом контракта в Remix), а адреса копируйте — при смене сети развернутые контракты сбрасываются. Чтобы вызвать методы уже развернутого контракта, во вкладке Contract выберите контракт и вставьте его адрес в At Address.
В примере: origin = Base Sepolia, data chain = Ethereum Sepolia.
1.Откройте Remix и добавьте контракты LzReadConfig.sol и UniswapV3ObserveRead.sol.
2.Собираем все данные для деплоя:
Endpoint берем для сети origin с этой таблицы;
EID для data chain берем тут;
targetPoolAddress для target сети можем найти через Uniswap deployments. Убедитесь, что вызов
observe на пуле возвращает данные;readChannel находим в этой таблице, указывая сеть origin и сеть target data;
readLib для origin сети находим тут;
libConfigParams для origin сети тоже находим тут. Он включает в себя такие параметры как: executor, requiredDVNCount, optionalDVNCount, optionalDVNThreshold, requiredDVNs, optionalDVNs;
enforced использует уже известный readChannel, msgType = 1, и options, который мы кодируем через специальную созданную тулзу;
alt text
alt text
3.Задеплойте сначала LzReadConfig.sol (аргумент: endpoint), затем UniswapV3ObserveRead.sol (endpoint, readChannel, targetEid, targetPoolAddress, адрес LzReadConfig).
alt text
alt text
4.Далее, на LzReadConfig вызовите
configureFull(OApp, readChannel, readLib, libConfigParams, 0, enforcedParams).
alt text
5.Оцените комиссию:
quoteObserve(secondsAgos, extraOptions, false) на задеплоенном контракте UniswapV3ObserveRead.sol. Например, secondsAgos = [3600,0] для TWAP за последний час. extraOptions можно передать 0x — enforced options уже заданы.
Вызов quoteObserve в Remix
6.В Remix в поле Value укажите
fee.nativeFee (в Wei) и вызовите readObserve(secondsAgos, extraOptions).
Value и вызов readObserve
7.После подтверждения транзакции, в сканере по адресу
UniswapV3ObserveRead.sol можно найти состояние вашего запроса testnet.layerzeroscan.com.
LayerZero Scan: Response transaction
8.После статуса Delivered в origin будет вызван
_lzReceive и эмитировано событие ObserveResult. Проверить можно по ссылке на Response transaction в разделе логов.
Remix: логи события ObserveResult
Заключение
С lzRead ваш контракт в одной сети может запросить данные в другой и получить ответ обратно — без деплоя контрактов там и без двух отдельных сообщений туда-сюда. Вы формируете запрос, отправляете его по read-каналу и обрабатываете ответ в
_lzReceive. Так же, как мы ранее говорили, есть дополнительный функционал — Compute (lzMap, lzReduce).Ссылки
Docs: Omnichain Queries (lzRead)
Read Data Channels
EVM DVN and Executor Configuration
The lzRead Deep Dive (MapReduce, BQL)
GitHub: LayerZero v2
LayerZeroScan / Testnet LayerZeroScan
Мы с командой делимся мыслями и наблюдениями в своём Telegram-канале. Не всё доходит до статей, что-то проще оформить в пост, чем расписывать длинный текст. Если интересно, чем живём, что обсуждаем и с какими задачами сталкиваемся, то заглядывайте.