区块链这个技术在2017年是比较火的,基于区块链技术的比特币的价格也是高的惊人,于是我就想对区块链技术做个深入了解。在网上看了大量文章后,发现大多数文章要么只讲理论,要么就只贴代码,都不太满意。于是我就写了这篇文章,代码结合理论一起讲解。
定义
我有一个习惯,就是查找一个概念或一个专有名词的时候就喜欢去维基百科上找,而不喜欢去百度上面搜。维基百科对区块链有这样一段描述:
it as an open, distributed ledger that can record transactions between two parties efficiently and in a verifiable and permanent way
简单说区块链就是开放的分布式账本,并且可以以一种可核查的(verifiable)和永久的(permanent)方式记录双方的交易。这里有几个关键词(已经加重标出了),我们依次来讲解一下这几个词。开放的,与开放的相对的是封闭的(或者说加密的),我们平常使用的数据库(如MySQL),在访问数据库之前都要先输入密码,密码错了则不能访问,这个是封闭的的做法。而开放的则不是这样,任何人都可以免费的得到这份账本,无需输入密码。分布式说明无中心节点,每个节点都是平等的,并且都保存着一份完整的账本。账本说明区块链可以存储、读取信息,我们可以大胆的认为它就是一个数据库,只不过与我们传统的数据库不太一样而已。可核查的,我们知道既然每个节点都是平等的,那么每个节点都有权向账本中写入数据(向区块链的末尾添加一个区块),其他节点就要判断你添加的这个数据是否是合法的,只有合法的数据才会被认可,并且把这个数据也添加到自己的账本。
区块
一个区块需要包含以下信息:
- id,用于唯一表示这个区块
- prev_hash,前一个区块的hash
- nonce,工作量证明相关,表明获取这个区块总共计算了多少次
- data,存储的数据,我们这个是很简单的区块链,所以存放的是简单的字符串
- timestamp,挖到这个区块的时间,用unix时间戳表示
- hash,256位(二进制,每一位都是0或1),每个区块的hash都不同,同id,也可用于唯一表示这个区块
上面只是对每个属性做了个简单的介绍,具体的介绍放到对应的小节讲,如nonce会在讲解工作量证明那节会详细讲到。我们按照上面的定义可以很快写出下面的类:
class Block(object):
def _calc_hash(self, data, nonce, prev_hash):
return hashlib.sha256(data + str(nonce) + prev_hash).hexdigest()
def __init__(self, **kwargs):
self.id = kwargs['id']
self.prev_hash = kwargs['prev_hash']
self.nonce = kwargs['nonce']
self.data = kwargs['data']
self.timestamp = kwargs['timestamp']
self.hash = self._calc_hash(self.data, self.nonce, self.prev_hash)
这里我假设输入是合法的,如data和prev_hash均有值且都是str类型,nonce有值且可用str()内置函数转成str类型。如果把这段代码用到生产环境中是非常不明智的,最好先对用户的输入做一系列的合法性判断,如果不合法该怎么处理,因为本篇主要讲解原理,所以代码追求尽可能的简单。
这里用到了hashlib这个库来计算hash值,关于hash有以下几个重要推论:
- 源不一样则hash出来的结果一定不一样
- 源一样则hash出来的结果也一样
- 源一旦有任何改变都会导致hash出来的结果变得面目全非,实际上这点可以通过前面两点推导而来
- 根据结果无法反向推导出源,即不可逆
- 用同一个hash函数(或hash算法)得到的结果的长度都是一样长的,不管源的长度如何,
本程序中用到了sha256算法,该算法得出的结果长度恒为256位。 大家可能注意到了,我是说以上几点都是推论,至于为什么说是推论,因为理论上并不成立。比如说第1点,我们可以试想以下,因为hash是256位,每一位都要么是0要么是1,那么总共有2的256次方种可能, 如果我们有2的256次方加1次不同的输入,那么必然会有至少两次hash出来的结果是一样的,但是要计算2的256次方加1次hash所需要的时间很长很长(感兴趣的读者可以自己计算一下,设每次hash所需时间为T,那么2的256次方乘以T就是需要的时间),长到两次hash的结果完全一样的概率无限趋近于0,这一点只要知道就好。
基于以上几点,我们就知道,除非某两个区块的data + str(nonce) + prev_hash
的结果完全一样,否则任意两个区块的hash值都不一样且是唯一的。而两个区块的data + str(nonce) + prev_hash
的结果完全一样几乎是不可能的,所以使用hash来唯一表示一个区块也是可行的。
创建创世区块
创世区块是区块链的第一个区块,一般都是预先定义好,不存储任何有价值的信息。我们定义一个创建创世区块的函数,代码如下:
def create_genesis_block():
params = {
'id': 0,
'data': '',
'nonce': 0,
'prev_hash': '',
'timestamp': int(time.time())
}
return Block(**params)
这段代码很好理解,就不详细说了。需要注意的是,因为创世区块是区块链的第一个区块,所以它的prev_hash为空字符串,prev_hash是表示前一个区块的hash。
工作量证明
前面说过区块链是可核查的,即区块链要有一种方法来证明你这个区块是合法的,是合法的区块才能append到区块链的末尾,本篇就来讲述如何判断一个区块是否是合法的。 工作量证明的核心是难于计算但是易于验证。换句话说就是,客户端通过大量的计算才能得到一个结果,但是要验证这个结果是否正确则是非常容易的。要验证一个区块 是否合法,我们要先定义一个规则,任何遵守这个规则的区块我们都认为是合法的。比如我们规定一个区块的hash必须以4个0(十六进制,二进制这是16个0)开头,如以下是合法的hash
0000ea046ff88074619b8c984ad64416d7b38a7cd2601339bc31718c0c1faad7
0000a19c1565f45229616010a136680a1540a6a4f580a6acbb84d0bf65f04b60
以下则是不合法的hash
12deea046ff88074619b8c984ad64416d7b38a7cd2601339bc31718c0c1faad7
ef23a19c1565f45229616010a136680a1540a6a4f580a6acbb84d0bf65f04b60
这个是很好验证的,符合工作量证明的核心易于验证。我们再来讨论得到这个hash值是否是难于计算。转换成二进制后,hash的前16位必须是0,根据概率学,那么大概需要2的16次方(65536)次计算才能得到一个合法的hash值,读者可以自己写代码验证一下,如:
import hashlib
def calc_hash(data, nonce, prev_hash):
return hashlib.sha256(data + str(nonce) + prev_hash).hexdigest()
def main():
nonce = 0
data = 'hello, blockchaindfewdffbv'
prev_hash = ''
while True:
hash = calc_hash(data, nonce, prev_hash)
if hash[:4] == '0000':
break
nonce += 1
print 'Got you:'
print nonce
print hash
根据概率学,跑的次数越多,平均值就会越接近65536,理论上是这样,实际上应该也是,有兴趣的读者可以自己测一下。所以平均要经过65536次计算才能得到一个合法的hash,如果改变一下规则,要求hash值(十六进制)的前6位必须是0,那么平均要经过2的24次方(16777216)才能得到一个合法的hash值,因此符合工作量证明的核心难于计算。 从理论上这个规则是OK的吗,我们就来写代码来实现它吧。
NUM_ZEROS = 4
class ProofOfWork(object):
def __init__(self, block):
self.block = block
def is_block_valid(self):
return self.block and self.block.hash and self.block.hash.startswith('0' * NUM_ZEROS)
代码很简单,就不解释了。NUM_ZEROS越大,难度越高,需要的计算次数也越多。但是不管NUM_ZEROS设的多大,都是易于验证的。
这个ProofOfWork类基本可以工作,但是有个问题,Block有那么多个属性,我们仅仅是验证了它的其中一个属性(hash),而忽略了其他属性。没错,我们还要验证id和prev_hash,id 必须比前一个区块的id多1,pre_hash必须等于前一个区块的hash,我们来完善代码。
NUM_ZEROS = 4
class ProofOfWork(object):
def __init__(self, block, prev_block):
self.block = block
self.prev_block = prev_block
def is_block_valid(self):
return self.block and self.block.hash and self.block.hash.startswith('0' * NUM_ZEROS) \
and self.block.id == self.prev_block.id + 1 \
and self.block.prev_hash == self.prev_block.hash
挖矿
现在我们只有一个区块,也就是创世区块,我们自然而然就想得到更多的块,这个就是挖矿了。挖矿的目的是要根据blockchain中的最后一个block,找到一个符合条件的新的block。 符合条件是指符合工作量证明,只有通过工作量证明的区块才能被其他节点认可和接受。挖矿的原理很简单,就是不断的递增nonce值,得到一个对应的区块,然后去验证这个区块是否满足工作量证明,满足则说明找到了一个符合条件的矿,没找到则继续递增nonce。代码如下:
def mine(prev_block):
nonce = 0
while True:
data = 'block ' + str(prev_block.id + 1)
params = {
'id': prev_block.id + 1,
'data': data,
'nonce': nonce,
'prev_hash': prev_block.hash,
'timestamp': int(time.time())
}
block = Block(**params)
if ProofOfWork(block, prev_block).is_block_valid():
return block
nonce += 1
因为挖矿需要做大量的计算,所以挖矿是很耗费能源的(电力)。
区块链
区块链,顾名思义,就是区块形成的链。上节讲了挖矿,就算挖得再多,不把它们链起来,依然没用。话不多说,直接上代码:
class BlockChain(object):
def __init__(self):
self.blocks = []
def add_block(self, block):
if not ProofOfWork(block, self.get_last_block()).is_block_valid():
return False
self.blocks.append(block)
return True
def get_last_block(self):
return self.blocks[-1]
def is_chain_valid(self):
prev_block = self.blocks[0]
for block in self.blocks[1:]:
if not ProofOfWork(block, prev_block).is_block_valid():
return False
prev_block = block
return True
我们把区块链定义成了一个list,并添加了几个方法:
- add_block: 向区块链的末尾添加一个区块,只有经过工作量证明的区块才能添加
- get_last_block:返回区块链中的最后一个区块
- is_chain_valid: 判断该区块链是否合法
代码很简单,就不解释了。
后记
本文在讲解区块链的原理的基础上,也实现了一个简单的区块链。当然由于篇幅所限,只讲了区块链的部分,还有很多没讲,比如既然区块链是一个数据库的话,我们并没有讲到区块链的存储,以及各个节点的同步等。这个有机会放到下篇再讲(要准备年会了,我也不知道下篇是什么时候^_^)。实际上存储以及同步都已经实现了,代码在github上,希望读者可以star一下, 另希望也能关注一下本人的技术公众号。