EIP-8025 선택적 실행 증명 (Optional Execution Proofs)

Hegota 포함 제외 (DFI) · 네트워크·노드 · 원문 상태 Draft · 2025-09-17 작성

합의 계층 노드가 실행 페이로드를 직접 재실행하지 않고도 암호학적 증명으로 유효성을 확인할 수 있게 하는 선택 기능입니다. 참여는 자율이며 합의 규칙은 바뀌지 않습니다.

왜 필요한가

지금 비콘 블록을 검증하려면 그 안의 실행 페이로드를 실행 계층(EL) 상태 전체에 대고 다시 실행해야 합니다. 계정과 저장소 데이터가 모두 필요하고 재실행 비용은 가스 한도에 비례해 늘어납니다. 그래서 노드가 갖춰야 할 하드웨어가 체인의 처리량과 상태 크기에 묶여 있습니다. 가스 한도를 올리거나 상태가 커질수록 검증자가 되기 위한 장비 문턱도 함께 올라갑니다. 이 EIP는 증명을 만드는 노드와 증명을 검사하는 노드를 나눕니다. 검사하는 쪽은 EL 상태 없이도 일정한 시간 안에 페이로드 유효성을 확인할 수 있는 길을 엽니다. 다만 증명 체계가 아직 성숙 중이므로 처음부터 필수로 넣지 않고 원하는 노드만 켜서 실제 네트워크에서 운영 경험과 지표를 쌓는 방식을 택했습니다. 증명 생성 노드에 보상은 없습니다. 그래서 자발적 참여로 간주합니다.

무엇이 바뀌나

지금

EIP-8025 이후

누구에게 영향이 있나

심층 설명

두 가지 선택 모드와 보조 신호 원칙

노드는 증명 생성 모드와 증명 검사 모드 중 하나 또는 둘 다 켤 수 있습니다. 생성 모드는 활성 검증자가 증명자(prover)로 나서 실행 증명을 만들어 execution_proof 가십 토픽에 올리는 역할입니다. 검사 모드는 전파된 증명을 받아 해당 페이로드 유효성의 보조 신호로 삼습니다. 이 EIP 단계에서 검사 노드도 페이로드 재실행을 계속하며 검증된 증명은 재실행을 대체하지 않습니다. 포크 선택(fork choice)이 증명에 의존하지 않게 해 증명 스택을 실제 네트워크 조건에서 측정하려는 설계입니다. 증명 생성과 검사는 EIP-7732가 도입한 실행 검증 창을 활용합니다. 실행 검증이 다음 비콘 블록까지 미뤄지므로 증명을 만들 시간이 더 생깁니다. 증명 생성 노드에 대한 보상은 없으며 그래서 자발적(altruistic) 역할로 간주합니다. 또한 증명 인지 노드는 요청·생성·검사 흐름과 별개로, 동기화 중인 피어가 백필할 수 있도록 최종 확정 체크포인트까지의 정준 체인 블록 증명을 보관해야 합니다. 후속 EIP가 증명을 필수로 올리고 재실행을 없애는 안을 낼 수 있다고만 적혀 있습니다.

증명 컨테이너와 상수

가십 메시지의 최상위는 SignedExecutionProof이며 message(ExecutionProof), validator_index, signature 세 필드로 구성됩니다. ExecutionProof는 proof_data(ByteList[MAX_PROOF_SIZE]), proof_type(uint8), public_input으로 이루어집니다. PublicInput은 new_payload_request_root, successful_validation, chain_id(uint64), schema_id(uint16)를 담습니다. new_payload_request_root는 Engine API NewPayloadRequest의 SSZ hash_tree_root로, 증명과 특정 페이로드를 묶는 고리입니다. 검사자는 successful_validation이 True인지 반드시 확인해야 합니다. chain_id와 schema_id는 체인 간 재생과 포크 불일치를 막습니다. 상수는 MAX_PROOF_SIZE = 409600(400 KiB), MAX_EXECUTION_PROOFS_PER_PAYLOAD = uint64(4), DOMAIN_EXECUTION_PROOF = DomainType('0x0D000000')입니다. 400 KiB는 네트워크 대역폭 예산으로 정한 값입니다. 페이로드당 증명 종류 4개를 꽉 채워도 슬롯당 최악 4 × 400 KiB = 1.6 MiB로, 기존 블롭 사이드카 트래픽과 비슷한 규모입니다. 더 큰 증명을 내는 시스템은 전파 전에 재귀나 간결한 래퍼로 압축해야 합니다. proof_type 값은 노드별 동적 설정이며 새 증명 시스템은 새 ProofType 값을 대역 밖에서 알리고 동의한 운영자에게 배포하는 식으로 추가됩니다.

