EIP-7002 실행 레이어 트리거 출금 (Execution layer triggerable withdrawals)

최종 확정 · 합의·스테이킹 · 원문 상태 Final · 2023-05-09 작성

검증자의 출금 자격증명(0x01 실행 레이어 주소) 소유자가 실행 레이어에서 직접 검증자 종료와 부분 출금을 요청할 수 있게 합니다. 요청은 전용 컨트랙트의 큐에 쌓였다가 블록에 담겨 합의 레이어로 전달됩니다.

왜 필요한가

검증자는 키를 두 개 씁니다. 하나는 매일 서명하며 검증 업무를 수행하는 활성 키이고, 다른 하나는 예치한 ETH의 소유권을 갖는 출금 자격증명(withdrawal credential)입니다. 그런데 기존 규칙에서는 활성 키만 검증자 종료를 시작할 수 있었습니다. 활성 키를 다른 업체에 맡기는 위탁 운영에서는 정작 자금의 주인인 출금 자격증명 소유자가 스스로 종료를 결정할 수 없어 활성 키 운영자가 ETH를 사실상 인질로 잡을 수 있는 신뢰 문제가 생깁니다. 사전에 서명해 둔 종료 메시지 같은 우회책은 불완전하고 활성 키를 분실하면 자금을 되찾을 길도 막힙니다. 이 EIP는 출금 자격증명이 EOA든 스마트 컨트랙트든 신뢰 없이 예치금의 운명을 결정하도록, 0x01 자격증명으로 종료와 출금을 직접 걸 수 있게 합니다.

무엇이 바뀌나

지금

EIP-7002 이후

누구에게 영향이 있나

심층 설명

EIP-7685 요청 타입 0x01과 필드 구성

출금 요청은 EIP-7685 범용 요청 체계의 타입 0x01로 정의됩니다. 필드는 세 개로, source_address(Bytes20), validator_pubkey(Bytes48), amount(uint64)입니다. 컨트랙트는 amount를 리틀엔디언으로 반환하며 인코딩도 그대로 리틀엔디언을 씁니다. 요청을 받는 컨트랙트 호출 입력에서는 반대로 공개키 48바이트 뒤에 빅엔디언 uint64 금액을 붙인 56바이트를 받습니다.

프리디플로이 컨트랙트의 세 가지 코드 경로

WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS(0x00000961Ef480Eb55e80D19ad83579A64c007002)에 배포되는 컨트랙트는 입력에 따라 세 갈래로 동작합니다. 입력이 정확히 56바이트면 수수료 충족을 확인(check_fee)하고 블록 내 요청 수를 1 올린 뒤(increment_count) msg.sender와 공개키, 금액을 큐에 넣습니다(insert_withdrawal_request_into_queue). 입력 길이가 0이면 현재 수수료(get_fee 결과)를 반환합니다. SYSTEM_ADDRESS가 호출하면 이번 블록 몫의 요청을 큐에서 꺼냅니다. 큐는 스토리지 슬롯 2(head)·3(tail) 포인터와 슬롯 4부터 요청당 3슬롯을 쓰는 배열로 구성됩니다.

EIP-1559식 수수료로 하는 속도 제한

가스만으로는 공격자가 시장가 수수료를 내고 종료 한도를 값싸게 채워 정직한 검증자를 방해할 수 있습니다. 이를 막기 위해 요청 자체에 동적 수수료를 붙입니다. 수수료는 fee = MIN_WITHDRAWAL_REQUEST_FEE * e**(excess / WITHDRAWAL_REQUEST_FEE_UPDATE_FRACTION)을 근사하는 fake_exponential로 계산하며, MIN_WITHDRAWAL_REQUEST_FEE는 1, WITHDRAWAL_REQUEST_FEE_UPDATE_FRACTION은 17입니다. 블록당 TARGET_WITHDRAWAL_REQUESTS_PER_BLOCK(2)을 넘긴 초과분만큼 excess가 늘어 수수료가 지수적으로 오르고 사용이 줄면 다시 내려갑니다. 블록당 최대 변화율은 약 1.125배입니다. 첫 시스템 호출 전에는 excess 슬롯에 EXCESS_INHIBITOR(2**256-1)가 들어 있어 get_fee가 실패하도록 막아 둡니다. 비콘 체인 증명을 쓰는 대안(방법 a)은 EIP-4788 의존과 레이어 간 복잡도 때문에 채택하지 않았습니다.

메시지 큐와 블록당 16건 한도

