在上一篇文章 Asch 源码 core 模块之 loader 启动 已经基本上把 Asch 服务启动过程梳理清楚了
本文开始阅读 ./src/core/blocks.js , 希望在此之前已经阅读过 Asch 源码 core 模块之 loader 启动
源码概况
照旧先把主要功能函数框架罗列, 如下:
- event:
- onReceiveBlock
- onReceivePropose
- onReceiveVotes
- onBind
- public:
- Blocks Constructor
- getCommonBlock
- count
- getBlock
- loadBlocksData
- loadBlocksPart
- loadBlocksOffset
- setLastBlock
- getLastBlock
- verifyBlock
- verifyBlockVotes
- applyBlock
- processBlock
- simpleDeleteAfterBlock
- parseBlock
- loadBlocksFromPeer
- deleteBlocksBefore
- generateBlock
- sandboxApi
- getSupply
- getCirculatingSupply
- cleanup
- private:
- attachApi
- saveGenesisBlock
- deleteBlock
- list
- getByField
- saveBlock
- popLastBlock
- getIdSequence
- getIdSequence2
- readDbRows
- applyTransaction
- shared:
- getBlock
- getBlocks
- getFullBlock
- getHeight
- getFee
- getMilestone
- getReward
- getSupply
- getStatus
源码详解
还是照旧从事件触发函数入手
onReceiveBlock
接受新区块的时候, 会有以下几种情况发生:
新区块的父区块就是我们当前区块, 并且高度也是我们当前高度 + 1, 说明新区块就是我们要的, 则调用 processBlock 对这个区块进行后续操作
新区块高度是当前高度 + 1, 但是父区块不是我们的当前区块, 说明发生了分叉, 则调用 module.delegates.fork 进行分叉记录
新区块的父区块是当前区块的父区块, 高度也是当前高度, 说明发生了分叉, 这个新区块就是当前区块的兄弟区块, 也是调用 module.delegates.fork 进行分叉记录, 记录的分叉原因和上一个不同
新区块的高度大于当前高度 + 1, 则说明当前节点的区块高度不够, 则调用 module.loader.startSyncBlocks 进行区块同步
其他情况, 则不作为
onReceivePropose
在 Asch 源码 base 模块基础之共识 里面说过, 受托人在产块的时候, 需要先提出 propose 去向其他节点收集投票, 收到足够的票才算是达成了共识, 才能正常产块
所以这里的 onReceivePropose 就是共识达成的过程之一
onReceivePropose 先会做如下异常检查:
如果新的 propose 高度和当前 propose 高度一致, 但是 id 不一致, 则打出 warn 日志
新 propose 高度不等于当前 propose 高度 + 1, 则认为是无效
如果新 propose 高度大于当前 propose 高度 + 1, 认为是无效的同时, 还会调用 modules.loader.startSyncBlocks 开始同步区块
如果最新一次 vote 的时间 (这里的 vote 就是对 propose 的投票) 距离现在在 5 秒之内, 则认为 propse 过于频繁, 忽略之
如果以上检查都通过, 则开始走下面流程:
bus.message(newPropose ) 发出新 propose 来临的事件
modules.delegates.validateProposeSlot 验证此 propose slot 是否有效
base.consensus.acceptPropose 接受这个新 propose
modules.delegates.getActiveDelegateKeypairs 获取本节点的受托人密钥对
base.consensus.createVotes 使用本节点的受托人密钥对创建对 propose 的投票
然后把 vote 发送给 propose 发起者
这里的 votes 和用户在钱包对受托人进行投票不是同一个含义 这个需要区分清楚, 如果对 propose 和 vote 还不是特别清楚的话, 建议可以再阅读一遍 Asch 源码 base 模块基础之共识
onReceiveVotes
base.consensus.addPendingVotes 把收到的票先暂存起来
base.consensus.hasEnoughVotes 判断目前收到的票是否已经足够
如果已经有足够的票了, 则 base.consensus.getPendingBlock 把之前暂存的 block 再拿出来, 开始进行区块处理 processBlock
所以其实这个过程在 Asch 源码 base 模块基础之共识 已经阐述过了
onBind
打酱油而已, 基本上等于没干啥
上面就是事件触发函数的介绍, 下面开始介绍 public 函数
Constructor
构造函数基本上也是打酱油
getCommonBlock
getCommonBlock 就是在之前的 Asch 源码 core 模块之 loader 启动 提到过的, 当本节点和 peer 节点同步区块时调用的
主要做的就是轮询 peer 节点, 通过网络交互最后找到和本节点最近的才 common block
count
就一句 sql:
- select count(rowid) from blocks
- getBlock
public 的 getBlock 其实就是 shared.getBlock 的封装, shared 相关的函数后面会一起讲
loadBlocksData & loadBlocksPart & loadBlocksOffset
通过拼 sql 读取数据库 block 信息,
setLastBlock & getLastBlock
设置, 读取最新区块
verifyBlock & verifyBlockVotes
验证区块, 比较繁琐, 不细讲
processBlock
processBlock 主要有以下步骤:
base.block.sortTransactions 排序区块的交易列表
verifyBlock 验证区块
dbLite 中查询该 block 信息, 如果 block 已存在则报错返回
modules.delegates.validateBlockSlot 验证区块 slot, 如果验证没通过, 则记录这次是分叉, 并报错返回
从 dbLite 中查询该交易信息, 如果交易已存在则报错返回
base.transactions.verify 验证该交易
applyBlock 生效该区块 (接下来谈谈 applyBlock )
applyBlock
modules.transactions.getUnconfirmedTransactionList 获取待确认交易列表
modules.transactions.undoUnconfirmedList 撤销当前的待确认交易列表(只是暂时的全部撤销, 后面会 redo 回来)
library.dbLite.query(SAVEPOINT applyblock); 先存一个可以回滚的点
遍历 block 里排序好的交易列表
创建和获取交易发送者的账号
apply 该交易
如果 saveBlock 参数为 true, 则 saveBlock,saveBlock 是把 block 和 transactions 都通过 sql 写入数据库
modules.round.tick 这个后面介绍 core/round 会讲
如果中间有 error 发生, 则让 db 回滚到第 3 步设置好的 savepoint
如果一切顺利, 最后还有如下收尾工作
setLastBlock 设置该区块为当前最新区块
oneoff.clear() 状态位重置
library.base.consensus.clearState 共识的状态清理
如果 broadcast 为 true 的话, 则发送 newBlock 事件
modules.transactions.receiveTransactions 最后把志之前第 1 步取得的未确认交易列表做一次过滤, 过滤出这次没有被 apply 进去的 transactions, 再重新 redo 它们一次
所以其实每次产生区块的函数调用过程是
- generateBlock -> processBlock -> applyBlock
- simpleDeleteAfterBlock
一句 sql 而已:
- DELETE FROM blocks WHERE height>= (SELECT height FROM blocks where id = $id
- deleteBlocksBefore
这里的 Before 有些歧义, 我的理解应该叫 After 吧, 就是仍然是删除某个 block 高度后面的区块, 从而降低区块高度 和 simpleDeleteAfterBlock 的区别在于, simpleDeleteAfterBlock 只操作了数据库 而 deleteBlocksBefore 是通过 popLastBlock 不停的 pop 出最新的区块
popLastBlock
popLastBlock 和简单的删除不同, 原因主要是在于, 在 po 最新区块的时候, 同时会回滚该区块的交易记录
loadBlocksFromPeer
从 peer 节点读取区块, 并且调用 processBlock 处理区块 这个函数也是在 loader 的定时同步区块的过程中被调用的功能函数之一
parseBlock
解析区块, 因为区块需要在 peer 中定期同步, 所以有对应的解析函数
generateBlock
generateBlock 主要是在受托人产块中被调用的功能函数
受托人产块过程其实已经在之前的文章 区块链开源项目 Asch 源码初探 和 Asch 源码 base 模块基础之共识 说过 是非常重要的一个过程 在此可以在过一遍整个流程:
获取当前待确认的交易列表(最多 N 个)
如果当前还有 pendingBlock 的话, 则说明还没有达成共识, 则停止这个产块流程至于关于达成共识这块还不是很了解的话, 可以先阅读 Asch 源码 base 模块基础之共识
遍历第 1 步获取到的待确认交易列表:
通过交易的 senderPublicKey 从 getAccount 中获取该用户信息
如果该交易已经 ready 了(对于多重签名的交易, 需要等到所有签名都到位才算 ready)
base.transactions.verify 验证该交易, 如果验证通过, 则放入 ready 数组
base.block.create 创建新的区块, 并且打包步骤 3 里面验证过的交易列表
verifyBlock 验证新创建的区块
获取本节点的受托人密钥对
base.consensus.createVotes 用这些密钥对创建 localVotes
base.consensus.hasEnoughVotes 检查创建的 votes 是否足够
如果 votes 足够, 则顺利产块, 如果不足够, 就需要 createPropose 去收集其他节点的 votes, 直到达成共识
getSupply & getCirculatingSupply
就是 blockStatus.calcSupply 的封装, 计算当前已供应的代币数量
cleanup
loaded 置为 false
- attachApi
- /api/blocks
- / (getBlocks)
- /get (getBlock)
- /full (getFullBlock)
/getHeight 同名 api 函数, 下同
- /getFee
- /getMilestone
- /getReward
- /getSupply
- /getStatus
attachApi 注册了一下 http api, 不过这些 api 基本上都是 shared 各个 api 的映射, 看函数名也基本上都知道了功能逻辑, 就不展开了 如果需要深究的时候再展开读读源码就知道了
总结
其实本文的内容大部分都是之前的文章涉及过的, 所以也算是比较好懂的模块 算是一次温故知新, 查缺补漏吧
来源: http://www.tuicool.com/articles/r2yQ3ue