ProofEngine 인터페이스

ProofEngine은 증명 시스템별 로직을 감싸 합의 계층이 어떤 증명 시스템을 쓰는지 몰라도 되게 하는 인터페이스입니다. API 모양은 Engine API를 본떴고 ExecutionEngine이 페이로드 실행을 EL 클라이언트에 맡기듯 암호학적 작업은 외부 증명 노드(proof node)에 위임합니다. 연산은 네 가지입니다. verify_execution_proof는 가십 검증을 통과한 모든 증명에 대해 호출되며 결과를 public_input.new_payload_request_root가 가리키는 페이로드에 묶습니다. notify_new_payload와 notify_forkchoice_updated는 엔진이 비콘 노드 상태를 따라가게 하는 알림으로, 각각 들어오는 증명을 페이로드와 연결하고 정준 체인을 추적해 증명 보관과 정리를 이끕니다. request_proofs는 생성 노드가 NewPayloadRequest와 ProofAttributes(proof_types)를 넘겨 비동기 생성을 시작하는 연산이며 반환값은 new_payload_request.hash_tree_root()입니다. 증명자는 이 값을 요청 ID로 삼아 증명 노드의 완료 SSE 이벤트와 맞춥니다. 한 페이로드는 유효한 SignedExecutionProof가 k개 검증되면 증명 검증된 것으로 간주합니다. k 값은 Review 단계로 넘어가기 전에 확정할 예정이고 지금은 실제 데이터를 모으기 위해 일부러 열어 두었습니다.

비콘 체인 상태 전이와 process_execution_proof

process_block은 process_execution_payload에 EXECUTION_ENGINE과 함께 PROOF_ENGINE을 넘기도록 수정됩니다. process_execution_payload는 실행 엔진이 페이로드를 받아들인 뒤 proof_engine.notify_new_payload(NewPayloadRequest(...))를 호출해 새 페이로드를 증명 엔진에 알립니다. 새로 추가되는 process_execution_proof는 가십 검증을 통과한 SignedExecutionProof를 받아 증명자가 현재 상태에서 활성 검증자인지 확인하고 DOMAIN_EXECUTION_PROOF 도메인으로 BLS 서명을 검증한 뒤, proof_engine.verify_execution_proof에 암호학적 검사를 맡깁니다. 이 함수는 비콘 블록 상태 전이 함수 바깥에서 실행되며 온체인 상태를 바꾸지 않습니다. 전파된 증명을 페이로드 유효성 신호로 받아들일지 노드가 판단하는 운영용 훅입니다. 증명 보관은 ProofEngine이 맡고 보관 범위는 proof_serve_range, 즉 최종 확정된 체크포인트부터 현재 슬롯까지입니다.

가십 검증 규칙, req/resp, 디스커버리

