Skip to content

Latest commit

 

History

History
202 lines (147 loc) · 14.8 KB

readme.md

File metadata and controls

202 lines (147 loc) · 14.8 KB

ABI

Автор: Павел Найданов 🕵️‍♂️

Contract Application Binary Interface (ABI) - это стандартный способ взаимодействия с контрактом в экосистеме Ethereum. Взаимодействие может происходить как извне блокчейна, так и внутри экосистемы между контрактами.

Чтобы понять необходимость в ABI, необходимо понимать следующие аспекты solidity разработки:

  • EVM. Виртуальная вычислительная среда, другими словами распределенный компьютер, отвечающий за выполнение алгоритмов в сети Ethereum. Такие алгоритмы называют смарт-контрактами
  • Смарт-контракт. Код для выполнения операций в сети Ethereum
  • Machine-Readable Code. EVM не может интерпретировать код смарт-контракта, написанный на высокоуровневом языке программирования. Solidity не интерпретируется в том числе. Любой код должен быть скомпилирован в машиночитаемый код или байт-код, который содержит инструкции в двоичном формате

Таким образом для того, чтобы код смарт-контрактов стал понятен EVM необходима компиляция высокоуровнего кода в байт-код. В результате компиляции мы получаем два продукта:

  • Machine-Readable Code или байт-код. Мы говорили о нем выше.
  • Двоичный интерфейс приложения(ABI). Он необходим для того, чтобы мы понимали что закодировано в байт-коде и могли с ним взаимодействовать.

Как происходит кодирование?

Мы знаем, что существуют статические и динамические типы данных.

Пример динамических типов:

  • bytes
  • string
  • T[] для любого T(массив с любыми типами данных)

Все остальные типы являются статическими(uint, address, bool и т.д.)

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

Важно! Статические и динамические типы кодируются по-разному. Статические типы кодируются на месте. Для динамических типов кодируется "ссылка" или "сдвиг", которая означает количество байтов. Выполняя смещение на это количество байт можно получить значение для динамического типа данных.

Кодирование вызова функций

