이더리움은 어떻게 동작하는가 ? — Part 3.

강솔
TOMAK
Published in
24 min readJul 30, 2018

이 글은 Preethi Kasireddy의 How does Ethereum work, anyway?라는 글을 번역한 것이다. 지금까지 우리 학회에서 연구했던 이더리움 프로토콜의 내용들을 잘 요약하고 있기 때문에 블록 체인에 관심을 갖는 사람이라면 한번 읽어 보면 좋을 것 같다. 만약 원문을 보고 싶다면 아래 링크를 참조하자.

How does Ethereum work, anyway?

이번 포스팅은 이더리움 프로토콜 개괄에 대한 마지막 글이다. 저번 포스팅에 이어 블록 구조와 채굴에 대해서 다루도록 하겠다. 이 부분은 조금 어려운 부분이 있기 때문에 토막 내에 올라온 Ghost protocol이나 Ethash 포스팅을 참고하도록 하자.

5. Blocks

모든 트랜잭션들은 블록 단위로 묶여 있는데, 블록 체인은 이렇게 묶여 있는 일련의 체인을 포함한다.

이더리움에서 블록은 다음의 사항들을 포함한다.

1. 블록 헤더

2. 해당 블록에 포함된 트랜잭션 집합에 대한 정보

3. 현재 블록의 ommers 블록에 대한 블록 헤더 집합

Ommers란?

ommer란 무엇인가? ommer는 현재 블록의 부모와 동일한 부모를 두고 있는 블록이다. ommer가 어디서 사용되고 왜 블록이 ommer의 해시의 리스트를 포함하고 있는지 간략하게 살펴보자.

Ommer 블록을 사용하게 된 이유는 이더리움이 처음 구상될 때 트랜잭션 처리를 더 빠르게 할 수 있도록 블록 생성 주기를 15초 이하로 설계했기 때문이다. 그러나 더 짧은 블록 생성 시간의 단점 중 하나는 채굴자들에 의해 발견되는 블록의 경쟁이 더 심화 되었다는 것이다. 이러한 경쟁 블록들은 “orphaned blocks”라고 언급되기도 한다(채굴된 블록 중 메인 체인으로 들어갈 수 없는 경우).

ommer의 목적은 orphaned 블록(비트코인에서는 stale 블록)들을 포함함으로써 채굴자에 대한 보상을 확대하는 것이다. 이를 위해서 먼저 orphaned 블록이라는 것을 증명하는 과정이 필요하다. 이 증명은 orphaned 블록 다음에 생성되는 블록이 검사를 한다.

예를 들어, 10번 블록이 마이닝이 끝나 네트워크에 전파가 되고, 이와 매우 근소한 차이로 늦게 제출한 동일한 10번 블록이 있다고 생각하자. 이 경우 11번 블록에서 늦게 제출한 블록이 orphaned 블록이라는 것을 증명해준다. 이렇게 orphaned 블록을 증명해준 11번 블록은 이에 대해서 추가로 일정 보상을 받는다.

11번 블록 생성할 때 네트워크 지연 등의 문제로 앞서 발생한 orphaned 블록을 찾지 못하고 놓칠 수도 있다. 이 경우에는 그 다음 블록인 12번 블록에서도 증명할 수 있다. 이것은 최대 7블록 내에서, 즉 17번 블록까지는 이 10번 블록에 대해 orphaned 블록이 있는지 확인하는 작업을 수행한다는 것이다. 더 자세한 참조는 수정된 GHOST 프로토콜을 참조하면 될 것이다.

보상의 경우, Ommer 블록들은 full 블록들 보다 더 작은 보상을 받는다. 게다가 빨리 발견된 orphaned 블록일 수록 보상이 크고(기존 보상 x ⅞) 7블록 차이까지 가서 증명된 orphaned 블록일수록 보상을 적게(기존 보상 x ⅛) 받는다. 그리고 이렇게 증명된 ommer 블록들의 리스트를 블록 헤더에 포함하여 이를 검증하도록 한다.

블록 헤더

다시 블록으로 돌아가도록 하겠다. 우리는 이전에 모든 블록은 블록 헤더를 포함한다고 말했는데, 정확히 헤더는 무엇일까?

