EIP-7906 상태 변화 옵코드로 트랜잭션 결과 검증 (Transaction Assertions via State Diff Opcode)
Hegota 포함 검토 (CFI) · 보안·프라이버시 · 원문 상태 Draft · 2025-02-21 작성
트랜잭션 맨 끝에 읽기 전용 검사 단계(ASSERT 프레임)를 붙이고 그 안에서 새 옵코드 TXTRACE와 EVENTDATACOPY로 트랜잭션이 남긴 잔액·저장소·코드·논스 변화와 이벤트를 읽어 조건에 어긋나면 실행 결과를 되돌리게 합니다.
왜 필요한가
지금까지 도난당한 암호자산의 총액은 중간 규모 국가의 연간 GDP를 넘습니다. 그런데 일반 사용자나 지갑 앱이 트랜잭션이 실제로 실행할 EVM 코드를 찾아 모으고 검토하고 분석하는 능력은 매우 제한적입니다. 서명한 뒤에는 트랜잭션이 실제로 무엇을 하는지 제한할 수단이 전혀 없습니다. 그래서 사용자는 이더리움과 상호작용할 때마다 사실상 내용을 모른 채 서명(블라인드 서명)을 하고 큰 위험에 노출됩니다. 이 제안은 코드를 미리 읽는 대신 트랜잭션의 결과를 보고 제한하는 길을 엽니다. 지갑과 디앱이 트랜잭션에 온체인 검사를 붙이고 검사가 실패하면 트랜잭션의 효과를 버리게 하면 사용자가 자기 계정에 일어날 수 있는 일의 범위를 스스로 정할 수 있습니다.
무엇이 바뀌나
지금
- 사용자는 트랜잭션이 실행할 코드를 다 읽고 판단할 수 없어 사실상 내용을 모른 채 서명합니다
- 서명한 뒤에는 트랜잭션이 실제로 무엇을 하는지 제한할 방법이 없습니다
- 컨트랙트는 자기가 속한 트랜잭션이 전체적으로 어떤 변화를 남겼는지 실행 중에 알 수 없습니다
EIP-7906 이후
- 지갑이나 디앱이 트랜잭션 끝에 ASSERT 프레임을 붙여 결과를 온체인에서 검사합니다
- 검사가 실패하면 실행 부분의 효과는 모두 되돌아가고 트랜잭션은 유효한 채 가스만 지불됩니다
- TXTRACE로 잔액·저장소·코드·논스 변화와 이벤트를, EVENTDATACOPY로 이벤트 데이터를 읽습니다
- 여러 어설션 제공자가 각자 ASSERT 프레임을 붙여 서로 협력 없이도 함께 검사할 수 있습니다
누구에게 영향이 있나
- 사용자: 트랜잭션 결과에 제한을 걸 수 있어 블라인드 서명의 위험이 줄어듭니다. 다만 검사 범위가 좁은 어설션은 없는 것과 같으므로 안심의 근거가 되지 못합니다.
- 지갑·디앱 개발자: 어설션 컨트랙트를 만들고 스마트 계정의 VERIFY 프레임이 자기 ASSERT 프레임의 포함을 반드시 요구하도록 설계해야 합니다. 어설션 대상은 업그레이드할 수 없는 컨트랙트여야 합니다.
- 클라이언트 팀: 새 옵코드 두 개와 ASSERT 프레임 모드를 구현하고 트랜잭션의 상태 차이와 로그에 대한 인덱스를 만들어 TXTRACE 호출마다 일정한 비용으로 답해야 합니다.
- 어설션 제공자: ASSERT 프레임은 여러 개를 둘 수 있어 각 제공자가 자기 프레임에서 독립적으로 검사하고 실행 본문을 되돌릴 수 있습니다.
심층 설명
ASSERT 프레임 모드의 규칙
EIP-8141 프레임 트랜잭션의 mode 값에 ASSERT(값 3)를 추가합니다. 기존 DEFAULT, VERIFY, SENDER 옆에 놓이는 네 번째 모드입니다. ASSERT 프레임은 tx.frames의 맨 뒤에 연속해서 와야 하고 한 번 ASSERT가 나오면 그 뒤 프레임은 모두 ASSERT여야 합니다. 어기면 트랜잭션 자체가 무효입니다. caller는 ENTRY_POINT이고 STATICCALL로 실행되므로 상태를 바꿀 수 없습니다. VERIFY 프레임과 달리 APPROVE 예외도 없어서 ASSERT 호출 트리 안에서 APPROVE를 부르면 STATICCALL 위반으로 예외 중단됩니다. 승인 범위 플래그는 둘 수 있지만 효과가 없고 원자적 배치 플래그는 ASSERT 프레임과 그 바로 앞 프레임에 둘 수 없습니다. ASSERT 프레임이 revert하거나 예외 중단되면 실행 본문 전체가 무조건 되돌아가고 상태와 승인 컨텍스트는 payer를 정한 프레임 직후 시점으로 복원됩니다. payer를 정한 프레임이 없는 상태에서 실패하면 EIP-8141 규칙에 따라 트랜잭션이 무효입니다. 남은 ASSERT 프레임은 실행하지 않고 영수증 status 2, 가스 0으로 남기며 그 가스는 트랜잭션 끝에 환불합니다. 실패해도 트랜잭션은 유효하고 블록에 포함됩니다. 실패한 ASSERT 프레임의 영수증은 status 0이고 되돌려진 실행 본문 프레임의 영수증은 status와 실행 가스를 유지하되 로그는 버리고 상태 가스는 제거합니다. 검증 접두의 변화(APPROVE에 따른 가스 지불, deploy 프레임의 계정 생성)는 그대로 확정되고 payer는 실패 시점까지 쓴 가스를 모두 냅니다. EIP-8141 기본 코드는 ASSERT를 SENDER나 DEFAULT와 같게 다룹니다.
TXTRACE 옵코드(0xb6)와 파라미터 표
TXTRACE는 트랜잭션 시작 전 상태와 호출 시점 상태의 순 차이를 읽습니다. 스택 맨 위가 param, 그 아래가 in2이고 결과로 256비트 값 하나를 밀어 넣습니다. EIP-8141의 FRAMEPARAM과 같은 선택자·인덱스 방식이지만 피연산자 순서가 반대입니다. 개수 파라미터는 0x00 TXTRACE_BALANCES_CHANGED, 0x01 TXTRACE_SLOTS_CHANGED, 0x02 TXTRACE_CODES_CHANGED, 0x03 TXTRACE_NONCES_CHANGED, 0x11 TXTRACE_EVENTS_COUNT이며 in2는 0이어야 합니다. 각 상태 변화 표는 주소, before, after 순서로 필드가 같습니다. 잔액은 0x04~0x06, 저장소는 0x07~0x09에 슬롯 키 0x0A, 코드는 0x0B~0x0D, 논스는 0x0E~0x10입니다. 이벤트는 0x12 발생 주소, 0x13 토픽 수(0~4), 0x14~0x17 topic0~topic3(없는 토픽이면 예외 중단), 0x18 비인덱스 데이터 길이입니다. 0x19 TXTRACE_GAS_PRE_CHARGE는 EIP-8141의 max_cost로 지불 승인 때 payer에게서 걷은 금액이며 블록 기본 수수료에 따라 max_fee보다 작을 수 있고 블롭 수수료를 포함합니다. 0x1A TXTRACE_GAS_PAYER는 APPROVE(APPROVE_PAYMENT) 또는 APPROVE(APPROVE_EXECUTION_AND_PAYMENT)를 부른 프레임의 대상, 즉 payer이며 sender가 아닐 수 있습니다. 모든 param의 비용은 EIP-2929의 WARM_STORAGE_READ_COST로 같습니다. ASSERT 프레임 밖에서는 레거시·EIP-1559·다른 프레임 모드 어디서든 예외 중단됩니다.
상태 차이를 세는 방법
before 값은 트랜잭션 실행 전 prestate이고 after 값은 TXTRACE 호출 시점의 현재 상태입니다. 중간 쓰기는 따로 보이지 않습니다. balances_changed에는 잔액이 달라진 모든 주소가 들어가며 payer에게 적용된 가스 선납도 포함됩니다. 순수 ETH 이동을 구하려면 0x1A로 payer를 찾아 그 주소의 잔액 차이에서 0x19 값을 빼면 됩니다. slots_changed는 (주소, 슬롯) 쌍 단위이고 같은 슬롯을 여러 번 써도 한 항목으로 합쳐집니다. 저장소 전체가 지워지는 경우(저장소만 있는 주소에 CREATE, 저장소를 가진 빈 계정 제거)는 보고하지 않습니다. prestate 저장소를 전부 훑어야 해서 범위가 무한하기 때문입니다. codes_changed는 EXTCODEHASH처럼 저장된 코드 그대로의 해시를 비교하므로 EIP-7702 위임 지시자는 자기 자신이 해시되고 가리키는 코드를 따라가지 않습니다. 존재하지 않는 계정은 빈 코드 해시로 칩니다. 그래서 새로 만든 컨트랙트와 위임 지시자의 설치·변경·해제가 모두 잡히고 CREATE/CREATE2가 빈 코드를 남기면 잡히지 않습니다. nonces_changed에는 APPROVE가 논스를 올린 tx.sender, CREATE/CREATE2를 실행한 계정, 새로 만든 컨트랙트가 들어갑니다. 재전송 방지를 계정 논스 대신 저장소로 하면 slots_changed에 나타납니다. EIP-6780 삭제는 마지막 프레임 뒤에 일어나므로 같은 트랜잭션에서 만들고 없앤 컨트랙트도 codes_changed와 nonces_changed에 남습니다. EIP-7708 전송 로그처럼 프로토콜이 내는 로그도 이벤트로 세므로 이벤트 표로 ETH 전송도 볼 수 있습니다.
EVENTDATACOPY 옵코드(0xb7)
TXTRACE는 이벤트의 토픽과 데이터 길이만 주고 가변 길이인 데이터 자체는 스택 한 워드로 돌려줄 수 없습니다. EVENTDATACOPY가 그 데이터를 메모리로 복사합니다. 스택은 위에서부터 event_index, memOffset, dataOffset, length이고 출력은 없습니다. 이벤트의 비인덱스 데이터에서 dataOffset부터 length 바이트를 memOffset에 복사하며 복사와 메모리 확장은 CALLDATACOPY와 같습니다. 가스도 CALLDATACOPY와 같이 고정 3에 메모리 확장과 복사 비용이 붙습니다. 다만 소스 범위 검사가 더 엄격합니다. event_index가 events_count 이상이면 예외 중단이고 dataOffset + length가 이벤트 데이터 길이를 넘으면 length가 0이어도 예외 중단입니다. CALLDATACOPY는 소스 범위 때문에 중단되지 않는다는 점이 다릅니다. 덧셈은 256비트 래핑 없이 계산합니다.
가스, 접근 목록, 입력 검사, 정렬
두 옵코드는 이미 기록된 상태 차이와 로그만 읽습니다. 실제 상태를 읽지 않고 EIP-2929 접근 목록과 상호작용하지 않으며 블록 레벨 접근 목록에 접근을 추가하지 않습니다. 가스는 활성 포크의 스케줄을 따르고 EIP-8038이 활성이면 WARM_STORAGE_READ_COST는 WARM_ACCESS라는 이름으로 값 100입니다. EIP-7928이 활성일 때 ASSERT 실패로 실행 본문이 되돌아가면 되돌려진 프레임은 revert된 호출처럼 다룹니다. 상태 변화는 버리지만 접근한 주소와 슬롯은 목록에 남고 본문이 썼다가 복원된 슬롯은 순 변화가 없으므로 검증 접두나 같은 블록의 다른 트랜잭션이 바꾸지 않은 한 storage_reads에 기록됩니다. ASSERT 프레임 자체의 접근도 남고 검증 접두의 변화는 트랜잭션의 block_access_index에 기록됩니다. 입력 검사는 엄격합니다. must be 0인 in2에 0이 아닌 값을 주면 예외 중단, 0x1A보다 큰 param도 예외 중단이며 값은 잘라내지 않고 256비트 전체로 봅니다. 인덱스 파라미터(0x04~0x10, 0x12~0x18)의 in2가 해당 표의 개수 이상이면 예외 중단입니다. 정렬은 잔액·슬롯·코드·논스 변화가 주소를 uint160 숫자로 본 오름차순이고 한 주소 안의 슬롯은 키를 uint256으로 본 오름차순입니다. 이벤트는 발생 순서, 즉 트랜잭션 안의 전역 로그 인덱스 순입니다.
왜 이렇게 설계했는가
TXTRACE와 EVENTDATACOPY는 코드 캡슐화를 깨뜨릴 만큼 강한 조회 능력입니다. ASSERT 프레임 안에서만 허용해 결과의 유효성 판단과 실행 본문 되돌리기에만 쓰이게 했습니다. ASSERT가 맨 뒤에 붙어야 하므로 TXTRACE가 보는 차이는 항상 최종 결과이고 실패가 실행 본문 전체를 되돌리므로 원자적 배치나 앞선 프레임으로 일부만 피해 갈 수 없습니다. ASSERT 프레임을 여럿 둘 수 있어 서로 다른 어설션 제공자가 협력 없이도 각자 프레임에서 검사합니다. ASSERT 실패가 트랜잭션을 블록에서 빼고 가스 지불까지 되돌리면 공격자가 블록 가스 한도까지 쓰고 공짜로 revert하는 DoS 경로가 되므로 트랜잭션은 유효하게 두고 가스는 그대로 받습니다. 가스 상수는 새로 추가하지 않았습니다. 차이에 있는 슬롯과 계정은 트랜잭션이 이미 썼으므로 warm이고 SLOAD나 BALANCE로 읽어도 WARM_STORAGE_READ_COST이니 같은 값을 매기고 개수나 주소 같은 값도 가격을 하나로 통일했습니다. 클라이언트는 ASSERT 프레임이 처음 읽을 때 차이와 로그의 인덱스를 한 번 만들어 재사용하면 되고 그 비용은 트랜잭션이 이미 낸 접근 비용과 LOG 비용 안에 들어갑니다. 이벤트를 전달 통로로 쓰는 이유는 EIP-8141이 프레임 사이에 일시 저장소를 버리고 앞 프레임의 반환 데이터도 주지 않아서 실행 전 값을 ASSERT 프레임에 넘길 길이 이벤트뿐이기 때문입니다. 상태 변화는 재진입 호출로 같은 슬롯이 여러 번 쓰여도 한 항목이라 실행 순서로는 결정적 순서를 만들 수 없어 주소와 슬롯 키로 정렬하고 이벤트는 로그 인덱스가 있어 발생 순서를 씁니다.
보안 고려 사항
가장 큰 위험은 거짓 안심입니다. 검사 범위가 좁은 어설션은 위험한 트랜잭션을 안전하다고 믿게 하므로 불완전한 어설션은 없는 것과 같다고 봐야 합니다. 프로토콜은 ASSERT 프레임 포함을 강제하지 않습니다. 스마트 계정은 VERIFY 프레임에서 자기 ASSERT 프레임이 있는지 확인해야 하며 ASSERT가 끝에 모여 있으니 뒤에서부터 훑으면 됩니다. 어설션 대상은 업그레이드할 수 없는 불변 컨트랙트여야 합니다. 실행 본문이 어설션보다 먼저 돌므로 본문이 쓸 수 있는 상태는 공격자가 고를 수 있습니다. 오라클, balanceOf, 레지스트리, 프록시의 호출 결과나 after 값을 기준값으로 쓰면 조작되고 호출된 컨트랙트가 TXTRACE를 실행해 봐서 ASSERT 안인지 알아채고 다르게 답할 수도 있습니다. 기준값은 서명 시점에 고정된 ASSERT 프레임의 data나 before 값에서 가져와야 합니다. 어설션은 그 트랜잭션 하나만 제한합니다. 기존 allowance나 오프체인 permit은 다른 트랜잭션에서 쓰이고 대기 중인 출금이나 브리지 입금은 다른 곳에서 완료됩니다. 검증 접두의 변화는 보여도 되돌릴 수 없고 이벤트는 코드가 냈다는 사실만 보여 주므로 저장소와 잔액이 진실의 원천입니다. 가스 고갈도 문제입니다. 현재 설정에서 트랜잭션 하나가 약 42,600개 이벤트를 낼 수 있고 악의적 디앱이 값싼 로그로 비용을 부풀릴 수 있습니다. 어설션은 먼저 개수를 읽어 한도를 확인하고 프레임워크는 예상 개수에 비례한 가스를 넘기며 어설션의 OOG는 반드시 revert로 다뤄야 합니다. 정렬된 표는 이진 탐색이 되지만 이벤트는 전부 훑어야 합니다. 검증 접두는 되돌아가지 않으므로 신뢰할 수 없는 실행을 거기 두면 안 되고 deploy 프레임은 tx.sender 코드 설치 외의 용도로 쓰면 안 됩니다.
현재 상태와 포크 논의
이 EIP는 2025-02-21에 만들어졌고 상태는 Draft입니다. EIP-2929와 EIP-8141을 전제로 합니다. Draft 단계이므로 옵코드 번호, 파라미터 표, ASSERT 프레임 규칙은 앞으로 바뀔 수 있습니다. 코어 개발자 콜에서는 07.30 ACDE #242에서 헤고타 후보(PFI)로 접수했고 09.24 ACDE #246에서 EIP-8250, EIP-8272, EIP-7668, EIP-8253, EIP-3298, EIP-8131 등과 함께 헤고타 포함 검토(CFI)로 올렸습니다. 10.12 ACDT #100에서는 bogota-devnet-1에 EIP-8141(프레임), EIP-8250(키드 논스), EIP-8272(리센트 루트)와 함께 EIP-7906을 포함하기로 확정했습니다. 실행 계층 쪽 헤고타 후보들을 묶어 검증한다는 뜻입니다.