EIP-8379 탑업 싱크: 합의층이 이끄는 실행층 동기화 (Top-up Sync)

포함 제외 (DFI) · 네트워크·노드 · 원문 상태 Draft · 2026-08-06 작성

합의 클라이언트가 실행 클라이언트의 블록 머리와 실행 머리를 조회하는 엔진 API 메서드를 추가하고 상태가 없는 경우와 블록 이력이 없는 경우를 구분하는 응답 값을 더해 합의 클라이언트가 실행 클라이언트의 블록 동기화를 직접 이끌 수 있게 합니다.

왜 필요한가

지금은 합의 클라이언트와 실행 클라이언트가 블록을 각자 받습니다. 합의 클라이언트는 가십과 req/resp로, 실행 클라이언트는 devp2p로 받으니 같은 네트워크 기능이 두 층에 중복됩니다. 합의 클라이언트가 실행 클라이언트에 블록을 직접 넘겨 주려면 실행 클라이언트가 어느 블록까지 갖고 있는지, 또 어디까지 실행해 상태를 갖고 있는지 알아야 하는데 엔진 API에는 그걸 묻는 방법이 없습니다. 두 값은 실행 클라이언트가 블록은 받았지만 실행하지 않았을 때 달라집니다. 바로 이 차이 덕분에 블록을 실행 없이 받아 두고 상태는 다른 방법으로 빨리 따라잡는 효율적인 동기화가 가능합니다. 또 engine_newPayload가 SYNCING을 돌려주면 합의 클라이언트는 조상 블록이 빠진 것인지(이때는 조상을 보내 줘야 함) 스냅 싱크 중이라 사전 상태가 없는 것인지(이때는 보내 봐야 소용없고 기다려야 함) 구분할 수 없습니다. 이 둘을 구분해야 engine_forkchoiceUpdated와 engine_newPayload의 주고받기가 결정적으로 돌아갑니다.

무엇이 바뀌나

지금

EIP-8379 이후

누구에게 영향이 있나

심층 설명

engine_getSyncStatus 메서드

엔진 API에 engine_getSyncStatus 메서드를 추가합니다. 클라이언트는 이 EIP가 포함된 포크 활성화 시점부터 이 메서드를 지원해야 하고 engine_exchangeCapabilities로 지원 사실을 알려야 합니다. 매개변수는 없고 타임아웃은 500ms입니다. 응답은 SyncStatus 객체이며 네 필드로 이뤄집니다. headBlockHash(DATA, 32바이트)와 headBlockNumber(QUANTITY, 64비트)는 클라이언트 정규 체인의 가장 최신 블록을, executedBlockHash(DATA, 32바이트 또는 null)와 executedBlockNumber(QUANTITY, 64비트 또는 null)는 클라이언트가 사후 상태(post-state)를 보유한 가장 최신 정규 블록을 가리킵니다. VALID 응답의 latestValidHash가 가리키는 블록이 이 실행 머리와 같은 블록입니다. 비규범 절인 Interface migration에는 REST/SSZ 엔진 API로 옮길 때 engine_getSyncStatus를 GET /identity와 비슷한 비범위 엔드포인트 GET /syncstatus로, MISSING_STATE를 해당 포크 범위 엔드포인트의 페이로드 상태 SSZ 열거형에 추가하는 대응이 적혀 있습니다.

블록 머리와 실행 머리를 보고하는 규칙

실행 머리(executed head)는 블록 머리의 정규 조상 또는 자기 자신 가운데 완전히 검증되고 사후 상태가 있는 가장 최신 블록입니다. 그런 블록이 없으면, 예컨대 초기 상태 동기화가 끝나기 전이면 executedBlockHash와 executedBlockNumber를 null로 둡니다. 블록 머리(block head)는 실행 머리에서 시작해(실행 머리가 없으면 제네시스에서 시작해) 끊김 없이 이어진 보유 블록 가운데, 가장 최근 engine_forkchoiceUpdated가 고른 정규 체인을 따르는 가장 최신 블록입니다. 그래서 새로 시작한 클라이언트는 초기 상태 동기화가 끝날 때까지 제네시스 블록을 보고합니다. 보고한 블록 머리를 부모로 하는 페이로드의 engine_newPayload 호출에는 VALID, INVALID, MISSING_STATE 중 하나로 답해야 하고 SYNCING으로 답하면 안 됩니다. 두 호출 사이에 engine_newPayload나 engine_forkchoiceUpdated가 처리되지 않았다면 같은 SyncStatus를 돌려줘야 합니다. engine_newPayload로 받았지만 아직 정규 체인에 붙지 않은 블록은 보고하면 안 됩니다.

