從Go語言編碼角度解釋實現簡易區塊鏈——打造公鏈

轉載請註明出處:http://www.javashuo.com/article/p-sfaggmbx-en.htmlhtml

區塊鏈技術

人們能夠用許多不一樣的方式解釋區塊鏈技術,其中經過加密貨幣來看區塊鏈一直是主流。大多數人接觸區塊鏈技術都是從比特幣談起,但比特幣僅僅是衆多加密貨幣的一種。python

到底什麼是區塊鏈技術?程序員

從金融學相關角度來看,區塊鏈是一種存儲數據的方式,去中心化的數據庫,應用到比特幣也就是去中心化帳本;算法

從密碼學角度來看,區塊鏈是一種傳遞價值的協議;數據庫

從計算機科學的角度來看,區塊鏈只是一種數據結構;編程

不一樣於咱們平時接觸的手機電腦,先有系統,而後纔會在系統裏開發各類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更先進,但一涉及生態又是另外一回事了。

下一節,將實現區塊鏈的交易機制,逐步完善整個區塊鏈。

相關文章
相關標籤/搜索