客至汲泉烹茶, 抚琴听者知音

区块链的代码实现01—创造、存储、同步、展示、挖矿与工作量证明

原文地址

译者前言

中共中央政治局10月24日下午就区块链技术发展现状和趋势进行第十八次集体学习。中共中央总书记习近平在主持学习时强调,区块链技术的集成应用在新的技术革新和产业变革中起着重要作用。我们要把区块链作为核心技术自主创新的重要突破口,明确主攻方向,加大投入力度,着力攻克一批关键核心技术,加快推动区块链技术和产业创新发展。

于是周一区块链概念股集体涨停……

不管怎么说,这一说法代表了未来政策的一种倾向,说是国策也不为过。我感觉金融会成为区块链应用最为广泛的领域,有必要对它进行深入的了解。光看别人写的介绍也是云里雾里,干脆从代码入手,从原理上学习区块链。

这一篇文章我觉得讲得挺清楚的,不过运行操作过程中也遇到了一些小问题,我已经将其改正过来了,反正在Anaconda 3.6.5环境下是能正常运行的。

这里要提到的一件大事是,像我在这里描述的那样,基本区块链与“专业”区块链之间存在差异。该链不会创建加密货币。区块链不需要生产可以进行交易的货币(比如比特币)。区块链用于存储和验证信息。硬币(coin)可以激励节点(nodes)参与验证,但不是必须的。

我写这篇文章的原因是

1)让阅读本文的人可以了解更多关于区块链的知识;

2)我可以尝试通过解释代码而不是仅仅编写代码来学到更多。

在这篇文章中,我将展示存储区块链数据并生成初始块(initial block)的方式,节点如何与本地区块链数据同步,如何显示区块链(将来将用于同步)以及其他节点),然后介绍如何挖矿(mine)和创建有效的新块。这是第一篇文章,没有其他节点,没有钱包(wallets),没有其他peers,也没有重要的数据。这些概念后续文章会有。

引言

从高层次上讲,区块链是一个数据库,参与该区块链的每个人都可以存储,查看,确认和永不删除数据。

从某种低层次的角度讲,这些块中的数据可以是任何数据,只要是区块链允许的即可。例如,比特币区块链中的数据仅是账户之间的比特币交易。以太坊区块链不仅可以进行以太币的类似交易,还可以用于运行代码的交易。

稍微向下一点,在创建区块并将其链接到区块链之前,区块链上的大多数人(称为节点,nodes)都会对其进行验证。真正的区块链是包含最多数量的,已经被大多数节点正确验证了的块的链。这意味着,如果节点尝试更改上一个块中的数据,则较新的块将无效,并且节点将不信任来自不正确块的数据。

如果这一切令人困惑,请不要担心。我花了一段时间才搞清楚,同时花了更长时间才能够以我的姐姐(在任何区块链领域都没有背景的)理解的方式来编写它。

译者注:我想他姐姐一定有编程基础……如果你觉得一些概念不太懂,不要急,把文章看完,把代码搞懂,自然而然就理解它们的意思了。如果还是不懂,网上搜一下相关解释,有了代码基础也能更容易理解。

如果您想获取源码,请查看 Github上的第1部分分支

第一步:类和文件

第一步是编写一个在节点运行时处理块的类,class Block。坦白说,这个类无足轻重。在__init__ 函数中,我们将相信所有必需的信息都在字典中提供。如果我正在编写区块链,那不是很聪明,但是作为示例来说很不错。我还想编写一种方法,将重要的块信息输出到字典,就可以清晰地在终端显示这些块信息了。

以下代码均写入block.py
mport hashlib
import os
import json
import datetime as date