블록 헤더의 구성은 다음과 같다.

1) parentHash : 부모 블록의 해시값(이는 블록 집합을 체인으로 만들어 준다)

2) ommerhash : 현재 블록의 ommers 해시값의 리스트

3) beneficiary : 이 블록 채굴에 대한 수수료를 받을 account 주소

4) stateRoot : state 트리의 루트 노드 해시값(헤더에서 state 트리가 어떻게 저장되고, 이를 통해서 라이트 클라이언트들이 state를 어떻게 쉽게 입증하는지를 다시 생각해보자)

5) transactionsRoot : 이 블록에 포함된 모든 트랜잭션을 포함한 트리의 루트 노드의 해시값

6) receiptsRoot : 이 블록에서의 모든 트랜잭션의 receipt(일종의 거래 영수증)를 포함한 트리의 루트 노드의 해시값

7) logsBloom : log 정보를 구성하는 bloom filter라는 자료 구조의 형태

8) difficulity : 블록 생성 난이도

9) number : 현재 블록의 count(제네시스 블록이 0일 때, 이 후 블록들에 대해서 이 값이 하나씩 증가)

10) gasLimit : 블록 당 현재 gas 제한량

11) gasUsed : 현재 블록에서 사용된 가스의 총량

12) timestamp : 현재 블록 시작 시 unix 타임 스탬프

13) mixHash : nonce와 더불어 현재 블록이 충분한 연산을 실행됐음을 입증하는 해시값(nonce를 조절하여 구한 유효한 범위 내의 해시값인 것 같다)

14) nonce : mixHash와 더불어 현재 블록이 충분한 연산을 행했음을 입증하는 해시값(유효한 mixHash값을 찾았을 때의 nonce값을 말한다)

특히 어떻게 모든 블록 헤더가 세 개의 트리 구조를 포함하는지를 주목하자.

1. state( stateRoot )

2. transaction( transactionRoot )

3. receipts( receiptsRoot )

이러한 트리 구조는 우리가 이전에 논의했던 머클 패트리시아 트리로만 구성된다. 추가적으로 몇 가지 분류할 만한 용어에 대해서 살펴보도록 하겠다.

Logs

이더리움은 로그들이 다양한 트랜잭션들과 메시지들을 추적할 수 있도록 한다. contract는 로그를 원하는 “events”를 정의하는 것을 통해서 로그를 확실하게 발생시킬 수 있다.

log는 다음과 같이 구성된다:

1) logger의 account 주소

2) 이 트랜잭션에 의해 수행되는 다양한 event들을 표현하는 일련의 topic들

3) 이러한 event들과 관련된 데이터

log는 수많은 정보를 효과적으로 저장하기 위해서 bloom filter로 저장된다.

Transaction receipt

헤더에 저장된 로그들은 트랜잭션 receipt에 포함된 로그 정보로부터 나온다. 상점에서 무언가를 살 때 나오는 영수증처럼, 이더리움은 모든 트랜잭션마다 receipt를 발행한다. 우리가 예상할 수 있듯이, 각 각의 영수증은 트랜잭션에 대한 어떤 정보를 포함하는데, 이 때 포함되는 정보는 다음과 같다.

1) 블록의 number(블록의 번호)

2) 블록의 해시값

3) 트랜잭션 해시값

4) 현재 트랜잭션에서 사용된 gas

5) 현재 블록 내 트랜잭션들의 누적 gas량(해당 영수증을 발행한 트랜잭션까지 포함된)

6) 현재 트랜잭션을 실행하면서 생성된 로그들

7) 기타 등

Block difficulty

블록의 난이도는 블록들을 생성 주기의 일관성을 강화하기 위해 사용된다. 제네시스 블록은 131,072의 난이도를 가졌고, 특별한 공식이 매 블록 생성 이후 난이도를 계산한다. 만약 특정 블록이 이전 블록보다 더 빨리 생성된다면, 이더리움 프로토콜은 블록의 난이도를 증가시킨다.

블록의 난이도는 PoW 알고리즘을 사용하며 블록을 채굴할 때 계산될 해시값에 필요한 nonce에 영향을 준다,

블록의 난이도와 nonce 사이의 관계는 수학적으로 다음과 같다.

