《Nodejs开发加密货币》之二十三:区块链
前言
亿书是一款加密货币产品。用时尚化的语言来说的话,则是一款实用的区块链产品。那么,在区块链是什么?有哪些特点?最近,“以太坊硬分叉事件”给我们带来了很多启示,“能不能彻底杜绝区块链分叉行为”成为一个值得深思的问题。在本章中,则通过仔细阅读并理解亿书相关代码逻辑来详细阐述并说明这些问题。
源码
blocks.js https://github.com/Ebookcoin/ebookcoin/blob/v0.1.3/modules/blocks.js
block.js https://github.com/Ebookcoin/ebookcoin/blob/logic/block.js
loader.js https://github.com/Ebookcoin/ebookcoin/blob/v0.1.3/modules/loader.js
类图

流程图

解读
1.区块链是什么?
(1)基本概念
简述区块链技术的核心概念是什么?以产品设计为例,在像 Google Earth 这样的应用中,AJAX 并非一种全新的技术(虽然 AJAX 并非一种全新的技术),但通过与其他技术结合(如 HTML5, CSS3, JavaScript 等),最终形成了像 Google Earth 这样的成功应用。同样地,在区块链领域中,并非一种全新的技术(同样地,并非一种全新的技术),但与加密解密技术和 P2P 网络等结合在一起(就像 AJAX 一样诞生于互联网发展时期),创造出了比特币等数字货币系统。技术人员尤其是 Web 开发工程师们最早受到 AJAX 技术的影响是由于其独特效果(最初受到其独特效果所吸引)。如今这一情形再次上演——随着比特币市场狂热氛围的浓厚(如今这一情形再次上演——随着比特币市场狂热氛围的浓厚),越来越多的人开始关注其背后的技术支撑——即区块链。
作为一种专用的数据存储系统,
区块链采用独特的数据结构与组织形式来记录海量交易记录。
各记录之间通过前后有序链接形成网状结构,
确保了公开透明性的同时实现了不可篡改性,
并且便于追踪溯源。
不仅限于单一的交易行为,
而是涵盖多种具有价值属性且归属明确的数字资产。
因此可以简化理解为一种去中心化的公共账本。
既可以保存为独立文件,
也可以通过数据库技术实现,
例如比特币就采用了Google的LevelDB数据库技术作为其底层存储方案。
相较于加密货币而言,在区块链这一名称中已经舍弃了代币这一概念,并且更具形象性地具备技术属性的同时也去掉了明显的政治色彩;更适合将其作为一门技术进行研究与推广。
(2)从数据库设计角度理解区块链
用数据库的概念去理解,则区块链相当于一张"相互引用"的数据库表。每个区块都由一条记录表示,在这条记录中包含着其之前的一条 record 的信息内容。由此可知从任何一条 record 开始都可以向前依次追踪回去直至第一条 record. 在常规的 self-reference 表结构中一般采用 ID 作为关联外键而加密货币则采用了经过加密处理后存储于字段中的信息数据这些数据字段不仅带有签名认证功能还能实现自我验证功能从而确保数据不会被篡改.
另一张与其紧密相连的关键表格就是交易表。由于加密货币具有高度活跃的交易属性,这些交易涵盖了多种类型包括加密货币债权股权以及版权等多种数字资产它们以独立表格的形式存在同时与区块链建立了多对一对应关系这样一来通过追踪特定区块我们可以轻松获取该区块所涉及的所有交易信息
上面涉及的是从数据库读取数据的角度来进行考量,在这种情况下较为直观。相反地,则是从数据存储的角度出发会更加引人深思。存储过程需要根据不同需求采用不同的编码方案,在之前的章节中提到过:加密货币的各种功能均可通过扩展交易类型来实现编码;而将一些现实中的合同规则进行编码处理,则会使得系统在特定条件下自动触发相应的交易操作(包括更新现有记录),这正是"智能合约"这一概念的基本内涵所在
我们已经在第一部分介绍了"智能合约"的概念,在这次讨论中再次提及这一概念。作为程序员来说能够更加直接地理解其技术实现的可能性。"智能合约"如今已成为加密货币领域的一个热门话题,并且具有广阔的应用前景。它可以延伸至现实世界的多个应用场景:例如自动贩卖机与销售终端的大规模应用;大型企业之间的电子数据交换;银行间用于转移与清算的支付网络;以及音乐、电影和其他数字版权交易等多个领域。
(3)形象化理解区块链
人们常常将具有时间先后顺序的数据结构采用栈的形式进行表示。比特币白皮书对这种结构进行了形象化的展示:首区块被设为栈底位置;随后的区块则按照时间顺序依次叠加在其上;这样一来;各区块与首区块之间的距离即代表了"高度";"顶端"则象征着最新添加的那笔交易数据。每个区块实际上包含了大量交易信息;这些信息存储在对应的栈中;因此我们可以说:这个系统类比于一个大型橱柜;其中每一个抽屉都代表着一个特定的交易批次;而抽屉内部则是充满了各种类型的信息条目。

