EIP-8272 프레임 트랜잭션용 최근 루트 (Recent Roots for Frame Transactions)

Hegota 포함 검토 (CFI) · 계정 추상화 · 원문 상태 Draft · 2026-05-15 작성

EIP-8141 프레임 트랜잭션이 앱이 최근에 기록한 머클 루트를 공개 멤풀 검증 단계에서 확인할 수 있게 합니다. 표준 컨트랙트 하나가 슬롯별 루트를 보관하고 전용 VERIFY 프레임이 이를 검사합니다.

왜 필요한가

EIP-8141 프레임 트랜잭션은 공개 멤풀에서 검증될 때 다른 계정이나 앱이 관리하는 저장소를 읽지 못합니다. 공유 저장소는 다른 거래가 언제든 바꿀 수 있어서 거기에 기댄 검증 결과를 노드가 믿고 들고 있을 수 없기 때문입니다. 그런데 어떤 검증은 최근 앱 상태가 꼭 필요합니다. 프라이버시 앱은 커밋먼트(commitment) 트리를 유지하고 최근 트리 루트를 기준으로 지출을 증명합니다. 지갑 권한 루트나 계정 검증용 루트도 같은 처지입니다. 이 EIP는 쓰는 주체와 슬롯으로 키를 정해 루트를 저장하는 표준 컨트랙트를 둡니다. 리오그가 아니면 거래가 참조하는 동안 그 항목은 바뀌지 않으므로, 노드가 안심하고 확인하고 색인할 수 있습니다. 거래 봉투도 옵코드도 새로 만들지 않습니다.

무엇이 바뀌나

지금

EIP-8272 이후

누구에게 영향이 있나

심층 설명

현재 슬롯과 참조 가능한 창

이 EIP의 시간 기준은 블록 타임스탬프가 아니라 합의 계층의 슬롯입니다. 블록 실행 중 current_slot은 그 실행 페이로드를 담은 비콘 블록의 슬롯이고 실행 클라이언트는 EIP-7843의 slotNumber 필드에서 읽어야 합니다. block.timestamp에 고정 슬롯 길이를 적용해 역산하는 방식은 금지합니다. 공개 멤풀 검증에서는 최신 정규 실행 헤더의 slotNumber에 1을 더한 값을 current_slot으로 쓰고 시뮬레이션의 SLOTNUM 값도 이와 같아야 합니다. 빈 슬롯이 지나가도 이 값은 일부러 올리지 않습니다. 대신 페이로드에 거래를 넣기 전에 그 페이로드의 slotNumber로 다시 검증해야 합니다. 참조는 current_slot보다 엄격히 앞선 슬롯만 가리킬 수 있고 슬롯 S에 쓴 루트는 S + 1부터 참조할 수 있습니다. 창 크기도 여기서 나옵니다. 슬롯 S의 쓰기는 S mod RECENT_ROOT_LENGTH 칸을 갱신하는데, 그 칸을 같이 쓰는 옛 루트는 정확히 8192 슬롯 전 것이라 이미 만료돼 있습니다. 그래서 현재 슬롯의 쓰기가 유효한 참조를 깨뜨리지 못하고 RECENT_ROOT_LENGTH = 8192에서 참조 가능한 창은 현재 슬롯을 뺀 RECENT_ROOT_USABLE_WINDOW = 8191이 됩니다. 만료 직전 참조는 슬롯이 건너뛰어지면 상하므로 빌더가 정확한 페이로드 슬롯으로 재검증합니다.

루트 소스와 저장 키 계산

