Blockchain

Go로 만드는 블록체인 part 3 - Persistence

hou27 2022. 1. 23. 17:41

지금까지는 블록을 생성해도 매번 실행 시마다 코드 상의 변수에 블록들을 저장했기 때문에 프로그램 실행 중에만 데이터들이 유효했다가 종료 시에 전부 사라졌었다.

 

그래서 이번에는 데이터베이스를 활용하여 '영속성'을 부여해줄 것이다.

 

그렇다면 어떤 DB를 사용해야 할까?

 

구글에 열심히 찾아보았지만 bitcoin이 어떤 DB를 사용하는지 찾을 수 없었다.(일단 나는..)

그래서 Jeiwan이란 분이 사용하신 bolt란 db를 나도 사용하도록 하기로 했다.

 

Jeiwan의 설명에 따르면 이유는 아래와 같다.

1. It’s simple and minimalistic.
2. It’s implemented in Go.
3. It doesn’t require to run a server.
4. It allows to build the data structure we want.

One important thing about BoltDB is that there are no data types: keys and values are byte arrays. Since we’ll store Go structs (Block, in particular) in it, we’ll need to serialize them, i.e. implement a mechanism of converting a Go struct into a byte array and restoring it back from a byte array. We’ll use encoding/gob for this, but JSON, XML, Protocol Buffers, etc. can be used as well. We’re using encoding/gob because it’s simple and is a part of the standard Go library.

https://jeiwan.net/posts/building-blockchain-in-go-part-3/

 

Building Blockchain in Go. Part 3: Persistence and CLI - Going the distance

Chinese translations: by liuchengxu, by zhangli1. Introduction So far, we’ve built a blockchain with a proof-of-work system, which makes mining possible. Our implementation is getting closer to a fully functional blockchain, but it still lacks some impor

jeiwan.net

즉,

  1. 단순하고 가볍다.
  2. Go로 작성되었다.
  3. 별도의 서버가 필요하지 않다.
  4. 데이터 구조 설계가 자유롭다.

BoltDB의 README.md를 보면

 

데이터 타입이 없으며 키와 값은 바이트 배열로 저장된다고 한다. 구조체를 직렬화하여 저장할 것이기 때문에 구조체를 바이트 배열로 변환하고 바이트 배열로부터 구조체를 복원할 예정인 상황에서 상당히 적절한 데이터베이스인 것이다.

https://github.com/mingrammer/blockchain-tutorial/tree/master/persistence-and-cli

 

GitHub - mingrammer/blockchain-tutorial: 블록체인 튜토리얼 (Building Blockchain in Go 한국어 버전)

블록체인 튜토리얼 (Building Blockchain in Go 한국어 버전). Contribute to mingrammer/blockchain-tutorial development by creating an account on GitHub.

github.com

 

우선 다른 작업을 진행하기 전에 방금 말했듯이 직렬화, 역직렬화하는 과정이 필요하기 때문에

관련 함수를 작성해주도록 하겠다.

Function Serialize, DeserializeBlock

// Serialize before sending
func (b *Block) Serialize() []byte {
	var value bytes.Buffer

	encoder := gob.NewEncoder(&value)
	err := encoder.Encode(b)
	if err != nil {
		log.Fatal("Encode Error:", err)
	}

	return value.Bytes()
}

// Deserialize block(not a method)
func DeserializeBlock(d []byte) *Block {
	var block Block

	decoder := gob.NewDecoder(bytes.NewReader(d))
	err := decoder.Decode(&block)
	if err != nil {
		log.Fatal("Decode Error:", err)
	}

	return &block
}

여기서 사용한 패키지는 encoding/gob이다.

 

gob package - encoding/gob - pkg.go.dev

