【转】用 Go 构建一个区块链 - Go语言中文社区

【转】用 Go 构建一个区块链


Part 1: 基本原型

引言

区块链是 21 世纪最具革命性的技术之一,它仍然处于不断成长的阶段,而且还有很多潜力尚未显现出来。 本质上,区块链只是一个分布式数据库而已。 不过,使它独一无二的是,区块链是一个公开的数据库,而不是一个私人数据库,也就是说,每个使用它的人都有一个完整或部分的副本。 只有经过其他数据库管理员的同意,才能向数据库中添加新的记录。 此外,也正是由于区块链,才使得加密货币和智能合约成为现实。

在本系列文章中,我们将实现一个简化版的区块链,基于它来构建简化版的加密货币。

区块

让我们从 “区块链” 中的 “区块” 谈起。在区块链中,存储有效信息的是区块。比如,比特币区块存储的有效信息,就是比特币交易,交易信息也是所有加密货币的本质。除此以外,区块还包含了一些技术信息,比如版本,当前时间戳和前一个区块的哈希。

在本文中,我们并不会实现一个像比特币技术规范所描述的区块链,而是实现一个简化版的区块链,它仅包含了一些关键信息。看起来就像是这样:

type Block struct {
    Timestamp     int64
    Data          []byte
    PrevBlockHash []byte
    Hash          []byte
}
  • Timestamp 是当前时间戳,也就是区块创建的时间。
  • Data 是区块存储的实际有效的信息。
  • PrevBlockHash 存储的是前一个块的哈希。
  • Hash 是当前块的哈希。

在比特币技术规范中,TimestampPrevBlockHashHash 是区块头(block header),区块头是一个单独的数据结构。而交易,也就是这里的 Data, 是另一个单独的数据结构。为了简便起见,我把这两个混合在了一起。

那么,我们要如何计算哈希呢?如何计算哈希,是区块链一个非常重要的部分。正是由于这个特性,才使得区块链是安全的。计算一个哈希,是在计算上非常困难的一个操作。即使在高速电脑上,也要花费不少时间 (这就是为什么人们会购买 GPU 来挖比特币) 。这是一个有意为之的架构设计,它故意使得加入新的区块十分困难,因此可以保证区块一旦被加入以后,就很难再进行修改。在本系列未来几篇文章中,我们将会讨论和实现这个机制。

目前,我们仅取了 Block 结构的一些字段(Timestamp, Data 和 PrevBlockHash),并将它们相互连接起来,然后在连接后的结果上计算一个 SHA-256 的哈希. 让我们在SetHash方法中完成这个任务:

func (b *Block) SetHash() {
    timestamp := []byte(strconv.FormatInt(b.Timestamp, 10))
    headers := bytes.Join([][]byte{b.PrevBlockHash, b.Data, timestamp}, []byte{})
    hash := sha256.Sum256(headers)

    b.Hash = hash[:]
}

接下来,按照 Golang 的惯例,我们会实现一个用于简化创建一个区块的函数:

func NewBlock(data string, prevBlockHash []byte) *Block {
    block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}}
    block.SetHash()
    return block
}

这就是区块部分的全部内容了!

区块链

下面让我们来实现一个区块链。本质上,区块链仅仅是一个有着特定结构的数据库,是一个有序,后向连接的列表。这也就是说,区块按照插入的顺序进行存储,每个块都被连接到前一个块。这样的结构,能够让我们快速地获取链上的最新块,并且高效地通过哈希来检索一个块。

在 Golang 中,可以通过一个 array 和 map 来实现这个结构:array 存储有序的哈希(Golang 中 array 是有序的),map 存储 hask -> block 对(Golang 中, map 是无序的)。 但是在基本的原型阶段,我们只用到了 array,因为现在还不需要通过哈希来获取块。

type Blockchain struct {
    blocks []*Block
}

这就是我们的第一个区块链!我从来没有想过它会是这么容易。

现在,让我们能够给它添加一个块:

func (bc *Blockchain) AddBlock(data string) {
    prevBlock := bc.blocks[len(bc.blocks)-1]
    newBlock := NewBlock(data, prevBlock.Hash)
    bc.blocks = append(bc.blocks, newBlock)
}

完成!不过,真的就这样了吗?

为了加入一个新的块,我们必须要有一个已有的块,但是,现在我们的链是空的,一个块都没有!所以,在任何一个区块链中,都必须至少有一个块。这样的块,也就是链中的第一个块,通常叫做创世块(genesis block). 让我们实现一个方法来创建一个创世块:

func NewGenesisBlock() *Block {
    return NewBlock("Genesis Block", []byte{})
}

现在,我们可以实现一个函数来创建有创世块的区块链:

func NewBlockchain() *Blockchain {
    return &Blockchain{[]*Block{NewGenesisBlock()}}
}

来检查一个我们的区块链是否如期工作:

func main() {
    bc := NewBlockchain()

    bc.AddBlock("Send 1 BTC to Ivan")
    bc.AddBlock("Send 2 more BTC to Ivan")

    for _, block := range bc.blocks {
        fmt.Printf("Prev. hash: %xn", block.PrevBlockHash)
        fmt.Printf("Data: %sn", block.Data)
        fmt.Printf("Hash: %xn", block.Hash)
        fmt.Println()
    }
}

输出:

Prev. hash:
Data: Genesis Block
Hash: aff955a50dc6cd2abfe81b8849eab15f99ed1dc333d38487024223b5fe0f1168

Prev. hash: aff955a50dc6cd2abfe81b8849eab15f99ed1dc333d38487024223b5fe0f1168
Data: Send 1 BTC to Ivan
Hash: d75ce22a840abb9b4e8fc3b60767c4ba3f46a0432d3ea15b71aef9fde6a314e1

