分享

基于Docker容器百行代码实现自己的分布式区块链【python版】

本帖最后由 regan 于 2018-4-16 11:52 编辑

1.区块链技术最近非常的火,到处都在热议区块链,去中心化,挖矿等等各种概念层出不穷。各大公司也在大力部署区块链。也许你跟我一样刚听别人说区块链的时候,只是懵懵懂懂的只是知道个概念,但作为一名技术人员,用手里的技术是实现一个自己的区块链,不仅可以深入了解区块链的运行机制,还有一个重要的原因是乐在其中,因为马上自己就要用Python代码实现自己的区块链了!

2.确保安装了pip Flask request库
pip install Flask==0.12.2 requests==2.18.4
1.png
3.接下来,我们要考虑如何去实现区块链了,区块链需要得到记录并且需要记录交易的信息。因此需要使用两个列表来保存这些节点信息。当然要让新的区块加入进来,需要实现一个new_block的添加方法,添加的新的区块中需要记录交易信息,因此还要实现一个new_transaction方法用于将交易信息添加到交易列表中。每一个区块都是不重复且唯一的,因此需要为每个区块定义一个唯一的标志,这里借助Hash模块计算Hash值,定义一个hash方法,当然也可以使用别的算法为每个区块生成一个唯一的值。当我们添加新的区块的时候,需要把区块添加到区块链的最后,因此要实现一个方法,这个方法用于返回区块链最后的记录,这个方法取名叫last_block.这里先定义出BlockChain类的框架
class Blockchain(object):
    def __init__(self):
        #定义两个列表,用于记录区块链及交易信息
        self.chain = []
        self.current_transactions = []

    def new_block(self):
        # 创建一个新的Block区块,并添加到区块链中
        pass

    def new_transaction(self):
        # 在交易列表中添加一个交易信息
        pass

    @staticmethod
    def hash(block):
        # 通过Hash算法返回区块的Hash值
        pass

    @property
    def last_block(self):
        # 返回区块链中最后一个区块
        pass

4.接下来我们考虑下区块的结构,区块链最主要的作用是去中心化,并且要记录区块与前面区块的关联,交易及交易发生的时间,因此Block这个数据结构至少要有timestamp、transactions、工作量证明【也就是所说的通过挖矿【劳动】来获取的一个值,这个地方只要是通过一个算法,用计算机的算力去碰撞这个值,最先算出这个值的将会得到挖矿的奖励】、以及形成链条的用于记录前一个区块的previous_hash,这样在整个链条上只要有一个Block被攻击破坏了,后面所有区块的Hash都是错误的,这保证了区块链的不变性。当然每个区块还得有自己的索引编号,表示是第几个Block,transactions里要记录交易双方的信息及交易的数量。Block数据结构如下:
block = {
    'index': 1,
    'timestamp': 1506057125.900785,
    'transactions': [
        {
            'sender': "8527147fe1f5426f9dd545de4b27ee00",
            'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f",
            'amount': 5,
        }
    ],
    'proof': 324984774000,
    'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}

5.理解了上面的基本的概念,那我们接下来实现BlockChain中的new_transaction方法,一个交易的完成少不了交易双方及交易的量这几个重要的值。因此这几个值做成参数传递给new_transaction方法
def new_transaction(self, sender, recipient, amount):
        """
        生成新交易信息,信息将加入到下一个待挖的区块中
        :param sender: <str> 发送者的地址
        :param recipient: <str> 接受者的地址
        :param amount: <int> 交易额度
        :return: <int> 返回新的Block的Id值,新产生的交易将会被记录在新的Block中
        """
        #实现很简单,向交易列表中添加一个字典,这个字典中记录交易双发的信息
        self.current_transactions.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount,
        })
        #返回最后一个区块的index加上1,即对应到新的区块上
        return self.last_block['index'] + 1

