Go로 블록체인 만들기 #3

Min Seo Park
CAU_CLink
Published in
14 min readAug 24, 2020

Go 로 블록체인 만들기 시리즈 3번째

#3 Persistence & CLI (영속성과 명령어 인터페이스)

이번 Go로 블록체인 만들기 3편은 다음과 같이 구성되어있다.

  • Introduction
  • Bolt DB
  • 데이터 베이스 구조
  • 직렬화(Serialization)
  • 영속성(Persistence)
  • 명령어 인터페이스(CLI)
  • 결론

Introduction

지금까지, 채굴이 실제로 실시될 수 있게 pow 시스템을 가진 블록체인을 구현하였다. 조금씩 완전체에 가까워지고 있기는 하지만, 여전히 부족한 부분들이 있다. 이번에는 블록체인을 데이터베이스에 저장하는 부분을 구현하고, 블록체인을 작동시킬 수 있는 간단한 cli(명령어 인터페이스)를 구현할 것이다. 본질적으로 블록체인은 분산 데이터베이스이다. 이번에는 “분산”부분을 생략하고 “데이터베이스”부분에 집중하려고 한다.

데이터 베이스 선택

아직까지, DB는 구현되어 있지 않다. 대신 프로그램을 실행할 때마다 생성되는 블록을 메모리에 저장하였다. 이 체인은 재사용과 공유가 안되기에 디스크에 저장을 해야한다.

그렇다면 어떤 종류의 DB가 필요할까? 사실 어떤 것이든 상관없다. 비트코인 백서에서도 DB의 종류에 대한 언급은 전혀 없다. 따라서, 이는 개발자의 마음대로 진행하면 된다. 사토시 나카모토가 배포하고 수많은 다른 비트코인 구현의 참고가 되는 비트코인 코어는 현재 LevelDB 를 사용하고 있다. 여기에서 우리가 사용할 것은 Bolt DB이다.

<Part 3에서 구현할 6가지 파일들>

BoltDB

BoltDB를 사용하는 이유는 아래와 같다:

1. 상당히 간단하다.
2. Go 언어로 구성되어있다.
3. 서버가 따로 필요없다.
4. 우리가 원하는 자료구조를 만들 수 있다.

BoltDB’의 Github에 있는 README 문서에는 아래와 같이 적혀있다:

Bolt는 순수 Go언어로 작성된 key/value이며, Howard Chu의 LMDB 프로젝트에 의해 영감을 받아 시작되었다. Postgres나 MySQL와 같이 데이터베이스 서버가 아닌 프로젝트의 목표는 간단하고, 빠르고 믿을만한 데이터베이스를 만드는 것이다. Bolt는 low-level piece의 기능을 목표로 만들어졌기에, 간단함이 최우선이다. API는 작고 오직 값을 설정하고 받아오는 것에만 집중하였다. That’s it.

딱 우리의 상황에 맞는다. 잠시 살펴보자.

BoltDB는 키/값 쌍 형식으로 저장한다. 즉, SQL RDBMS와 같이 테이블, 줄, 열 등이 있지 않다는 것이다.(Go언어의 map과 같다.) 비슷한 쌍들이 묶이는 형식인 키-값 쌍은 bucket에 저장된다.(RDBMS의 테이블과 비슷하다.) 그러므로, 값을 얻기 위해서는 bucket과 키 값을 알아야만 한다.

BoltDB의 한가지 중요한 특징은 데이터 타입이 없다는 것이다 : 키와 값은 모두 바이트 배열이다.(byte arrays) 이 안에 Go 구조체(특히 Block)를 저장할 것이기에, 직렬화를 할 필요가 있다. i.e. Go의 struct를 byte array로 전환하거나 byte array에서 struct로 전환하는 것을 구현하기. 이를 위하여 encoding/gob 를 사용할 것이다. 하지만, JSON, XML, Protocol Buffers, 다른 것들도 사용될 수 있다. encoding/gob 를 사용하는 이유는 간단하고 표준 Go 라이브러리이기 때문이다.

