[译] 用 Go 编写你本身的区块链挖矿算法!

若是你对下面的教程有任何问题或者建议,加入咱们的 Telegram 消息群,能够问咱们全部你想问的!前端

随着最近比特币和以太坊挖矿大火,很容易让人好奇,这么大惊小怪是为何。对于加入这个领域的新人,他们会听到一些疯狂的故事:人们用 GPU 填满仓库,每月赚取价值数百万美圆的加密货币。电子货币挖矿究竟是什么?它是如何运做的?我如何能试着编写本身的挖矿算法?python

在这篇博客中,咱们将会带你解答上述每个问题,并最终完成一篇教你如何编写本身的挖矿算法的教程。咱们将展现给你的算法叫作工做量证实,它是比特币和以太坊这两个最流行的电子货币的基础。别急,咱们立刻将为你解释它是如何运做的。android

什么是电子货币挖矿ios

为了有价值,电子货币须要有必定的稀缺性。若是谁均可以随时生产出他们想要的任意多的比特币,那么做为货币,比特币就毫无价值了。(等一下,美国联邦储备不是这么作了么?打脸)比特币算法每十分钟将会发放一些比特币给网络中一个获胜成员,这样最多能够供给大约 122 年。因为定量的供应并非在最一开始就所有发行,这种发行时间表在必定程度上也控制了膨胀。随着时间流逝,发行地速度将愈来愈慢。git

决定胜者是谁并给出比特币的过程须要他完成必定的“工做”,并与同时也在作这个工做的人竞争。这个过程就叫作挖矿,由于它很像采矿工人花费时间完成工做并最终(但愿)找到黄金。github

比特币算法要求参与者,或者说节点,完成工做并相互竞争,来保证比特币不会发行过快。web

挖矿是如何运做的?算法

一次谷歌快速搜索“比特币挖矿如何运做?”将会给你不少页的答案,解释说比特币挖矿要求节点(你或者说你的电脑)解决一个很难的数学问题。虽然从技术上来讲这是对的,可是简单的把它称为一个“数学”问题太过有失实质而且太过陈腐。了解挖矿运做的内在原理是很是有趣的。为了学习挖矿运做原理,咱们首先要了解一下加密算法和哈希。编程

哈希加密的简短介绍json

单向加密的输入值是能读懂的明文,像是“Hello world”,并施加一个函数在它上面(也就是,数学问题)生成一个不可读的输出。这些函数(或者说算法)的性质和复杂度各有不一样。算法越复杂,逆运算解密就越困难。所以,加密算法能有力保护像用户密码和军事代码这类事物。

让咱们来看一个 SHA-256 的例子,它是一个很流行的加密算法。这个哈希网站能让你轻松计算 SHA-256 哈希值。让咱们对“Hello world”作哈希运算来看看将会获得什么:

试试对“Hello world”重复作哈希运算。你每次都将会获得一样的哈希值。给定一个程序相同的输入,反复计算将会获得相同的结果,这叫作幂等性。

加密算法一个基本的属性就是(输出值)靠逆运算很难推算输入值,可是(靠输入值)很容易就能验证输出值。例如,用上述的 SHA-256 哈希算法,其余人将 SHA-256 哈希算法应用于“Hello world”很容易的就能验证它确实输出同一个哈希值结果,可是想从这个哈希值结果推算出“Hello world”将会很是困难。这就是为何这类算法被称为单向

比特币采用 双 SHA-256 算法,这个算法就是简单的将 SHA-256 再一次应用于“Hello world”的 SHA-256 哈希值。在这篇教程中,咱们将只应用 SHA-256 算法。

挖矿

如今咱们知道了加密算法是什么了,咱们能够回到加密货币挖矿的问题上。比特币须要找到某种方法,让但愿获得比特币参与者“工做”,这样比特币就不会发行的过快。比特币的实现方式是:让参与者不停地作包含数字和字母的哈希运算,直到找到那个以特定位数的“0”开头的哈希结果。

例如,回到哈希网站而后对“886”作哈希运算。它将生成一个前缀包含三个零的哈希值。

