一、背景

近期用 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)的概念,算法可以保证:

    1. 当数据从发送方传送到接收方时,数据不会被修改;
    2. 数据由某一确定的发送方创建;
    3. 发送方无法否认发送过数据这一事实。

    通过在数据上应用签名算法(也就是对数据进行签名),你就可以得到一个签名,这个签名晚些时候会被验证。生成数字签名需要一个私钥,而验证签名需要一个公钥。签名有点类似于印章,比方说我做了一幅画,完了用印章一盖,就说明了这幅画是我的作品。给数据生成签名,就是给数据盖了章。

  • 区块链网络

    区块链特性可以认为是规则(rule),区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。

    区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。节点就是一切:它既是一个客户端,也是一个服务器。这一点需要牢记于心,因为这与传统的网页应用非常不同。

    区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在节点的世界中没有层级之分。

  • 节点角色

    尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:

    1. 矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。
    2. 全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。
    3. 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_11DB3GnPvCXEeLyzL9FxZHNzUz6C2eQFxvN
  • WALLET_219g2ZZBiVCafgTQjUrEmJnwGgoc16nk1Yi
  • WALLET_314A4DXBji2pnZRmXL97dVGVDe2ckuVcGGh

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 !