Rust 实现一个简单的区块链
一、背景
近期用 Rust 实现了 Jeiwan/blockchain_go,与原项目相比没有加入新的功能,只是换了一个编程语言实现了一遍,源码放在 Github 上。
开发这个项目,花费了好几个周末,比较低效,需要反思。中途差点烂尾,被情绪影响,不知道做这件事的意义在哪里,有什么收益,还好坚持了下来。我很佩服原项目的作者,能够持之以恒将一个项目做得那么好,还有完整的文档讲解。循序渐进,代码配合文档,非常清晰易懂。换句话说,作者为了写一篇技术科普文章介绍区块链,捎带用代码写了一个演示案例 ————————— 代码是文档的注释
过去一年里,在学习摄影过程中,了解到美术的一个概念:
- 临摹:将别人的作品惟妙惟肖地画出来,这祌方法就是临摹。
- 写生:直接面对实物进行描绘就是写生。
- 创作:是对现实生活通过观察、体验、研究、分析、选择、加工和提炼后,塑造艺术形象的创造性劳动。
(图:https://www.laihuihua.com/K/article-1481.html)
临摹是学习、研究和掌握前人绘画经验的一个桥梁,可以使画者获得一定的绘画技巧,为写生奠定基础。写生是不断提高画者对客观事物的感受能力以及绘画技艺的唯嚙径,为创作积累生动的素材,是创作的必经之路。创作则集中体现了画家的绘画功底和综合艺术修养。临摹、写生和创作三者相互依存,互相促进,缺一不可。
我觉得这个概念同样适合于技术领域,甚至任何行业,以技术领域举例:
- 临摹:学习框架、开源项目的源码,临摹其中的优秀设计,将其用于实际项目。
- 写生:发现一个需求,根据过往的经验,输出技术方案,完成程序开发工作。
- 创作:推动技术创新,改变世界,比如:Google 大数据的三大论文。
好啦,前面啰嗦了那么多,究竟想干啥。其实就是临摹这个项目,同时提高学习内容留存率:
- 通过做实践项目,提高 Rust 编程能力。
- 学习区块链基本概念,理解 BTC 内部实现原理。
二、区块链概念
下面简要摘录下里面使用的名词术语:
-
区块
在区块链中,真正存储有效信息的是区块(block)。而在比特币中,真正有价值的信息就是交易(transaction)。实际上,交易信息是所有加密货币的价值所在。除此以外,区块还包含了一些技术实现的相关信息,比如版本,当前时间戳和前一个区块的哈希。
-
区块链
本质上,区块链就是一个有着特定结构的数据库,是一个有序,每一个块都连接到前一个块的链表。也就是说,区块按照插入的顺序进行存储,每个块都与前一个块相连。
-
工作量证明
区块链的一个关键点就是,一个人必须经过一系列困难的工作,才能将数据放入到区块链中。正是由于这种困难的工作,才保证了区块链的安全和一致。此外,完成这个工作的人,也会获得相应奖励(这也就是通过挖矿获得币)。
这个机制与生活现象非常类似:一个人必须通过努力工作,才能够获得回报或者奖励,用以支撑他们的生活。在区块链中,是通过网络中的参与者(矿工)不断的工作来支撑起了整个网络。矿工不断地向区块链中加入新块,然后获得相应的奖励。在这种机制的作用下,新生成的区块能够被安全地加入到区块链中,它维护了整个区块链数据库的稳定性。值得注意的是,完成了这个工作的人必须要证明这一点,即他必须要证明他的确完成了这些工作。整个 “努力工作并进行证明” 的机制,就叫做工作量证明(proof-of-work)。要想完成工作非常地不容易,因为这需要大量的计算能力:即便是高性能计算机,也无法在短时间内快速完成。另外,这个工作的困难度会随着时间不断增长,以保持每 10 分钟出 1 个新块的速度。
在比特币中,这个工作就是找到一个块的哈希
,同时这个哈希满足了一些必要条件。这个哈希,也就充当了证明的角色。因此,寻求证明(寻找有效哈希),就是矿工实际要做的事情。 -
交易
交易(transaction)是比特币的核心所在,而区块链唯一的目的,也正是为了能够安全可靠地存储交易。在区块链中,交易一旦被创建,就没有任何人能够再去修改或是删除它。
由于比特币采用的是 UTXO 模型,并非账户模型,并不直接存在“余额”这个概念,余额需要通过遍历整个交易历史得来。一笔交易由一些输入(input)和输出(output)组合而来。
-
奖励
挖矿奖励,实际上就是一笔
coinbase
交易。当一个挖矿节点开始挖出一个新块时,它会将交易从队列中取出,并在前面附加一笔coinbase
交易。coinbase
交易只有一个输出,里面包含了矿工的公钥哈希。 -
UTXO 集
Bitcoin Core 存储设计,是将区块存储在
blocks
数据库,将交易输出存储在chainstate
数据库。其中,chainstate
也就是未花费交易输出的集合,可以理解为blocks
数据库的未花费交易输出的索引,称之为UTXO
集。 -
P2PKH
在比特币中有一个 脚本(Script) 编程语言,它用于锁定交易输出;交易输入提供了解锁输出的数据。它所做的事情就是向一个公钥哈希支付,也就是说,用某一个公钥锁定一些币。这是比特币支付的核心:没有账户,没有资金转移;只有一个脚本检查提供的签名和公钥是否正确。
有了一个这样的脚本语言,实际上也可以让比特币成为一个智能合约平台:除了将一个单一的公钥转移资金,这个语言还使得一些其他的支付方案成为可能。
-
钱包地址
比特币地址是完全公开的,如果你想要给某个人发送币,只需要知道他的地址就可以了。但是,地址并不是用来证明你是一个“钱包”所有者的信物。实际上,所谓的地址,只不过是将公钥表示成人类可读的形式而已,因为原生的公钥人类很难阅读。在比特币中,你的身份(identity)就是一对(或者多对)保存在你的电脑(或者你能够获取到的地方)上的公钥(public key)和私钥(private key)。比特币基于一些加密算法的组合来创建这些密钥,并且保证了在这个世界上没有其他人能够取走你的币,除非拿到你的密钥。
-
数字签名
在数学和密码学中,有一个数字签名(digital signature)的概念,算法可以保证:
- 当数据从发送方传送到接收方时,数据不会被修改;
- 数据由某一确定的发送方创建;
- 发送方无法否认发送过数据这一事实。
通过在数据上应用签名算法(也就是对数据进行签名),你就可以得到一个签名,这个签名晚些时候会被验证。生成数字签名需要一个私钥,而验证签名需要一个公钥。签名有点类似于印章,比方说我做了一幅画,完了用印章一盖,就说明了这幅画是我的作品。给数据生成签名,就是给数据盖了章。
-
区块链网络
区块链特性可以认为是规则(rule),区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。
区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。节点就是一切:它既是一个客户端,也是一个服务器。这一点需要牢记于心,因为这与传统的网页应用非常不同。
区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在节点的世界中没有层级之分。
-
节点角色
尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:
- 矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。
- 全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。
- SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。
三、节点网络事件
注:下面是一个简化的网络模型,用于模拟多节点网络。笔者没有研究过 BTC 的P2P网络,不了解真实的P2P网络通讯过程。
在简化后的网络模型中,节点之间采用异步消息通信机制。将每个消息称为网络事件,梳理的事件机制如下:
四、项目介绍
源码地址:https://github.com/ZuoFuhong/blockchain_rust
前面提到,Rust 实现的版本,仅仅是编程语言不同于原项目,其它的逻辑均是一致的。如果您阅读过源码,还是会发现一丢丢的不同。
- 单元测试:Rust 编码有个很爽的点,就是可以在同一个源码文件中写单元测试,运行单元测试。不用像 Golang 那样需要在不同文件中切换。所以可以看到,项目源码中有着非常多的单元测试。
- 命令行程序:由于语言上的差异,命令行的实现差异较大,可以看到 Rust 的版本使用因语言特性,使用属性宏整体更加简洁。
- 区块链 db / 钱包文件:区块数据和钱包数据会持久化到磁盘上,相比原项目,Rust 的版本没有给文件打上端口号,减少源码阅读干扰。
五、开始游戏
我很乐于将 BTC 称之为游戏,每个节点有自己的身份角色,也可以切换角色。通过网络,大家遵守着同样的游戏规则。玩家穿行于虚拟和现实,在现实世界中赚取游戏中的虚拟货币,用游戏中的虚拟货币进行现实消费。Wow! That's totally awesome!
好啦,现在开始一场游戏吧 !
1.在同一台机器上,使用三个不同的端口,模拟三个节点:
其中:
- node1: 中心节点
- node2: 钱包节点
- node3: 矿工节点
2.在 node1 中心节点下创建一个钱包和一个新的区块链
生成一个仅包含创世块的区块链,并在其他节点使用。创世块承担了一条链标识符的角色(在 Bitcoin Core 中,创世块是硬编码的)
其中:
- 钱包地址:1QAjnwyGZL3woxUvzhdVePyjThtxQYpogL
- 创世块 hash:00e2b7601305ffe6ac39a7272324d42d3de2cf6f68129c9f555127c8ccb94b7f
3.接下来,在 node2 钱包节点生成一些钱包地址,我们称这些地址叫做:
- WALLET_1:1DB3GnPvCXEeLyzL9FxZHNzUz6C2eQFxvN
- WALLET_2:19g2ZZBiVCafgTQjUrEmJnwGgoc16nk1Yi
- WALLET_3:14A4DXBji2pnZRmXL97dVGVDe2ckuVcGGh
4.在 node1 中心节点,向钱包地址发一些币:
其中:
# 创世块矿工地址,向 WALLET_1 发 10 个币
$ blockchain_rust send ${CENTREAL_NODE} ${WALLET_1} 10 1
# 创世块矿工地址,向 WALLET_2 发 10 个币
$ blockchain_rust send ${CENTREAL_NODE} ${WALLET_2} 10 1
5.在 node1 中心节点,运行节点
$ blockchain_rust startnode
6.在 node2 钱包节点,运行节点
$ blockchain_rust startnode
它会从中心节点(node1)下载所有区块。为了检查一切正常,暂停节点运行并检查余额:
$ blockchain_rust getbalance ${WALLET_1}
Balance of 'WALLET_1': 10
$ blockchain_rust getbalance ${WALLET_2}
Balance of 'WALLET_2': 10
7.在 node3 矿工节点中生成一个钱包地址。初始化区块链:
# 将钱包地址,作为矿工钱包
$ blockchain_rust startnode ${MINER_WALLET}
其中:
- 钱包地址:1EwpTmQ941b1B7VMxzbVXvc8CrXR89dNXM
8.在 node2 钱包节点,发送一些币:
# WALLET_1 地址,向 WALLET_3 发 2 个币
$ blockchain_rust send ${WALLET_1} ${WALLET_3} 2 0
# WALLET_2 地址,向 WALLET_3 发 3 个币
$ blockchain_rust send ${WALLET_2} ${WALLET_3} 3 0
迅速切换到矿工节点(node3),你会看到挖出了一个新块!同时,检查中心节点(node1) 的输出。
9.切换到 node2 钱包节点并启动
它会下载最近挖出来的块!
暂停节点并检查余额:
Done !