EIP-161 상태 트라이 빈 계정 정리 (State trie clearing (invariant-preserving alternative))
최종 확정 · EVM·실행 · 원문 상태 Final · 2016-10-24 작성
코드와 논스, 잔액이 모두 없는 '빈 계정'을 트랜잭션이 끝날 때 상태에서 삭제하고 새 빈 계정이 생기지 않도록 계정 생성과 가스 부과 규칙을 바꿉니다.
왜 필요한가
이더리움은 모든 계정 정보를 상태 트라이(state trie)라는 자료구조에 보관하고 모든 노드가 이를 저장합니다. 그런데 코드가 없고 논스(nonce)와 잔액이 모두 0인 '빈 계정(empty account)'이 상태에 그대로 남을 수 있었습니다. 이런 계정은 아무 기능도 하지 않으면서 저장 공간만 차지합니다. 원문이 인용한 보안 공지도 상태 정리가 끝나면 빈 계정이 더는 일반적인 우려 대상이 아니게 된다고 적어, 빈 계정이 실제 운영 부담이었음을 보여 줍니다. 앞서 같은 목적의 EIP-158이 제안되었지만 새로 만들어진 계정이 배포되기 전에 삭제될 수 있는 등 기존에 지켜지던 규칙(불변식)을 깨는 엣지 케이스가 있었습니다. EIP-161은 이런 불변식을 지키면서 같은 정리를 해내는 대안입니다. 빈 계정을 트랜잭션이 끝나는 시점에 지우고 애초에 새 빈 계정이 생기지 않도록 네 가지 규칙을 도입합니다. 이 EIP는 Spurious Dragon 하드포크(블록 2,675,000)에 포함되어 메인넷에 적용되었습니다.
무엇이 바뀌나
지금
- 코드, 논스, 잔액이 모두 0인 빈 계정이 상태 트라이에 계속 남습니다
- CALL이나 SUICIDE의 대상 계정이 존재하지 않으면 값 전송이 없어도 25,000 가스를 부과합니다
- 존재하지 않던 계정이 빈 계정 상태로 새로 만들어질 수 있습니다
- 컨트랙트 계정도 논스 0으로 생성됩니다
EIP-161 이후
- 트랜잭션이 건드린 계정이 끝나는 시점에 비어 있으면 상태에서 삭제됩니다
- 25,000 가스는 0보다 큰 값을 전송하고 대상이 dead 계정일 때만 부과합니다
- 어떤 연산도 계정을 비존재에서 '존재하지만 빈' 상태로 바꿀 수 없습니다
- CREATE로 만들어진 계정은 초기화 코드 실행 전에 논스가 1 올라간 값으로 시작합니다
누구에게 영향이 있나
- 노드 운영자: 아무 역할이 없는 빈 계정이 상태 트라이에서 사라져 모든 노드가 저장해야 하는 상태가 정리됩니다.
- 클라이언트 팀: touched, empty, dead 같은 정의와 삭제 시점, 상태 되돌림 시의 처리까지 네 가지 규칙을 정확히 같게 구현해야 합니다. 실제로 구현 차이로 2016년 합의 버그가 발생했습니다.
- 컨트랙트 개발자: CREATE로 배포된 컨트랙트 계정의 논스가 0이 아니라 1부터 시작합니다. 0 값 전송만으로는 새 계정 생성 비용이 들지 않습니다.
심층 설명
규칙 a: CREATE 시 논스를 1 올려 시작
계정 생성 트랜잭션과 CREATE 연산은 초기화 코드를 실행하기 전에 새 계정의 논스를 기본 시작값보다 1 크게 설정합니다. 일반 네트워크에서는 단순히 1이 되고 기본 시작 논스가 0이 아닌 테스트넷에서는 값이 달라집니다. Rationale은 논스를 0으로 두지 않는 이유를, CREATE로 만들어지는 계정이 생성 도중에 빈 계정으로 분류되어 삭제 대상이 되는 이상한 상황을 피하기 위해서라고 설명합니다.
규칙 b: 25,000 가스 부과 조건 축소
기존에는 CALL과 SUICIDE의 대상이 존재하지 않는 계정이면 25,000 가스를 부과했습니다. 이제는 해당 연산이 0보다 큰 값을 전송하고 대상 계정이 dead일 때만 부과합니다. dead는 비존재이거나 empty인 계정을 뜻합니다. 따라서 0 값 전송에는 이 비용이 발생하지 않습니다.
규칙 c: 비존재에서 빈 계정으로의 전환 금지
어떤 연산도 계정을 비존재 상태에서 '존재하지만 빈' 상태로 바꿀 수 없습니다. 어떤 연산이 그런 결과를 만들려 하면 그 계정은 그대로 비존재 상태로 남습니다. 규칙 d가 기존 빈 계정을 지우는 역할이라면, 이 규칙은 새 빈 계정이 애초에 생기지 않게 막는 역할입니다.
규칙 d: 트랜잭션 종료 시 touched 빈 계정 삭제
트랜잭션 실행이 건드린(touched) 계정이 트랜잭션이 끝나는 시점에 empty이면 비존재 상태로, 즉 삭제됩니다. touched는 상태를 바꿀 가능성이 있는 연산에 관여한 경우로, 0 값 전송의 수신자가 되는 것도 포함됩니다. empty는 코드가 없고 논스가 0이며 잔액이 0인 계정입니다. 계정이 '상태를 바꾼다'로 간주되는 경우는 0 이상의 값에 대한 SUICIDE의 대상이나 환급처, CALL이나 메시지 콜 트랜잭션의 출발지나 목적지, CREATE나 컨트랙트 생성 트랜잭션의 출발지나 생성 대상, 그리고 블록 생성자로서 블록 보상이나 수수료를 받는 경우입니다. '트랜잭션 종료 시점'은 suicide 목록 실행 직후이자 영수증에 넣을 상태 트라이 루트를 결정하기 전입니다.
구현이 추적할 네 가지 컨텍스트
원문 Notes에 따르면 현재 프로토콜에서 트랜잭션 실행 후 빈 계정이 남을 수 있는 경로는 많지 않아서 구현이 추적해야 하는 상황은 네 가지입니다. 빈 계정이 CALL로 0 값을 받는 경우, SUICIDE로 0 값을 받는 경우, 메시지 콜 트랜잭션으로 0 값을 받는 경우, 그리고 가스 가격이 0인 수수료 전송으로 0 값을 받는 경우입니다.
EIP-158과의 차이: 불변식 보존
Rationale은 이 제안이 EIP-158과 같되, 불변식을 깨지 않기 때문에 여러 엣지 케이스를 피한다고 설명합니다. 구체적으로 새로 생성된 계정이 배포되기 전에 삭제될 수 없다는 불변식이 지켜집니다. CREATE 시 논스를 0이 아닌 값으로 시작하는 것도 생성 도중의 계정이 삭제 대상으로 취급되는 오류 가능성을 없애기 위한 장치입니다.
2016-11-24 합의 버그와 명세 보완
2016년 11월 24일, 상태 되돌림(revert) 상황에서 두 구현의 동작이 달라 합의 버그가 발생했습니다. Geth는 빈 계정 삭제를 일으킨 트랜잭션이 가스 부족 예외로 끝났을 때 그 삭제를 되돌리지 못했고 Parity도 프리컴파일 컨트랙트에 대한 가스 부족 호출이 얽힌 더 제한된 맥락에서 같은 실수를 했습니다. 이후 Geth가 Parity의 동작에 맞추도록 수정되었고 2017년 8월 15일 명세에 상태가 되돌려지면 빈 계정 삭제도 되돌려진다는 점이 명시되었습니다. 이 EIP는 Spurious Dragon 하드포크(블록 2,675,000, 메인넷 CHAIN_ID 1)에 포함되어 적용되었으며 상태는 Final입니다.