Blockchain - 이더리움 EVM 동작 구조

김도영·2022년 12월 12일
0

이더리움 EVM 동작 구조

이더리움 상태머신

이더리움은 거래에 기반을 둔 상태머신이다. 여기서 상태 머신이란 어떠한 입력을 읽고, 그 입력을 기반으로 새로운 상태로 전환하는 것을 의미한다. 이더리움의 상태 머신은 Genesis State에서 시작한다. 그리고 트랜잭션이 실행되면, 다음 상태로 전환하게 된다. 트랜잭션이 더 이상 들어오지 않은 마지막 상태가 이더리움의 현재 상태이다.

이더리움에는 수 천개의 트랜잭션이 있다. 이 트랜잭션은 블록으로 묶여 있으며, 블록은 직전에 만들어진 블록과 이어져 있다.

EOA(External Owned Account)와 CA(Contraact Account)

이더리움에는 EOA와 CA라는 두 종류의 계정이 있다. 모든 계정은 160bit 길이의 주소로 식별 되고, 동일한 주소 공간을 가진다.

EOA와 CA는 잔액(Balance), 논스(Nonce), 스토리지(Storage), 컨트랙트 코드(Contract Code)로 구성되어 있다. EOA의 주소는 상응하는 비밀 키를 가지고 있지만, CA는 비밀 키가 없다.

  • Balance: 계정의 현재 이더 잔고
  • Nonce: EOA의 경우, 해당 EOA로부터 보내진 트랜잭션의 숫자, CA의 경우 해당 CA로부터 생성된 컨트랙트의 숫자를 의미
  • StorageRoot: 머클 패트리샤의 루트 노드를 해싱한 값
  • Contract Code: EVN이 실행할 코드의 해싱된 값, EOA에는 코드를 저장할 수 없기에 비어있다.

여기서 CA는 새로운 트랜잭션을 만들 수 없고, EOA나 다른 CA에게서 받은 트랜잭션에 대한 응답의 트랜잭션만 만들 수 있다. 따라서, 이더리움 블록체인에서 일어나는 모든 액션은 항상 EOA에서 만든 트랜잭션에서부터 시작한다.

전역 상태

이더리움의 전역상태는 계정 주소와 계정 상태를 매핑한 것으로 구성되어 있다. 이 매핑은 머클 패트리샤 트리형태로 저장되어 있다.

  • 트리 맨 아래에 있는 노드들은 데이터를 가지고 있다.
  • 부모 노드는 두 자식 노드를 모아 해싱한 값을 가진다. 맨 위의 루트 노드도 자식 노드의 해싱한 값이다.

트리 맨 아래에 있는 데이터는 저장하려는 데이터를 청크(Chunk)로 분할한 다음, 각 청크를 두 개씩 모아 해시를 취하여 부모 노드를 만든다. 이렇게 하나의 루트 노드가 만들어질 때까지 동일한 과정을 반복한다. 이더리움의 블록 헤더에는 세 개의 머클트리 구조의 루트 노드의 해시값이 저장되어 있다.

  • 상태트리: 트랜잭션과 연관된 계정의 상태 정보 저장
  • 트랜잭션 트리: 현재 블록의 트랜잭션 정보 버장
  • 영수증 트리: 현재 블록의 거래 영수증 정보 저장

부모 노드는 자식 노드의 해싱 값이기 때문에, 단방향 함수인 해시의 특성상 자식 노드의 값이 조금이라도 바뀌면 부모 노드의 값도 바뀌게 된다. 따라서 머클 패트리샤 트리에서 변조는 즉시 발견된다.

트랜잭션과 메시지

트랜잭션은 EOA에서 생성되고, 일련의 과정을 거쳐 블록체인에 올라간 암호화 서명된 명령어의 집합이다. 그 외의 트랜잭션은 메시지 호출과 컨트랙트 생성 두 종류로 나뉜다. 이더리움의 모든 트랜잭션은 항상 EOA에서 만들어지고 블록체인에 올라간다. 즉, 트랜잭션은 외부 세계를 이더리움 내부의 상태로 연결해주는 다리 역할이고, 그 연결의 창구가 바로 CA이다.

CA 생성

새로운 CA를 만들기 위해서는 먼저 특별한 공식을 이용해 새로운 계정의 주소를 정의해야 한다. 그리고 다음의 과정을 통해 만들어진다.

  1. 논스를 0으로 설정한다.
  2. 송신자가 CA생성 트랜잭션에 이더를 함께 보낸 경우, 이를 CA의 잔금(Balance)으로 설정한다.
  3. EOA 계정의 잔금에서 Value 만큼을 제외한다.
  4. CA의 스토리지를 빈 값으로 초기화한다.
  5. CA의 코드를 빈 문자열의 해시값으로 초기화한다.