6.接下来我们完善创建区块的方法,想一下在区块链“创世之初”还没有任何区块的时候,这个时候区块链刚刚被创建,因此我们需要在创建区块链的时候,指定一个创世块,并给它添加一个工作量证明。先来理一下创建新块需要做哪些事情,需要记录前面一个区块的hash地址吧!需要记录工作量的证明吧,没有工作量,谁给你发工资,类似于公司里的考核。接下来我们就来实现new_block方法

def new_block(self, proof, previous_hash=None):
        """
        生成新块
        :param proof: <int> 工作量证明,它是一个工作算法对应的一个值
        :param previous_hash: (Optional) <str> 前一个区块的hash值
        :return: <dict> 返回一个新的块,这个块block是一个字典结构
        """

        block = {
             #新block对应的index
            'index': len(self.chain) + 1,
             #时间戳,记录区块创建的时间
            'timestamp': time(),
             #记录当前的交易记录,即通过new_transactions创建的交易,记录在这个新的block里
            'transactions': self.current_transactions,
            #工作量证明
            'proof': proof,
             #前一个block对应的hash值
            'previous_hash': previous_hash or self.hash(self.chain[-1]),
        }

        # 重置当前的交易,用于记录下一次交易
        self.current_transactions = []
        #将新生成的block添加到block列表中
        self.chain.append(block)
        #返回新创建的blcok
        return block

7.前面说了在区块链创建之初,需要先创建创世块,这个创世块应该在哪个位置被创建呢?没错,就是和区块链一起创建,自然是在__init__方法中啦。接下来我们看下__init__方法。
def __init__(self):
        self.current_transactions = []
        self.chain = []

        # 创建“创世块”
        self.new_block(previous_hash=1, proof=100)

8.在new_transaction中调用了last_block这个方法,其实这个方法很简单,返回区块链最后一个区块。
    @property
    def last_block(self):
        return self.chain[-1]

9.还有一个方法没有实现,那就是hash方法,在这个方法内部需要为当前的block计算得出一个hash值,最简单的方法就是使用“摘要函数”,python工具箱中已经实现了很多摘要函数例如md5,sha256等。
    @staticmethod
    def hash(block):
        """
        生成块的 SHA-256 hash值
        :param block: <dict> Block
        :return: <str>
        """

        # 首先将block字典结构转换成json字符串,通过sort_keys指定按key拍好序。
        block_string = json.dumps(block, sort_keys=True).encode()
        #调用sha256函数求取摘要
        return hashlib.sha256(block_string).hexdigest()


10.上面实现了区块链的基本结构及方法,那区块是如何挖出来的呢?如何实现挖矿呢?还记得上面定义的proof工作量证明这个变量吗?它是如何计算出来的呢?它和挖矿又有何联系呢?
工作量实际上是需要使用工作量证明方法PoW来构造!它的目标就是找出符合特定条件的数字,而这个数字往往很难计算出来,因此要计算它需要耗费大量的算力,即工作量!这个数据虽然难计算,很很容易验证的。
为了便于理解,假设有两个数字x,y.这里定义一个规则x乘以y的hash值必须以'a'结尾,设x变量为math.e,求y?
from hashlib import sha256
import math
x = math.e
y = 0  # y未知
while sha256(f'{x*y}'.encode()).hexdigest()[-1] != "a":
    y += math.pi
print(f'The solution is y = {y}')
2.png
最后打印出一个结果,这个结果。
在比特币中,使用称为Hashcash的工作量证明算法,它和上面的问题很类似。矿工们为了争夺创建区块的权利而争相计算结果。通常,计算难度与目标字符串需要满足的特定字符的数量成正比,矿工算出结果后,会获得比特币奖励。
证明很困难,相反验证就很容易了,
x = math.e
y = 9.42477796076938
sha256(f'{x*y}'.encode()).hexdigest()
3.png
11.理解了工作量,接下来我们就来实现一个工作量证明的PoW算法,规则为:寻找一个数p,使得他和前面一个区块的proof拼接成的字符串的Hash值以‘abc'符开头。定义两个方法proff_of_work和valid_proff方法。

