区块链技术丛书
点击查看第二章
点击查看第三章
区块链开发实战:
基于JavaScript的公链与DApp开发
梁培利 曹帅 吴延毅 编著
第一部分
区块链开发概述
目前开发一个区块链应用主要有以下三种方式:
从零开始开发一个区块链的应用。需要实现的部分主要包括:交易和区块的构造、加密算法、共识机制以及 P2P 通信等。这种方式适用于有较大创新的区块链项目,可以灵活选择各种算法。缺点在于开发周期长,实现难度大,需要有较强的开发能力才可以实现。
如果你只是想利用区块链的特性开发一个应用,而不想从头实现一遍区块链的底层机制,那么可以选择一个成熟的区块链应用开发平台。这类平台类似于现在的云服务平台,一般会提供开发工具以及底层接口,便于开发者根据自己的业务场景来编写区块链应用。
基于现有的区块链项目改造。开源是区块链项目的一贯风格,无论是区块链的鼻祖比特币还是目前非常火的 EOS,其代码都在 GitHub 上开源了。所以可以阅读这些开源项目的源代码,查看功能实现以及评估安全性等。这也给新的区块链项目开发者带来了福音,新项目的开发者可以拉取目前已经开源的项目,然后根据自己的需求进行改造(当然首先要满足对方项目的开源协议)。这也是一种快速开始一个区块链项目的方法。
本书的第一部分包含两章,在第1章里我们会简单梳理一下从比特币到区块链的发展历程。然后用300行代码实现了一个简单的区块链项目,内容涉及交易及区块的构造、挖矿以及基于工作量证明的共识机制等。相信没有任何开发区块链基础的开发者都可以很快了解区块链项目的基础知识。第2章阐述了区块链应用里智能合约和 DApp 的含义,然后介绍了几个主流区块链应用的开发平台。
第1章
自己动手实现一个区块链系统
比特币是近十年才诞生的新事物,想要进行区块链开发,需要先了解比特币以及区块链的发展背景。本章首先介绍比特币的诞生和发展,以及区块链技术的基本概念,包括加密哈希函数、数字签名、共识机制等,希望能够帮助读者快速了解区块链技术的知识背景。
“纸上得来终觉浅,绝知此事要躬行”。学习区块链的最好方式就是自己动手实现。有了基本概念之后,我们将一起从头开始实现一个简单的区块链系统,包括区块和区块链的构造、工作量证明算法的实现以及通过 HTTP API 的方式提供和区块链进行交互等。通过自己动手实现一个区块链系统,可以对区块链运行的基本原理有一个更深刻的理解。
1.1 从比特币到区块链
1.1.1 比特币的诞生和发展
长久以来,人们对货币的普遍认知是国家基于国家信用发行的、固定的、充当一般等价物的商品。从上个世纪开始,无论是经济学界还是极客圈都已经在探索数字货币的可行性。数字货币可以不基于国家来发行,完全诞生于虚拟世界。上个世纪末,陆续有人提出了多种数字货币的方案,比如 Wei Dai 的“b—money”以及 Adam Back 的 “hashcash”等。他们在数字货币的发展道路上做出了突破性的贡献,但是他们提出的方案依然不够完美,最后都没有流行开来。
2008年11月。一个署名为中本聪的人在一个密码学的邮件组发表了一篇论文《Bitcoin: A Peer-to-Peer Electronic Cash System》(如图1-1所示),首次提出了比特币的概念。在这篇论文中,中本聪详细阐述了如何使用 P2P 网络以及加密算法来创造一种不需要依赖信任的电子交易系统。2009 年 1 月,中本聪发布并开源了第一版的比特币软件,并用该软件挖出了第一个区块(也称为“创世区块”),并获得了第一批的挖矿奖励:50 个比特币,比特币的故事由此正式拉开序幕。
图1-1 比特币的白皮书
比特币是第一个诞生于虚拟世界并且可以方便地进行价值转移的真正数字货币。在比特币这个系统里,资产的发行、交易的确认等各种规则是由协议来约束的。比特币的发行总量是 2100 万枚,但是比特币的发行不是由某一个具体的人或组织控制的,而是交给了所有维护这个系统运行的节点。不同的节点之间通过挖矿来竞争比特币的发行权,新生产的比特币可以转入到自己指定的账户。随着挖矿节点的竞争越来越激烈,比特币网络的整体算力也越来越高。比特币系统中引入了一个根据当前全网算力来自动调整挖矿的机制,用于保证系统的产块时间维持在10分钟左右。比特币最初的区块奖励是每挖一个区块可以获取 50 个比特币,比特币的区块高度每增长21万(大概是四年的时间),区块的奖励就会减半。2018年7月,区块奖励为每个区块12.5 个比特币。
比特币在诞生初期只是作为一个新鲜物种流行于极客圈,那时,一台普通的笔记本就可以参与到比特币的挖矿过程中并且有巨大的回报。比特币的价格从一开始每个比特币兑换 0.003 美元到 2017年底达到最高 2 万美元,中间也经历过几次大起大落,如图1-2所示。
图1-2 比特币2012年以来的价格走势(图片来自于 coindesk)
经过近十年的发展,比特币目前已经形成了完整的生态体系。这个体系包括比特币的核心开发者(Bitcoin Core)、矿池、矿机生产商、交易所、钱包以及用户等。他们在比特币这个开放的社区中贡献着自己的资源,发表自己的看法,共同推动着比特币社区的发展。
1.1.2 区块链
比特币是第一个真正意义上的加密数字货币,也是一个价值传递的网络。然而它也面临着很多问题:交易并发量过低(扩容前比特币每秒仅支持7笔交易,也就是TPS为7),确认速度慢(一般认为经过 6 次确认是安全的,大概需要一个小时),浪费电力。根据权威统计,目前维持比特币系统运转所耗费的电力相当于一个中型国家的消耗。
比特币的TPS是如何计算出来的? 在比特币系统里,最小的交易包含了一个输入和两个输出(包括收款人以及找零地址)的 P2PKH 类型的交易。P2PKH 的输入为 148 个字节,输出为34个字节。每笔交易的额外开销(overhead)又占了 10 个字节。所以在比特币系统里一笔最小交易的长度为 148+234+10=226 个字节。比特币的区块大小限制为 1M 字节,也就是 1000000 字节。 考虑极限情况下,一个区块里所有的交易都是这种最小的交易,而比特币是每 10 分钟生产一个新的区块,因此比特币每秒最多可以处理的交易为 1000000 / 226 / 6010 = 7.37 TPS。目前比特币系统里交易的平均大小为 520 字节,所以实际比特币的 TPS 为 3 左右。
其实,在中本聪的白皮书中并没有“区块链”这个词,中本聪提出的是一个基于哈希链表的数据结构,可以用于解决支付中的双花问题(Double spend problem)。因为比特币底层技术有巨大的应用潜力,所以人们将比特币的底层技术抽离出来,并给它起了一个好听的名字:区块链。区块链技术是一种分布式不可篡改的加密数据库技术,主要解决的是去中心化节点间的数据一致性问题,并且融入了通证(Token)的经济激励机制。可以大大增强数据的安全度和可信度。
区块链技术主要包括以下几个部分。
1. 加密哈希函数
我们都知道,一个函数可以接收一个或若干个输入值,然后经函数运算产生一个或若干个输出值。哈希函数满足所有下列条件:
接收任意长度的字符串作为输入。
产生一个固定长度的输出值。
计算时间在合理范围内。
只要满足上述条件,一个函数就可以称为哈希函数。举个简单的例子:取模运算,任意数字对10取模后得到的结果都是0~9之间的一个数字,那么取模运算就可以认为是一个哈希函数。
目前用于比特币等数字货币的哈希函数则是加密哈希函数。加密哈希函数除了拥有上述哈希函数的三个特点外,还有着更为独特的特性:无碰撞性、隐藏性、结果随机性,下面分别解释。
(1)无碰撞性
无碰撞性分为强无碰撞性和弱无碰撞性。强无碰撞性的意思是,对于一个哈希函数 H,我们无法找到两个不同的x和y值,使得H(x) = H(y)。弱无碰撞性则是,对于一个哈希函数H以及输入 x 值,无法找到另外一个 y 值,使得 H(x) = H(y)。
无碰撞性并不是真正的“无”碰撞,碰撞是肯定存在的,这里强调的是寻找碰撞的难度。我们以比特币中使用到的哈希函数 SHA256 为例,它的输出值为 256位,因此结果只有2256种可能,但是输入值却可以有无限种可能。现在就有一种必然能找到碰撞的办法:我们首先找到2256+1个不同的值,分别计算出它们的哈希值,那结果集里必然有重复的值,这样就会发现一次碰撞了。但是这种方法的可行性怎样呢?假设把全世界所有的计算设备集合起来,从宇宙诞生的时刻到现在一直不停地运算,能够找到一次碰撞的概率和下一秒钟地球被陨石撞击而毁灭的概率一致。既然你读到了这里,那就说明地球毁灭没有发生,也就是没有碰撞发生。
现在还没有一种加密哈希函数在数学上被证明是严格无碰撞性的,现在市面上提到的无碰撞性,一般认为是目前除了暴力破解之外没有其他的途径能够更快地找到碰撞而已。以前也有曾经被认为是无碰撞性的哈希函数后来找到了破解方案的案例。比如 MD5 哈希算法。比特币使用的 SHA256 哈希算法目前也被认为是无碰撞性的,但是不排除以后被破解的可能。
无碰撞性有什么应用呢?一个比较常见的就是消息摘要(Message Digest)。消息摘要是指针对任意长度的输入,通过加密哈希函数运算后得到的哈希值。以现在常用的哈希算法 MD5 为例,其运算示例如下:
liangpeili@LiangXiaoxin:~$ echo 'liang' | md5sum
ca75d8d934e4ae05a7146fb68f99f059 -
liangpeili@LiangXiaoxin:~$ echo 'aschplatform' | md5sum
150fa3630db1d8f576d1266176f6e0f7 -
liangpeili@LiangXiaoxin:~$ md5sum mongodb-linux-x86_64-3.4.6.tgz
7b32958579f2a92e5d0471ae19c0f5eb mongodb-linux-x86_64-3.4.6.tgz
可见输入任何长度的字符串,得到的结果都是固定长度的随机字符串。因为无碰撞性的存在,我们可以认为这个字符串能唯一地代表输入值。
我们平时在互联网上下载软件时,如何确定我们下载的这个软件和网站上的软件就是同一个呢?这时消息摘要就可以发挥作用了。例如,有的网站在下载软件时提供该软件的md5sum值,那么我们在下载完该软件时,就可以手工计算一遍该软件的 md5sum 值,然后和网站上的值进行对比,只要两个数值一致,就可以说明我们下载的软件是完整无误的。
(2)隐藏性
给定 H(x),无法推测出 x 的值。不仅无法推测出x的值,也不能推测出关于x的任何特点,比如奇偶性等。
(3)结果随机性
无论x值是否相近,经过哈希运算后得出的 H(x) 都是完全随机的。
这个特点是说,哪怕输入值x的长度很长,同时另一个输入值 x' 和 x 值只有一位不同,那它们经过哈希函数H运算后得到的结果没有任何的相关性,就像输入了两个完全不同的 x 值一样。
继续以 md5sum值为例:
liangpeili@LiangXiaoxin:~$ echo 'aschplatform' | md5sum
150fa3630db1d8f576d1266176f6e0f7 -
liangpeili@LiangXiaoxin:~$ echo 'aschplatform1' | md5sum
e915a617b2301631ec14d1ca2c093c63 -
liangpeili@LiangXiaoxin:~$ echo 'aschplatform2' | md5sum
bbb9d830f4a5d47051f9fd19cb0fc75e -
从上面的程序中可以看出,即使只改变一个很小的值,经过哈希运算后的结果也会有很大的不同。
这个特性有什么作用呢?如果我们针对特定的结果值 H(x),想找到一个符合条件的输入值 x,那么除了暴力尝试之外没有其他办法。继续以 SHA256 为例,它的输出结果长度为 256位,如果我们想找到这样的一个 x 值,使得它经过 SHA256 运算后,结果的第一位是0,求解这样的 x 值的期望次数为 2,那如果想要得到连续 10位为 0 的哈希值呢?期望计算次数就是 210 了。通过调整结果范围,我们就可以对计算次数(也可以认为是结果难度)进行调整,这也是比特币调整难度值的原理。
使用 Python 实现的挖矿算法如下:
2. 数字签名
在现实工作和生活中,我们使用签名的方式表达了对一份文件的认可,其他人可以识别出你的签名并且无法伪造你的签名。数字签名就是对现实签名的一种电子实现,它不仅可以完全达到现实签名的特点,甚至能做得更好。常用的数字签名算法有 RSA(Rivest-Shamir-Adleman Scheme)、DSS(Digital Signature Standard)等。比特币使用ECDSA(椭圆曲线数字签名算法)来生成账户的公私钥以及对交易和区块进行验证(参见后面5.1.2节)。
数字签名的工作原理如下所示:
1)Alice 生成一对密钥,一个是 sk(signing key),是非公开的;另一个是 vk(verification key),是公开的。这一对密钥同时生成,并且在数学上是相互关联的,同时,根据 vk 无法推测出关于 sk 的任何信息。
2)数字签名算法接收两个输入:信息 M 和 sk, 生成一个数字签名 Sm。
3)验证函数接收信息 M、Sm 以及 vk 作为输入,返回的结果是 yes 或者 no。这一步的目的是为了验证你看到的针对信息M的数字签名确实是由 Alice 的 sk 来签发的,用于确认信息与签名是否相符。
与手写签名不同,手写签名基本都是相似的,但是数字签名却受输入影响很大。对输入的轻微改变都会产生一个完全不同的数字签名。一般不会直接对信息进行数字签名,而是对信息的哈希值进行签名。由加密哈希函数的无碰撞性可知,这样和对原信息进行签名一样安全。
3. 共识机制
区块链可以看做是一本记录所有交易的分布式公开账簿,而区块链中每个节点都是对等的。这就带来一个问题:谁有权往这个账本录入数据?如果有好几个节点同时对区块链进行数据写入,最终以谁的为准?这就是在分布式网络中如何保持数据一致性的问题。共识机制是指在一个分布式的网络中,让各个参与网络的节点达成数据上的一致性。在区块链中,共识机制的作用还包括区块生产、区块验证以及系统的经济激励等功能。
不同的共识机制适用于不同的应用场景,以下是常用的共识机制及其适用的应用场景介绍:
- 工作量证明(Proof of Work, POW)—比特币使用的就是工作量证明的共识机制。在这种机制里,任何拥有计算能力的设备都可以参与竞争区块的生产,系统会根据当前全网的算力动态调整难度值,来保证平均每 10 分钟网络将根据后续区块的态度来决定认可哪个区块。一般来说,一笔交易在经过 6 次确认(约 1 个小时)后被认为是比较安全而且不可逆的。中本聪在设计比特币时,使用工作量证明机制背后的核心思想是“one cpu one vote”,期望能够把比特币设计成一个完全去中心化的系统,任何人都可以使用电脑等终端参与进来。虽然后来由于矿池的出现,使得比特币系统的算力比较集中,但目前工作量证明机制仍然被认为是最适合公链的共识机制。
- 股权证明(Proof of Stake, POS)—股权证明机制于 2013 年被提出,最早应用于 Peercoin 中。在工作量证明机制中,生产区块的概率和你拥有的算力成正比。相应的,在股权证明机制中,生产区块的难度和你在该系统中占有的股权成正比。在股权证明机制中,一个区块的生产过程为:节点通过保证金(代币、资产、名声等具备价值属性的物品即可)来对赌一个合法的区块会成为新的区块,其收益为抵押资本的利息和交易服务费。提供的保证金越多,获得记账权的概率就越大。一旦生产了一个新的区块,节点就可以获得相应的收益。股权证明机制的目标是为了解决工作量证明机制里大量能源被浪费的问题。恶意参与者存在保证金被罚没的风险。
- 授权股权证明(Delegated Proof of Stake, DPOS)—工作量证明和股权证明机制虽然都可以解决区块链数据的一致性问题,但正如上面提到的工作量证明机制存在算力集中(矿池)的问题,而股权证明机制根据保证金的数量来调节生产区块难度的方式则会导致“马太效应”的出现,也就是拥有大量代币的账户权利会越来越大,有可能支配记账权。为了解决前两者的问题,后来又有人提出了基于股权证明机制的改进算法—授权股权证明机制。在这种共识机制里,系统中的每个持币用户都可以投票给某些代表,最终得票率在前 101 名的代表可以获得系统的记账权。这些代表按照既定时间来锻造区块,并且获取锻造区块的收益。授权股权证明机制既可以提高共识的效率(相比较比特币每 10 分钟生产一个区块,这种机制可以实现 10 秒以内生产一个区块),又避免了能源的浪费和马太效应,因此成为了很多新兴公链(比如 EOS)的选择。
4. 交易的区块链
在比特币网络中,每笔交易完成后,这笔交易会广播到比特币的 P2P 网络。矿工不仅能够接收到这笔交易,而且还能接收到相同时间段内其他的所有未被记录的交易。矿工的工作就是把这些所有交易打包成一个交易区块。具体的过程是:
1)矿工会把这些交易记录两两配对,通过默克尔树计算出根节点的值。
2)根节点和上一个区块的哈希值结合,作为一个 Challenge String,供矿工作为工作量证明的输入值。
3)矿工完成工作量证明,并把 proof 公开出去供其他节点验证。同时在第一条记录(这条记录也称为 coinbase transaction)里给自己分配挖矿奖励。
4)其他节点验证通过,该区块作为新区块加入到区块链中。
5)矿工也可以收集其他交易记录里的交易费分配给自己。
比特币的诞生和区块链技术的不断发展给我们巨大的想象力。目前,互联网完成了信息的传递,而区块链技术或许可以为互联网带来价值的传递。区块链技术的基础设施、应用场景或许还需要一定的时间才可以发展到目前互联网技术的水平,但是区块链技术的潜力却不容小觑。
1.2 用300 行代码开发一个区块链系统
本节使用 Node.js 来实现一个简单的区块链系统,只需300行代码。
1.2.1 区块和区块链的创建
区块链是把区块用哈希指针连接起来的链条,区块是其中的基本单位。这里我们从设计一个区块的数据结构开始。
1. 创建区块
区块是构建区块链的基本单位,一个区块至少要包含以下信息。
- index:区块在区块链中的位置。
- timestamp:区块产生的时间。
- transactions:区块包含的交易。
- previousHash:前一个区块的Hash值。
- hash:当前区块的Hash值。
其中,最后两个属性 previousHash 和 hash 是区块链的精华所在,区块链的不可篡改特性正是由这两个属性来保证的。
根据上面的信息,我们来创建一个 Block 类:
const SHA256 = require('crypto-js/sha256');
class Block {
// 构造函数
constructor(index, timestamp) {
this.index = index;
this.timestamp = timestamp;
this.transactions = [];
this.previousHash = '';
this.hash = this.calculateHash();
}
// 计算区块的哈希值
calculateHash() {
return SHA256(this.index + this.previousHash + this.timestamp + JSON.stringify(this.transactions) + this.nonce).toString();
}
// 添加新的交易到当前区块
addNewTransaction(sender, recipient, amount) {
this.transactions.push({
sender,
recipient,
amount
})
}
// 查看当前区块里的交易信息
getTransactions() {
return this.transactions;
}
}
在上面的 Block 类的实现中,我们实用了 crypto-js 里的 SHA256 来作为区块的哈希算法,这也是比特币中使用的哈希算法。transactions 是一系列交易对象的列表,其中包含的每笔交易的格式为:
{
sender: sender,
recipient: recipient,
amount: amount
}
另外我们给 Block 类添加了三个方法:calculateHash、addNewTransaction、get-Transactions,分别用来计算当前区块哈希、增加新交易到当前区块、获取当前区块所有交易。
区块构建完成后,下一步就是考虑如何把区块组装成一个区块链了。
2. 创建区块链
一个区块链就是一个链表,链表中的每个元素都是一个区块。区块链需要一个创世区块(Genesis Block)来进行初始化,这也是区块链的第一个区块,需要手工生成。在我们创建 Blockchain 类时,需要考虑到创世区块的生成。以下是代码示例:
class Blockchain {
constructor() {
this.chain = [this.createGenesisBlock()];
}
// 创建创始区块
createGenesisBlock() {
const genesisBlock = new Block(0, "01/10/2017");
genesisBlock.previousHash = '0';
genesisBlock.addNewTransaction('Leo', 'Janice', 520);
return genesisBlock;
}
// 获取最新区块
getLatestBlock() {
return this.chain[this.chain.length - 1];
}
// 添加区块到区块链
addBlock(newBlock) {
newBlock.previousHash = this.getLatestBlock().hash;
newBlock.hash = newBlock.calculateHash();
this.chain.push(newBlock);
}
// 验证当前区块链是否有效
isChainValid() {
for (let i = 1; i < this.chain.length; i++){
const currentBlock = this.chain[i];
const previousBlock = this.chain[i - 1];
// 验证当前区块的 hash 是否正确
if(currentBlock.hash !== currentBlock.calculateHash()){
return false;
}
// 验证当前区块的 previousHash 是否等于上一个区块的 hash
if(currentBlock.previousHash !== previousBlock.hash){
return false;
}
}
return true;
}
}
在 Blockchain 这个类中,我们实现了一个创建创世区块的方法。由于创世区块中并没有前一个区块,因此 previousHash 设置为 0。另外假定这一天是 Leo 和 Janice 的结婚纪念日,Leo 给 Janice 转账520个代币,由此产生了一笔交易并记录到创世区块中。最后我们把这个创世区块添加到构造函数中,这样区块链就包含一个创世区块了。方法 getLatestBlock 和 addBlock 含义比较明显,含义分别是获取最新区块和往区块链中添加新的区块。最后一个 isChainValid 方法是通过验证区块的哈希值来验证整个区块链是否有效,如果已经添加到区块链的区块数据被篡改,那么该方法则返回为 false。我们会在下一部分对此场景进行验证。
3.对区块链进行测试
到现在为止,我们已经实现了一个最简单的区块链了。在这一部分,我们会对创建的区块链进行测试。方法是向区块链中添加两个完整的区块,并且通过尝试修改区块内容来展示区块链的不可篡改的特性。
我们先创建一个名字叫作 testCoin 的区块链。使用 Blockchain 类新建一个对象,此时它应该只包含创世区块:
const testCoin = new Blockchain();
console.log(JSON.stringify(testCoin.chain, undefined, 2));
运行该程序,结果为:
[
{
"index": 0,
"timestamp": "01/10/2017",
"transactions": [
{
"sender": "Leo",
"recipient": "Janice",
"amount": "520"
}
],
"previousHash": "0",
"hash": "23975e8996cd37311c7fd0907f9b2511c3bf23cf9c9147cca329dec76d7b544e"
}
]
然后,我们新建两个区块,每个区块里都包含一笔交易。然后把这两个区块依次添加到 testCoin 这个区块链上:
block1 = new Block('1', '02/10/2017');
block1.addNewTransaction('Alice', 'Bob', 500);
testCoin.addBlock(block1);
block2 = new Block('2', '03/10/2017');
block2.addNewTransaction('Jack', 'David', 1000);
testCoin.addBlock(block2);
console.log(JSON.stringify(testCoin.chain, undefined, 2));
可以得到以下结果:
[
{
"index": 0,
"timestamp": "01/10/2017",
"transactions": [
{
"sender": "Leo",
"recipient": "Janice",
"amount": 520
}
],
"previousHash": "0",
"hash": "23975e8996cd37311c7fd0907f9b2511c3bf23cf9c9147cca329dec76d7b544e"
},
{
"index": "1",
"timestamp": "02/10/2017",
"transactions": [
{
"sender": "Alice",
"recipient": "Bob",
"amount": 500
}
],
"previousHash": "23975e8996cd37311c7fd0907f9b2511c3bf23cf9c9147cca329dec76d7b544e",
"hash": "32b96fa0bba9a7353e67498d822fb0c1f89c307098295c288459cb44dbc5d0f1"
},
{
"index": "2",
"timestamp": "03/10/2017",
"transactions": [
{
"sender": "Jack",
"recipient": "David",
"amount": 1000
}
],
"previousHash": "32b96fa0bba9a7353e67498d822fb0c1f89c307098295c288459cb44dbc5d0f1",
"hash": "3a0b9a0471bb474f7560968f2f05ff93306cfc26be7f854a36dc4fea92018db2"
}
]
testCoin 现在包含三个区块,除了一个创世区块以外,剩下的两个区块是我们刚刚添加的。注意每一个区块的 previousHash 属性是否正确地指向了前一个区块的哈希值。
此时我们使用 isChainValid 方法可以验证该区块链的有效性。console.log(testCoin.isChainValid())的返回结果为 true。
区块链的防篡改性体现在哪里呢?我们先来修改第一个区块的交易。在第一个区块中,Alice 向 Bob 转账 500 元,假设 Alice 后悔了,她只想付 100 元给 Bob,于是修改交易信息如下:
block1.transactions[0].amount = 100;
console.log(block1.getTransactions())
Alice 查看区块链的交易信息,发现已经改成了 100 元,放心地走了。Bob看到后,发现交易遭到了篡改。于是 Bob 开始收集证据,他怎么证明 block1 的那笔交易是被人为篡改后的交易呢?Bob 可以调用 isChainValid 方法来证明目前的 testCoin 是无效的。因为 testCoin.isChainValid() 返回值为 false。但是 testCoin.isChainValid() 为什么会返回 false 呢?我们来看一下背后的逻辑:首先 Alice 修改了交易的内容,这个时候 block1 的哈希值肯定和通过之前交易计算出的哈希值是不同的。这两个值的不同会触发 isChainValid 返回为 false,也就是如下代码实现的功能:
if(currentBlock.hash !== currentBlock.calculateHash()){
return false;
}
既然如此,Alice 在修改交易内容的同时修改 block1 的 hash 不就可以了吗?Alice 可以继续篡改其他的区块内容:
block1.transactions[0].amount = 100;
block1.hash = block1.calculateHash();
console.log(testCoin.isChainValid())
这样的话,最后的结果依然是 false。为什么呢?是因为下面这段代码:
if(currentBlock.previousHash !== previousBlock.hash){
return false;
}
每一个区块都存储了上一个区块的哈希值,只修改一个区块是不够的,还需要修改下一个区块存储的 previousHash。如果我们已经安全地存储了 block2 的哈希值,那无论如何 Alice 都是不可能在不被发现的情况下篡改已有数据的。在真实的区块链项目中,修改一个区块必须修改接下来该区块之后的所有区块,这也是无法办到的事情。区块链的这个“哈希指针”的特性,保证了区块链数据的不可篡改性。
1.2.2 工作量证明
上节实现的区块链系统还比较简单,并且没有解决电子货币系统中需要解决的“双重支付”问题。要想维持整个系统健康运转,需要在系统中设计一定的经济激励机制。在比特币体系中,中本聪就设计了一个“工作量证明”的机制,解决了系统里的经济激励问题以及双重支付问题。下面我们介绍工作量证明算法的原理和实现。
1. 工作量证明算法
一个健康运行的区块链系统随时会产生交易,我们需要有服务器进行以下工作:定时把一个时间段(比特币是 10 分钟,Asch 是 10 秒)的交易打包到一个区块,并且添加到现有的区块链中。但是一个区块链系统中可能有很多台服务器,究竟是以哪台服务器打包的区块为准呢?为了解决这个问题,比特币中采用了一种叫做工作量证明的算法来决定采用哪一台服务器打包的区块并且给予相应的奖励。
工作量证明算法可以简单地描述为:在一个时间段同时有多台服务器对这一段时间的交易进行打包,打包完成后连带区块 Header 信息一起经过 SHA256 算法进行运算。在区块头以及奖励交易 coinbase 里各有一个变量 nonce,如果运算的结果不符合难度值(稍后会解释这个概念)要求,那么就调整 nonce 的值继续运算。如果有某台服务器率先计算出了符合难度值的区块,那么它可以广播这个区块。其他服务器验证没问题后就可以添加到现有区块链上,然后大家再一起竞争下一个区块。这个过程也称为“挖矿”。
工作量证明算法采用了哈希算法 SHA256,这种算法的特点是难以通过运算得到特定的结果,但是一旦计算出来合适的结果后则很容易验证。在比特币系统里,找到一个符合难度要求的区块需要耗费 10 分钟左右,但是验证它是否有效却是瞬间的事。在下一节的代码实现里我们会看到这一点。
举一个简单的例子: 假设有一群人玩一个扔硬币游戏,每个人有十枚硬币,依次扔完十枚硬币,最后看十枚中的正面和反面的排序结果。由于最后的结果是有顺序的,结果总有 210 种可能。现在有个规定,在一轮游戏中,谁先扔出了前 4 枚硬币都是正面的结果,谁就可以得到奖励。于是大家都开始扔十枚硬币并统计结果。前四枚都是正面的可能性有 26 种,因此一个人能获取该结果的期望尝试次数为 24。如果规定正面的个数越多,那么每个人的尝试次数就会越多,而这里的个数就是难度。如果玩游戏的人逐渐增多,那我们就可以要求结果的前 6 个、前 8 个是正面,这样每轮游戏的时间依然差不多。这也是为什么比特币的算力大增,而依然能保持平均每 10 分钟产生一个区块的原因。
上面阐述了什么是工作量证明,那如何把它添加到我们的区块链应用中呢?
任何一个数据经过 SHA256 运算后都会得到长度为 256位的二进制数值,我们可以通过调整最开始的部分连续0的个数作为“难度值”。比如我们要求最后的区块经过 SHA256 运算后第一位为 0,那么平均每两次运算就会得到一个这样的结果。但是如果我们要求连续10位都是 0,那就需要平均计算 210 次才能得到一次这样的结果了。系统可以通过调整计算结果里连续 0 的个数来达成调整难度的目标。
我们在区块的头信息中添加一个变量 nonce。通过不停地调节 nonce 的值来重新计算整个区块的哈希值,直到计算的结果满足难度要求。
2. 工作量证明的代码实现
基于上节的概念,我们开始改造现在的区块链应用。首先在 Block 类添加一个 nonce 变量:
class Block {
constructor(index, timestamp) {
this.index = index;
this.timestamp = timestamp;
this.transactions = [];
this.previousHash = '';
this.hash = this.calculateHash();
this.nonce = 0;
}
...
}
然后在 Block 类中添加一个 mineBlock 方法:
mineBlock(difficulty) {
console.log(`Mining block ${this.index}`);
while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")) {
this.nonce++;
this.hash = this.calculateHash();
}
console.log("BLOCK MINED: " + this.hash);
}
方法 mineBlock 就是根据难度值来寻找 nonce,只有找到合适的 nonce 之后才可以提交区块。这里的 difficulty 指的是结果里从开头连续为 0 的个数。如果计算出来的哈希值不符合要求,那么 nonce 加 1,然后重新计算区块的哈希值。
于是,我们在 Blockchain 类里定义一个难度值:
constructor() {
this.chain = [this.createGenesisBlock()];
this.difficulty = 2;
}
把挖矿的过程应用到添加区块到区块链的过程中:
addBlock(newBlock) {
newBlock.previousHash = this.getLatestBlock().hash;
newBlock.mineBlock(this.difficulty);
this.chain.push(newBlock);
}
到此为止,我们对应用的改造就完成了。下面对这部分添加后的代码进行测试。
我们先只添加一个区块:
const testCoin = new Blockchain();
block1 = new Block('1', '02/10/2017');
block1.addNewTransaction('Alice', 'Bob', 500);
testCoin.addBlock(block1);
console.log(block1)
运算结果为:
Mining block 1
BLOCK MINED: 005fed00324fcbe1f0ab1703afe94e45a99e197a7df142e669444687f9513e57
Block {
index: '1',
timestamp: '02/10/2017',
transactions: [ { sender: 'Alice', recipient: 'Bob', amount: 500 } ],
previousHash: '31b15cc32d6772f237dcf298d5b7a2417f298f40ce6d8d5fbe07958141df7a4c',
hash: '005fed00324fcbe1f0ab1703afe94e45a99e197a7df142e669444687f9513e57',
nonce: 419 }
注意那个 nonce 值以及 hash 值。nonce 值表明了计算次数,hash值是最后得到的结果。这次我们设置的难度值为 2,期望计算次数是 28 次(hash 里一个字符代表 4 位)。如果把难度值改成 3 呢?运算结果为:
Mining block 1
BLOCK MINED: 000b7f17beaf58bc8fea996a9fed11103ed27ad6d63818b87d89a440cd9757b5
Block {
index: '1',
timestamp: '02/10/2017',
transactions: [ { sender: 'Alice', recipient: 'Bob', amount: 500 } ],
previousHash: '31b15cc32d6772f237dcf298d5b7a2417f298f40ce6d8d5fbe07958141df7a4c',
hash: '000b7f17beaf58bc8fea996a9fed11103ed27ad6d63818b87d89a440cd9757b5',
nonce: 4848 }
可以看到,计算的次数增加了。随着难度值增大,CPU 计算的次数也会呈指数级增加,相应耗费的时间也就越长。
1.2.3 提供和区块链进行交互的API
1. 挖矿奖励
在实现相关 API 之前,我们首先来看一下什么是挖矿奖励。
上面介绍了挖矿的原理并且实现了工作量证明算法,可是服务器为什么愿意贡献自己的 CPU 资源去打包区块呢?答案就是挖矿时有一个奖励机制。矿工在打包一个时间段的交易后,会在区块的第一笔交易的位置创建一笔新的交易。这笔交易没有发送人,接收人可以设为任何人(一般设置为自己的地址),奖励的数额是多少呢?目前比特币矿工每打包一个区块的奖励是 12.5 个 BTC。这笔奖励交易是由系统保证的,并且可以通过任何一个其他节点的验证。
这里面有几个问题。首先,奖励金额的问题。比特币刚开始发行时,每个区块的奖励是 50BTC,其后每隔四年时间减半,2018年7月已经是 12.5 个BTC了。其次,矿工能否创建多笔奖励交易或者加大奖励金额?矿工当然可以这么干,但是这么做以后广播出去的区块是无法通过其他节点验证的。其他节点收到区块后会进行合法性验证,如果不符合系统的规则就会丢弃该区块,而该区块最终也不会被添加到区块链中。
2. 代码重构
为了把我们当前的代码改造成适合通过 API 对外提供的形式,需要做以下几个处理:
1)在 Blockchain 类中添加属性 currentTransactions,用于收集最新交易,并且准备打包到下一个区块中:
constructor() {
this.chain = [this.createGenesisBlock()];
this.difficulty = 3;
this.currentTransactions = [];
}
2)把 Block 类中的 addNewTransaction 方法移到 Blockchain 类里。
3)把 Block 类和 Blockchain 类输出(export),将app.js重命名为blockchain.js。
最后的 blockchain.js 内容应该为:
const SHA256 = require('crypto-js/sha256');
// 区块类
class Block {
constructor(index, timestamp) {
this.index = index;
this.timestamp = timestamp;
this.transactions = [];
this.previousHash = '';
this.hash = this.calculateHash();
this.nonce = 0;
}
calculateHash() {
return SHA256(this.index + this.previousHash + this.timestamp + JSON.stringify(this.transactions) + this.nonce).toString();
}
mineBlock(difficulty) {
console.log(`Mining block ${this.index}`);
while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")) {
this.nonce++;
this.hash = this.calculateHash();
}
console.log("BLOCK MINED: " + this.hash);
}
getTransactions() {
return this.transactions;
}
}
// 区块链类
class Blockchain {
constructor() {
this.chain = [this.createGenesisBlock()];
this.difficulty = 3;
this.currentTransactions = [];
}
addNewTransaction(sender, recipient, amount) {
this.currentTransactions.push({
sender,
recipient,
amount
});
}
createGenesisBlock() {
const genesisBlock = new Block(0, "01/10/2017");
genesisBlock.previousHash = '0';
genesisBlock.transactions.push({
sender: 'Leo',
recipient: 'Janice',
amount: 520
});
return genesisBlock;
}
getLatestBlock() {
return this.chain[this.chain.length - 1];
}
addBlock(newBlock) {
newBlock.previousHash = this.getLatestBlock().hash;
newBlock.mineBlock(this.difficulty);
this.chain.push(newBlock);
}
isChainValid() {
for (let i = 1; i < this.chain.length; i++){
const currentBlock = this.chain[i];
const previousBlock = this.chain[i - 1];
if(currentBlock.hash !== currentBlock.calculateHash()){
return false;
}
if(currentBlock.previousHash !== previousBlock.hash){
return false;
}
}
return true;
}
}
module.exports = {
Block,
Blockchain
}
注意,上面顺便修改了 Blockchain 里的方法 createGenesisBlock 的代码。
3. 使用 Express 提供 API 服务
为了能够提供 API 服务,这里我们采用 Node.js 中最流行的 Express 框架。区块链对外提供以下三个接口:
POST /transactions/new:添加新的交易,格式为JSON。
GET /mine:将目前的交易打包到新的区块。
GET /chain:返回当前的区块链。
基础代码如下:
const express = require('express');
const uuidv4 = require('uuid/v4');
const Blockchain = require('./blockchain').Blockchain;
const port = process.env.PORT || 3000;
const app = express();
const nodeIdentifier = uuidv4();
const testCoin = new Blockchain();
// 接口实现
app.get('/mine', (req, res) => {
res.send("We'll mine a new block.");
});
app.post('/transactions/new', (req, res) => {
res.send("We'll add a new transaction.");
});
app.get('/chain', (req, res) => {
const response = {
chain: testCoin.chain,
length: testCoin.chain.length
}
res.send(response);
})
app.listen(port, () => {
console.log(Server is up on port ${port}
);
});
下面我们完善路由 /mine 以及 /transactions/new,并添加一些日志功能(非必需)。
先来看路由 /transactions/new,在这个接口中,我们接收一个 JSON 格式的交易,内容如下:
{
"sender": "my address",
"recipient": "someone else's address",
"amount": 5
}
然后,把该交易添加到当前区块链的 currentTransactions 中。这里会用到 body-parser 模块,最后的代码为:
const bodyParser = require("body-parser");
const jsonParser = bodyParser.json();
app.post('/transactions/new', jsonParser, (req, res) => {
const newTransaction = req.body;
testCoin.addNewTransaction(newTransaction);
res.send(The transaction ${JSON.stringify(newTransaction)} is successfully added to the blockchain.
);
});
接下来是路由 /mine。该接口实现的功能是收集当前未被打包的交易,将其打包到一个新的区块中;添加奖励交易(这里设置为 50,接收地址为 uuid);进行符合难度要求的挖矿,返回新区块信息。代码实现如下:
app.get('/mine', (req, res) => {
const latestBlockIndex = testCoin.chain.length;
const newBlock = new Block(latestBlockIndex, new Date().toString());
newBlock.transactions = testCoin.currentTransactions;
// Get a reward for mining the new block
newBlock.transactions.unshift({
sender: '0',
recipient: nodeIdentifier,
amount: 50
});
testCoin.addBlock(newBlock);
testCoin.currentTransactions = [];
res.send(Mined new block ${JSON.stringify(newBlock, undefined, 2)}
);
});
至此,代码基本完成,最后我们添加一个记录日志的中间件:
app.use((req, res, next) => {
var now = new Date().toString();
var log = ${now}: ${req.method} ${req.url}
;
console.log(log);
fs.appendFile('server.log', log + 'n', (err) => {
if (err) console.error(err);
});
next();
})
4. 测试 API
使用Node Server.js 启动应用,我们使用 Postman 来对当前的 API 进行测试。
在启动应用后,当前区块链应该只有一个创世区块,我们使用/chain来获取当前区块链信息,如图1-3所示。
可以看到,当前区块链只有一个区块。那怎么添加新的交易呢?方法如图1-4所示。
图1-3 区块链信息
图1-4 区块链里的交易
把交易以 JSON 的形式添加到请求的 Body 中,返回结果如图1-5所示。
图1-5 交易的返回结果
接下来,我们可以进行挖矿了:把当前的交易打包到新的区块,并给自己分配奖励。这次我们使用mine 接口,如图1-6所示。
图1-6 调用mine 接口
返回的结果如图1-7所示。
图1-7 挖出第一个区块
可以看到,交易已经被打包到新的区块中了。新的区块中包含一笔奖励交易,难度也符合要求(连续 3 个 0)。
至此,三个接口全部工作正常,我们也可以继续添加交易、挖矿,一直进行下去。
有人会问:如果不添加交易是否可以挖矿呢?答案是 Yes!一般在一个区块链项目的早期,交易的数量可能一天也没有几笔。但是挖矿的工作是要一直进行下去的,只不过每个区块除了奖励交易再没有其他了,这种区块一般成为“空块”。在我们这里也可以实现,不添加交易,直接调用 mine 接口,如图1-8所示。
此时,再查看区块链信息,就可以看到刚刚建立的两个区块了,如图1-9所示。
图1-8 挖出第二个区块
1.3 本章总结
本章实现了一个简单的区块链,这个区块链只实现了区块和区块链的创建、工作量证明算法等,其实,区块链里还有其他重要的部分并没有在这里实现,比如 P2P 通信、UTXO 等。大家可以自行参考网上的资料对本章所实现的区块链进行后续功能的扩展。
参考资料:
Implementing proof-of-work
https://www.savjee.be/2017/09/Implementing-proof-of-work-javascript-blockchain/
Learn Blockchains by Building One
https://hackernoon.com/learn-blockchains-by-building-one-117428612f46
Building Blockchain in Go
https://jeiwan.cc/posts/building-blockchain-in-go-part-2/
Bitcoin whitepaper
https://bitcoin.org/bitcoin.pdf
图1-9 区块链信息