可是,咱们怎么知道 “886” 能得出一个开头三个零的结果呢?这就是关键点了。在写这篇博客以前,咱们不知道。理论上,咱们须要遍历全部数字和字母的组合、测试结果,直到获得一个可以匹配咱们需求的三个零开头的结果。给你举一个简单的例子,咱们其实已经预先作了计算,发现 “886” 的哈希值是三个零开头的。

任何人均可以很轻松的验证 “886” 的哈希结果是三个零前缀,这个事实证实了:我作了大量的工做来对不少字母和数字的组合进行测试和检查以得到这个结果。因此,若是我是第一个获得这个结果的人,我就能经过证实我作了工做来获得比特币 - 证据就是任何人都能轻松验证 “886” 的哈希结果为三零前缀,正如我宣称的那样。这就是为何比特币共识算法被称为工做量证实

可是若是我很幸运,我第一次尝试就获得了三零前缀的结果呢?这几乎是不可能的,而且那些偶然状况下第一次就成功挖到了区块(证实他们作了工做)的节点会被那些作了额外工做来找到合适的哈希值的成千上万的其余区块所压倒。试试看,在计算哈希的网站上输入任意其余的字母和数字的组合。我打赌你不会获得一个三零开头的结果。

比特币的需求要比这个复杂不少(更多个零的前缀!),而且可以经过动态调节需求来确保工做不会太难也不会太容易。记住,目标是每十分钟发行一次比特币,因此若是太多人在挖矿,就须要将工做量证实调整的更难完成。这就叫难度调节(adjusting the difficulty)。为了达成咱们的目的,难度调整就意味着需求更多的零前缀。

如今你就知道了,比特币共识机制比单纯的“解决一个数学问题”要有意思的多!

足够多背景介绍了。咱们开始编程吧!

如今咱们已经有了足够多的背景知识,让咱们用工做量共识算法来创建本身的比特币程序。咱们将会用 Go 语言来写,由于咱们在 Coral Health 中使用它,而且说实话,棒极了

开始下一步以前,我建议读者读一下咱们以前的博文,Code your own blockchain in less than 200 lines of Go!并非硬性需求,可是下面的例子中咱们将讲的比较粗略。若是你须要更多细节,能够参考以前的博客。若是你对前面这篇很熟悉了,直接跳到下面的“工做量证实”章节。

结构

咱们将有一个 Go 服务,咱们就简单的把全部代码就放在一个 main.go 文件中。这个文件将会提供给咱们所需的全部的区块链逻辑(包括工做量证实算法),并包括全部 REST 接口的处理函数。区块链数据是不可改的,咱们只须要 GETPOST 请求。咱们将用浏览器发送 GET 请求来观察数据,并使用 Postman 来发送 POST 请求给新区块(curl 也一样好用)。

引包

咱们从标准的引入操做开始。确保使用 go get 来获取以下的包

github.com/davecgh/go-spew/spew 在终端漂亮地打印出你的区块链

github.com/gorilla/mux 一个使用方便的层,用来链接你的 web 服务

github.com/joho/godotenv 在根目录的 .env 文件中读取你的环境变量

让咱们在根目录下建立一个 .env 文件,它仅包含一个咱们一下子将会用到的环境变量。在 .env 文件中写一行:ADDR=8080

对包做出声明,并在根目录的 main.go 定义引入:

package main

import (
        "crypto/sha256"
        "encoding/hex"
        "encoding/json"
        "fmt"
        "io"
        "log"
        "net/http"
        "os"
        "strconv"
        "strings"
        "sync"
        "time"

        "github.com/davecgh/go-spew/spew"
        "github.com/gorilla/mux"
        "github.com/joho/godotenv"
)
复制代码

若是你读了在此以前的文章,你应该记得这个图。区块链中的区块能够经过比较区块的 previous hash 属性值和前一个区块的哈希值来被验证。这就是区块链保护自身完整性的方式以及黑客组织没法修改区块链历史记录的缘由。

BPM 是你的心率,也就是一分钟心跳次数。咱们将会用一分钟内你的心跳次数做为咱们放到区块链中的数据。把两个手指放到手腕数一数一分钟脉搏内跳动的次数,记住这个数字。

一些基础探测

