EIP-1884 상태 크기 의존 명령어 가스 재조정 (Repricing for trie-size-dependent opcodes)
최종 확정 · 수수료·가스 · 원문 상태 Final · 2019-03-28 작성
상태를 읽는 SLOAD, BALANCE, EXTCODEHASH 명령어의 가스 비용을 올리고 자기 잔액을 5 가스에 읽는 새 명령어 SELFBALANCE를 추가합니다.
왜 필요한가
이더리움의 상태(state)가 커지면서 상태를 읽는 일부 명령어는 가격이 매겨졌을 때보다 실제로 훨씬 많은 자원(CPU 시간, 메모리 등)을 쓰게 됐습니다. 가격과 자원 소모가 어긋나면 두 가지 문제가 생깁니다. 첫째, 공격자가 저평가된 명령어로 블록을 가득 채워 블록 처리 시간을 과도하게 늘릴 수 있습니다. 둘째, 같은 가스를 쓴 블록인데 어떤 블록은 빨리 끝나고 어떤 블록은 느리게 끝나는 식으로 처리 시간이 들쭉날쭉해집니다. 실제로 Geth로 전체 블록을 동기화하며 측정한 결과, 저장소 읽기(storage_reads)와 계정 읽기(account_reads)가 블록 처리 시간에서 가장 큰 비중을 차지했습니다. 명령어 가격이 자원 소모와 균형을 이루면 블록 가스 한도를 최대로 쓰면서도 처리 시간을 안정적으로 유지할 수 있습니다.
무엇이 바뀌나
지금
- SLOAD(저장소 읽기)는 200 가스입니다
- BALANCE(잔액 조회)와 EXTCODEHASH(코드 해시 조회)는 각 400 가스입니다
- 컨트랙트가 자기 자신의 잔액을 볼 때도 BALANCE를 써서 똑같이 400 가스를 냅니다
- 실제 처리 비용보다 싸게 매겨져 블록 처리 시간이 불안정하고 공격에 쓰일 여지가 있습니다
EIP-1884 이후
- SLOAD는 800 가스로 오릅니다
- BALANCE와 EXTCODEHASH는 각 700 가스로 오릅니다
- 새 명령어 SELFBALANCE(0x47)로 자기 잔액을 5 가스에 읽을 수 있습니다
- 명령어 가격이 자원 소모와 균형을 이뤄 블록 처리 시간이 더 안정됩니다
누구에게 영향이 있나
- 컨트랙트 개발자: 상태를 읽는 호출이 비싸집니다. 같은 값을 여러 번 SLOAD하는 패턴이나 불필요한 값을 로그에 남기는 패턴은 줄이는 쪽으로 바뀔 수 있습니다.
- 기존 배포 컨트랙트: 가스 비용이 고정이라고 가정한 컨트랙트, 특히 이더를 받을 때 2,300 가스 안에서 저장소를 읽던 코드가 작동을 멈출 수 있습니다.
- 클라이언트 팀: 하드포크로 가격을 바꿔야 합니다. 두 명령어 모두 과거에 재조정된 적이 있어 가격 변경을 다루는 내부 구조는 이미 갖춰져 있습니다.
- 사용자: 상태 읽기가 많은 트랜잭션의 가스 사용량이 늘어납니다.
심층 설명
측정 근거: Geth 풀 싱크에서 SLOAD 실행 시간이 다시 올랐습니다
Geth 풀 싱크에서 10K 블록 단위로 각 opcode의 실행 시간을 집계했습니다. 500만~700만 블록 구간에서 SLOAD가 무거운 opcode 상위권으로 올라왔고 블록 처리 시간 전체에서는 storage_reads와 account_reads가 가장 큰 두 요인이었습니다. SLOAD는 EIP-150에서 50에서 200으로 인상돼 실행 시간 지표가 약 67에서 23으로 떨어졌지만 블록 500만 부근에서 EIP-150 이전 수준으로 되돌아갔고 블록 700만에서는 평균 약 150으로 인상 전의 두 배를 넘었습니다. 비용을 4배(200에서 800)로 올리면 약 40 수준으로 내려갈 것으로 보지만 상태 정리(state clearing)가 이뤄지지 않으면 다시 올라 추가 재조정이 필요할 수 있다고 원문은 밝힙니다. GASPRICE(0x3a)도 상위에 있지만 이는 클라이언트 내부 최적화로 없앨 수 있는 반면 SLOAD와 BALANCE는 그렇지 않습니다.
BALANCE와 EXTCODEHASH는 700 가스로 맞춥니다
BALANCE는 상태 트라이에서 데이터를 가져오는 연산으로, EIP-150에서 20에서 400으로 인상됐습니다. 같은 계정 조회인 EXTCODESIZE와 EXTCODEHASH 중 EXTCODESIZE는 이미 700이므로 이에 맞춰 700으로 올립니다. EXTCODEHASH는 콘스탄티노플에서 EIP-1052로 도입될 때 'BALANCE와 같은 계정 조회가 필요하다'는 이유로 400으로 매겨졌으므로, BALANCE를 올리면 EXTCODEHASH도 함께 올려야 합니다. 한편 BALANCE는 자기 자신(this)의 잔액 조회처럼 본질적으로 싼 용도와, 임의 계정 조회처럼 트라이(디스크) 접근이 필요한 용도가 섞여 있어 비용 편차가 큽니다. 원문은 돌이켜 보면 EXTBALANCE(address)와 SELFBALANCE 두 opcode로 나눠 다른 가격을 매기는 편이 나았을 것이라고 적습니다.
새 명령어 SELFBALANCE(0x47) 명세
SELFBALANCE는 스택에서 인자를 0개 꺼내고 현재 주소의 balance를 스택에 올립니다. 가격은 GasFastStep인 5 가스입니다. 비슷한 조회 명령어들이 쓰는 GasQuickStep(2 가스)이 아닌 5 가스인 이유는, EVM 실행 엔진이 캐시된 트라이를 한 번 조회해야 하고 balance가 gasPrice나 timeStamp와 달리 실행 중에 바뀔 수 있어 고유한 오버헤드가 더 있기 때문입니다. opcode 번호가 0x4X 대역인 이유는 잔액 조회류가 모여 있는 0x3X 대역이 이미 가득 찼기 때문입니다. SELFBALANCE를 '주소를 스택에서 꺼내는 대신 자기 주소를 쓰는 BALANCE'로 정의하면 BALANCE가 이미 잘 정의돼 있으므로 특별한 엣지 케이스는 없습니다.
하위 호환성: 하드포크가 필요하고 일부 컨트랙트가 깨질 수 있습니다
이 변경은 하드포크를 요구하며 세 가지 결과를 낳습니다. 첫째, 특정 호출이 더 비싸집니다. 둘째, 호출 시 항상 보장되는 최소 가스인 2,300 가스 안에서 저장소에 접근하던 기본 수신 함수(default function)가 가스 부족을 겪을 수 있습니다. 셋째, 호출이나 내부 구간의 가스 비용이 고정이라고 가정한 컨트랙트는 작동을 멈출 수 있습니다. ERC-165의 supportsInterface는 bool을 반환하며 최대 30,000 가스를 쓰도록 고정돼 있고 영향받는 opcode를 사용하지만 작성 시점의 두 예시 구현은 각각 586 가스(입력 무관)와 236 가스(지원 인터페이스 수에 비례 증가)여서 한도를 넘으려면 두 번째 방식으로 서른 개 안팎의 인터페이스를 지원해야 하므로 가능성이 낮습니다. 이 연산들은 이미 과거에 재조정된 전례가 있어 고정 가스 가정 자체를 피했어야 한다는 점도 지적합니다.
보안 고려: 이더 수신 후 로그를 남기는 패턴이 실패할 수 있습니다
CALL로 이더를 받은 쪽은 흔히 LOG를 남기려 합니다. LOG는 375 가스에 토픽당 375 가스가 추가됩니다. 여기에 SLOAD(800 가스)까지 하면 2,300 가스를 넘을 수 있어 일부 이더 전송이 실패할 수 있습니다. 원문은 또 Solidity에 이 연산들의 가스 비용을 하드코딩한 기대가 있는지 조사해야 한다고 적습니다. 개발 패턴 면에서는 같은 값을 SLOAD하는 modifier 여러 개를 하나로 합치거나, 꼭 필요하지 않은 SLOAD 값을 로그에 담지 않는 쪽으로 바뀔 것으로 원문 저자는 예상합니다.
상태와 테스트: Final이며 이스탄불 전에는 SELFBALANCE가 무효입니다
이 EIP의 상태는 Final입니다. 원문이 요구하는 테스트는 다섯 가지입니다. selfbalance가 balance(address)와 같은 값인지, balance(this)의 비용이 종전 방식대로 부과되는지, selfbalance가 스택에서 값을 꺼내지 않는지, SLOAD·EXTCODEHASH·SELFBALANCE의 가스 비용이 명세대로인지, 그리고 이스탄불(Istanbul) 이전 블록에서 SELFBALANCE가 무효 명령어인지입니다. 일부 테스트는 상태 테스트 형태로 github.com/holiman/IstanbulTests에 구현돼 있습니다. 원문에는 go-ethereum용 SELFBALANCE 구현 예시 코드도 실려 있습니다.