온체인에서 USDT(ERC-20 토큰) 100개를 전송해도 "갑이 을에게 USDT 100개를 지급했다"는 항목이 새로 추가되지는 않습니다. 실제로 일어나는 일은 컨트랙트 안 balanceOf 매핑의 값이 다시 쓰이는 것입니다. 호출자는 100이 줄고 수신자는 100이 늘어납니다. 이 변경이 최종적으로 어느 스토리지 슬롯에 반영되는지는 컨트랙트 작성 방식과 컴파일러가 정하며, 프로토콜은 balanceOf가 무엇을 반환하는지만 규정합니다. 동시에 이벤트 로그(log) 하나가 트랜잭션 영수증에 덧붙는데, 로그는 오프체인 인덱서가 소비하도록 만든 부산물이며 잔액의 진실은 스토리지의 현재 값에만 있습니다.
원장 비유는 여기서 깨집니다. 원장은 이미 일어난 일을 기록하고, 읽는 쪽은 역사를 누적해 결론을 얻습니다. 반면 이더리움은 계속 덮어써지는 "현재 세계"를 보관하고, 트랜잭션은 그것을 다음 버전으로 밀어 넣을 뿐입니다. "EVM이란 정확히 무엇인가"에 답하려면 먼저 이 "현재 세계"를 명확히 정의하고, 그다음에 누가 그것을 다시 쓰는지 이야기해야 합니다.
비트코인의 원장 모델이 충분한 이유는 상태 구조가 극도로 제한적이기 때문이다
"원장"은 비트코인을 정확하게 설명하는 말입니다. 트랜잭션의 입력은 이전 트랜잭션의 출력을 참조하고, 출력은 이후 입력으로 소비되기 전까지 미사용 트랜잭션 출력(unspent transaction output, UTXO)으로 남습니다. UTXO는 한 번만 소비할 수 있고, 지갑에 표시되는 잔액은 실제로 여러 UTXO의 합계입니다.
비트코인에 상태가 없는 것은 아닙니다. UTXO 집합이 곧 상태입니다. 다만 그 상태의 구조는 매우 제한적입니다. 통째로 생성하고 통째로 소비할 수 있는 "동전" 더미일 뿐이고, 범용 키-값 저장소가 없으며, 스크립트도 트랜잭션을 넘어 존재하는 변수를 보관하지 않습니다. 그래서 "원장과 미사용 출력 집합"만으로 시스템 전체를 완전히 설명할 수 있습니다. 트랜잭션을 검증하려면 참조된 출력이 존재하는지, 조건 스크립트가 충족되었는지만 확인하면 되고, 어떤 "컨트랙트 내부 변수"도 조회할 필요가 없습니다.
이더리움이 컨트랙트가 임의로 읽고 쓸 수 있는 매핑을 갖도록 지원하려면, 그 매핑을 더 이상 구현 세부 사항으로 취급할 수 없습니다. ERC-20 전송이 유효한지는 컨트랙트 스토리지에 있는 호출자의 잔액 슬롯이 현재 얼마인지에 달려 있습니다. 그리하여 상태는 "검증하면서 곁들여 유지하는 집합"에서 프로토콜이 반드시 약속해야 하는 일급 객체로 격상됩니다.
월드 스테이트: "지금 이 순간의 모든 것"을 32바이트로 압축한다
이더리움 옐로 페이퍼는 월드 스테이트(world state)를 주소에서 계정으로 가는 매핑으로 정의합니다. 각 계정은 4-튜플입니다:
- nonce: 해당 주소가 이미 보낸 트랜잭션 수, 또는 해당 컨트랙트 계정이 생성한 컨트랙트 수입니다.
- balance: wei로 표시한 잔액입니다.
- storageRoot: 해당 계정 스토리지 내용의 머클 패트리시아 트라이(MPT) 루트 해시입니다.
- codeHash: 해당 계정 코드의 Keccak-256 해시이며, 코드 자체는 이 해시를 키로 상태 데이터베이스에 저장됩니다.
여기서 두 가지를 구분해야 합니다. storageRoot가 약속하는 것은 계정 자체의 키-값 공간이며, 컨트랙트 작성자가 선언한 상태 변수는 결국 모두 여기에 들어갑니다. codeHash가 가리키는 것은 프로그램이고, 배포된 뒤에는 특정 트랜잭션에 의해 다시 쓰이지 않습니다. "데이터"와 "코드"를 두 필드로 나누어 각각 약속하는 것은 뒤에서 읽기/쓰기 집합과 충돌 감지를 논의할 때 거듭 쓰게 될 전제입니다(같은 시리즈의 0.5 "헷갈리기 쉬운 세 가지 스토리지: Memory, Storage, Transient Storage"에서 이 세 가지 스토리지의 소속과 수명을 다룹니다).
모든 계정은 다시 하나의 전역 머클 패트리시아 트라이로 조직되고, 그 루트 해시가 바로 stateRoot이며 블록 헤더에 기록됩니다. 즉 "지금 이 순간 이더리움 전체가 어떤 모습인가"는 32바이트로 압축될 수 있고, 전 네트워크는 이 32바이트에 대해서만 합의하면 됩니다.
온체인에 저장되는 것은 상태 루트이고, 상태 자체는 노드 로컬에 남는다
여기에는 흔히 모호하게 넘어가는 사실이 하나 있습니다. 옐로 페이퍼는 월드 스테이트가 블록체인에 저장되지 않으며 각 노드가 따로 유지하고, 체인에는 stateRoot라는 약속만 남는다고 분명히 씁니다. 트랜잭션과 영수증은 완전히 공개되지만 상태는 그렇지 않습니다.
이 역할 분담은 사용상의 경계를 많이 결정합니다. 어떤 계정의 현재 잔액을 알려면 두 가지 길밖에 없습니다. 스스로 모든 과거 트랜잭션을 재실행해 상태를 계산하거나, 어떤 노드에 stateRoot 아래에서 검증할 수 있는 머클 증명을 요청하는 것입니다. 라이트 클라이언트는 두 번째 길을 갑니다. 전체 상태를 저장할 필요는 없지만 누군가 증명을 제공해야 하고, 그 증명은 "어떤 stateRoot 아래에서 이 값이 무엇인가"만 말해 줄 뿐 그 stateRoot가 최신인지는 말해 주지 못합니다. 후자는 합의 계층의 몫입니다.
같은 맥락에서 아카이브 노드와 프루닝 노드의 차이도 이해할 수 있습니다. 역사적 상태는 계속 누적되므로 모든 역사 상태를 보관하는 노드는 디스크 비용이 계속 커집니다. 최근 상태만 유지하는 노드는 차지하는 공간이 작고 시작이 빠르지만, 임의의 과거 블록에서 상태 조회에 답할 수 없다는 대가를 치릅니다. 이른바 "온체인 데이터"는 공학적으로 두 가지로 나눠야 합니다. 트랜잭션과 영수증의 가용성, 그리고 상태 루트의 약속입니다. 이를 섞어서 이야기하면 나중에 데이터 가용성과 상태 팽창을 평가할 때 분별력을 잃게 됩니다.
상태 전이 함수: 이더리움이 규정하는 것은 하나의 함수다
옐로 페이퍼는 트랜잭션 수준 의미론을 한 줄짜리 공식으로 정의합니다:
σ_{t+1} ≡ Υ(σ_t, T)
여기서 σ는 월드 스테이트, T는 트랜잭션, Υ는 상태 전이 함수입니다. 읽기 쉬운 표기로 바꾸면 Y(S, T) → S'입니다. 옐로 페이퍼는 바로 이어서 유효한 상태 변경은 트랜잭션을 통해서만 일어날 수 있으며, 무효 상태 변경이 유효 상태 변경보다 훨씬 많다고 강조합니다. 예컨대 어느 계정의 잔액을 다른 곳의 동등한 증가 없이 허공에서 줄이는 경우입니다.
이 뜻을 이해하면 이렇게 바꿔 말할 수 있습니다. 이더리움 프로토콜이 실제로 규정하는 것은 "현재 상태와 트랜잭션 하나가 주어졌을 때 다음 버전 상태는 무엇인가"입니다. "체인에 무엇이 저장되었는가"는 이 함수의 입력과 출력일 뿐입니다. 블록 수준도 마찬가지로, 옐로 페이퍼는 Π를 써서 같은 함수를 트랜잭션 열을 따라 접어 나갑니다:
Π(σ, B) ≡ Υ(Υ(σ, T₀), T₁)…
상태 기계라는 말은 여기서야 실체를 얻습니다. 상태는 σ, 전이 규칙은 Υ, 블록은 전이의 묶음입니다.
원장 모델과 상태 기계 모델은 검증 비용이 다르다
두 모델을 나란히 비교하면 차이는 데이터 조직 방식뿐 아니라 검증 방식에서 드러납니다.
UTXO 트랜잭션의 입력은 명시적으로 선언됩니다. 각 입력이 어느 트랜잭션의 어느 출력을 참조하는지 밝힙니다. 검증자는 UTXO 집합에서 이 구체적 항목만 조회하면 되고, 어떤 전역 구조도 이해할 필요가 없습니다. 덕분에 비트코인의 트랜잭션 검증은 자연스럽게 국소화될 수 있고, 서로 무관한 트랜잭션을 독립적으로 처리할 수 있습니다. 선언적 계정 모델(예: Solana의 트랜잭션 메시지에 계정 주소를 미리 나열하고 명령이 인덱스로 참조하는 방식)도 같은 발상입니다.
계정 모델에서는 트랜잭션의 읽기/쓰기 집합이 트랜잭션에 적혀 있지 않습니다. 컨트랙트는 임의 주소의 임의 스토리지 슬롯을 읽을 수 있고, 무엇을 읽었는지는 실행이 끝나야 알 수 있습니다. 검증자는 실행 시점과 같은 상태 뷰를 반드시 가지고 있어야 하며, 그렇지 않으면 결과를 재현할 방법이 없습니다. 이 차이가 뒤에 이어지는 기술 노선 전체의 출발점입니다. 읽기/쓰기 집합 추론, 낙관적 동시성 제어, 충돌 감지는 모두 "트랜잭션이 무엇을 건드릴지 모른다"는 문제를 다룹니다.
이더리움도 선언적 모델에 다가가려 한 적이 있습니다. EIP-2930이 도입한 액세스 리스트(access list)는 새 트랜잭션 타입 0x01의 선택 필드로, 트랜잭션이 접근할 주소와 스토리지 슬롯을 미리 선언해 accessed_addresses와 accessed_storage_keys 집합에 넣고 할인을 받을 수 있게 합니다. EIP 본문은 동시에 목록 밖의 접근도 여전히 허용되며 다만 더 비쌀 뿐이라고 밝힙니다. 따라서 읽기/쓰기 집합은 트랜잭션에 강제로 적히지 않았고, 위 결론은 그것 때문에 바뀌지 않습니다.
프로토콜에서 EVM의 위치: "상태를 어떻게 바꾸는가"를 규정하는 명세
이더리움 가상 머신(Ethereum Virtual Machine, EVM)은 Υ의 구체적인 실행 명세로, 명령어 의미론 한 벌, 가스 계량 규칙 한 벌, 예외와 종료 의미론 한 벌을 포함합니다. 컨트랙트 코드는 바이트 열이며, 트랜잭션이나 다른 컨트랙트가 메시지 호출(message call)로 트리거할 때만 실행에 들어갑니다.
EVM은 이더리움 프로토콜과 같지 않습니다. 합의, 데이터 가용성, 정산은 각각 다른 계층의 시스템이며, 확장 지도 글에서 이를 따로 풀어냅니다. EVM은 한 가지만 답합니다. 바이트코드 한 조각과 입력 하나가 주어졌을 때 상태 변경과 반환값을 어떻게 결정론적으로 계산하는가입니다. 또한 이 계산에 얼마의 비용이 드는지도 정합니다. 가스 가격 책정 규칙 자체가 합의의 일부이고 하드 포크로 바꿀 수 있으며, EIP-2929가 계정과 스토리지 슬롯의 최초 접근 가격을 크게 올린 것이 그 예입니다.
헷갈리기 쉬운 지점이 하나 더 있습니다. EVM의 기계 모델에 "스택"만 있는 것이 아닙니다. 실행 컨텍스트마다 스택(stack), 메모리(memory), 프로그램 카운터(program counter, PC), 가스 카운터가 있고, 컨트랙트의 영속 변수는 SLOAD/SSTORE를 통해 계정 스토리지에 들어갑니다. 스택, 메모리, 실행 루프는 0.4의 주제이고, 세 가지 스토리지의 경계는 0.5의 주제입니다.
전 네트워크 재실행: 결과가 왜 비트 단위로 일치해야 하는가
이더리움에는 중앙 실행자가 없습니다. 모든 풀 노드가 같은 트랜잭션 묶음을 독립적으로 실행해 각자 stateRoot를 계산하고, 합의를 통해 이 값을 비교합니다. 실행 세부에서의 어떤 불일치든 곧바로 합의 실패가 되므로, 명세는 바이트 단위까지 세밀해야 합니다.
여기서 몇 가지 강한 제약이 따라옵니다. 부동소수점 연산은 명령어 집합에 들어오지 않았고, 256비트 워드 정수 기반 산술만 있습니다. 벽시계 시각, 프로세스 ID, 진짜 난수 같은 외부 입력은 합의 경로에 등장할 수 없습니다. 컨트랙트가 읽을 수 있는 타임스탬프와 블록 번호는 블록 헤더에서 오는, 합의로 고정된 값입니다.
계산은 또 계량 가능하고 반드시 종료되어야 합니다. 그렇지 않으면 유한한 블록 안에서 임의 코드를 안전하게 실행할 수 없습니다. 가스는 "정지 문제"를 "예산을 다 쓰면 멈춘다"로 바꿔 놓은 장치이고, 그 계량 상수도 성능 튜닝 항목이 아니라 합의 파라미터에 속합니다. 일반 전송의 고유 비용은 21000 gas이고, calldata에서 0 바이트마다 4 gas, 0이 아닌 바이트마다 16 gas가 부과됩니다. 이 숫자들은 옐로 페이퍼의 수수료표에 고정되어 있습니다. 이들을 바꾸는 것은 블록 크기를 바꾸는 것과 같은 종류의 프로토콜 변경이며, 하드 포크나 검증인 신호가 필요하고 특정 클라이언트가 마음대로 정할 수 없습니다.
결정성에는 쉽게 간과되는 결과가 하나 더 있습니다. 명세는 "미정의 동작"의 존재를 허용하지 않습니다. 옐로 페이퍼는 DIV가 제수가 0일 때 예외를 던지지 않고 0을 반환한다고 씁니다. 이 선택의 이유는 모든 클라이언트가 같은 잘못된 입력에 대해 같은 결과를 얻도록 하기 위함이며, 언어 설계 취향과는 무관합니다. 클라이언트의 단위 테스트와 execution-spec-tests 명세 테스트 스위트가 바로 이런 경계 동작을 고정하는 데 쓰입니다. 여러 독립 클라이언트(geth, revm, evmone 등)가 공존하는 것 자체도 결정성을 검증하는 수단입니다. 어느 구현이 에지 케이스를 다르게 이해하면 체인이 갈라지므로, 불일치는 테스트 단계에서 발견되어야 합니다.
결정성과 예측 가능성은 다른 문제다
두 층위의 불확실성을 구분해야 합니다. 실행 계층은 결정적입니다. 같은 바이트코드, 같은 입력, 같은 상태라면 반드시 같은 결과를 얻습니다. 그러나 트랜잭션이 어떤 결과를 얻는지는 실행 계층만으로 결정되지 않습니다. 같은 트랜잭션도 블록의 다른 위치에 놓이면 선행 상태가 달라져 결과가 달라질 수 있습니다. 선행 매매와 샌드위치의 수익은 정렬 권한에서 나오며, 실행 계층 자체는 여전히 결정적입니다.
이 구분은 병렬 실행 방식을 평가할 때 매우 중요합니다. 병렬화가 지켜야 하는 것은 "실행 결과가 어떤 직렬 순서와 동등하다"는 점입니다. "결과가 트랜잭션 제출 시각과 무관하다"는 목표는 애초에 성립하지 않습니다.
EVM은 고정된 명세가 아니다
EVM을 고정 표준으로 취급하면 호환성을 잘못 판단하게 됩니다. 명세는 하드 포크를 따라 진화합니다. PUSH0(EIP-3855, Shanghai)는 스택 기계에 상수 0을 푸시하는 명령어를 새로 추가했습니다. EIP-2929는 콜드/핫 접근의 가격을 다시 매겼습니다. 트랜잭션 계층은 트랜잭션당 16,777,216 gas 상한을 도입했습니다(EIP-7825, Fusaka와 함께 적용). EIP-7935는 클라이언트 기본 블록 gas 상한을 60M으로 올렸습니다.
따라서 "EVM 호환"이라는 말은 포크 높이와 구체적 범위를 함께 붙여야 의미가 있습니다. 바이트코드, 프리컴파일 컨트랙트, JSON-RPC, 툴링이 각각 어디까지 호환되는지는 더 읽기의 해당 글 범위입니다.
이 모델이 어떤 조건에서 부담이 되는가
상태 중심 설계는 범용 계산을 얻는 대신 세 곳에 대가를 치릅니다.
풀 노드는 항상 읽을 수 있는 현재 상태를 장기간 보관해야 하며, 그 규모는 계정과 스토리지 항목이 늘수록 커져 하드웨어 문턱을 계속 높입니다.
새 노드가 스스로 stateRoot를 계산하려면 과거 트랜잭션을 재실행하거나 스냅샷 동기화에 의존해야 하고, 검증 비용이 역사 길이에 비례하게 됩니다.
모든 트랜잭션이 전역 상태의 임의 위치를 건드릴 수 있기 때문에 고전적 구현은 한 건씩 직렬로 실행할 수밖에 없고, 이것이 뒤의 모든 병렬 실행 논의가 다루어야 할 원초적 제약입니다.
반대로 원장 비유가 완전히 무효한 것도 아닙니다. 감사, 인덱싱, 대사 같은 시나리오는 여전히 이벤트 스트림으로 데이터를 처리합니다. 영수증의 로그가 애초에 오프체인 소비를 위해 설계되었기 때문입니다. 문제는 이런 관점을 프로토콜 모델로 착각하지 않는 것뿐입니다.
출처
- Ethereum Yellow Paper(Upsilon, Pi, 월드 스테이트의 저장 방식, 계정 4-튜플, DIV 의미론, gas 수수료표): https://ethereum.github.io/yellowpaper/paper.pdf
- ethereum.org, Ethereum Virtual Machine (EVM): https://ethereum.org/developers/docs/evm/
- ethereum.org, Understanding the Yellow Paper's EVM Specifications: https://ethereum.org/developers/tutorials/yellow-paper-evm/
- Bitcoin Developer Guide, Transactions(UTXO는 한 번만 소비할 수 있고 입력은 구체적 출력을 참조): https://developer.bitcoin.org/devguide/transactions.html
- Solana Docs, Transactions(트랜잭션 메시지에 계정 주소를 미리 나열하고 명령이 인덱스로 참조): https://solana.com/docs/core/transactions
- ethereum.org, Nodes and clients(풀 노드, 아카이브 노드와 상태 프루닝, 전체 역사 상태는 아카이브 노드만 보존): https://ethereum.org/developers/docs/nodes-and-clients/
- EIP-2929, Gas cost increases for state access opcodes: https://eips.ethereum.org/EIPS/eip-2929
- EIP-2930, Optional access lists(타입 0x01 트랜잭션의 선택 필드, 목록 밖 접근도 여전히 허용되며 다만 더 비쌈): https://eips.ethereum.org/EIPS/eip-2930
- EIP-3855, PUSH0 instruction: https://eips.ethereum.org/EIPS/eip-3855
- EIP-7825, Transaction Gas Limit Cap(16,777,216 gas): https://eips.ethereum.org/EIPS/eip-7825
- EIP-7935, Set default gas limit to 60M: https://eips.ethereum.org/EIPS/eip-7935
- Ethereum Foundation, Fusaka Mainnet Announcement: https://blog.ethereum.org/2025/11/06/fusaka-mainnet-announcement
- go-ethereum, core/vm/instructions.go(opDiv가 제수가 0일 때 0을 기록): https://github.com/ethereum/go-ethereum/blob/master/core/vm/instructions.go
- ethereum/execution-spec-tests(명세 일관성 테스트 스위트): https://github.com/ethereum/execution-spec-tests
