轉載請注明出處:https://www.cnblogs.com/ustca/p/11747826.html
區塊鏈技術
人們可以用許多不同的方式解釋區塊鏈技術,其中通過加密貨幣來看區塊鏈一直是主流。大多數人接觸區塊鏈技術都是從比特幣談起,但比特幣僅僅是眾多加密貨幣的一種。
到底什么是區塊鏈技術?
從金融學相關角度來看,區塊鏈是一種存儲數據的方式,去中心化的數據庫,應用到比特幣也就是去中心化賬本;
從密碼學角度來看,區塊鏈是一種傳遞價值的協議;
從計算機科學的角度來看,區塊鏈只是一種數據結構;
不同於我們平時接觸的手機電腦,先有系統,然后才會在系統里開發各種APP應用。09年第一枚比特幣誕生,15年也就是6年之后,才有區塊鏈這個概念。許多人了解區塊鏈,都是從金融學或者密碼學的角度作為切入,從中本聰的比特幣白皮書開始談起。通過金融角度看待區塊鏈,總有種霧里看花的感覺,從密碼學角度看區塊鏈,分析粒度又太細了。就計算機而言,我們所需要的只是看到這項技術的本質。當技術與金融一旦掛鈎,往往就會變成玄學,區塊鏈也是這樣,當這項技術概念被從比特幣中抽離出來的時候,比特幣就只不過是這項技術的一個Demo而已。
接下來拋開金融學的概念,密碼學的理論,不關心區塊鏈金融,不研究區塊鏈安全,只分析區塊鏈技術,從計算機科學來了解區塊鏈的模型。
本質是一種數據結構
下面將通過Go語言,來編碼實現一個簡易的區塊鏈模型,模型分為不同階段,本文先實現區塊鏈簡易的數據結構、工作量證明共識、數據庫持久化存儲以及命令行接口。
即使沒學過Go語言,也可以立刻上手。Go語言語法與python類似,卻又是編譯型而非解釋型語言,有着媲美C的高性能,是區塊鏈開發主流語言。
對於程序員而言,需要用邏輯解釋的問題,通過代碼結合語言特性來描述是最簡單易懂的。實際上,當你讀完這篇文章,不只是Go語言,你可以用你熟悉的其他語言實現同樣的效果,因為這只是個簡簡單單的數據結構。從學習角度來說,都是C類語法,所以你只需要看懂,不需要會寫,就可以轉換到你熟悉的語言實現。
接下來的代碼,是在多個文件中實現的,這里有必要先簡單討論下Go語言的一些語言特性,這有助於后續的代碼邏輯理解。
"一個程序就是一個世界,有許多不同的對象",從C語言不完全面向對象,到Java的面向對象,再到Go語言的不純粹面向對象,都與現實世界中抽離出的對象這個概念緊密相連。Go語言實際上是沒有對象的面向對象編程,因為從語法角度上來說她沒有”類“。最吸引人的不是Go擁有的特征,而是那些被故意遺漏的特征。
為什么你要創造一種從理論上來說,並不令人興奮的語言?
因為它非常有用。 —— Rob Pike
C語言沒有完全的面向對象,她在這方面沒有完全的語法約束,而后來的Java做了這種約束,到如今的Golang去除了這些約束。從語言本身角度,這並不讓人興奮,但確實非常好用。
“如果你可以重新做一次Java,你會改變什么?”
“我會去掉類class,” 他回答道。
在笑聲消失后,他解釋道,真正的問題不是類class本身,而是“實現”的繼承(類之間extends的關系)。接口的繼承(implements的關系)是更可取的方式。
只要有可能,你就應該盡可能避免“實現”的繼承。
—— James Gosling(Java之父)
Go可以包含對象的基本功能:標識、屬性和特性,所以是基於對象的。如果一種語言是基於對象的,並且具有多態性和繼承性,那么它被認為是面向對象的。
那么Go語言是如何實現繼承的?
Go通過嚴格地遵循了符合繼承原則的組合方式,明確地避免了繼承,她使用嵌入類型來實現組合。
繼承把“知識”向下傳遞,組合把“知識”向上拉升
—— Steve Francia
Go語言又如何實現多態?
Go明確避免了子類型和重載,尚未提供泛型,利用接口提供了多態功能。
總而言之,Go用盡可能少的語法規則,實現了盡可能多的語言特性。這使得go語言面向對象非常簡潔,去掉了傳統OOP語言的繼承,方法重載,構造函數和析構函數等等。
我們之后的編碼,分多個文件完成的原因也正是因為這些,我們將感覺像對象的多個文件當作對象,只要它走起路來像鴨子,叫起來像鴨子,那么我們就認為它是一只鴨子。(實際上,一些文件中用到了Go語言提供的面向對象特性,但這里不做詳細解釋區分)
結構描述
這是我們要編寫的main.go文件:
package main
func main() {
bc := NewBlockchain()
defer bc.db.Close()
cli := CLI{bc}
cli.Run()
}
我們需要編寫的還有
block.go、 blockchain.go、 cli.go、 proofofwork.go、 utils.go
每部分代碼都很簡短,分文件是為了通過盡量簡單的方式做出我們想看到的對象。
你可以把這些文件看成一個個”類“,也可以把這些文件看成某些業務邏輯關系,因為Go本身沒有做出限制你不能這么用,你可以按照你感覺像的邏輯去理解。
那么我們究竟要寫的是什么樣的結構?
你可以把我們的任務理解成,先構造一種結點,然后再將這種結點鏈接成鏈表,只不過鏈表結點的加入需要滿足一種特殊條件——工作量證明共識機制(只不過是暴力搜索湊出一個特定條件的字符串而已,讓你不能隨隨便便往鏈表添加數據,美其名曰工作量證明,跟計算機算力較勁...所謂的挖礦),最后我們需要把完成的鏈表保存到數據庫,本節所要討論的簡易區塊鏈從編碼角度來看只是這么個簡單結構而已。從非編碼角度去分析區塊鏈,往往就復雜了,可能還沒必要,因為那些不是你想理解的。
編碼實現
實現順序為:
- utils.go 提供工具函數
- proofofwork.go 提供工作量證明相關的函數
- block.go 提供關於結點的函數
- blockchain.go 提供關於鏈表的函數
- cli.go 提供命令行操作控制的函數(為了方便使用命令行執行程序)
- main.go 提供程序入口
你可以理解成是自底向上的順序,也可以理解成某種業務依賴關系的順序,可以自由的用你熟悉的方式去理解。
untils.go
因為我們要用這個go語言沒提供的類型轉換,所以有了untils.go
package main
import (
"bytes"
"encoding/binary"
"log"
)
// IntToHex converts an int64 to a byte array
func IntToHex(num int64) []byte {
buff := new(bytes.Buffer)
err := binary.Write(buff, binary.BigEndian, num)
if err != nil {
log.Panic(err)
}
return buff.Bytes()
}
proofofwork.go
假設現在有了一個區塊鏈,大家都能隨便往區塊鏈上寫入自己的塊,那么這個區塊鏈就沒什么價值可言。所以需要設置一個門檻,當你滿足一定條件時,才允許你往區塊鏈上添加新的數據,這樣付出了成本才有了價值。而區塊鏈的門檻,就是一個被叫做工作量證明共識的東西,這是一個所有礦工(想獲得添加區塊資格的人)都認同的門檻。
比如我們當前實現的簡易區塊鏈,考慮到執行時間問題,我們的門檻就是找到一串長度為64的字符串,而這串字符串的前6位為0.
這個字符串不是隨便構造的,不然可以直接指定前6位為0,后面都為隨機的字符串,那樣就沒有價值可言了,所以我們還需要一個構造的規則。
怎么讓找到某種字串具有工作難度?容易想到的是尋找哈希值。
好比你現在知道了一個用戶密碼的哈希值,想去找出用戶密碼,區塊鏈所謂的工作量證明、挖礦,都只是去找一個大家共識承認的哈希值而已(...與計算機算力斗智斗勇,手動滑稽)。
我們本節用到的字符串獲取規則,是采用sha256算法。關於sha256算法的具體原理,或者為什么選擇該哈希算法,不是我們當前要考慮的問題,對密碼學有興趣的可以自行拓展了解。要進行哈希運算的字符串,也有一定的共識規則:
字符串 = 前一個區塊的哈希值 + 當前塊數據 + 時間戳 + targetBits(后面會解釋)+ 自由參數。
實際挖礦我們只是想找到有價值的自由參數,其余值都是給定的。
了解完這些概念,接下來就可以直接看代碼了:
package main
import (
"bytes"
"crypto/sha256"
"fmt"
"math"
"math/big"
)
var (
maxNonce = math.MaxInt64
)
const targetBits = 24
// ProofOfWork represents a proof-of-work
type ProofOfWork struct {
block *Block
target *big.Int
}
// NewProofOfWork builds and returns a ProofOfWork
func NewProofOfWork(b *Block) *ProofOfWork {
target := big.NewInt(1)
target.Lsh(target, uint(256-targetBits))
pow := &ProofOfWork{b, target}
return pow
}
func (pow *ProofOfWork) prepareData(nonce int) []byte {
data := bytes.Join(
[][]byte{
pow.block.PrevBlockHash,
pow.block.Data,
IntToHex(pow.block.Timestamp),
IntToHex(int64(targetBits)),
IntToHex(int64(nonce)),
},
[]byte{},
)
return data
}
// Run performs a proof-of-work
func (pow *ProofOfWork) Run() (int, []byte) {
var hashInt big.Int
var hash [32]byte
nonce := 0
fmt.Printf("Mining the block containing \"%s\"\n", pow.block.Data)
for nonce < maxNonce {
data := pow.prepareData(nonce)
hash = sha256.Sum256(data)
// fmt.Printf("\n%x", hash)
hashInt.SetBytes(hash[:])
if hashInt.Cmp(pow.target) == -1 {
break
} else {
nonce++
}
}
// fmt.Print("\n\n")
return nonce, hash[:]
}
// Validate validates block's PoW
func (pow *ProofOfWork) Validate() bool {
var hashInt big.Int
data := pow.prepareData(pow.block.Nonce)
hash := sha256.Sum256(data)
hashInt.SetBytes(hash[:])
isValid := hashInt.Cmp(pow.target) == -1
return isValid
}
關於全局變量targetBits,是用做規定當前的“共識”,24位代表哈希值前24位為0,也就是6個十六進制的0,才算有效哈希值(礦)。
我們定義了一個proofofwork的結構體,包含兩個指針,一個指向區塊,一個指向閾值。
關於閾值,我們可以理解成,既然需要找到一個哈希值前24位為0,那么把它當作二進制數字看的話,一個第23位為1,其余位為0的二進制數就是我們要找的哈希值上限(閾值)。只要我們找到的哈希值小於這個數,那么該哈希值的前24位肯定為0.
除此之外,
- NewProofOfWork函數,可以當成“類”的構造函數,表示我們要初始化一個對象來挖礦了
- prepareData函數用來准備進行哈希運算的字符串
- Run函數使用從0開始的整數作為自由參數,進行挖礦(找到一個比閾值小的哈希值)
- Validate函數用來驗證區塊哈希值是否滿足條件
函數都很好理解,簡單到只需要看懂語法,不需要過多解釋邏輯,這就是最本質的區塊鏈。
block.go
主要包含以下內容:
- Block結構體,包含時間戳、數據、前塊哈希值、當前哈希與自由參數
- NewBlock函數,初始化一個新塊
- NewGenesisBlock函數,初始化一個創世塊(區塊鏈的第一個塊)
- Serialize與DeserializeBlock函數,對Block進行序列化與反序列化,用戶實現數據庫存儲
package main
import (
"bytes"
"encoding/gob"
"log"
"time"
)
// Block keeps block headers
type Block struct {
Timestamp int64
Data []byte
PrevBlockHash []byte
Hash []byte
Nonce int
}
// NewBlock creates and returns Block
func NewBlock(data string, prevBlockHash []byte) *Block {
block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}, 0}
pow := NewProofOfWork(block)
nonce, hash := pow.Run()
block.Hash = hash[:]
block.Nonce = nonce
return block
}
// NewGenesisBlock creates and returns genesis Block
func NewGenesisBlock() *Block {
return NewBlock("Genesis Block", []byte{})
}
// Serialize serializes the block
func (b *Block) Serialize() []byte {
var result bytes.Buffer
encoder := gob.NewEncoder(&result)
err := encoder.Encode(b)
if err != nil {
log.Panic(err)
}
return result.Bytes()
}
// DeserializeBlock deserializes a block
func DeserializeBlock(d []byte) *Block {
var block Block
decoder := gob.NewDecoder(bytes.NewReader(d))
err := decoder.Decode(&block)
if err != nil {
log.Panic(err)
}
return &block
}
blockchain.go
這里要引入數據庫,用來存儲我們當前的區塊鏈,不使用數據庫也可以,但那樣每次都需要重新運行查看,無法持久化。
本節使用到的boltdb是go實現的一個k-v數據庫。
package main
import (
"fmt"
"log"
"bolt-master"
)
const dbFile = "blockchain.db"
const blocksBucket = "blocks"
// Blockchain keeps a sequence of Blocks
type Blockchain struct {
tip []byte
db *bolt.DB
}
// BlockchainIterator is used to iterate over blockchain blocks
type BlockchainIterator struct {
currentHash []byte
db *bolt.DB
}
// AddBlock saves provided data as a block in the blockchain
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
})
if err != nil {
log.Panic(err)
}
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())
if err != nil {
log.Panic(err)
}
err = b.Put([]byte("l"), newBlock.Hash)
if err != nil {
log.Panic(err)
}
bc.tip = newBlock.Hash
return nil
})
}
// Iterator ...
func (bc *Blockchain) Iterator() *BlockchainIterator {
bci := &BlockchainIterator{bc.tip, bc.db}
return bci
}
// Next returns next block starting from the tip
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
})
if err != nil {
log.Panic(err)
}
i.currentHash = block.PrevBlockHash
return block
}
// NewBlockchain creates a new Blockchain with genesis Block
func NewBlockchain() *Blockchain {
var tip []byte
db, err := bolt.Open(dbFile, 0600, nil)
if err != nil {
log.Panic(err)
}
err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
if b == nil {
fmt.Println("No existing blockchain found. Creating a new one...")
genesis := NewGenesisBlock()
b, err := tx.CreateBucket([]byte(blocksBucket))
if err != nil {
log.Panic(err)
}
err = b.Put(genesis.Hash, genesis.Serialize())
if err != nil {
log.Panic(err)
}
err = b.Put([]byte("l"), genesis.Hash)
if err != nil {
log.Panic(err)
}
tip = genesis.Hash
} else {
tip = b.Get([]byte("l"))
}
return nil
})
if err != nil {
log.Panic(err)
}
bc := Blockchain{tip, db}
return &bc
}
blockchain文件的實現:
- Blockchain結構體,定義了一個字節數組,一個數據庫對象指針,用於連接數據庫進行操作
- BlockchainIterator結構體,用於迭代過程
- AddBlock函數,在數據庫中鏈接一個新的區塊
- Iterator迭代器,用於迭代遍歷區塊鏈
- Next函數,用於遍歷中尋找后一個區塊
- NewBlockchain函數,初始化一個新的區塊鏈
總之,blockchain只是將區塊鏈存入了數據庫,有向數據庫初始化一個新區塊鏈與增加區塊的功能。
cli.go
cli.go只是方便了在命令行下運行這些代碼
package main
import (
"flag"
"fmt"
"log"
"os"
"strconv"
)
// CLI responsible for processing command line arguments
type CLI struct {
bc *Blockchain
}
func (cli *CLI) printUsage() {
fmt.Println("Usage:")
fmt.Println(" addblock -data BLOCK_DATA - add a block to the blockchain")
fmt.Println(" printchain - print all the blocks of the blockchain")
}
func (cli *CLI) validateArgs() {
if len(os.Args) < 2 {
cli.printUsage()
os.Exit(1)
}
}
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
}
}
}
// Run parses command line arguments and processes commands
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:])
if err != nil {
log.Panic(err)
}
case "printchain":
err := printChainCmd.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.addBlock(*addBlockData)
}
if printChainCmd.Parsed() {
cli.printChain()
}
}
主要包括:
- CLI結構體,含有一個指向區塊鏈的指針
- printUsage,打印命令行使用說明
- validateArgs,驗證命令行參數
- addBlock,添加一個區塊(這里有點像三層架構的業務邏輯層)
- printChain,打印當前區塊鏈
- Run,CLI函數功能選擇
main.go
main函數入口
package main
func main() {
bc := NewBlockchain()
defer bc.db.Close()
cli := CLI{bc}
cli.Run()
}
運行效果
"Pay 0.0013 BTC for a coffee" 挖礦的時間比"Send 1 BTC to Tom"多了一個數量級,程序根據你的輸入數據,計算哈希所需要的時間是不確定的,感興趣的可以記錄挖礦時間然后輸出。(不要打印挖礦過程,io輸出會讓處理時間多出好幾個量級,會以為是無限循環)
小結
本節實現了區塊鏈的簡易數據結構、工作量證明機制、持久化以及命令行接口。實際上核心只有proofofwork.go的工作量證明與block.go的區塊鏈結構,所謂挖礦也只是找一個有價值的哈希值而已。但實際的區塊鏈不僅僅只是這些,之后將會結合比特幣實現交易與地址機制。之所以結合比特幣實現,是因為比特幣是區塊鏈技術的一個成功Demo,並不是炒作比特幣概念。再好的概念,與現有法律和監管體系不兼容,也無法成為主流。做個不恰當的類比,有點像C#與JAVA,就語言本身C#比JAVA更先進,但一涉及生態又是另一回事了。