class Block(object):
    def __init__(self, dictionary):
        #查询索引,时间戳(timestamp),数据,父区块(即上一个区块)哈希值,随机数(nonce)
        for k, v in dictionary.items():
            setattr(self, k, v)  # 设置self的k属性值为v

        if not hasattr(self, 'nonce'):
            self.nonce = 'None'
        if not hasattr(self, 'hash'): #用于创建hash值,hasattr() 函数用于判断对象是否包含对应的属性。
            self.hash = self.create_self_hash()

    def header_string(self): # 上述几个要素组合在一起成了header,它就是要计算hash值的对象。
        return str(self.index) + self.prev_hash + self.data + str(self.timestamp) + str(self.nonce)

    # 计算hash值
    def create_self_hash(self):
        sha = hashlib.sha256()  #hash值算法
        sha.update(self.header_string().encode("utf8"))
        return sha.hexdigest()  #输出加密字符串

    def self_save(self):
        chaindata_dir = 'chaindata'
        # 以几个零作为开头,以便可以按照数字排序
        # zfill() 方法返回指定长度的字符串,原字符串右对齐,前面填充0
        index_string = str(self.index).zfill(6) 
        filename = '%s/%s.json' % (chaindata_dir, index_string)
        with open(filename, 'w') as block_file:
            json.dump(self.__dict__(), block_file)  #json.dumps将一个Python数据结构转换为JSON

    def __dict__(self):
        info = {}
        info['index'] = str(self.index)
        info['timestamp'] = str(self.timestamp)
        info['prev_hash'] = str(self.prev_hash)
        info['hash'] = str(self.hash)
        info['data'] = str(self.data)
        info['nonce'] = str(self.nonce)
        return info

    def __str__(self):
        return "Block<prev_hash: %s,hash: %s>" % (self.prev_hash, self.hash)

我们可以通过以下简单的代码创建第一个块:

import datetime as date
def create_first_block():
    # 索引为0,且随机化前一个区块的hash值
    block_data = {}
    block_data['index'] = 0
    block_data['timestamp'] = date.datetime.now()
    block_data['data'] = 'First block data'
    block_data['prev_hash'] = ''
    block_data['nonce'] = 0 #随机数从0开始
    return Block(block_data)

本部分的最后一个问题是将数据存储在何处。我们希望当我们关闭节点的时候不会丢失本地块数据。

为了稍微复制以太坊浏览器(Etherium Mist)的文件夹方案,我将使用数据“ chaindata”命名该文件夹。现在,每个块将被允许拥有自己的文件,并根据其索引进行命名。我们需要确保文件名以许多零开头,以便块按数字顺序排列。

使用上面的代码,这就是我需要创建的第一个块。

if __name__ == '__main__':
    #检查链数据文件夹是否存在
    chaindata_dir = 'chaindata/'
    if not os.path.exists(chaindata_dir):
        #创建链数据文件夹
        os.mkdir(chaindata_dir)
    #目录下是否有块,若无则创建第一个块
    if os.listdir(chaindata_dir) == []:
        #创建第一个块
        first_block = create_first_block()
        first_block.self_save()     

运行之后,效果如下:

{"index": "0", "timestamp": "2019-10-28 19:37:12.727400", "prev_hash": "", "hash": "2b8889b9ff3f59d179e01fc2d137d89afdd716590589b945e062a0bf67fb2cbd", "data": "First block data", "nonce": "0"}

第二步:在本地同步区块链

启动节点时,必须先同步节点,然后才能开始挖矿,解释数据或发送/创建链的新数据。由于没有其他节点,所以我只是在讨论从本地文件读取块。将来,从文件中读取将是同步的一部分,而且还会连接其他peers,以收集您不运行自己的节点时生成的块。

以下代码均写入sync.py
import os
from block import Block
import json

def sync():
    node_blocks = []
    #我们假定至少初始的块和文件夹存在
    chaindata_dir = 'chaindata'
    if os.path.exists(chaindata_dir):
        for filename in os.listdir(chaindata_dir):
            if filename.endswith('.json'): 
                filepath = '%s/%s' % (chaindata_dir, filename)
                with open(filepath, 'r') as block_file:
                    block_info = json.load(block_file)
                    block_object = Block(block_info) #因为我们可以用一个字典初始化块对象
                    node_blocks.append(block_object)
    return node_blocks

现在从文件夹中读取字符串并将其加载到数据结构中并不需要超级复杂的代码。但是在以后的文章中,当我编写用于不同节点进行通信的sync函数时,该代码将变得更加复杂。

第三步:显示区块链

现在我们已经在内存中拥有了区块链,我希望也能够在浏览器中显示链。有两个原因:首先是在浏览器中确认情况已发生变化。其次是将来还要使用浏览器来查看区块链并在其上采取行动。例如发送交易或管理钱包。

我在这里使用Flask是因为它非常容易上手。

以下代码均写入node.py
from flask import Flask
import sync
import json

node = Flask(__name__)

node_blocks = sync.sync()

@node.route('/blockchain.json', methods=['GET'])
def blockchain():
    # 块信息为索引,时间戳,数据,hash值,父节点hash值
    node_blocks = sync.sync() #如果发生变化则更新
    # 将我们的块转换为字典,以便稍后以json格式输出
    python_blocks = []
    for block in node_blocks:
        python_blocks.append(block.__dict__())
    json_blocks = json.dumps(python_blocks)
    # json.dumps()用于将字典形式的数据转化为字符串
    return json_blocks