def proof_of_work(self, last_proof):
        """
        简单的工作量证明:
         - 查找一个数 p 使得 hash(p+last_proof) 以'abc'开头
         - last_proof 是上一个块的证明,  p是当前的证明
        :param last_proof: <int>
        :return: <int>
        """

        proof = 0
        #定义一个死循环,直到valid_proof验证通过
        while self.valid_proof(last_proof, proof) is False:
            proof += math.pi

        return proof

    @staticmethod
    def valid_proof(last_proof, proof):
        """
        验证证明: 是否hash(last_proof, proof)以'abc'开头?
        :param last_proof: <int> 前一个证明
        :param proof: <int> 当前证明
        :return: <bool>
        """

        guess = f'{last_proof}{proof}'.encode()
        guess_hash = hashlib.sha256(guess).hexdigest()
        return guess_hash[:3] == "abc"

PoW工作量的证明算法和验证算法就写完了,是不是很简单!当然对于算法的复杂度来说,字符串的个数越长,验证的复杂度就越高,例如验证以’abcd‘四个字符开头,每增加以为都将大大增加验证所需要的时间。

12.BlockChain类已经基本完成,是不是非常简单呢?接下来,我们需要制作一个Restful接口,以供区块链在网络上调用。
我们将使用Python Flask框架,这是一个轻量Web应用框架,它方便将网络请求映射到 Python函数,现在我们来让Blockchain运行在基于Flask web上。
我们将创建三个接口:
  • /transactions/new 创建一个交易并添加到区块
  • /mine 告诉服务器去挖掘新的区块
  • /chain 返回整个区块链

Flask服务器将会扮演一个“节点”,先来书写Flask的整体框架,而后再实现。
import hashlib
import json
from textwrap import dedent
from time import time
from uuid import uuid4
from flask import Flask

#实例化一个Flask节点
app = Flask(__name__)

# 为当前节点生成一个全局唯一的地址,使用uuid4方法
node_identifier = str(uuid4()).replace('-', '')

#初始化区块链
blockchain = Blockchain()

# 告诉服务器去挖掘新的区块
@app.route('/mine', methods=['GET'])
def mine():
    return "We'll mine a new Block"
# 创建一个交易并添加到区块,POST接口可以给接口发送交易数据
@app.route('/transactions/new', methods=['POST'])
def new_transaction():
    return "We'll add a new transaction"
#返回整个区块链,GET接口
@app.route('/chain', methods=['GET'])
def full_chain():
    response = {
        'chain': blockchain.chain,
        'length': len(blockchain.chain),
    }
    return jsonify(response), 200

if __name__ == '__main__':
    #服务器运行在5000端口上
    app.run(host='0.0.0.0', port=5000)

13.有了整体的框架,那我们来实现吧!
首先来实现发送交易的方法new_transaction方法,发送交易的数据格式如下:
{
"sender": "sender address",
"recipient": "other address",
"amount": 10
}

@app.route('/transactions/new', methods=['POST'])
def new_transaction():
    #获取请求的参数,得到参数的json格式数据
    values = request.get_json()
    print('request parameters:%s'%(values))
    #检查请求的参数是否合法,包含sender,recipient,amount几个字段
    required = ['sender', 'recipient', 'amount']
    if not all(k in values for k in required):
        return 'Missing values', 400

    # 使用blockchain的new_transaction方法创建新的交易
    index = blockchain.new_transaction(values['sender'], values['recipient'], values['amount'])
    #构建response信息
    response = {'message': f'Transaction will be added to Block {index}'}
    #返回响应信息
    return jsonify(response), 201

13.接下来就实现最“神奇”的地方——挖矿!挖矿要做三件事情。
  • 计算工作量证明PoW
  • 通过新增一个交易授予矿工(自己)一个币
  • 构造新区块并将其添加到链中