데이터 베이스 구조

논리를 구현하기 전에, 먼저 DB에 정보를 저장하는 방법을 결정해야 한다. 그리고 이를 위해, 비트코인 코어에서 하는 방법을 알아보려고 한다.

간단하게 말하면, 비트코인 코어에서는 2개의 “buckets”이 데이터를 저장하는데 사용된다 :

1. blocks 는 체인에 속한 블록을 설명하는 메타데이터를 가지고 있다.
2. chainstate 은 체인의 상태를 저장한다. 체인의 상태라하면, 모든 UTXO와 메타데이터 일부분이다.

또한, 블록들은 디스크에 별도의 파일로 저장된다. 기능의 효율성을 위해서이다 : 1개의 블록을 읽을 때 전체를 메모리에 들여올 필요는 없다. 이 부분은 구현하지 않는다.

blocks에는, key -> value 이 아래와 같이 작동한다:

1. 'b' + 32-byte 블록 해시 -> 블록 번호
2. 'f' + 4-byte 파일 번호 -> 파일 정보
3. 'l' -> 4-byte 파일 번호: the last block file number used
4. 'R' -> 1-byte boolean: whether we're in the process of reindexing
5. 'F' + 1-byte flag name length + flag name string -> 1 byte boolean: various flags that can be on or off
6. 't' + 32-byte 거래 해시 -> 거래 인덱스 기록

chainstate 에서는, key -> value 쌍은 다음과 같다 :

1. 'c' + 32-byte 거래 해시 -> unspent transaction output record for that transaction
2. 'B' -> 32-byte 블록 해시: the block hash up to which the database represents the unspent transaction outputs

(더 자세한 설명은 여기서 볼 수 있다.)

아직 거래부분은 구현되지 않았기에, block의 버킷만 사용할 것이다. 또한, 이미 말한 것처럼, 블록들을 각자 별도가 아닌 전체 DB를 하나의 파일로 저장할 것이다. 그렇기 때문에, 파일 번호와 연관을 지을 필요는 따로 없게 된다. 아래는 우리가 사용할 key -> value가 있다:

1. 32-byte 블록해시 -> 블록 구조(직렬화)
2. 'l' -> 체인 내 마지막 블록의 해시값

이것들이 영속성 메커니즘을 구현하는데 있어서 필요한 것들이다.

직렬화(Serialization)

이전에 말한 것처럼, BoltDB 값은 오직 []byte 타입으로만 존재하나 우리는 DB에 Block 구조를 저장하고 싶다. 구조체를 직렬화에 encoding/gob 를 사용한다.

Block의 직렬화 방법을 구현하자. (오류 처리 과정은 생략한다.):

<code 3_1 : 블록 구조의 직렬화>

이 부분은 꽤나 간단하다 : 일단, 직렬화된 데이터를 저장하는 버퍼를 선언한다. gob 인코더를 초기화 하고 블록을 인코드 한다. ; 결과는 바이트 배열(byte array)로 반환된다.

다음으로, 입력값으로 받은 바이트 배열을 Block 으로 전환하는 역직렬화 기능을 구현한다. 메소드는 아니고 개별 함수로 구현했다 :

<code 3_2 : 블록 구조의 역직렬화>

자 직렬화는 이제 끝이다!

영속성(Persistence)

NewBlockchain 함수와 시작한다. 현재로서는, Blockchain 의 새로운 인스턴스를 생성하고 제네시스 블록을 이에 추가한다. 아래에 그 과정이 있다 :

1. DB 파일을 연다.
2. 블록체인이 저장되어 있는지 확인한다.
3. 만약 블록체인이 있다면 :
새로운 Blockchain 인스턴스를 생성한다.
Blockchain 인스턴스의 tip이 DB의 마지막 블록에 향하도록 설정한다.
4. 만약 블록체인이 없다면:
제네시스 블록을 생성하라.
DB에 저장하라.
제네시스 블록의 해시를 마지막 해시로 저장한다.
제네시스 블록을 tip pointing 하는 새로운 Blockchain 인스턴스를 생성한다.

코드는 아래와 같다 :

<code 3_3 : 새로운 블록체인이 만들어지는 과정>

코드를 부분으로 쪼개어 하나씩 살펴보겠다.

<code 3_4 : bolt DB 파일을 여는 과정>

BoltDB 파일을 여는 가장 정석적인 방법이다. 해당 파일이 없어도 오류를 반환하지 않는다.

<code 3_4 : db.Update 과정을 사용한 것>

BoltDB에서, 데이터베이스의 작동은 거래내에서 일어난다. 거래에는 2가지 종류가 있다 : read-only 와 read-write이다. 여기서 read-write 거래 (db.Update(…))를 열었다. 그래야 DB에 제네시스 블록을 넣을 수 있다.

<code 3_5 : bucket을 취하는 과정과 l키를 업데이트 하는 과정>

기능의 핵심이다. 블록을 저장하고 있는 버킷을 취한다. : 존재한다면, 그곳에서 l키를 읽어온다; 만약에 버킷이 없다면, 제네시스 블록을 생성하고 버킷을 만들어 블록을 그 안에 저장한다 그리고 체인의 마지막 블록해시에 저장되어 있는 l 키를 업데이트 한다.

또한, Blockchain을 만드는 새로운 방법을 살펴봐야한다 :

<code 3_6 : 새로운 블록체인을 만드는 과정>

더 이상 모든 블록들을 이 안에 저장하지 않는다, 대신에 체인의 tip을 저장한다. 또한, DB 커넥션도 저장해놓는다. 왜냐하면, 필요할 때 열거나 프로그램이 작동하는 내내 열어야 할 수도 있기 때문이다. 그렇게 되면, Blockchain 구조는 이제 아래와 같게 된다 :

<code 3_7 : tip이 추가된 블록체인의 구조>

다음으로 수정할 곳은 AddBlock 이다 : 이제는 체인에 블록을 더하는 것이 단순히 배열에 요소를 더하는 것처럼 간단하지 않다. 지금부터는 블록들을 DB에 저장할 것이다 :

<code 3_8 : 추가 구현한 AddBlock>

한줄씩 차례대로 살펴보자 :

<code 3_9 : 마지막 블록의 해시값을 가져오는 과정>

이는 BoltDB 거래의 또 다른 타입(read-only)이다. DB에서 마지막 블록 해시값을 가져와 새로운 블록을 채굴하는데에 사용한다.

<code 3_10 : 채굴 후 DB와 l 값 업데이트 하는 과정>

블록을 채굴한 후에는, 직렬화된 형태로 DB에 저장하고 새로운 블록의 해시값을 저장한 l 키를 업데이트 한다.

끝! 그렇게 어렵지는 않았다.

블록체인 검색(Inspecting blockchain)

이제 모든 블록들은 데이터베이스에 저장된다, 이제 우리는 블록체인을 언제든지 열람하거나 새로운 블록을 더할 수 있다. 그러나 이를 구현하고 나면, 괜찮은 특징을 하나 잃게 된다: 더 이상 블록체인 블록들을 출력할 수 없게 된다. 왜냐하면 블록들이 더 이상 배열로 저장되지 않기 때문이다. 이 부분을 수정해보자!

BoltDB는 버킷에 있는 모든 키들을 반복적으로 호출할 수 있다. 키들은 byte 기반으로 소트 되어있지만, 우리는 블록체인에 담긴 순서대로 호출하고 싶다. 또한, 모든 블록을 메모리에 load 하면 부담이 갈 수 있기 때문에 우리는 하나씩 읽을 예정이다. 이를 위해서 (블록체인 반복자)blockchain iterator를 만들려고 한다 :

<code 3_11 : 블록체인 반복자>

