Blockchain on Go. Part 3: read-only memory and command line interface
- Transfer
- Tutorial
Content
In the previous part, we built a blockchain with a PoW system and the possibility of mining. Our implementation is getting closer to a fully functional blockchain, but it still lacks some important functions. Today we will begin to store the blockchain in the database, after that we will make a command line interface for operations with the blockchain. In essence, blockchain is a distributed database. For now, we omit the “distributed” and focus on the “database”.
So far, we do not have a database in the implementation, we just create blocks when the program starts and store them in memory. We cannot reuse or share our blockchain with others, so we need to save it to disk.
What database do we need? In fact, any will do. The Bitcoin Paper says nothing about a specific database, so the choice is up to the developer. Bitcoin Core , which was originally published by Satoshi Nakamoto and which is currently the reference implementation of Bitcoin, uses LevelDB (although it was introduced to the client only in 2012). And we will use ...
Because:
From BoltDB README :
BoltDB is a key-value storage, which means that there are no tables, as in relational DBMSs (MySQL, PostgreSQL, etc.), there are no rows and columns. Instead, data is stored in key-value pairs (as in the Golang map). Pairs are stored in “baskets” that are designed to group similar pairs (like tables in relational DBMSs). Thus, to get the value, you need to know the basket and the key.
The important thing about BoltDB is that there are no data types: keys and values are byte arrays. Since we store Go structures (in particular
Before we begin to implement persistent logic, we must decide how we will store our data in the database. And for this we will use the method that we use Bitcoin Core.
If in a simple way, then Bitcoin Core uses two “baskets” for storing data.
Blocks are also stored as separate files on disk. This is done to improve performance: reading one block does not require loading all (or some) into memory. This we will not implement.
In
This is all we need to know in order to implement the mechanism of constancy (persistence).
As mentioned earlier, in BoltDB values can only be of
Let's implement a method
Everything is simple here: at the beginning, we declare a buffer where the serialized data will be stored, then we initialize the
Now we need a deserialization function that receives an array of bytes and returns
That's all we need for serialization.
Let's start with the function
In code, it looks like this:
Let's analyze the code in parts.
This is the standard way to open a BoltDB file. Please note that it will not return an error if there is no file.
In BoltDB, database operations are performed as part of a transaction. There are two types of transactions: read-only and read-write. Here we open a read-write transaction
This is the core of the function. Here we get a basket that stores our blocks: if it exists, then we read the key
Also notice the new way to create
We do not store all the blocks; instead, we only store the tip of the chain. We also store the connection to the database, because we want to open it once and keep it open while the program is running. This is how the structure
The next thing we want to change is the method
Consider the code piece by piece:
This is a different (read-only) type of BoltDB transaction. Here we get the hash of the last block from the database to use it to mine the hash of the new block.
After mining a new block, we save the serialized view in the database and update the key
Done! It was not difficult, right?
All new blocks are now stored in the database, so we can reopen the blockchain and add a new block to it. But after implementing this, we lose one useful feature: we cannot print blocks because we no longer store them in an array. Let's fix it.
BoltDB allows you to go through all the keys in the basket, but all the keys are stored in sorted by bytes, and we want the blocks to be printed in the order in which they are placed on the blockchain. Also, since we do not want to load all the blocks into memory (our blockchain can be very huge), we will read them one by one. For this purpose, we need a blockchain iterator:
An iterator will be created every time we want to iterate over the blocks in the blockchain and it will store the block hash of the current iteration and the connection to the database. Because of the latter, the iterator is logically bound to the blockchain (this is an instance
Note that the iterator first points to the tip of the blockchain, so the blocks will be received from top to bottom, from the newest to the oldest. In fact, choosing a tip means “voting” for the blockchain . The blockchain can have several branches and the longest of them is considered the main one. After receiving the tip (it can be any block in the blockchain), we can recreate the entire blockchain and find its length, and the work necessary to build it. This fact also means that the tip is a kind of blockchain identifier.
That's all about the database!
So far, our implementation does not provide us with any interface for interacting with the program: we just ran
All command line related operations will be handled by the structure
The "entry point" of the structure is a function
We use the standard flag package to parse command line arguments.
To get started, we create two subcommands
Then we check the command specified by the user and parse the associated subcommand.
Next, we check which subcommand we parsed and run the associated function.
This code is similar to the one that was before. The only difference is what we use now
Also, do not forget to change the function
Note that a new one
That's all! Check that everything works as we expect:
(the sound of opening a beer can )
Original article
The first part of a series of articles
Sources
Bitcoin Core Data Storage
BoltDB
encoding / gob
flag
- Blockchain on Go. Part 1: Prototype
- Blockchain on Go. Part 2: Proof-of-Work
- Blockchain on Go. Part 3: read-only memory and command line interface
- Blockchain on Go. Part 4: Transactions, Part 1
- Blockchain on Go. Part 5: Addresses
- Blockchain on Go. Part 6: Transactions, Part 2
- Blockchain on Go. Part 7: Network
Introduction
In the previous part, we built a blockchain with a PoW system and the possibility of mining. Our implementation is getting closer to a fully functional blockchain, but it still lacks some important functions. Today we will begin to store the blockchain in the database, after that we will make a command line interface for operations with the blockchain. In essence, blockchain is a distributed database. For now, we omit the “distributed” and focus on the “database”.
Database selection
So far, we do not have a database in the implementation, we just create blocks when the program starts and store them in memory. We cannot reuse or share our blockchain with others, so we need to save it to disk.
What database do we need? In fact, any will do. The Bitcoin Paper says nothing about a specific database, so the choice is up to the developer. Bitcoin Core , which was originally published by Satoshi Nakamoto and which is currently the reference implementation of Bitcoin, uses LevelDB (although it was introduced to the client only in 2012). And we will use ...
Boltdb
Because:
- She is simple and minimal.
- It is implemented on Go
- She does not need to start the server
- It allows us to build the data structures we need.
From BoltDB README :
Bolt is just a key-value repository, inspired by Howard Chu’s LMDB project . The aim of the project is to provide a simple, fast and reliable database for projects that do not require a full-fledged database server such as Postgres or MySQL.Sounds perfect for our needs! Take a moment to review the base.
Since Bolt is intended to be used as such a low-level element of functionality, simplicity is key. The API will be small and focus only on getting values and setting values. It's all!
BoltDB is a key-value storage, which means that there are no tables, as in relational DBMSs (MySQL, PostgreSQL, etc.), there are no rows and columns. Instead, data is stored in key-value pairs (as in the Golang map). Pairs are stored in “baskets” that are designed to group similar pairs (like tables in relational DBMSs). Thus, to get the value, you need to know the basket and the key.
The important thing about BoltDB is that there are no data types: keys and values are byte arrays. Since we store Go structures (in particular
Block
), we must serialize them, that is, implement a mechanism for translating the structure into a byte array and restoring it back from the array. We will use encoding / gobfor this, although JSON, XML, Protocol Buffers
also suitable. We use encoding/gob
because it is simple and it is part of the Go standard library.Database structure
Before we begin to implement persistent logic, we must decide how we will store our data in the database. And for this we will use the method that we use Bitcoin Core.
If in a simple way, then Bitcoin Core uses two “baskets” for storing data.
blocks
stores metadata describing all blocks in a chainchainstate
stores the state of the chain, which is all unspent transaction outputs and some metadata
Blocks are also stored as separate files on disk. This is done to improve performance: reading one block does not require loading all (or some) into memory. This we will not implement.
In
blocks
pairs key->value
it is: In pairs it is: (A detailed explanation can be found here )
Since we do not have transactions yet, we will only make a basket . In addition, as mentioned above, we will store the entire database in one file, without storing blocks in separate files. Therefore, we do not need anything related to file numbers. Therefore, the pairs that we will use are:- 'b' + 32-байтовый хэш блока -> запись индекса блока
- 'f' + 4-байтовый номер файла -> запись информации о файле
- 'l' -> 4-байтовый номер файла: номер использованного файла для последнего блока
- 'R' -> 1-байтовый boolean : находимся ли мы в процессе переиндексации
- 'F' + 1-байтовая длина имени флага + строка имени флага -> 1 байт boolean: различные флаги, которые могут быть включены или выключены
- 't' + 32-байтовый хеш транзакции -> запись индекса транзакции
chainstate
key->value
- 'c' + 32-байтовый хеш транзакции -> запись о непотраченном выходе транзакции для этой транзакции
- 'B' -> 32-байтовый хеш блока: хеш блока, до которого база данных представляет собой неизрасходованные выходы транзакции
blocks
key->value
- 32-byte block hash -> block structure (serialized)
- 'l' -> hash of the last block in the chain
This is all we need to know in order to implement the mechanism of constancy (persistence).
Serialization
As mentioned earlier, in BoltDB values can only be of
[]byte
type, and we want to store the structure Block
in the database. We will use encoding/gob
to serialize structures. Let's implement a method
Serialize
for Block
(error handling omitted for brevity)func (b *Block) Serialize() []byte {
var result bytes.Buffer
encoder := gob.NewEncoder(&result)
err := encoder.Encode(b)
return result.Bytes()
}
Everything is simple here: at the beginning, we declare a buffer where the serialized data will be stored, then we initialize the
gob
encoder and encode the block, the result is returned as an array of bytes. Now we need a deserialization function that receives an array of bytes and returns
Block
. This will not be a method, but an independent function:func DeserializeBlock(d []byte) *Block {
var block Block
decoder := gob.NewDecoder(bytes.NewReader(d))
err := decoder.Decode(&block)
return &block
}
That's all we need for serialization.
Persistence
Let's start with the function
NewBlockchain
. Now she creates a new instance Blockchain
and adds a genesis block to it. We want to do the following:- Open db file
- Check if the blockchain is saved there
- If he is there:
- Create new instance
Blockchain
- Set the tip of the instance
Blockchain
to the hash of the last block stored in the database
- Create new instance
- If there is no existing blockchain
- Create Genesis Block
- Save to DB
- Save Genesis Hash as Hash of Last Last Block
- Create a new instance
Blockchain
with a tip pointing to the genesis block
In code, it looks like this:
func NewBlockchain() *Blockchain {
var tip []byte
db, err := bolt.Open(dbFile, 0600, nil)
err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
if b == nil {
genesis := NewGenesisBlock()
b, err := tx.CreateBucket([]byte(blocksBucket))
err = b.Put(genesis.Hash, genesis.Serialize())
err = b.Put([]byte("l"), genesis.Hash)
tip = genesis.Hash
} else {
tip = b.Get([]byte("l"))
}
return nil
})
bc := Blockchain{tip, db}
return &bc
}
Let's analyze the code in parts.
db, err := bolt.Open(dbFile, 0600, nil)
This is the standard way to open a BoltDB file. Please note that it will not return an error if there is no file.
err = db.Update(func(tx *bolt.Tx) error {
...
})
In BoltDB, database operations are performed as part of a transaction. There are two types of transactions: read-only and read-write. Here we open a read-write transaction
(db.Update(...))
, because we plan to put the genesis block in the database.b := tx.Bucket([]byte(blocksBucket))
if b == nil {
genesis := NewGenesisBlock()
b, err := tx.CreateBucket([]byte(blocksBucket))
err = b.Put(genesis.Hash, genesis.Serialize())
err = b.Put([]byte("l"), genesis.Hash)
tip = genesis.Hash
} else {
tip = b.Get([]byte("l"))
}
This is the core of the function. Here we get a basket that stores our blocks: if it exists, then we read the key
l
from it, if it does not exist, then we generate the genesis of the block, create a basket, save the block in it and update the key l
that stores the hash of the last block in the chain. Also notice the new way to create
Blockchain
:bc := Blockchain{tip, db}
We do not store all the blocks; instead, we only store the tip of the chain. We also store the connection to the database, because we want to open it once and keep it open while the program is running. This is how the structure
Blockchain
looks now:type Blockchain struct {
tip []byte
db *bolt.DB
}
The next thing we want to change is the method
AddBlock
: adding blocks to the chain is now not as simple as adding an element to an array. From now on we will store blocks in the database:func (bc *Blockchain) AddBlock(data string) {
var lastHash []byte
err := bc.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
lastHash = b.Get([]byte("l"))
return nil
})
newBlock := NewBlock(data, lastHash)
err = bc.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
err := b.Put(newBlock.Hash, newBlock.Serialize())
err = b.Put([]byte("l"), newBlock.Hash)
bc.tip = newBlock.Hash
return nil
})
}
Consider the code piece by piece:
err := bc.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
lastHash = b.Get([]byte("l"))
return nil
})
This is a different (read-only) type of BoltDB transaction. Here we get the hash of the last block from the database to use it to mine the hash of the new block.
newBlock := NewBlock(data, lastHash)
b := tx.Bucket([]byte(blocksBucket))
err := b.Put(newBlock.Hash, newBlock.Serialize())
err = b.Put([]byte("l"), newBlock.Hash)
bc.tip = newBlock.Hash
After mining a new block, we save the serialized view in the database and update the key
l
, which now saves the hash of the new block. Done! It was not difficult, right?
Checking Blockchain
All new blocks are now stored in the database, so we can reopen the blockchain and add a new block to it. But after implementing this, we lose one useful feature: we cannot print blocks because we no longer store them in an array. Let's fix it.
BoltDB allows you to go through all the keys in the basket, but all the keys are stored in sorted by bytes, and we want the blocks to be printed in the order in which they are placed on the blockchain. Also, since we do not want to load all the blocks into memory (our blockchain can be very huge), we will read them one by one. For this purpose, we need a blockchain iterator:
type BlockchainIterator struct {
currentHash []byte
db *bolt.DB
}
An iterator will be created every time we want to iterate over the blocks in the blockchain and it will store the block hash of the current iteration and the connection to the database. Because of the latter, the iterator is logically bound to the blockchain (this is an instance
Blockchain
that stores the connection to the database) and, thus, is created in the method Blockchain
:func (bc *Blockchain) Iterator() *BlockchainIterator {
bci := &BlockchainIterator{bc.tip, bc.db}
return bci
}
Note that the iterator first points to the tip of the blockchain, so the blocks will be received from top to bottom, from the newest to the oldest. In fact, choosing a tip means “voting” for the blockchain . The blockchain can have several branches and the longest of them is considered the main one. After receiving the tip (it can be any block in the blockchain), we can recreate the entire blockchain and find its length, and the work necessary to build it. This fact also means that the tip is a kind of blockchain identifier.
BlockchainIterator
does only one thing: returns the next block from the blockchain.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.currentHash)
block = DeserializeBlock(encodedBlock)
return nil
})
i.currentHash = block.PrevBlockHash
return block
}
That's all about the database!
Command Line Interface (CLI)
So far, our implementation does not provide us with any interface for interacting with the program: we just ran
NewBlockchain, bc.AddBlock
in main
. It's time to improve it! We want to have the following commands:blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain
All command line related operations will be handled by the structure
CLI
type CLI struct {
bc *Blockchain
}
The "entry point" of the structure is a function
Run
func (cli *CLI) Run() {
cli.validateArgs()
addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
addBlockData := addBlockCmd.String("data", "", "Block data")
switch os.Args[1] {
case "addblock":
err := addBlockCmd.Parse(os.Args[2:])
case "printchain":
err := printChainCmd.Parse(os.Args[2:])
default:
cli.printUsage()
os.Exit(1)
}
if addBlockCmd.Parsed() {
if *addBlockData == "" {
addBlockCmd.Usage()
os.Exit(1)
}
cli.addBlock(*addBlockData)
}
if printChainCmd.Parsed() {
cli.printChain()
}
}
We use the standard flag package to parse command line arguments.
addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
addBlockData := addBlockCmd.String("data", "", "Block data")
To get started, we create two subcommands
addblock
and printchain
then add a flag -data
to the first. printchain
does not require any flags.switch os.Args[1] {
case "addblock":
err := addBlockCmd.Parse(os.Args[2:])
case "printchain":
err := printChainCmd.Parse(os.Args[2:])
default:
cli.printUsage()
os.Exit(1)
}
Then we check the command specified by the user and parse the associated subcommand.
if addBlockCmd.Parsed() {
if *addBlockData == "" {
addBlockCmd.Usage()
os.Exit(1)
}
cli.addBlock(*addBlockData)
}
if printChainCmd.Parsed() {
cli.printChain()
}
Next, we check which subcommand we parsed and run the associated function.
func (cli *CLI) addBlock(data string) {
cli.bc.AddBlock(data)
fmt.Println("Success!")
}
func (cli *CLI) printChain() {
bci := cli.bc.Iterator()
for {
block := bci.Next()
fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash)
fmt.Printf("Data: %s\n", block.Data)
fmt.Printf("Hash: %x\n", block.Hash)
pow := NewProofOfWork(block)
fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
fmt.Println()
if len(block.PrevBlockHash) == 0 {
break
}
}
}
This code is similar to the one that was before. The only difference is what we use now
BlockchainIterator
to iterate over the blocks in the blockchain. Also, do not forget to change the function
main
accordingly:func main() {
bc := NewBlockchain()
defer bc.db.Close()
cli := CLI{bc}
cli.Run()
}
Note that a new one
Blockchain
is created regardless of which command line arguments were passed. That's all! Check that everything works as we expect:
$ 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
(the sound of opening a beer can )
References
Original article
The first part of a series of articles
Sources
Bitcoin Core Data Storage
BoltDB
encoding / gob
flag