Prev. hash: d75ce22a840abb9b4e8fc3b60767c4ba3f46a0432d3ea15b71aef9fde6a314e1
Data: Send 2 more BTC to Ivan
Hash: 561237522bb7fcfbccbc6fe0e98bbbde7427ffe01c6fb223f7562288ca2295d1

总结

我们创建了一个非常简单的区块链原型:它仅仅是一个数组构成的一系列区块,每个块都与前一个块相关联。真实的区块链要比这复杂得多。在我们的区块链中,加入新的块非常简单,而且很快,但是在真实的区块链中,加入新的块需要很多工作:你必须要经过十分繁重的计算(这个机制叫做工作量证明),来获得添加一个新块的权力。并且,区块链是一个没有单一决策者的分布式数据库。因此,一个新的块必须要被网络的其他参与者确认和同意(这个机制叫做共识(consensus))。还有一点,我们的区块链还没有任何的交易!

在接下来的文章的我们将会一一覆盖这些特性。


  1. 本文涉及的源代码:part_1
  2. 区块哈希算法:https://en.bitcoin.it/wiki/Block_hashing_algorithm

 

Part 2: 工作量证明

在 前面一文 中,我们构造了一个非常简单的数据结构,这个数据结构也是整个区块链数据库的核心。目前所完成的区块链原型,已经可以通过链式关系把区块相互关联起来:每个块都被连接到前一个块。

但是,我们实现的区块链有一个巨大的缺点:向链中加入区块太容易和廉价了。而区块链和比特币的其中一个核心就是,要想加入新的区块,必须先完成一些非常困难的工作。在本文,我们将会解决这个缺点。

工作量证明

区块链的一个关键点就是,一个人必须经过一系列困难的工作,才能将数据放入到区块链中。正是这种困难的工作,才使得区块链是安全和一致的。此外,完成这个工作的人也会获得奖励(这也就是通过挖矿获得币)。

这个机制与生活的一个现象非常类似:一个人必须通过努力工作,才能够获得回报或者奖励,用以支撑他们的生活。在区块链中,是通过网络中的参与者(矿工)不断的工作来支撑整个网络,也就是矿工不断地向区块链中加入新块,然后获得相应的奖励。作为他们努力工作的结果,新生成的区块就能够被安全地被加入到区块链中,这种机制维护了整个区块链数据库的稳定性。值得注意的是,完成了这个工作的人必须要证明这一点,他必须要证明确实是他完成了这些工作。

整个 “努力工作并进行证明” 的机制,就叫做工作量证明(proof-of-work)。要想完成工作非常地不容易,因为这需要大量的计算能力:即便是高性能计算机,也无法在短时间内快速完成。此外,这个工作的困难度会随着时间不断增长,以保持每个小时大概出 6 个新块的速度。在比特币中,这个工作的目的是为了找到一个块的哈希,同时这个哈希满足了一些必要条件。这个哈希,也就充当了证明的角色。因此,寻求证明(寻找有效哈希),就是实际要做的事情。

哈希计算

在本节中,我们会讨论哈希计算。如果你已经熟悉了这个概念,可以跳过这一节。

获得指定数据的一个哈希值的过程,就叫做哈希计算。一个哈希,就是对所计算数据的一个唯一的表示。一个哈希函数输入任意大小的数据,输出一个固定大小的哈希值。下面是哈希的几个关键特性:

  1. 无法从一个哈希值恢复原始数据。也就是说,哈希并不是加密。
  2. 对于特定的数据,只能有一个哈希,并且这个哈希是唯一的。
  3. 即使是仅仅改变输入数据中的一个字节,也会导致输出一个完全不同的哈希。


-哈希过程-

哈希函数被广泛用于检测数据的一致性。一些软件提供者除了提供软件包以外,还会发布校验和。当下载完一个文件以后,你可以用哈希函数对下载好的文件计算一个哈希,并与作者提供的哈希进行比较,以此来保证文件下载的完整性。

在区块链中,哈希被用于保证一个块的一致性。哈希算法的输入数据包含了前一个块的哈希,因此使得不太可能(或者,至少很困难)去修改链中的一个块:因为如果一个人想要修改前面一个块的哈希,那么他必须要重新计算这个块以及后面所有块的哈希。

Hashcash

比特币使用 Hashcash ,一个最初用来防止垃圾邮件的工作量证明算法。它可以被分解为以下步骤:

  1. 取一些公开的数据(比如,如果是 email 的话,它可以是接收者的邮件地址;在比特币中,它是区块头)
  2. 给这个公开数据添加一个计数器。计数器默认从 0 开始
  3. 将 data(数据) 和 counter(计数器) 组合到一起,获得一个哈希
  4. 检查哈希是否符合一定的条件: 1.如果符合条件,结束 2.如果不符合,增加计数器,重复步骤 3-4

因此,这是一个暴力算法:改变计数器,计算一个新的哈希,检查,增加计数器,计算一个哈希,检查,如此反复。这也是为什么说它是在计算上是非常昂贵的,因为这一步需要如此反复不断地计算和检查。

现在,让我们来仔细看一下一个哈希要满足的必要条件。在原始的 Hashcash 实现中,它的要求是 “一个哈希的前 20 位必须是 0”。在比特币中,这个要求会随着时间而不断变化。因为按照设计,必须保证每 10 分钟生成一个块,而不论计算能力会随着时间增长,或者是会有越来越多的矿工进入网络,所以需要动态调整这个必要条件。

为了阐释这一算法,我从前一个例子(“I like donuts”)中取得数据,并且找到了一个前 3 个字节是全是 0 的哈希。


-一个以三个0开头的哈希值-

ca07ca 是计数器的 16 进制值,十进制的话是 13240266.

实现

好了,完成了理论层面,来开始写代码吧!首先,定义挖矿的难度值:

const targetBits = 24