Hd는 난이도를 의미하며, 해당 난이도에서의 조건을 충족하는 nonce값을 찾는 유일한 방법은 모든 가능성들을 일일이 대입하는 PoW 알고리즘을 사용하는 것뿐이다. 따라서 해결책을 찾는데 걸리는 예상 시간은 난이도와 비례한다. 난이도가 더 높을수록, nonce를 찾는 것은 더 어려울 것이고 블록을 증명하는 것 또한 어려울 것이며, 이에 따라 새로운 블록을 찾는데 걸리는 시간도 더 오래 걸릴 것이다. 그러므로블록의 난이도를 조절하여 프로토콜은 블록 생성 시간을 조절할 수 있다.

반대로 만약에 증명 시간이 점 점 오래 걸린다면, 프로토콜은 난이도를 낮출 것이다. 이런 식으로 변함없는 생성 속도(약 15초 정도)를 유지하기 위해서 난이도를 조절할 수 있다.

6. 트랜잭션 실행

이제 이더리움 프로토콜의 가장 복잡한 부분인 트랜잭션 실행에 대해서 다루도록 하겠다. 한 사용자가 이더리움 네트워크로 처리되어야 할 트랜잭션을 보낸다고 생각해 보자. 사용자의 트랜잭션을 포함하고 있는 이더리움의 state는 어떻게 변할까?

먼저 모든 트랜잭션들은 실행하는데 필요한 모든 초기 조건들을 충족해야 한다. 그 조건들은 아래와 같다.

  1. 트랜잭션들은 적절한 RLP 형식을 가져야 한다.

“RLP”는 Recursive Length Prefix를 의미하고 이는 2진의 nested 배열을 암호화하는데 사용되는 데이터 형식이다. RLP는 이더리움이 객체들을 정렬(serialize)할 때 사용하는 포맷이다. 이 부분은 단순하게 데이터를 처리하기 위해서 통일된 포맷을 갖도록 인코딩 한다고생각하면 편하다. 혹시나 추가적인 내용이 필요하다면 이 링크를 참조하여라.

2. 트랜잭션 송신자의 유효한 서명

유효한 트랜잭션 nonce(블록 헤더가 아닌 account의 nonce). account으로부터 보내진 트랜잭션의 개수를 nonce라고 한다는 것을 기억해라. 유효하기 위해서 트랜잭션 nonce는 반드시 송신자 account의 nonce와 일치해야 한다. 여기서 말하는 nonce는 쉽게 말해 트랜잭션 송신자가 지금까지 몇개의 트랜잭션을 보냈는 지를 기억하는 변수다.

트랜잭션의 gas limit은 트랜잭션이 사용할 intrinsic gas의 양보다 크거나 같아야 한다.

이 때, intrinsic gas는 다음을 포함한다.

(1) 트랜잭션을 실행하기 위해 미리 정의된 21,000 gas 비용

(2) 트랜잭션과 함께 보낼 데이터에 대한 gas 요금(0인 데이터나 코드에 대해서 바이트 당 4 gas, 0이 아닌 데이터나 코드에 대해서 바이트 당 68 gas)

송신자의 계좌 잔금은 송신자가 지불해야 할 “upfront” gas 요금을 내기에 충분해야 한다. upfront 가스 요금 계산은 간단하다: 먼저 gas 비용 최대값을 계산하기 위해서 트랜잭션의 gas limit과 트랜잭션의 gas price를 곱한다. 이렇게 구한 최대 비용과 송신자에서 수령자(recipient)로 전송될 총 이더량을 더한다.

만약에 트랜잭션이 유효성을 위한 위의 모든 조건을 충족한다면 우리는 다음 단계로 넘어갈 수 있다.

우선 우리는 송신자의 잔액에서 실행을 위한 upfront 비용을 빼고, 현재의 트랜잭션(gas송금)에 대해서 송신자의 account의 nonce를 1 증가시킨다. 이 때, 우리는 트랜잭션에 대한 총 gas limit에서 사용된 intrinsic 가스를 빼서 남아 있는 가스를 계산할 수 있다.

다음, 트랜잭션을 실행한다. 트랜잭션이 실행되는 동안, 이더리움은 “substate”라는 것을 추적한다. 이러한 substate는 트랜잭션이 실행되는 동안 바뀌는 정보들을 저장하는 임시 공간으로서 다음의 사항들을 포함한다.

