Автор: Роман Ярлыков 🧐
Важно! Команда протокола активно работает над улучшением совместимости, поэтому некоторые (или все) особенности, которые будут описаны ниже могут устареть или потерять актуальность со временем.
Note: Опкоды ниже более подробно описаны в официальной документации.
В zkEVM объеденины код развертывания (creationCode) и код выполнения (runtimeCode), из-за этого скорректированы datasize
и dataoffset
.
Здесь существует пара основных моментов о которых нужно помнить:
- Проверять как был получен
bytecode
Если в коде контрактов или библиотек явно (не через new
) используются create
или create2
(с помощью Yul), то bytecode
развертываемого контракта обязательно должен быть получен в этом же контракте, либо в другом, но через вызов type(T).creationCode
.
Дело в том, что компилятор zksolc
модифицирует bytecode
для системного контракта ContractDeployer
. Если взять bytecode
который был получен с помощью стандартного solc
это не сработает. Примеры показаны в документации.
Более того, bytecode из artifacts-zk
, который был предварительно скомпилирован с помощью zksolc
тоже не подойдет, вот два примера байткода одного контракта:
Этот взят из .json
файла папки artifacts-zk
0x0000008003000039000000400030043f0000000102200190000000120000c13d000000000201001900000009022001980000001a0000613d000000000101043b0000000a011001970000000b0110009c0000001a0000c13d0000000001000416000000000101004b0000001a0000c13d0000002a01000039000000800010043f0000000c010000410000001d0001042e0000000001000416000000000101004b0000001a0000c13d00000020010000390000010000100443000001200000044300000008010000410000001d0001042e00000000010000190000001e000104300000001c000004320000001d0001042e0000001e000104300000000000000000000000020000000000000000000000000000004000000100000000000000000000000000000000000000000000000000fffffffc000000000000000000000000ffffffff00000000000000000000000000000000000000000000000000000000f2c9ecd80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000008000000000000000000000000000000000000000000000000000000000000000000000000000000000fe656f219dacf5ce8b73b813cb9203d0f8598845700707ab67bcbac593600ec9
Если передать его в эту функцию, контракт не будет создан:
function createContract(bytes memory bytecode) external returns (address addr) {
assembly {
addr := create(0, add(bytecode, 0x20), mload(bytecode))
}
require(addr != address(0), "Create: Failed on deploy");
}
А вот bytecode
того-же контракта получен через вызов функции type(T).creationCode:
0x0000000000000000000000000000000000000000000000000000000000000000000000000100000fad4cfc3855d0e61bd17ecca835f2a2f01ddfdeb9a48d4d5ced5cf98a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Получен вызовом функции:
function getCreationCode() external pure returns (bytes memory) {
return type(Implementation).creationCode;
}
Этот байткод сработает в функции createContract
.
- Проверять нет ли в контрактах вызова type(T).runtimeCode
C runtimeCode проще, если где-то в смарт-контрактах встречается вызов этой функции компилятор zksolc
выдаст ошибку и контракты не скомпилируются.
Так как байт-код zkSync отличается от Ethereum, поскольку zkSync использует модифицированную версию EVM - адрес, полученный из хэша байт-кода, также будет отличаться. Это означает, что один и тот же байт-код, развернутый в Ethereum и zkSync, будет иметь разные адреса, а адрес Ethereum по-прежнему будет доступен и не используется в zkSync.
Важно! В будущем может быть достигнут паритет с Ethereum в деривации адресов.
Здесь есть пара особенностей.
В вызовах функций Yul, таких как call
(call
, callcode
, delegatecall
, staticcall
) можно последним аргументом передать размер возвращаемых данных и если он будет отличаться от фактического returndatasize
в EVM мы получим ошибку типа Panic
. В zkEVM такого не произойдет, т.к. память выделяется только после вызова другого контракта.
Это не довольно редкий кейс, потому что после хардфорка Byzantium можно не указывать размер возвращаемых данных (outsize
).
Вторая особенность в том, что передача эфира под капотом обрабатывается смарт-контрактом MsgValueSimulator
, но разработчикам для этого ничего не нужно делать, вся логика с msg.value будет работать как и в обычной EVM среде.
Здесь 3 особенности:
-
Прирост memory осуществляется в байтах а не в словах. Напомню что слово - это 32 байта. Поэтому, в EVM когда мы осуществляем запись значения по адресу 100 (
0x64
) - из-за смещения в 4 байта относительно слова memory будет дополнена еще 28 байтами и окончательный размер станет 160 байт (0x64
+0x20
+0x1c
=0xa0
). В zkSync этого прироста не произойдет и в аналогичной ситуации при записи по адресу 100 (0x64
) размер memory будет 132 байта (0x64
+0x20
=0x84
). -
В связи с первой особенностью компилятор zksolc может самостоятельно убирать неиспользуемую память, что привело бы к ошибке типа "Panic" в EVM, но в zkEVM ошибок это не вызовет.
-
Стоимость газа при работе с memory в EVM имеет квадратичный рост - в zkEVM этот рост будет линейным.
В этом пункте все просто, размер calldata ограничен значением 232 байт.
Переменные типа immutable
не будут проинициализированы если в конструкторе есть assembly вставка с вызовом return(p, s)
или stop()
потому что это переопределит массив переменных immutable
.
В zkSync Era существует два понятия блоков: блоки L2 и пакеты L1. Блоки L2 — это блоки, созданные на уровне L2 (то есть в сети zkSync Era), они создаются каждые несколько секунд и не включаются в цепочку Ethereum. С другой стороны, пакеты L1 — это пакеты последовательных блоков L2, которые содержат все транзакции в одном и том же порядке, от первого блока до последнего блока в пакете.
До этого обновления block.timestamp
, block.number
и block.hash
отображали данные блоков L1. После обновления они будут передавать данные пакетов L2, а для соответствующих значений L1 будут добавлены дополнительные методы на уровне смарт-контрактов (сейчас эти методы доступны только по API).
Использование CODECOPY c new Yul codegen приведет к ошибке компиляции.
EXTCODECOPY всегда выдает ошибку времени компиляции компилятором zkEVM.
Развертывание контракта обрабатывается двумя частями протокола zkEVM: интерфейсом компилятора и системным контрактом, называемым ContractDeployer. Поэтому если развертывание выполняется с использованием Yul, а не через new
необходимо учитывать эту особенность.
В zkEVM доступ к байт-коду контракта отсутствует, поэтому immutable
значения (неизменяемые переменные) симулируют с помощью системных контрактов. Код развертывания (конструктор) собирает массив иммутабельных значений в вспомогательной памяти, возвращая его как данные в ContractDeployer
. Затем массив передается в специальный системный контракт ImmutableSimulator
, где он хранится в отображении с адресом контракта в качестве ключа. Для доступа к иммутабельным значениям из исполняемого кода контракты вызывают ImmutableSimulator
, используя адрес и индекс значения.
Изменения простые для понимания - смотреть тут.
В zkEVM библиотеки работают немного иначе, чем в традиционном EVM:
Встраивание Библиотеки: Оптимизатор компилятора Solidity должен встроить библиотеку, чтобы её можно было использовать без развертывания. Встраивание означает, что код библиотеки напрямую вставляется в байткод контракта, устраняя необходимость в отдельном развертывании.
Развернутые Библиотеки: Если библиотека не встроена и развернута, её адрес должен быть указан в конфигурации проекта. Во время компиляции эти адреса используются для замены заполнителей в Промежуточном Представлении (IR): linkersymbol в Yul или PUSHLIB в устаревшей сборке EVM.
Связывание на Этапе Компиляции: Все связывание библиотек, то есть процесс интеграции кода библиотеки с кодом контракта, происходит на этапе компиляции. zkEVM не поддерживает связывание во время развертывания, когда адреса библиотек устанавливаются во время развертывания контракта, а не во время компиляции.
В zkEVM не все криптографические функции, которые есть в обычном EVM, доступны. Например, функции для работы с эллиптическими парами и RSA пока недоступны, но в приоритете разработки находится добавление поддержки эллиптических пар, чтобы можно было использовать некоторые протоколы без изменений в коде.
Однако базовые криптографические операции, такие как восстановление адреса из подписи (ecrecover), хеш-функции keccak256 и sha256, уже поддерживаются как предварительно скомпилированные функции. Это означает, что вы можете использовать их так же, как и в обычной сети Ethereum, и компилятор автоматически обработает все вызовы этих функций за вас.