在比特币中,当一个块被挖出来以后,“target bits” 代表了区块头里存储的难度。这里的 24 指的是算出来的哈希前 24 位必须是 0,用 16 进制表示化的话,就是前 6 位必须是 0,这一点可以在最后的输出可以看出来。目前不会实现一个动态调整目标的算法,所以将难度定义为一个全局的常量即可。

24 其实是一个可以任意取的数字,目的是要有一个目标(target)而已,这个目标占据不到 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** 函数中,我们将** big.Int** 初始化为 1,然后左移 256 - targetBits位。256 是一个 SHA-256 哈希的位数,我们将要使用的是 SHA-256 哈希算法。target(目标) 的 16 进制形式为:

0x10000000000000000000000000000000000000000000000000000000000

它在内存上占据了 29 个字节。下面是与前面例子哈希的形式化比较:

0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3
0000010000000000000000000000000000000000000000000000000000000000
0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca

第一个哈希(基于 “I like donuts” 计算)比目标要大,因此它并不是一个有效的工作量证明。第二个哈希(基于 “I like donutsca07ca” 计算)比目标要小,所以是一个有效的证明。

译者注:评论有人提出上面的形式化比较有些“言不符实”,其实它应该并非由 “I like donuts” 而来,但是原文作者表达的意思是没问题的,可能是疏忽而已。下面是我做的一个小实验:

package main

import (
    "crypto/sha256"
    "fmt"
    "math/big"
)

func main() {

    data1 := []byte("I like donuts")
    data2 := []byte("I like donutsca07ca")
    targetBits := 24
    target := big.NewInt(1)
    target.Lsh(target, uint(256-targetBits))
    fmt.Printf("%xn", sha256.Sum256(data1))
    fmt.Printf("%64xn", target)
    fmt.Printf("%xn", sha256.Sum256(data2))

}

输出:


-实验-

你可以把目标想象为一个范围的上界:如果一个数(由哈希转换而来)比上界要小,那么这是有效的,反之无效。因为要求比上界要小,所以会导致更少的有效数字。因此,也就需要通过困难的工作(一系列反复的计算),才能找到一个有效的数字。

现在,我们需要有数据来进行哈希,准备数据:

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
}

这个部分比较直观:只需要将 target ,nonce 与 Block 进行合并。这里的 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)
        hashInt.SetBytes(hash[:])

        if hashInt.Cmp(pow.target) == -1 {
            fmt.Printf("r%x", hash)
            break
        } else {
            nonce++
        }
    }
    fmt.Print("nn")

    return nonce, hash[:]
}

首先我们对变量进行初始化:

  • HashInt 是 hash 的整形表示;
  • nonce 是计数器。

然后开始一个 “无限” 循环:maxNonce 对这个循环进行了限制, 它等于 math.MaxInt64。这是为了避免 nonce 可能出现的溢出。尽管我们的 PoW 实现的难度太小了,以至于计数器其实不太可能会溢出,但最好还是以防万一检查一下。

在这个循环中,我们做的事情有:

  1. 准备数据
  2. 用 SHA-256 对数据进行哈希
  3. 将哈希转换成一个大整数
  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

成功了!你可以看到每个哈希都是 3 个字节的 0 开始,并且获得这些哈希需要花费一些时间。

还剩下一件事情需要做,对工作量证明进行验证:

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: %sn", 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

从下图可以看出,这次我们产生三个块花费了一分多钟,比没有工作量证明之前慢了很多(也就是成本高了很多):


-输出-

总结

我们的区块链离真正的区块链又进了一步:现在需要经过一些困难的工作才能加入新的块,因此挖矿就有可能了。但是,它还缺少一些至关重要的特性:区块链数据库并不是持久化的,没有钱包,地址,交易,也没有共识机制。不过,所有的这些,我们都会在接下来的文章中实现,现在,愉快地挖矿吧!


链接:
Full source codes
Blockchain hashing algorithm
Proof of work
Hashcash

本文源代码:part_2

 

Part 3:持久化和命令行接口

引言

到目前为止,我们已经构建了一个有工作量证明机制的区块链。有了工作量证明,挖矿也就有了着落。虽然目前的实现离一个有着完整功能的区块链越来越近了,但是它仍然缺少了一些重要的特性。在今天的内容中,我们会将区块链持久化到一个数据库中,然后会提供一个简单的命令行接口,用来完成一些与区块链的交互操作。本质上,区块链是一个分布式数据库,不过,我们暂时先忽略 “分布式” 这个部分,仅专注于 “存储” 这一点。

选择数据库

目前,我们的区块链实现里面并没有用到数据库,而是在每次运行程序时,简单地将区块链存储在内存中。那么一旦程序退出,所有的内容就都消失了。我们没有办法再次使用这条链,也没有办法与其他人共享,所以我们需要把它存储到磁盘上。

那么,我们要用哪个数据库呢?实际上,任何一个数据库都可以。在 比特币原始论文 中,并没有提到要使用哪一个具体的数据库,它完全取决于开发者如何选择。 Bitcoin Core ,最初由中本聪发布,现在是比特币的一个参考实现,它使用的是 LevelDB。而我们将要使用的是...

BoltDB

因为它:

  1. 非常简单和简约
  2. 用 Go 实现
  3. 不需要运行一个服务器
  4. 能够允许我们构造想要的数据结构

BoltDB GitHub 上的 README 是这么说的:

Bolt 是一个纯键值存储的 Go 数据库,启发自 Howard Chu 的 LMDB. 它旨在为那些无须一个像 Postgres 和 MySQL 这样有着完整数据库服务器的项目,提供一个简单,快速和可靠的数据库。

由于 Bolt 意在用于提供一些底层功能,简洁便成为其关键所在。它的
API 并不多,并且仅关注值的获取和设置。仅此而已。

听起来跟我们的需求完美契合!来快速过一下:

Bolt 使用键值存储,这意味着它没有像 SQL RDBMS (MySQL,PostgreSQL 等等)的表,没有行和列。相反,数据被存储为键值对(key-value pair,就像 Golang 的 map)。键值对被存储在 bucket 中,这是为了将相似的键值对进行分组(类似 RDBMS 中的表格)。因此,为了获取一个值,你需要知道一个 bucket 和一个键(key)。

需要注意的一个事情是,Bolt 数据库没有数据类型:键和值都是字节数组(byte array)。鉴于需要在里面存储 Go 的结构(准确来说,也就是存储(块)Block),我们需要对它们进行序列化,也就说,实现一个从 Go struct 转换到一个 byte array 的机制,同时还可以从一个 byte array 再转换回 Go struct。虽然我们将会使用 encoding/gob 来完成这一目标,但实际上也可以选择使用 JSONXMLProtocol Buffers 等等。之所以选择使用 encoding/gob, 是因为它很简单,而且是 Go 标准库的一部分。

数据库结构

在开始实现持久化的逻辑之前,我们首先需要决定到底要如何在数据库中进行存储。为此,我们可以参考 Bitcoin Core 的做法:

简单来说,Bitcoin Core 使用两个 “bucket” 来存储数据:

  1. 其中一个 bucket 是 blocks,它存储了描述一条链中所有块的元数据
  2. 另一个 bucket 是 chainstate,存储了一条链的状态,也就是当前所有的未花费的交易输出,和一些元数据

此外,出于性能的考虑,Bitcoin Core 将每个区块(block)存储为磁盘上的不同文件。如此一来,就不需要仅仅为了读取一个单一的块而将所有(或者部分)的块都加载到内存中。但是,为了简单起见,我们并不会实现这一点。

在 blocks 中,key -> value 为:

key value
b + 32 字节的 block hash block index record
f + 4 字节的 file number file information record
l + 4 字节的 file number the last block file number used
R + 1 字节的 boolean 是否正在 reindex
F + 1 字节的 flag name length + flag name string 1 byte boolean: various flags that can be on or off
t + 32 字节的 transaction hash transaction index record

在 chainstatekey -> value 为:

key value
c + 32 字节的 transaction hash unspent transaction output record for that transaction
B 32 字节的 block hash: the block hash up to which the database represents the unspent transaction outputs

详情可见 这里

因为目前还没有交易,所以我们只需要 blocks bucket。另外,正如上面提到的,我们会将整个数据库存储为单个文件,而不是将区块存储在不同的文件中。所以,我们也不会需要文件编号(file number)相关的东西。最终,我们会用到的键值对有:

  1. 32 字节的 block-hash -> block 结构
  2. l -> 链中最后一个块的 hash

这就是实现持久化机制所有需要了解的内容了。

序列化

上面提到,在 BoltDB 中,值只能是 []byte 类型,但是我们想要存储 Block 结构。所以,我们需要使用 encoding/gob 来对这些结构进行序列化。

让我们来实现 Block 的 Serialize 方法(为了简洁起见,此处略去了错误处理):

func (b *Block) Serialize() []byte {
    var result bytes.Buffer
    encoder := gob.NewEncoder(&result)

    err := encoder.Encode(b)

    return result.Bytes()
}

这个部分比较直观:首先,我们定义一个 buffer 存储序列化之后的数据。然后,我们初始化一个 gob encoder 并对 block 进行编码,结果作为一个字节数组返回。

接下来,我们需要一个解序列化的函数,它会接受一个字节数组作为输入,并返回一个 Block. 它不是一个方法(method),而是一个单独的函数(function):

func DeserializeBlock(d []byte) *Block {
    var block Block

    decoder := gob.NewDecoder(bytes.NewReader(d))
    err := decoder.Decode(&block)

    return &block
}

这就是序列化部分的内容了。

持久化

让我们从 NewBlockchain 函数开始。在之前的实现中,它会创建一个新的
Blockchain 实例,并向其中加入创世块。而现在,我们希望它做的事情有:

  1. 打开一个数据库文件
  2. 检查文件里面是否已经存储了一个区块链
  3. 如果已经存储了一个区块链:
    1. 创建一个新的 Blockchain 实例
    2. 设置 Blockchain 实例的 tip 为数据库中存储的最后一个块的哈希
  4. 如果没有区块链:
    1. 创建创世块
    2. 存储到数据库
    3. 将创世块哈希保存为最后一个块的哈希
    4. 创建一个新的 Blockchain 实例,其 tip 指向创世块(tip 有尾部,尖端的意思,在这里 tip 存储的是最后一个块的哈希)

代码大概是这样:

func NewBlockchain() *Blockchain {
    var tip []byte
    db, err := bolt.Open(dbFile, 0600, nil)

    err = db.Update(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))

        if b == nil {
            genesis := NewGenesisBlock()
            b, err := tx.CreateBucket([]byte(blocksBucket))
            err = b.Put(genesis.Hash, genesis.Serialize())
            err = b.Put([]byte("l"), genesis.Hash)
            tip = genesis.Hash
        } else {
            tip = b.Get([]byte("l"))
        }

        return nil
    })

    bc := Blockchain{tip, db}

    return &bc
}

来一段一段地看下代码:

db, err := bolt.Open(dbFile, 0600, nil)

这是打开一个 BoltDB 文件的标准做法。注意,即使不存在这样的文件,它也不会返回错误。

err = db.Update(func(tx *bolt.Tx) error {
...
})

在 BoltDB 中,数据库操作通过一个事务(transaction)进行操作。有两种类型的事务:只读(read-only)和读写(read-write)。这里,打开的是一个读写事务(db.Update(...)),因为我们可能会向数据库中添加创世块。

b := tx.Bucket([]byte(blocksBucket))

if b == nil {
    genesis := NewGenesisBlock()
    b, err := tx.CreateBucket([]byte(blocksBucket))
    err = b.Put(genesis.Hash, genesis.Serialize())
    err = b.Put([]byte("l"), genesis.Hash)
    tip = genesis.Hash
} else {
    tip = b.Get([]byte("l"))
}

