우리는 지금까지 블록체인을 메모리 내부에만 저장했는데, 그렇게 되어서는 프로그램이 동작하는 동안에만 살아있기 때문에 영속성을 유지할 수 없다. 따라서 우리는 Key-Value 를 사용하는 Local Database 를 사용해보기로 하자. 여기서 어떤 데이터베이스를 사용해야 하는지 결정할 필요가 있는데, 비트코인에서 과거에 사용했던 LevelDB 도 있겠지만, 우리는 BoltDB 를 사용해보도록하자. 이는 Go 언어로 작성된 스토어다.
boltdb/bolt
An embedded key/value database for Go. Contribute to boltdb/bolt development by creating an account on GitHub.
github.com
type Blockchain
저번에 선언한 Blockchain
구조체의 내부를 변경할 것이다. 이전에는 []*Block
을 가지고 있었으나, 이제는 데이터베이스를 기반으로 할 것이기 때문에 *bolt.DB
를 가지게 될 것이며 이미 작성되어있는 블록체인을 조회하거나 순회하기 위해 l
라는 키를 가진 마지막 블록의 해시를 갖도록 할 것이다.
type Blockchain struct {
db *bolt.DB
l []byte
}
func NewBlockchain() *Blockchain
해당 함수의 내용도 변경해주어야 한다. boltDB 는 RDBMS 의 테이블과 비슷한 개념을 가지고 있는데, 바로 버킷이라 하는 것이다. 버킷에 블록을 담고 조회하거나 내용을 갱신할 수 있다.
const (
BlocksBucket = "blocks"
dbFile = "chain.db"
)
func NewBlockchain() *Blockchain {
db, err := bolt.Open(dbFile, 0600, nil)
if err != nil {
log.Panic(err)
}
var l []byte
err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(BlocksBucket))
if b == nil {
// 새로운 블록체인을 만들어야 하는 경우
b, err := tx.CreateBucket([]byte(BlocksBucket))
if err != nil {
log.Panic(err)
}
genesis := NewBlock("Genesis Block", []byte{})
err = b.Put(genesis.Hash, genesis.Serialize())
if err != nil {
log.Panic(err)
}
// "l" 키는 마지막 블록해시를 저장합니다.
err = b.Put([]byte("l"), genesis.Hash)
if err != nil {
log.Panic(err)
}
l = genesis.Hash
} else {
// 이미 블록체인이 있는 경우
l = b.Get([]byte("l"))
}
if err != nil {
log.Panic(err)
}
return nil
})
return &Blockchain{db, l}
}
Bolt.Tx.Bucket()
메서드로 버킷을 얻어오지 못했다면, 이는 nil
을 반환하게 된다. 버킷이 없다는 것은 블록체인을 완전히 새로 생성해야 함을 의미하기 때문에 제네시스 블록부터 새로 지정하고, 마지막 블록해시인 l
키에 제네시스 블록의 해시를 저장한다. 이미 블록체인이 있는 경우에는 l
키만 불러와서 저장하면 된다.
또한 블록을 넣을 때 블록에 대해 Block.Serialize()
메서드를 호출함을 볼 수 있는데, 이는 블록데이터를 집어넣을 때는 직렬화를 할 것이기 때문이다. 이에 반대되는 DeserializeBlock()
함수도 있다.
func .Addblock(data string)
블록을 추가하는 이 메서드도 변경해야 한다. .blocks
에 저장하던 것을 스토어에 저장할 수 있도록 바꿔야 하기 때문이다. 마지막 블록해시에 해당하는 l
키를 바꿔야하는 것을 잊어서는 안 된다.
func (bc *Blockchain) AddBlock(data string) {
block := NewBlock(data, bc.l)
err := bc.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(BlocksBucket))
err := b.Put(block.Hash, block.Serialize())
if err != nil {
log.Panic(err)
}
err = b.Put([]byte("l"), block.Hash)
if err != nil {
log.Panic(err)
}
bc.l = block.Hash
return nil
})
if err != nil {
log.Panic(err)
}
}
Block
블록을 스토어에 저장하고, 다시 얻어올 때 사용할 .Serialize()
메서드와 DeserializeBlock()
함수를 살펴보자.
func .Serialize() []byte
encoding/gob
패키지를 사용하여 직렬화를 진행할 수 있는데, bytes.Buffer
에다가 현재 Block
을 인코드하고 바이트 배열을 반환하고 있다.
func (b *Block) Serialize() []byte {
var buf bytes.Buffer
encoder := gob.NewEncoder(&buf)
err := encoder.Encode(b)
if err != nil {
log.Panic(err)
}
return buf.Bytes()
}
func DeserializeBlock(*Block) *Block
스토어에 저장된 인코드된 블록데이터를 역직렬화를 하여 원본 데이터를 얻어올 수 있다. 디코더를 사용하고 Block
으로 가져온다.
func DeserializeBlock(encodedBlock []byte) *Block {
var buf bytes.Buffer
var block Block
buf.Write(encodedBlock)
decoder := gob.NewDecoder(&buf)
err := decoder.Decode(&block)
if err != nil {
log.Panic(err)
}
return &block
}
type BlockchainIterator
블록체인 내부를 순회하기 위해 반복자를 만들어보자. 맨 마지막 블록에서부터 제네시스 블록에 이르기까지 역순으로 순회할 것이다. 스토어에 접근하기 위한 *bolt.DB
와 마지막 블록해시를 위한 []byte
를 가진다.
type blockchainIterator struct {
db *bolt.DB
hash []byte
}
func NewBlockchainIterator() *BlockchainIterator
블록체인을 순회하기 위한 반복자를 만든다.
func NewBlockchainIterator(bc *Blockchain) *blockchainIterator {
return &blockchainIterator{bc.db, bc.l}
}
func .Next() *Block
스토어를 조회하여 다음 블록을 반환한다. 가장 처음나오는 것은 블록체인에 있는 제일 마지막 블록에서부터 시작한다.
func (i *blockchainIterator) Next() *Block {
var block *Block
err := i.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(BlocksBucket))
encodedBlock := b.Get(i.hash)
block = DeserializeBlock(encodedBlock)
i.hash = block.PrevBlockHash
return nil
})
if err != nil {
log.Panic(err)
}
return block
}
func .HasNext()
다음 블록이 있는지 검사한다. 제네시스 블록의 이전 블록은 없기 때문에 제네시스 블록의 값을 활용하자.
func (i *blockchainIterator) HasNext() bool {
return bytes.Compare(i.hash, []byte{}) != 0
}
func main()
이제 반복자를 사용하여 데이터가 잘 출력되는지 보면 된다. 그런데 이것을 테스트하기 위해서는 두 번의 실행이 필요하다. 첫 번째는 블록체인을 생성하고 새로운 블록을 추가하여 스토어에 저장하는 것, 두 번째에서는 이미 만들어진 스토어를 불러와서 반복자를 사용하여 출력해보는 것이다. 이를 매번 처리하기 번거롭기 때문에 다음 포스트에서 CLI(Command Line Interface) 프로그램을 만들어서 테스트하기 편하도록 만들어보도록 할 것이다.