EIP-7709 BLOCKHASH 저장소 읽기와 비용 조정 (Read BLOCKHASH from Storage and Update Cost)

Hegota 포함 검토 (CFI) · EVM·실행 · 원문 상태 Draft · 2024-05-18 작성

BLOCKHASH 명령어가 최근 256개 블록의 해시를 EIP-2935 시스템 컨트랙트 저장소에서 읽도록 바꾸고 그만큼 저장소 읽기 비용을 추가로 받습니다. 돌려주는 값은 지금과 같습니다.

왜 필요한가

스마트 컨트랙트는 BLOCKHASH(블록 해시) 명령어로 최근 블록의 해시를 가져옵니다. 지금 이 명령어는 노드(클라이언트)가 최근 블록 이력을 따로 들고 있다고 가정합니다. 다른 데이터 읽기는 모두 이더리움 상태(state)에서 읽는 것으로 설명되는데, BLOCKHASH만 과거 체인 데이터에 기대는 예외 규칙으로 남아 있습니다. EIP-2935가 도입되면 최근 블록 해시가 시스템 컨트랙트의 저장소에 기록됩니다. 그러면 BLOCKHASH 조회도 일반 저장소 읽기와 똑같은 방식으로 다룰 수 있습니다. 비용도 실제로 쓰는 자원에 맞춰 저장소 읽기 비용을 받게 됩니다. 블록을 검증할 때 필요한 데이터 묶음인 증인(witness)을 만들 때도 BLOCKHASH만 따로 처리할 필요가 없어집니다. 이 EIP는 그 전환을 명세하면서 돌려주는 값은 그대로 두어 기존 컨트랙트가 같은 결과를 받도록 합니다.

무엇이 바뀌나

지금

EIP-7709 이후

누구에게 영향이 있나

심층 설명

상수와 두 가지 창의 크기

명세는 네 가지 값을 정합니다. FORK_TIMESTAMP는 TBD, HISTORY_STORAGE_ADDRESS는 0x0000F90827F1C53a10cb7A02335B175320002935, BLOCKHASH_SERVE_WINDOW는 256, HISTORY_SERVE_WINDOW는 8191입니다. 창은 두 개입니다. BLOCKHASH_SERVE_WINDOW(256)는 BLOCKHASH 명령어가 해시를 돌려주는 범위이고 HISTORY_SERVE_WINDOW(8191)는 EIP-2935 링 버퍼의 크기이자 슬롯 번호를 계산할 때 쓰는 나눗수입니다. 즉 돌려주는 범위는 256개로 제한하면서 슬롯은 arg % 8191로 구합니다. 원문은 두 값이 다르다는 점을 거듭 강조합니다. 2935 컨트랙트가 더 많은 해시를 저장하고 있어도 BLOCKHASH는 256개 밖이면 계속 0을 돌려줍니다.

resolve_blockhash 동작

적용 시점은 fork_block입니다. fork_block.timestamp >= FORK_TIMESTAMP이고 fork_block.parent.timestamp < FORK_TIMESTAMP인 블록입니다. 그때부터 BLOCKHASH는 resolve_blockhash(block, state, arg)로 해시를 구합니다. 먼저 arg >= block.number이거나 (arg + BLOCKHASH_SERVE_WINDOW) < block.number이면 0을 돌려줍니다. 범위 안이면 state.load_slot(HISTORY_STORAGE_ADDRESS, arg % HISTORY_SERVE_WINDOW)를 실행합니다. 이 load_slot은 단순 조회가 아니라 가스 부과, 슬롯 웜 처리, 상태 접근 기록까지 포함한 SLOAD입니다. 명령어의 의미(semantics)는 바뀌지 않으므로 같은 입력에 같은 해시를 돌려줍니다.

클라이언트가 고를 수 있는 세 가지 구현

arg가 범위 안일 때 클라이언트는 셋 중 하나를 택할 수 있습니다(MAY). 첫째, 상태에서 직접 SLOAD를 합니다. 둘째, EIP-2935 컨트랙트의 get 경로로 시스템 호출을 합니다(SYSTEM_ADDRESS가 아닌 호출자). 셋째, 풀 클라이언트처럼 필요한 이력을 보유하고 있다면 메모리나 기존 방식대로 돌려줍니다. 어떤 방식을 고르든 활성 포크가 정의한 SLOAD의 효과를 모두 적용해야 합니다(MUST). 구체적으로 arg % HISTORY_SERVE_WINDOW 슬롯에 대한 콜드·웜 SLOAD 가스, 슬롯을 웜 상태로 만드는 후속 효과, 활성 포크가 요구하는 상태 접근 기록입니다. 구현 자유는 주되 합의에 영향을 주는 가스와 상태 효과는 하나로 맞춘 구조입니다.

가스 비용과 시스템 컨트랙트 실행 비용 면제