让咱们来添加一些在引入后将会须要的数据模型和其余变量到 main.go 文件

const difficulty = 1

type Block struct {
        Index      int
        Timestamp  string
        BPM        int
        Hash       string
        PrevHash   string
        Difficulty int
        Nonce      string
}

var Blockchain []Block

type Message struct {
        BPM int
}

var mutex = &sync.Mutex{}
复制代码

difficulty 是一个常数,定义了咱们但愿哈希结果的零前缀数目。须要获得越多的零,找到正确的哈希输入就越难。咱们就从一个零开始。

Block 是每个区块的数据模型。别担忧不懂 Nonce,咱们稍后会解释。

Blockchain 是一系列的 Block,表示完整的链。

Message 是咱们在 REST 接口用 POST 请求传送进来的、用以生成一个新的 Block 的信息。

咱们声明一个稍后将会用到的 mutex 来防止数据竞争,保证在同一个时间点不会产生多个区块。

Web 服务

让咱们快速链接好网络服务。建立一个 run 函数,稍后在 main 中调用他来支撑服务。还须要在 makeMuxRouter() 中声明路由处理函数。记住,咱们只须要用 GET 方法来追溯区块链内容, POST 方法来建立区块。区块链不可修改,因此咱们不须要修改和删除操做。

func run() error {
        mux := makeMuxRouter()
        httpAddr := os.Getenv("ADDR")
        log.Println("Listening on ", os.Getenv("ADDR"))
        s := &http.Server{
                Addr:           ":" + httpAddr,
                Handler:        mux,
                ReadTimeout:    10 * time.Second,
                WriteTimeout:   10 * time.Second,
                MaxHeaderBytes: 1 << 20,
        }

        if err := s.ListenAndServe(); err != nil {
                return err
        }

        return nil
}

func makeMuxRouter() http.Handler {
        muxRouter := mux.NewRouter()
        muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
        muxRouter.HandleFunc("/", handleWriteBlock).Methods("POST")
        return muxRouter
}
复制代码

httpAddr := os.Getenv("ADDR") 将会从刚才咱们建立的 .env 文件中拉取端口 :8080。咱们就能够经过访问浏览器的 [http://localhost:8080](http://localhost:8080) 来访问应用。

让咱们写 GET 处理函数来在浏览器上打印出区块链。咱们也将会添加一个简易 respondwithJSON 函数,它会在调用接口发生错误的时候,以 JSON 格式反馈给咱们错误消息。

func handleGetBlockchain(w http.ResponseWriter, r *http.Request) {
        bytes, err := json.MarshalIndent(Blockchain, "", " ")
        if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
        }
        io.WriteString(w, string(bytes))
}

func respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{}) {
        w.Header().Set("Content-Type", "application/json")
        response, err := json.MarshalIndent(payload, "", " ")
        if err != nil {
                w.WriteHeader(http.StatusInternalServerError)
                w.Write([]byte("HTTP 500: Internal Server Error"))
                return
        }
        w.WriteHeader(code)
        w.Write(response)
}
复制代码

记住,若是以为这部分讲解太过粗略,请参考在此以前的文章,这里更详细的解释了这部分的每一个步骤。

如今来写 POST 处理函数。这个函数就是咱们添加新区块的方法。咱们用 Postman 发送一个 POST 请求,发送一个 JSON 的 body,好比 {“BPM”:60},到 [http://localhost:8080](http://localhost:8080),而且携带你以前测得的你的心率。

func handleWriteBlock(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        var m Message

        decoder := json.NewDecoder(r.Body)
        if err := decoder.Decode(&m); err != nil {
                respondWithJSON(w, r, http.StatusBadRequest, r.Body)
                return
        }   
        defer r.Body.Close()

        //ensure atomicity when creating new block
        mutex.Lock()
        newBlock := generateBlock(Blockchain[len(Blockchain)-1], m.BPM)
        mutex.Unlock()

        if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
                Blockchain = append(Blockchain, newBlock)
                spew.Dump(Blockchain)
        }   

        respondWithJSON(w, r, http.StatusCreated, newBlock)

}
复制代码