1) self-destruct set : 트랜잭션 완료 후 삭제될 account의 집합

2) log series : 가상 machine의 코드 실행에 대한 아카이브 및 인덱싱 가능한 체크 포인트

3) Refund balance : 트랜잭션 이후 송신자 account으로 환불될 양. 이더리움에서의 저장소가 어떻게 가스를 소모하고, 저장소를 비울 때 송신자가 어떻게 환불을 받는지 기억하는가? 이더리움은 refund counter를 사용하여 이를 추적한다. refund counter는 0에서 시작하고 컨트랙트가 저장소에서 무언가를 삭제할 때마다 증가한다.

다음으로, 트랜잭션이 처리되는데 필요한 다양한 연산을 살펴보도록 하겠다.

일단 트랜잭션에 의해서 필요한 모든 단계들이 처리된다면, 유효하지 않은 state가 없다고 가정하고 송신자에게 환불될 사용되지 않은 gas의 양을 결정짓는다. 그리고 substate에 내용에 따라 state를 수정하여 트랜잭션을 완료한다. 이때사용되지 않은 gas의 경우, 송신자는 우리가 위에서 묘사했던 refund balance로부터 일정 금액을 환불 받는다.

일단 송신자가 환불을 받는다면 다음의 과정을 진행한다.

(1) 소모한 가스만큼의 이더가 채굴자에게 주어진다.

(2) 트랜잭션에 의해 사용된 gas를 블록의 gas counter에 더한다(이는 블록 내 트랜잭션의 총 gas 소모량을 추적하고 블록을 증명할 때 사용한다).

(3) 만약 self-destruct set에 account이 존재한다면 모두 삭제한다.

마지막으로, 트랜잭션에 의해 생성되는 새로운 state와 로그의 집합을 처리하면 된다. 지금까지 우리는 트랜잭션 실행의 기본을 다뤘고, contract 생성 트랜잭션들과 메시지 호출 사이의 차이점을 살펴보도록 하자.

contract account 생성

이더리움에는 contract account과 externally owned account두 가지 종류의 account가 있다는 사실을 기억하자. 우리가 보통 contract 생성 트랜잭션을 말할 때는 새로운 contract account을 만든다는 것을 의미하며, contract account는 contract 생성 트랜잭션을 통해 만들 수 있다.

새로운 contract account을 만들기 위해서, 우리는 먼저 특별한 공식을 사용하는 새로운 account의 주소를 선언해야 한다. 그러고 나서 우리는 다음의 과정에 따라 새로운 account을 생성할 수 있다.

(1) nonce(account의)를 0으로 설정한다.

(2) 만약에 송신자가 account 생성 트랜잭션을 통해 이더를 같이 보냈다면 account의 잔금을 그 value만큼 설정한다.

(3) 송신자의 잔금에서 value만큼을 뺀다.

(4) contract account의 storage를 초기화한다.

(5) contract의 codeHash(externally owned account에서 필요한 인자)를 빈 문자열의 해시값으로 설정한다.

일단 우리가 contract account 생성 트랜잭션을 보내게 되면, 이와 함께 보내진 init 코드를 사용하여 contract account가 생성된다.(init 코드에 대해서는 위의 트랜잭션과 메시지 색션을 통해 다시 확인하자). init 코드가 실행되는 동안 일어나는 일은 다양하다. contract의 생성자에 따라 account의 storage를 갱신하거나 다른 contract account을 생성하거나 다른 메시지 콜을 보내는 등의 여러 동작들을 한다.

contract를 시작시키기 위해 코드가 실행될 때 gas를 소모한다. 당연하게도 contract 생성 트랜잭션은 남아 있는 gas의 양보다 더 많은 gas를 사용할 수 없다. 만약 이러한 일이 발생한다면, out of gas(OOG. 가스 부족) 오류가 발생하고 종료되며, state는 즉시 이전 트랜잭션의 지점으로 돌아간다. 송신자는 이때 소모된 gas에 대해서는 환불 받지 못한다.

그러나 만약에 송신자가 트랜잭션으로 이더를 송금했다면, 이더는 비록 contract account이 실패했을지라도 환불될 것이다.