Для вызова функции сначала кодируется селектор функции(function selector). Это первые четыре байта хэша Keccak-256 сигнатуры функции.

  bytes4(keccak256("function signature")

Сигнатура определяется как каноническое выражение базового прототипа функции без модификаторов. Типы параметров разделяются одной запятой — пробелы не используются.

Пример закодированного селектора функции sum():

  sum(uint256,uint256) => cad0899b

Начиная с пятого байта кодируются аргументы функции:

  • если тип аргумента статический, то кодируется сразу его значение(в 32-х байтах)
  • если тип аргумента динамический, то кодируется указатель(смещение или offset) на начало хранения значение аргумента относительно начала блока аргументов. А уже по смещению можно найти само значение.

Закрепляем примером

В спецификации было несколько примеров. Ниже разберем похожий пример кодирования вызова функции.

Будем кодировать функцию bar(uint256,uint256[]). В эту функцию переданы аргументы 42 и массив [21, 22]. Алгоритм кодирования будет выглядеть следующим образом:

  1. Кодируем селектор функции, как
  bytes4(keccak256("bar(uint256,uint256[])") => ae2c7970

На данном этапе получаем:

  0xae2c7970
  1. Кодируем первый аргумент 42. Тип аргумента uint256 является статическим. Поэтому кодируем значение 42 на месте. 42 в hex равно 2a. 2a дополняем до 32-x байтного слова.
  000000000000000000000000000000000000000000000000000000000000002a

На данном этапе получаем:

  0xae2c7970 + 000000000000000000000000000000000000000000000000000000000000002a
  1. Кодируем второй аргумент [21, 22]. Тип аргумента uint256[] является динамическим. Поэтому кодируем сначала сдвиг(ссылку) на место, где будет закодирована длина массива. Помним, что считаем количество байтов от начала блока аргументов 000000000000000000000000000000000000000000000000000000000000002a => 32 байта + 32 байта(значение будущего сдвига). Получается длина массива будет закодирована после 64 байт. Кодируем значение 64. В hex это равно 40. По традиции дополняем до 32-х байт.
  0000000000000000000000000000000000000000000000000000000000000040

На данном этапе получаем:

  0xae2c7970 + 000000000000000000000000000000000000000000000000000000000000002a + 0000000000000000000000000000000000000000000000000000000000000040
  1. После того, как мы закодировали сдвиг, нам необходимо закодировать саму длину массива. Чтобы знать, когда остановится считывать элементы массива. В данном случае длина массива равна 2. Кодируем:
  0000000000000000000000000000000000000000000000000000000000000002

На данном этапе получаем:

  0xae2c7970 + 000000000000000000000000000000000000000000000000000000000000002a + 0000000000000000000000000000000000000000000000000000000000000040 + 0000000000000000000000000000000000000000000000000000000000000002
  1. Осталось только закодировать два значения массива 21(15 в hex) и 22(16 hex). Дополняем эти значения до 32-x байтных слов
  21 => 0000000000000000000000000000000000000000000000000000000000000015
  22 => 0000000000000000000000000000000000000000000000000000000000000016

На данном этапе получаем:

  0xae2c7970 + 000000000000000000000000000000000000000000000000000000000000002a + 00000000000000000000000000000000000000000000000000000000000000040 + 0000000000000000000000000000000000000000000000000000000000000002 + 0000000000000000000000000000000000000000000000000000000000000015 + 0000000000000000000000000000000000000000000000000000000000000016
  1. Для наглядности можно представить это следующим образом
Encoded function selector:
0xae2c7970

Arguments block:
0 - 000000000000000000000000000000000000000000000000000000000000002a - encoded 42 value
1 - 0000000000000000000000000000000000000000000000000000000000000040 - encoded offset for [21, 22]
2 - 0000000000000000000000000000000000000000000000000000000000000002 - number of items in the array [21, 22]
3 - 0000000000000000000000000000000000000000000000000000000000000015 - encoded 21 value(the first array item)
4 - 0000000000000000000000000000000000000000000000000000000000000016 - encoded 22 value(the second array item)

В результате мы получаем следующий хэш:

  0xae2c7970000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000150000000000000000000000000000000000000000000000000000000000000016

Как выглядит ABI?

После компиляции ABI представлен в виде json файла. Пример ABI ERC20 токена можно посмотреть тут.

Формат JSON для ABI контракта состоит из описания функций, событий и ошибок.

Описание функции представляет собой объект JSON с полями:

  • type: тип функции("function", "constructor", "receive","fallback");
  • name: имя функции;
  • inputs: массив объектов на вход(аргументы функции), каждый из которых содержит: name(имя параметра), type и components(для кортежей)
  • outputs: массив объектов на выход(возвращаемые значения), похож на inputs.
  • stateMutability: pure, view, nonpayable и payable.

Важно! Тип возвращаемого значения функции не является частью сигнатуры функции, а значит и не кодируется. Однако ABI содержит информацию о выходных значениях в поле outputs.

Encoding и decoding

Для взаимодействия с байт-кодом смарт контракта данные кодируются и декодируются. Это постоянный двусторонний процесс.

В solidity существуют специальные функции для работы с кодированием и декодированием данных:

  • abi.decode(bytes memory encodedData, (...)) returns (...)
  • abi.encode(...) returns (bytes memory)
  • abi.encodePacked(...) returns (bytes memory)
  • abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory)
  • abi.encodeWithSignature(string memory signature, ...) returns (bytes memory)
  • abi.encodeCall(function functionPointer, (...)) returns (bytes memory)

Подробнее про эти функции в документации тут

Неплохая статья, которая дает чуть больше пояснений для этих встроенных функций.

Encode vs encodePacked

abi.encode();

Это стандартный метод кодирования аргументов, согласно выше описанной спецификации.

abi.encodePacked();

Это нестандартный, упакованный режим кодировки. Особенности:

  • Значения, типы которых короче 32 байтов не дополняются нулями и не дополняются знаками
  • Динамические типы кодируются на месте и без длины.
  • Элементы массива дополняются нулями, но все еще кодируются на месте
  • Структуры и вложенные массивы не поддерживаются

Подробнее тут.

Важно! Если для keccak256(abi.encodePacked(a, b)), где a и b динамические типы, то легко получить коллизию хешей. Более того справедливо следующее abi.encodePacked("a", "bc") == abi.encodePacked("ab", "c"). Для таких случаев с динамическими типами лучше использовать стандартный abi.encode().

Эти и другие полезные наработки по кодированию и декодированию данных при помощи abi можно опробовать в remix с нашим заготовленным контрактом.

Links

  1. Solidity ABI docs
  2. ABI Encoding and Decoding Functions
  3. Everything You Need To Know About Solidity’s Application Binary Interface (ABI)
  4. ABI encode and decode using solidity
  5. Solidity ABI Encode and Decode