EIP-1283 더티 맵 없는 SSTORE 순가스 계산 (Net gas metering for SSTORE without dirty maps)
최종 확정 · 수수료·가스 · 원문 상태 Final · 2018-08-01 작성
스토리지 쓰기 명령어 SSTORE의 가스 비용을 트랜잭션이 끝났을 때 실제로 남는 변경량 기준으로 다시 계산합니다. 같은 슬롯에 반복해서 쓰면 두 번째부터는 200가스만 냅니다.
왜 필요한가
이더리움에서 컨트랙트가 데이터를 저장하는 명령어인 SSTORE는 쓸 때마다 최대 20000가스라는 비싼 요금을 받습니다. 그런데 실제 클라이언트 구현은 트랜잭션 도중의 변경을 메모리에만 들고 있다가 나중에 한 번에 디스크에 씁니다. 같은 트랜잭션 안에서 같은 슬롯을 여러 번 바꿔도 디스크 쓰기는 결국 한 번인데, 요금은 매번 전액을 받는 셈입니다. 이 때문에 재진입 잠금(reentry lock)처럼 트랜잭션 안에서만 잠깐 쓰고 지우는 용도나, 하위 호출과 상위 호출 사이에 오류 코드를 주고받는 용도가 과도하게 비쌌습니다. 앞서 나온 EIP-1087이 같은 문제를 풀려 했지만 변경된 슬롯 목록을 따로 기록하는 더티 맵(dirty map)이 필요해서 저장 구조를 다르게 최적화한 클라이언트에는 부담이 됐습니다. 이 EIP는 슬롯의 원래 값, 현재 값, 환불 카운터라는 거의 모든 구현이 이미 가진 정보만으로 같은 효과를 냅니다.
무엇이 바뀌나
지금
- 같은 트랜잭션 안에서 같은 슬롯에 여러 번 써도 매번 5000 또는 20000가스를 냅니다
- 트랜잭션 안에서만 쓰고 지우는 임시 데이터도 영구 저장과 같은 요금을 냅니다
- 값을 원래대로 되돌려도 쓴 가스가 돌아오지 않습니다
EIP-1283 이후
- 이미 바꾼 슬롯에 다시 쓰면 200가스만 냅니다
- 슬롯을 원래 값으로 되돌리면 19800 또는 4800가스를 환불 카운터에 돌려받습니다
- 어떤 경우에도 기존 방식보다 가스가 더 들지 않습니다
누구에게 영향이 있나
- 컨트랙트 개발자: 재진입 잠금, 같은 컨트랙트 안의 다중 송금, 하위 호출과의 메시지 전달처럼 한 트랜잭션 안에서 스토리지를 반복해 쓰는 패턴이 크게 싸집니다.
- 사용자: 원문은 가스 인상이 없고 많은 컨트랙트가 가스 절감을 보게 된다고 밝힙니다. 해당 컨트랙트를 쓰는 트랜잭션의 수수료가 줄어듭니다.
- 클라이언트 팀: SSTORE 가스 계산 로직을 교체해야 합니다. 호출 프레임마다 환불 카운터를 새로 만드는 구현이라면 카운터를 부호 있는 값으로 바꿔야 합니다.
심층 설명
세 가지 값의 정의
계산은 슬롯마다 세 값을 비교해서 이뤄집니다. 원래 값(original value)은 현재 트랜잭션이 통째로 되돌려질 때 남는 값, 즉 트랜잭션 시작 시점의 값입니다. 현재 값(current value)은 이번 SSTORE가 실행되기 직전의 값이고 새 값(new value)은 실행된 뒤의 값입니다. 원래 값이 트랜잭션 기준으로 정의되기 때문에 호출 프레임이 바뀌어도 가스 계산에 영향을 주지 않습니다.
새 가스 계산 규칙
현재 값과 새 값이 같으면 아무 일도 하지 않는 노옵(No-op)으로 보고 200가스만 뺍니다. 값이 바뀌는 경우, 원래 값과 현재 값이 같으면(이번 실행에서 처음 바꾸는 프레시 상태) 기존 요금 그대로 원래 값이 0이면 20000가스, 아니면 5000가스를 뺍니다. 이때 새 값이 0이면 15000가스를 환불 카운터에 더합니다. 원래 값과 현재 값이 다르면(이미 바뀐 더티 상태) 200가스만 뺍니다. 더티 상태에서는 두 가지 보정을 함께 적용합니다. 원래 값이 0이 아닐 때 현재 값이 0이면 환불 카운터에서 15000을 빼고 새 값이 0이면 15000을 더합니다. 또 새 값이 원래 값과 같아져 슬롯이 리셋되면 원래 값이 0일 때 19800, 아닐 때 4800가스를 환불 카운터에 더합니다.
상태 전이로 본 구조
원문은 전체 경우를 세 상태로 나눕니다. 처음 바꾸는 SSTORE는 모두 프레시(Fresh)에서 시작하고 값이 바뀌면 더티(Dirty)가 됩니다. 프레시에서 더티로 갈 때는 기존 요금 체계와 같은 가스를 받고, 더티에 머무는 동안은 200가스만 받습니다. 더티 슬롯을 원래 값으로 되돌리면 프레시로 돌아가며 환불이 발생합니다. 더티 상태에서는 0으로 바꾸는 변경에 주는 환불(R_SCLEAR)을 추적해서 이미 준 환불이 더는 해당하지 않으면 빼고 새로 해당하면 더합니다. 원문은 원래 값이 0인 경우와 아닌 경우를 표로 정리해 모든 전이의 가스와 환불을 보여 줍니다.
환불 카운터와 구현 주의점
환불은 기존처럼 소비한 가스의 절반까지만 적용되고 트랜잭션 수준에서 환불 카운터는 0 아래로 내려가지 않습니다. 다만 구현 방식에 따라 차이가 있습니다. 환불을 트랜잭션 수준 하나로 관리하고 프레임마다 체크포인트를 잡는 구현은 부호 없는 값을 그대로 쓰면 됩니다. 반면 호출 프레임마다 환불 카운터를 새로 만들어 끝날 때 부모에 합치는 구현은 내부 호출에서 자식 카운터가 음수가 될 수 있으므로 부호 있는 값으로 바꿔야 합니다.
EIP-1087과의 비교
이 EIP는 임시 스토리지 제안(EIP-1087, EIP-1153)이 노리는 효과를 더티 맵이나 별도 저장 구조 없이 달성합니다. EIP-1087은 스토리지 변경 목록을 더티 맵으로 들고 있다가 트랜잭션 끝에 트라이에 커밋한다고 암묵적으로 가정하는데, EIP-658 이후 효율적인 구현은 인메모리 트라이나 불변 자료구조로 변경을 추적하고 블록 끝에만 커밋하는 경우가 많아 변경 전체를 순회하기 어렵습니다. 이 EIP는 원래 값과 현재 값만 알면 되므로 런타임에 추가 메모리나 처리 비용이 없습니다. 절대 가스 사용량(사용 가스에서 환불을 뺀 값)은 모든 경우에 EIP-1087과 같고, 슬롯을 바꿨다가 원래 값으로 되돌린 뒤 다시 바꾸는 한 가지 경우에만 EIP-1087보다 더 많은 금액이 환불 카운터 쪽으로 이동합니다.
비용 예시와 테스트 케이스
원문이 든 예시로, 빈 스토리지에서 슬롯 0을 1로 썼다가 0으로 되돌리면 20000 더하기 200에서 19800을 뺀 400가스가 듭니다. 빈 스토리지에서 슬롯 0을 다섯 번 증가시키면 20000 더하기 5 곱하기 200인 21000가스입니다. 모든 계정의 시작과 끝 잔액이 0이 아닌 상태에서 A가 B에게, 이어서 B가 C에게 송금하면 5000 곱하기 3 더하기 200에서 4800을 뺀 10400가스입니다. 원문은 연속 두 번의 SSTORE를 다루는 15개와 슬롯을 리셋한 뒤 다시 쓰는 세 번짜리 2개, 총 17개 테스트 케이스를 사용 가스와 환불 값과 함께 제공합니다.
적용 방식과 현재 상태
이 변경은 하드포크가 필요합니다. 원문은 가스 비용 인상은 예상되지 않으며 많은 컨트랙트가 가스 절감을 보게 된다고 밝힙니다. 원문에는 증명 부록이 있어, 원래 값이 0인 경우와 아닌 경우를 나눠 귀납법으로 최종 결과별 절대 가스 사용량이 의도한 식과 일치함을 보입니다. 이 EIP의 상태는 Final로 확정된 명세입니다.