注意到 mutex 的 lock(加锁) 和 unlock(解锁)。在写入一个新的区块以前,须要给区块链加锁,不然多个写入将会致使数据竞争。精明的读者还会注意到 generateBlock 函数。这是处理工做量证实的关键函数。咱们稍后讲解这个。

基本的区块链函数

在开始工做量证实算法以前,咱们先将基本的区块链函数链接起来。咱们将会添加一个 isBlockValid 函数,来保证索引正确递增以及当前区块的 PrevHash 和前一区块的 Hash 值是匹配的。

咱们也要添加一个 calculateHash 函数,生成咱们须要用来建立 HashPrevHash 的哈希值。它就是一个索引、时间戳、BPM、前一区块哈希和 Nonce 的 SHA-256 哈希值(咱们稍后将会解释它是什么)。

func isBlockValid(newBlock, oldBlock Block) bool {
        if oldBlock.Index+1 != newBlock.Index {
                return false
        }

        if oldBlock.Hash != newBlock.PrevHash {
                return false
        }

        if calculateHash(newBlock) != newBlock.Hash {
                return false
        }

        return true
}

func calculateHash(block Block) string {
        record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.BPM) + block.PrevHash + block.Nonce
        h := sha256.New()
        h.Write([]byte(record))
        hashed := h.Sum(nil)
        return hex.EncodeToString(hashed)
}
复制代码

工做量证实

让咱们来看挖矿算法,或者说工做量证实。咱们但愿确保工做量证实算法在容许一个新的区块 Block 添加到区块链 blockchain 以前就已经完成了。咱们从一个简单的函数开始,这个函数能够检查在工做量证实算法中生成的哈希值是否知足咱们设置的要求。

咱们的要求以下所示:

  • 工做量证实算法生成的哈希值必需要以某个特定个数的零开始
  • 零的个数由常数 difficulty 决定,它在程序的一开始定义(在示例中,它是 1)
  • 咱们能够经过增长难度值让工做量证实算法变得困难

完成下面这个函数,isHashValid

func isHashValid(hash string, difficulty int) bool {
        prefix := strings.Repeat("0", difficulty)
        return strings.HasPrefix(hash, prefix)
}
复制代码

Go 在它的 strings 包里提供了方便的 RepeatHasPrefix 函数。咱们定义变量 prefix 做为咱们在 difficulty 定义的零的拷贝。下面咱们对哈希值进行验证,看是否以这些零开头,若是是返回 True 不然返回 False

如今咱们建立 generateBlock 函数。

func generateBlock(oldBlock Block, BPM int) Block {
        var newBlock Block

        t := time.Now()

        newBlock.Index = oldBlock.Index + 1
        newBlock.Timestamp = t.String()
        newBlock.BPM = BPM
        newBlock.PrevHash = oldBlock.Hash
        newBlock.Difficulty = difficulty

        for i := 0; ; i++ {
                hex := fmt.Sprintf("%x", i)
                newBlock.Nonce = hex
                if !isHashValid(calculateHash(newBlock), newBlock.Difficulty) {
                        fmt.Println(calculateHash(newBlock), " do more work!")
                        time.Sleep(time.Second)
                        continue
                } else {
                        fmt.Println(calculateHash(newBlock), " work done!")
                        newBlock.Hash = calculateHash(newBlock)
                        break
                }

        }
        return newBlock
}
复制代码

咱们建立了一个 newBlock 并将前一个区块的哈希值放在 PrevHash 属性里,确保区块链的连续性。其余属性的值就很明了了:

  • Index 增量
  • Timestamp 是表明了当前时间的字符串
  • BPM 是以前你记录下的心率
  • Difficulty 就直接从程序一开始的常量中获取。在本篇教程中咱们将不会使用这个属性,可是若是咱们须要作进一步的验证而且确认难度值对哈希结果固定不变(也就是哈希结果以 N 个零开始那么难度值就应该也等于 N,不然区块链就是受到了破坏),它就颇有用了。