这里是函数的核心。在这里,我们先获取了存储区块的 bucket:如果存在,就从中读取 l 键;如果不存在,就生成创世块,创建 bucket,并将区块保存到里面,然后更新 l 键以存储链中最后一个块的哈希。

另外,注意创建 Blockchain 一个新的方式:

bc := Blockchain{tip, db}

这次,我们不在里面存储所有的区块了,而是仅存储区块链的 tip。另外,我们存储了一个数据库连接。因为我们想要一旦打开它的话,就让它一直运行,直到程序运行结束。因此,Blockchain 的结构现在看起来是这样:

type Blockchain struct {
    tip []byte
    db  *bolt.DB
}

接下来我们想要更新的是 AddBlock 方法:现在向链中加入区块,就不是像之前向一个数组中加入一个元素那么简单了。从现在开始,我们会将区块存储在数据库里面:

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
    })

    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())
        err = b.Put([]byte("l"), newBlock.Hash)
        bc.tip = newBlock.Hash

        return nil
    })
}

继续来一段一段分解开来:

err := bc.db.View(func(tx *bolt.Tx) error {
    b := tx.Bucket([]byte(blocksBucket))
    lastHash = b.Get([]byte("l"))

    return nil
})

这是 BoltDB 事务的另一个类型(只读)。在这里,我们会从数据库中获取最后一个块的哈希,然后用它来挖出一个新的块的哈希:

newBlock := NewBlock(data, lastHash)
b := tx.Bucket([]byte(blocksBucket))
err := b.Put(newBlock.Hash, newBlock.Serialize())
err = b.Put([]byte("l"), newBlock.Hash)
bc.tip = newBlock.Hash

检查区块链

现在,产生的所有块都会被保存到一个数据库里面,所以我们可以重新打开一个链,然后向里面加入新块。但是在实现这一点后,我们失去了之前一个非常好的特性:我们再也无法打印区块链的区块了,因为现在不是将区块存储在一个数组,而是放到了数据库里面。让我们来解决这个问题!

BoltDB 允许对一个 bucket 里面的所有 key 进行迭代,但是所有的 key 都以字节序进行存储,而且我们想要以区块能够进入区块链中的顺序进行打印。此外,因为我们不想将所有的块都加载到内存中(因为我们的区块链数据库可能很大!或者现在可以假装它可能很大),我们将会一个一个地读取它们。故而,我们需要一个区块链迭代器(BlockchainIterator):

type BlockchainIterator struct {
    currentHash []byte
    db          *bolt.DB
}

每当要对链中的块进行迭代时,我们就会创建一个迭代器,里面存储了当前迭代的块哈希(currentHash)和数据库的连接(db)。通过 db,迭代器逻辑上被附属到一个区块链上(这里的区块链指的是存储了一个数据库连接的 Blockchain 实例),并且通过 Blockchain 方法进行创建:

func (bc *Blockchain) Iterator() *BlockchainIterator {
    bci := &BlockchainIterator{bc.tip, bc.db}

    return bci
}

注意,迭代器的初始状态为链中的 tip,因此区块将从头到尾,也就是从最新的到最旧的进行获取。实际上,选择一个 tip 就是意味着给一条链“投票”。一条链可能有多个分支,最长的那条链会被认为是主分支。在获得一个 tip (可以是链中的任意一个块)之后,我们就可以重新构造整条链,找到它的长度和需要构建它的工作。这同样也意味着,一个 tip 也就是区块链的一种标识符。

BlockchainIterator 只会做一件事情:返回链中的下一个块。

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
    })

    i.currentHash = block.PrevBlockHash

    return block
}

这就是数据库部分的内容了!

CLI

到目前为止,我们的实现还没有提供一个与程序交互的接口:目前只是在 main 函数中简单执行了 NewBlockchain 和 bc.AddBlock 。是时候改变了!现在我们想要拥有这些命令:

blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain

所有命令行相关的操作都会通过 CLI 结构进行处理:

type CLI struct {
    bc *Blockchain
}

它的 “入口” 是 Run 函数:

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:])
    case "printchain":
        err := printChainCmd.Parse(os.Args[2:])
    default:
        cli.printUsage()
        os.Exit(1)
    }

    if addBlockCmd.Parsed() {
        if *addBlockData == "" {
            addBlockCmd.Usage()
            os.Exit(1)
        }
        cli.addBlock(*addBlockData)
    }

    if printChainCmd.Parsed() {
        cli.printChain()
    }
}

我们会使用标准库里面的 flag 包来解析命令行参数:

addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
addBlockData := addBlockCmd.String("data", "", "Block data")

首先,我们创建两个子命令: addblock 和 printchain, 然后给 addblock 添加 -data 标志。printchain 没有任何标志。

switch os.Args[1] {
case "addblock":
    err := addBlockCmd.Parse(os.Args[2:])
case "printchain":
    err := printChainCmd.Parse(os.Args[2:])
default:
    cli.printUsage()
    os.Exit(1)
}

然后,我们检查用户提供的命令,解析相关的 flag 子命令:

if addBlockCmd.Parsed() {
    if *addBlockData == "" {
        addBlockCmd.Usage()
        os.Exit(1)
    }
    cli.addBlock(*addBlockData)
}

if printChainCmd.Parsed() {
    cli.printChain()
}

接着检查解析是哪一个子命令,并调用相关函数:

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: %xn", block.PrevBlockHash)
        fmt.Printf("Data: %sn", block.Data)
        fmt.Printf("Hash: %xn", block.Hash)
        pow := NewProofOfWork(block)
        fmt.Printf("PoW: %sn", strconv.FormatBool(pow.Validate()))
        fmt.Println()

        if len(block.PrevBlockHash) == 0 {
            break
        }
    }
}