This example shows the basic usage of the package: Create an encoder, transmit some values, receive them with a decoder. package main import ( "bytes" "encoding/gob" "fmt" "log" ) type P struct { X, Y, Z int Name string } type Q struct { X, Y *int32 Name s

pkg.go.dev

 

Serialize 함수와 달리 Deserialize 함수는 method가 아니며,

추후 다른 역직렬화 함수들을 만들 예정이기 때문에 함수명을 분명히 해주었다.

 

이전까지는

// Get All Blockchains
func GetBlockchain() *Blockchain {
	if Bc == nil {
		generateGenesis()
	}
	return Bc
}

이렇게 메모리에 블록체인이 있으면 return하고 없으면 제네시스 블록을 생성 후 리턴했지만,

이제는 DB를 이용할 것이므로

 

아래와 같이 다시 작성하였다.

Function GetBlockchain

// Get All Blockchains
func GetBlockchain() *Blockchain {
	var last []byte

	dbFile := fmt.Sprintf(dbFile, "0600")
	db, err := bolt.Open(dbFile, 0600, nil)
	if err != nil {
		log.Fatal(err)
	}
	// defer db.Close()
	err = db.Update(func(tx *bolt.Tx) error {
		bc := tx.Bucket([]byte("blocks"))
		if bc == nil {
			genesis := generateGenesis()
            fmt.Println("Generate Genesis block")
			b, err := tx.CreateBucket([]byte("blocks"))
			if err != nil {
				return err
			}
			err = b.Put(genesis.Hash, genesis.Serialize())
			if err != nil {
				return err
			}
			err = b.Put([]byte("last"), genesis.Hash)
			if err != nil {
				return err
			}
			last = genesis.Hash
		} else {
			last = bc.Get([]byte("last"))
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	bc := Blockchain{db, last}
    return &bc
}

https://pkg.go.dev/github.com/boltdb/bolt#section-readme

 

bolt package - github.com/boltdb/bolt - pkg.go.dev

Bolt Bolt is a pure Go key/value store inspired by Howard Chu's LMDB project. The goal of the project is to provide a simple, fast, and reliable database for projects that don't require a full database server such as Postgres or MySQL. Since Bolt is meant

pkg.go.dev

우선 BoltDB를 열어준다.

db, err := bolt.Open(dbFile, 0600, nil)

dbFile명으로 열고, 이때 DB를 읽고 쓸 것이기 때문에 mode는 0600으로 해서 열어주었다.

 

BoltDB는 트랜잭션을 열어 작업하기 때문에 아래 2가지 중 한 가지 모드로 트랜잭션을 열도록 하겠다.

 

Read-write transactions

To start a read-write transaction, you can use the DB.Update() function:

err := db.Update(func(tx *bolt.Tx) error {
	...
	return nil
})

Read-only transactions

To start a read-only transaction, you can use the DB.View() function:

err := db.View(func(tx *bolt.Tx) error {
	...
	return nil
})

블록을 쓰고 읽기도 할 것이기 때문에

읽기 쓰기가 전부 되는 트랜잭션을 DB.Update() function을 사용하여 열어주었다.

err = db.Update(func(tx *bolt.Tx) error {
    bc := tx.Bucket([]byte("blocks"))
    if bc == nil {
        genesis := generateGenesis()
        b, err := tx.CreateBucket([]byte("blocks"))
        err = b.Put(genesis.Hash, genesis.Serialize())
        err = b.Put([]byte("last"), genesis.Hash)
        last = genesis.Hash
    } else {
        last = bc.Get([]byte("last"))
    }
    return nil
})

Bucket이란 BoltDB 내의 key/value 쌍의 모음이다.

모든 버킷의 키는 고유해야하며 CreateBucket을 통해 만들 수 있다.

 

상세히 살펴보면,

bc := tx.Bucket([]byte("blocks"))
if bc == nil {
    ...
}

block들을 blocks 버킷에 저장할 것이기 때문에 가장 먼저 해당 버킷이 존재하는지 확인하고,

 

if bc == nil {
    genesis := generateGenesis()
    b, err := tx.CreateBucket([]byte("blocks"))
    err = b.Put(genesis.Hash, genesis.Serialize())
    err = b.Put([]byte("last"), genesis.Hash)
    last = genesis.Hash
}

없다면 제네시스 블록을 발행해준 후 해당 블록을 직렬화하여 blocks버킷에 넣어주었다.

 

err = b.Put([]byte("last"), genesis.Hash)
last = genesis.Hash

가장 마지막 블록을 컨트롤해주었는데,

if bc == nil {
    ...
} else {
    last = bc.Get([]byte("last"))
}

bc := Blockchain{db, last}
return &bc

이는 블록 버킷이 존재할 시에는 가장 마지막 블록을 가져올 것이기 때문이다.

 

Function AddBlock

// Add block
func (bc *Blockchain) AddBlock(data string) {
	var lastHash []byte

	err := bc.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("blocks"))
		lastHash = b.Get([]byte("last"))

		return nil
	})
	if err != nil {
		log.Panic(err)
	}

	newBlock := NewBlock(data, lastHash)

	err = bc.db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("blocks"))
		err := b.Put(newBlock.Hash, newBlock.Serialize())
		if err != nil {
			log.Panic(err)
		}

		err = b.Put([]byte("last"), newBlock.Hash)
		if err != nil {
			log.Panic(err)
		}

		bc.last = newBlock.Hash

		fmt.Println("Successfully Added")

		return nil
	})
	if err != nil {
		log.Panic(err)
	}
}