@app.route('/mine', methods=['GET'])
def mine():
    #获取区块链最后一个block
    last_block = blockchain.last_block
    #取出最后一个block的proof工作量证明
    last_proof = last_block['proof']
     # 运行工作量的证明和验证算法,得到proof。
    proof = blockchain.proof_of_work(last_proof)

    # 给工作量证明的节点提供奖励.
    # 发送者为 "0" 表明是新挖出的币
    # 接收者是我们自己的节点,即上面生成的node_identifier。实际中这个值可以用用户的账号。
    blockchain.new_transaction(
        sender="0",
        recipient=node_identifier,
        amount=1,
    )

    # 产生一个新的区块,并添加到区块链中
    block = blockchain.new_block(proof)
    #构造返回响应信息
    response = {
        'message': "New Block Forged",
        'index': block['index'],
        'transactions': block['transactions'],
        'proof': block['proof'],
        'previous_hash': block['previous_hash'],
    }
    return jsonify(response), 200

14.至此,全部代码就开发完成了!接下来运行区块链程序吧!
运行 python Blockchain.py
在浏览器中输入localhost:5000/mine进行挖矿
4.png
请求chain获取整个区块链
localhost:5000/chain
5.png
可以看到整个区块链的数据了。

使用curl 命令模拟post请求转账
curl -X POST -H "Content-Type: application/json" -d '{
"sender": "3acfc29432d6463187def555ea24c856",
"recipient": "mymymyacount",
"amount": 5
}' "http://localhost:5000/transactions/new"
6.png
7.png

15.惊不惊喜,刺不刺激!接下来我们玩点更刺激的,将我们的区块链分布式化!借助Docker容器技术,快速实现分布式的区块链。现在要想,既然是分布式的,那我们怎样来保证所有的节点拥有相同的链喃?这是一个一致性的问题,要实现分布式的多节点,必须实现一致性算法!

在实现一致性算法之前,我们需要找到一种方式让一个节点知道它相邻的节点。每个节点都需要保存一份包含网络中其它节点的记录。因此让我们新增几个接口:
  • /nodes/register 接收URL形式的新节点列表
  • /nodes/resolve 执行一致性算法,解决任何冲突,确保节点拥有正确的链
接下来,我们修改单节点版本的算法,在init方法中增加一个set集合,用于记录网络中所有的节点。set集合可以避免重复添加。
def __init__(self):
        self.current_transactions = []
        self.chain = []
        self.nodes = set()
        # 创建“创世块”
        self.new_block(previous_hash=1, proof=100)

接下来我们要实现一个注册方法,让每个节点都知道它相邻的节点,把相邻节点的信息注册到自己上,实际上就是记录下相邻节点的url地址到set集合中。
def register_node(self, address):
        """
        增加一个新的网络节点到set集合
        :param address: <str>网络地址 'http://172.16.0.50:5000'
        :return: None
        """
        parsed_url = urlparse(address)
        self.nodes.add(parsed_url.netloc)

16.接下来实现一致性共识算法。前面提到,冲突是指不同的节点拥有不同的链,为了解决这个问题,规定最长的、有效的链才是最终的链,换句话说,网络中有效最长链才是实际的链。基于这个假设,我们实现一致性算法。这个算法要做两件事
  • 检验链是否有效
  • 找到网络中链最长且有效的链,若最长的链不是自己的,则替换掉自己的链。