if __name__ == '__main__':
    node.run()

尝试运行一下,如果报错:

File "C:\Users\51445\Anaconda3\lib\site-packages\click\utils.py", line 259, in echo
    file.write(message)

UnsupportedOperation: not writable

就到给定的文件C:\Users\51445\Anaconda3\lib\site-packages\click\utils.py下,找到echo函数(大概在166行),把file=None改成file=sys.stdout

然后再打开C:\Users\51445\Anaconda3\lib\site-packages\click\termui.py,找到secho函数(大概在408行),将file=None,改成file=sys.stdout

Reloaded modules: sync, block报错不影响。

运行成功后,在浏览器中输入http://127.0.0.1:5000/blockchain.json,就能显示我们的链信息啦!

第四步:挖矿,也被称为块创建(BLOCK CREATION)

这一部分与第五步的代码都要写入mine.py

我们只有一个初始块,如果我们要存储和分发更多数据,需要一种将其添加到新块中的方法。问题是如何在链接回上一个块的同时创建一个新块。

在比特币白皮书中,中本聪将其描述如下。请注意,“时间戳服务器(timestamp server)”被称为“节点(node)”:

本解决方案首先提出一个“时间戳服务器”。时间戳服务器通过对以区块(block)形式存在的一组数据实施随机散列而加上时间戳,并将该随机散列进行广播,就像在新闻或世界性新闻组网络(Usenet)的发帖一样。显然,该时间戳能够证实特定数据必然于某特定时间是的确存在的,因为只有在该时刻存在了才能获取相应的随机散列值。每个时间戳应当将前一个时间戳纳入其随机散列值中,每一个随后的时间戳都对之前的一个时间戳进行增强(reinforcing),这样就形成了一个链条(Chain)。

下面是一个说明图:

它的大概意思是,为了将这些块链接在一起,一个新块的信息需要包括块创建时间,上一个块的哈希以及该块中的信息,我们将这组信息称为该块的“header”,然后再计算出header的hash值,传递给下一个块。通过这种方式,我们可以通过遍历块之前的所有哈希并验证序列来判断块的真实性。

在此情况下,我正在创建的header是将字符串值加到一个巨大的字符串中。它包含的数据有:

  1. 索引,表示它将是第几个块
  2. 上一个区块的哈希
  3. 数据,在这种情况下只是随机字符串。对于比特币,这称为摩尔根(Merkle root),即有关交易的信息
  4. 我们正在开采的区块的时间戳
def generate_header(index, prev_hash, data, timestamp):
    return str(index) + prev_hash + data + str(timestamp)

创建header并不是必须要将信息字符串加在一起。 只要每个人都知道如何生成块的header,并且header内有前一个块的哈希。这样,每个人都可以确认新块的正确哈希,并验证两个块之间的链接

Bitcoin的header 比单纯地组合字符串复杂得多。它使用数据,时间的哈希值,与字节在计算机内存中的存储方式来计算hash。目前而言,我们只用添加字符串就足够了。

获得header后,我们要遍历并计算经过验证的哈希。在我的哈希计算中,我要做的事情与比特币的方法略有不同,但是我仍然通过sha256函数创建块的header。

def calculate_hash(index, prev_hash, data, timestamp):
    header_string = generate_header(index, prev_hash, data, timestamp)
    sha = hashlib.sha256()
    sha.update(header_string.encode("utf8"))
    return sha.hexdigest()

最后,要挖掘此块,我们使用上面的函数获取新块的哈希,将哈希存储在新块中,然后将该块保存到chaindata目录。

import datetime as date
import sync
node_blocks = sync.sync()

def mine(last_block):
    index = int(last_block.index) + 1
    timestamp = date.datetime.now()
    data = "I block #%s" % (int(last_block.index) + 1) #random string for now, not transactions
    prev_hash = last_block.hash
    block_hash = calculate_hash(index, prev_hash, data, timestamp)

    block_data = {}
    block_data['index'] = int(last_block.index) + 1
    block_data['timestamp'] = date.datetime.now()
    block_data['data'] = "I block #%s" % last_block.index
    block_data['prev_hash'] = last_block.hash
    block_data['hash'] = block_hash
    return Block(block_data)

def save_block(block):
    chaindata_dir = 'chaindata'
    filename = '%s/%s.json' % (chaindata_dir, block.index)
    with open(filename, 'w') as block_file:
        print new_block.__dict__()
        json.dump(block.__dict__(), block_file)