execution_proof 토픽의 검증 규칙은 다른 비콘 가십 토픽과 같은 [IGNORE]/[REJECT] 관례를 따릅니다. IGNORE는 세 가지입니다. 해당 new_payload_request_root를 아직 보지 못했으면 무시하되 페이로드가 올 때까지 큐에 둘 수 있습니다. (new_payload_request_root, proof_type) 조합에 이미 유효한 증명이 있으면 무시해 종류별 첫 유효 증명만 전달합니다. (new_payload_request_root, proof_type, validator_index) 조합이 처음이 아니면 무시해 증명자당 한 번만 기회를 줍니다. REJECT는 validator_index가 활성 검증자가 아닐 때, BLS 서명이 틀릴 때, proof_data가 비었거나 MAX_PROOF_SIZE를 넘을 때, process_execution_proof 검사가 실패할 때 적용되며 기존 피어 점수로 감점됩니다. req/resp는 세 가지입니다. ExecutionProofsByRange는 (start_slot, count, proof_types)로 범위 백필에, ExecutionProofsByRoot는 (block_root, List[proof_type]) 식별자 목록으로 특정 블록 증명 조회에 씁니다. ExecutionProofStatus는 (block_root, slot, proof_types)를 주고받아 증명 검증 뷰와 지원 증명 종류를 교환하며 처음 연결하는 쪽이 먼저 보내야 합니다. 클라이언트는 proof_serve_range, 즉 [finalized_checkpoint.slot, current_slot] 안의 정준 블록에 대해 두 요청에 모두 응답해야 하고 못 하면 3: ResourceUnavailable로 답하며 감점될 수 있습니다. 디스커버리에는 uint8 ENR 키 eproof가 추가되며 값이 있고 0이 아니면 증명 인지 피어로 봅니다. Fulu의 cgc 필드와 같은 방식이라 포크나 포크 버전 변경 없이 광고를 켜고 끌 수 있습니다.

실행 계층: StatelessInput과 게스트 프로그램

증명자는 비공개 입력 StatelessInput 위에서 게스트를 돌리고 공개 출력 StatelessValidationResult를 냅니다. StatelessInput은 new_payload_request, witness, chain_id(uint64)로 구성되며 progressive 컨테이너와 리스트를 씁니다. 고정 경계는 MAX_EXTRA_DATA_BYTES = 32, MAX_WITNESS_HEADERS = 256, MAX_BYTES_PER_CODE = 2**16, MAX_BYTES_PER_HEADER = 2**10, MAX_BYTES_PER_WITNESS_NODE = 2**10입니다. ExecutionWitness의 state는 실행과 상태 루트 재계산에 필요한 RLP 인코딩 계정·저장소 트라이 노드 원상, codes는 사전 상태에서 읽은 바이트코드(같은 블록에서 배포된 코드는 제외), headers는 블록 번호순으로 페이로드 부모에서 끝나는 RLP 인코딩 조상 헤더입니다. 부모 헤더는 항상 들어가고 더 오래된 헤더는 BLOCKHASH가 돌려준 가장 오래된 블록까지 거슬러 갑니다. 게스트는 헤더가 연속된 체인인지 확인하고 그 해시로 BLOCKHASH를 제공합니다. 진입점 run_stateless_guest는 스키마 접두사가 붙은 SSZ 입력을 디코딩해 verify_stateless_new_payload를 실행하고 어떤 경우에도 결과를 직렬화합니다. 디코딩조차 실패하면 요청 루트, chain_id, schema_id가 모두 0인 실패 결과를 내므로 schema_id가 0이면 그 경우입니다. 디코딩 뒤 검증이 실패하면 디코딩된 값은 유지됩니다. WitnessState는 로컬 사전 상태 DB를 대신해 계정, 저장소, 코드 읽기를 증거에서 제공하고 블록 diff로 사후 상태 루트를 계산합니다. 키 부재는 증거에 부재를 증명하는 트라이 경로가 있을 때만 인정되고 경로상 노드나 바이트코드가 없거나 깨지면 검증 전체가 실패합니다. 참조 구현의 Amsterdam 게스트는 타임스탬프와 포크 활성화를 비교하지 않지만 프로덕션 구현은 반드시 비교해야 합니다.

호스트 측 입력 생성과 SSZ 스키마 ID

