Skip to content

shawnsky/blockchain

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

blockchain

参考 naivecoin tutorial: https://lhartikk.github.io/

本项目是 naivecoin 的 python 实现版本,包括区块链的数据结构、“挖矿”、节点同步的功能。

环境

  • Python >= 3.4
  • Flask == 1.1.1

介绍

区块链是什么?核心就是一句话,区块链是一个分布式数据库,它维护着一个持续不断增长的、有序的记录列表。

比如比特币是区块链技术的一个比较出名的应用。在数字货币的系统中,分布式数据库里保存的就是一条一条交易转账信息。在本次技术分享中,会用 Python 编程实现一个简单的区块链,包括区块链的数据结构、“挖矿”、节点同步的功能。具体来说,将会解决以下问题:

  • 区块以及区块链的数据结构
  • 如何创建区块
  • 定义区块之间的关系
  • 向区块链中添加区块
  • 区块链网络中的节点同步
  • 一个简单的区块链 HTTP 管理接口

区块

在一个系统中如何持续不断的存储数据呢?可能会遇到如下困难

  • 怎么让数据逐渐增加
  • 怎么对之前的数据进行修改

区块的数据结构

主要包括以下核心元素:

  • index 区块的递增编号
  • timestamp 时间戳
  • data 该区块所保存的数据
  • hash 该区块的hash值
  • previous_hash 前一个区块的hash值,从⽽将区块串联了起来

于是我们可以得到如下所示的代码

class Block:
    def __init__(self,index,transactions,timestamp,previous_hash,difficulty=3,nonce=0):
        self.index = index
        self.transactions = transactions
        self.timestamp = timestamp
        self.previous_hash = previous_hash

区块的哈希值

我们有一个共识,就是我们不会去修改已经保存了的区块数据。可是仅凭大家的共识没有用,那该怎么去真正的去限制呢?

hash值是最重要的属性之一。hash值是对该块的所有数据进行计算的,也就是这个区块的摘要。这意味着如果块中的任何内容发生变化,原始hash值将不再有效。我们可以通过这个办法来检查区块数据是否被恶意的篡改。

所以,在 Block 类中,定义了一个 compute_hash() 方法来计算本区块的hash值。

def compute_hash(self):
        block_json = json.dumps(self.__dict__, sort_keys=True)
        return sha256(block_json.encode()).hexdigest()

区块链

区块链保存着一个区块数组,以及定义了操作区块链的一些方法。

class BlockChain:

    def __init__(self):
        self.blocks = []
        self.__create_genesis_block()

    def __create_genesis_block(self):
        genesis_block = Block(0, [], time.time(), "cuc0123456789")
        self.blocks.append(genesis_block)

    @property
    def last_block(self):
        return self.blocks[-1]

创世区块

创世区块就是区块链中的第一个区块,因为每一个区块都需要一个 previous_hash 属性,区块链中的第一个区块需要我们用硬编码的方式创建出来,它的 previous_hash 可以被随便设置一个字符串

genesis_block = Block(0, [], time.time(), "cuc0123456789")

添加区块

分布式系统中,大家都可以添加区块,如何防止很多人一起添加造成的混乱呢?我们可以想出一种策略,大家同时去解一道数学难题,谁先解出来,谁就可以添加区块。

解出这道难题又叫做 Proof-of-Work(工作量证明),当你得到了这道难题的答案,大家就都会认可你解题的工作量。这个解题的过程通常被称为 “挖矿”。

一个好的题⽬要让计算机便于理解,运算规则相对简单,运算方式相对公平。于是结合Hash算法的题目被设计了了出来:

找到一个特定区块,这个区块的Hash需要有特殊的前缀

这个前缀越特殊,难度就越⼤大。于是我们可以定义出题⽬的难度 difficulty 为你所定义的特殊前缀是由几个0组成。例如,如果你只要求找到的区块的Hash前缀是一个0(difficulty=1),那么可能相对简单。但是如果要求找到的区块的Hash的前缀有10个0(difficulty=10),那么就有点难了。

那么有一个问题,怎么去找这个区块呢?

