动手学区块链学习笔记(二):区块链以及工作量证明算法

引言

紧接上文,在介绍完区块链中的加密解密以及公钥私钥等算法后,本篇开始正式进入区块链概念与一个简单区块链系统的实现过程介绍。

区块链技术介绍

什么是区块链?

区块链,就是一个又一个区块组成的链条。每一个区块中保存了一定的信息,它们按照各自产生的时间顺序连接成链条。这个链条被保存在所有的服务器中,只要整个系统中有一台服务器可以工作,整条区块链就是安全的。这些服务器在区块链系统中被称为节点,它们为整个区块链系统提供存储空间和算力支持。如果要修改区块链中的信息,必须征得半数以上节点的同意并修改所有节点中的信息,而这些节点通常掌握在不同的主体手中,因此篡改区块链中的信息是一件极其困难的事。相比于传统的网络,区块链具有两大核心特点:一是数据难以篡改、二是去中心化。基于这两个特点,区块链所记录的信息更加真实可靠,可以帮助解决人们互不信任的问题。
来源:百度百科

总结一句话就是,区块链本质上是一个去中心化的数据库。而它能提取以下几个特点:

  1. 信息不可修改,添加到区块链中的信息无法再被修改;
  2. 只支持「增」和「查」操作,不同于传统的数据库技术支持「增、删、查、改」,区块链技术只支持「增」操作(即往区块链里的添加区块信息),和「查」操作(即查询区块链里的区块信息);而不支持「删」和「改」操作;
  3. 没有权限限制,任何加入区块链网络的节点都有权限「增」和「查」区块信息。

更加通俗的理解,可以查看新华网在2019年发布的刷屏的区块链究竟是什么?你想知道的都在这里!

这里直接讲区块链的区块结构,见下表为:

数据项 字节 字段 说明
Magic NO 4 魔数 常数0xD9B4BEF9
Blocksize 4 区块大小 用字节表示的该字段之后的区块大小
Blockheader 80 区块头 组成区块头的几个字段,描述区块的元数据
Transaction counter 1-9 交易计数器 该区块包含的交易数量,包含coinbase交易,描述紧跟在该域后面的交易数据的数目
Transactions 不定 交易 记录在区块里的交易信息,使用原生的交易信息格式,并且交易在数据流中的位置必须与Merkle树的叶子节点顺序一致

其中Blocksize和Magic NO都是描述区块链的一个量词,余下的三个概念就构成了区块链:

其中区块头的结构为:

大小 域名 描述
4 字节 Version 版本号,升级了软件并指定了新版本
32 字节 Previous Block Hash 与本区块形成链的前一区块的散列值,更准确的讲是前一区块的区块头的散列值
32 字节 Merkle Root Hash 这个域的准确意义解释起来稍微有点复杂,暂时可以理解为区块体的散列值
4 字节 Timestamp 可以简单理解为该区块被创建时的 Unix 时间戳(即从 UTC 1970 年 1 月 1 日 0 时 0 分 0 秒起至现在的总秒数)
4 字节 Difficulty Target 难度调整的一个目标值,后续工作量介绍
4 字节 Nonce 为了找到满足难度目标所设定的随机数,为了解决32位随机数在算力飞升的情况下不够用的问题,规定时间戳和coinbase交易信息均可更改,以此扩展nonce的位数

在了解比特币系统中的区块头结构中 Previous Block HashMerkle Root Hash 这两个域的大致意义后,区块如何形成区块链的具体细节便清晰了。

区块链的结构有以下两个主要特点:

  • 区块链中的每个区块都通过 Previous Block Hash 记录了前一区块的区块头的散列值;

  • 区块链中的每个区块的区块头都通过 Merkle Root Hash 记录了该区块的区块体的散列值。

在这里插入图片描述

这种结构和数据结构中的链表是非常相似的,见下图所示:

在链表中,初始结点为头结点,而区块链中,初始的块则叫做创世区块(Genesis Block),链表的头结点指向的为Null,因为它本身就是head,创世块同样,它没有前置区块,所以它的Previous Block Hash指向的值便为空。

实现区块类与区块链类

区块类

基于上一节对于区块链结构的分析,下面我们可以使用 Python 中的 Block 类来实现我们第一个版本的区块链的区块了。Block 类包含以下几个域:

字段 解释
Timestamp 当前时间戳,也就是区块创建的时间
PrevBlockHash 前一个块的哈希,即父哈希
Hash 当前块的哈希
Data 区块存储的实际有效信息,也就是交易

用代码表示为:

class Block(object):
    def __init__(self, data, prev_block_hash=''):
        self.timestamp = int(time.time())
        self.prev_block_hash = prev_block_hash
        self.data = data
        self.data_hash = hashlib.sha256(data.encode('utf-8')).hexdigest()

代码中prev_block_hash=''相当于在申明类时,就自动创建了一个创世区块,并且它指向了在python中代表空的一种字符,使用hashlib中的sha256来计算区块体和区块头的散列值。