호스트(보통 상태를 가진 EL 클라이언트)는 블록을 실행하거나 만들면서 사용한 계정, 저장소, 코드, 조상 헤더 데이터를 기록합니다. build_stateless_input으로 ExecutionWitness, ExecutionRequests, 블록 접근 목록, chain_id를 묶어 StatelessInput을 만듭니다. 실행 증거는 EL이 블록 접근 목록(BAL)을 만들 때 이미 쓰는 블록 단위 읽기/쓰기 추적기와 사전 상태 트라이 데이터로 구성합니다. 호스트는 게스트가 읽을 때 쓰는 것과 같은 조회로 트라이 노드를 기록하고 양쪽 모두 각 저장소 트라이에서 삽입과 갱신을 먼저 적용한 뒤 삭제를 적용합니다. 삭제는 분기를 붕괴시킬 수 있고 그때 남은 형제 노드가 필요한데, 삭제를 마지막에 하면 붕괴가 줄어들고 양쪽 쓰기 순서가 같으므로 게스트가 호스트가 기록하지 않은 형제 노드를 찾는 일이 없습니다. build_execution_witness는 마지막에 mpt_root가 기대 사후 상태 루트와 같은지 단언합니다. 직렬화 시 SSZ 바이트 앞에 2바이트 스키마 ID를 붙입니다. 상위 바이트가 포크(Amsterdam = 0x15), 하위 바이트가 스키마 리비전이며 Amsterdam 리비전 1은 0x1501입니다. 게스트는 정준 SSZ 인코딩만 받아야 하며 참조 디코더는 디코딩한 값을 다시 인코딩해 바이트가 정확히 같지 않으면 거부합니다. 이 검사로 오프셋 틈과 읽지 않은 후행 바이트를 중첩 컨테이너 안까지 잡아냅니다. 적합성 테스트는 blockchain_test가 아니라 blockchain_test_engine 픽스처로 돌려야 합니다. StatelessInput이 Engine API NewPayloadRequest를 감싸므로 엔진 픽스처가 더 많은 무효 페이로드 변형을 다룹니다.

보안 고려 사항과 현재 상태

위조된 증명은 포크 선택이나 다른 합의 규칙에 연결되지 않으므로 체인을 분기시키거나 검증자를 슬래싱할 수 없고 영향은 검사 노드의 로컬 유효성 뷰에 그칩니다. 증명 생성과 검사는 어테스테이션 핫 패스 밖에 있습니다. 검증자는 증명을 기다리느라 블록 검증이나 어테스테이션 생성을 늦춰서는 안 되며 증명이 없거나 늦으면 실행 엔진의 재실행으로 정한 포크 선택을 따릅니다. 검사자와 증명자의 비대칭도 지적됩니다. 검사 문턱은 낮아지지만 증명자는 EL 상태 전체를 들고 증거 연산 크기 n에 대해 O(n log² n)으로 비용이 커지는 증명 스택을 돌려야 하므로, 검사 전용 노드가 증명 가능 노드보다 빨리 늘면 상태를 유지하고 증명을 만드는 주체가 더 집중될 수 있습니다. 가십 면에서는 MAX_PROOF_SIZE로 크기를 묶고 다른 CL 토픽과 같은 검증, DoS 방지 속도 제한, 피어 점수를 적용합니다. 이 EIP는 Draft 상태라 내용이 바뀔 수 있으며 k 값처럼 아직 정하지 않은 항목이 있습니다. 포크 일정으로는 10.15 ACDC #189에서 헤고타(Hegota) 후보에서 제외(DFI)됐습니다. CL 팀들의 지지가 CFI 기준에 못 미쳐 실험은 별도 브랜치에서 계속됩니다. 앞선 09.16 AMA #15와 10.01 ACDC #188에서는 논의만 있었고 결정은 없었습니다. 기각 다음 날에는 합의 변경이 없어 리스크가 없는 준비된 작업을 버렸다는 비판과, 조기 채택이 숙려를 우회하며 하드포크용 코어 EIP가 아니라 informational EIP에 어울린다는 반론이 오갔습니다.

EIP 원문 · ← EIPs