MISSING_STATE 상태값: 실행 클라이언트 규칙

이 EIP와 함께 도입되는 engine_newPayload와 engine_forkchoiceUpdated 버전의 페이로드 상태 열거형에 MISSING_STATE를 추가합니다. 페이로드의 모든 조상 블록은 알지만 필요한 사전 상태(pre-state)가 없어 실행하지 못했으면 {status: MISSING_STATE, latestValidHash: null, validationError: null}을 돌려줍니다. 이렇게 답한 페이로드는 나중에 처리하기 위해 보존해야 하며 실행한 것처럼 알고 있는 블록 체인을 연장합니다. 이후 engine_forkchoiceUpdated로 그 페이로드가 정규가 되면 블록 머리는 전진하지만 실행 머리는 그대로입니다. engine_forkchoiceUpdated도 headBlockHash가 가리키는 블록과 조상을 모두 알지만 사전 상태가 없어 실행하지 못했으면 같은 MISSING_STATE 응답을 돌려줍니다. SYNCING은 처리에 필요한 조상 블록 데이터가 없을 때만 돌려줍니다. 실행 머리와 블록 머리 사이의 간격은 어떤 방법으로 메워도 됩니다. 저장한 페이로드를 순서대로 실행해도 되고 스냅 싱크나 EIP-7928 블록 접근 목록 패스트포워드로 상태를 직접 받아 그 아래 블록의 실행을 건너뛰어도 됩니다.

합의 클라이언트의 응답 규칙과 하위 호환

합의 클라이언트는 SYNCING을 받으면 빠진 조상 페이로드를 자기 블록 저장소나 req/resp 프로토콜로 가져와 engine_newPayload로 오름차순 전달해야 합니다(SHOULD). MISSING_STATE를 받으면 자기 머리가 나아가는 대로 다음 페이로드와 engine_forkchoiceUpdated 호출을 계속 보내야 하고(SHOULD), MISSING_STATE를 조상 페이로드 요청으로 해석하면 안 됩니다(MUST NOT). MISSING_STATE로 답받은 페이로드를 검증된 것으로 취급해서도 안 됩니다. 그 페이로드는 실행 클라이언트의 실행 머리가 그 블록을 지나갈 때까지 미검증 상태로 남습니다. 하위 호환 측면에서 engine_getSyncStatus는 새 메서드라 기존 소비자에 영향이 없고 MISSING_STATE는 새 메서드 버전에서만 돌아옵니다. 이전 버전은 기존 의미를 유지하며 SYNCING이 두 경우를 모두 덮습니다. 탑업 싱크를 구현하지 않는 합의 클라이언트는 MISSING_STATE를 지금의 SYNCING과 똑같이 다뤄도 정확성을 잃지 않습니다.

포크 경계를 넘는 탑업과 engine_newPayload 버전

한 번의 탑업이 포크 경계를 넘을 수 있으므로, 실행 클라이언트는 이 EIP와 함께 도입되는 engine_newPayload 버전으로 이전 포크의 페이로드를 받아들여야 하며 그 페이로드가 속한 포크의 검증 규칙을 적용해야 합니다. 합의 클라이언트는 과거 페이로드를 그 페이로드의 포크에 맞는 버전이 아니라 이 새 버전으로 전달해야 하고(SHOULD), 그 포크가 요구하는 부속 데이터(예: 블롭 버전 해시, 실행 요청)를 함께 보내야 합니다(MUST). 이렇게 하면 포크 경계 너머에서도 MISSING_STATE를 쓸 수 있습니다. 기존 엔진 API 버전은 각각 정확히 한 포크의 페이로드만 받았으므로, 이는 정립된 버전 관리 방식에서 의도적으로 벗어나는 것입니다. 원문은 포크 경계를 넘는 탑업에 꼭 필요한 변화라고 설명합니다. engine_forkchoiceUpdated로 전달하는 포크초이스 상태는 기존 명세와 같습니다.