目前,我们仅取了 Block 结构的部分字段(Timestamp, DataPrevBlockHash),并将它们相互拼接起来,然后在拼接后的结果上计算一个 SHA-256,就得到了一个新的哈希值,这与比特币的实现方式也是一致的:

class Block(object):
    ......
    def hash(self):
        data_list = [str(self.timestamp), self.prev_block_hash, self.data_hash]
        return hashlib.sha256(''.join(data_list).encode('utf-8')).hexdigest()
    ......

为了能够以更好的可读性的打印出 Block 类,我们给它加上 __repr__ 方法:

class Block(object):
    ......
    def __repr__(self):
        s = 'Block(Hash={}, TimeStamp={}, PrevBlockHash={}, Data={}, DataHash={})'
        return s.format(self.hash(), self.timestamp, self.prev_block_hash,
                self.data, self.data_hash)

至此,一个区块类就完成了,可以将其理解为一种数据结构,跟单链表相似,但是功能与携带信息比单链表更多。

区块链类

如果说区块类为单节点行为,而区块链类相当于赋予了该类与其它节点通信的权利,区块链类为实现区块链整个系统的主要的一个类,所以这里直接给出最简版本为:

from block import Block
from datetime import datetime


class BlockChain(object):
    def __init__(self):
        print('Created a new blockchain.\n')
        self.chain = []
        genesis_block = Block('This is a genesis block')
        self.chain.append(genesis_block)

    def add_block(self, data):
        new_block = Block(data, self.chain[-1].hash())
        self.chain.append(new_block)

    def print_chain(self):
        for block in self.chain:
            print('Block Hash: {}'.format(block.hash()))
            print('Prev Block Hash: {}'.format(block.prev_block_hash))
            print('TimeStamp: {}'.format(datetime.fromtimestamp(block.timestamp)))
            print('Data: {}'.format(block.data))
            print('')

至此,我们创建一个main函数直接去调用上述代码为:

from block import Block
from blockchain import BlockChain


if __name__ == '__main__':
    # 创建区块链
    block_chain = BlockChain()

    # 添加第一个区块到区块链中
    block_chain.add_block('Send 1 BTC to Ivan')
    # 添加第二个区块到区块链中
    block_chain.add_block('Send 2 more BTC to Ivan')

    # 打印整个区块链的信息
    block_chain.print_chain()

输出为:
在这里插入图片描述

这里初步实现了区块链的功能,但为什么还需要使用GPU以及分布式等更多设备呢?那就是下面要讲的矿工核心算法。

工作量证明算法

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

但是,依照上述代码实现的区块链有一个巨大的缺陷:向链中加入区块太容易,也太廉价了。区块链的一个关键点就是,一个人必须经过一系列困难的工作,才能将数据放入到区块链中。正是由于这种困难的工作,才保证了区块链的安全和一致。此外,完成这个工作的人,也会获得相应奖励(这也就是通过挖矿获得币)。

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

整个 “努力工作并进行证明” 的机制,就叫做工作量证明(proof-of-work)。

工作量证明流程

工作量证明的核心思想就是给区块链添加新区块的这一操作设置一定的难度。谁都有权限往区块链添加新区块,但并不是谁都有能力这么做,而必须付出一定的代价,这个代价就是工作量证明。

怎么给「区块链添加新区块」的这一操作设置难度呢?其实也很简单,就是规定新区块的区块头的散列值必须满足某种特征。由于散列值具有「单向性」和「雪崩效应」,因此计算机只能通过穷举计算的方法来获得具备某种特征的散列值,而工作量证明算法也就是穷举计算散列值的过程:

这个穷举计算散列值的过程在比特币系统中也被称为挖矿。比特币的实现要求的新区块的区块头散列值特征是散列值必须是一定数目的前导 0前导 0 的数目被称为挖矿的难度,该数值越大表示挖矿的难度也就越大(即要找到能够加入比特币区块链的新区块的时间就越长)。比特币区块头结构中,有两个域与挖矿相关:

大小 域名 描述
4 字节 Difficulty Target 表示当前挖矿的难度,但该值不是直接表示前导 0 的数目,而是一个通过特殊编码处理的值,计算方法见如下图
4 字节 Nonce 为了找到满足难度目标所设定的随机数,为了解决32位随机数在算力飞升的情况下不够用的问题,规定时间戳和coinbase交易信息均可更改,以此扩展nonce的位数

关于Difficulty Target,表格里已经解释得很清楚,需要注意的是,这里的nonce是区块中的nonce,主要是调整挖矿难度;还有一种是每笔交易中nonce,每个外部账户(私钥控制的账户)都有一个nonce值,从0开始连续累加,每累加一次,代表一笔交易。关于后者,会之后介绍。

所以我们可以更新区块类中的哈希方法,将Nonce加入列表:

import time
import hashlib