범위 안 조회에는 arg % HISTORY_SERVE_WINDOW 슬롯에 대한 SLOAD 비용과 접근이 적용됩니다. 테스트 케이스에 따르면 BLOCKHASH 명령어 자체의 기본 비용은 그대로 받고 거기에 조회를 했다면 SLOAD 비용이 더해집니다. 슬롯이 콜드면 콜드 SLOAD 비용, 같은 트랜잭션에서 같은 슬롯을 다시 읽으면 웜 SLOAD 비용입니다. 반면 클라이언트가 EIP-2935 컨트랙트를 시스템 호출로 실행해 해시를 얻더라도 그 컨트랙트 코드 실행에 드는 가스는 받지 않습니다. 적용되는 것은 SLOAD 효과뿐입니다. 다만 BLOCKHASH를 거치지 않고 2935 컨트랙트를 직접 호출하면 일반 컨트랙트 실행으로 취급되어 가스, 상태 접근 기록 등이 모두 평소대로 적용됩니다.

설계 근거

원문이 드는 이유는 다섯 가지입니다. 첫째, 바뀐 가스 비용이 실제로 접근하는 자원인 저장소 읽기와 일치합니다. 둘째, 일반 SLOAD와 같은 콜드·웜 비용을 받으면 최근 블록 해시만을 위한 별도 가스 규칙을 만들지 않아도 됩니다. 셋째, 부모 헤더 체인으로 최근 해시를 증명하는 방법도 있지만 그러면 증인 구성에서 BLOCKHASH가 계속 특수 사례로 남습니다. 넷째, 항상 웜 비용만 받거나 BLOCKHASH 전용 가격을 두면 호환성 위험은 줄지만 새로운 특수 규칙이 생깁니다. 다섯째, 2935 컨트랙트 실행 비용을 빼는 것은 가스를 낮게 유지하고 직접 읽기나 메모리로 해석하는 클라이언트도 단순하게 구현하도록 하기 위해서입니다. BLOCKHASH가 256개만 돌려주는 것도 하위 호환을 지키고 이 면제 범위를 넓히지 않기 위해서입니다. 더 오래된 해시는 2935 컨트랙트를 직접 호출해 일반 실행 비용을 내고 가져와야 합니다.

활성화 조건과 하위 호환성

이 EIP는 EIP-2935가 먼저 켜져 있다고 가정합니다. 조건은 둘 중 하나입니다. 2935가 이 EIP보다 BLOCKHASH_SERVE_WINDOW(256블록) 이상 앞서 활성화되었거나, 테스트넷·데브넷처럼 제네시스에서 2935와 이 EIP를 함께 켜는 경우입니다. 2935 포크와 이 EIP 포크 사이가 256블록 미만이면 2935 컨트랙트에 필요한 이력이 다 쌓이지 않아 깨지는 변경이 됩니다. 돌려주는 값의 의미는 바뀌지 않지만 범위 안 BLOCKHASH 조회 비용이 크게 오르므로 예전 가스 비용에 기대는 용도는 깨질 수 있습니다. 보안 측면에서는 EIP-2935에 담긴 것 외에 새로 확인된 사항이 없다고 적혀 있습니다.

테스트 케이스

원문이 제시한 확인 항목입니다. 최근 256개 조상 밖이거나 현재 블록 번호 이상인 인자로 부르면 추가 저장소 접근 효과 없이 0을 돌려줍니다. 범위 안이고 슬롯이 콜드면 BLOCKHASH 기본 비용에 콜드 SLOAD 비용을 더해 받습니다. 같은 트랜잭션에서 같은 슬롯으로 이어지는 블록 번호를 다시 조회하면 뒤의 조회는 웜 SLOAD 비용입니다. 조회를 수행했다면 매 BLOCKHASH마다 명령어 비용과 SLOAD 비용이 함께 부과됩니다. 2935 컨트랙트를 직접 호출하면 현재 포크의 일반 컨트랙트 실행대로 가스와 상태 접근 기록이 적용됩니다. 2935보다 256블록 이상 뒤에 제대로 활성화됐다면 BLOCKHASH는 일관되게 해석되어야 합니다.

현재 상태와 코어 개발자 콜 결정

원문 상태는 Draft입니다. 상수 값이나 구현 요구 사항은 아직 바뀔 수 있으며 FORK_TIMESTAMP도 TBD입니다. 콜 기록을 보면 10.08 ACDE #247에서 EIP-7709를 Hegota 포크의 검토 대상(CFI)으로 올렸습니다. 네더마인드는 평가 등급을 A로 올렸고 BAL 기반 라이트 클라이언트 활용에 이득이 있다는 점이 근거로 언급되었습니다. 검토 대상은 포크 포함이 확정된 단계가 아니므로 이후 콜에서 결정이 달라질 수 있습니다.

EIP 원문 · ← EIPs