Deep dive into EIP-1153
Transient Storage Opcodes 일시적인 저장 공간

Intro
이더리움의 스마트 컨트랙트는 EVM(Ethereum Virtual Machine) 기반으로 스택(Stack), 메모리(Memory), 스토리지(Storage)와 같은 세 가지 데이터 저장 공간을 사용한다. EIP-1153은 새로운 저장 공간으로 “일시적인 스토리지”, 즉 Transient storage를 도입하고, 해당 스토리지에 접근할 수 있는 Opcode를 EVM에 추가함으로써 데이터 처리를 좀 더 효율적으로 수행하고자 하는 내용을 담고 있다.
EIP-1153는 칸툰(Cancun) 업그레이드 내역에 포함된 표준(EIP) 중 하나로, 덴쿤(Dancun) 업그레이드를 통해 메인넷에 반영된다. 그리고 현재 Solidity 0.8.24에서 transient 라는 새로운 저장 공간 태그(Tag)와 EIP-1153에서 주장하는 Opcode를 inline-assembly에 함수로 추가하는 작업까지 마친 상태이다.
Transient Storage
Transient Storage란 번역 그대로 일시적인 저장 공간이다. 함수가 실행되는 잠시 동안만 데이터를 저장하고 함수 실행이 끝나면 다시 원래 데이터로 되돌아가는 휘발성의 특성을 갖고 있다.
Transient Storage의 구조(Layout)와 동작 방식은 기존의 EVM에 있던 Storage와 완전히 같은 매커니즘을 갖는다. 때문에, 이더리움은 Storage에 접근하기 위한 함과수와 인터페이스를 그대로 Transient Storage에 적용하여 함수를 구현했다.
Opcode of Transient Storage
EIP-1153는 Transient Storage를 제어하기 위해 Opcode TSTORE와 TLOAD를 새로 만들기를 제시한다. 이더리움 Github Geth opcode.go를 보면 TSTORE(key,value)는 0x5d, TLOAD(key)는 0x5c라는 Opcode를 확인할 수 있다.
TSTORE(key,value)는 Transient Storage의 Key 위치에 해당하는 value를 설정(set)하는 기능이다. 스택(stack)에서 최상단 32 bytes의 데이터를 pop하여 데이터가 저장될 주소값(key)로 사용하고, 스택(stack)에서 그 다음 데이터를 value로 사용한다.
TLOAD(key)는 key 위치의 데이터를 읽어오는 기능을 한다. 마찬가지로 스택(stack)에서 최상단 32 bytes의 데이터를 pop하여 데이터가 저장될 주소값(key)로 사용한다.
Storage를 관리하는 Opcode는 SSTORE(key,value) ,SLOAD(key)가 있는데, EIP-1153 에서 제시한 TSTORE(key,value), TLOAD(key) 가 이와 똑같은 인터페이스로 설계되었다. 그럼 코드는 어떨까?
- SLOAD & SSTORE code by Geth

- TLOAD & TSTORE by Geth

Geth에 업데이트된 TSTORE와 TLOAD 코드를 보면 인터페이스 뿐만 아니라 로직의 동일함 또한 확인할 수 있다.
EVM이 Storage와 Transient Storage를 동작하는 Opcode의 인터페이스와 로직 모두 동일하지만, 두 저장 공간에 차이점은 “휘발성”이다. Transient Storage는 트랜잭션이 종료되면 원래 데이터로 돌아가지만, Storage는 영구적으로 데이터를 저장한다.
위에 첨부한 Geth의 코드를 보면 _StateDB.GetState()_와 _StateDB.SetState()_를 사용하는 SLOAD와 SSTORE와 달리, TLOAD와 TSTORE는 _StateDB.GetTransientState()_와 _StateDB.SetTransientState()_를 사용한다. Transient Storage를 다루는 GetTransientState()와 SetTransientState()에 대해 알아보자.
TSTORE : SetTransientState()

Transient Storage에 데이터를 저장하는 TSOTRE 함수 내부의 StateDB.SetTransientState()는 먼저 key에 해당하는 기존의 데이터(prev)를 journal라는 변수에 뒤쪽 삽입(append)한다. 만약 prev의 데이터와 새로 저장하려는 value가 같으면 이후의 코드는 따로 수행하지 않는다. 여기서 journal하는 역할은 무엇일까?

journal contains the list of state modifications applied since the last state commit. These are tracked to be able to be reverted in the case of an execution exception or request for reversal.
journal에는 마지막 상태 커밋 이후 적용된 상태 수정 목록이 포함됩니다. 이는 실행 중 예외나 되돌리기 요청이 있을 때 되돌릴 수 있도록 추적됩니다. 예외 또는 되돌리기 요청이 있을 경우 되돌릴 수 있도록 추적됩니다.
Geth에 나와있는 설명대로 journal은 트랜잭션이 revert 되거나 예외처리(excaption)될 때, storage에 저장된 데이터를 원래 데이터로 되돌리기 위해 일시적으로 캐싱(cache)하는 변수이다.
이는 Transient Storage뿐만 아니라 Storage 또한 적용되는 규칙으로, Storage에 데이터를 설정하는 stateObject.SetState(key, value) 함수에도 동일하게 들어가 있는것을 확인할 수 있다.
이런 이유로 StateDB.SetTransientState()가 실행되면 _GetTransientState()_를 통해 기존의 데이터(prev)를 journal에 저장한 후, setTransientState()를 통해 새로운 데이터를 Transient Storage에 저장한다.
TLOAD : GetTransientState()