为了找到满足 difficulty 条件的Hash,我们需要对同一个区块计算出不同的Hash,明显这是不可能的。所以我们可以通过在区块中加⼊新的可变参数来实现Hash结果的变化,因为SHA256算法会因为数据的任何微⼩变化扩散为整个hash结果的完全变化。

于是我们添加了一个叫做 Nonce 的参数,并且不断的改变这个参数的值,直到挖到我们想要的Hash结果。于是我们 Block 类的定义变为这样

class Block:
    def __init__(self, index, transactions, timestamp, previous_hash, difficulty=3, nonce=0):
        self.index = index
        self.transactions = transactions
        self.timestamp = timestamp
        self.previous_hash = previous_hash
        self.difficulty = difficulty
        self.nonce = nonce

    def compute_hash(self):
        block_json = json.dumps(self.__dict__, sort_keys=True)
        return sha256(block_json.encode()).hexdigest()

在 “挖矿” 之前,我们先实例化一个 Block

new_block = Block(index=last_block.index + 1,
                          transactions=[],
                          timestamp=time.time(),
                          previous_hash=last_block.compute_hash(),
                          difficulty=self.get_difficulty(self.blocks))

然后不断的去改变 new_blocknonce 属性的值,直到计算出了满足条件的hash值,我们定义一个方法来实现这个操作

def do_a_difficult_work(block):
    block.nonce = 0
    computed_hash = block.compute_hash()
    while not computed_hash.startswith('0' * block.difficulty):
        block.nonce += 1
        computed_hash = block.compute_hash()
    return computed_hash

如何确定难度

虽然我们能够指定问题的难度,但是我们要如何设置难度呢?而且如何才能让网络的节点都认同这个难度呢?

让我们回归到区块链的本身。区块链无非是一个区块的链表,并且每隔一段时间会加入一个新的区块。而我们的题目就是在控制加入区块的难度,也就是加入的时间间隔,于是我们引入一个全局参数:

  • BLOCK_GENERATION_INTERVAL:定义了多久产生一个区块

但是随着环境的变化,例如有更多的节点加入网络,我们并不能一致维持这个时间,因此我们每隔一段时间需要调整一下难度,于是我们引入第二个全局参数:

  • DIFFICULTY_ADJUSTMENT_INTERVAL:定义了每隔多久调整一次难度

在我们的代码中,我们会设置间隔为10秒。

我们会根据预期和现实之间的差异决定如何调整难度。具体来说,判断差异是否到了2倍的范围,然后对difficulty进行+1或者-1的操作

    def get_difficulty(self, blocks):
        last_block = self.last_block
        if last_block.index % self.DIFFICULTY_ADJUST_INTERVAL == 0 and last_block.index != 0:
            return self.get_adjusted_difficulty(last_block, blocks)
        else:
            return last_block.difficulty

    def get_adjusted_difficulty(self, last_block, blocks):
        previous_adjusted_block = blocks[len(blocks) - self.DIFFICULTY_ADJUST_INTERVAL]
        time_expected = self.BLOCK_GEN_INTERVAL * self.DIFFICULTY_ADJUST_INTERVAL
        time_taken = last_block.timestamp - previous_adjusted_block.timestamp

        if time_taken < (time_expected / 2):
            return previous_adjusted_block.difficulty + 1
        elif time_taken > (time_expected * 2):
            return previous_adjusted_block.difficulty - 1
        else:
            return previous_adjusted_block.difficulty

超长链攻击

存在一种情况,假如我们的数学难题被多个人同时解出来怎么办?这完全有可能,毕竟网络中的节点非常多。但是很少会有连续被多人同时解出来的情况,所以,在区块链中,对于这种“分叉”的情况,我们一般会选择最长的那一条链。

因此就会有恶意的攻击者通过自行设置低难度产生超长链,使网络中的节点信任他的恶意的超长链。面对这样的问题该如何解决呢?

我们可以实现一个难度评估函数,综合考虑整个链的实际难度,而不仅仅取决于链的长度。

    def eval_real_difficulty(blocks):
        tot = 0
        for block in blocks:
            tot += math.pow(2, block.difficulty)
        return tot

节点通讯