이전까지는 메모리의 배열에 추가하는 것이 AddBlock의 역할이었다면,

이제는 DB에 추가해주는 역할을 맡을 것이다.

 

err := bc.db.View(func(tx *bolt.Tx) error {
    b := tx.Bucket([]byte("blocks"))
    lastHash = b.Get([]byte("last"))

    return nil
})

읽기 전용인 DB.View() function으로 트랜잭션을 열어 가장 마지막 블록을 가져온다.

 

newBlock := NewBlock(data, lastHash)

다음 데이터와 이전 블록의 해쉬값을 NewBlock 함수에 넘겨준다.

(Func NewBlock은 변경된 부분이 없다.)

 

err = bc.db.Update(func(tx *bolt.Tx) error {
    b := tx.Bucket([]byte("blocks"))
    err := b.Put(newBlock.Hash, newBlock.Serialize())
    err = b.Put([]byte("last"), newBlock.Hash)
    bc.last = newBlock.Hash

    fmt.Println("Successfully Added")

    return nil
})

그 후 DB.Update()를 이용하여 blocks 버킷에 추가하고, last버킷을 업데이트해주었다.

 

Blockchain Iterator

가장 마지막 블록을 얻게 되면, 다른 나머지 블록들도 탐색할 수 있도록 장치가 필요하다.

때문에 아래와 같이 반복자를 만들어주었다.

// Blockchain iterator
func (bc *Blockchain) Iterator() *BlockchainTmp {
	bcT := &BlockchainTmp{bc.db, bc.last}
 
	return bcT
}

위에 등장한 BlockchainTmp는

type BlockchainTmp struct {
	db          *bolt.DB
	currentHash []byte
}

이렇게 생겼다.

 

Function getNextBlock

func (bct *BlockchainTmp) getNextBlock() *Block {
	var block *Block

	err := bct.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("blocks"))
		encodedBlock := b.Get(bct.currentHash)
		block = DeserializeBlock(encodedBlock)

		return nil
	})
	if err != nil {
		log.Panic(err)
	}

	bct.currentHash = block.PrevHash
	return block
}

그리고 반복자를 활용하여 이 getNextBlock은 다음 블록을 반환해주는 역할을 한다.

 

이제 ShowBlocks에서 다음 블록이 없을 때의 경우를 처리해주어야 한다.

// Show Blockchains
func (bc Blockchain) ShowBlocks() {
	bcT := bc.Iterator()
	
	for {
		block := bcT.getNextBlock() // 반복자 이용
		pow := NewProofOfWork(block)

		fmt.Println("\nTimeStamp:", block.TimeStamp)
		fmt.Printf("Data: %s\n", block.Data)
        fmt.Printf("Hash: %x\n", block.Hash)
		fmt.Printf("Prev Hash: %x\n", block.PrevHash)
		fmt.Printf("Nonce: %d\n", block.Nonce)

		fmt.Printf("is Validated: %s\n", strconv.FormatBool(pow.Validate()))

		if len(block.PrevHash) == 0 { // 다음 블록이 없다면 break!
			break
		}
	}
}

 

이렇게 BoltDB를 활용한 영속성 부여는 마무리하였다.

 

 

# 중간 점검

main.go

package main

import "strconv"

func main() {
	chain := GetBlockchain()
	defer chain.db.Close()

	for i := 1; i < 10; i++ {
		chain.AddBlock(strconv.Itoa(i))
	}
	chain.ShowBlocks()
}

지금까지의 내용을 main.go에 반영하여 잘 작동하는지 테스트해보고 다음으로 넘어가도록 하겠다.

 

실행 시 모습 :

016959d39f2cf317788cdeb86873147fc90e0c9d0230c029c8e8bca08a745d8b
Generate Genesis block
005d5bd38debd2772cf668e9cacb398c6f1cb95a29ad941a0b06d94efef26cfd
Successfully Added
01d0ec38b38959d7ed52262f449aedf17cd9f02e8d3cd8e95ebdd87900172b5c
Successfully Added
0015555174e9391380b0ff8b896ddeff1816e8258dbcfdeee88575397bc2cff4
Successfully Added
01e6b4e364d7927df35061ff4ca5f53b3fa97028fef2511c0063763c7f35661c
Successfully Added
023ac85a17e46b82365f8a8ee109938c4058f97480cb62935a77af6271188968
Successfully Added
01c52e28c44f7c9b362481062c44a059a26f7b96f331003e41ee547071640357
Successfully Added
001c170ed70bd872b719c57c747bcc400628c66523144e2b6a3f4a211a691aea
Successfully Added
00f4ebaadc8ef02be09361fcbb49e9988dbc3acb27eb9578a14572a67e27d51f
Successfully Added
01514ddc25ed77a288a0a1f72d33fcfe89619d3605df6e55cec2cf08e1536355
Successfully Added

TimeStamp: 1643021299
Data: 9
Hash: 01514ddc25ed77a288a0a1f72d33fcfe89619d3605df6e55cec2cf08e1536355
Prev Hash: 00f4ebaadc8ef02be09361fcbb49e9988dbc3acb27eb9578a14572a67e27d51f
Nonce: 18
is Validated: true

TimeStamp: 1643021299
Data: 8
Hash: 00f4ebaadc8ef02be09361fcbb49e9988dbc3acb27eb9578a14572a67e27d51f
Prev Hash: 001c170ed70bd872b719c57c747bcc400628c66523144e2b6a3f4a211a691aea
Nonce: 32
is Validated: true

TimeStamp: 1643021299
Data: 7
Hash: 001c170ed70bd872b719c57c747bcc400628c66523144e2b6a3f4a211a691aea
Prev Hash: 01c52e28c44f7c9b362481062c44a059a26f7b96f331003e41ee547071640357
Nonce: 89
is Validated: true

TimeStamp: 1643021299
Data: 6
Hash: 01c52e28c44f7c9b362481062c44a059a26f7b96f331003e41ee547071640357
Prev Hash: 023ac85a17e46b82365f8a8ee109938c4058f97480cb62935a77af6271188968
Nonce: 33
is Validated: true

