让我们自己动手实现区块链吧


#1

对于一些初学者,对于区块链底层的实现机制可能会比较感兴趣。如果你们是像我一样的新手,那我们就在这里一点一点记录、分享自己的学习过程吧!:grinning:祝大家早日成为大牛:beer::beer::beer:


#3

BF社区的开发者你们好,作为初学者,我将翻译的文章从自己博客搬到这里,希望对大家有所帮助。

使用Golang构建区块链Part1:基本原型

最前面

这个系列的博客是受了AnnatarHe的启发,他将JeiWan的__《Building Blockchain in Go》__系列文章进行了翻译。于是我要半翻译半转载的尽量完成这系列博客。

References

为了不在以后忘记,我将__《使用Golang构建区块链》__系列博客的参考、引用、转载等信息写在最前面:

  • AnnatarHe 这是翻译者的博客地址,具体文章在他的主页里。我很喜欢他的一些文章,包括非技术文,有一种像是“读了很多很多书、学了很多很多知识的另一个我”,或许我们在性格上有很多相似之处。
  • JeiWan 本系列文章的原创作者,我很喜欢他的博客名字__Going the distance__。
  • Github上另一个中文翻译版本,补充了很多要点

以上,致谢!

介绍

区块链是21世纪最具有革命性的技术,它技术足够成熟而且潜力尚未完全发掘。在本质上,区块链是一个可以记录数据的分布式数据库。但独特的是它并不是一个隐私的数据库而是一个公开的,每一个使用它的人可以对它完全或者部分拷贝。并且,一条新的记录想要加入,必须经过所有维持它的人(所有节点)的同意。区块链使得加密货币以及智能合约成为可能。

在接下来的一系列文章中,我们将使用区块链技术来构建一个简单的加密货币系统。

区块

让我们从“区块链”的组成“区块”开始。区块链中,区块来存储有价值的信息。比如,比特币区块存储交易信息,这就是加密货币的精髓所在。除此之外,一个区块还包含一些技术信息,就像当前的版本,区块所存储的信息就包含当前的时间戳和上一个区块的hash(哈希)。

本文中我们并不会按照描述区块链以及比特币那样构建一个十分完备的区块,而是实现一个较为简单的版本,仅仅包含关键的技术信息要点,它看起来就像下面:

type Block struct {
	Timestamp     int64			// 时间戳,由当前区块创建的时间转化而来
	Data          []byte		// 区块存储的实际的有价值的信息,也就是交易
	PrevBlockHash []byte		// 前一个区块的哈希,也叫父哈希
	Hash          []byte		// 当前区块的哈希
}

在比特币规格说明中,Timestamp、PrevBlockHash、Hash属于区块头(headers),这些形成一个独立数据结构,完整的比特币区块头的结构如下:

Field Purpose Updated when… Size (Bytes)
Version Block version number You upgrade the software and it specifies a new version 4
hashPrevBlock 256-bit hash of the previous block header A new block comes in 32
hashMerkleRoot 256-bit hash based on all of the transactions in the block A transaction is accepted 32
Time Current timestamp as seconds since 1970-01-01T00:00 UTC Every few seconds 4
Bits Current target in compact format The difficulty is adjusted 4
Nonce 32-bit number (starts at 0) A hash is tried (increments) 4

下面是比特币中使用Golang语言实现的btcd的BlockHeader的实现:

// BlockHeader defines information about a block and is used in the bitcoin
// block (MsgBlock) and headers (MsgHeaders) messages.
type BlockHeader struct {
    // Version of the block.  This is not the same as the protocol version.
    Version int32

    // Hash of the previous block in the block chain.
    PrevBlock chainhash.Hash

    // Merkle tree reference to hash of all transactions for the block.
    MerkleRoot chainhash.Hash

    // Time the block was created.  This is, unfortunately, encoded as a
    // uint32 on the wire and therefore is limited to 2106.
    Timestamp time.Time

    // Difficulty target for the block.
    Bits uint32

    // Nonce used to generate the block.
    Nonce uint32
}

而交易(在我们这里是Data)则是另一个独立的数据结构。所以在这里我们为了简单这样来处理。在真正的比特币中,区块的数据结构如下:

Field Description Size
Magic no value always 0xD9B4BEF9 4 bytes
Blocksize number of bytes following up to end of block 4 bytes
Blockheader consists of 6 items 80 bytes
Transaction counter positive integer VI = VarInt 1 - 9 bytes
transactions the (non empty) list of transactions -many transactions