한 블록이 합의 레이어로 넘길 수 있는 요청은 MAX_WITHDRAWAL_REQUESTS_PER_BLOCK(16)으로 제한됩니다. 16은 비콘 체인의 VoluntaryExit, Deposit 같은 유사 연산의 한도에 맞춘 값입니다. 실행 레이어 가스 한도상 한 블록에서 훨씬 많은 호출이 들어올 수 있으므로, 한도 초과 호출을 실패시키는 대신 상태 내 큐에 쌓아 다음 블록들로 넘깁니다. 호출 실패로 UX가 나빠지는 대안 설계보다 큐 유지 비용이 낮다고 판단했습니다. TARGET을 2로 잡은 이유는 종료 처리량이 극단적으로 몰리는 상황에서도 비콘 상태의 부분 출금 큐가 무시할 만한 수준으로만 자라게 하기 위해서입니다.

블록 끝 시스템 호출 규칙

FORK_BLOCK부터 모든 블록은 트랜잭션 처리를 마친 뒤 SYSTEM_ADDRESS(0xfffffffffffffffffffffffffffffffffffffffe)로 프리디플로이 컨트랙트를 콜데이터 없이 호출합니다. 이 호출은 큐에서 요청을 꺼내고(dequeue_withdrawal_requests), excess를 갱신하고(update_excess_withdrawal_requests), 블록 카운트를 0으로 되돌립니다(reset_withdrawal_requests_count). 호출에는 전용 가스 한도 30_000_000이 주어지고 소비 가스는 블록 가스 사용량에 포함되지 않으며 블록 가스 한도 검사에서도 제외됩니다. EIP-1559 수수료 소각도 적용되지 않습니다. 해당 주소에 코드가 없거나 호출이 실패하면 블록은 무효입니다. 꺼낸 요청은 dequeue가 반환한 순서 그대로 EIP-7685 요청 목록에 실려야 합니다.

인덱스 대신 validator_pubkey를 쓰는 이유

여러 검증자가 같은 실행 레이어 출금 자격증명을 공유할 수 있어, 어느 검증자를 종료할지 공개키로 특정합니다. validator_index로도 구분할 수 있지만 인덱스는 검증자가 비콘 체인에 생성된 뒤에야 정해지는 반면 공개키는 미리 알 수 있어 일부 스테이킹 풀 컨트랙트가 인덱스를 모르는 문제를 피할 수 있습니다. 요청을 합의 레이어 서명 검증 방식이 아닌 실행 레이어 제출로 설계한 것도 같은 맥락으로, secp256k1 서명을 Engine API로 검증하는 대안은 구현은 쉽지만 스마트 컨트랙트가 이 기능을 쓸 수 없게 됩니다. 요청을 비콘 블록 본문에 직접 싣는 이유는 합의 레이어와 실행 레이어가 서로 독립적으로 실행돼야 하기 때문입니다.

합의 레이어 처리

합의 레이어에는 새 연산 ExecutionLayerWithdrawalRequest가 추가됩니다. ExecutionPayload 안에 MAX_WITHDRAWAL_REQUESTS_PER_BLOCK으로 길이가 제한된 SSZ 리스트로 나타나며 process_operations가 각 요청마다 process_voluntary_exit과 비슷한 함수를 호출합니다. 다만 이미 종료된 검증자 같은 검증 실패가 나도 블록 자체는 실패하지 않습니다. 실행 레이어에서 들어오는 예치금(deposit) 처리와 같은 방식입니다. 전체 명세는 consensus-specs의 electra beacon-chain 문서에 있습니다.

보안 고려사항: 과납, 호출자 일치, 실패 처리

과납한 수수료는 돌려주지 않습니다. 컨트랙트에서 요청할 때는 빈 입력 staticcall로 현재 수수료를 읽어 정확히 그만큼만 보내고, 수수료 상한(requestFeeLimit)을 넘으면 revert하는 패턴을 권장합니다. 수수료는 트랜잭션 생성과 블록 포함 사이에 크게 변할 수 있습니다. EOA로 요청하면 과납을 피할 수 없어, 시뮬레이션으로 적정 금액을 추정하라고 권고합니다. 시스템 컨트랙트는 CALLER(msg.sender)를 출금 대상 주소로 쓰므로, 호출 주소가 비콘 상태에 기록된 0x01 출금 자격증명과 일치해야 합니다. 시스템 호출이 실패하면 블록이 무효가 되는데, 원인 트랜잭션이 멤풀에 남아 다시 포함되면 연속 슬롯이 빌 수 있어 블록 생산자가 트랜잭션 구성을 섞어 보라고 권장합니다. 포크 시점에 컨트랙트 코드가 없으면 모든 블록이 무효가 되므로, 트랜잭션으로 코드를 배포하거나 포크 시점을 미루는 복구 전략이 제시돼 있습니다.

EIP 원문 · ← EIPs