这部分内容跟之前的很像,唯一的区别是我们现在使用的是 BlockchainIterator 对区块链中的区块进行迭代:

记得不要忘了对 main 函数作出相应的修改:

func main() {
    bc := NewBlockchain()
    defer bc.db.Close()

    cli := CLI{bc}
    cli.Run()
}

注意,无论提供什么命令行参数,都会创建一个新的链。

这就是今天的所有内容了! 来看一下是不是如期工作:

$ blockchain_go printchain
No existing blockchain found. Creating a new one...
Mining the block containing "Genesis Block"
000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b

Prev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true

$ blockchain_go addblock -data "Send 1 BTC to Ivan"
Mining the block containing "Send 1 BTC to Ivan"
000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13

Success!

$ blockchain_go addblock -data "Pay 0.31337 BTC for a coffee"
Mining the block containing "Pay 0.31337 BTC for a coffee"
000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148

Success!

$ blockchain_go printchain
Prev. hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
Data: Pay 0.31337 BTC for a coffee
Hash: 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148
PoW: true

Prev. hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
Data: Send 1 BTC to Ivan
Hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
PoW: true

Prev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true

总结

在下篇文章中,我们将会实现地址,钱包,(可能实现)交易。尽请收听!

链接

本文源码:part_3

Part 4:交易(1)

 

引言

交易(transaction)是比特币的核心所在,而区块链的唯一目的,也正是为了能够安全可靠地存储交易。在区块链中,交易一旦被创建,就没有任何人能够再去修改或是删除它。在今天的文章中,我们将会开始实现交易这个部分。不过,由于交易是很大的话题,我会把它分为两部分来讲:在今天这个部分,我们会实现交易的通用机制。在第二部分,我们会继续讨论它的一些细节。

此外,由于代码实现变化很大,就不在文章中罗列所有代码了,这里 可以看到所有的改动。

没有勺子

如果以前开发过 web 应用,在支付的实现环节,你可能会在数据库中创建这样两张表:

  • accounts
  • transactions

account(账户)会存储用户信息,里面包括了个人信息和余额。transaction(交易)会存储资金转移信息,也就是资金从一个账户转移到另一个账户这样的内容。在比特币中,支付是另外一种完全不同的方式:

  1. 没有账户(account)
  2. 没有余额(balance)
  3. 没有住址(address)
  4. 没有货币(coin)
  5. 没有发送人和接收人(sender,receiver)(这里所说的发送人和接收人是基于目前现实生活场景,交易双方与人是一一对应的。而在比特币中,“交易双方”是地址,地址背后才是人,人与地址并不是一一对应的关系,一个人可能有很多个地址。)

鉴于区块链是一个公开开放的数据库,所以我们并不想要存储钱包所有者的敏感信息(所以具有一定的匿名性)。资金不是通过账户来收集,交易也不是从一个地址将钱转移到另一个地址,也没有一个字段或者属性来保存账户余额。交易就是区块链要表达的所有内容。那么,交易里面到底有什么内容呢?

比特币交易

一笔交易由一些输入(input)和输出(output)组合而来:

type Transaction struct {
    ID   []byte
    Vin  []TXInput
    Vout []TXOutput
}

对于每一笔新的交易,它的输入会引用(reference)之前一笔交易的输出(这里有个例外,也就是我们待会儿要谈到的 coinbase 交易)。所谓引用之前的一个输出,也就是将之前的一个输出包含在另一笔交易的输入当中。交易的输出,也就是币实际存储的地方。下面的图示阐释了交易之间的互相关联:

img

the interconnection of transactions

注意:

  1. 有一些输出并没有被关联到某个输入上
  2. 一笔交易的输入可以引用之前多笔交易的输出
  3. 一个输入必须引用一个输出

贯穿本文,我们将会使用像“钱(money)”,“币(coin)”,“花费(spend)”,“发送(send)”,“账户(account)” 等等这样的词。但是在比特币中,实际并不存在这样的概念。交易仅仅是通过一个脚本(script)来锁定(lock)一些价值(value),而这些价值只可以被锁定它们的人解锁(unlock)。

交易输出

让我们先从输出(output)开始:

type TXOutput struct {
    Value        int
    ScriptPubKey string
}

实际上,正是输出里面存储了“币”(注意,也就是上面的 Value 字段)。而这里的存储,指的是用一个数学难题对输出进行锁定,这个难题被存储在 ScriptPubKey 里面。在内部,比特币使用了一个叫做 Script 的脚本语言,用它来定义锁定和解锁输出的逻辑。虽然这个语言相当的原始(这是为了避免潜在的黑客攻击和滥用而有意为之),并不复杂,但是我们并不会在这里讨论它的细节。你可以在这里 找到详细解释。

在比特币中,value 字段存储的是 satoshi 的数量,而不是>有 BTC 的数量。一个 satoshi 等于一百万分之一的 >BTC(0.00000001 BTC),这也是比特币里面最小的货币单位>(就像是 1 分的硬币)。

由于还没有实现地址(address),所以目前我们会避免涉及逻辑相关的完整脚本。ScriptPubKey 将会存储一个任意的字符串(用户定义的钱包地址)。

顺便说一下,有了一个这样的脚本语言,也意味着比特币其实也可以作为一个智能合约平台。

关于输出,非常重要的一点是:它们是不可再分的(invisible),这也就是说,你无法仅引用它的其中某一部分。要么不用,如果要用,必须一次性用完。当一个新的交易中引用了某个输出,那么这个输出必须被全部花费。如果它的值比需要的值大,那么就会产生一个找零,找零会返还给发送方。这跟现实世界的场景十分类似,当你想要支付的时候,如果一个东西值 1 美元,而你给了一个 5 美元的纸币,那么你会得到一个 4 美元的找零。

交易输入

这里是输入:

type TXInput struct {
    Txid      []byte
    Vout      int
    ScriptSig string
}

