Skip to content

使用 go 实现一个简单的区块链

介绍

本质上来说,区块链就是一个分布式的数据库。它的独特之处在于,它不是一个私有的数据库,而是一个公共的数据库,每个人都可以使用它,也可以复制它。但是如果要添加数据,需要每个数据库的使用者或者说维护者都同意,才能往里添加数据。

Block

区块链是两个词的组合,一个是区块,一个链。在区块链中,块是用于存储有价值的信息的。例如: BTC 的块记录交易,这是任何加密货币的本质。除此之外,一个块包含一些技术信息,如其版本,当前时间戳和上一个块的哈希值。
这是一个最简单的 Block:

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

Timestamp 当该块被创建时的时间戳
Data 记录块中实际的信息
PrevBlockHash 记录前一个块的 hash 值
Hash 该块的 hash 值

我们这里的 Timestamp, PrevBlockHash, Hash 在比特币的技术规范中属于区块头部(block header),头部是一个单独的数据结构。完整的比特币区块头部(block header)如下:

FieldPurposeUpdated when...Size (Bytes)
VersionBlock version numberYou upgrade the software and it specifies a new version4
hashPrevBlock256-bit hash of the previous block headerA new block comes in32
hashMerkleRoot256-bit hash based on all of the transactions in the blockA transaction is accepted32
TimeCurrent timestamp as seconds since 1970-01-01T00:00 UTCEvery few seconds4
BitsCurrent target in compact formatThe difficulty is adjusted4
Nonce32-bit number (starts at 0)A hash is tried (increments)4

如果用 go 实现 BTC header 的规范

go
type BlockHeader struct {
	// Version of the block.
	Version int32

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

	// Merkle tree reference to hash of all transactions for the block.
	MarkleRoot 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
}

我们还需要计算 hash 。这个是用来保证区块链安全的。这是一个架构上有意为之的设计,它故意使得加入新的区块十分困难,继而保证区块一旦被加入以后,就很难再进行修改。
使用 Block 里面的部分字段(PrevBlockHash, Data, timestamp),将他们连接,然后计算 SHA-256 就得到了 Hash
编写一个 SetHash() 函数,用来完成这些操作

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

	b.Hash = hash[:]
}

实现一个用于简化创建 Block 的函数 NewBlock

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

	return block
}

Blockchain

现在有了区块,要让区块形成。实际上,区块链就是一个有着特定结构的数据库,每个区块的最前面都放着前一个区块,像链表一样,顺序相连。这种结构,可以快速获取到最新的区块。
定义一个区块链的结构体

go
type Blockchain struct {
	Blocks []*Block
}

实际上就是一个 Block 数组。
再写一个添加区块的函数 AddBlock

go
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 blcok) 。

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

有了创世块,也需要为创世块创建链

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

写一个 main 函数来测试

go
func main() {
	bc := blockchain.NewBlockchain()

	bc.AddBlock("Send 1")
	bc.AddBlock("Send 2")

	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