Deep EVM #11: Jump Table Huff — Dispatch Fungsi O(1) Tanpa Overhead Solidity
Engineering Team
Masalah dengan Dispatcher Solidity
Ketika Anda memanggil kontrak Solidity, hal pertama yang dieksekusi EVM adalah function dispatcher. Solidity menghasilkan rantai if-else linear yang membandingkan 4 byte pertama calldata (function selector) dengan setiap selector yang diketahui:
CALLDATASIZE -> CALLDATALOAD -> SHR 224 -> DUP1 -> PUSH4 selector
-> EQ -> PUSH2 dest -> JUMPI -> ...
Untuk kontrak dengan N fungsi, ini O(N) — kasus terburuk memeriksa semua N selector sebelum menemukan kecocokan. Setiap perbandingan memerlukan sekitar 22 gas (DUP1 + PUSH4 + EQ + PUSH2 + JUMPI). Kontrak dengan 20 fungsi membuang hingga 440 gas hanya untuk dispatch.
Untuk kontrak bot MEV yang dipanggil jutaan kali, 400+ unit gas per panggilan bertambah menjadi ETH nyata.
Pendekatan Jump Table: O(1)
Jump table memetakan function selector langsung ke offset kode menggunakan aritmatika, bukan perbandingan. Idenya dipinjam dari arsitektur CPU — computed GOTO telah digunakan dalam pemrograman sistem sejak 1960-an.
Konsepnya:
- Ekstrak function selector dari calldata (4 byte).
- Gunakan aritmatika untuk menghitung tujuan jump dari selector.
- JUMP langsung ke tujuan tersebut.
Tanpa perbandingan, tanpa branching, waktu konstan terlepas dari berapa banyak fungsi yang ada.
Pendekatan 1: Encoding Selector Minimal
Jika kontrak Anda memiliki sejumlah kecil fungsi (1-8), Anda bisa menetapkan selector secara manual dengan mining vanity selector di mana byte pertama atau dua mengkodekan integer kecil yang unik:
#define macro DISPATCHER() = takes(0) returns(0) {
0x00 calldataload // [calldata_word]
0xe0 shr // [selector]
// Ekstrak routing byte — byte pertama selector
0x18 shr // [first_byte]
// Setiap entri di tabel kita adalah 2 byte (PUSH2 offset)
0x02 mul // [offset_dalam_tabel]
__tablestart(JumpTable) // [table_start, offset_dalam_tabel]
add // [entry_address]
0x00 codecopy
0x00 mload
0xf0 shr
jump
}
#define jumptable JumpTable {
swap_exact
add_liq
remove_liq
flash_loan
}
Pendekatan 2: Tabel Kode yang Dipadatkan
Untuk kepadatan maksimum, Anda bisa memadatkan jump table langsung ke dalam bytecode menggunakan __tablestart dan __tablesize. Huff secara native mendukung jump table sebagai konstruk kelas satu:
#define jumptable__packed SELECTOR_TABLE {
fn_swap
fn_transfer
fn_approve
fn_balance
}
#define macro MAIN() = takes(0) returns(0) {
0x00 calldataload 0xe0 shr
0x18 shr
dup1 0x04 lt
valid jumpi
0x00 0x00 revert
valid:
__tablestart(SELECTOR_TABLE)
swap1 0x02 mul add
0x1e mload
jump
fn_swap:
SWAP_IMPL()
fn_transfer:
TRANSFER_IMPL()
fn_approve:
APPROVE_IMPL()
fn_balance:
BALANCE_IMPL()
}
Perbandingan Gas
| Fungsi | Solidity (if-else) | Solidity (binary) | Huff Jump Table |
|---|---|---|---|
| 2 | 22-44 gas | 22-44 gas | 15 gas |
| 4 | 22-88 gas | 22-66 gas | 15 gas |
| 8 | 22-176 gas | 22-88 gas | 15 gas |
| 16 | 22-352 gas | 22-110 gas | 15 gas |
| 32 | 22-704 gas | 22-132 gas | 15 gas |
Biaya jump table konstan: CALLDATALOAD (3) + SHR (3) + aritmatika (3-6) + JUMP (8) = ~15-18 gas. Tidak pernah berubah terlepas dari jumlah fungsi.
Mining Vanity Selector
Agar pendekatan jump table berfungsi, Anda memerlukan function selector dengan routing byte yang bisa diprediksi. Anda bisa mining ini:
import hashlib
import itertools
target_byte = 0x00
base_name = "swap"
for suffix in itertools.count():
name = f"{base_name}{suffix}(uint256,address)"
selector = hashlib.sha3_256(name.encode()).digest()[:4]
if selector[0] == target_byte:
print(f"Ditemukan: {name} -> 0x{selector.hex()}")
break
Dalam praktik, kami menggunakan cast sig dari Foundry atau tool Rust yang brute-force nama fungsi untuk menemukan selector dengan prefix yang diinginkan.
Dampak Ukuran Bytecode
| Pendekatan | Runtime Bytecode | Gas Deploy |
|---|---|---|
| Solidity (8 funcs) | ~800 byte | 160.000 gas |
| Huff jump table (8 funcs) | ~200 byte | 40.000 gas |
| Huff minimal (2 funcs) | ~61 byte | 12.200 gas |
Untuk bot MEV yang sering men-redeploy kontrak, bytecode lebih kecil langsung diterjemahkan menjadi biaya operasional yang lebih rendah.
Batasan
- ABI non-standar — Tool eksternal (Etherscan, wallet) tidak bisa mendecode calldata Anda tanpa definisi ABI kustom.
- Mining selector — Memerlukan pekerjaan di muka dan membatasi penamaan fungsi.
- Biaya pemeliharaan — Huff lebih sulit diaudit dan dimodifikasi dari Solidity.
- Tabel yang dipadatkan — Builtin
__tablestartdan__tablesizememiliki edge case dengan compiler Huff; uji secara menyeluruh.
Ringkasan
Jump table menggantikan rantai dispatch O(N) Solidity dengan computed jump O(1). Penghematan gas bertambah melintasi jutaan panggilan — keunggulan signifikan untuk kontrak frekuensi tinggi seperti bot MEV dan router DEX.