所以,我们该如何计算哈希呢?哈希的计算是区块链的一个非常重要的特点,这个特点使得区块链是安全的。计算哈希在计算方面上是一件非常困难的操作,即使在一些性能非常好的计算机上也要花些时间(这就是为什么人们使用性能更加强劲的GPU来挖比特币)。这是一种刻意的结构设计,使得向区块链中添加区块是一件非常困难的事情,由此防止添加区块后又进行修改。我们将在后续的文章讨论并实现这个机制。

至此,我们拿到了区块字段,然后连接起来,使用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的惯例,我们将实现一个简单的创建区块的函数__NewBlock​__:

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

这就是区块。

区块链

现在,让我们实现区块链。区块链的本质是一个具有确定结构的数据库:一个有序的,向后连接(新区块由前一个区块生成,也就是可以向后迭代)的列表。也就意味着,每个新区块的插入是连接着前一个区块的。这种结构使得可以快速的得到前面的区块并且(有效的)通过哈希值找到某个区块。

在Golang中,我们可以使用 arraymap 结构来实现:数组可以保证哈希的有序性(Golang中数组是有序的),而 map (__map__是无序的)可以保证__哈希__到__区块__的映射。但是在我们的区块链原型中,我们仅使用一个数组,因为我们现在不需要通过哈希去寻找区块。

type Blockchain struct {
	blocks []*Block
}

这是我们第一个区块链,我从没想过会这么简单 :wink:

现在,让添加区块成为可能:

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