for 循环是这个函数中关键的部分。咱们来详细看看这里作了什么:

  • 咱们将设置 Nonce 等于 i 的十六进制表示。咱们须要一个为函数 calculateHash 生成的哈希值添加一个变化的值的方法,这样若是咱们没能获取到咱们指望的零前缀数目,咱们就能用一个新的值从新尝试。这个咱们加入到拼接的字符串中的变化的值 **calculateHash** 就被称为“Nonce”
  • 在循环里,咱们用 i 和以 0 开始的 Nonce 计算哈希值,并检查结果是否以常量 difficulty 定义的零数目开头。若是不是,咱们用一个增量 Nonce 开始下一轮循环作再次尝试。
  • 咱们添加了一个一秒钟的延迟来模拟解决工做量证实算法的时间
  • 咱们一直循环计算直到咱们获得了咱们想要的零前缀,这就意味着咱们成功的完成了工做量证实。当且仅当这以后才容许咱们的 Block 经过 handleWriteBlock 处理函数被添加到 blockchain

咱们已经写完了全部函数,如今咱们来完成 main 函数:

func main() {
        err := godotenv.Load()
        if err != nil {
                log.Fatal(err)
        }   

        go func() {
                t := time.Now()
                genesisBlock := Block{}
                genesisBlock = Block{0, t.String(), 0, calculateHash(genesisBlock), "", difficulty, ""} 
                spew.Dump(genesisBlock)

                mutex.Lock()
                Blockchain = append(Blockchain, genesisBlock)
                mutex.Unlock()
        }() 
        log.Fatal(run())

}
复制代码

咱们使用 godotenv.Load() 函数加载环境变量,也就是用来在浏览器访问的 :8080 端口。

一个 go routine 建立了创世区块,由于咱们须要它做为区块链的起始点

咱们用刚才建立的 run() 函数开始网络服务。

完成了!是时候运行它了!

这里有完整的代码。

让咱们试着运行这个宝宝!

go run main.go 来开始程序

而后用浏览器访问 [http://localhost:8080](http://localhost:8080)

创世区块已经为咱们建立好。如今打开 Postman 而后发送一个 POST 请求,向同一个路由以 JSON 格式在 body 中发送以前测定的心率值。

发送请求以后,在终端看看发生了什么。你将会看到你的机器忙着用增长 Nonce 值不停建立新的哈希值,直到它找到了须要的零前缀值。

当工做量证实算法完成了,咱们就会获得一条颇有用的 work done! 消息,咱们就能够去检验哈希值来看看它是否是真的以咱们设置的 difficulty 个零开头。这意味着理论上,那个咱们试图添加 BPM = 60 信息的新区块已经被加入到咱们的区块链中了。

咱们来刷新浏览器并查看:

成功了!咱们的第二个区块已经被加入到创世区块以后。这意味着咱们成功的在 POST 请求中发送了区块,这个请求触发了挖矿的过程,而且当且仅当工做量证实算法完成后,它才会被添加到区块链中。

接下来

很棒!刚才你学到的真的很重要。工做量证实算法是比特币,以太坊以及其余不少大型区块链平台的基础。咱们刚才学到的并不是小事;虽然咱们在示例中使用了一个很低的 difficulty 值,可是将它增长到一个比较大的值就正是生产环境下区块链工做量证实算法是如何运做的。

如今你已经清楚了解了区块链技术的核心部分,接下来如何学习将取决于你。我向你推荐以下资源:

  • 在咱们的 Networking tutorial 教程中学习联网区块链如何工做。
  • 在咱们的 IPFS tutorial 教程中学习如何以分布式存储大型文件并用区块链通讯。

若是你作好准备作另外一次技术上的跳跃,试着学习 股权证实(Proof of Stake) 算法。虽然大多数的区块链使用工做量证实算法做为共识算法,股权证实算法正得到愈来愈多的关注。不少人相信以太坊未来会从工做量证实算法切换到股权证实算法。

想看关于工做量证实算法和股权证实算法的比较教程?在上面的代码中发现了错误?喜欢咱们作的事?讨厌咱们作的事?

经过 加入咱们的 Telegram 消息群 让咱们知道你的想法!你将获得本教程做者以及 Coral Health 团队其余成员的热情应答。

想要了解更多关于 Coral Health 以及咱们如何使用区块链来改进我的医药研究,访问咱们的网站


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索