Transient Storage에 저장된 데이터를 조회하는 TLOAD 함수 내부의 _StateDB.GetTransientState()_는 key에 해당하는 데이터를 불러온다. ( 이게 전부이다.. )
📢 Geth에서 Transient Storage의 데이터 자료구조 형태는 가장 깊은 단계의 함수가 구현된 transient_Storage.go 에서 확인할 수 있다.
Transaction Runtime Function : Execute()
EIP-1153이 적용되면서 업데이트된건 TSTORE와 TLOAD 함수 추가뿐만이 아니다. 위에서 언급했던것 처럼 Transient Storage에 저장된 데이터는 트랜잭션이 끝나면 없어지는 휘발성을 갖고 있다. 이런 특성을 적용하기 위해 EVM이 함수를 실행할 때 호출되는 Execute() 또한 수정이 된다.

Execute executes the code using the input as call data during the execution.
It returns the EVM's return value, the new state and an error if it failed.
Execute sets up an in-memory, temporary, environment for the execution of the given code. It makes sure that it's restored to its original state afterwards.Execute()가 실행하는 동안 input으로 받은 Calldata를 사용하여 코드(code)를 실행합니다. Execute()는 EVM으로 부터 받은 반환(return) 값 또는 새 상태(new state), 실패한 경우 오류를 반환(revert)합니다. Execute()는 주어진 코드를 실행하기 위한 인메모리 임시 환경을 설정합니다. 이후에는 원래 상태로 복원되도록 합니다.
Geth에 나와있는 설명대로 Execute() 함수는 호출하고자 하는 함수의 Calldata를 인자로 받아 함수를 실행한다. 빨간 네모 박스에 있는 state.Prepare()함수 위에 주석의 설명을 보면 “reset transient storage(eip 1153)”라고 적혀 있다.
state.Prepare()

Prepare handles the preparatory steps for executing a state transition with.This method must be invoked before state transition.
Prepare()는 상태 전환을 실행하기 위한 준비 단계를 처리합니다. 이 메서드는 상태 전환 전에 반드시 호출해야 합니다.
Geth에 주석으로 나와있는 state.Prepare()의 설명을 보면 상태 전환 실행, 즉 트랜잭션을 실행할 때 반드시 호출되는 함수이다. 함수 내부에는 여러 로직이 구현되어 있지만, 빨간 박스에 있는 코드를 보면 Null 상태의 새로운 transient storage를 만들어 저장한다. 이 과정에서 함수가 실행되는 동안 일시적으로 저장된 데이터를 리셋시켜 transient storage의 휘발성을 구현했다.
Motivation
지금까지 EIP-1153에 의해 업데이트된 핵심적인 내용을 다뤄봤다. 이더리움은 기존에 데이터를 저장하고 관리할 수 있는 Storage가 있다. 그럼에도 EIP-1153(transient storage)이 왜 등장하게 되었을까? 이유는 바로 Storage의 지독한 가스비 때문이다.