正如之前所提到的,一个输入引用了之前一笔交易的一个输出:Txid 存储的是这笔交易的 ID,Vout 存储的是该输出在这笔交易中所有输出的索引(因为一笔交易可能有多个输出,需要有信息指明是具体的哪一个)。ScriptSig 是一个脚本,提供了可作用于一个输出的 ScriptPubKey 的数据。如果 ScriptSig 提供的数据是正确的,那么输出就会被解锁,然后被解锁的值就可以被用于产生新的输出;如果数据不正确,输出就无法被引用在输入中,或者说,也就是无法使用这个输出。这种机制,保证了用户无法花费属于其他人的币。

再次强调,由于我们还没有实现地址,所以 ScriptSig 将仅仅存储一个任意用户定义的钱包地址。我们会在下一篇文章中实现公钥(public key)和签名(signature)。

来简要总结一下。输出,就是 “币” 存储的地方。每个输出都会带有一个解锁脚本,这个脚本定义了解锁该输出的逻辑。每笔新的交易,必须至少有一个输入和输出。一个输入引用了之前一笔交易的输出,并提供了数据(也就是 ScriptSig 字段),该数据会被用在输出的解锁脚本中解锁输出,解锁完成后即可使用它的值去产生新的输出。

也就是说,每一笔输入都是之前一笔交易的输出,那么从一笔交易开始不断往前追溯,它涉及的输入和输出到底是谁先存在呢?换个说法,这是个鸡和蛋谁先谁后的问题,是先有蛋还是先有鸡呢?

先有蛋

在比特币中,是先有蛋,然后才有鸡。输入引用输出的逻辑,是经典的“蛋还是鸡”问题:输入先产生输出,然后输出使得输入成为可能。在比特币中,最先有输出,然后才有输入。换而言之,第一笔交易只有输出,没有输入。

当矿工挖出一个新的块时,它会向新的块中添加一个 coinbase 交易。coinbase 交易是一种特殊的交易,它不需要引用之前一笔交易的输出。它“凭空”产生了币(也就是产生了新币),这也是矿工获得挖出新块的奖励,可以理解为“发行新币”。

在区块链的最初,也就是第一个块,叫做创世块。正是这个创世块,产生了区块链最开始的输出。对于创世块,不需要引用之前交易的输出。因为在创世块之前根本不存在交易,也就没有不存在有交易输出。

来创建一个 coinbase 交易:

func NewCoinbaseTX(to, data string) *Transaction {
    if data == "" {
        data = fmt.Sprintf("Reward to '%s'", to)
    }

    txin := TXInput{[]byte{}, -1, data}
    txout := TXOutput{subsidy, to}
    tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}
    tx.SetID()

    return &tx
}

coinbase 交易只有一个输出,没有输入。在我们的实现中,它的 Txid 为空,Vout 等于 -1。并且,在目前的视线中,coinbase 交易也没有在 ScriptSig 中存储一个脚本,而只是存储了一个任意的字符串。

在比特币中,第一笔 coinbase 交易包含了如下信息:“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。可点击这里查看.

subsidy 是奖励的数额。在比特币中,实际并没有存储这个数字,而是基于区块总数进行计算而得:区块总数除以 210000 就是 subsidy。挖出创世块的奖励是 50 BTC,每挖出 210000 个块后,奖励减半。在我们的实现中,这个奖励值将会是一个常量(至少目前是)。

将交易保存到区块链

从现在开始,每个块必须存储至少一笔交易。如果没有交易,也就不可能挖出新的块。这意味着我们应该移除 Block 的 Data 字段,取而代之的是存储交易:

type Block struct {
    Timestamp     int64
    Transactions  []*Transaction
    PrevBlockHash []byte
    Hash          []byte
    Nonce         int
}

NewBlock 和 NewGenesisBlock 也必须做出相应改变:

func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block {
    block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0}
    ...
}

func NewGenesisBlock(coinbase *Transaction) *Block {
    return NewBlock([]*Transaction{coinbase}, []byte{})
}

接下来修改创建新链的函数:

func CreateBlockchain(address string) *Blockchain {
    ...
    err = db.Update(func(tx *bolt.Tx) error {
        cbtx := NewCoinbaseTX(address, genesisCoinbaseData)
        genesis := NewGenesisBlock(cbtx)

        b, err := tx.CreateBucket([]byte(blocksBucket))
        err = b.Put(genesis.Hash, genesis.Serialize())
        ...
    })
    ...
}

现在,这个函数会接受一个地址作为参数,这个地址会用来接收挖出创世块的奖励。

工作量证明

工作量证明算法必须要将存储在区块里面的交易考虑进去,以此保证区块链交易存储的一致性和可靠性。所以,我们必须修改 ProofOfWork.prepareData 方法:

func (pow *ProofOfWork) prepareData(nonce int) []byte {
    data := bytes.Join(
        [][]byte{
            pow.block.PrevBlockHash,
            pow.block.HashTransactions(), // This line was changed
            IntToHex(pow.block.Timestamp),
            IntToHex(int64(targetBits)),
            IntToHex(int64(nonce)),
        },
        []byte{},
    )

    return data
}

不像之前使用 pow.block.Data,现在我们使用 pow.block.HashTransactions() :

func (b *Block) HashTransactions() []byte {
    var txHashes [][]byte
    var txHash [32]byte

    for _, tx := range b.Transactions {
        txHashes = append(txHashes, tx.ID)
    }
    txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))

    return txHash[:]
}

我们使用哈希提供数据的唯一表示,这个之前也遇到过。我们想要通过仅仅一个哈希,就可以识别一个块里面的所有交易。为此,我们获得每笔交易的哈希,将它们关联起来,然后获得一个连接后的组合哈希。

