EIP-2935 과거 블록 해시를 상태에 저장 (Serve historical block hashes from state)

최종 확정 · EVM·실행 · 원문 상태 Final · 2020-09-03 작성

최근 8191개 블록의 해시를 시스템 컨트랙트의 저장소에 링 버퍼로 보관합니다. 블록 해시가 상태 안에 들어가므로 상태만 가진 클라이언트도 과거 해시를 조회하고 증명할 수 있게 됩니다.

왜 필요한가

지금의 이더리움 가상머신(EVM)은 클라이언트가 최근 블록 해시를 항상 직접 들고 있다고 가정합니다. 그런데 앞으로 등장할 무상태 클라이언트(stateless client)는 전체 상태를 보관하지 않고 블록마다 전달받는 증인(witness) 자료만으로 검증하므로, 이 가정이 더 이상 성립하지 않습니다. 블록 해시를 상태 안에 넣어 두면 증인에 함께 담아 전달할 수 있어 이 문제가 풀립니다. 또 BLOCKHASH 명령이 조회 가능한 범위(최근 256개)를 직접 늘리면 기존 동작의 의미가 바뀌는 부담이 있는데, 별도 컨트랙트 저장소를 두면 기존 동작은 그대로 둔 채 부드럽게 더 긴 범위를 제공할 수 있습니다. 롤업도 이 컨트랙트를 직접 조회해 더 긴 해시 기록을 활용할 수 있고, 최근 8191개 조상 블록에 대한 증명을 현재 상태에 대고 바로 만들거나 검증하는 것이 가능하다는 부수 이점도 있습니다.

무엇이 바뀌나

지금

EIP-2935 이후

누구에게 영향이 있나

심층 설명

블록 처리 시 시스템 호출

EIP가 활성화된 모든 블록은 트랜잭션 처리 전에 SYSTEM_ADDRESS(0xfffffffffffffffffffffffffffffffffffffffe)가 HISTORY_STORAGE_ADDRESS(0x0000F90827F1C53a10cb7A02335B175320002935)를 호출합니다. 입력은 block.parent.hash 32바이트, 가스 한도는 30_000_000, 값은 0입니다. EIP-4788과 같은 시스템 연산 규약을 따르므로 호출은 반드시 끝까지 실행되어야 하고 블록 가스 한도에 포함되지 않으며 EIP-1559의 소각 규칙을 따르지 않습니다. 해당 주소에 코드가 없으면 조용히 실패해야 합니다. 클라이언트가 EVM 호출 대신 상태에 직접 쓰는 방법도 허용되지만 Verkle 포크 전까지는 EVM 호출 방식이 권장됩니다. 컨트랙트가 배포되지 않은 채 EIP만 활성화된 잘못 설정된 네트워크 문제를 피하기 위해서입니다.

히스토리 컨트랙트의 get과 set

컨트랙트에는 두 동작만 있습니다. caller가 SYSTEM_ADDRESS와 같을 때만 set이 실행되고 그 외에는 모두 get입니다. get은 조회할 블록 번호를 빅엔디언 인코딩 32바이트로 받습니다. calldata가 32바이트가 아니면 revert하고 조회 범위 [block.number-HISTORY_SERVE_WINDOW, block.number-1]를 벗어나도 revert합니다. set은 calldata로 받은 block.parent.hash를 block.number-1 % HISTORY_SERVE_WINDOW 슬롯에 저장합니다. 원문에는 이 로직을 구현한 정확한 EVM 어셈블리 바이트코드가 함께 명세되어 있습니다.

링 버퍼 크기 8191의 근거

저장 구조는 길이 HISTORY_SERVE_WINDOW(8191)의 링 버퍼입니다. 다른 시스템 컨트랙트에서는 슬롯이 비거나 슬롯 시간이 바뀌어도 버퍼 전체가 채워지기 전에는 값이 덮어쓰이지 않도록 소수(prime) 크기를 씁니다. 이 EIP에서는 나머지 연산의 피연산자가 블록 번호이고 매번 정확히 1씩만 늘어나므로 버퍼가 항상 포화 상태로 유지되어 소수일 필요는 없지만 다른 시스템 컨트랙트와의 일관성을 위해 8191을 유지했습니다. 현재 메인넷 기준으로 8191개면 약 하루 분량이라, 특정 해시에 대한 검증을 담은 트랜잭션을 만들어 체인에 포함시키기에 충분한 시간을 줍니다.

합성 주소를 통한 배포

컨트랙트는 원하는 배포 트랜잭션에서 거꾸로 계산한 특수한 합성 주소로 배포됩니다. 명세된 배포 트랜잭션의 서명 값에서 보낸 사람 주소 0x3462413Af4609098e1E27A490f554f260213D685이 계산되고 이 계정이 처음 배포하는 컨트랙트 주소 rlp([sender, 0])이 곧 HISTORY_STORAGE_ADDRESS가 됩니다. create2처럼 특정 initcode에 묶이는 방식은 아니지만 합성 주소가 트랜잭션 입력 데이터에 암호학적으로 묶여 있습니다. 배포된 계정은 EIP-4788과 같은 방식으로 코드와 nonce 1을 가지므로 EIP-161의 빈 계정 정리 대상에서 제외됩니다.

가스 비용과 BLOCKHASH 불변

블록 시작 시의 시스템 업데이트는 EIP-2929 규칙상 HISTORY_STORAGE_ADDRESS 계정이나 그 저장 슬롯을 웜(warm) 상태로 만들지 않습니다. 따라서 그 블록에서 이 컨트랙트를 처음 호출하는 쪽이 계정과 슬롯의 웜업 비용을 냅니다. 이 컨트랙트에 대한 호출은 그 외에는 일반 EVM 실행 규칙을 그대로 따릅니다. 한편 BLOCKHASH 옵코드의 의미는 바뀌지 않으므로 BLOCKHASH_SERVE_WINDOW(256)와 그 비용에는 아무 영향이 없습니다. 더 긴 범위가 필요하면 BLOCKHASH 대신 이 컨트랙트를 직접 조회하면 됩니다.

활성화 후 버퍼가 채워지는 방식

포크 블록에서 과거 8191개 해시를 한꺼번에 저장하는 대신, 활성화 이후 블록마다 하나씩 쌓는 방식을 택했습니다. 컨트랙트에는 포크 블록의 부모 해시부터만 담기고 그 이전 해시는 없으며 버퍼가 완전히 차는 데 HISTORY_SERVE_WINDOW개 블록(약 하루)이 걸립니다. 로직이 크게 단순해지고 이것이 새로운 히스토리 접근 방식이라 아직 의존하는 컨트랙트가 없다는 점에서 유리한 절충으로 판단했습니다. 제네시스에서 활성화되면 블록 1 시작 때 제네시스 해시가 슬롯 0에 쓰입니다. 블록 32에서 활성화되면 블록 31의 해시가 슬롯 31에 쓰이며 나머지 슬롯은 0입니다.

보안 고려사항과 호환성

자주 갱신되는 저장 경로를 가진 컨트랙트는 공격자가 그 경로(브랜치) 주변에 소액의 이더를 뿌려 상태 루트 갱신을 늦추려는 브랜치 오염(branch poisoning) 공격의 대상이 될 수 있습니다. 다만 의미 있는 속도 저하를 일으키려면 공격 비용이 크게 치솟는다고 평가되었습니다. 이 EIP는 블록 검증 규칙을 하위 호환되지 않게 바꾸지만 현재 사용자의 활동이나 경험에서 깨지는 것은 없습니다. 이 EIP의 상태는 Final로 확정되어 있습니다.

EIP 원문 · ← EIPs