이더리움의 백서 yellow paper의 27번 페이지를 보면 SSTORE에 대한 가스비가 정의 되어 있다. Storage에 0이 아닌 데이터(non-zero)가 있는 경우 새로운 데이터를 저장할 때 2900 가스비 밖에 안들지만, 만약 0 밖에 없는 텅빈 곳에 새로운 데이터를 저장하는 경우 무려 20000 가스비가 소모된다. 이는 EVM 내 opcode 중에서 높은 수준의 가스를 요구한다.
TSTOREallows the developer to store 32 bytes for 100 gas, excluding any other required operations to prepare the stack.개발자는 스택을 준비하는 데 필요한 다른 작업을 제외하고 100 가스에 대해 32바이트를 저장할 수 있습니다.
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1153.md#security-considerations
EIP-1153에 나와있는 설명대로, TSTORE는 32 bytes를 저장하는데 100 가스비가 필요하며, 기존의 SSTORE보다 95%나 저렴해졌다.
Gas Refund
transient storage는 데이터를 일시적으로 저장하고 함수가 종료되면 초기화되는 메커니즘을 갖고 있다. 만약, Storage를 통해 이런 매커니즘을 구현해야한다면, 처음 데이터를 저장할때 20000 가스비가 필요하고 다시 0으로 초기화 할 때 2900의 가스비가 필요하다.
다행히 이더리움은 불필요한 스마트컨트랙트와 Storage Slot를 없애, 블록체인 용량을 최적화하게끔 개발자들에게 동기부여하기 위해, EIP-2200을 통해 가스비 환불(Gas Refund) 제도를 도입한다. Storage에 저장된 데이터를 초기화(0으로 변경)할 때, 최대치를 사용한 가스비의 50%를 다시 돌려준다. 이런 이유로 EIP-1153은 큰 주목을 받지 못했다.
하지만 일부 사용자들은 이런 가스비 환불 메커니즘을 의도와 다르게 사용하는 사례가 발생한다. 가스 가격이 낮을 때 Storage에 데이터를 저장하고 가스 가격이 높을 때 데이터를 초기화 시켜, 저장할 때 든 가스비보다 더 높은 가스비를 얻는 차익거래(aribtrage)로 사용한 것이다. 이더리움 개발자들은 의도와 다르게 사용되는 사례를 방지하기 위해 EIP-3529를 도입하여 환불 받는 가스비를 50% → 20%로 변경한다. 결국 transient storage와 같이 효율적인 가스비로 일시적으로 데이터를 저장해야하는 기능이 개발자들에게 다시 필요해졌다.
Use Case
transient storage처럼 데이터를 일시적으로 저장하고 함수가 종료되면 초기화되는 메커니즘이 스마트 컨트렉트에서 어떤 개발에 활용 될 수 있는지 알아보자. 대표적인 예시가 바로 재진입 공격 방지(Re-entrancy Guard) 패턴이다.
ReEntrancny.sol
contract ReEntranccy {
error ReentrancyGuardReentrantCall();
uint256 private status;
constructor() {
status = 1;
}
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
if (status == 2) {
revert ReentrancyGuardReentrantCall();
}
status = 2;
}
function _nonReentrantAfter() private {
status = 1;
}
}
contract example is ReEntranccy {
function transfer(address to, uint256 value) public nonReentrant() {
// ...
}
}재진입 공격 방지(Re-entrancy Guard) 패턴은 하나의 트랜잭션 실행환경(context) 내에서 컨트랙트 내 특정 함수가 여러번 실행되는 재진입 공격을 방지하는 역할을 수행한다. 동작 방식은 간단하다.
_uint256 status_는 컨트랙트 Storage Slot에 지정된 변수이다. _modifier nonReentrant()_를 함수에 적용하여, 함수가 실행되면 _status_를 2로 바꾸고 종료되는 시점에 1로 다시 바꾼다. 만약 함수가 종료되지 않은 시점에서 동일한 함수가 다시 호출(재진입)하게 되면 revert()가 발생된다.
이러한 재진입 공격 방지(Re-entrancy Guard) 패턴은 일시적으로 저장하고 함수가 종료되면 초기화되는 메커니즘을 갖는 transient storage와 동일하다. 다만 EIP-1153이 생기기 전까진 비싼 Storage를 사용하여 구현해야 했다.
contract ReEntranccy {
modifier nonreentrant {
assembly {
if tload(0) { revert(0, 0) }
tstore(0, 1)
}
_;
// Unlocks the guard, making the pattern composable.
// After the function exits, it can be called again, even in the same transaction.
assembly {
tstore(0, 0)
}
}
function claimGift() nonreentrant public {
// ...
}
}
EIP-1153로 수혜를 받는건 재진입 공격 방지 패턴뿐만이 아니다. ERC-20의 “(ERC-2612를 활용한) approve pattern”, “Till Pattern”, “Proxy Pattern” 등 기존의 다양한 코드에서 가스비 절감 효과를 받을 수 있다.
이 처럼 transient storage의 등장으로 최적화 될 수 있는 코드가 다수 존재하기에 EIP-1153는 개발자에게 반가운 소식이 아닐수 없다.
Dencum Upgrage
EIP-1153은 2018-06-15에 제안된 표준이다. 지금까지 이더리움 메인넷에 반영되지 못하다가 마침내 덴쿤(Dencum) Upgrage에서 메인넷에 적용되기로 결정됬다. 블록에 저장되는 공간 및 데이터를 최적화 시켜 확장성을 향상시키겠다는 덴쿤 업그레이드의 목표에 적합하기 때문이다.

이더리움 공식 문서의 “BLOCK SIZE”에 대한 내용을 보면 블록이 수용할 수 있는 용량을 물리적인 데이터 사이즈가 아닌 소비된 가스양으로 제한한다. 각 블록의 목표 크기는 1,500만 가스이지만, 블록의 크기는 네트워크 수요에 따라 최대 3,000만 가스까지 증가하거나 감소할 수 있다.
일반적으로 TPS(초당 거래 수)는 블록에 포함된 트랜잭션의 수와 관련되어 있다. 따라서 트랜잭션의 가스비가 줄어들면, 블록에 더 많은 트랜잭션이 포함될 수 있게 되어 TPS가 증가할 것이다. 이는 블록체인 네트워크의 확장성을 향상시키는 요인 중 하나이며 덴쿤 업그레이드 내역에 해당 내용이 포함된 이유이다.
마무리
기존의 스마트 컨트랙트 개발자들은 Storage를 최소화하고 최적화된 기법을 찾아내기 위해 노력하고 있었다. 따라서 EIP-1153은 이러한 개발자들에게 더욱 개선된 코드를 작성할 수 있는 기회를 제공하며, 이는 이더리움의 사용자 가스비를 저렴하게 만들어주는 효과도 기대해볼 수 있다.