블록체인/Mainnet

Go 언어로 블록체인 메인넷 만들기 - 영속성

우리는 지금까지 블록체인을 메모리 내부에만 저장했는데, 그렇게 되어서는 프로그램이 동작하는 동안에만 살아있기 때문에 영속성을 유지할 수 없다. 따라서 우리는 Key-Value 를 사용하는 Local Database 를 사용해보기로 하자. 여기서 어떤 데이터베이스를 사용해야 하는지 결정할 필요가 있는데, 비트코인에서 과거에 사용했던 LevelDB 도 있겠지만, 우리는 BoltDB 를 사용해보도록하자. 이는 Go 언어로 작성된 스토어다.

 

github.com/boltdb/bolt

 

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) 프로그램을 만들어서 테스트하기 편하도록 만들어보도록 할 것이다.