现在,我们可以实现一个函数,使用__genesis__ __block__来创建一个区块链:

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: %x\n", block.PrevBlockHash)
		fmt.Printf("Data: %s\n", block.Data)
		fmt.Printf("Hash: %x\n", 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

就是这样了!

总结

我们构建了一个非常简单的区块链原型:仅仅是区块数组,每一个区块连接到它前一个区块。真实的区块更加的复杂。在我们的区块链中添加新的区块是非常简单并且迅速的,但是在真正的区块链中添加区块需要这样的工作:一是在添加区块之前需要进行十分繁重的计算(这个机制叫做Proof-of-Word工作量证明)。同时,区块链是一个分布式数据库,没有单独的决策者。因此,一个新的区块必须被这个网络(区块链网络)的参与者们确认和承认(这个机制叫共识机制)。别忘了我们现在区块中还没有交易!

后面的文章中我们将覆盖到所有的这些要点。

Links:

  1. Full source codes: https://github.com/Jeiwan/blockchain_go/tree/part_1
  2. Block hashing algorithm: https://en.bitcoin.it/wiki/Block_hashing_algorithm

#4

使用Golang构建区块链Part2:工作量证明

介绍

在前面的文章中我们构建了一个非常简单的数据结构,也是区块链数据库的精华所在。然后我们让区块链以“链式”的形式添加区块成为可能:每一个区块连接到它前面的一个区块。可是,我们的区块链实现有一个重大的瑕疵:往链上添加区块太过简单,花费的代价太低。让添加区块是一件非常艰难的工作,这是区块链和比特币的主旨。今天我们就去修复这个瑕疵。

工作量证明

区块链的一个核心思想就是存放一个数据需要执行困难的工作。这个艰难的工作保证了区块链安全性和一致性。同时,这项困难的工作也带来相应的报酬(人们就是这样在挖矿中收获的比特币的)。

这项机制非常像我们的真实生活:一个人必须去努力工作,获得报酬以至去维持生活。在区块链中,一些维持网络工作的参与者(矿工)去维持区块链网络,向其中添加新的区块,并获得奖励。得益于他们的工作,区块可以以一种安全的方式加入区块链,维持区块链数据库的稳定性。值得注意的是,完成这项工作的人必须注意到这一点。

这些全部的“困难的工作并且去证明”的机制就叫做工作量证明(Proof-of-Work)。这是非常困难的,因为它需要非常强大的算力(计算能力):甚至是高性能计算机也不能很快的将它计算出。此外,这项工作难度的增加使其保持着每小时的出块速度为6块。在区块链中,这项工作的目标是是为每个区块去寻找一个哈希(或者叫做散列),用来满足一些要求。然后用这个哈希去作为一个证明。因此,为这个真正的工作寻找出一个证明。

最后一件需要注意的事情是,工作量证明必须满足一个要求:做这项工作是困难的,但是验证它却是容易的。一个证明通常情况下是交给别人完成的,所以对于他们,这不应该花费太多时间。

哈希计算

在这一段落,我们将讨论哈希,如果你熟悉这个概念,你可以跳过这个部分。获取指定数据哈希值的过程就叫做哈希运算。一个哈希是对数据进行计算得到的独一无二的代表。一个哈希函数的功能是对任意大小的数据产生一个固定大小的哈希。这里有一些关于哈希的关键特点:

  1. 原始数据不能从哈希反推。所以哈希并不是一种加密。
  2. 确定的数据只能产生一个哈希并且是独一无二的。
  3. 即使改变一个比特的输入数据也会得到截然不同的哈希。

哈希函数广泛的应用于检测数据的一致性(数据是否被篡改)。一些软件提供者为软件包添加出版验证,在下载之后你可以通过一个哈希函数来与软件开发者提供的进行比较。

在区块链中,哈希用来保证区块的一致性。哈希函数的输入数据包括前一个区块,因此对于区块的修改成为不可能的事情(或者…至少说是非常非常困难),一个人要是修改一个区块必须连同它后面的区块一并修改。

Hashcash

比特币中使用了__Hashcash__,一个最初成熟于防止垃圾邮件的工作量证明算法。它可以被分成这样的几步:

  1. 获得一些公开的数据(在电子邮件中,它是收件者的邮箱地址,在比特币中,它是区块头)。
  2. 添加一个计数器__counter__进去,计数器从0开始。
  3. 得到__data+counter__组合的哈希。
  4. 检查哈希是否满足确定的要求。
    1. 如果是,那么你就完成了!
    2. 如果不是,增加__counter__的数值,然后重复步骤3和步骤4.

因此,这是一种蛮力算法:你改变__counter__计数器的值,不断地计算新的哈希,检查它,增加__counter__的值,计算哈希…这就是为什么在计算上来讲,是代价昂贵的。

现在,让我们更近一步的看一下哈希需要满足的基本需求。在原始的__Hashcash__的实现中,这种需求是听起来像“哈希的前20比特位必须是零”这样的。在比特币中,这个必要条件随着时间不断调整,因为,刻意的,每十分钟才可以产生一个区块,即使计算能力随着时间的推移不断增加,而且有越来越多的矿工的加入。

为了证明这个算法,我找了一个和先前例子相似的(“I like donuts”),然后找到一个前三位是0的哈希:

ca07ca__是计数器的16进制表示,也就是十进制系统中的__13240266

实现

好啦,我们现在完成了理论部分,让我们开始coding!!!首先,我们定义挖矿:pick:难度:

const targetBits = 24

在比特币中,__“target bits”__是区块被挖出来时存储在区块头的难度。我们现在不用实现target的调整算法,所以我们可以用一个全局常量来定义难度。

24可以是任意的一个数字,我们的目标是有一个占用内存少于256bit的target。同时,我们也想要足够的差异让它具有代表性,但是不要太大,因为差异越大就越难找到一个合适的哈希。

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__结构体,有一个指向区块的指针和一个指向target的指针。这里的“target”就是前面段落描述的“必要条件”的另一个名字。我们使用__big integer__是由于我们对哈希和target的比较方式:我们将哈希转换成一个big integer类型然后检查它是否小于target。

在__NewProofOfWork__函数中,我们使用1初始化了一个__big.Int__并且将它左移了__256 - targetBits__比特位。256是一个__SHA-256__哈希函数,同时__SHA-256__哈希函数也是我们正要使用的。这个__target__的16进制的表示是:

0x10000000000000000000000000000000000000000000000000000000000

然后它在内存中占用了29个字节。这里是它与前一个例子的哈希的视觉上的比较:

0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3
0000010000000000000000000000000000000000000000000000000000000000
0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca

第一个哈希(从"I like donuts"计算得到)要比target大,因此它不是一个合法的工作量证明。第二个哈希(从"I like donutsca07ca"计算得到)是小于target的,所以这是一个有效证明。

引自Github上的翻译版本:

译者注:上面的形式化比较有些“言不符实”,其实它应该并非由 “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("%x\n", sha256.Sum256(data1))
 fmt.Printf("%64x\n", target)
 fmt.Printf("%x\n", sha256.Sum256(data2))

}

