이더리움의 13가지 가스 모델 불일치
이 게시물은 이더리움 가스 모델의 13가지 불일치점을 요약합니다.
모든 심볼은 이더리움 옐로페이퍼(특히 부록 G. 수수료 일정)에서 나왔습니다.
1. 외부 거래는 계정을 생성할 때 신규 계정 수수료( G_{newaccount} G n e w a c c o u n t )가 발생하지 않지만 내부 거래는
예
아직 존재하지 않는 새로운 계정 A
로 이더리움(ETH) 이체하는 계약 C
필요하다고 가정해 보겠습니다.
-
C
직접 이체하는 경우 내부 신규 계정 경로는 25,000가스 ( G_{newaccount} G n e w a c c o u n t )를 청구합니다. - 대안: 먼저 EOA에서
A
로 1 Wei. 이 tx는 기본 가스 21,000 개비입니다. 그 후A
존재합니다. 이제C
가A
에게 이체할 때 더 이상 신규 계정 수수료가 발생하지 않습니다( 약 4,000개비 절약 ).
결론
계약에 따라 새 계정을 만드는 경우 25,000의 가스비를 지불합니다.
하지만 먼저 외부 거래(21,000가스)를 보낸 다음 계약을 통해 자금을 보내면 실제로 는 약 4,000가스 정도를 절약할 수 있습니다 .
원인
외부 및 내부 생성 경로는 서로 다른 청구 방식을 사용합니다. 명시적인 신규 계정 수수료를 트리거하는 것은 해당 계약을 호출하는 경우에만 가능합니다.
2. 사전 컴파일 호출은 때때로 트랜잭션 입력 바이트 수수료를 건너뜁니다.
예
미리 컴파일된 컨트랙트(예: ECRECOVER
)를 호출하면 트랜잭션 입력 바이트에 대한 요금 청구를 건너뛸 수 있습니다. 일반적으로 입력 바이트는 각각 4가스(0) 또는 16가스(0이 아님)의 비용이 발생합니다. 두 가지 실제 트랜잭션 사례를 살펴보겠습니다.
* 1. 24,276 가스를 사용하여 ECRECOVER
호출하는 트랜잭션 0x6b01 , 여기서 276 가스는 입력 바이트에 대한 것입니다.
* 2. 24,000 가스로 ECRECOVER
호출하는 트랜잭션 0x1fb0 , 여기서 0 가스는 입력 바이트에 대한 것입니다.
실행 클라이언트의 소스 코드를 살펴보면 두 번째 트랜잭션도 입력 바이트에 대한 요금을 부과하지 않고 ECRECOVER
실행할 수 있음을 알 수 있습니다. 클라이언트는 예상 크기에 맞춰 입력 데이터를 0으로 채웁니다.
추가 참고 사항
당신은 제가 언급한 두 가지 거래가 외부 거래라는 것을 알아차릴 만큼 똑똑하군요.
그렇다면 왜 미리 작성된 계약서를 적용하는 걸까?
그 이유 중 하나는 일부 탐색기가 사전 컴파일 주소(0x01–0x0A)를 "번 주소"로 잘못 표시하여 사용자를 더욱 혼란스럽게 하기 때문이라고 생각합니다(아래 스냅샷참조 ).
게다가 이러한 특수 주소(0x01–0x0A)에 사전 컴파일 주소를 배포하는 것은 실패한 설계입니다.
가끔 사람들은 이 특별한 주소로 직접 전화를 걸고 싶어합니다.
원인
사전 컴파일된 계약의 열악한 주소 설계와 블록 탐색기의 오해로 인해 혼란과 잘못된 라벨링이 발생합니다.
3. 액세스 목록 항목은 액세스되지 않은 경우에도 요금이 청구됩니다(예: G_{accesslistaddress} G a c c e s s l i s t a d d re s s , G_{accessliststorage} G a c c e s s l i s t s t o r a g e )
예
이더리움 개선 제안(EIP)-2930은 액세스 리스트를 도입하여 트랜잭션이 접근할 주소와 저장 슬롯을 지정할 수 있도록 합니다. 그러나 트랜잭션은 액세스 리스트에 주소와 슬롯을 포함할 수는 있지만, 실제로는 이를 건드리지 않을 수 있습니다.
예를 들어, 트랜잭션 0x0dd0c 는 액세스 목록을 설정하지만 주소로 인해 지정된 슬롯에 액세스하지 못합니다.
원인
이 프로토콜은 항목 사용 여부와 관계없이 실행을 간소화하기 위해 포함 에 대한 요금을 부과합니다. 사용자가 올바른 입력을 제공할 수 있다고 믿는다면, 테일러 스위프트를 당신의 아내로 믿는 것이나 마찬가지입니다.
4. 셀프 전송은 여전히 전송 가스를 충전합니다.
예
계정 A가 자기 자신에게 이더리움(ETH) 보냅니다.
잔액 변동은 없지만, 가치 이전을 위해 9,000가스 ( G_{callvalue} G call v a l l v a l u e )가 여전히 청구됩니다. @vbuterin 의 게시물 에 따르면,
두 개의 계정 쓰기(잔액 편집 CALL은 일반적으로 9000가스 비용이 듭니다)
왜 계정 하나 만드는 데 9,000 가스 가 드는 걸까요? 실제로 실행 클라이언트 소스를 읽어보면, 보낸 사람 주소와 받는 사람 주소가 같으면 클라이언트가 아무 작업도 하지 않는다는 것을 알 수 있습니다.
위의 경우는 거래가 자체 이체이거나 CALLCODE를 사용하여 가치를 이체하는 경우 발생할 수 있습니다.
원인
전송이 무작업인지 여부에 관계없이 실행 요금이 발생합니다.
5. Calldata와 Contract Bytecode 디스크 가격 불일치
예
- Tx calldata: 16가스/바이트(0이 아닌 경우) 또는 4가스/바이트(0) .
- 계약 바이트코드: 200 가스/바이트 .
둘 다 디스크를 사용하지만 가격이 일정하지 않습니다. tx calldata가 contract bytecode보다 실제 디스크 사용량과 네트워크 오버헤드를 고려해야 하기 때문에 더 저렴하다는 점이 매우 혼란스럽습니다.
원인
가스 일정은 실제 디스크 사용량에 맞춰 조정하지 않고 "호출 데이터"와 "코드 입금"을 분리합니다.
6. 되돌려진 거래는 디스크에 쓴 것처럼 청구됩니다.
예
되돌린 트랜잭션은 메모리의 상태를 수정하지만 변경 사항은 유지되지 않습니다. 그러나 쓰기에 대한 요금은 계속 적용되며, 다음과 같은 가스 요금이 영향을 받습니다.
- 25,000 가스 ( 신규 계정 , G_ { newaccount } G 새 계정 )
- 9,000 가스 ( 가치 전송 , G_ { callvalue } G call value )
- 2,100 가스 (콜드 슬롯 , G_ { coldslot } G 콜드 슬롯 )
- 200 가스 ( 코드 입금 , G_ { codedeposit } G 코드 입금 )
실제 메모리 전용 비용은 ~ 100가스 였을 것입니다.
원인
실행 중에 가스가 청구되며, 나중에 되돌리면 상태 변경은 취소되지만 수수료는 취소되지 않습니다. 구현 시 서비스 거부(DoS)를 방지하기 위해 보수적으로 요금을 청구합니다.
7. 단일 거래에서 여러 이더리움(ETH) 전송이 콜드체인으로 잘못 청구되었습니다.
예
단일 거래 내에서 계약이 이더리움(ETH) 여러 계정으로 보낸다고 가정해 보겠습니다.
- 첫 번째 전송은 G_{callvalue} G 호출 값 ( 9,000 가스 ) 을 올바르게 발생 시켜 계정 잔액에 기록합니다.
- 동일 거래에서 다른 계좌로 후속 이체를 하는 경우 웜 액세스 수수료( 100가스 + 4,500가스 )가 부과되지만, 때로는 여전히 콜드 액세스 수수료( 9,000가스 )로 청구됩니다.
원인
웜/콜드 액세스 회계는 단일 거래 내에서 여러 가치 이전에 대해 지속적으로 업데이트되지 않습니다.
8. 마이너/검증자 보상 또는 출금 쓰기는 무료입니다.
예
프로토콜 수준의 잔액 업데이트(예: 보상, 인출)는 디스크의 상태를 수정하지만 가스 비용은 0입니다 .
원인
시스템 수준의 회계는 가스 회계 절차를 우회합니다.
9. SSTORE의 첫 번째 디스크 읽기는 무료입니다( 이더리움 개선 제안(EIP)-2200에 따라)
예
SSTORE
명령어가 실행되면 먼저 디스크(컨트랙트 저장소)에서 현재 값을 읽은 후 새 값을 쓸지 여부를 결정합니다. 이더리움 개선 제안(EIP)-2200 에 따르면, 저장된 값이 기존 값과 일치하면 디스크 쓰기가 발생하지 않고 최소한의 가스 요금만 부과됩니다. 그러나 초기 디스크 읽기 자체에는 가스 요금이 부과되지 않습니다 . 프로토콜은 값이 변경되는 경우에만 후속 쓰기에 대한 가스 요금을 부과합니다.
원인
이더리움 개선 제안(EIP)-2200의 로직은 상태 변경에 대한 요금 부과에 초점을 맞추지만, 항상 먼저 발생하는 디스크 읽기에 대한 요금 부과는 생략합니다. 즉, 콜드 읽기(cold read)일지라도 스토리지 슬롯에 대한 첫 번째 접근은 무료입니다.
10. 스토리지 읽기 최적화로 I/O가 감소했지만 가스는 변경되지 않았습니다.
예
이더리움 클라이언트는 플랫 스토리지/ 스냅샷 최적화(예: Geth의 스냅샷 가속 구조 )를 채택했습니다. 이 최적화는 상태를 플랫 키-값 저장소로 구성하고 기존 Merkle-Patricia Trie(MPT)에 필요한 중간 노드를 우회하여 직접 디스크 읽기를 허용합니다. 이 최적화는 콜드 스토리지 읽기 시 디스크 I/O를 크게 줄입니다. 예를 들어, Geth와 다른 클라이언트는 이제 SAS 또는 유사한 구조를 사용하지만, 콜드 액세스에 대한 가스 요금 (2,600 / 2,100 / 2,400 / 1,900 가스) 은 변동이 없습니다.
원인
콜드 액세스용 가스 상수는 원래 MPT(Multiple Time Transfer)를 위해 보정되었는데, MPT에서는 여러 트라이 노드를 통과해야 하기 때문에 디스크 읽기 비용이 더 많이 들었습니다. SAS를 사용하면 실제 디스크 리소스 소비량이 훨씬 낮아지지만, 프로토콜은 해당 가스 요금을 업데이트하지 않았습니다.
완화
클라이언트가 SAS 또는 유사한 최적화된 스토리지 백엔드로 전환하면 감소된 디스크 I/O를 반영하여 가스 상수를 재보정합니다.
11. SLOAD 대 MLOAD 가격 불일치
예
-
SLOAD
(따뜻함) → 100가스 -
MLOAD
→ 3가스
둘 다 메모리 읽기 방식이지만, 가격은 크게 다릅니다.
원인
상태 작업과 메모리 작업 간의 기존 구분; 최적화로 인해 실제 비용 격차가 모호해졌습니다.
12. 내부 거래는 때때로 가스 없이 계정을 업데이트합니다.
예
디스크에서 계정 업데이트가 가스 요금 없이 발생하는 경우입니다. 특히, 이 문제는 사용자가 외부 트랜잭션을 계약 A로 전송하고, 계약 A가 계약 B를 내부적으로 호출하는 상황에서 발생합니다. 계약 B가 스토리지의 슬롯을 수정하면 계약 B 계정의 해당 스토리지 루트가 디스크에서 업데이트되어야 합니다. 그러나 이 계정 B 업데이트에는 가스 요금이 부과되지 않아 불일치가 발생합니다.
원인
이 버그는 계약 B의 스토리지 트리 수정 시 계정 상태 업데이트에 추가 가스 요금이 발생하지 않기 때문에 발생합니다. 이는 디스크 쓰기가 수행됨에도 불구하고 프로토콜이 내부 트랜잭션으로 인해 발생하는 계정 상태 업데이트에 요금을 부과하지 않기 때문입니다.
13. EXT* 명령어 가격이 너무 비싸다
예
EXTCODESIZE
BALANCE
보다 더 많은 데이터를 읽을 수 있지만, 둘 다 동일한 콜드 계정 수수료( 2,600가스 )가 부과됩니다.
원인
명령어 가격 책정 버킷은 거칠고 가변적인 작업을 무시합니다.
마무리 노트
이 문제는 저의 논문에서 다음과 같이 나왔으며, 이 링크(Chainlink) 로 공유합니다.
He, Z., Li, Z., Luo, J., Luo, F., Duan, J., Li, J., … & Zhang, X. (2025년 2월). Auspex: 블록체인 거래 수수료 메커니즘의 불일치 버그 발견. 제23회 USENIX 파일 및 저장 기술 컨퍼런스 논문집.
제 논문을 인용해 주시면 감사하겠습니다.
이 모든 것은 이더리움 프로토콜 내 가스 가격 책정 메커니즘에 대한 포괄적인 검토 및 조정의 필요성을 강조합니다. 이러한 불일치를 해결함으로써 다양한 작업의 기본 자원 비용을 정확하게 반영하는 더욱 효율적이고 공정한 가스 시장을 확보할 수 있습니다.