class Block(object):
    def __init__(self, data, prev_block_hash=''):
        self.timestamp = int(time.time())
        self.prev_block_hash = prev_block_hash
        self.data = data
        self.nonce = 0 # 添加 nonce 成员,初始值为 0
        self.data_hash = hashlib.sha256(data.encode('utf-8')).hexdigest()

    def hash(self): # 计算散列值时,同样加入 nonce值
        data_list = [str(self.nonce),str(self.timestamp),
                    self.prev_block_hash, self.data_hash]
        return hashlib.sha256(''.join(data_list).encode('utf-8')).hexdigest()

    def __repr__(self): # 打印输出,同样加入 nonce值
        return 'Block(Hash={
    
    }, TimeStamp={
    
    }, PrevBlockHash={
    
    }, Nonce={
    
    }, \
        Data={
    
    }, DataHash={
    
    })'.format(self.hash(), self.timestamp,
        self.prev_block_hash, self.nonce, self.data, self.data_hash)

然后定义工作量的noncedifficulty_bits

class ProofOfWork(object):
    MAX_NONCE = sys.maxsize

    def __init__(self, difficulty_bits=12):
        self._target = 1 << (256-difficulty_bits)

这里的MAX_NONCE是用于限定在求解 nonce 值时的上限,sys.maxsize与操作系统有关,如果是32位的操作系统,那么该值将为2^31-1,即2147483647。如果是64位, 将为2^63-1,即9223372036854775807

difficulty_bits的计算规则如下图:
在这里插入图片描述
可能看到左边的1 << (256 - n),就知道这是移位的符号,学过计算机组成原理的,还能说出具体的操作,为算术左移:依次左移n位,尾部补0,最高的符号位保持不变。
在这里插入图片描述

这就是工作量类的核心,也是证明是否付出劳力的两道质检工序,代码为:

    def mine_block(self, data, prev_hash=''):
        tmp_block = Block(data, prev_hash)
        while tmp_block.nonce<ProofOfWork.MAX_NONCE:
            hash_int = int(tmp_block.hash(), 16)	# 16进制
            if hash_int < self._target:	# 区块头的散列值满足要求,小于target,算法完成
                break
            else :	# 区块头的散列值不满足要求,nonce加1,继续循环
               tmp_block.nonce+=1
        return tmp_block

这里还可以加一个验证功能,在后续将挖到的矿持久化以及上传数据库后,该值可随时进行验证,这里简单写为:

def validate_block(self, block):
        hash_int = int(block.hash(), 16)
        return True if hash_int<self._target else False

有了工作量证明我们的 Blockchain 类,在生成新区块的时候就都需要通过 proofOfWork 类的 mine_block 方法来生成区块了,另外为了更易于理解我们相应的将 Blockchain 类的 add_block 方法也更名为 mine_block 方法,在 print_chain 方法中我们加入了对区块的工作量证明校验结果,因此修改后Blockchain 类的完整代码如下:

from block import Block
from datetime import datetime
from proofofwork import ProofOfWork


class BlockChain(object):
    def __init__(self):
        print('Created a new blockchain.\n')
        self.chain = []
        self.pow = ProofOfWork()  # 创建工作量证明类
        genesis_block = self.pow.mine_block('This is a genesis block') # 通过工作量证明类来挖出创世区块
        self.chain.append(genesis_block)

    # 挖出一个数据为data的区块
    def mine_block(self, data):
        new_block = self.pow.mine_block(data, self.chain[-1].hash())
        self.chain.append(new_block)

    def print_chain(self):
        for block in self.chain:
            print('Block Hash: {}'.format(block.hash()))
            print('Prev Block Hash: {}'.format(block.prev_block_hash))
            print('TimeStamp: {}'.format(datetime.fromtimestamp(block.timestamp)))
            print('Nonce: {}'.format(block.nonce)) # 增加打印 nonce值
            print('Data: {}'.format(block.data))
            print('POW: {}'.format(self.pow.validate_block(block))) # 校验区块
            print('')

我们更改main.py运行为:

from block import Block
from blockchain import BlockChain


if __name__ == '__main__':
    # 创建区块链
    block_chain = BlockChain()

    # 挖出第一个区块
    block_chain.mine_block('Send 1 BTC to Ivan')
    # 挖出第二个区块
    block_chain.mine_block('Send 2 more BTC to Ivan')

    # 打印整个区块链的信息
    block_chain.print_chain()

在这里插入图片描述

这里以每4位为首位添一位0,因为默认的difficulty_bits为12,可以看到截图中,有3位的16进制0开头,换算为12位的二进制0,而每向上添加4位,都将增加其计算量,比如说将其改为24,增大两倍,我们使用Linux默认计算时间工具测试:

time python3 main.py

在这里插入图片描述
很明显,对比上一个,时间慢了一大截,因为现在还是单线程,看单核情况发现,直接将我这个学生机的CPU的1核跑满了:

在这里插入图片描述
所以,这也是为什么在比特币大火的20年到22年间,各种云服务器被植入挖矿程序,导致每次上去查看,要不就显卡跑满,要不就CPU整个也全部跑满,原因就是太密集的哈希计算,让日常使用都不会触顶的服务器,第一次有了算不过来的感觉,emmm。。。

当然,之后的交易以及网络等篇幅还会对工作量算法进行修改,比如说预防作弊等,具体的可以见如下图,但也不会做得那么完全,那么至此,本篇结束。

猜你喜欢

转载自blog.csdn.net/submarineas/article/details/128647550