输出:

你可以把一个target想象成一个目标范围的上界:如果一个数(哈希)比这个界限小,它就是合法的;反之就是不合法的。比界限小将导致合法的数更少,因此也就需要进行更困难的工作去找到更有效的一个。

现在,我们需要进行哈希的数据。让我们去准备它:

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__。nonce__是上文中__Hashcash__中所描述的计数器__counter,这是密码学术语。

好啦,所有的准备已完成,让我们实现__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__是哈希的整数表现形式;nonce__是计数器。接下来,我们跑一个无限循环:它被__maxNonce__所限制,大小为__math.MaxInt64;这是避免__nonce__可能的溢出。虽然,对于计数器溢出来说,PoW的实现难度太低,但为了以防万一最好还是检查一下。

在这个循环中,我们要做:

  1. 准备数据。
  2. 使用__SHA-256__取得哈希。
  3. 将哈希转化成__big integer__类型。
  4. 将这个__integer__与__target__进行比较。

同前面的解释一样简单。现在我们可以移除__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

Wow!你可以看到每个哈希现在都是以三个字节的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__。

让我们再检查一次一切是否:ok:

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

引自Github上的翻译版本:

译者注:

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

总结

我们距离真正的区块链更近了:添加区块现在需要繁重的工作,因此挖矿就成为可能。但是它仍然缺少一些重要的要点,这里没有钱包、没有地址、没有交易,也没有共识机制。这些事情我们将会在后面的文章中实现,至于现在,开心的挖矿:pick:叭!

Links:

  1. Full source codes
  2. Blockchain hashing algorithm
  3. Proof of work
  4. Hashcash

#5

使用Golang构建区块链Part3:持久化和命令行接口

介绍

到现在,我们已经构建了一个有工作量证明系统和可以挖矿的区块链。我们的实现离一个具有完整功能的区块链又进了一步,但是它仍然少一些重要的性质。今天,我们要将区块链存储到数据库中,在这之后,我们将做一个简单的命令行接口来支持区块链的操作。在它的一个重要的本质中,区块链是一个分布式数据库;我们现在要省略“分布式”的部分,而集中精力于“数据存储”部分。

数据库的选择

目前,在我们的实现中并没有数据库;反而我们在每一次运行程序创建区块时都存储在内存中。我们既不能重复使用区块链也不能与其他人共享,因此我们需要将它存储在磁盘中。

我们需要哪一个数据库呢?事实上,任何一个都可以。在比特币的原始论文中,没有说明使用哪一个特定的数据库。所以,使用什么数据库完全取决于开发者。 Bitcoin Core ,最初由中本聪发布的,现在是比特币的一个参考实现,它使用的是LevelDB(虽然它在2012年才发布客户端)。而我们将要使用的是…

BoltDB

原因:

  1. 它非常简单便捷。
  2. 它是由Go语言实现的。
  3. 它不需要运行任何一个服务器。
  4. 它允许我们想要的数据结构。

在__BoltDB__ Github上的README写道:

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

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

听起来很完美的契合了我们的需求!让我们快速的回顾一下它:

__BoltDB__是一个键值存储结构,这就意味着它不像关系型数据库(MySQL、PostgreSQL等)有表,没有行、列。相反的,数据以一种key-value(键值对)的组合形式存储(就像Go语言中的__map__结构)。键值对存储在桶中,这是有意的给键值对分组(这有些像关系型数据库中的表)。因此,为了获得一个值,你需要知道一个桶(bucket)和一个键(key)。

关于__BoltDB__一个重要的点是它没有数据类型:键和值都是以字节数组的形式存储。由于我们要存储Go的数据结构(具体来说是__Block__),我们需要将它们进行序列化。也就是说,实现一个可以将Go结构体转化为字节数组并可以从字节数组恢复到Go结构体的机制。我们将使用encoding/gob来实现这个,但是JSON、XML、Protocol Buffers等也是可以的。我们使用encoding/gob是因为它简单而且是Go的标准库。

数据库结构

在开始实现持久化的逻辑之前,我们需要决定怎样将数据存储到数据库中。为此,我们参考比特币的做法。

简单来说,比特币使用了两个桶(bucket)来存储数据:

  1. __blocks__存储链上区块的元数据描述。
  2. __chainstate__存储链的状态,也就是目前所有的未花费交易输出以及一些数据。

