从0到1简易区块链开发手册V0.3-数据持久化与创世区块
Author: brucefeng
Email: brucefeng@brucefeng.com
編程語言:Golang
1.BoltDB簡介
Bolt是一個純粹Key/Value模型的程序。該項目的目標是為不需要完整數據庫服務器(如Postgres或MySQL)的項目提供一個簡單,快速,可靠的數據庫。
BoltDB只需要將其鏈接到你的應用程序代碼中即可使用BoltDB提供的API來高效的存取數據。而且BoltDB支持完全可序列化的ACID事務,讓應用程序可以更簡單的處理復雜操作。
其源碼地址為:https://github.com/boltdb/bolt
2.BoltDB特性
BoltDB設計源于LMDB,具有以下特點:
- 使用Go語言編寫
- 不需要服務器即可運行
-
支持數據結構
- 直接使用API存取數據,沒有查詢語句;
- 支持完全可序列化的ACID事務,這個特性比LevelDB強;
- 數據保存在內存映射的文件里。沒有wal、線程壓縮和垃圾回收;
- 通過COW技術,可實現無鎖的讀寫并發,但是無法實現無鎖的寫寫并發,這就注定了讀性能超高,但寫性能一般,適合與讀多寫少的場景。
BoltDB是一個Key/Value(鍵/值)存儲,這意味著沒有像SQL RDBMS(MySQL,PostgreSQL等)中的表,沒有行,沒有列。相反,數據作為鍵值對存儲(如在Golang Maps中)。鍵值對存儲在Buckets中,它們旨在對相似的對進行分組(這與RDBMS中的表類似)。因此,為了獲得Value(值),需要知道該Value所在的桶和鑰匙。
3.BoltDB簡單使用
//通過go get下載并import import "github.com/boltdb/bolt"3.1 打開或創建數據庫
db, err := bolt.Open("my.db", 0600, nil) if err != nil {log.Fatal(err) } defer db.Close()- 執行注意點
如果通過goland程序運行創建的my.db會保存在
GOPATH /src/Project目錄下 如果通過go build main.go ; ./main 執行生成的my.db,會保存在當前目錄GOPATH /src/Project/package下3.2 數據庫操作
(1) 創建數據庫表與數據寫入操作
//1. 調用Update方法進行數據的寫入 err = db.Update(func(tx *bolt.Tx) error { //2.通過CreateBucket()方法創建BlockBucket(表),初次使用創建b, err := tx.CreateBucket([]byte("BlockBucket"))if err != nil {return fmt.Errorf("Create bucket :%s", err)}//3.通過Put()方法往表里面存儲一條數據(key,value),注意類型必須為[]byteif b != nil {err := b.Put([]byte("l"), []byte("Send $100 TO Bruce"))if err != nil {log.Panic("數據存儲失敗..")}}return nil })//數據Update失敗,退出程序 if err != nil {log.Panic(err) }(2) 數據寫入
//1.打開數據庫 db, err := bolt.Open("my.db", 0600, nil) if err != nil {log.Fatal(err) } defer db.Close()err = db.Update(func(tx *bolt.Tx) error {//2.通過Bucket()方法打開BlockBucket表b := tx.Bucket([]byte("BlockBucket")) //3.通過Put()方法往表里面存儲數據if b != nil {err := b.Put([]byte("l"), []byte("Send $200 TO Fengyingcong"))err = b.Put([]byte("ll"), []byte("Send $100 TO Bruce"))if err != nil {log.Panic("數據存儲失敗..")}}return nil }) //更新失敗 if err != nil {log.Panic(err) }(3) 數據讀取
//1.打開數據庫 db, err := bolt.Open("my.db", 0600, nil) if err != nil {log.Fatal(err) } defer db.Close()//2.通過View方法獲取數據 err = db.View(func(tx *bolt.Tx) error {//3.打開BlockBucket表,獲取表對象b := tx.Bucket([]byte("BlockBucket"))//4.Get()方法通過key讀取valueif b != nil {data := b.Get([]byte("l"))fmt.Printf("%s\n", data)data = b.Get([]byte("ll"))fmt.Printf("%s\n", data)}return nil })if err != nil {log.Panic(err) }4.通過BoltDB存儲區塊
該代碼包含對BoltDB的數據庫創建,表創建,區塊添加,區塊查詢操作
//1.創建一個區塊對象block block := BLC.NewBlock("Send $500 to Tom", 1, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})//2. 打印區塊對象相關信息 fmt.Printf("區塊的Hash信息為:\t%x\n", block.Hash) fmt.Printf("區塊的數據信息為:\t%v\n", string(block.Data)) fmt.Printf("區塊的隨機數為:\t%d\n", block.Nonce)//3. 打開數據庫 db, err := bolt.Open("my.db", 0600, nil) if err != nil {log.Fatal(err) } defer db.Close()//4. 更新數據 err = db.Update(func(tx *bolt.Tx) error {//4.1 打開BlockBucket表對象b := tx.Bucket([]byte("blocks")) //4.2 如果表對象不存在,創建表對象if b == nil {b, err = tx.CreateBucket([]byte("blocks"))if err != nil {log.Panic("Block Table Create Failed")}}//4.3 往表里面存儲一條數據(key,value)err = b.Put([]byte("l"), block.Serialize())if err != nil {log.Panic("數據存儲失敗..")}return nil })//更新失敗,返回錯誤 if err != nil {log.Panic("數據更新失敗") }//5. 查看數據 err = db.View(func(tx *bolt.Tx) error {//5.1打開BlockBucket表對象b := tx.Bucket([]byte("blocks"))if b != nil {//5.2 取出key=“l”對應的valueblockData := b.Get([]byte("l"))//5.3反序列化 block := BLC.DeserializeBlock(blockData)//6. 打印區塊對象相關信息fmt.Printf("區塊的Hash信息為:\t%x\n", block.Hash)fmt.Printf("區塊的數據信息為:\t%v\n", string(block.Data))fmt.Printf("區塊的隨機數為:\t%d\n", block.Nonce)}return nil }) //數據查看失敗 if err != nil {log.Panic("數據更新失敗") }五.創建創世區塊
1.概念
北京時間2009年1月4日2時15分5秒,比特幣的第一個區塊誕生了。隨著時間往后推移,不斷有新的區塊被添加到鏈上,所有后續區塊都可以追溯到第一個區塊。第一個區塊就被人們稱為創世區塊。
2. 工作量證明
在比特幣世界中,獲取區塊記賬權的過程稱之為挖礦,一個礦工成功后,他會把之前打包好的網絡上的交易記錄到一頁賬本上,同步給其他人。因為這個礦工能夠最先計算出超難數學題的正確答案,說明這個礦工付出了工作量,是一個有權利記賬的人,因此其他人也會同意這一頁賬單。這種依靠工作量來證明記賬權,大家來達成共識的機制叫做“工作量證明”,簡而言之結果可以證明你付出了多少工作量。Proof Of Work簡稱“PoW”,關于其原理跟代碼實現,我們在后面的代碼分析中進行講解說明。
2.1 定義結構體
type ProofOfWork struct {Block *Block //要驗證的blockTarget *big.Int //目標hash }2.2 創建工作量證明對象
const TargetBit = 16 //目標哈希的0個個數,16,20,24,28 func NewProofOfWork(block *Block) *ProofOfWork {//1.創建pow對象pow := &ProofOfWork{}//2.設置屬性值pow.Block = blocktarget := big.NewInt(1) // 目標hash,初始值為1target.Lsh(target, 256-TargetBit) //左移256-16pow.Target = targetreturn pow}我們首先設定一個難度系數值為16,即目標哈希前導0的個數,0的個數越多,挖礦難度越大,此處我們創建一個函數NewProofOfWork用于返回Pow對象。
目標Hash的長度為256bit,通過64個16進制byte進行展示,如下所示為前導0為16/4=4的哈希
0000c01d342fc51cb030f93979343de70ab771855dd8ca28e6f5888737759747- 通過big.NewInt創建一個BigInt對象target
- 對target進行通過左移(256-TargetBit)位操作
2.3 將int64類型轉[]byte
func IntToHex(num int64) []byte {buff := new(bytes.Buffer)//將二進制數據寫入w//err := binary.Write(buff, binary.BigEndian, num)if err != nil {log.Panic(err)}//轉為[]byte并返回return buff.Bytes() }通過func Write(w io.Writer, order ByteOrder, data interface{}) error方法將一個int64的整數轉為二進制后,每8bit一個byte,轉為[]byte
2.4 拼接區塊屬性數據
func (pow *ProofOfWork) prepareData(nonce int64) []byte {data := bytes.Join([][]byte{IntToHex(pow.Block.Height),pow.Block.PrevBlockHash,IntToHex(pow.Block.TimeStamp),pow.Block.HashTransactions(),IntToHex(nonce),IntToHex(TargetBit),}, []byte{})return data}通過bytes.Join方法將區塊相關屬性進行拼接成字節數組
2.5 "挖礦"方法
func (pow *ProofOfWork) Run() ([]byte, int64) {var nonce int64 = 0var hash [32]bytefor {//1.根據nonce獲取數據data := pow.prepareData(nonce)//2.生成hashhash = sha256.Sum256(data) //[32]bytefmt.Printf("\r%d,%x", nonce, hash)//3.驗證:和目標hash比較/*func (x *Int) Cmp(y *Int) (r int)Cmp compares x and y and returns:-1 if x < y0 if x == y+1 if x > y目的:target > hashInt,成功*/hashInt := new(big.Int)hashInt.SetBytes(hash[:])if pow.Target.Cmp(hashInt) == 1 {break}nonce++}fmt.Println()return hash[:], nonce}代碼思路
- 設置nonce值:0,1,2.......
- block-->拼接數組,產生hash
- 比較實際hash和pow的目標hash
不斷更改nonce的值,計算hash,直到小于目標hash。
2.6 驗證區塊
func (pow *ProofOfWork) IsValid() bool {hashInt := new(big.Int)hashInt.SetBytes(pow.Block.Hash)return pow.Target.Cmp(hashInt) == 1 }判斷方式同挖礦中的策略
3.區塊創建
3.1 定義結構體
type Block struct {//字段屬性//1.高度:區塊在區塊鏈中的編號,第一個區塊也叫創世區塊,一般設定為0Height int64//2.上一個區塊的Hash值PrevBlockHash []byte//3.數據:Txs,交易數據Txs []*Transaction//4.時間戳TimeStamp int64//5.自己的hashHash []byte//6.NonceNonce int64 }關于屬性的定義,在代碼的注釋中比較清晰了,需要提一下的就是創世區塊的PrevBlockHash一般設定為0 ,高度也一般設定為0
3.2 創建創世區塊
func CreateGenesisBlock(txs []*Transaction) *Block{return NewBlock(txs,make([]byte,32,32),0) }設定創世區塊的PrevBlockHash為0,區塊高度為0
3.3 序列化區塊對象
func (block *Block) Serialize()[]byte{//1.創建一個buffvar buf bytes.Buffer//2.創建一個編碼器encoder:=gob.NewEncoder(&buf)//3.編碼err:=encoder.Encode(block)if err != nil{log.Panic(err)}return buf.Bytes() }通過gob庫的Encode方法將Block對象序列化成字節數組,用于持久化存儲
3.4 字節數組反序列化
func DeserializeBlock(blockBytes [] byte) *Block{var block Block//1.先創建一個readerreader:=bytes.NewReader(blockBytes)//2.創建×××decoder:=gob.NewDecoder(reader)//3.解碼err:=decoder.Decode(&block)if err != nil{log.Panic(err)}return &block }定義一個函數,用于將[]byte反序列化為block對象
4.區塊鏈創建
4.1 定義結構體
type BlockChain struct {DB *bolt.DB //對應的數據庫對象Tip [] byte //存儲區塊中最后一個塊的hash值 }定義區塊鏈結構體屬性DB用于存儲對應的數據庫對象,Tip用于存儲區塊中最后一個塊的Hash值
4.2 判斷數據庫是否存在
const DBName = "blockchain.db" //數據庫的名字 const BlockBucketName = "blocks" //定義bucket定義數據庫名字以及定義用于存儲區塊數據的bucket(表)名
func dbExists() bool {if _, err := os.Stat(DBName); os.IsNotExist(err) { return false //表示文件不存在}return true //表示文件存在 }需要注意IsNotExist返回true,則表示不存在成立,返回值為true,則dbExists函數的返回值則需要返回false,否則,返回true
4.3 創建帶有創世區塊的區塊鏈
func CreateBlockChainWithGenesisBlock(address string) {/*1.判斷數據庫如果存在,直接結束方法2.數據庫不存在,創建創世區塊,并存入到數據庫中*/if dbExists() {fmt.Println("數據庫已經存在,無法創建創世區塊")return}//數據庫不存在fmt.Println("數據庫不存在")fmt.Println("正在創建創世區塊")/*1.創建創世區塊2.存入到數據庫中*///創建一個txs--->CoinBasetxCoinBase := NewCoinBaseTransaction(address)genesisBlock := CreateGenesisBlock([]*Transaction{txCoinBase})db, err := bolt.Open(DBName, 0600, nil)if err != nil {log.Panic(err)}defer db.Close()err = db.Update(func(tx *bolt.Tx) error {//創世區塊序列化后,存入到數據庫中b, err := tx.CreateBucketIfNotExists([]byte(BlockBucketName))if err != nil {log.Panic(err)}if b != nil {err = b.Put(genesisBlock.Hash, genesisBlock.Serialize())if err != nil {log.Panic(err)}b.Put([]byte("l"), genesisBlock.Hash)}return nil})if err != nil {log.Panic(err)}//return &BlockChain{db, genesisBlock.Hash} }代碼分析
(1) 判斷數據庫是否存在,如果不存在,證明還沒有創建創世區塊,如果存在,則提示創世區塊已存在,直接返回
if dbExists() {fmt.Println("數據庫已經存在,無法創建創世區塊")return}(2) 如果數據庫不存在,則提示開始調用相關函數跟方法創建創世區塊
fmt.Println("數據庫不存在")fmt.Println("正在創建創世區塊")(3) 創建一個交易數組Txs
關于交易這一部分內容,將在后面一個章節中進行詳細說明,篇幅會非常長,這也是整個課程體系中最為繁瑣,知識點最廣的地方,屆時慢慢分析
txCoinBase := NewCoinBaseTransaction(address)通過函數NewCoinBaseTransaction創建一個CoinBase交易
func NewCoinBaseTransaction(address string) *Transaction {txInput := &TxInput{[]byte{}, -1, nil, nil}txOutput := NewTxOutput(10, address)txCoinBaseTransaction := &Transaction{[]byte{}, []*TxInput{txInput}, []*TxOutput{txOutput}}//設置交易IDtxCoinBaseTransaction.SetID()return txCoinBaseTransaction }(4) 生成創世區塊
genesisBlock := CreateGenesisBlock([]*Transaction{txCoinBase})(5) 打開/創建數據庫
db, err := bolt.Open(DBName, 0600, nil)if err != nil {log.Panic(err)}defer db.Close()通過bolt.Open方法打開(如果不存在則創建)數據庫文件,注意數據庫關閉操作不能少,用defer實現延遲關閉。
(6) 將數據寫入數據庫
err = db.Update(func(tx *bolt.Tx) error {b, err := tx.CreateBucketIfNotExists([]byte(BlockBucketName))if err != nil {log.Panic(err)}if b != nil {err = b.Put(genesisBlock.Hash, genesisBlock.Serialize())if err != nil {log.Panic(err)}b.Put([]byte("l"), genesisBlock.Hash)}return nil})if err != nil {log.Panic(err)}通過db.Upadate方法進行數據更新操作
- 創建/打開存儲區塊的Bucket:BlockBucketName
- 將創世區塊序列化后存入Bucket中
- 通過Put方法更新K/V值(Key:區塊哈希,Value:區塊序列化后的字節數組)
- 通過Put方法更新Key為“l”的Value為最新區塊哈希值,此處即genesisBlock.Hash
5.命令行調用
func (cli *CLI) CreateBlockChain(address string) {CreateBlockChainWithGenesisBlock(address) }測試命令
$ ./mybtc createblockchain -address 1DHPNHKfk9uUdog2f2xBvx9dq4NxpF5Q4Q返回結果
數據庫不存在 正在創建創世區塊 32325,00005c7b4246aa88bd1f9664c665d6424d1522f569d981691ac2b5b5d15dd8d9本章節介紹了如何創建一個帶有創世區塊的區塊鏈,并持久化存儲至數據庫blockchain.db
$ ls BLC Wallets.dat blockchain.db main.go mybtc總結
以上是生活随笔為你收集整理的从0到1简易区块链开发手册V0.3-数据持久化与创世区块的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在win10系统下怎样快速切换任务视图
- 下一篇: 神经网络- receptive fiel