TimeStamp: 1643021299
Data: 5
Hash: 023ac85a17e46b82365f8a8ee109938c4058f97480cb62935a77af6271188968
Prev Hash: 01e6b4e364d7927df35061ff4ca5f53b3fa97028fef2511c0063763c7f35661c
Nonce: 3
is Validated: true

TimeStamp: 1643021299
Data: 4
Hash: 01e6b4e364d7927df35061ff4ca5f53b3fa97028fef2511c0063763c7f35661c
Prev Hash: 0015555174e9391380b0ff8b896ddeff1816e8258dbcfdeee88575397bc2cff4
Nonce: 29
is Validated: true

TimeStamp: 1643021299
Data: 3
Hash: 0015555174e9391380b0ff8b896ddeff1816e8258dbcfdeee88575397bc2cff4
Prev Hash: 01d0ec38b38959d7ed52262f449aedf17cd9f02e8d3cd8e95ebdd87900172b5c
Nonce: 3
is Validated: true

TimeStamp: 1643021299
Data: 2
Hash: 01d0ec38b38959d7ed52262f449aedf17cd9f02e8d3cd8e95ebdd87900172b5c
Prev Hash: 005d5bd38debd2772cf668e9cacb398c6f1cb95a29ad941a0b06d94efef26cfd
Nonce: 137
is Validated: true

TimeStamp: 1643021299
Data: 1
Hash: 005d5bd38debd2772cf668e9cacb398c6f1cb95a29ad941a0b06d94efef26cfd
Prev Hash: 016959d39f2cf317788cdeb86873147fc90e0c9d0230c029c8e8bca08a745d8b
Nonce: 195
is Validated: true

TimeStamp: 1643021299
Data: Genesis Block
Hash: 016959d39f2cf317788cdeb86873147fc90e0c9d0230c029c8e8bca08a745d8b
Prev Hash: 
Nonce: 67
is Validated: true

 

이제 작성한 프로그램과 상호작용할 수 있도록 커맨드를 작성해보도록 하겠다.

 


CLI 추가

cli란

: Command Line Interface. 터미널 등을 통해 사용자가 프로그램과 상호작용할 수 있는 환경을 말한다.

 

Type Cli

type Cli struct {
	bc *Blockchain
}

지금은 구조체 안에 blockchain이 들어있지만 추후 코드가 변경되어가며 삭제될 예정이다.

 

CLI's Methods

func (cli *Cli) Active() {
	...
}

func (cli *Cli) printUsage() {
	...
}

이번 포스트에서는 cli구조체의 메서드로 위의 2개만 만들어보도록 하겠다.

 

Active는 메서드명 그대로 cli를 활성화하는 메서드이며 우리 프로그램의 entry point(진입점)가 되겠다.

printUsage는 명령어 사용법을 출력해줄 친구이다.

func (cli *Cli) Active() {
	if len(os.Args) < 2 {
		cli.printUsage()
		os.Exit(1)
	}
	addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
	showBlocksCmd := flag.NewFlagSet("showblocks", flag.ExitOnError)

	addBlockData := addBlockCmd.String("data", "", "Block data")

	switch os.Args[1] {
	case "addblock":
		err := addBlockCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	case "showblocks":
		err := showBlocksCmd.Parse(os.Args[2:])
		if err != nil {
			log.Panic(err)
		}
	default:
		cli.printUsage()
		os.Exit(1)
	}

	if addBlockCmd.Parsed() {
		if *addBlockData == "" {
			addBlockCmd.Usage()
			os.Exit(1)
		}
		cli.bc.AddBlock(*addBlockData)
	}

	if showBlocksCmd.Parsed() {
		cli.bc.ShowBlocks()
	}
}

먼저 Active()이다.

 

코드 상으로 지금까지 구현한 것은

블록을 추가하는 것과, 블록들을 출력하는 것 두가지로 볼 수 있다.

 

그렇다면 command 상으로도 블록 추가와 출력이 가능하도록 해야 한다.

 