同时,区块被存储为磁盘上不同的文件。出于对性能的考虑:加载单个的区块不需要从内存中加载所有(或者部分)文件。我们不需要实现这些。

blocks 中, key -> value 对应关系是这样的:

  1. ‘b’ + 32 字节的区块 hash -> 区块索引记录
  2. ‘f’ + 4 字节文件数字 -> 文件信息记录
  3. ‘l’ -> 4 字节文件数字: 最后一个使用过的区块文件数字
  4. ‘R’ -> 1 字节布尔值: 我们是否要去重新索引
  5. ‘F’ + 1 字节标志名长度 + 标志名 -> 1 字节布尔值: 开或关的多种标志
  6. ‘t’ + 32 字节交易 hash -> 交易索引记录

chainstate, key -> value 对应关系是这样的:

  1. ‘c’ + 32 字节交易 hash -> 未使用的交易出账记录
  2. ‘B’ -> 32 字节区块 hash: 数据库应该表示的未使用交易出账的区块哈希

(更详细的解释可以在这里找到)

因为我们现在还不需要交易,我们只需要有一个__blocks__桶就可以。同时,就像前面说的,我们将以单个文件的形式存储在数据库中,不需要在分开的文件中存储区块。所以我们也不需要任何与文件相关的数字编号。因此,我们只需要这些键值的对应关系:

  1. 32 字节区块 hash -> 区块数据(序列化后的)
  2. ‘l’ -> 链上最后一个区块的 hash

以上,是我们在实现持久化之前所需要知道的全部。

序列化

如前文所说,在__BoltDB__中数据只能是__[]byte__的形式,我们想存储__Block__结构在数据库中。我们将使用encoding/gob来对结构体进行序列化。

让我们实现区块的__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__区块。这不是一个方法而是一个独立的函数:

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. 创建一个新的区块链实例。
    2. 设置这个区块链实例的__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__中,数据库通过一个事务进行操作。这里有两种事务:只读(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"(字母L小写,不是1);如果不存在,我们就生成一个创世纪块,创建一个桶,将区块保存进去,之后更新"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__数据库的另一种事务:只读(read-only)。这里我们从数据库中得到了最后一个区块的哈希,用来挖新的区块哈希。

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

在挖出新区块之后,我们将其序列化特征值存储到数据库中,并且更新"l"键,让它保存最新区块的哈希。

完成了!并不是很难,对吗!

检查区块链

所有的新区块现在保存在数据库中,所以我们现在可以重新打开这条链并且向其中添加新的区块。但是在实现了这些后,我们失去了一个非常好的特性:我们不能打印区块链中的区块了,因为我们不再像以前那样存储区块了。让我们修复这个瑕疵!

__BoltDB__数据库允许对桶里的所有key进行迭代,但是所有的key都以字节顺序存储,我们又想以区块在区块链中的顺序进行打印。而且,因为我们不想加载内存中所有的区块(我们的区块链存储数据可能非常庞大!或者假装是这样),我们将把它们一个一个读出来。为此,我们需要一个区块链迭代器:

type BlockchainIterator struct {
	currentHash []byte
	db          *bolt.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__,然后我们添加__-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: %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
		}
	}
}

这部分非常类似于我们前面的那个。唯一的不同是我们现在使用了一个__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

Github的译者版本附加的测试:

(看起来可以开酒(:wine_glass:or​:beer:)了)

总结

接下来我们将实现地址、钱包以及交易(或许也有)。尽情期待!

Links:

  1. Full source codes
  2. Bitcoin Core Data Storage
  3. boltdb
  4. encoding/gob
  5. flag

#6

使用Golang构建区块链Part4:交易(1)

在对照实现本部分内容时,我进行的稍微困难,因为本篇中的实现代码与前面的代码相差的太多。所以,当遇到困难时,就直接去查源码。在后面会有对应代码改动的链接。

介绍

交易是比特币的核心,将交易安全可靠的存储是区块链唯一目的,所以没有人可以在交易创建之后进行修改。今天我们将开始实现交易。但由于这是一个较为庞大的部分,我将它分成了两部分:在本部分,我们会实现交易的通用机制,在后续的那部分中我们将会实现细节部分。

对了,由于代码改动了很多,并且没有必要全部细说。你们可以在这里找到所有的改动。

