在Go中构建区块链。 第2部分:工作量证明

介绍

在[前面的文章中]我们构建了一个非常简单的数据结构,这是区块链数据库的本质。 我们可以通过它们之间的链式关系为它添加块:每个块链接到前一个块。 不过,我们的区块链实施存在一个重大缺陷:向链条添加区块非常简单且便宜。 区块链和比特币的关键之一在于添加新块是一项艰巨的工作。 今天我们要解决这个缺陷。

验证的工作

区块链的一个关键想法是,人们必须执行一些艰苦的工作才能将数据放入其中。 正是这项艰巨的工作才使得区块链更加安全和一致。 此外,这项艰苦的工作还有奖励(这是人们如何获得用于采矿的硬币)。

这种机制与现实生活中的机制非常相似:必须努力工作以获得回报并维持​​生活。 在区块链中,网络的一些参与者(矿工)努力维持网络,为其添加新块,并为他们的工作获得奖励。 作为他们工作的结果,一个区块以一种安全的方式整合到区块链中,从而保持了整个区块链数据库的稳定性。 值得注意的是,完成这项工作的人必须证明这一点。

这整个“努力工作和证明”机制被称为工作证明。 这很难,因为它需要大量的计算能力:即使是高性能的计算机也无法快速完成。 此外,这项工作的难度不断增加,以保持新的区块率在每小时6块左右。 在比特币中,这种工作的目标是为一个块找到一个符合要求的散列。 这就是这个散列作为证明。 因此,找到一个证明就是实际的工作。

最后要注意的是。 工作量证明算法必须满足一项要求:做这项工作很难,但验证证明是容易的。 证明通常交给其他人,所以对他们来说,不需要太多时间来验证它。

哈希(Hashing)

在这一段中,我们将讨论哈希。 如果你熟悉这个概念,你可以跳过这个部分。

散列是为指定数据获取散列的过程。 哈希是它计算的数据的唯一表示。 散列函数是一种可以获取任意大小数据并生成固定大小散列的函数。 以下是哈希的一些关键特性:

原始数据无法从散列中恢复。 因此,哈希不是加密。
某些数据只能有一个散列,散列是唯一的。
更改输入数据中的一个字节将导致完全不同的散列。


image

散列函数广泛用于检查数据的一致性。 除软件包外,某些软件提供商还发布校验和。 下载文件后,您可以将其提供给哈希函数,并将生成的哈希与软件开发人员提供的哈希进行比较。

在区块链中,散列用于保证块的一致性。 散列算法的输入数据包含前一个块的散列,因此不可能(或者至少非常困难)修改链中的块:必须重新计算其后的所有块的散列和散列。

Hashcash

比特币使用[Hashcash] ,这是一种最初用于防止电子邮件垃圾邮件的Proof-of-Work算法。它可以分成以下几个步骤:

  1. 拿一些公开的数据(如电子邮件,它是接收者的电子邮件地址;比特币的情况下,它是块头)。
  2. 给它添加一个计数器。 计数器从0开始。
  3. 获取 data + counter 组合的散列。
  4. 检查哈希是否符合某些要求。
    1. 如果是这样,你就完成了。
    2. 如果没有,请增加计数器并重复步骤3和4。

因此,这是一个蛮力算法:你改变计数器,计算一个新的哈希,检查它,增加计数器,计算哈希等。这就是为什么它的计算成本很高。

现在我们来看看哈希必须满足的要求。 在最初的Hashcash实现中,需求听起来像“哈希的前20位必须为零”。 在比特币中,需求会随时进行调整,因为在设计上,尽管计算能力随着时间的推移而增加,越来越多的矿工加入网络,但必须每10分钟创建一个块。

为了演示这种算法,我从前面的例子(“我喜欢甜甜圈”)中获取数据,并找到一个以3个零字节开头的散列:


image

ca07ca 是计数器的十六进制值,即十进制系统中的13240266。

履行
好的,我们已经完成了理论,让我们编写代码! 首先,我们来定义挖掘的难度:

const targetBits = 24

在比特币中,“目标比特”是存储块开采难度的块头。 目前我们不会执行目标调整算法,因此我们可以将难度定义为全局常量。

24是一个任意数字,我们的目标是在内存中占用少于256位的目标。 我们希望差异足够大,但不要太大,因为差异越大,找到合适的散列越困难。

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结构,它包含一个指向块的指针和一个指向目标的指针。 “目标”是前一段描述的要求的另一个名称。 我们使用一个整数,因为我们将哈希与目标进行比较:我们将哈希转换为大整数,并检查它是否小于目标。

NewProofOfWork函数中,我们初始化一个值为1的big.Int并将其左移256 - targetBits位。 256是以位为单位的SHA-256哈希的长度,它是我们要使用的SHA-256哈希算法。 target的十六进制表示是:

  0x10000000000000000000000000000000000000000000000000000000000

它在内存中占用29个字节。 下面是它与前面例子中的哈希值的视觉比较:

  0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3       
  0000010000000000000000000000000000000000000000000000000000000000   
  0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca

第一个散列(根据“我喜欢甜甜圈”计算)大于目标,因此它不是有效的工作证明。 第二个散列(以“我喜欢donutsca07ca”计算)小于目标,因此这是一个有效的证明。

您可以将目标视为范围的上限:如果数字(哈希)低于边界,则该数字有效,反之亦然。 降低边界将导致更少的有效数字,因此,找到一个有效数字需要更困难的工作。

现在,我们需要数据来散列。 让我们来准备它:

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这里是来自上面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[:]
  }

首先,我们初始化变量: hashInt是hash的整数表示; nonce是计数器。 接下来,我们运行一个“无限”循环:它受限于maxNonce ,它等于math.MaxInt64 ; 这样做是为了避免nonce可能溢出。 尽管我们的PoW实现的难度太低而不能使计数器溢出,但为了以防万一,进行此项检查仍然更好。

在循环中我们:

准备数据。
用SHA-256对其进行散列。
将散列转换为一个大整数。
将整数与目标进行比较。
和前面解释的一样简单。 现在我们可以删除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

好极了! 您可以看到每个散列现在都以三个零字节开始,并且需要一些时间来获取这些散列。

还有一件事要做:让我们可以验证作品的证明。

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 
}

这就是我们需要保存的随机数的地方。

让我们再次检查一切正常:

  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

推荐阅读更多精彩内容