实现两个方法valid_chain和resove_confilicts方法,如下
def valid_chain(self, chain):
        """
         检查给定的链是否是有效的
        :param chain: <list> 区块链
        :return: <bool>
        """
        last_block = chain[0]
        current_index = 1
        while current_index < len(chain):
            block = chain[current_index]
            print(f'{last_block}')
            print(f'{block}')
            # 检验当前block的previous_hash值和前面block的hash值是否相等
            if block['previous_hash'] != self.hash(last_block):
                return False
            # 验证前面block的工作量证明和当前block的工作量证明拼接起来的字符串的hash是否以'abc'为开头
            if not self.valid_proof(last_block['proof'], block['proof']):
                return False
            last_block = block
            current_index += 1
        #验证通过,返回True
        return True
    def resolve_conflicts(self):
        """
        共识算法解决冲突
        使用网络中最长的链.
        :return: <bool> True 如果链被取代, 否则为False
        """
        #所有的邻居节点
        neighbours = self.nodes
        new_chain = None
        # 在所有邻居中查找比自己链更长的
        max_length = len(self.chain)
        # 遍历并且验证邻居链的有效性
        for node in neighbours:
            response = requests.get(f'http://{node}/chain')
            if response.status_code == 200:
                length = response.json()['length']
                chain = response.json()['chain']
                # 检查链是否更长,且有效。更新new_chain,并更新max_length。
                if length > max_length and self.valid_chain(chain):
                    max_length = length
                    new_chain = chain
        # 如果new_chain是有定义的,则说明在邻居中找到了链更长的,用new_chain替换掉自己的链
        if new_chain:
            self.chain = new_chain
            return True
        return False

17.核心的方法定义好了,别急!还需要定义两个Restful接口,来接收注册和一致性。
@app.route('/nodes/register', methods=['POST'])
def register_nodes():
    values = request.get_json()
    nodes = values.get('nodes')
    if nodes is None:
        return "Error: Please supply a valid list of nodes", 400
    #注册节点到blockchain中
    for node in nodes:
        blockchain.register_node(node)
     #构造一个响应
    response = {
        'message': 'New nodes have been added',
        'total_nodes': list(blockchain.nodes),
    }
  #201:提示知道新文件的URL
    return jsonify(response), 201

#解决一致性问题的API
@app.route('/nodes/resolve', methods=['GET'])
def consensus():
   #调用resolve_conficts()方法,让网络中的chain协调一致
    replaced = blockchain.resolve_conflicts()
    #如果当前节点的chain被替换掉,返回替换掉的信息;否则返回当前节点的chain是有权威的!
    if replaced:
        response = {
            'message': 'Our chain was replaced',
            'new_chain': blockchain.chain
        }
    else:
        response = {
            'message': 'Our chain is authoritative',
            'chain': blockchain.chain
        }
    return jsonify(response), 200

18.接下来编写程序的入口
if __name__ == '__main__':
    from argparse import ArgumentParser
    parser = ArgumentParser()
    parser.add_argument('-p', '--port', default=5000, type=int, help='port to listen on')
    args = parser.parse_args()
    port = args.port
    app.run(host='127.0.0.1', port=port)

19.进入到BlockChain_distribution.py所在目录,运行
a.     python BlockChain_distribution.py
8.png b.   python BlockChain_distribution.py --port 5001
9.png

c.启动另一个console窗口,使用curl命令模拟请求,/nodes/register注册。
curl -X POST -H "Content-Type: application/json" -d '{
"nodes": ["http://127.0.0.1:5001"]
}' "http://127.0.0.1:5000/nodes/register"
10.png


curl -X POST -H "Content-Type: application/json" -d '{
"nodes": ["http://127.0.0.1:5000"]
}' "http://127.0.0.1:5001/nodes/register"


11.png


在端口为5001的上面挖矿。运行:localhost:5001/mine


12.png


查看blockchain


localhost:5001/chain


13.png


然后在端口为5000的机器上运行/nodes/resolve接口,解决一致性问题


localhost:5000/nodes/resolve
14.png


看到同步成功!再在5000上运行localhost:5000/chain


15.png


可以看到两个网络节点的blockchain得到了同步!分布式的区块链开发完成!,实际上这里可以把区块部署在不同的网络节点,启动不同的端口,只要一个网络节点知道其他的网络节点就可以,因为他在内部可以通过请求邻居网络节点来获取区块链的最新情况,从而做一致性决策。

