지난 글에서는 Contract Account가 호출되면 코드가 실행된다고 했습니다.
그럼 바로 다음 질문이 생깁니다.
그 코드는 어디에서 실행되는가?
그 답이 EVM입니다.
EVM은 Ethereum Virtual Machine의 줄임말입니다.
처음에는 이렇게 이해하면 됩니다.
EVM
= 이더리움에서 스마트 컨트랙트 코드를 실행하는 공통 실행 규칙
EVM은 특정 회사의 서버 한 대가 아닙니다.
이더리움 노드들이 같은 규칙으로 트랜잭션과 컨트랙트 코드를 실행하기 위한 가상 실행 환경입니다.
누가 EVM을 실행하나
EVM을 실행하는 주체는 이더리움 노드입니다.
더 정확히는 실행 클라이언트가 EVM 구현을 가지고 있고, 블록 안의 트랜잭션을 처리할 때 EVM 규칙에 따라 실행합니다.
처음에는 이렇게 생각하면 충분합니다.
이더리움 노드
-> 블록을 받음
-> 블록 안의 트랜잭션을 확인
-> 컨트랙트 호출이 있으면 EVM으로 코드 실행
-> 실행 결과로 상태를 계산
즉 EVM은 어딘가에 있는 중앙 서버가 아닙니다.
각 노드가 같은 입력을 가지고 같은 규칙으로 실행해 같은 결과를 계산하기 위한 공통 규칙입니다.
왜 모든 노드가 같은 결과를 얻나
이더리움에서는 같은 이전 상태와 같은 트랜잭션 목록이 주어지면, 같은 결과 상태가 나와야 합니다.
Ethereum.org의 EVM 문서는 이더리움을 상태 기계로 설명합니다.
단순화하면 이런 모양입니다.
이전 상태 S
+ 트랜잭션들 T
= 새로운 상태 S'
조금 더 풀면:
같은 이전 상태
같은 트랜잭션
같은 EVM 실행 규칙
-> 같은 결과 상태
이 성질이 중요합니다.
각 노드가 같은 블록을 검증하면서 서로 다른 결과를 내면 합의가 불가능합니다.
그래서 EVM 실행은 예측 가능해야 합니다. 같은 입력이면 같은 출력이 나와야 합니다.
Solidity가 그대로 실행되나
사람이 스마트 컨트랙트를 작성할 때는 보통 Solidity 같은 언어를 사용합니다.
하지만 EVM은 사람이 쓴 Solidity 코드를 그대로 실행하지 않습니다.
스마트 컨트랙트는 배포되기 전에 EVM이 이해할 수 있는 바이트코드로 컴파일됩니다.
Solidity 코드
-> 컴파일
-> EVM bytecode
-> Contract Account에 저장
그리고 누군가 그 컨트랙트를 호출하면, EVM은 저장된 바이트코드를 실행합니다.
EOA가 컨트랙트 호출 트랜잭션 전송
-> Contract Account 선택
-> Contract Account의 bytecode 실행
-> 실행 결과로 상태 변화
즉 Solidity는 사람이 작성하기 위한 언어이고, EVM이 실제로 실행하는 것은 컴파일된 바이트코드입니다.
컨트랙트 호출은 어떻게 이어지나
EOA가 Contract Account를 호출하는 흐름을 다시 보면 이렇습니다.
다만 “사용자가 한다”와 “시스템의 각 주체가 한다”를 구분해야 합니다.
사용자는 시작점입니다. 모든 단계를 사용자가 직접 수행하는 것은 아닙니다.
사용자
-> 지갑 앱에서 트랜잭션 승인
지갑 앱
-> 트랜잭션 생성
-> EOA 개인키로 서명
-> 서명된 트랜잭션을 노드에 전송
이더리움 노드
-> 서명된 트랜잭션을 네트워크에 전파
블록 제안자
-> 트랜잭션을 블록에 포함
각 검증 노드
-> 블록 안의 트랜잭션을 순서대로 다시 계산
-> EVM 규칙으로 Contract Account의 코드 실행
따라서 사용자가 직접 하는 일은 승인입니다.
지갑 앱은 트랜잭션을 만들고 서명합니다.
노드는 서명된 트랜잭션을 전파하고, 블록을 검증할 때 블록 안의 트랜잭션을 순서대로 다시 계산합니다.
EVM은 그 실행 과정에서 Contract Account의 코드를 실행하는 규칙입니다.
여기서 EVM은 트랜잭션의 to, value, input 같은 정보를 사용합니다.
to: 호출할 Contract Account 주소
value: 함께 보낼 ETH
input: 호출할 함수와 인자 데이터
EVM은 이 정보를 바탕으로 컨트랙트 코드를 실행합니다.
컨트랙트 코드는 실행 중에 계정 상태를 읽거나 바꿀 수 있습니다.
예를 들어:
balance 변경
storage 안의 값 읽기
storage 안의 값 쓰기
event log 생성
다른 컨트랙트 호출
EVM 실행과 stateRoot
Account 모델 글에서 stateRoot를 봤습니다.
stateRoot는 블록 안의 트랜잭션을 실행한 뒤의 상태를 대표하는 root입니다.
EVM은 이 연결의 가운데에 있습니다.
이전 상태
-> 트랜잭션 실행
-> EVM이 컨트랙트 코드 실행
-> 계정 상태 변경
-> 새로운 stateRoot 계산
검증 노드는 블록 안의 트랜잭션을 순서대로 다시 계산합니다.
여기서 “다시 계산한다”는 말은 이전 상태 위에서 블록의 트랜잭션을 하나씩 처리해본다는 뜻입니다.
예를 들어 블록 안에 트랜잭션이 두 개 있다면, 노드는 첫 번째 트랜잭션이 만든 상태 변화 위에서 두 번째 트랜잭션을 처리합니다.
이전 상태
-> Tx1 처리
-> Tx1 처리 후 상태
-> Tx2 처리
-> Tx2 처리 후 상태
그 과정에서 컨트랙트 호출이 있으면 EVM 규칙에 따라 코드를 실행합니다.
그리고 자신이 계산한 최종 상태의 root가 블록 헤더의 stateRoot와 같은지 확인합니다.
블록 헤더의 stateRoot
vs
내가 EVM 실행 후 계산한 stateRoot
같으면 실행 결과가 일치합니다.
다르면 그 블록은 검증에 실패합니다.
실행이 실패하면 어떻게 되나
컨트랙트 실행이 항상 성공하는 것은 아닙니다.
예를 들어 컨트랙트 코드 안에서 조건을 만족하지 못할 수 있습니다.
토큰 잔액 부족
권한 없음
잘못된 입력
실행 중 gas 부족
실행이 실패하면 상태 변경은 되돌려질 수 있습니다.
예를 들어 컨트랙트 storage 안의 값을 바꾸다가 중간에 실패하면, 그 트랜잭션으로 인한 상태 변경은 최종 상태에 반영되지 않습니다.
다만 계산을 시도한 일 자체에는 비용이 들 수 있습니다.
이 비용과 실행 한도는 Gas 글에서 자세히 봅니다.
Gas가 왜 같이 나오나
EVM은 코드를 실행합니다.
코드 실행에는 계산 자원이 필요합니다.
만약 실행 비용이나 한도가 없다면 문제가 생깁니다.
무한 반복 코드
너무 많은 계산을 요구하는 코드
네트워크 자원을 과도하게 쓰는 트랜잭션
그래서 EVM 실행에는 gas가 붙습니다.
Ethereum.org는 gas를 이더리움 네트워크에서 특정 작업을 실행하는 데 필요한 계산량을 측정하는 단위로 설명합니다.
이번 글에서는 여기까지만 기억합니다.
EVM: 코드를 실행하는 규칙
Gas: 그 실행량을 재고 제한하는 단위
Gas는 다음 글에서 따로 정리합니다.
정리
오늘 기준으로는 이렇게 정리합니다.
EVM은 이더리움 노드들이 스마트 컨트랙트 코드를 같은 규칙으로 실행하기 위한 가상 실행 환경이다. EVM 실행 결과는 계정 상태 변화를 만들고, 그 결과는 stateRoot로 요약되어 블록 검증에 사용된다.
참고 자료
- Ethereum.org, Ethereum Virtual Machine: https://ethereum.org/developers/docs/evm/
- Ethereum.org, Introduction to smart contracts: https://ethereum.org/developers/docs/smart-contracts/
- Ethereum.org, Gas and fees: https://ethereum.org/developers/docs/gas/
- Ethereum Yellow Paper: https://ethereum.github.io/yellowpaper/paper.pdf