루트를 쓰는 주체는 source_id = keccak256(source_address || salt)로 식별합니다. source_address는 외부 소유 계정이든 컨트랙트든 되고 salt를 달리하면 한 주소가 여러 소스를 둘 수 있습니다. 누가 쓸 수 있는지와 salt를 어떻게 배분할지는 앱 책임입니다. 저장값은 entry_hash = keccak256(RECENT_ROOT_ENTRY_DOMAIN || source_id || uint64_be(slot) || root), 저장 위치는 storage_key = keccak256(RECENT_ROOT_STORAGE_DOMAIN || source_id || uint64_be(i))이고 i = slot mod RECENT_ROOT_LENGTH입니다. 소스마다 길이 8192의 링 버퍼 entries: bytes32[RECENT_ROOT_LENGTH]를 갖는 셈이고 모든 칸은 처음에 0입니다. 슬롯과 인덱스는 8바이트 빅엔디안, 주소는 20바이트, 도메인·루트·salt·source_id·해시·키는 32바이트 고정 길이로 이어 붙입니다. 저장값이 source_id와 slot과 root를 모두 담은 해시라서 다른 소스의 루트나 같은 칸에 남아 있던 옛 루트로는 검사를 통과할 수 없습니다. 소스 생성 거래는 따로 없습니다. 새 (source_address, salt)로 처음 쓸 때 소스가 생기고 저장 칸을 새로 만드는 쓰기는 보통 상태 증가 비용을 냅니다. 전체 저장소는 쓰인 source_id 수에 비례해 늘고 소스당 최대 RECENT_ROOT_LENGTH개 키를 씁니다.

컨트랙트 배포와 쓰기 연산

컨트랙트는 RECENT_ROOT_ADDRESS(0x8272D9679689Ea2f307140CdF9002D27dC00Ffff)에 보통 컨트랙트처럼 배포하며 프로토콜이 활성화 때 설치하지 않습니다. 배포 트랜잭션은 type 0x0, nonce 0, gas 0x13d620, gasPrice 0xe8d4a51000으로 고정돼 있고 그 서명에서 거꾸로 구한 합성 송신자 0x14bf16d4c9842bf1EbF396e553477C66EB0a8A82가 보냅니다. 값이 0이 아닌 호출은 되돌리며 쓰기와 검증 두 연산은 calldata 길이로 고릅니다. 쓰기는 정확히 64바이트로, 앞 32바이트가 salt, 뒤 32바이트가 root입니다. 호출한 msg.sender가 source_address가 되고 슬롯 S에 성공한 호출은 i = S mod RECENT_ROOT_LENGTH 칸에 entry_hash를 저장합니다. 정적 컨텍스트에서는 실패하고 저장소는 그대로입니다. DELEGATECALL·CALLCODE는 호출한 계정의 저장소를, EIP-7702 위임 코드는 위임한 계정의 저장소를 쓰므로, 저장소 컨텍스트가 RECENT_ROOT_ADDRESS가 아니면 어느 것도 최근 루트 저장소를 바꾸지 못합니다. 같은 소스 주소와 salt가 한 슬롯에 여러 번 쓰면 같은 키를 겨냥하므로 정규 블록 실행 순서상 마지막 값만 남고 그 값만 S + 1부터 참조할 수 있습니다. 성공한 쓰기는 반환값과 로그가 없습니다.

검증 연산과 최근 루트 검증 프레임

검증 연산의 calldata는 n * RECENT_ROOT_TUPLE_BYTES(72)바이트이고 1 <= n <= MAX_RECENT_ROOT_REFERENCES(16)입니다. 빈 calldata, 72의 배수가 아닌 길이, 16개 초과는 되돌리고 64바이트 쓰기 인코딩과는 겹치지 않습니다. 튜플마다 slot < current_slot, current_slot - slot <= RECENT_ROOT_USABLE_WINDOW(8191), storage[storage_key] == entry_hash를 확인하며 하나라도 틀리면 전체를 되돌립니다. 중복 튜플은 각각 따로 검사하고 모두 통과하면 반환값·로그·상태 변화 없이 성공합니다. 현재 슬롯은 SLOTNUM으로 읽고 다른 계정을 부르지 않으며 calldata에서 파생한 저장 키만 읽습니다. 이 연산을 담는 최근 루트 검증 프레임은 mode == VERIFY, target == RECENT_ROOT_ADDRESS, flags == 0, value == 0, limits.state == 0이고 data 길이가 72 이상 16 * 72 이하인 72의 배수입니다. 공개 멤풀 분류에서는 recent_root_verify라 부릅니다. EIP-8141의 STATICCALL 의미로 실행되며 컨트랙트가 되돌리거나 프레임이 예외 정지하면 거래가 무효가 됩니다. 가스는 보통 EVM 가스이고 대상 접근과 저장소 읽기는 일반 warm·cold 규칙을, 데이터는 EIP-8141 거래 데이터 요금을 따릅니다. 봉투 필드 대신 표준 프레임을 쓴 덕에 EIP-8141의 인코딩·서명 해시·실행·가스·프레임 조회를 그대로 재사용하고 거래 페이로드와 서명 해시도 바뀌지 않습니다.

