翻译:使用go构建区块链(二)工作量证明

原文:Building Blockchain in Go. Part 2: Proof-of-Work

简介

上一篇文章中,我们构建了一个非常简单的数据结构,这正是区块链数据库的本质所在。同时,我们也实现了通过链式关系去添加区块,每一个区块都连接着上一个区块。然而,我们的区块链实现有一个很明显的瑕疵:添加区块到链上实在是太容易,太廉价了。区块链和比特币其中一个主旨是,添加区块应该是一件困难的工作。今天我们就来修复这个瑕疵吧。

工作量证明

区块链其中一个关键的想法就是,要把数据成功添加进去必须执行一些困难的工作才能通过。也正是这些困难的工作才能让区块链变得更加安全和保持一致性。同样,执行这些困难的工作也是可以获取到特定的报酬的(这就是人们为什么能够通过挖矿来获取货币)。

这个机制跟我们现实生活是很相似的:人们需要通过工作才能获取报酬并且维持他们的生活。在区块链中,网络节点上的一些参与者(矿工)负责维持网络,并且通过添加新的区块去获取相应的报酬。正是有了他们的工作,一个区块才能以安全的方式编入区块链中,从而维持了整个区块链数据库的稳定性。值得注意的是,完成这项工作的人必须证明这一点。

整个"执行困难工作并且证明"的机制也成为工作量证明。这项工作的困难是在于其需要大量的算力:即使是高性能的计算机也不能迅速的完成。此外,随着每次1小时6个区块的增长,这项工作的困难程度将会越来越大。在比特币中,这项工作的目标就是为一个区块找出一个符合特定要求的hash值,而这个hash值则可以作为一个证明。因此,找到一个证明就是实际的工作。

哈希算法

在这一段中我们将会讨论hash算法。如果你已经对这个概念比较熟悉了,那么可以跳过这部分。

hash算法是为特别数据获取对应hash值的一个过程。hash值是它计算的数据的唯一表示。一个哈希函数可以接收任意大小的数据,并且输出固定大小的hash值。下面是哈希算法的关键特性:
1.原始数据不能从一个hash值中反推出来。因此,hash算法并不是加密算法。
2.固定的数据有且仅有一个唯一的hash值。
3.改变输入数据中的任何一个字节都会输出一个完全不同的hash值。


hashing-example.png

哈希函数广泛用于检查数据的一致性。某些软件厂商会把对应的摘要添加到软件包中。当你下载完一个文件之后,你就可以将其输入到哈希函数中,并将输出的hash值与软件开发人员提供的hash值进行比较。

在区块链中,hash通常用于保证一个区块的一致性。每一个区块输入到哈希函数的数据中都会包含着上一个区块的hash值,因此它使得在链上修改一个区块变得完全不可能(至少是非常困难):因为修改一个区块必须重新计算后面所有区块的hash值。

Hashcash

比特币使用Hashcash,Hashcash最初是用于防止邮件轰炸的一个工作量证明的算法。以下是它的一些算法步骤:
1.提供公开的数据(在邮件中指的是收件人的邮箱地址;在比特币中指的是区块头)。
2.往其添加一个计数器。计数器从0开始。
3.把数据和计数器组合并计算hash值
4.检查这个hash值是否符合特定要求。
(1)如果符合则完成。
(2)如果不符合,增加计数器并重复第三步和第四步。

因此,这是一个非常暴力的算法:修改一个计数器,计算一个新的hash值并检查,继续增加计数器并检查,以此类推。这就是计算费用很昂贵的原因了。

现在我们来仔细看看hash值如何才能满足要求。在最初的Hashcash实现中,听起来要求是"hash值的前20位必须是0"。在比特币中,这个要求还是一次次地被调整,因为从设计上来考虑,尽管计算力随着越来越多的矿工加入到网络中变得越来越高,但还是必须隔十分钟才能产生一个区块。

为了证明这个算法,我从上一个例子中的(“I like donuts”)拿过来了,发现其hash值是已3个0字节开头的:


hashcash-example.png

ca07ca是计数器的十六进制值,也是13240266的十进制值。

实现

好了,我们终于完成了理论部分,让我们开始写代码吧!让我们先定义一个挖矿的难度系数:

const targetBits = 24

在比特币中,“目标比特”是存储块开采难度的区块头。我们目前不会实现目标难度的调整算法,因此我们只需要定义一个全局常量即可。

24是一个任意数字,我们的目标是在内存中拥有少于256位的目标。我们需要有一定象征意义的难度,但也不能太庞大,因为难度越大就越难找出一个合适的hash值。

type ProofOfWork struct {
    block  *Block
    target *big.Int
}