比特币使用了一个更加复杂的技术:它将一个块里面包含的所有交易表示为一个 Merkle tree,然后在工作量证明系统中使用树的根哈希(root hash)。这个方法能够让我们快速检索一个块里面是否包含了某笔交易,即只需 root hash 而无需下载所有交易即可完成判断。

来检查一下到目前为止是否正确:

$ blockchain_go createblockchain -address Ivan
00000093450837f8b52b78c25f8163bb6137caf43ff4d9a01d1b731fa8ddcc8a

Done!

很好!我们已经获得了第一笔挖矿奖励,但是,我们要如何查看余额呢?

未花费的交易输出

我们需要找到所有的未花费交易输出(unspent transactions outputs, UTXO)。未花费(unspent) 指的是这个输出还没有被包含在任何交易的输入中,或者说没有被任何输入引用。在上面的图示中,未花费的输出是:

  1. tx0, output 1;
  2. tx1, output 0;
  3. tx3, output 0;
  4. tx4, output 0.

当然了,当我们检查余额时,我们并不需要知道整个区块链上所有的 UTXO,只需要关注那些我们能够解锁的那些 UTXO(目前我们还没有实现密钥,所以我们将会使用用户定义的地址来代替)。首先,让我们定义在输入和输出上的锁定和解锁方法:

func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
    return in.ScriptSig == unlockingData
}

func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
    return out.ScriptPubKey == unlockingData
}

在这里,我们只是将 script 字段与 unlockingData 进行了比较。在后续文章我们基于私钥实现了地址以后,会对这部分进行改进。

下一步,找到包含未花费输出的交易,这一步相当困难:

func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction {
  var unspentTXs []Transaction
  spentTXOs := make(map[string][]int)
  bci := bc.Iterator()

  for {
    block := bci.Next()

    for _, tx := range block.Transactions {
      txID := hex.EncodeToString(tx.ID)

    Outputs:
      for outIdx, out := range tx.Vout {
        // Was the output spent?
        if spentTXOs[txID] != nil {
          for _, spentOut := range spentTXOs[txID] {
            if spentOut == outIdx {
              continue Outputs
            }
          }
        }

        if out.CanBeUnlockedWith(address) {
          unspentTXs = append(unspentTXs, *tx)
        }
      }

      if tx.IsCoinbase() == false {
        for _, in := range tx.Vin {
          if in.CanUnlockOutputWith(address) {
            inTxID := hex.EncodeToString(in.Txid)
            spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
          }
        }
      }
    }

    if len(block.PrevBlockHash) == 0 {
      break
    }
  }

  return unspentTXs
}

由于交易被存储在区块里,所以我们不得不检查区块链里的每一笔交易。从输出开始:

if out.CanBeUnlockedWith(address) {
    unspentTXs = append(unspentTXs, tx)
}

如果一个输出被一个地址锁定,并且这个地址恰好是我们要找的未花费交易输出的地址,那么这个输出就是我们想要的。不过在获取它之前,我们需要检查该输出是否已经被包含在一个输入中,也就是检查它是否已经被花费了:

if spentTXOs[txID] != nil {
    for _, spentOut := range spentTXOs[txID] {
        if spentOut == outIdx {
            continue Outputs
        }
    }
}

我们跳过那些已经被包含在其他输入中的输出(被包含在输入中,也就是说明这个输出已经被花费,无法再用了)。检查完输出以后,我们将所有能够解锁给定地址锁定的输出的输入聚集起来(这并不适用于 coinbase 交易,因为它们不解锁输出):

if tx.IsCoinbase() == false {
    for _, in := range tx.Vin {
        if in.CanUnlockOutputWith(address) {
            inTxID := hex.EncodeToString(in.Txid)
            spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
        }
    }
}

这个函数返回了一个交易列表,里面包含了未花费输出。为了计算余额,我们还需要一个函数将这些交易作为输入,然后仅返回一个输出:

func (bc *Blockchain) FindUTXO(address string) []TXOutput {
       var UTXOs []TXOutput
       unspentTransactions := bc.FindUnspentTransactions(address)

       for _, tx := range unspentTransactions {
               for _, out := range tx.Vout {
                       if out.CanBeUnlockedWith(address) {
                               UTXOs = append(UTXOs, out)
                       }
               }
       }

       return UTXOs
}

就是这么多了!现在我们来实现 getbalance 命令:

func (cli *CLI) getBalance(address string) {
    bc := NewBlockchain(address)
    defer bc.db.Close()

    balance := 0
    UTXOs := bc.FindUTXO(address)

    for _, out := range UTXOs {
        balance += out.Value
    }

    fmt.Printf("Balance of '%s': %dn", address, balance)
}

账户余额就是由账户地址锁定的所有未花费交易输出的总和。

在挖出创世块以后,来检查一下我们的余额:

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 10

这就是我们的第一笔钱!

发送币

现在,我们想要给其他人发送一些币。为此,我们需要创建一笔新的交易,将它放到一个块里,然后挖出这个块。之前我们只实现了 coinbase 交易(这是一种特殊的交易),现在我们需要一种通用的交易:

func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
    var inputs []TXInput
    var outputs []TXOutput

    acc, validOutputs := bc.FindSpendableOutputs(from, amount)

    if acc < amount {
        log.Panic("ERROR: Not enough funds")
    }

    // Build a list of inputs
    for txid, outs := range validOutputs {
        txID, err := hex.DecodeString(txid)

        for _, out := range outs {
            input := TXInput{txID, out, from}
            inputs = append(inputs, input)
        }
    }

    // Build a list of outputs
    outputs = append(outputs, TXOutput{amount, to})
    if acc > amount {
        outputs = append(outputs, TXOutput{acc - amount, from}) // a change
    }

    tx := Transaction{nil, inputs, outputs}
    tx.SetID()

   
                        
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/shangsongwww/article/details/90112529
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-04-18 12:52:10
  • 阅读 ( 1730 )
  • 分类:区块链

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