EIP-4788 EVM 속 비콘 블록 루트 (Beacon block root in the EVM)
최종 확정 · EVM·실행 · 원문 상태 Final · 2022-02-10 작성
실행 블록 헤더마다 부모 비콘 블록의 해시 트리 루트를 넣고 이 루트들을 전용 스마트 컨트랙트에 저장해 EVM 안에서 합의 계층 정보를 검증할 수 있게 합니다.
왜 필요한가
이더리움은 합의를 맡는 비콘 체인(beacon chain)과 트랜잭션을 실행하는 실행 계층으로 나뉘어 있습니다. 그런데 EVM에서 도는 스마트 컨트랙트는 비콘 체인에서 무슨 일이 일어나는지 직접 확인할 길이 없었습니다. 그래서 스테이킹 풀이나 브리지 같은 서비스는 합의 계층 정보를 외부 오라클이나 운영자의 보고에 의존해야 했고 그만큼 믿어야 할 대상이 늘었습니다. 비콘 블록의 루트는 임의의 합의 상태를 증명할 수 있는 암호학적 누산기(accumulator)입니다. 이 루트를 EVM 안에 넣어 주면 컨트랙트가 합의 계층 데이터를 신뢰를 최소화한 방식으로 검증할 수 있습니다. 원문은 스테이킹 풀, 리스테이킹, 스마트 컨트랙트 브리지, MEV 완화 등의 신뢰 가정을 개선한다고 설명합니다.
무엇이 바뀌나
지금
- 컨트랙트가 비콘 체인 정보를 알려면 외부 오라클이나 운영자를 믿어야 합니다
- 실행 블록 헤더에는 합의 계층을 가리키는 값이 없습니다
- 스테이킹 풀과 브리지가 합의 데이터를 스스로 검증하지 못합니다
EIP-4788 이후
- 모든 실행 블록 헤더에 부모 비콘 블록 루트(parent_beacon_block_root)가 들어갑니다
- 전용 컨트랙트에서 약 하루치(8191개)의 루트를 조회할 수 있습니다
- 컨트랙트가 증명을 통해 합의 계층 상태를 직접 검증할 수 있습니다
누구에게 영향이 있나
- 지갑·앱 개발자: 스테이킹 풀, 리스테이킹, 브리지 컨트랙트가 외부 오라클 없이 비콘 체인 상태를 검증하는 설계를 만들 수 있습니다.
- 클라이언트 팀: 블록 헤더에 새 필드를 추가하고 블록 처리를 시작할 때 비콘 루트 컨트랙트를 호출하는 시스템 동작을 구현해야 합니다.
- 사용자: 자산을 맡기는 스테이킹·브리지 서비스가 외부를 덜 믿는 구조로 바뀌어 간접적으로 안전성이 높아집니다.
심층 설명
헤더 스키마 확장
실행 타임스탬프 FORK_TIMESTAMP(1710338135)부터 실행 클라이언트는 헤더에 parent_beacon_block_root 필드를 추가해야 합니다. 이 값은 32바이트이며 해당 실행 블록의 부모 비콘 블록을 SSZ 머클화한 해시 트리 루트입니다. RLP 인코딩에서는 excess_blob_gas 다음의 마지막 필드로 들어갑니다. 값의 유효성은 출금 처리와 마찬가지로 합의 계층이 보장하며 블록을 검증할 때 실행 클라이언트는 헤더의 루트가 합의 클라이언트가 제공한 값과 일치하는지 확인해야 합니다. 부모 비콘 블록 루트가 없는 제네시스 블록에서는 32바이트 0 값을 자리 표시자로 씁니다.
비콘 루트 컨트랙트의 get과 set
BEACON_ROOTS_ADDRESS(0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02)의 컨트랙트는 get과 set 두 동작만 합니다. 어느 동작을 할지는 입력이 아니라 caller로 정합니다. caller가 SYSTEM_ADDRESS(0xfffffffffffffffffffffffffffffffffffffffe)면 set, 아니면 get입니다. get은 조회할 timestamp를 32바이트 빅엔디언으로 받고 입력이 정확히 32바이트가 아니거나 0이면 revert합니다. timestamp % HISTORY_BUFFER_LENGTH 슬롯에 저장된 timestamp와 입력이 다르면 revert하고 일치하면 timestamp % HISTORY_BUFFER_LENGTH + HISTORY_BUFFER_LENGTH 슬롯의 비콘 루트를 돌려줍니다. set은 header.timestamp % HISTORY_BUFFER_LENGTH 슬롯에 header.timestamp를 쓰고 거기에 HISTORY_BUFFER_LENGTH를 더한 슬롯에 calldata[0:32]의 루트를 씁니다.
블록 처리 시 시스템 호출
block.timestamp >= FORK_TIMESTAMP인 모든 실행 블록은 트랜잭션을 처리하기 전에 SYSTEM_ADDRESS로서 BEACON_ROOTS_ADDRESS를 호출합니다. 입력은 header.parent_beacon_block_root 32바이트, 가스 한도는 30_000_000, value는 0입니다. 시스템 동작이므로 호출은 끝까지 실행되어야 하고 블록 가스 한도에 포함되지 않으며 EIP-1559의 소각 규칙을 따르지 않아 값이 이전되지 않습니다. BEACON_ROOTS_ADDRESS에 코드가 없으면 조용히 실패해야 합니다. 클라이언트는 명시적 EVM 호출을 생략하고 스토리지 값을 직접 쓸 수도 있는데, 메인넷에서는 유효한 최적화지만 다른 컨트랙트를 쓰는 비메인넷 환경에서는 문제가 될 수 있다고 원문은 적습니다. 제네시스 블록에서 활성화되는 경우 헤더의 parent_beacon_block_root는 0x0이어야 하고 시스템 트랜잭션은 일어나지 않습니다.
합성 주소로 배포
컨트랙트는 보통의 컨트랙트처럼 배포하되, 원하는 배포 트랜잭션에서 거꾸로 계산한 합성(synthetic) 주소를 씁니다. 공개된 배포 트랜잭션에서 보낸 주소를 계산하면 0x0B799C86a49DEeb90402691F1041aa3AF2d3C875이고 이 계정이 처음 배포하는 컨트랙트의 주소 rlp([sender, 0])이 곧 BEACON_ROOTS_ADDRESS입니다. 트랜잭션의 input에는 런타임 코드 앞에 간단한 생성자가 붙어 있습니다. create2처럼 특정 initcode에 묶이는 생성 방식은 아니지만 합성 주소가 트랜잭션 입력 데이터에 암호학적으로 묶인다고 원문은 설명합니다.
링 버퍼가 두 개인 이유
루트만 담는 링 버퍼 하나로는 공격이 가능합니다. 슬롯이 건너뛰어진 상황에서 버퍼 길이로 나눈 나머지가 같은 timestamp가 들어오면 최신 루트가 아니라 옛 루트를 돌려줄 수 있기 때문입니다. 이를 막으면서 고정된 저장 공간을 유지하려고, 각 인덱스에 (parent_beacon_block_root, timestamp) 쌍을 저장하고 읽을 때 루트를 기록할 당시의 timestamp와 일치하는지 검증합니다. 스토리지 슬롯이 32바이트로 고정되어 있어 값 쌍을 담으려면 링 버퍼가 하나가 아니라 두 개 필요합니다.
버퍼 크기 8191을 고른 이유
링 버퍼는 합의 계층 루트 8191개를 담도록 크기가 정해졌습니다. 소수를 쓰면 버퍼 전체가 가득 차기 전에는 어떤 값도 덮어써지지 않고 그 뒤로는 한 바퀴마다 각 값이 한 번씩 갱신됩니다. 슬롯 시간이 바뀌더라도 쓰이는 스토리지는 최대 8191 슬롯입니다. 현재 메인넷 값 기준으로 8191개 루트는 약 하루 분량입니다. 특정 루트에 대한 검증을 담은 트랜잭션을 만들어 체인에 포함시키기에 충분한 시간이라고 원문은 설명합니다.
BLOCKHASH 재활용과 상태 루트를 택하지 않은 이유
BLOCKHASH 명령어가 실행 블록 해시 대신 비콘 루트를 돌려주도록 고칠 수도 있었지만 코드 변경을 줄이고 기존 컨트랙트를 깨지 않으며 메인넷 배포를 단순하게 하려고 BLOCKHASH는 그대로 두고 새 기능을 더했습니다. 또 상태 루트가 아니라 블록 루트를 씁니다. 상태 루트를 쓰면 건너뛴 슬롯이 생길 때마다 선형으로 늘어나는 작업이 필요하지만 블록 루트는 새 블록마다 일정한 작업만 하면 됩니다. 블록 루트를 쓰면 증명에 노드 몇 개가 더 필요하지만 이 비용은 무시할 수준이고 슬롯별 증명을 캐시하는 싱글톤 상태 루트 컨트랙트처럼 소비자 전체가 비용을 나눌 수도 있다고 원문은 적습니다.