if __name__ == '__main__':
    node_blocks = sync.sync() #gather last node
    prev_block = node_blocks[-1]
    new_block = mine(prev_block)
    new_block.self_save()

通过这种类型的块创建,谁拥有最快的cpu,谁就能够创建一个其他节点认为最长的链。我们需要一些方法来减慢块的创建速度,并在传递到下一个块之前相互确认。

第五步:工作量证明(PROOF-OF-WORK)

为了降低速度,我要像比特币一样应用工作量证明。工作量证明是区块链用于达成共识的另一种方式。

具体操作是调整要求,使块的hash具有某种特定的属性。和比特币一样,我要确保哈希值以一定数量的零开始,然后才能传递给下一个块。这就是header中nonce的作用。

def generate_header(index, prev_hash, data, timestamp, nonce):
    return str(index) + prev_hash + data + str(timestamp) + str(nonce)

现在调整了mine函数创建hash的功能,如果该块的哈希没有以零开头,则我们增加nonce值,创建新的标头,计算新的哈希并检查是否以足够的零开头。

import datetime as date
NUM_ZEROS = 4

def mine(last_block):
    index = int(last_block.index) + 1
    timestamp = date.datetime.now()
    data = "I block #%s" % (int(last_block.index) + 1) #random string for now, not transactions
    prev_hash = last_block.hash
    nonce = 0

    block_hash = calculate_hash(index, prev_hash, data, timestamp, nonce)
    while str(block_hash[0:NUM_ZEROS]) != '0' * NUM_ZEROS:
        nonce += 1
        block_hash = calculate_hash(index, prev_hash, data, timestamp, nonce)
    block_data = {}
    block_data['index'] = int(last_block.index) + 1
    block_data['timestamp'] = date.datetime.now()
    block_data['data'] = "I block #%s" % last_block.index
    block_data['prev_hash'] = last_block.hash
    block_data['hash'] = block_hash
    block_data['nonce'] = nonce
    return Block(block_data)

完美!这个新块包含有效的nonce值,因此其他节点可以验证哈希。我们可以生成,保存此新块并将其分发给其余的块。

四、五步代码整理如下

from block import Block
import datetime as date
import sync
import hashlib

NUM_ZEROS = 5

def generate_header(index, prev_hash, data, timestamp, nonce):
    return str(index) + prev_hash + data + str(timestamp) + str(nonce)

def calculate_hash(index, prev_hash, data, timestamp, nonce):
    header_string = generate_header(index, prev_hash, data, timestamp, nonce)
    sha = hashlib.sha256()
    sha.update(header_string.encode("utf8"))
    return sha.hexdigest()

def mine(last_block):
    index = int(last_block.index) + 1
    timestamp = date.datetime.now()
    data = "I block #%s" % (int(last_block.index) + 1) #random string for now, not transactions
    prev_hash = last_block.hash
    nonce = 0

    block_hash = calculate_hash(index, prev_hash, data, timestamp, nonce)
    while str(block_hash[0:NUM_ZEROS]) != '0' * NUM_ZEROS:
        nonce += 1
        block_hash = calculate_hash(index, prev_hash, data, timestamp, nonce)

    #dictionary to create the new block object.
    block_data = {}
    block_data['index'] = index
    block_data['prev_hash'] = last_block.hash
    block_data['timestamp'] = timestamp
    block_data['data'] = "Gimme %s dollars" % index
    block_data['hash'] = block_hash
    block_data['nonce'] = nonce
    return Block(block_data)


if __name__ == '__main__':
    node_blocks = sync.sync() #gather last node
    prev_block = node_blocks[-1]
    new_block = mine(prev_block)
    new_block.self_save()

按一下F5,会生成000001.json,内容如下:

{"index": "1", "timestamp": "2019-10-28 21:56:48.244912", "prev_hash": "2b8889b9ff3f59d179e01fc2d137d89afdd716590589b945e062a0bf67fb2cbd", "hash": "000005f4f9d3f6729a289c14c29d4d0cda1247c2453101428d492d06b02fb18a", "data": "Gimme 1 dollars", "nonce": "4713178"}

再按一下F5,就会生成000002.json……以此类推。

总结

这个区块链还有很多问题和功能,我没有提到。例如,其他节点如何参与?节点如何传输块中的数据?除了字符串之外,我们如何将信息存储在块中?是否有更好的,不包含巨大数据字符串的header类型?后续文章会解决这个问题。

添加新评论