(4)区块链分叉
物理分叉 。每个新区块与其前驱块之间存在关联关系,在此基础之上对其后续产生的子块并无约束限制。顶端新区块必定了解其父块已存在于主链上(因为这些数据已经被记录完毕),但却无法预知后续产生的子块(或许还未出现或者正处于传输过程中)。从硬件层面来说,在同一时间段内多新区块能够同时定位到主链上的父节点是非常常见的情况。这种情况必然会导致区块链向多个方向发展并存的现象出现——这在同一软件版本及其兼容版本环境下发生且无需任何人工干预操作即可发生。这一现象被称作物理分叉
可以看出外部环境对物理分叉的影响至关重要,并不涉及任何共识机制无论是采用工作量证明机制(PoW)如比特币以及采用股权证明机制(PoS)如Point native这样的项目在这里也采用了授权股权证明机制(DPos)如亿书币都遵循这一原则为了避免区块链上出现多个独立的发展方向解决分叉问题的一个常见方法是允许每个分叉继续扩展这样它们会在下一步就会产生差异此时系统会选择长度最长的那个分支作为主链为此在具体的系统设计和开发过程中这一问题确实会增加一定的复杂性
人为分叉的现象通常发生在软件升级时。这就会导致旧版本与新版本之间可能出现分叉。我们知道这种现象极为常见。我们都知道像微软window系统时不时都会发布漏洞修复提醒的情况很常见。而需求不断变化是我们必须不断更新软件的功能以适应这些变化的原因之一。因此在这种情况下旧版本与新版本之间出现兼容性问题或者需要人工改变区块链的发展方向就不足为奇了这就是人为分叉的原因所在
不可否认的是,在加密货币的发展过程中,人为因素导致分叉现象不可避免。
硬分支与软支解 。两者均属人为设计的分支类型,在这一领域内存在较大程度上的分歧问题。最初区分这两种分支的方法较为简单,“硬支解”通常指在旧版本兼容性较低但获得广泛共识的新分支,“软支解”则相反。“现在只要出现明确的‘支解行为’都会引起各方共同协商的结果。”这里的“一致意见”指的是经过投票或其他达成一致的方式获得超过90%的支持率。“例如最近以太坊为了应对The Dao遭受黑客攻击而实施了紧急性的hard split”,该行动得到了87%左右的支持率(接近90%,此处暂不涉及该事件本身的评价)。
就技术角度来看,在区块链领域所谓的"hard"术语主要体现在与旧系统之间存在较大的不兼容性(或仅有较小的兼容性),这种设计属于完全抛弃旧系统的行为。如果一个用户选择不升级现有的系统或软件,则会一直留在原有的生态系统中,并且会感觉这种状态下的系统管理更加难以处理。相比之下,“soft fork”的概念则最大限度地保留了与以前系统的兼容性设计,在这种情况下设计得当的话,则能够允许多个不同系统的并存运行状态,并且类似于常规的软件更新流程。有人认为"hard fork"可能带来的负面影响较大;另一些学者则指出"soft fork"的风险更高。实际上,在编码实现时"hard fork"的设计相对更为简单直接;而无论是"hard fork"还是"soft fork"的设计方案都值得深入研究和实践尝试;不过在编码实现时"soft fork"需要考虑更多细节因素(对用户的影响则较为隐晦)。
我不太喜欢谈论政治话题,但就作为一种广泛应用于经济活动的重要工具——加密货币而言,它天然上与政治有着千丝万缕的联系。其长期发展的内在动力正是各相关方之间不断博弈的结果:起初由开发者主导,随后矿工群体的力量愈发壮大,而后者的影响力逐渐变得不可忽视。正是这种多方力量之间的平衡关系,使得加密货币能够持续稳健地发展下去,而当这三种力量达到一种平衡状态时,就意味着这个技术体系已经相对成熟了。最近,以太坊网络在处理智能 contract 时遭遇大规模黑客攻击事件引起了整个加密货币社区的高度关注,各方对此褒贬不一——这是件好事,也凸显出该技术仍处于幼年期阶段,未来的发展道路任重而道远
2.区块链的特点
我们可以利用堆栈模型来理解数据结构,并通过自引用关联的方式构建数据库模型;然而这并不意味着就是区块链技术因为还需要引入加密解密功能并且必须嵌入到去中心化的网络中由P2P节点共同维护才能真正实现区块链的特点;值得注意的是也有其他人提出了不同的看法他们认为在中心化的应用中采用类似的数据结构不仅可以提高安全性(相较于传统中心化系统)还能避免分支问题并且可能带来更高的性能(与去中心化系统相比)但事实是失去了P2P网络基础的安全性就无法称得上性能上的提升;我们之前已经分析过P2P网络本身具有的天然安全优势即使单一节点遭受攻击或被破解也不会对整个系统的稳定性造成重大影响而仅仅依赖于单个节点的安全性在中心化系统中将显得脆弱;因此为了所谓的性能优化却牺牲了更好的安全性这种做法似乎得不偿失
汇总以上信息,区块链应该具备这样几个特点:
- 分布式存储方案采用;不管哪种类型的链(无论是公链、私链还是联盟链),都必须采用分布式存储基于某种机制实现区块链的同步与统一。
- 所有节点都拥有完整且独立的区块链副本;这一技术不依赖于加密方法提供无限制的数据访问能力;即便进行局部调整也无济于事。
- 这一特性源于巧妙地运用加密技术;每个新区块包含其上一个新区块的关键信息;防止任何形式的数据篡改。
- 这种特性便于追踪;从任何一个特定区块向前追溯即可查找到与之相关的所有交易。
- 这种现象是由多种因素决定的;人们无法从根本上消除它。
正是由于这一显著特点的存在,区块链概念因此迅速走红。经过实践检验,区块链技术不仅能够覆盖所有中心化的应用场景,而且在解决许多传统中心化应用无法处理的问题方面具有显著优势。具体而言,它能够有效应对诸如分布式财务管理问题、分布式存储问题等各类挑战。特别在金融领域中,资金清算流程以及审计管理等问题将通过区块链技术实现大幅简化和成本下降。与此同时,亿书平台正是通过利用区块链技术所具有的公开透明性和可追溯性特点,与数字出版产业深度结合,从而实现了自媒体与版权保护的无缝衔接,最终彻底解决了当前数字出版行业面临的版权保护不足这一痛点。
3.区块链开发应该解决的问题
在掌握区块链的基本概念及其运作机制后,便可以开始构建基础功能模块。从功能需求出发,在实际开发过程中需要重点关注以下几点:
(1)加载区块链。确保本地区块链合法,未被篡改。
- 保存创世区块
- 加载本地区块
- 验证本地区块
(2)处理新区块。加载后,该节点就可以处理网络中的交易了。
- 生成新区块;
- 统计收集交易信息,并记录到(关联到)新区块中;
- 将新产生的新区块正确地添加到主区块链网络中;
- 检测并处理可能出现的区块链分叉问题。
(3)同步区块链。确保本地区块链与网络中完整的区块链同步。
在接下来的内容中,在本节框架下,我们将深入探讨与数据库设计相关的代码实现细节,并对亿书区款链的运作机制进行详细分析。
4.亿书区块链数据库设计
亿书使用SQLite数据库,与区块链相关的数据库结构如图:

