Deep EVM #9: Язык Huff — макросы, метки и сырые опкоды
Engineering Team
Зачем нужен Huff
Solidity — замечательная абстракция, пока вы не упрётесь в её ограничения. Когда контракт должен уместиться в 100 байт рантайм-байткода, диспатчить функции за O(1) через упакованную jump-таблицу или сэкономить 200 газа на горячем пути, который исполняется миллионы раз в сутки, вам нужно нечто ближе к железу. Это нечто — Huff.
Huff — низкоуровневый язык ассемблера EVM с тонкой макросистемой поверх. Нет переменных, нет типов, нет компилятора, который оптимизирует за вашей спиной. Что вы написали — то и окажется on-chain, опкод за опкодом.
Установка Huff
Каноничный компилятор — huffc, написанный на Rust:
curl -L get.huff.sh | bash
huffup
huffc --version
Команда устанавливает huffc в ~/.huff/bin. Добавьте директорию в PATH и проверьте:
$ huffc --version
huffc 0.3.2
Вы также можете использовать Huff внутри Foundry-проектов с помощью foundry-huff, который позволяет деплоить .huff файлы так же, как .sol.
Hello World: минимальный контракт
Напишем контракт, возвращающий 32-байтное слово 0x01 на любой вызов:
#define macro MAIN() = takes(0) returns(0) {
0x01 // [0x01]
0x00 // [0x00, 0x01]
mstore // [] — memory[0x00..0x20] = 0x01
0x20 // [0x20]
0x00 // [0x00, 0x20]
return // halt — return memory[0x00..0x20]
}
Компиляция:
huffc src/HelloWorld.huff -r
Флаг -r выводит рантайм-байткод. Вы увидите что-то вроде 600160005260206000f3 — 10 байт. Контракт на Solidity, возвращающий 1, компилируется в ~200+ байт рантайм-байткода, потому что solc генерирует полный диспатчер функций, хеш метаданных, инициализацию free memory pointer и ABI-кодировщик.
Макросы vs функции
Huff имеет два примитива повторного использования: макросы и функции.
Макросы (#define macro)
Макросы инлайнятся в каждом месте вызова. Нет оверхеда JUMP, нет лишнего газа — компилятор буквально копирует опкоды в вызывающий код. Это выбор по умолчанию для критичного по газу кода.
#define macro REQUIRE_NOT_ZERO() = takes(1) returns(0) {
// takes: [value]
continue // [continue_dest, value]
jumpi // [] — jump if value != 0
0x00 0x00 revert
continue:
}
Функции (#define fn)
Функции генерируют реальную пару JUMP/JUMPDEST. Они экономят размер байткода ценой ~22 лишних газа за вызов (8 за JUMP + 1 за JUMPDEST + манипуляции со стеком). Используйте только когда размер байткода важнее газа.
#define fn safe_add() = takes(2) returns(1) {
// takes: [a, b]
dup2 dup2 // [a, b, a, b]
add // [sum, a, b]
dup1 // [sum, sum, a, b]
swap2 // [a, sum, sum, b]
gt // [overflow?, sum, b]
overflow jumpi
swap1 pop // [sum]
back jump
overflow:
0x00 0x00 revert
back:
}
Метки и точки перехода
Метки в Huff — именованные JUMPDEST-локации. Компилятор разрешает их в конкретные смещения байткода во время компиляции.
#define macro LOOP_EXAMPLE() = takes(1) returns(1) {
// takes: [n]
0x00 // [acc, n]
loop:
dup2 // [n, acc, n]
iszero // [n==0?, acc, n]
done jumpi // [acc, n]
swap1 // [n, acc]
0x01 swap1 sub // [n-1, acc]
swap1 // [acc, n-1]
0x01 add // [acc+1, n-1]
loop jump
done:
swap1 pop // [acc]
}
Каждая метка компилируется в один байт JUMPDEST (0x5b). Ссылки (loop jump, done jumpi) компилируются в PUSH2 <offset> JUMP (или JUMPI). Это в точности то, что вы бы написали вручную на сыром ассемблере EVM — Huff просто берёт на себя учёт смещений.
takes() и returns()
Аннотации takes(n) и returns(m) на макросах и функциях — документация и подсказки компилятору. Они сообщают читателю (и стековому чекеру huffc), сколько элементов стека блок ожидает потребить и произвести.
#define macro ADD_TWO() = takes(2) returns(1) {
add // потребляет 2 элемента, производит 1
}
Если реальное поведение стека не совпадает с аннотацией, huffc выдаст предупреждение. Рассматривайте аннотации как бедную систему типов — они предотвращают случайное оставление мусора на стеке или underflow.
Сравнение: Huff vs Solidity байткод
Рассмотрим простую view-функцию getValue(), возвращающую слот хранилища:
Solidity:
function getValue() external view returns (uint256) {
return value;
}
Solc генерирует ~40 байт для диспатчера + ABI-кодирования.
Huff-эквивалент:
#define function getValue() view returns (uint256)
#define macro GET_VALUE() = takes(0) returns(0) {
[VALUE_SLOT] // [slot]
sload // [value]
0x00 mstore // [] — store in memory
0x20 0x00 return
}
Huff-версия — 12 байт байткода для тела. Нет оверхеда ABI-кодирования, нет free memory pointer, нет хеша метаданных. Когда вы контролируете вызывающий код (например, MEV-бот вызывает собственный контракт), вы можете убрать всё, что solc считает необходимым.
Константы и слоты хранилища
Константы Huff — значения, вычисляемые во время компиляции, которые инлайнятся как PUSH-инструкции:
#define constant VALUE_SLOT = 0x00
#define constant OWNER_SLOT = 0x01
#define constant MAX_UINT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
Использование: [VALUE_SLOT] кладёт 0x00, [MAX_UINT] — полное 32-байтное значение. Константы улучшают читаемость без затрат газа.
Инклюды и структура проекта
Реальные Huff-проекты разносят код по нескольким файлам:
// src/Main.huff
#include "./utils/SafeMath.huff"
#include "./interfaces/IERC20.huff"
#include "./Dispatcher.huff"
#define macro MAIN() = takes(0) returns(0) {
DISPATCHER()
}
Система инклюдов — простое текстовое включение, без модульной области видимости или пространств имён. Тщательно именуйте макросы во избежание коллизий.
Когда использовать Huff
Huff — не язык общего назначения. Используйте его когда:
- Газ — главное ограничение — MEV-контракты, где 100 газа определяют прибыльность.
- Размер байткода критичен — контракты, деплоимые другими контрактами (CREATE2-фабрики).
- Нужен кастомный диспатч — jump-таблицы, бит-упакованные селекторы, нестандартное ABI.
- Вы изучаете EVM — ничто не учит EVM лучше, чем написание сырых опкодов.
Для всего остального пишите на Solidity и читайте вывод компилятора через solc --asm.
Итоги
Huff даёт прямой доступ к байткоду EVM с минимальной абстракцией. Макросы инлайнят код с нулевым оверхедом. Метки берут на себя учёт смещений. Аннотации takes/returns ловят стековые ошибки на ранних этапах. В следующей статье мы глубже погрузимся в управление стеком — искусство dup, swap и синхронизации ментальной модели стека с реальностью.