// flag 설정
addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
showBlocksCmd := flag.NewFlagSet("showblocks", flag.ExitOnError)
// addblock에 -data라는 옵션 추가
addBlockData := addBlockCmd.String("data", "", "Block data")

flag패키지를 활용하여 프로그램을 실행할 때의 명령어의 인자를 파싱 해주도록 한다.

 

1, 2번째 라인은 flagset을 설정한 것이고,

3번째 라인은 data라는 특정 이름으로 문자열 flag를 추가로 만들어준 것이다.

 

flag package - flag - pkg.go.dev

Package flag implements command-line flag parsing. Usage ¶Define flags using flag.String(), Bool(), Int(), etc. This declares an integer flag, -n, stored in the pointer nFlag, with type *int: import "flag" var nFlag = flag.Int("n", 1234, "help message for

pkg.go.dev

switch os.Args[1] {
case "addblock":
    err := addBlockCmd.Parse(os.Args[2:])
    if err != nil {
        log.Panic(err)
    }
case "showblocks":
    err := showBlocksCmd.Parse(os.Args[2:])
    if err != nil {
        log.Panic(err)
    }
default:
    cli.printUsage()
    os.Exit(1)
}

입력된 명령어를 통해 적절한 case문으로 넘겨주어 해당 플래그를 파싱한다.

if addBlockCmd.Parsed() {
    if *addBlockData == "" {
        addBlockCmd.Usage()
        os.Exit(1)
    }
    cli.bc.AddBlock(*addBlockData)
}

if showBlocksCmd.Parsed() {
    cli.bc.ShowBlocks()
}

그리고 파싱되었는지 확인해주어 적절한 기능를 실행해준다.

 

printUsage()이다.

func (cli *Cli) printUsage() {
	fmt.Printf("How to use:\n\n")
	fmt.Println("  addblock -data DATA - add a block to the blockchain")
	fmt.Println("  showblocks - print all the blocks of the blockchain")
}

작성한 2가지 커맨드에 대한 설명을 담았다.

하위 인자값이 올바르지 않거나 없을 때 실행될 함수이다.

 

이제 main함수만 수정해주면 이번 포스트에서 할 내용은 모두 다루었다.

main.go

package main

func main() {
	chain := GetBlockchain()
	defer chain.db.Close()

	cli := Cli{chain}
	cli.Active()
}

이제는 코드로 일일히 할 행동을 지정해주지 않고 cli를 활성화해준다.

 

실행 시 모습 :

$ go run .
How to use:

  addblock -data DATA - add a block to the blockchain
  showblocks - print all the blocks of the blockchain
exit status 1

$ go run . addblock -data "first"
00e722b0b646488be6977e8c45e30cc72b5ff8e2ab32e275a30ec51eb941f122
Generate Genesis block
000985f4f9bdc5d310935bcb3f0983eeb88c32b062c4c0f08c3c776e6d200dca
Successfully Added

$ go run . showblocks

TimeStamp: 1643028807
Data: first
Hash: 000985f4f9bdc5d310935bcb3f0983eeb88c32b062c4c0f08c3c776e6d200dca
Prev Hash: 00e722b0b646488be6977e8c45e30cc72b5ff8e2ab32e275a30ec51eb941f122
Nonce: 86
is Validated: true

TimeStamp: 1643028807
Data: Genesis Block
Hash: 00e722b0b646488be6977e8c45e30cc72b5ff8e2ab32e275a30ec51eb941f122
Prev Hash: 
Nonce: 22
is Validated: true

성공!

 

앞으로 추가되는 기능들에 대해서도 계속해서 커맨드 라인을 추가할 것이므로

코드 변경사항에 유의하기 바란다.

https://github.com/hou27/blockchain_go/tree/part3

 

GitHub - hou27/blockchain_go: Making a Cryptocurrency with GO.

Making a Cryptocurrency with GO. . Contribute to hou27/blockchain_go development by creating an account on GitHub.

github.com