한번 계정을 초기화하고 나면, 트랜잭션에서 보낸 init코드를 사용해 새로운 CA를 생성할 수 있다. 이 init 코드가 실행되는 동안에 계약 생성자에 따라 CA의 스토지리를 업데이트 하거나, 다른 CA를 새롭게 생성하거나, 다른 메시지호출을 만들 수 있다.

계약을 초기화하는 이 init 코드를 실행할 때는 가스를 사용한다. 트랜잭션은 남아있는 가스보다 더 많은 가스를 소비할 수 없기 때문에, 만약 남아있는 가스를 다 사용한 경우 OOG(Out-of-Gas) 예외 처리와 함께 코드 실행이 종료된다. OOG로 인한 트랜잭션 종료가 발생하면 상태는 트랜잭션 실행 이전 상태로 돌아간다.

트랜잭션이 실패해도 트랜잭션 송신자는 소진된 가스를 환불받을 수 없다. 그러나, 송신자가 트랜잭션과 함께 보낸 이더값은 환불된다.

init코드가 성공적으로 실행되면, 마지막으로 CA 코드에 대한 비용이 지불된다. 이 비용은 스토리지 비용이며, 생성된 CA코드의 크기에 비례한다. 예외 없이 트랜잭션 생성이 완료된 경우, 미사용된 가스는 송신자에게 환불되고, 변경된 상태가 저장된다.

메시지 호출

메시지 호출을 실행하는 것은 CA 생성과 비슷하지만, 몇 가지 다른 점이 있다.

새 계정이 생성되지 않기 때문에 메시지 호출 상태에는 init 코드가 포함되지 않는다. 그러나, 입력 데이터를 가지고 있을 수는 있다. 메시지 호출은 한번 실행되면 출력 데이터를 포함한 추가적인 요소들을 가지게 될 수도 있는데, 이 추 가요소들은 뒤에 이어질 다른 실행에 필요한 데이터이다.

CA생성과 마찬가지로, 메시지 호출을 실행할 때 가스가 부족하거나, 트랜잭션이 잘못되어 종료되는 경우(스택 오버플로우가 발생하거나, 잘못된 명령어를 사용할 경우) 사용된 가스는 환불되지 않으며 직전 상태로 돌아간다.

트랜잭션 실행 모델과 EVM

다음으로 트랜잭션이 실제로 EVM에서 어떻게 동작되는지 보고자 한다.

EVM의 구성요소는 다음과 같다.

  • 스택: EVM은 비트코인 스크립트와 마찬가지로, 스택 기반 아키텍쳐를 가지고 있다. EVM 내에는 하나의 빈 스택이 있으며, 스택에 들어가는 크기는 256bits이며, 슽개의 최대 크기는 1,024 elements(개수)이다.
  • 메모리: EVM는 메모리를 가지고 있다. 메모리 에서는 Word-Address형식의 바이트 배열로 데이터를 저장한다. 메모리는 휘발성이기 때문에, 프로그램이 종료되면 메모리에 들어있는 데이터는 사라진다.
  • 스토리지: 메모리와 다르게 스토리지는 비휘발성이며 시스템 상태에 따라 유지된다. EVM은 특정한 명령어로만 접근할 수 있는 가상ROM이라는 공간에 프로그램 코드를 분리해서 저장한다.

EVM이 솔리디티 코드를 컴파일한 바이트 코드를 받으면 실행이 시작되고, 맨 처음에는 메모리와 스택은 비어있다. 프로그램 카운터(연산 실행 횟수)는 0이다.

EVM은 트랜잭션을 반복적으로 실행하며, 각 사이클에서 시스템 상태(이더리움 전역상태)와 머신 상태를 게산한다. 머신 상태는 다음과 같이 구성된다.

  • 사용 가능한 가스
  • 프로그램 카운터
  • 메모리에 들어있는 값
  • 메모리에서 활성화 된 단어의 수
  • 스택에 들어있는 값

코드가 실행되면서 스택에 있는 요소들은 왼쪽에서부터 순서대로 추가되거나 제거되며, 남은 가스에서 적절한 양의 가스가 제거되고, 프로그램 카운터가 올라간다.

매 사이클이 끝날 때 다음과 같은 케이스가 발생할 수 있다.

  1. EVM이 예외 상황을 만나 중지되고, 변경 사항은 폐기된다. (가스 부족, 명렁어가 잘못된 경우, 스택에 잘못된 요소가 들어간 경우 등)
  2. 다음 사이클을 돌아야 하는 경우
  3. 머신이 프로세스 실행을 끝낸 경우

머신이 정상적으로 실행을 마치면, 결과적으로 발생한 상태와 남아 있는 가스, 발생한 세부 상태, 결과값을 생성한다.

profile
Blockchain Developer

0개의 댓글