설계 근거: 새 메서드, 단일 머리, 새 상태값, 별도 필드명

새 메서드를 둔 이유는 탑업 싱크가 제어 방향을 뒤집기 때문입니다. 실행 클라이언트가 스스로 블록을 받지 않으므로 수동 상태에서도 의미 있는 조회가 필요한데, 기존 엔진 API 메서드 중 어느 머리도 바로 알려 주는 것이 없습니다. 엔진 API 밖의 조회는 인증 포트에서 일관되게 제공되지 않으며 결정적 동기화 루프를 세울 만큼 정밀하지도 않습니다. finalized 블록에 뿌리를 둔 서브트리의 모든 끝을 돌려주는 대안은 실행 클라이언트가 모든 끝을 추적해야 해서 접어 두었습니다. 단일 머리라도 보고한 머리의 자식은 SYNCING으로 답하지 않고 보고는 engine_newPayload나 engine_forkchoiceUpdated에만 반응해 바뀐다는 보장이 있어 합의 클라이언트가 분기점을 찾아 다시 전달할 수 있습니다. 임의 블록 해시의 상태 조회는 뒤에 확장으로 더할 수 있습니다. MISSING_STATE는 ACCEPTED가 응답 구조를 바꾸지 않고 추가된 선례를 따랐습니다. SYNCING에 syncingReason 필드를 붙이는 안도 검토했지만 별도 상태값이면 합의 클라이언트 쪽 분기가 단순한 switch로 남고 조용히 무시될 수 없습니다. executedBlockHash라는 별도 이름을 쓰는 이유는 INVALID 응답의 latestValidHash가 무효 분기 기준의 상대적인 값인 반면, 실행 머리는 정규 체인의 속성이어서 그 아래에서 리오그가 나면 뒤로 갈 수도 있기 때문입니다.

보안 고려와 미결 사항

탑업 중 engine_newPayload로 전달되는 블록은 평상시 받은 블록과 같은 규칙으로 검증됩니다. 합의 클라이언트가 자기 정규 체인의 페이로드만 보내므로, devp2p 블록 다운로드와 비교하면 실행 클라이언트가 잘못되거나 악의적인 블록 데이터에 노출되는 정도가 줄어듭니다. 엔진 API는 인증 포트에서 제공되므로 engine_getSyncStatus와 MISSING_STATE는 신뢰하는 합의 클라이언트에만 노출되고 제3자에게 동기화 상태를 흘리지 않습니다. 반면 MISSING_STATE로 답한 페이로드를 보존해야 한다는 요구는 실행 머리가 끝까지 따라잡지 못하면 저장 공간이 무한히 커질 수 있음을 뜻합니다. 실행 클라이언트가 그런 페이로드를 지우고 합의 클라이언트의 재전달에 의존해도 되는지, 버퍼 한도를 어떻게 둘지는 논의가 필요하다고 적혀 있습니다. 설계는 합의 클라이언트 하나가 엔진 API를 이끈다고 가정합니다. 여러 합의 클라이언트가 한 실행 클라이언트에 붙으면 호출이 섞여 각 클라이언트 관점의 안정성 보장이 깨지며 멀티플렉서 소프트웨어가 탑업 트래픽을 직렬화할 것으로 기대합니다. 멀티플렉서에 규범적 요구를 둘지는 TODO로 남아 있습니다. 1024개 단위 같은 페이로드 일괄 전달과 finalized 블록의 검증 완화는 의도적으로 미래 EIP로 넘겼습니다.

현재 상태와 포크 논의

이 EIP는 Draft 상태이므로 메서드 이름, 필드, 상태값의 의미는 아직 바뀔 수 있습니다. 포크 논의로는 08.20 ACDC #185에서 EIP-8379가 헤고타(Hegota) 포함 제안(PFI)으로 접수됐습니다. 합의 클라이언트를 동기화의 단일 기준으로 만드는 엔진 API 확장이 후보가 됐다는 뜻입니다. 이어 10.01 ACDC #188에서 탑업 싱크(Top Up Sync)를 헤고타 후보에서 제외(DFI)했습니다. 원하는 팀은 포크 일정 밖에서 엔진 API 실험을 계속할 수 있습니다.

EIP 원문 · ← EIPs