만약 init 코드가 성공적으로 실행된다면, 마지막으로 contract 코드에 대한 account 비용이 지불된다. 이는 storage 비용이며 생성된 contract 코드의 크기에 비례한다. 만약에 이 마지막 비용을 지불할 gas가 충분하지 않는다면, 트랜잭션은 다시 OOG 오류를 선언하고 중단된다.

만약에 모든 과정에서 오류 없이 잘 생성된다면 남아 있는 사용되지 않은 gas는 본래 트랜잭션의 송신자에게 환불되고, 변경된 state는 저장된다.

메시지 호출

메시지 호출의 실행은 contract account의 실행과 비슷하지만 약간의 차이가 있다.

함수 호출은 account 생성을 위한 init 코드를 포함하지 않는다. 또한 트랜잭션 송신자가 함수에 이용될 입력값을 전달할 수 있다. 잘 실행되면 함수의 출력값과 추가적인 요소를 이용할 수 있다.

contract 생성과 마찬가지로 메시지 호출 실행이 OOG 오류나 트랜잭션이 유효하지 않아서(스택 오버플로우, 유효하지 않은 점프나 지시) 중단된다면 호출자에게 소모됐던 gas가 환불되지 않는다. 대신에 사용되지 않은 gas는 환불되고, 이전 state로 돌아간다.

7. Excution model

지금까지 우리는 트랜잭션이 실행되고 종료되는 과정에서 일어나는 일련의 단계들을 배웠다. 이제 어떻게 트랜잭션이 VM에서 실행되는지를 살펴보도록 하자.

트랜잭션 처리를 실제로 다루는 프로토콜의 부분은 EVM이라고 불리는 이더리움의 가상 machine이다. EVM은 앞에서 정의된 것처럼 튜링 완전 가상 machine이다. EVM의 유일한 한계는 본래의 튜링 완전 machine과 달리 gas에 종속되어 있다는 점 때문에 실행가능한 연산은 제공되는 gas의 양에 의해 제한된다.

EVM은 stack에 기반한 구조를 가진다. stack machine은 일시적인 값을 보관하기 위해서 last in, first-out이라는 스택을 사용하는 컴퓨터이다.

EVM에서 각 각의 스택의 항목들의 사이즈는 256 비트이며, 스택은 최대 1024개의 항목들을 가질 수 있다.

EVM은 메모리를 가지고 있으며, item들은 word-address 형태의 바이트 배열로 저장된다. 또한 메모리는 변할 수 있으며 이는 영원하지 않다는 것을 의미한다.

그리고 EVM은 Storage를 가진다. 메모리와 달리, Storage는 변하지 않고 시스템 state의 일부로 유지된다. EVM은 프로그램 코드를 특별한 지시들을 통해서만 접근이 가능한 가상의 ROM에 개별적으로 저장한다. 이런 식으로 EVM은 메모리 또는 저장소에 프로그램 코드를 저장하는 방식에서 전형적인 폰 노이만 구조와 다르다.

EVM은 또한 “EVM 바이트 코드”라는 자체의 언어를 가지고 있다. 우리와 같은 프로그래머들이 이더리움에서 구동되는 스마트 contract을 작성할 때, 우리는 보통 솔리디티와 같은 high-level 언어로 작성한다. 이를 EVM 바이트 코드로 컴파일하여 EVM이 이해할 수 있도록 한다.

이제 실행에 대해서 이야기해보자. 특정 연산을 실행하기 전에, 프로세서는 다음의 정보들을 이용할 수 있다.

  • 시스템 state
  • 연산을 위해 남아 있는 gas
  • 실행 중인 코드를 소유하고 있는 account의 주소
  • 실행중인 트랜잭션의 송신자 주소
  • 실행할 코드를 지닌 account의 주소(송신자와는 다를 수 있다 — 송신자가 다른 account의 코드를 실행하는 트랜잭션을 보낼 수 있다)
  • 실행중인 트랜잭션의 gas price
  • 실행을 위한 입력 데이터
  • 현재 실행의 부분으로서 이 account으로 보내진 value값(wei로 표현된)
  • 실행될 machine 코드
  • 현재 블록의 헤더
  • 현재 메시지 호출 또는 contract 생성 스택의 깊이