There is no spoon(这没勺子)

如果你曾经开发过Web应用,为了实现支付你会在数据库中创建这样的tables:accountstransactions 。一个账户会保存一个用户的信息,包含他们的私人信息以及账户余额,同时一个交易会存储money从一个账户到另一个账户的转移信息。在比特币中,支付是实现于一个完全不同的方式。在这:

  1. 没有账户。
  2. 没有余额。
  3. 没有地址。
  4. 没有币。
  5. 没有发送者和接受者。

因为区块链是一个公共开放的数据库,我们不想存储钱包所有者的敏感信息在里面。币不会在账户中被收集。

交易不会把money从一个地址转移到另一个。这里也没有字段或者说是特征来持有账户的余额。这里只有交易。但是,交易里有什么呢?

比特币交易

Github译者补充:

点击 这里 在 blockchain.info 查看下图中的交易信息。

交易包含一些输入(inputs)和一些输出(outputs):

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

一个新的交易的入账/输入取决于上一个交易的出账/输出(不过这儿有一个例外,我们在稍后会讨论到)。出账/输出是币实际上存储在哪里。下面的图表对交易的内在联系进行了示范:

请注意:

  1. 有的出账没有和入账相连。
  2. 在一个交易中,入账涉及多个交易的出账。
  3. 一个入账必须依赖一个出账。

整篇文章中,我们将会使用像“钱”、“币”、“花费”、“发出”、“账户”等等这样的字眼。但是对比特币并不存在这样的概念,交易仅仅是通过一个脚本来锁定一些值,而这些实际的值只可以被锁定他们的人解锁。

( Transactions just lock values with a script, which can be unlocked only by the one who locked them.)

交易出账

让我们从交易出账开始:

type TXOutput struct {
	Value        int
	ScriptPubKey string
}

实际上,这是存储“币”的出账(注意一下上面的“value”字段)。这个存储被一个保存在__ScriptPubKey__里的“谜题”锁定。往深了说,比特币使用了一个叫做__Script__的脚本语言,它用于定义交易输出(出账)的锁定和解锁逻辑。这个语言有一些原始(它有意这样设计以便避免可能的入侵和滥用),但是我们不会讨论它的细节。你可以在这里找到所有细节的解释。

在比特币中,_value_字段存储着_satoshis_的数量,而不是比特币的数量。1 _satoshis_是1亿比特币分之1(0.00000001 BTC)。所以这是比特币货币中最小的单位(像是分)。

因为我们没有实现地址,我们现在将避免涉及到全部逻辑相关的脚本。__ScriptPubKey__将会存储为一个专用的字段(用户定义钱包地址)。

顺便说一下,拥有脚本语言意味着比特币也可以用做智能合约平台。

关于交易输出有一个重要的点是它们是不可分割的,这意味着你不能使用这个值的一部分。当一个交易输出被一个新的交易引用时,它会全部花费。如果这个值比所需要的多,找零会自动生成并返回给发送者。这有些相似于现实世界中你支付的场景,就是说,一个5元的钞票去买一个1元的东西,会得到4元找零。

交易入账

这儿是交易入账:

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

如前面所述,一个交易的入账涉及到前一个交易的出账:Txid__存储这个交易的__ID,__Vout__存储这个交易中一个出账的索引。__ScriptSig__是一个为出账时提供使用的__ScriptPubKey__所需要数据的脚本(ScriptSig是个脚本,这个脚本提供数据,提供的数据是ScriptPubKey所需要的,ScriptPubKey是出账时所需要的脚本)。如果数据正确,交易出账可以被解锁,它的值可以用于生成新的出账;如果不正确,这个出账则不能被入账所引用。这就是保证用户不能花费属于别人的币的一个机制。

再说一次,因为我们没有实现地址,__ScriptSig__将仅仅存储一个专用的用户定义钱包地址。我们在下一篇文章将会实现公钥和签名检查。

让我们总结一下。交易出账是“币”所存在的地方。每一个交易出账都带有一个解锁的脚本,决定着解锁这个交易出账的逻辑。每一个新的交易必须至少有一个入账和出账。一个入账会引用上个交易的出账和数据(ScriptSig 字段),这些被用来在出账中解锁脚本去解锁并使用其中的值创建新的出账。(好吧我长难句不过关…原文: An input references an output from a previous transaction and provides data (the ScriptSig field) that is used in the output’s unlocking script to unlock it and use its value to create new outputs.)