공개 멤풀에서의 자리와 허용 범위

공개 멤풀 대상 거래는 recent_root_verify 프레임을 최대 하나만 넣을 수 있고 자리는 [expiry_verify?] [recent_root_verify?] [deploy?] [계정 검증] ... 순서로 고정됩니다. 즉 프레임 0이거나, expiry_verify가 있으면 프레임 1입니다. EIP-8141의 네 가지 검증 접두 모양을 맞출 때는 앞의 선택적 프로토콜 검증 프레임 둘을 건너뛴 뒤 deploy가 첫 번째여야 한다는 규칙을 적용합니다. 송신자 상태를 읽기 전에 프레임 모양과 데이터 인코딩부터 검사해야 합니다. 추가 허용은 둘뿐입니다. 이 프레임이 최상위에서 실행되고 RECENT_ROOT_ADDRESS의 런타임 코드가 RECENT_ROOT_CODE와 정확히 같을 때만, 그 코드 안에서 SLOTNUM을 실행할 수 있고 튜플에서 파생한 storage_key에 한해 SLOAD를 할 수 있습니다. 중첩 호출에는 허용하지 않고 코드가 다르면 거래를 거절합니다. 클라이언트는 RECENT_ROOT_CODE를 실행하는 대신 프레임을 직접 평가해도 되지만 프레임 영수증·가스 사용량·warm 계정과 저장 키의 갱신과 롤백이 EVM 실행과 같아야 하고 limits.execution이 모자라 예외 정지할 경우도 똑같이 거절해야 합니다. 이 프레임의 limits.execution은 MAX_VERIFY_GAS에 포함해 셉니다. 앱의 계정 검증 코드는 FRAMEPARAM으로 resolved_target == RECENT_ROOT_ADDRESS, mode == VERIFY, flags == 0, limits.state == 0, 올바른 데이터 길이, status == 1을 확인한 뒤 FRAMEDATALOAD·FRAMEDATACOPY로 튜플을 읽습니다.

만료, 의존성 추적, 리오그 처리

튜플 중 하나라도 slot >= current_slot이면 거래를 받지 않고 대기 중이던 거래도 내보냅니다. 그 밖에는 어느 튜플이든 current_slot - slot >= RECENT_ROOT_LENGTH에 이르면 내보냅니다. 받자마자 만료될 거래를 피하려고, 가장 오래된 참조가 노드가 정한 여유 폭 안에 들면 받지 않는 것이 좋습니다. 받을 때는 서로 다른 (storage_key, entry_hash) 의존성마다 처음 만료되는 current_slot을 함께 기록하고 슬롯이 넘어가거나 리오그가 나도 무관한 거래를 훑지 않도록 이 값으로 색인하는 것이 좋습니다. RECENT_ROOT_ADDRESS의 코드와 활성 상태도 의존성으로 추적합니다. 거래가 멤풀을 떠나면 기록을 지우고 거래가 교체되면 기록도 원자적으로 함께 교체합니다. 의존성이 바뀌었을 수 있으면 영향받는 거래의 나이·저장소 조건을 모두 다시 확인하고 만족하지 못하는 거래를 내보냅니다. 리오그 때는 제거된 분기와 추가된 분기 양쪽의 상태 변화를 보며 롤백으로 항목이 사라지고 대체 쓰기가 없는 경우도 포함합니다. 컨트랙트 코드나 활성 상태가 바뀌면 이 프레임이 있는 모든 대기 거래를 재검증합니다. 프레임 데이터에 (source_id, slot, root) 전체를 넣은 이유가 여기 있습니다. 클라이언트는 앱 검증을 돌리지 않고도 저장소 의존성을 전부 뽑을 수 있고 리오그 뒤 고아가 된 루트를 참조하는 거래를 바로 골라낼 수 있습니다. 72바이트 패킹 인코딩은 개수나 셀렉터가 없고 유효 길이가 64바이트 쓰기와 겹치지 않습니다.