실행에 대해서 시작하면, 메모리와 스택은 비어 있고 프로그램 카운터는 0이다.

(PC-프로그램 카운터는 0이고, 스택, 메모리, 저장소 모두 비어 있다)

EVM은 반복적으로 트랜잭션을 실행하고 각 루프에 대한 시스템 state와 machine state를 연산한다. 시스템 state는 단순히 이더리움의 글로벌 state를 말하며, machine state는 다음과 같이 구성된다.

1) 이용 가능한 gas

2) 프로그램 카운터

3) 메모리 내용

4) 메모리 상 활성화된 단어(word)들의 수

5) 스택의 내용

이 때 스택의 항목들은 가장 왼쪽에서부터 더하고 제거되고, 각 사이클마다, 적절한 양의 gas만큼 남아 있는 gas가 감소하고 프로그램 카운터는 증가한다. 그리고 각 루프의 마지막에 세 가지 케이스가 존재한다.

(1) machine이 예외(오류)적인 state에 도달한다(불충분한 gas, 유효하지 않은 지시, 유효하지 않은 스택 items, 1024를 초과하는 stack의 items, 유효하지 않은 JUMP/JUMPI 주소 등). 이 경우에는 동작이 중지되며, 모든 변경 사항들은 취소된다.

(2) 순서대로 다음 루프를 처리한다.

(3) machine이 정상적으로 실행을 멈춘다(실행 프로세스의 끝에 도달).

실행이 예외적인 state로 들어서지 않고 예상되었거나 정상적으로 멈춘다고 가정하면, machine은 결과적으로 발생한 state와 실행 후 남아 있는 gas, 발생한 세부 state(substate), 그리고 결과적으로 발생한 출력을 발생시킨다.

우리는 지금 이더리움의 가장 복잡한 부분들 중 하나를 다뤘다. 비록 이 부분이 완전히 이해되지 않았을지라도 괜찮다. 아주 깊은 수준에서 일하지 않는다면 핵심적인 실행의 세부 사항들을 이해할 필요는 없다.

어떻게 블록은 완결되는가?

마지막으로, 어떻게 많은 트랜잭션들의 블록이 완결되는지를 살펴보자.

“완결된다”라고 말할 때, 새로운 블록인지 기존에 존재하던 블록인지에 따라 이는 두 가지를 의미한다. 만약 새로운 블록일 경우, 이 블록을 채굴하는데 필요한 프로세스를 말하며, 기존의 블록일 경우 해당 블록을 입증하는 프로세스를 말한다. 그리고 각 각의 경우 모두 블록이 완결되기 위해서는 4가지 조건이 필요하다.

조건 1. ommers를 입증해라(또는, 만약 채굴한다면 알아내라).

블록 헤더에 들어 있는 각 각의 ommer 블록들은 유효한 헤더이며 현재 블록의 6 세대 안에 존재해야 한다.

조건 2. 트랜잭션들을 입증해라(또는, 만약 채굴한다면 알아내라).

블록의 gasUsed 숫자는 블록 내 트랜잭션들에 의해 사용된 누적 gas량과 동일해야 한다(트랜잭션이 실행될 때, 우리는 블록의 gas counter를 추적했고, 블록 내 모든 트랜잭션들에 의해 사용된 총 gas량을 계산했던 것을 기억해라).

조건 3. 보상을 적용해라(채굴하는 경우).

beneficiary 주소는 블록 채굴 당 5 이더를 보상받는다. (Ethereum proposal EIP-649에 따르면 5 이더의 보상은 곧 3 이더로 감소할 것이다). 게다가 각 각의 ommer의 경우, 현재 블록의 benificiary는 현재 블록 보상의 1/32를 추가적으로 받는다. 마지막으로 ommer 블록들의 benificiary는 또한 특정 양을 보상 받는다(이를 계산하는 특별한 공식이 존재한다).

조건 4. state와 nonce를 증명해라(만약 채굴한다면 valid를 연산해라)

모든 트랜잭션과 이로 파생되는 state 변화가 적용되는 것을 확실히 하고, 블록 보상이 마지막 트랜잭션의 결과가 state에 적용된 후로 새로운 블록을 정의해라. 헤더에 저장된 state 트리에서 마지막 state를 점검하여 이를 증명한다.

8. Mining proof of work

위의 Block 섹션에서는 간략하게 블록 난이도의 개념에 대해서 설명했었는데, 블록 난이도에 의미를 부여하는 알고리즘을 작업 증명 방식(PoW)라고 한다.

이 때 이더리움의 PoW 알고리즘을 Ethash라고 부르며, 이 알고리즘은 다음과 같이 정의된다.

여기서 m은 mixHash이고, n이 nonce를 의미한다. 그리고 Hn(첫 번째 인자)은 새로운 블록의 헤더(연산 되어야 하는 nonce와 mixHash를 제외한)이며, 두 번째 Hn은 블록 헤더의 nonce를 가르킨다. 마지막으로 d는 DAG라는 거대한 데이터 집합을 말한다.

Block 섹션에서 우리는 블록 헤더에 존재하는 다양한 항목들에 대해서 말했었는데, 그 중 mixHash와 nonce에 대해서 다시 한번 살펴보겠다.

mixHash : nonce와 결합될 때, 이 블록이 충분한 연산을 동반하고 있음을 증명하는 해시값

nonce : mixHash와 결합하여 이 블록이 충분한 연산을 수행했음을 증명하는 해시값

PoW 함수는 이 두 가지 항목들을 평가하기 위해 사용된다.

PoW 함수를 mixHash와 nonce값을 정확하게 계산하는 과정은 다소 복잡하기 때문에 다른 포스팅으로 분리해서 더 깊게 다루도록 하겠다. 일단 이해하기 쉽게 설명하자면 이는 다음과 같이 동작한다.

먼저 각 각의 블록에 대해서 seed가 계산된다. 이 seed는 매 epoch마다 다르며, 이 때 각 epoch는 3만개의 블록이 생성되는 정도의 시간을 의미한다. 첫 번째 epoch에서 seed는 32바이트 크기 0의 해시값을 의미한다. 그 다음 epoch부터 seed는 이전 seed hash의 해시값을 사용하고, 노드는 이 seed를 이용하여 cache라는 임의의 난수를 계산한다.

이 때, cache는 이전에 우리가 다뤘던 light node에서 특정 트랜잭션을 증명하는데 필요하다는 것도 기억해두자.

cache를 사용하면, 노드는 DAG이라는 데이터 집합을 생성할 수 있는데, 이 데이터 집합에 있는 각 각의 항목들은 cache로부터 임의로 랜덤하게 선별된 적은 수의 항목들에 의존한다. 채굴자가 되기 위해서는 반드시 DAG이라는 데이터 집합을 생성해야하며, 모든 full 클라이언트들과 채굴자들은 이 데이터 집합을 저장해야한다(참고로 이 데이터의 크기는 시간에 비례하여 증가한다).

채굴자는 이 데이터 집합의 일부를 랜덤하게 추출하고 이를 mixHash로 함께 해시하기 위해 함수에 넣는다. 그리고 목표로 하는 nonce값 아래로 출력이 나올 때까지 반복적으로 mixHash를 생성한다. 출력값이 이 조건을 충족할 때, nonce는 유효하다고 여겨지고 블록은 체인에 추가될 수 있다. Ethash에 대한 추가적인 내용은 이 링크를 참조하자.

안전성을 위한 채굴

전체적으로 PoW의 목적은 암호학적으로 안전하게 특정한 연산량이 어떤 출력값(nonce)를 발생시키는데 사용되었음을 증명하는 것이다. 이는 모든 가능성을 하나 하나 해보는 것 외에는 적절한 범위 이하의 nonce를 찾는 방법이 없기 때문에 가능하다. 반복적으로 해시 함수를 적용하여 생성한 출력값들은 균일한 분포를 가지며, 평균적으로 특정한 nonce를 찾는데 필요한 시간은 난이도에 달려 있다고 할 수 있다. 난이도가 높아질수록 nonce를 찾는데 더 많은 시간이 걸릴 것이며, 이런 방식으로 PoW 알고리즘은 블록 체인의 안정성을 강화하기 위해 사용되는 난이도라는 개념에 의미를 부여하고 있다.

블록 체인 안정성이 의미하는 것은 무엇인가? 이는 간단하다. 우리는 모든 사람이 믿을 수 있는 블록 체인을 생성하길 바란다. 이 포스팅에서는 이전에 논의했듯이, 만약 하나의 체인 이상이 존재한다면, 사용자들이 합리적으로 어떤 체인이 유효한지를 결정할 수 없기 때문에 블록 체인은 사용자들의 신뢰를 잃어 버릴 것이다. 사용자 집단이 블록 체인에 저장되는 하나의 유효한 state를 받아들이기 위해서는 사람들이 신뢰할 수 있는 하나의 canonical 체인(main chain)이 필요하다.

PoW 알고리즘이 그러한 역할을 하는 것이다. 이는 공격자가 과거의 특정 부분들을 덮어쓰거나 분기를 유지하기 매우 어렵게 만들어 특정 블록 체인이 미래에도 유효하게(canonical) 남아 있도록 보장한다(트랜잭션들을 삭제하거나 가짜 트랜잭션을 생성하는 것을 방지한다). 그들의 블록이 처음에 입증되기 위해서는 공격자는 네트워크에 있는 다른 누구보다도 nonce를 빠르게 계속해서 해결해야 한다. 이를 통해서 네트워크가 그들의 체인이 가장 무거운 체인으로 믿을 수 있게 해야 한다(이는 이전에 언급한 GHOST 프로토콜의 원칙에 기반하고 있다). 즉 공격자가 네트워크 채굴 파워의 절반 이상을 가지고 있지 않는다면(51% 공격) 불가능하다.

부의 분배로서의 채굴

안전한 블록 체인을 제공하는 것을 넘어서, PoW는 이 안정성을 제공하기 위해서 연산 능력을 사용한 사람들에게 부를 분배하는 방식이기도 하다. 블록을 채굴하는 것에 대해서 보상을 받는 것을 기억해라. 이 때 보상은 3가지의 형태로 주어진다.

1. 승리 블록에 대해서 3 이더의 고정 블록 보상

2. 블록에 포함된 트랜잭션에 의해서 블록 내 소모된 gas 비용

3. 블록의 부분으로 ommer들을 포함한 것에 대한 추가적인 보상

안정성과 부의 분배를 위한 PoW 합의 구조가 장기적으로 안전하게 보장받기 위해서, 이더리움은 두 가지 특성을 주입시키려고 노력하고 있다.

  • 가능한 많은 사람들이 접근할 수 있도록 한다. 즉 사람들이 알고리즘을 동작시키기 위해서 특별하거나 흔하지 않은 하드웨어가 필요 없다. 부의 분배 모델은 누구든 이더 보상을 얻기 위해서 자신의 연산 능력을 제공할 수 있다.
  • 어떤 하나의 노드가 불균형의 이윤을 가져갈 가능성을 감소시킨다. 불균형의 이윤을 가져가는 노드는 공식 블록 체인을 결정하는데 많은 영향력을 끼친다는 것을 의미한다. 이는 네트워크 안정성을 감소시키기 때문에 문제가 된다.

이 문제를 완화시키기 위해 이더리움은 PoW 알고리즘(Ethash)이 점차 메모리에 부담을 주도록 만들었고, nonce를 계산하기 위해서 많은 양의 메모리와 bandwidth를 필요로 하도록 알고리즘을 설계했다. 많은 메모리 조건은 컴퓨터가 동시에 여러 nonce들을 발견하도록 병렬적으로 메모리를 사용하는 것을 어렵게 만들었고, 높은 bandwidth 조건은 엄청 빠른 컴퓨터가 동시에 여러 nonce를 발견하는 것을 어렵게 만들었다. 이는 중앙화에 대한 위험을 감소시키고 증명하는 노드들이 더 많은 수준에서 경쟁할 수 있도록 하였다.

여기서 주목할 만한 점은 이더리움은 지금 PoW 합의 구조에서 PoS라고 불리는 구조로 변경하는 중이다. 이는 우리 학회에서 앞으로 다뤄야할 문제이며, 차후에 연구의 진행에 따라 이 부분에 대해서도 계속해서 포스팅을 해나가도록 하겠다.

--

--