这一章会举个例子,让我们来看看整个交易的生命周期是怎么运行的。
提要
假设有这样一个场景:
- Alice和Bob是两个用户,他们各自有一个Aptos公链账户(account)
- Alice的账户里有 110 个 Aptos 币
- Alice 要转给 Bob 10 个币
- 目前,Alice 的账户序列号(sequece nubmer)是 5(这说明历史上,从 Alice 的账户中,曾经发出过5笔交易)
- 当前网络中有 100 个验证器 —— 假设叫 V1...V100
- 某个 Aptos 客户端通过 REST 服务,把 Alice 的交易提交到一个全节点,接着被转发到一个验证全节点,然后再继续转发给验证器V1
验证器V1,成为本轮提案人(proposer/learder)
客户端提交交易
某 Aptos 客户端创建一条原始交易(比如说叫做 Traw5),说明 Alice 的账户要给 Bob 的账户转账 10 Aptos 币。该客户端用 Alice 的私钥签名这笔交易,签过的 T5 包括如下内容:
- 原始交易信息
- Alice 的公钥
- Alice 的签名
原始交易中的字段见下表:
字段 | 描述 |
---|---|
Account Address | Alice 的账户地址 |
Move Module | 将以 Alice 身份执行的一段Move模块(或程序)。在本例中,包括: |
- 一段点对点 Move 字节码交易脚本
- 脚本需要的一组输入(在本例中,就包括 Bob 的账户,和要支付的金额)
|
| Maximum
gas amount | Alice 愿意付的最高 gas 金额。Gas 费为了支付交易过程中,消耗的存储和计算资源。Gas unit 是计算资源的抽象衡量指标。 |
| Gas price | Alice 愿意为每个gas unit(单元)付出的价格 |
| Expiration time | 本次交易的到期时间 |
| Sequece number | 一个账户的序列号,表示公链上,曾经从这个账户提交并确认的交易数量。在本例中,从 Alice 的账户总体提交过5笔交易,包括正在提交的这笔 Traw5。
注意:只有当前账户的序列号是5 ,才能确认携带序列号为5的交易 |
| Chain ID | 当前 Aptos 网络的ID号 (防止交叉网络攻击) |
生命周期5阶段
本节我们来看一下 T5 这笔交易,从客户端提交以后,到公链上最终确认的过程。
先来熟悉一下每一步对应的组件间的信息交互:
图 1.0 交易生命周期
图中的箭头,表示从一个组件向另一个组件,发起交互/动作,不代表数据的流动
交易生命周期包含5个阶段:
- Accepting:接受交易
- Sharing:向其他验证器共享交易
- Proposing:发起区块提案
- Executing and Conensus:执行区块并达成共识
- Committing:确认区块
下表详细描述了5个阶段各自发生了什么,并且附上相关概念链接
接受交易
描述 | 组件交互 |
---|---|
- 客户端 -> REST 服务:客户端通过REST服务,向全节点提交交易T5,全节点通过REST服务,将交易转发给自己的 mempool ,它再转发给网络中其他节点的 mempool,最终转到某个验证全节点的 mempool 里,再交给验证器(本例中的V1)
| 1.REST Service |
| - REST 服务 -> Mempool:全节点的 REST 服务,把交易 T5 传递给 验证器
| 2.REST Service,
1.Mempool |
| - Mempool -> Virtual Machine (VM):Mempool 通过 VM 组件执行交易验证,比如 确认签名, 确认账户余额 ,还有用序列号防止重复提交
| 4.Mempool
3.Virtual Machine |
|
|
|
共享交易到其他验证器节点
描述 | 组件交互 |
---|---|
- Mempool:mempool 把交易 T5 记入缓存,此时缓存中有可能还有 Alice 账号发过来的其他交易
| Mempool |
| - Mempool -> 其他验证器:通过缓存共享协议(shared-mempool protocol), V1 跟其他验证器节点共享自己的交易(包括T5和其他交易),也接收其他验证器的交易,放在自己的缓存池中
| 2.Mempool |
提案区块
描述 | 组件交互 |
---|---|
- Consensus -> Mempool:由于验证器 V1 是本交易的提案人,它会从缓存池拉出一个区块(block), 复制成一个提案(Proposal),通过共识组件,发给其他验证器节点
| 1.Consensus
3.Mempool |
| - Consensus -> 其他验证器:V1 的共识组件,负责协调其他所有验证器,就这个交易提案块,达成一致
| 2.Consensus |
执行区块并达成共识
描述 | 组件交互 |
---|---|
- Consensus -> Execution:为了达成一致,交易区块(含T5)会被共享给执行组件
| 3.Consensus
1.Execution |
| - Execution -> Virtual Machine:执行组件负责在虚拟机中执行交易。注意这种执行是在交易达成共识一致性之前,推测式地完成的。
| 2.Execution
3.Virtual Machine |
| - Consensus -> Execution:交易执行完成后,执行组件会把包含交易T5的区块,添加到默克尔树累加器(或者叫记账历史)中。这是一个默克尔树的临时版本(在内存中)。此时推测式执行结果中的必要部分,会被传递给共识组件,期望达成一致。
| 3.Consensus
1.Execution |
| - Consensus -> Other Validators:V1(共识leader) 会协调其他参与共识的验证器进行投票,就当前提案区块的执行结果达成一致。
| 3.Consensus |
确认区块
描述 | 组件交互 |
---|---|
- Consensus -> Execution, Execution -> Storage:如果参与投票的验证器中,确认并签名的数量超过了法定个数,V1验证器就会从预执行缓存中读出整个提案区块的执行结果,提交区块中所有交易,并把它们和执行结果一起写入永久存储。
| 1.Consensus
3.Mempool |
现在Alice的账户只剩 100 个 Aptos 币了,它的序列号(sequence number)也长到了6。如果Bob试图重新执行T5,就会被系统拒绝,因为 Alice 账户的当前序列号(6)已经比 T5 的序列号(5)大了。
Aptos 节点组件间交互
在上一节,我们描述了一笔交易典型的生命周期(从提交到确认)现在我们来看看区块链在处理交易和响应查询时,Aptos节点组件间互动的情况。互动的详细信息,对以下这些人尤其有用:
- 想知道系统底层是如何工作的
- 想参与 Aptos 公链建设,贡献自己的力量
你们可以在这里学到不同类型的 Aptos 节点:
- 验证器节点(Validator nodes)
- 全节点(Fullnodes)
我们的讲述结构会是这样的:假设一个客户端向验证器 Vx 提交了交易 Tn。对每一个验证器组件,我们将会给每个组件设置一个章节,并且在里面的每个小节中,描述它们之间的互动。不过各个小节的顺序,不是严格按照它们的执行顺序列出的。大部分互动都是和交易处理相关的,一小部分跟客户端查询区块链相关(查询链上已经存在的信息)
下面是交易生命周期中用到的 Aptos 核心组件:
全节点
- REST 服务
验证器节点
- 缓存池(Mempool)
- 共识(Consensus)
- 执行(Execution)
- 虚拟机(Virtual Machine)
存储(Storage)
REST 服务
客户端的任何请求,都是从调用全节点的REST服务开始的,之后,交易后被转发给一个验证器全节点,它再传给验证器节点 Vx 。
1. 客户端 -> REST服务
客户端发起REST服务调用,提交交易到一个 Aptos 全节点
2. REST 服务 -> 缓存池(Mempool)
REST 服务把交易转发给某个验证器全节点,它再传给验证器节点 Vx 的缓存池。缓存池会验证这个交易TN,只有在它的序列号,大于等于发送者账户当前序列号的时候,才会接受这笔交易(也就是说,如果交易序列号不满足条件,它就不会被发送给共识组件)。
3. REST 服务 -> 存储
当任意客户端向 Aptos 链发起只读查询时(比如,获取 Alice 账户余额),REST 服务会直接和存储组件交互,获取信息。(注:不是从缓存池中读)
虚拟机(Virtual Machine)
图 1.2 虚拟机
Move 语言虚拟机,负责验证和执行 Move bytecode 格式的交易脚本1. 虚拟机 -> 存储
当缓存池请求虚拟机验证一笔交易的时候,它会调用方法 VM::ValidateTransaction() ,虚拟机从存储中加载交易发送人的账户,并执行验证,具体过程如下:(完整的验证列表,可以从这里看到)
- 检查交易中的输出签名(如果签名不正确,就拒绝)
- 检查发送者账户的认证秘钥,与公钥的哈希值是否一致(交易应该是被发送者的私钥签名过的)
- 验证交易序列号大于等于发送者账户当前序列号。做这项检查可以防止同一个交易被重复执行。
- 验证签名交易中的程序没有被篡改,防止虚拟机执行篡改过的程序
验证发送者账户的余额,大于等于“交易中设定的最大gas费 ✖️ 当前gas价格”,保证可以支付将要消耗的资源
2. 执行组件 -> 虚拟机
执行组件通过 VM::ValidateTransaction() 方法,调用虚拟机来执行交易。
我们需要理解,执行交易不等于更新账本中的状态,并持久化结果到存储中。执行 TN 的交易,是达成区块共识过程的一部分。只有其他验证器,就当前块中一系列交易的顺序和执行结果都达成一致,它们才会被写入存储,同时更新状态。3. 缓存池 -> 虚拟机
缓存池有可能从 REST 服务或者其他验证器的共享缓存服务中,获取到交易信息,之后它会调用虚拟机的 VM::ValidateTransaction() 方法,来验证这笔交易。
虚拟机的详细信息可以看这里。缓存池
图 1.3 缓存池
缓存池中保存了“等待”执行的交易。一旦某笔交易被加入缓存池,它立即就和其他验证器节点分享这条交易。为了减少网络消耗,每个缓存池只负责提交它自己的交易给其他验证器,并且把其他验证器共享出来的交易添加到自己的池子里。1. REST 服务 -> 缓存池
- 从客户端接收到一笔交易 TN 之后,REST 服务把交易转发给某个验证器全节点,再继续转发到验证器节点 VX的缓存池中。
只有在 TN 的序列号大于等于 交易发送者账户的当前序列号时,VX 的缓存池才会接受这笔交易。
2. 缓存池 -> 其他验证节点
- 验证节点 VX 把交易 TN 分享给同网络中其他验证节点
其他验证节点,也把它们各自缓存池中的交易,分享给 VX
3. 共识组件 -> 缓存池
- 当一笔交易被首次添加到某个验证节点,它就成为这笔交易的提案人(proposer / leader),它的共识组件,会从缓存池中,为这条交易拉取一个提案区块,并且把该区块复制给其他验证器;之后,共识组件将试图协调其他验证器,就这个区块中交易的顺序和执行结果,达成一致的共识。
注意到目前为止,TN 这笔交易仅仅是存在于一个提案区块中的,所以不能保证将来 TN 一定会被持久化到 Aptos 公链的分布式数据库里。
4. 缓存池 -> 虚拟机
- 缓存池从其他验证节点获取到交易以后,也会针对每一笔交易调用虚拟机的 VM::ValidateTransaction() 方法,来完成验证。
详见 缓存池的 readme 文档。
共识组件
图 1.4 共识组件
共识组件负责给交易排序,并且通过 共识协议 和其他验证器交互,来确认排好的顺序和交易的执行结果。
1. 共识组件 -> 缓存池
如果验证器 VX 是提案人,那么 VX 的共识组件,就会通过 Mempool::GetBlock() 方法,从它的缓存池中拉取一个区块,并且构建出一个提案区块,其中包含多个交易。
2. 共识组件 -> 其他验证器
如果验证器 VX 是提案人,那么它的共识组件,就会复制提案区块给所有其他验证器
3. 共识组件 -> 执行组件, 共识组件 -> 其他验证器
- 共识组件调研执行组件的 Excecution::ExecuteBlock() 方法,来执行整个区块的所有交易。(参见 共识组件 -> 执行组件)
- 完成提案区块中的每一项交易执行后,执行组件向共识组件返回所有执行结果。
共识组件为这组结果签名,然后试图与其他验证器达成一致
4. 共识组件 -> 执行组件
- 如果有足够多的验证器投了赞成票,共识组件 VX 就会调用执行组件的 Execution::CommitBlock() 方法,来确认区块。
详见 共识组件 readme。
执行组件
图1.5 执行组件
执行组件协调一个区块中所有交易的执行,维持它们的暂时状态(Transient State),交给共识组件投票。一旦这些交易成功取得共识,它们就会被记入永久存储。
1. 共识组件 -> 执行组件
- 共识组件通过调用 Excecution::ExecuteBlock() 方法,请求执行交易
- 执行组件内部维持了一个 “寄存器”(scratchpad),它在内存中保持了一份默克尔累加器(Merkle accumulator)相关部分的副本,以便计算 Aptos 链当前状态的根哈希值。
- 当前状态的根哈希值,和提案区块中的交易信息相结合,就可以确定新的累加器根哈希值。这一步在持久化数据之前完成,就可以保证,只要没有法定数量的验证器投赞成票,就不会有交易或者状态变更被记入永久存储。
执行组件先预计算一个根哈希值,交给 VX 的共识组件签发,然后试图和其他验证器的根哈希值匹配
2. 执行组件 -> 虚拟机
当共识组件通过调用 Excecution::ExecuteBlock() 方法,请求执行交易的时候,执行组件通过虚拟机来获取区块中交易的执行结果
3. 共识组件 -> 执行组件
如果法定数量的验证器投票赞成了区块执行结果,每个验证器的共识组件都会调用自己执行组件的 Excecution::CommitBlock() 方法来确认区块。调用时,应当传入所有验证器签发的同意共识信息。
4. 执行组件 -> 存储
- 执行组件把确认后的所有交易信息,从自己的“寄存器”中取出,传给存储,并通过调用 Storage::SaveTransactions() 方法来持久化。之后,执行组件立即“修剪”掉寄存器中没用的分支(比如,那些不是被提交进来,但是未能确认的区块)
详情请见 执行组件 readme
存储
图 1.6 存储
存储组件负责把区块交易及其执行结果持久化到 Aptos 链上。当超过法定数量的验证器(2f+1)投票达成一致之后,整个区块的交易(其中包括 TN )都会通过存储组件保存起来。共识必须包含以下内容:
- 区块中的交易
- 交易之间的顺序
- 区块内交易的执行结果
要详细了解交易是如何被加入 Aptos 链上数据结构的,参见 默克尔累加器
1. 虚拟机 -> 存储
当缓存池调用 VM::ValidateTransaction() 方法验证某个交易时,VM::ValidateTransaction() 方法内部,会通过存储加载发送者账户信息,并执行该交易的只读验证
2. 执行组件 -> 存储
当共识组件调用 Excecution::ExecuteBlock() 方法时,执行组件会从存储读取当前状态,结合“寄存器”中的数据,来确认执行结果
3. 执行组件 -> 存储
- 一旦整个区块中交易的共识达成,执行组件就会调用存储的 Storage::SaveTransactions() 方法,永久保存交易信息。这一步同时会保存所有赞成结果的验证器,签发的投票信息。寄存器中的区块数据,会被传递给存储组件,并永久保存。存储被更新之后,所有被修改过的相关账户,其序列号都会+1。
注意:每一笔确认的交易,都会导致其发起者账户的序列号 +1。
4. REST 服务 -> 存储
- 如果客户端要查询区块链上的信息,REST 服务会直接和存储组件交互,来获取需要的信息。
详见 存储 readme
我的语雀原文链接
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。