Deep EVM #9: Huff 언어 입문 — 매크로, 레이블, 원시 옵코드
Engineering Team
Huff가 존재하는 이유
Solidity는 훌륭한 추상화입니다 — 그렇지 않을 때까지. 100바이트의 런타임 바이트코드 안에 들어가는 컨트랙트가 필요하거나, 압축된 점프 테이블로 O(1) 함수 디스패치가 필요하거나, 하루에 수백만 번 실행되는 핫 패스에서 200 가스를 절약해야 할 때, 메탈에 더 가까운 무언가가 필요합니다. 그것이 바로 Huff입니다.
Huff는 얇은 매크로 시스템이 덧붙여진 저수준 EVM 어셈블리 언어입니다. 변수도, 타입도, 뒤에서 최적화하는 컴파일러도 없습니다. 당신이 쓰는 것이 그대로 온체인에 올라갑니다 — 옵코드 대 옵코드.
Huff 설치
표준 컴파일러는 Rust로 작성된 huffc입니다:
curl -L get.huff.sh | bash
huffup
huffc --version
이것은 huffc를 ~/.huff/bin에 설치합니다. PATH에 추가하고 확인하세요:
$ huffc --version
huffc 0.3.2
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바이트. 1을 반환하는 Solidity 컨트랙트는 약 200바이트 이상의 런타임 바이트코드로 컴파일됩니다. solc가 전체 함수 디스패처, 메타데이터 해시, 프리 메모리 포인터 설정, ABI 인코더를 생성하기 때문입니다.
매크로 vs 함수
Huff에는 두 가지 코드 재사용 기본 요소가 있습니다: 매크로와 함수.
매크로 (#define macro)
매크로는 모든 호출 사이트에서 인라인됩니다. JUMP 오버헤드도, 추가 가스도 없습니다 — 컴파일러가 문자 그대로 옵코드를 호출자에 복사-붙여넣기합니다. 가스 크리티컬 코드에서는 기본이자 선호되는 선택입니다.
#define macro REQUIRE_NOT_ZERO() = takes(1) returns(0) {
// takes: [value]
continue // [continue_dest, value]
jumpi // [] — value != 0이면 점프
0x00 0x00 revert
continue:
}
함수 (#define fn)
함수는 실제 JUMP/JUMPDEST 쌍을 생성합니다. 호출당 약 22 가스의 추가 비용(JUMP 8 + JUMPDEST 1 + 스택 조작)으로 바이트코드 크기를 절약합니다. 바이트코드 크기가 가스보다 중요할 때만 사용하세요.
#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) 어노테이션은 문서화 및 컴파일러 힌트입니다. 독자 — 그리고 Huff 컴파일러의 스택 검사기 — 에게 블록이 소비하고 생성할 스택 항목 수를 알려줍니다.
#define macro ADD_TWO() = takes(2) returns(1) {
add // 2개 항목 소비, 1개 생성
}
실제 스택 동작이 어노테이션과 일치하지 않으면 huffc가 경고를 발생시킵니다. 이 어노테이션을 간이 타입 시스템으로 취급하세요 — 실수로 스택에 쓰레기를 남기거나 언더플로가 발생하는 것을 방지합니다.
비교: Huff vs Solidity 바이트코드
스토리지 슬롯을 반환하는 간단한 getValue() view 함수를 생각해 봅시다:
Solidity:
function getValue() external view returns (uint256) {
return value;
}
Solc는 디스패처 + ABI 인코딩에 약 40바이트를 생성합니다:
CALLDATASIZE → CALLDATALOAD → SHR 224 → DUP1 → PUSH4 selector
→ EQ → PUSH2 dest → JUMPI → ... → SLOAD → PUSH1 0x20
→ MSTORE → PUSH1 0x20 → PUSH1 0x00 → RETURN
Huff 등가물:
#define function getValue() view returns (uint256)
#define macro GET_VALUE() = takes(0) returns(0) {
[VALUE_SLOT] // [slot]
sload // [value]
0x00 mstore // [] — 메모리에 저장
0x20 0x00 return
}
Huff 버전은 본문에 12바이트의 바이트코드입니다. ABI 인코딩 오버헤드도, 프리 메모리 포인터도, 메타데이터 해시도 없습니다. 호출자를 제어할 때(예: 자체 컨트랙트를 호출하는 MEV 봇), Solidity 컴파일러가 필요하다고 가정하는 모든 것을 제거할 수 있습니다.
상수와 스토리지 슬롯
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는 범용 언어가 아닙니다. 다음 경우에 사용하세요:
- 가스가 주요 제약 조건일 때 — 100 가스가 수익성을 결정하는 MEV 컨트랙트.
- 바이트코드 크기가 중요할 때 — 다른 컨트랙트에 의해 배포되는 컨트랙트(CREATE2 팩토리)에서 더 작은 initcode = 더 적은 배포 가스.
- 커스텀 디스패치가 필요할 때 — 점프 테이블, 비트 패킹된 셀렉터, 또는 비표준 ABI 인코딩.
- EVM을 배우고 있을 때 — 원시 옵코드를 작성하는 것보다 EVM을 더 잘 가르치는 것은 없습니다.
그 외의 모든 것에 대해서는 Solidity를 작성하고 solc --asm으로 컴파일러 출력을 읽으세요. 더 생산적이고 오류가 적을 것입니다.
요약
Huff는 정신을 유지하기 위한 최소한의 추상화와 함께 EVM 바이트코드에 직접 연결을 제공합니다. 매크로는 제로 오버헤드 재사용을 위해 코드를 인라인합니다. 레이블은 점프 오프셋 관리를 처리합니다. takes/returns 어노테이션은 스택 오류를 조기에 포착합니다. 다음 기사에서는 스택 관리 — dup, swap의 기술과 스택의 멘탈 모델을 현실과 동기화하는 방법 — 를 심층적으로 다룹니다.