blocks表具有区块链特性特征标识符属性值存储功能 trs表则用于存储各种交易记录 forks_stat表则用于追踪系统分叉状态
5.亿书区块链实现
针对相关文献中提出的现有技术问题, 采用一种新的思路进行一一比对核查, 并对其实施详细的技术分析与评价.
(1)保存创世区块
创世区块会被预先设置到客户端程序中,并在运行时自动注入数据库中。这种方法的好处在于确保每个客户端都能拥有一个安全可靠的区块链基础。
// modules/blocks.js
// 78行
function Blocks(cb, scope) {
library = scope;
// 80行
genesisblock = library.genesisblock;
self = this;
self.__private = private;
private.attachApi();
// 85行
private.saveGenesisBlock(function (err) {
setImmediate(cb, err, self);
});
}
此模块中的构造函数在入口程序app.js中执行时会立即创建相应的模块实例。具体来说,在第85行处定义了private.saveGenesisBlock的方法,在入口程序执行时会立即调用此方法。若此操作之前已执行过一次,则private.saveGenesisBlock将返回并不再执行后续操作;若首次执行,则该方法将生成初始基础块,并随后调用了位于第266行处定义的方法private.saveBlock()。随后将包含交易记录的基础块信息存储到数据库中。
这一类的区块创建流程通常具有很强的代表性,并非偶然出现的情况
// genesisBlock.json 文件
{
"version": 0,
// 3行
"totalAmount": 10000000000000000,
"totalFee": 0,
"reward": 0,
"payloadHash": "1cedb278bd64b910c2d4b91339bc3747960b9e0acf4a7cda8ec217c558f429ad",
"timestamp": 0,
"numberOfTransactions": 103,
"payloadLength": 20326,
"previousBlock": null,
"generatorPublicKey": "b7b46c08c24d0f91df5387f84b068ec67b8bfff8f7f4762631894fce4aff6c75",
// 1757行
"height": 1,
"blockSignature": "2985d896becdb91c283cc2366c4a387a257b7d4751f995a81eae3aa705bc24fdb950c3afbed833e7d37a0a18074da461d68d74a3a223bc5f8e9c1fed2f3fec0e",
"id": "8593810399212843182",
// 12行。为了方便阅读,这里把关联的交易信息排版在最后位置
"transactions": [
{
"type": 0,
// 15行
"amount": 10000000000000000,
"fee": 0,
"timestamp": 0,
"recipientId": "6722322622037743544L",
"senderId": "5231662701023218905L",
"senderPublicKey": "b7b46c08c24d0f91df5387f84b068ec67b8bfff8f7f4762631894fce4aff6c75",
"signature": "aa413208c32d00b89895049ff21797048fa41c1b2ffc866900ffd97570f8d87e852c87074ed77c6b914f47449ba3f9d6dca99874d9f235ee4c1c83d1d81b6e07",
"id": "5534571359943011068"
},
{
"type": 2,
...
},
...
{
"type": 3,
...
}
...
]
}
这些字段,我们在上面的数据库表里已经列出,下面看看几个关键数据:
在创世区块中设定初始代币总量为一亿。
该创世区块的高度为一。
每个区块都必须包含至少三种类型的交易:转账、受托人和投票。
第一个转账交易将该区块内的全部代币转移至另一个账户。
这使得该方案在实际应用中具有较高的可行性。
另外两种类型是受托人协议和投票机制。
(2)加载本地区块
每个节点都需要先验证本地区块链数据以确保未被篡改。这个验证步骤属于软件初始化阶段的一部分,在实际开发中无需过多关注网络节点之间的连接问题。因此应放置在入口文件位置以便执行。我们已经在《入口程序app.js解读》一章对app.js进行了初步解析但较为简略仅概述了程序的整体运行流程这里我们再次强调特别关注本地区块链数据的验证问题并采用增量开发策略是合理且常见的做法
// app.js文件
...
ready: ['modules', 'bus', function (cb, scope) {
// 435行
scope.bus.message("bind", scope.modules);
cb();
}]
app.js文件 435行: 调用了特殊的'bind'事件(请参考开发实践中关于自定义事件处理机制的相关章节),导致所有模块中的'onBind()'方法被调用。在该方法运行之前,各个模块仅被创建实例并处于待机状态;因此'bind'事件是启动各模块的关键时刻,在各模块构造函数运行之后成为关键步骤(具体流程,请参考本章第一节中关于模块加载流程图的内容)。绝大多数模块中的'onBind()'方法主要用于初始化特定变量;然而唯一例外的是loader.js文件所执行的具体代码如下所示:
// modules/loader.js文件
Loader.prototype.onBind = function (scope) {
modules = scope;
// 534行
private.loadBlockChain();
};
private.loadBlockChain() 这个函数用于加载区块链的信息,并且具体内容包括以下几点:
// modules/loader.js文件
private.loadBlockChain = function () {
var offset = 0, limit = library.config.loading.loadPerIteration;
var verify = library.config.loading.verifyOnLoading;
// 357行,闭包
function load(count) {
verify = true;
private.total = count;
library.logic.account.removeTables(function (err) {
if (err) {
throw err;
} else {
library.logic.account.createTables(function (err) {
if (err) {
throw err;
} else {
// 369行
async.until(
function () {
return count < offset;
}, function (cb) {
library.logger.info('Current ' + offset);
setImmediate(function () {
modules.blocks.loadBlocksOffset(limit, offset, verify, function (err, lastBlockOffset) {
if (err) {
return cb(err);
}
// 380行
offset = offset + limit;
private.loadingLastBlock = lastBlockOffset;
cb();
});
});
}, function (err) {
...
// 398行
library.logger.info('Blockchain ready');
library.bus.message('blockchainReady');
}
}
...
}
// 408行
library.logic.account.createTables(function (err) {
if (err) {
throw err;
} else {
library.dbLite.query("select count(*) from mem_accounts where blockId = (select id from blocks where numberOfTransactions > 0 order by height desc limit 1)", {'count': Number}, function (err, rows) {
...
var reject = !(rows[0].count);
modules.blocks.count(function (err, count) {
...
if (reject || verify || count == 1) {
// 428行
load(count);
} else {
// 其他情况,请查看源码
...
};
通过调用logic/account.js文件中的createTables()方法来生成一系列与用户相关的关联表。所有生成的表均以"mem_"前缀命名,并包含以下字段:mem_accounts、mem_accounts2contacts、mem_accounts2u_contacts、mem_accounts2delegates、mem_accounts2u_delegates、mem_accounts2multisignatures、mem_accounts2u_multisignatures以及专为记录会话而设计的mem_round表。这些信息主要涉及用户的关联数据...仅供本地节点使用。
当新客户端处于初始创建状态且创世区块首次插入时count等于1时,则会立即调用闭包load()函数(428行)来加载验证缺失的区块。我们暂不考虑其他复杂情况,在此聚焦于第357行定义的load()函数:它先是移除账号表(removeTables),随后重建(createTables),并借助‘async.until’方法(369行)循环加载区块链数据的具体步骤来自 modules/blocks.js中的 loadBlocksOffset() 函数。一旦 count < offset返回 true时,则循环终止;此时区块链数据完成同步,并触发blockchainReady事件(398行)。
在本系统中, Offset并非表示分页数据,而是指每次加载时所处理的数据块数量。采用分段加载策略的好处在于能够有效避免一次性导入整个区块链的数据,这不仅减少了单次数据请求的数量,还能显著降低计算系统的负载压力。其中最大值由变量 limit 决定(见第380行)。该变量 limit 的取值来源于 config.json 文件中的 global variable loading.loadPerIteration 设置。系统允许用户自定义这一行为:在运行程序时可通过命令行参数 --config 来指定使用的配置文件。以此实现个性化配置的目的。
// config.json文件
// 132行
"loading": {
"verifyOnLoading": false,
"loadPerIteration": 5000
}
(3)验证本地区块
在提及 modules/blocks.js 的 loadBlocksOffset() 方法时,在说明中指出该方法不仅负责处理加载操作,并且主要用来验证
// modules/blocks.js文件
Blocks.prototype.loadBlocksOffset = function (limit, offset, verify, cb) {
...
library.dbSequence.add(function (cb) {
library.dbLite.query("SELECT " +
...
async.eachSeries(blocks, function (block, cb) {
async.series([
function (cb) {
if (block.id != genesisblock.block.id) {
if (verify) {
// 627行 追溯区块
if (block.previousBlock != private.lastBlock.id) {
return cb({
message: "Can't verify previous block",
block: block
});
}
try {
// 635行 验证块签名
var valid = library.logic.block.verifySignature(block);
}
...
if (!valid) {
return cb({
message: "Can't verify signature",
block: block
});
}
// 650行 验证块时段(Slot)
modules.delegates.validateBlockSlot(block, function (err) {
...
}, function (cb) {
// 先给交易排序,让投票或签名交易排在前面
...
async.eachSeries(block.transactions, function (transaction, cb) {
if (verify) {
modules.accounts.setAccountAndGet({publicKey: transaction.senderPublicKey}, function (err, sender) {
...
if (verify && block.id != genesisblock.block.id) {
// 690行 验证交易
library.logic.transaction.verify(transaction, sender, function (err) {
...
});
} else {
setImmediate(cb);
}
}, function (err) {
if (err) {
// 如果出现错误,要回滚
async.eachSeries(transactions.reverse(), function (transaction, cb) {
async.series([
function (cb) {
modules.accounts.getAccount({publicKey: transaction.senderPublicKey}, function (err, sender) {
...
modules.transactions.undo(transaction, block, sender, cb);
});
}, function (cb) {
modules.transactions.undoUnconfirmed(transaction, cb);
}
...
};
逐个加载区块,并验证:
627行:追溯前一区块,无法追溯自然是不正确的。
通过验证区块签名来确保区块内容不受篡改。建议详细了解该行使用的签名验证方法"verifySignature()"(在"logic/block.js"文件第150行,请参考源码库),这通常是对二进制数据(此处为区块数据)进行签名验证的标准做法。一旦发现验证失败,则需立即停止整个循环,并删除当前以及后续的所有区块。
通过确认区块的时间段(Slot),可以防止该位置被篡改。这种做法实际上是在确认该区块的高度以及相关的时间戳。(具体内容请参见《关于时间戳及相关问题》一节)亿书网络遵循特定的时间间隔周期(详见《DPOS机制》一节)。每个区块都可以通过其高度计算出对应的出块时间段,并与该区块的时间戳相对应。从而锁定了该区块的位置;否则就会出现问题。选择确认时间段的原因在于:由于区块链系统可能出现分叉情况,在相同高度下可能存在多个不同的时间戳值;但任何一个确定的时段时间都只能对应一个唯一的位置值。
690行:验证交易。具体流程请参考《交易》一章的相关内容。
(4)创建新区块
从设计角度出发,在本节中将为实现这一目标提供一种可行的设计方案。在本节中将详细阐述反向阅读代码的具体操作步骤与注意事项:无需深入探究其具体实现细节即可直观地观察流程图理解其运行逻辑。遵循模块化设计的基本原则所有与区块链相关的处理逻辑都应该集中于'modules/blocks.js'文件中这样便能够轻松定位到生成新区块的方法'generateBlock()'并完成相关功能开发过程
// modules/blocks.js文件
// 1126行
Blocks.prototype.generateBlock = function (keypair, timestamp, cb) {
// 1127行 获取未确认交易,并再次验证,放入一个数组变量里备用
var transactions = modules.transactions.getUnconfirmedTransactionList();
var ready = [];
async.eachSeries(transactions, function (transaction, cb) {
...
ready.push(transaction);
...
}, function () {
try {
// 1147行
var block = library.logic.block.create({
keypair: keypair,
timestamp: timestamp,
previousBlock: private.lastBlock,
transactions: ready
});
} catch (e) {
return setImmediate(cb, e);
}
// 1157行
self.processBlock(block, true, cb);
});
};
在第1127行处:首先获取所有未确认交易记录,并对它们进行双重验证后存入一个名为"ready"的数组变量中作为备用。通过这里具体的处理方式可以看出,在这种情况下与之相关联的一类交易相比,在现有系统中(如亿书区块)并未实现像比特币那样复杂的处理流程。在比特币区块链系统中,所有的交易都被编码为二叉树结构(Merkle树)的形式,并且能够高效地完成存储、维护、查询和验证等操作。值得注意的是,在当前版本代码中这些功能尚未得到充分实现,在后续版本中将对其进行优化补充工作
将整理好的数据构建为块数据结构(具体形式上类似于前面所述的创世区块),由于是新建数据,在此场景下关键在于keypair、timestamp、previousBlock以及transactions这四个字段的信息。其余字段如totalFee、reward与payloadHash等,则将在第1157行定义的processBlock()方法中进行处理
这里的keypair、timestamp字段是该方法的参数,在调用的地方传入。我们通过审查代码发现,在位于'modules/delegates.js'文件中的位置调用了该方法,并将其命名为'loop'。具体实现细节如下:
// modules/delegates.js
// 492行
private.loop = function (cb) {
...
var currentSlot = slots.getSlotNumber();
var lastBlock = modules.blocks.getLastBlock();
...
// 511行
private.getBlockSlotData(currentSlot, lastBlock.height + 1, function (err, currentBlockData) {
...
library.sequence.add(function (cb) {
if (slots.getSlotNumber(currentBlockData.time) == slots.getSlotNumber()) {
// 519行
modules.blocks.generateBlock(currentBlockData.keypair, currentBlockData.time, function (err) {
...
});
...
};
我们来看一下"loop"方法,在实际应用中与"modules/delegates.js"模块相关联的具体调用是什么样的。该方法位于第470行的位置,并且从其名称便可推测出其主要功能——获取区块时段数据以提供密钥对和时间戳。由于该方法被标记为私有函数,在设计意图上必然与受托人角色相关联。基于此,请让我们来深入了解一下:
// modules/delegates.js
// 470行
private.getBlockSlotData = function (slot, height, cb) {
self.generateDelegateList(height, function (err, activeDelegates) {
...
for (; currentSlot < lastSlot; currentSlot += 1) {
var delegate_pos = currentSlot % constants.delegates;
var delegate_id = activeDelegates[delegate_pos];
if (delegate_id && private.keypairs[delegate_id]) {
return cb(null, {time: slots.getSlotTime(currentSlot), keypair: private.keypairs[delegate_id]});
}
}
cb(null, null);
});
};
通过查看代码可以看到, 该方法首先依次获得了能够生产的区块所涉及的所有受托人列表. 接着根据当前时间段的信息确定了处于激活状态的受托人标识, 进而确定对应的密钥对. 由此可知, 这个密钥对与特定时间段相关的具体参与者有关.
接下来,“private.loop()”这个方法能够运行以实现新建区块的操作任务。因此,在继续深入搜索代码的过程中,“我们通过轻松地查找代码即可完成这一任务。”
// modules/delegates.js
// 735行
Delegates.prototype.onBlockchainReady = function () {
private.loaded = true;
private.loadMyDelegates(function nextLoop(err) {
// 743行
private.loop(function () {
setTimeout(nextLoop, 1000);
});
...
这是一个针对区块链就绪事件的方法,在具体实现过程中发现:当当前区块链加载完成时(对应代码第744行),该方法会被调用;由此可知,在区块链验证完成之后(即新区块生成条件满足),系统将开始创建新区块;为实现这一功能目标,在代码中已经配置了一个每秒(即1000毫秒)触发一次"private.loop()"的操作循环;然而由于slot时间限制的存在,在实际运行过程中每10秒仅能生成一个新区块。
在我们的分析中发现,在执行缺失区块同步操作之前存在一个更新节点信息的过程。这样一来,在整个区块链系统已加载完毕的情况下(即此时整个区块链系统已加载完毕),节点先完成新区块的生成要比完成同步缺失区块的操作提前完成(即这样就能使节点优先生成新区块,并且最大限度地服务亿书网络)。然而这可能使同步区块操作变得更加复杂(即这可能使同步区块操作变得更加复杂)。
(5)产生区块链分叉
从编码层面来看,在什么情况下会产生分支?通常情况下,在将新区块写入区块链的过程中会发生分叉现象。具体来说,在modules/blocks.js文件中的第1157行处“processBlock()”函数被调用时会触发这一过程。该函数被调用的位置包括第1174行所在的“onReceiveBlock()函数中以及第1084行所在的“loadBlocksFromPeer()函数中。“值得注意的是,在这些函数中进行操作时,“processBlock()``负责生成新的区块,并在此过程中完成相关验证工作。”
该方法很长,但并不复杂,下面仅仅粘贴与分叉相关的源码,如下:
// modules/blocks.js文件
// 800行
Blocks.prototype.processBlock = function (block, broadcast, cb) {
...
if (block.previousBlock != private.lastBlock.id) {
// 859行 高度相同,父块不同
modules.delegates.fork(block, 1);
return done("Can't verify previous block: " + block.id);
}
//
if (err) {
// 877行 受托人时段不同
modules.delegates.fork(block, 3);
return done("Can't verify slot: " + block.id);
}
if (tId) {
// 910行 交易已经存在
modules.delegates.fork(block, 2);
setImmediate(cb, "Transaction already exists: " + transaction.id);
}
};
上面列出了3种,还有一种:
// modules/blocks.js文件
// 1166行
Blocks.prototype.onReceiveBlock = function (block) {
...
if (block.previousBlock == private.lastBlock.previousBlock && block.height == private.lastBlock.height && block.id != private.lastBlock.id) {
// 1181行 高度和父块相同,但块ID不同
modules.delegates.fork(block, 4);
cb("Fork");
}
...
实际上,在代码中进行分叉操作时,“modules.delegates.fork()”这个方法其实很简单——它仅仅是为了将相关数据同步到主数据库中的forks_stat表上而已。值得注意的是,“上面罗列了四种不同的分叉情况”,它们的具体原因是什么呢?
- 859行:处于同一高度状态的两个区块却属于不同的父节点。这表明可能存在父节点验证的问题;
- 910行:已存在一条与之相同的交易记录。这种情况通常由用户重复提交交易引起,“双花攻击”即是其典型表现形式;
- 877行:受托方时段不一致导致了时段验证失误;由于block的时间字段与time stamp相关联的原因可知,在此情况下可能存在时间处理不当的问题;
- 1166行:处于同一高度且拥有相同的父节点的两个区块却具有不同的ID值。当接收到的区块的高度比最新区块高超过1,并且其父节点为最新区块时才能正常存储于区块链中;否则只能存储于分叉中。
其中因为ID信息是通过sha256算法对各区块进行哈希编码得到的结果,
(6)同步区块链,并解决分叉
如前所述,在完成验证本地块的加载过程后(见modules/loader.js第398行),系统会触发"blockchainReady"事件(该事件被触发)。随后,在各个模块内会依次执行对应的"onBlockchainReady()"方法。通过查看各个模块内的对应"onBlockchainReady()"方法(该操作),我们可以清晰地了解程序接下来将执行的操作是什么。
在这里,我们研究如何从其他节点同步区块链,并关注节点模块中对应的实现方法。在《一个精巧的P2P网络实现》一章中已经介绍了"onBlockchainReady()"方法的具体代码实现,在此不再赘述相关内容。请查看对应源码中的第364行,在更新了相关节点后触发了"peerReady"事件这一关键操作才是我们重点探讨的内容。随后,请返回modules/loader.js文件中,在该文件中同样定义了"peerReady"事件,请参考以下代码:
// modules/loader.js
// 492行
Loader.prototype.onPeerReady = function () {
setImmediate(function nextLoadBlock() {
...
// 499行
private.loadBlocks(lastBlock, cb);
...
});
setImmediate(function nextLoadUnconfirmedTransactions() {
...
// 514行
private.loadUnconfirmedTransactions(function (err) {
...
});
setImmediate(function nextLoadSignatures() {
...
// 523行
private.loadSignatures(function (err) {
...
});
};
该事件采用的方法主要包含三个子步骤:利用'private.loadBlocks()'同步各个区块的数据(共499行),处理未确认交易以及完成交易的签名验证工作。鉴于篇幅限制原因,在本节中我们仅对'private.loadBlocks()'这一子步骤进行详细分析;其余两个相关逻辑的具体实现细节,请参考完整代码
// modules/loader.js
// 225行
private.loadBlocks = function (lastBlock, cb) {
// 226行
modules.transport.getFromRandomPeer({
api: '/height',
method: 'GET'
}, function (err, data) {
var peerStr = data && data.peer ? ip.fromLong(data.peer.ip) + ":" + data.peer.port : 'unknown';
...
if (bignum(modules.blocks.getLastBlock().height).lt(data.body.height)) {
...
if (lastBlock.id != private.genesisBlock.block.id) {
// 259行
private.findUpdate(lastBlock, data.peer, cb);
} else { // Have to load full db
// 261行
private.loadFullDb(data.peer, cb);
}
...
});
};
在第226行中,“transport.getFromRandomPeer()”这一方法已经在《一个精巧的P2P网络实现》一章中进行了详细讨论,请无需进一步详细说明此处的内容)。该方法通过随机选择节点,并调用本模块提供的接口获取远程节点的height数据;在保证本地区块链高度低于远程节点区块链高度的前提下进行操作:如果当前本地处于创世区块状态,则将整个本地数据库复制到远程节点处(使用private.loadFullDb()方法);否则则仅更新本地缺少的高度块(通过private.findUpdate()方法)。其中前者属于后者的特殊情况之一,请集中分析后者即可。代码如下:
// modules/loader.js
// 75行
private.findUpdate = function (lastBlock, peer, cb) {
...
// 80行 获得正常块
modules.blocks.getCommonBlock(peer, lastBlock.height, function (err, commonBlock) {
...
var toRemove = lastBlock.height - commonBlock.height;
if (toRemove > 1010) {
// 89行 该节点的分支太长,限制从该节点同步数据(1小时)
library.logger.log("long fork, ban 60 min", peerStr);
modules.peer.state(peer.ip, peer.port, 0, 3600);
return cb();
}
// 暂存未确认的交易
var overTransactionList = [];
modules.transactions.undoUnconfirmedList(function (err, unconfirmedList) {
...
async.series([
function (cb) {
if (commonBlock.id != lastBlock.id) {
// 还能反向循环
modules.round.directionSwap('backward', lastBlock, cb);
} else {
cb();
}
},
function (cb) {
// 这里处理侧链
library.bus.message('deleteBlocksBefore', commonBlock);
// 117行 删除正常块之前的块
modules.blocks.deleteBlocksBefore(commonBlock, cb);
},
...
function (cb) {
// 129行
modules.blocks.loadBlocksFromPeer(peer, commonBlock.id, function (err, lastValidBlock) {
...
这种方法具有一定较高的复杂度,在处理过程中将所有异常分叉的数据删除以避免潜在的问题。这种方法具有一定较高的复杂度,在处理过程中将所有异常分叉的数据删除以避免潜在的问题。这种方法具有一定较高的复杂度,在处理过程中将所有异常分叉的数据删除以避免潜在的问题。
为了深入理解这一点,请注意真实存在的数据库存储机制与前述描述存在本质差异。具体而言,在实际操作中,默认情况下默认值会被系统进行排序并以特定格式呈现。通常情况下,在正常操作下,默认情况下的区块与分叉节点会按照时间顺序进行组织,并以杂乱无章的形式存在。然而,在我们设计的理想状态下——即通过编码实现了这一理想状态——这种混乱将得到彻底消除或被系统自动处理完毕。
总结
本文从技术层面探讨了区块链的核心概念,并借助源代码对亿书 blockchain的具体实现进行了深入分析与阐述解读。该方法对于读者更好地理解和掌握 blockchain 技术具有重要的指导意义。
值得注意的是,在现实世界中,“理想总是超越现实的极限”。然而,“只有更好”的理念却无法达到最佳状态。“因此,在人类社会经济领域中涉及 blockchain 技术的应用绝非单纯的技术创新问题。”
这一部分内容较为复杂,在区块链链分术(Chain Split)处理方面以及与交易关联性上仍需进一步优化工作。我们将在后续版本中对其进行全面改进工作以提升整体性能水平。值得注意的是受托人机制模块(modules/blocks.js)的核心功能涉及受托人协议的内容因此建议各位读者深入学习该部分内容以便更好地理解DPOS共识机制的基础运作原理为此我们推荐您阅读下篇文章——《DPOS共识机制》
链接
本系列文章即时更新,若要掌握最新内容,请关注下面的链接
本源文地址: https://github.com/imfly/bitcoin-on-nodejs
亿书白皮书: http://ebookchain.org/ebookchain.pdf
亿书官网: http://ebookchain.org
亿书官方QQ群:185046161(亿书完全开源开放,欢迎各界小伙伴参与)
参考
关于比特币硬分叉和软分叉的争议