但是,先有谁:入账还是出账?

The egg(蛋)

在比特币中,是先有蛋,后有鸡的。这个交易入账和交易出账的相互关系逻辑是经典的“鸡和蛋”的关系:入账产生出账,而出账使入账成为可能。在比特币中,交易出账产生于交易入账之前,也就是,先有交易出账。

当矿工开始挖矿时,他会添加一笔币基交易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中存储脚本。取而代之的,专有数据存储在这里。

在比特币中,最开始的一个交易中带有这样的信息: “The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”. 在这里可以自个看

subsidy是奖励的总数。在区块链中,这个值不会存储在任何地方而是仅仅根据区块的总数量进行计算:区块被分为210000个。挖这些创世区块产生50BTC,并且每210000个区块这个奖励减半。在我们的实现中,我们将这个奖励存储为一个常量(至少现在是这样:wink:)。

在区块链中存储交易

从现在开始,每个区块必须存储至少1个交易同时没有可能挖一个不包含交易的区块。这意味我们应该移出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{})
}

在这部分以及后面的代码中,由于我理解能力的原因(或是作者描述的略微粗略),这部分以及以后的代码都是在Part3和Part4代码比较里一一对应继续修改的。链接在文章开始的地方。

接下来调整创建区块链的部分:

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

我将完整部分也给大家贴上:

// creates a new blockchain db
func CreateBlockchain(address string) *Blockchain {
	if dbExists() {
		fmt.Println("Blockchain is already exists.")
		os.Exit(1)
	}

	var tip []byte
	db, err := bolt.Open(dbFile, 0600, nil)
	if err != nil {
		log.Panic(err)
	}

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

		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

		return nil

	})

	if err != nil {
		log.Panic(err)
	}

	bc := Blockchain{tip, db}

	return &bc
}

现在,这个函数接收了一个地址,这个地址就是接受挖出创世区块的奖励的。

工作量证明

工作量证明算法必须考虑到存储在区块中的交易,去保证区块链作为一个存储交易仓库的一致性和可靠性。所以,我们现在必须修改Proof-Of-Work.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.HashTransactions()来代替pow.block.Data,就是:

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代表一个区块中包含的所有交易并且在工作量证明系统中使用树的根哈希值。这个方法运行快速的检查一个区块是否包含某个确定的交易,只需要树根的哈希而不需要下载整个交易。

让我们检查一下到目前为止一切是否正确:

$ blockchain_go createblockchain -address Ivan
00000093450837f8b52b78c25f8163bb6137caf43ff4d9a01d1b731fa8ddcc8a

Done!

实话说做到这里,这个我没做出来,应该还是缺一些的。在整篇文章涉及到的代码全部完成后,我才实现这样的内容。

好!我们现在收到我们的第一笔挖矿奖励。但是,我们怎样检查我们的余额呢?

未花费交易输出

我们需要找到全部的未花费交易输出(unspent transaction outputs - UTXO)。未花费表示这些输出没有在任何地方被任何交易入账所引用(也就是哪一个交易的入账都没有使用它)。在上面的图解中,这些就是的:

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

当然,在我们检查余额时,我们并不需要全部的这些,只需要那些可以被我们所拥有的key解锁的(目前我们还没有key的实现,我们会用用户定义地址去代替)。首先,让我们在入账和出账上定义加锁和解锁方法:

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

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

这里我们仅仅使用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交易上,因为它们没有解锁出账):

PS:这里我翻译不好,把原文贴上了

After checking outputs we gather all inputs that could unlock outputs locked with the provided address (this doesn’t apply to coinbase transactions, since they don’t unlock outputs)

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': %d\n", 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()

	return &tx
}

在创建新的输出之前,我们必须寻找所有的未花费输出并且确保它们存储了足够的“钱”。这就是FindSpendableOutputs方法所做的。在那之后,入账所需要引用的出账就被找出来了。接下来,我们创建两个输出:

  1. 一个由接收者的地址锁定。这就是真实的币到一个地址的转移。
  2. 一个由发送者的地址锁定。这是一个改变。它仅仅在未花费交易输出的总额大于新交易所需要的总额时被创建。记住,输出是__不可分割__的。

FindSpendableOutputs方法依赖于我们先前定义的FindUnspentTransactions方法:

func (bc *Blockchain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) {
	unspentOutputs := make(map[string][]int)
	unspentTXs := bc.FindUnspentTransactions(address)
	accumulated := 0

Work:
	for _, tx := range unspentTXs {
		txID := hex.EncodeToString(tx.ID)

		for outIdx, out := range tx.Vout {
			if out.CanBeUnlockedWith(address) && accumulated < amount {
				accumulated += out.Value
				unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)

				if accumulated >= amount {
					break Work
				}
			}
		}
	}

	return accumulated, unspentOutputs
}

这个方法迭代所有的未花费交易并计算它们的总额。当积累的值大于或者等于我们需要进行转换的值时,它就会停止并且返回积累的总值已经由交易ID所聚会的出账索引。我们不想花更多的钱。

现在,我们可以修改Blockchain.MineBlock方法:

func (bc *Blockchain) MineBlock(transactions []*Transaction) {
	...
	newBlock := NewBlock(transactions, lastHash)
	...
}

完整代码:

func (bc *Blockchain) MineBlock(transactions []*Transaction) {
	var lasthash []byte

	// BoltDB的只读事务,读取最后一个区块的hash,用它挖下一个区块的hash
	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)
	}

	// 用提供的交易以及上一个区块的hash构建新的区块
	newBlock := NewBlock(transactions, lasthash)

	// 挖到新区块后进行DB存储并更新“l”键
	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)
		}

		// 更新“l”键
		err = b.Put([]byte("l"), newBlock.Hash)
		if err != nil {
			log.Panic(err)
		}

		bc.tip = newBlock.Hash

		return nil
	})
}

最后,我们实现send命令:

func (cli *CLI) send(from, to string, amount int) {
	bc := NewBlockchain(from)
	defer bc.db.Close()

	tx := NewUTXOTransaction(from, to, amount, bc)
	bc.MineBlock([]*Transaction{tx})
	fmt.Println("Success!")
}

发送币意味着创建一个交易,并且通过挖出一个区块将其添加到区块链上。但是比特币没有像我们实现这些。相反的,它把所有交易存储到内存池(一般叫做矿池)中,当矿工准备好挖出一个矿时,它打包内存池中的所有交易并且产生一个候选区块。交易只有在包含它的区块被挖出来并且添加到区块链之后才被确认。

然我们检查发送币命令的运行:

$ blockchain_go send -from Ivan -to Pedro -amount 6
00000001b56d60f86f72ab2a59fadb197d767b97d4873732be505e0a65cc1e37

Success!

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

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 6

现在,让我们创建更多的交易确定发送在更多的输出程序中可以很好的运行:

$ blockchain_go send -from Pedro -to Helen -amount 2
00000099938725eb2c7730844b3cd40209d46bce2c2af9d87c2b7611fe9d5bdf

Success!

$ blockchain_go send -from Ivan -to Helen -amount 2
000000a2edf94334b1d94f98d22d7e4c973261660397dc7340464f7959a7a9aa

Success!

现在,Helen’s的币被两个出账锁定:一个来自Pedro一个来自Ivan。让我们分别发送它们:

$ blockchain_go send -from Helen -to Rachel -amount 3
000000c58136cffa669e767b8f881d16e2ede3974d71df43058baaf8c069f1a0

Success!

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

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4

$ blockchain_go getbalance -address Helen
Balance of 'Helen': 1

$ blockchain_go getbalance -address Rachel
Balance of 'Rachel': 3

看起来很好!让我们测试一个失败情况:

$ blockchain_go send -from Pedro -to Ivan -amount 5
panic: ERROR: Not enough funds

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4

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

总结

噢!它并不容易,但好歹我们现在有交易了!虽然,一些类似于区块链这种加密货币的特性遗失了:

  1. 地址。我们没有真实的,基于私钥的地址。
  2. 奖励。挖矿是毫无利益可图的。
  3. UTXO集合。需要扫描整个区块获得余额,当区块非常非常多的时候这很花费时间。而且在后面我们要验证交易的话也非常花费时间。UTXO集就是为了解决这些问题并且让交易操作更快捷。
  4. 内存池(矿池)。这是交易在打包到区块之前所要存储的地方。在我们目前的实现中,一个区块只包含一个交易,这是非常低效的。

Links:

  1. Full source codes
  2. Transaction
  3. Merkle tree
  4. Coinbase