func NewProofOfWork(b *Block) *ProofOfWork {
    target := big.NewInt(1)
    target.Lsh(target, uint(256-targetBits))

    pow := &ProofOfWork{b, target}

    return pow
}

这里创建了一个名为ProofOfWork的结构体,其包含了一个区块指针和目标指针。“目标”也就是上一段中提到的“要求”的别名。我们使用golang的big包主要是因为我们要比较hash值和目标值:我们会把hash转为一个大整数并且检查其是否小于目标值。

NewProofOfWork函数里面,我们初始化了一个类型为big.Int值为1的变量,并且把它左移了256 - targetBits位。256位是SHA-256哈希算法的长度,我们也即将使用SHA-256算法。target的十六进制表示为:

0x10000000000000000000000000000000000000000000000000000000000

它在内存中占用了29个字节,这是它与前面例子中的hash值进行的可视化比较:

0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3
0000010000000000000000000000000000000000000000000000000000000000
0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca

第一个hash(由“ I like donuts”计算得来)比目标值要更大,因此它不是一个合法的工作量证明。第二个hash值(由“I like donutsca07ca”计算得来)则比目标值更小,因此这才是合法的工作量证明。

你可以把目标值视为范围的上限:如果一个数字(hash值)比边界小,那么它就是合法的。越小的范围会导致合法数字越小,因此也需要更困难的工作才能找到合法的数字。

现在,我们需要对数据进行hash,让我们先来准备数据:

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
}

这里做的事情很简单:我们只是把区块字段和目标以及nonce值进行了合并。nonce在这里则作为上面Hashcash描述中的计数器,这是一个加密术语。

好了,一切准备就绪,我们来实现PoW算法的核心部分吧:

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("\r%x", hash)
        hashInt.SetBytes(hash[:])

        if hashInt.Cmp(pow.target) == -1 {
            break
        } else {
            nonce++
        }
    }
    fmt.Print("\n\n")

    return nonce, hash[:]
}

首先,我们先初始化一些变量:hashInthash的整型标识;nonce是一个计数器。接下来,我们开始一个“无限”循环:它限制于maxNonce,这个变量等于math.MaxInt64;主要是用于防止nonce的溢出。尽管难度对于我们实现的PoW算法来说,溢出的可能性很低,但我们最好还是检查一下,以防万一。

在这个循环我们主要做了:
1.准备数据。
2.使用SHA-256函数进行哈希。
3.把hash值转换为大整数。
4.把这个整数跟目标值比较。

我们的解释尽可能的简单。现在我们可以去掉Block中的SetHash方法了,并修改NewBlock函数:

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
}

这里你能看到nonce已经被作为Block的属性保存起来了。这是有必要的,因为nonce是检查证明的必要属性。现在Block结构体看起来是这样的:

type Block struct {
    Timestamp     int64
    Data          []byte
    PrevBlockHash []byte
    Hash          []byte
    Nonce         int
}

好的!接下来我们开始运行程序看看是否一切如常:

Mining the block containing "Genesis Block"
00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1

Mining the block containing "Send 1 BTC to Ivan"
00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804

Mining the block containing "Send 2 more BTC to Ivan"
000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbe

Prev. hash:
Data: Genesis Block
Hash: 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1

Prev. hash: 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1
Data: Send 1 BTC to Ivan
Hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804

Prev. hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804
Data: Send 2 more BTC to Ivan
Hash: 000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbe

Yay!你可以看到每个hash值都是由3个0字节开头的,并且需要一定时间才能获取到这些hash值。

但这里还有一件事情没完成:让我们可以验证这个工作的证明。

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
}

这就解释了我们为什么需要保存nonce值了。

让我们再检查一次是否运行正常:

func main() {
    ...

    for _, block := range bc.blocks {
        ...
        pow := NewProofOfWork(block)
        fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
        fmt.Println()
    }
}

输出:

...

Prev. hash:
Data: Genesis Block
Hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038
PoW: true

Prev. hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038
Data: Send 1 BTC to Ivan
Hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b
PoW: true

Prev. hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b
Data: Send 2 more BTC to Ivan
Hash: 000000e42afddf57a3daa11b43b2e0923f23e894f96d1f24bfd9b8d2d494c57a
PoW: true

总结

现在添加区块需要进行一系列困难的工作,因此需要进行挖掘,可以看到,我们的区块链又向真实的架构迈向了一步。但这依然缺少一些重要的特性:这个区块链数据库不可以持久化,没有钱包、地址、交易,也没有一致性机制。这些特性我们将会在后面的文章中实现,现在我们就先快乐的挖矿吧!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,064评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,606评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,011评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,550评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,465评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,919评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,428评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,075评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,208评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,185评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,191评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,914评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,482评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,585评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,825评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,194评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,703评论 2 339

推荐阅读更多精彩内容