보안 고려 사항

합의 계층은 root를 뜻 없는 bytes32로만 다룹니다. 그 값이 무엇을 약속하는지는 앱이 정하고 승인하려는 내용에 기대하는 (source_id, slot, root) 튜플을 묶어야 합니다. 검증 프레임 성공은 그 튜플이 지금 저장돼 있고 최근 것이라는 사실만 증명하므로, 앱 검증은 프레임 데이터를 믿기 전에 완료된 프레임의 정의 필드와 성공 여부를 확인해야 합니다. EIP-8141은 표준 서명 해시 대신 명시적 32바이트 서명 메시지로 검증하는 것도 허용하는데, 이 경우 검증 로직이 최근 루트 프레임과 관련 프레임을 스스로 묶어야 합니다. 같은 (source_id, slot)에는 정규 실행 순서상 마지막 루트만 남으므로, 한 슬롯에 여러 루트가 필요한 앱은 집계 커밋을 써야 합니다. 이 EIP는 루트 쓰기 거래의 포함을 보장하지 않습니다. 제때 게시해야 하는 앱은 자체 게시 경로, 여분의 루트 소스, 쓰기 거래 포함 정책을 갖춰야 합니다. 같은 (source_address, salt)는 다른 체인에서도 같은 source_id가 되므로, 체인 밖에서 만든 증명·브리지 메시지·증명서는 튜플 바깥에서 체인 도메인을 묶어야 합니다. 저장소는 RECENT_ROOT_ADDRESS 아래 영구적으로 남습니다. 기존 소스는 최대 RECENT_ROOT_LENGTH칸을 덮어쓰지만 새 소스는 칸을 늘리므로, 보통 상태 증가 요금으로 부족하면 소스 등록 비용이나 첫 쓰기 할증을 뒤에 제안할 수 있다고 적었습니다. 인기 있는 루트 하나가 리오그나 동시 만료로 많은 거래를 한꺼번에 내보낼 수 있지만 의존성 색인 덕에 무관한 거래의 검증 접두를 다시 돌릴 필요는 없습니다.

활성화, 테스트 케이스, 상태와 포크 결정

이 EIP는 EIP-8141과 EIP-7843과 같은 시점이나 그 뒤에 활성화해야 하며 활성화 자체는 상태를 바꾸지 않습니다. 배포 트랜잭션이 들어가기 전에는 RECENT_ROOT_ADDRESS에 코드가 없어 VERIFY 모드의 EIP-8141 기본 코드가 돌고 flags가 0이라 되돌려져 프레임이 실패합니다. 배포 이전 슬롯을 가리키는 참조는 저장소가 비어 있어 만족될 수 없습니다. 테스트 케이스는 나이 8191과 8192 슬롯의 경계, 71·73바이트와 17개 튜플 데이터, flags == 1이나 상태 가스가 0이 아닌 프레임(컨트랙트는 정상 실행되지만 recent_root_verify로 분류하지 않아 멤풀에서 거절), MAX_VERIFY_GAS에 정확히 맞는 경우와 1 초과, 직접 평가와 EVM 실행의 동일성, 리오그로 루트나 배포 트랜잭션이 사라지는 경우를 다룹니다. 열여섯 튜플 케이스는 EIP-8141이 프레임 진입 때 매기는 cold 대상 계정 접근 비용까지 포함해야 합니다. current_slot = 2, slot 1 기준의 참조 벡터도 원문에 있습니다. 원문 상태는 Draft라 세부 규칙이 바뀔 수 있습니다. 코어 개발자 콜에서는 07.02 ACDE #240에서 EIP-8272를 헤고타 후보(PFI)로 접수했고 09.24 ACDE #246에서 EIP-7906·8250·7668·8253·3298·8131 등과 함께 헤고타 포함 검토(CFI)로 올렸으며 10.12 ACDT #100에서 bogota-devnet-1에 EIP-8141·8250·8272·7906을 포함하기로 확정했습니다.

EIP 원문 · ← EIPs