20.为了便于大家一键部署把玩,这里带大家把刚才我们开发的分布式的区块链部署到Docker容器中。在linux服务器上安装Docker ,centos使用yum install docker,ubuntu/debian使用apt-get install docker安装即可。然后启动docker服务器。
【Docker相关的内容可以查考课程:http://edu.51cto.com/course/12675.html
   
  • docker search python3
       16.png
选择docker.io/avnergoncalves/ubuntu-python3.5作为基镜像
  • 编写Dockerfile制作镜像。
#指定基镜像
FROM docker.io/avnergoncalves/ubuntu-python3.5
#设置工作目录
WORKDIR /app
#更新
RUN apt-get update -qqy
#安装必要的软件和工具
RUN apt-get -qqy install  vim wget net-tools  iputils-ping  openssh-server python python3-dev  python3-pip

#使用pip命令安装Flask,jsonify,request库
RUN pip3 install --upgrade pip
RUN pip3 install flask jsonify request
#开放5000端口
EXPOSE 5000
ENV BUILD_ON 2018-03-03
#拷贝py文件到app目录
COPY docker_blockchain.py /app
#指定容器启动后执行的命令
#CMD ['/bin/bash']
ENTRYPOINT ["python3", "/a


新建blockchain目录,将上面的Dockerfile,docker_blockchain.py文件拷贝到这个目录中,新建build.sh文件和build_network.sh文件 17.png
build.sh文件内容如下:
18.png


build_network.sh文件内容如下:
19.png
运行sh build.sh等待镜像的构建完成,完成后使用docker iamges查看生成的blockchain镜像。
20.png
运行sh build_network.sh构建网络。使用docker network ls 查看网络情况 21.png
接下来就可以运行镜像生成容器了。
测试脚本如下:
#注意:运行的时候将192.168.0.4改成你自己虚拟机的ip即可!其他不用变。
#启动node1节点
docker run --rm -p 12381:5000  --net blockchain --name node1 --ip 172.19.0.2 blockchain
#启动node1节点
docker run --rm -p 12382:5000  --net blockchain --name node2 --ip 172.19.0.3 blockchain


#模拟挖矿
curl -X GET -H "Content-Type: application/json"  "http://192.168.0.4:12381/mine"
#获取整个区块链信息
curl -X GET -H "Content-Type: application/json"  "http://192.168.0.4:12381/chain"



#模拟转账
curl -X POST -H "Content-Type: application/json" -d '{
"sender": "891a9c1f3d1a4575871eaa2f2e44b85e",
"recipient": "mymymyacount",
"amount": 5
}' "http://192.168.0.4:12381/transactions/new"

#模拟注册
curl -X POST -H "Content-Type: application/json" -d '{
"nodes": ["http://192.168.0.4:12381"]
}' "http://192.168.0.4:12382/nodes/register"

curl -X POST -H "Content-Type: application/json" -d '{
"nodes": ["http://192.168.0.4:12382"]
}' "http://192.168.0.4:12381/nodes/register"

#在node2上挖矿
curl -X GET -H "Content-Type: application/json"  "http://192.168.0.4:12382/mine"


#在node1上挖矿,先调用一致性同步算法。在node2上挖矿没挖一次都要调用一下同步算法

curl -X GET -H "Content-Type: application/json"  "http://192.168.0.4:12381/nodes/resolve"
curl -X GET -H "Content-Type: application/json"  "http://192.168.0.4:12382/nodes/resolve"



如果你先制作镜像麻烦,可以直接pull我已经制作好的镜像,docker search reganzm/blockchain,docker pull reganzm/blockchain即可。然后运行上面的测试脚本!










本帖被以下淘专辑推荐:

已有(3)人评论

跳转到指定楼层
jiangzi 发表于 2018-7-19 14:54:45
Docker容器百行代码实现自己的分布式区块链~~~
回复

使用道具 举报

江火似流星 发表于 2018-12-29 13:51:29
谢谢楼主的分享
回复

使用道具 举报

yuntian0215 发表于 2019-4-29 09:50:55
厉害,学习了
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

推荐上一条 /2 下一条