300 行 ABAP 代码实现一个最简单的区块链原型

不知从什么时候起,区块链在网上一下子就火了。

这里 Jerry 就不班门弄斧了,网上有太多的区块链介绍文章。我的这篇文章没有任何高大上的术语,就是 300 行 ABAP 代码,实现一个最简单的区块链原型。
我个人觉得,同区块链本身的实现技术相比,更难的事情是如何找到一个合适的业务场景,把区块链集成到 SAP 产品中去,让它发挥出作用。
这篇文章包含三个版本,每个版本在前一版本基础上增添了一些新的功能。
版本 1:区块和链这两个数据结构的实现
区块链,顾名思义,由区块组成的一条链。
下图和我们在大学计算机专业课《数据结构》里学到的单链表很像。在这个版本里,每个区块包含了最基本的字段:块索引,块的创建时间戳,当前块的哈希值(hash)和前一块的哈希值。每个区块的 pHash 字段存储了前一块的哈希值,这样就构成了一个链表。链表的第一个节点,就是下图最左边红色抬头的区块为创世块,其索引为 0,pHash 字段为空。

区块的 ABAP 实现:ZCL_BLOCK。上图所示的字段都建模在这个类里,出于简单起见,大部分字段设置为 public。

每个区块的哈希值是由该区块所有其他字段的值作为输入,通过 SHA1 算法计算出来,存储到字段 mv_hash 里。

链的实现:ZCL_BLOCKCHAIN

ADD_BLOCK: 接受一个区块的实例作为输入参数,将该实例的 pHash 指向当前链表尾部的区块,这样该实例成为链表新的尾部区块。
CONSTRUCTOR: 构造函数,执行链的初始化操作,创建创世块区块。
GET_BLOCK_BY_INDEX: 根据索引访问指定的区块。
GET_SIZE: 返回链里包含的区块数量。
IS_VALID: 检查该区块链是否有效。具体检查逻辑在后续介绍。
第一版的所有代码在我的github上。
执行测试程序 ZBLOCKCHAIN_V1,输入您想创建的区块个数,会看到如下输出:(我选择的个数是 5)

因为 SAP GUI 没有链表的 UI 控件,因此我用树控件模拟。
下图第 21 行,我把区块链里索引为 1 的区块内容篡改为"Change by Jerry", 然后再执行第 23 行的 is_valid 方法进行检查:

因为第 21 行 set_data 方法修改了第一个区块的内容后,会触发其哈希值的重新计算。这样第一个区块的哈希发生了变化(假设从 YYY 变到了 CCC),而第二个区块的 pHash 仍然指向第一个区块变化之前的旧哈希值 YYY,因此这个区块链被判定为无效。


上图的输出来自校验方法 is_valid: 遍历链里每个区块,比较区块里存储前一区块哈希值的字段 pHash 和位于该区块前一个位置的区块的哈希值是否一致。

版本 2:增加挖矿成本,增加对工作量证明(Proof of Work,缩写为 POW)的支持
第二版代码的地址在我的github上。
这一版的链实现类 ZCL_BLOCKCHAIN_V2 的构造函数增加了一个输入参数:iv_difficulty。这个参数有什么用?
仔细观察第一版测试程序的树状输出,可以看到每个区块的哈希值没有任何规律。而第二版的这个输入参数就是为了提高哈希值的计算成本,即只有当计算出来的哈希值满足一定规则时,该哈希值才能被区块链所接受。参数 iv_difficulty 定义了能够被接受的哈希值的前导零个数。

例如我指定前导零个数为 3:

执行结果:能看到所有的哈希值的前三位都为零。
这里有两个问题:
下图最后一列 Nonce 的含义是什么?
iv_difficulty 这个参数是如何参与哈希值计算的呢?

首先在区块的实现类 ZCL_BLOCK 里增加了一个新的成员字段 mv_nonce:

在将一个区块实例添加到链里的方法 add_block 里,增加了一个方法 mine。

这个方法里是一个循环,在循环体内计算出一个哈希值,然后检查其是否包含指定位数的前导零。如果没有,将 mv_nonce 加 1,然后继续循环。mv_nonce 也会作为输入的一部分参与哈希计算。也就是说,最终区块字段 mv_nonce 的值代表了代表了在得到符合前导零位数要求的合法哈希值之前,一共经过了多少次计算。而通过在循环里不断尝试最终得到一个合法的哈希值的这一过程,就是区块链圈内俗称的“挖矿”。

在我的测试系统里,创建 10 个区块,前导零个数为 4,总共花费了 10 秒钟。

从这个花费的时间能体会出,POW 其实是一种机制,通过引入需要一定工作量的哈希计算来避免区块链被垃圾填充或者区块内容被篡改。
版本 3:使用区块链记录交易明细,增加挖矿奖励
这一版的源代码:
https://github.com/i042416/KnowlegeRepository/tree/master/ABAP/blockchain/v3
使用 ZCL_TRANSACTION 来代表一笔交易,包含三个字段:mv_from_address(支付方),mv_to_address(收款方)和 mv_amount(交易金额)。

在这一版本里,首先被增强的是 ZCL_BLOCK3: 去掉了前两个版本使用的 mv_index 和 mv_data 字段,增加了一个字段 mt_transaction, 存储的是交易的集合。

在计算哈希值时,交易类的每一个字段也要参与计算:

类 ZCL_BLOCKCHAIN_V3 增加了一个新的成员变量 mt_pending_trans。每次调用方法 create_transaction,并不会创建一个新的区块用于记录该条交易,只是简单地把该条交易添加到待处理任务队列 mt_pending_trans 里。

字段 mv_mine_reward 存储了挖矿的奖励,硬编码成 100。

这个待处理任务队列仅当方法 mine_pending_trans 被调用时才会得到处理。
第 6 行的区块实例的 mine 方法调用之后,计算出一个符合前导零规范的哈希值。接着待处理任务队列被清空,然后一个新的交易记录在第 13 行被创建出来,作为挖矿的奖励,奖励方的账号信息由输入参数 iv_award_address 定义。

既然现在交易信息存储在了每个区块里,那么简单遍历这些区块,就能得出某个账号最后的余额是多少。采用的逻辑是,遍历每个区块记录的每笔交易,如果某帐号出现在交易记录的支付方,则余额减去当前这笔交易的交易金额(第 5 行), 反之账号如果出现在发送方,则余额加上交易金额(第 9 行)。

测试程序如下。因为 Tom 转了 100 元给 Jerry,Jerry 又转了 10 元给 Tom,然后第 15 行挖矿设置的奖励账号是 Jerry,故最后 Jerry 的余额是 100-10+100 = 190 元。

希望您读完本文之后,对区块链的工作原理有一个最基本的认识。感谢阅读。
版权声明: 本文为 InfoQ 作者【Jerry Wang】的原创文章。
原文链接:【http://xie.infoq.cn/article/4e066ac694d44034bd97a8d95】。文章转载请联系作者。
评论