반복자는 블록체인에서 블록들을 반복적으로 호출할 때마다 생성되고 현재 반복자에 블록 해시에 저장하며 DB에 연결한다. 후자의 기능 때문에 반복자는 블록체인과 논리적으로 연결되어 있으며,(DB와의 연결점을 저장하는 블록체인 인스턴스이다) 블록체인 메소드에서 생성된다.

<code 3_12 : 반복자 함수>

반복자는 일단 블록체인의 tip을 가리킨다. 그러므로 블록은 위부터 아래까지 즉, 새로운 것부터 오래된 것들 순으로 호출된다. 사실, tip을 선택한다는 것은 블록체인에서는 “투표”를 의미한다. 블록체인은 다수의 브랜치들을 가질 수 있다, 그리고 그 중 가장 긴 것이 main으로 여겨진다. tip을 취한 후에는 우리는 전체 블록체인을 재구성하여 그것의 길이와 형성에 필요한 work들을 찾을 수 있다. 이 사실은 tip이 블록체인의 인식자 역할도 한다는 것을 의미한다.

BlockchainIterator는 1가지 일을 수행한다 : 다음 블록을 반환한다.

<code 3_13 : 다음 블록 반환>

자 이제 DB 부분은 다 끝난 것이다!

명령어 인터페이스(CLI)

지금까지 구현된 프로그램과는 상호작용할 수 있는 인터페이스가 따로 존재하지 않는다 : 우리는 지금까지 단지 main 함수에서 NewBlockchain, bc.AddBlock 만을 실행했다. 이 부분을 개선해볼 것이다. 아래의 명령어를 실행할 수 있게 할 것이다 :

blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain

모든 명령어 실행은 CLI 구조로 실행될 것이다:

<code 3_14 : 명령어 구조>

그 “entrypoint”는 Run 함수이다 :

<code 3_15 : 명령어 인터페이스 실행 함수>

명령어 요소들을 파싱하는데 표준 flag 패키지를 사용하려고 한다.

<code 3_16 : 세부 명령어 구현>

일단 2개의 세부 명령어, addblock 와 printchain를 만든다. 그 후에, -data flag를 앞에 붙인다. printchain은 아무 flag를 갖고 있지 않다.

<code 3_17 : 명령어 인터페이스 태그 추가>

다음으로 사용자가 제공한 명령어를 확인하고 관련된 세부 명령어 flag 파싱한다.

<code 3_18 : 명령어 파싱 과정>

다음으로 어떤 세부 명령어가 파싱되었는지 확인하고 관련 함수를 실행한다.

<code 3_19 : 블록체인 반복자를 이용한 체인 호출>

이 부분은 이전에 했던 것과 상당히 비슷하다. 유일한 차이점은 이제 블록 반복 호출하는데 BlockchainIterator를 사용한다는 것이다.

또한, main 함수는 아래와 같이 수정된다 :

<code 3_20 : 3단계 메인 함수>

새로운 Blockchain이 그 어떤 명령어 인자가 제공되더라도 만들어진다.

자 이제 완성되었다! 잘 작동하는지 한번 확인해보자 :

$ blockchain_go printchain 
No existing blockchain found. Creating a new one...
Mining the block containing "Genesis Block" 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
Prev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b PoW: true
$ blockchain_go addblock -data "Send 1 BTC to Ivan" Mining the block containing "Send 1 BTC to Ivan" 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13 Success! $ blockchain_go addblock -data "Pay 0.31337 BTC for a coffee" Mining the block containing "Pay 0.31337 BTC for a coffee" 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148 Success! $ blockchain_go printchain
Prev. hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13 Data: Pay 0.31337 BTC for a coffee
Hash: 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148 PoW: true
Prev. hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b Data: Send 1 BTC to Ivan
Hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13 PoW: true
Prev. hash:
Data: Genesis Block Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b PoW: true

결론

다음에는 주소, 지갑 그리고 거래까지 한번 구현해보자.

--

--

Min Seo Park
CAU_CLink

Interested in Blockchain, Project Financing and Smart city and Love DJing and EDM