因为在整个网络中有很多节点,大家都有可能去创建区块,这就需要大家通过协商通讯的方式达成共识,这需要以下三个基本能力:

  • 某节点创建了一个区块,需要通知整个网络中的每个节点
  • 新节点加入网络
  • 节点收到消息后,可以验证链的合法性,更新自己的链

验证区块链是否合法

如果其他人给了我们一个区块链,我们如何验证这个链是正确的呢?这需要符合以下的基本要求:

  • 区块之间的索引是+1递增的
  • 每个区块的previous_hash需要和之前区块的Hash相同
  • 每个区块自身的Hash需要正确,而且是一个正确的工作量证明
    def is_valid_new_block(new_block, previous_block):
        # 检查索引是否正确
        if previous_block.index + 1 != new_block.index:
            print("Wrong index: " + str(new_block.index))
            return False
        # 检查hash是否正常连接
        elif previous_block.compute_hash() != new_block.previous_hash:
            print("Wrong previous hash: " + new_block.previous_hash)
            return False
        # 检查hash是否是一个正确的proof
        elif not new_block.compute_hash().startswith('0' * new_block.difficulty):
            print("Not a proof: " + new_block.compute_hash())
            return False
        return True

    def is_valid_chain(self, blocks):
        # skip genesis block
        for i in range(1, len(blocks)):
            if not self.is_valid_new_block(blocks[i], blocks[i - 1]):
                return False
        return True

更新本地的链

如果其他人给我们的区块链是合法的,而且比本地的链要新(实际难度更大),那么就用这个新链替换本地的链。

def replace_chain(self, blocks):
    if self.is_valid_chain(blocks) and self.eval_real_difficulty(self.blocks) < self.eval_real_difficulty(blocks):
        self.blocks = blocks
        return True
    return False

为区块链创建控制接口

使用 HTTP API 的方式实现对区块链的管理,需要实现如下接口

  • 查看本地区块链
  • 添加区块
  • 替换本地区块链
  • 查看节点列表
  • 注册节点

使用 Flask 微框架来快速实现这个功能,使用 @app.route() 装饰器快速创建 HTTP 控制器

如下代码,实现了操作本地区块链对象的 HTTP 接口

@app.route('/chain', methods=['GET', 'POST'])
def chain_handler():
    if request.method == 'GET':
        return Response(__block_list_to_json(local_chain.blocks), mimetype='application/json')

    if request.method == 'POST':
        blocks_in_json = request.form['chain']
        blocks = json.loads(blocks_in_json, object_hook=Block.dict2block)
        print("Recv blocks: " + str(len(blocks)))
        # todo: check chain and replace local chain
        if local_chain.replace_chain(blocks):
            print("local chain replaced")
        return "OK"

网络节点

节点列表是通过 Python 的集合来维护的,节点在刚启动的时候,只有一个节点,也就是本身。如果想要加入到网络中,就需要先联络网络中的其中一个节点。

@app.route('/peer', methods=['GET', 'POST'])
def peer_handler():
    if request.method == 'GET':
        dict_list = []
        for peer in peers:
            dict_list.append(peer)
        return Response(json.dumps(dict_list), mimetype='application/json')

    if request.method == 'POST':
        url = request.form['url']
        # todo: validate the url
        print("Recv url: " + url)
        peers.add(url)
        return 'OK'

消息广播

有两个场景需要消息广播

  • 发现了新的区块
  • 有新的节点加入网络

当新的节点加入到本地的 peers 集合内之后,需要对集合内所有节点发送一个 POST 请求来注册新节点

def sync_peers():
    # post net node to each peer
    for peer in peers:
        if peer == local_server:
            continue
        for url in peers:
            r = requests.post('http://' + peer + '/peer',
                              data={'url': url})
    print("peers sync OK")

当我们发现了新的区块,就需要通知区块链网络中的每个节点,可以通过遍历 peers 集合,构造 POST 请求并发送区块链。对方收到区块链后,会去验证区块链的有效性并更新它本地的链

def broadcast(blocks):
    for peer in peers:
        if peer == local_server:
            continue
        chain_json = __block_list_to_json(blocks)
        r = requests.post('http://' + peer + '/chain',
                          data={'chain': chain_json})
    print("local chan broadcast OK")

About

使用 Python 从零搭建一个自己的区块链

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages