Deep EVM #9: Pengantar Bahasa Huff — Makro, Label, dan Opcode Mentah
Engineering Team
Mengapa Huff Ada
Solidity adalah abstraksi yang luar biasa — sampai ia tidak cukup. Ketika Anda memerlukan kontrak yang muat dalam 100 byte runtime bytecode, men-dispatch fungsi dalam O(1) dengan jump table yang dipadatkan, atau menghemat 200 gas dari hot path yang dieksekusi jutaan kali per hari, Anda memerlukan sesuatu yang lebih dekat ke metal. Sesuatu itu adalah Huff.
Huff adalah bahasa assembly EVM tingkat rendah dengan sistem makro tipis yang ditempelkan di atasnya. Ia tidak memiliki variabel, tipe, atau compiler yang mengoptimasi di belakang Anda. Apa yang Anda tulis adalah apa yang berakhir di chain — opcode per opcode.
Menginstal Huff
Compiler kanonik adalah huffc, ditulis dalam Rust:
curl -L get.huff.sh | bash
huffup
huffc --version
Ini menginstal huffc ke ~/.huff/bin. Tambahkan ke PATH Anda dan verifikasi:
$ huffc --version
huffc 0.3.2
Anda juga bisa menggunakan Huff dalam proyek Foundry dengan foundry-huff, yang memungkinkan Anda men-deploy file .huff dengan cara yang sama seperti file .sol.
Hello World: Kontrak Minimal
Mari kita tulis kontrak yang mengembalikan word 32-byte 0x01 untuk setiap panggilan:
#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]
}
Kompilasi:
huffc src/HelloWorld.huff -r
Flag -r menghasilkan runtime bytecode. Anda akan melihat sesuatu seperti 600160005260206000f3 — 10 byte. Kontrak Solidity yang mengembalikan 1 dikompilasi menjadi sekitar 200+ byte runtime bytecode karena solc menghasilkan dispatcher fungsi lengkap, hash metadata, setup free memory pointer, dan ABI encoder.
Makro vs Fungsi
Huff memiliki dua primitif penggunaan ulang kode: makro dan fungsi.
Makro (#define macro)
Makro di-inline di setiap call site. Tanpa overhead JUMP, tanpa gas ekstra — compiler secara harfiah menyalin-tempelkan opcode ke dalam pemanggil. Ini adalah default dan pilihan yang diprioritaskan untuk kode yang kritis gas.
#define macro REQUIRE_NOT_ZERO() = takes(1) returns(0) {
// takes: [value]
continue // [continue_dest, value]
jumpi // [] — jump jika value != 0
0x00 0x00 revert
continue:
}
Fungsi (#define fn)
Fungsi menghasilkan pasangan JUMP/JUMPDEST yang sebenarnya. Mereka menghemat ukuran bytecode dengan mengorbankan ~22 gas ekstra per panggilan (8 untuk JUMP + 1 untuk JUMPDEST + manipulasi stack). Gunakan hanya ketika ukuran bytecode lebih penting dari gas.
#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:
}
Label dan Jump Destination
Label dalam Huff adalah lokasi JUMPDEST yang diberi nama. Compiler menyelesaikannya menjadi offset bytecode konkret saat waktu kompilasi.
#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]
}
Setiap label dikompilasi menjadi satu byte JUMPDEST (0x5b). Referensi (loop jump, done jumpi) dikompilasi menjadi PUSH2 <offset> JUMP (atau JUMPI). Ini persis apa yang akan Anda tulis dengan tangan di assembly EVM mentah — Huff hanya menangani pembukuan offset.
takes() dan returns()
Anotasi takes(n) dan returns(m) pada makro dan fungsi adalah dokumentasi dan petunjuk compiler. Mereka memberitahu pembaca — dan pemeriksa stack compiler Huff — berapa banyak item stack yang diharapkan blok untuk dikonsumsi dan dihasilkan.
#define macro ADD_TWO() = takes(2) returns(1) {
add // mengkonsumsi 2 item, menghasilkan 1
}
Jika perilaku stack aktual Anda tidak cocok dengan anotasi, huffc akan mengeluarkan peringatan. Perlakukan anotasi ini sebagai sistem tipe sederhana — mereka mencegah Anda secara tidak sengaja meninggalkan sampah di stack atau underflow.
Perbandingan: Huff vs Bytecode Solidity
Pertimbangkan fungsi getValue() view sederhana yang mengembalikan slot storage:
Solidity:
function getValue() external view returns (uint256) {
return value;
}
Solc menghasilkan ~40 byte untuk dispatcher + ABI encoding.
Setara Huff:
#define function getValue() view returns (uint256)
#define macro GET_VALUE() = takes(0) returns(0) {
[VALUE_SLOT] // [slot]
sload // [value]
0x00 mstore // [] — simpan di memory
0x20 0x00 return
}
Versi Huff adalah 12 byte bytecode untuk body. Tanpa overhead ABI encoding, tanpa free memory pointer, tanpa hash metadata. Ketika Anda mengontrol pemanggil (misalnya bot MEV yang memanggil kontraknya sendiri), Anda bisa menghilangkan semua yang diasumsikan compiler Solidity Anda butuhkan.
Konstanta dan Slot Storage
Konstanta Huff adalah nilai waktu kompilasi yang di-inline sebagai instruksi PUSH:
#define constant VALUE_SLOT = 0x00
#define constant OWNER_SLOT = 0x01
#define constant MAX_UINT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
Penggunaan: [VALUE_SLOT] mendorong 0x00, [MAX_UINT] mendorong nilai 32-byte penuh. Konstanta membantu keterbacaan tanpa biaya gas — mereka murni sintaksis.
Include dan Struktur Proyek
Proyek Huff nyata membagi kode ke beberapa file:
// src/Main.huff
#include "./utils/SafeMath.huff"
#include "./interfaces/IERC20.huff"
#include "./Dispatcher.huff"
#define macro MAIN() = takes(0) returns(0) {
DISPATCHER()
}
Sistem include adalah inklusi teks sederhana — tanpa scoping modul atau namespace. Beri nama makro Anda dengan hati-hati untuk menghindari collision.
Kapan Menggunakan Huff
Huff bukan bahasa tujuan umum. Gunakan ketika:
- Gas adalah kendala utama — Kontrak MEV di mana 100 gas menentukan profitabilitas.
- Ukuran bytecode penting — Kontrak yang di-deploy oleh kontrak lain (factory CREATE2) di mana initcode lebih kecil = gas deployment lebih rendah.
- Anda memerlukan dispatch kustom — Jump table, selector yang dipadatkan bit, atau encoding ABI non-standar.
- Anda sedang belajar EVM — Tidak ada yang mengajarkan EVM lebih baik dari menulis opcode mentah.
Untuk segala hal lain, tulis Solidity dan baca output compiler dengan solc --asm. Anda akan lebih produktif dan lebih sedikit kesalahan.
Ringkasan
Huff memberikan jalur langsung ke bytecode EVM dengan abstraksi yang cukup untuk tetap waras. Makro meng-inline kode untuk penggunaan ulang tanpa overhead. Label menangani pembukuan offset jump. Anotasi takes/returns menangkap error stack lebih awal. Di artikel berikutnya, kita akan menyelam lebih dalam ke manajemen stack — seni dup, swap, dan menjaga model mental Anda tentang stack tetap sinkron dengan realitas.