概述
Libra 区块链是一个复制的状态机。每个验证器都是一个系统的副本。从状态 S0 开始,每笔交易 Ti 更新前一个状态 Si-1 到 Si。每一个 Si 实际是一个 map,映射了账户(以 32 字节地址表示)和该账户所关联的数据。
执行组件输入的交易是有序的交易,通过 Move 虚拟机计算每笔交易的输出,然后把输出结果应用到前一个状态,最后生成新的状态。执行组件使用 HotStuff 的领导者共识算法,从而在执行中达成一致的同意。这样一组的交易就构成了一个区块。与其他区块链系统不同,Libra 的区块除了包含一组交易之外没有任何意义。每个交易通过账本来定位和标识——那也称为“版本”。每个共识参与者构建一个块树,如下所示:
┌-- C
┌-- B <--┤
| └-- D
<--- A <--┤ (A 是最后提交的区块)
| ┌-- F <--- G
└-- E <--┤
└-- H
↓ 提交区块 E 之后
┌-- F <--- G
<--- A <--- E <--┤ (E 是最后提交的区块)
└-- H
一旦提交区块,区块里面的交易将会被排序。最后一个提交的区块和未提交的区块之间形成的路径,就相当于一条有效的“链”。无论共识算法的提交规则如何,树的操作可能有以下两种:
- 在树上添加一个区块,需要一个指定的父节点和一条用来扩展的链(例如,区块 F 是父节点,G 是扩展的区块)。当我们扩展新区块时,区块应该包含交易的正确执行结果,就好像它所有父辈那样子。但是,所有未提交的区块及其执行结果都保存在某个临时位置,对外部客户端是不可见的。
- 提交一个区块。随着共识收集越来越多的区块投票,它就会提交区块及其所有父节点,然后我们就可以把这些的区块保存起来,同时丢弃那些冲突的块。
execute_block 与 commit_block 是完成上述操作的 API,也就是我们说的执行组件。
实施细节
每个版本的状态存储在稀疏的 Merkle 树中。当交易修改帐户时,帐户和所有兄节点(从根节点开始算起)都会被加载到内存中。例如如果我们执行交易 Ti,修改账号 A,最后得到下面这棵树:
S_i
/ \
o y
/ \
x A
A 是账户新的状态,而 y 与 x 是从根节点下来的兄弟节点。如果下一个交易 Ti+1 修改后了位于子树 y 中另一个的帐户 B, 那么 y 将构造一个新树,结构将如下所示:
S_i S_{i+1}
/ \ / \
/ y / \
/ _______/ \
// \
o y'
/ \ / \
x A z B
使用这个结构我们不但可以查询全局状态,而且还考虑到了未提交的交易输出。例如,如果我们想执行另一个交易 Ti+1,我们可以用树 Si。如果我们寻找帐户 A,我们可以在这颗树中找到它的新值。否则,我们知道该帐户不存在于树中,我们可以退回到存储。另一个例子是,如果我们想要执行交易 Ti+2,我们可以使用树 Si+1,它已更新了两个帐户 A 与 B 的值。
文件结构
execution
└── execution_client # A Rust wrapper on top of GRPC clients.
└── execution_proto # All interfaces provided by the execution component.
└── execution_service # Execution component as a GRPC service.
└── executor # The main implementation of execution component.