以太坊节点在构建新区块时具体的执行流程
区块的构建是一个涉及共识层 (CL) 与执行层 (EL) 协同。具体执行流程:
- 启动构建程序
区块构建过程始于共识层。当一个验证者被选定在某个时隙 (Slot) 提议区块时,共识层会通过 Engine API 的 forkchoiceUpdated 端点向执行层发出指令,执行层接收到指令后,会启动名为负载构建程序 (Payload building routine) 的过程。
- 初始化环境与空区块
在执行层(以 Geth 客户端为例),buildPayload 函数会首先创建一个空区块。目的是为了防止节点错过该时隙,节点可以确保自己无论如何都有一个可用的区块可以提议,从而履行其作为验证者的职责。
- 环境准备:节点会设置构建环境,包括获取区块号、时间戳、前一区块哈希、基础费用 (Base Fee) 以及所有需要在该区块中进行的提款 (Withdrawals) 信息。
- 交易填充与执行 随后节点会启动一个并发进程(Go 协程)来尝试填充交易。
-
交易来源:交易从 Mempool 中获取。这些交易之前已经过验证(如检查 Nonce、余额和签名)并存储在池中。
-
排序与筛选:为了使收益最大化,执行层通常会将交易按价值(如 Gas 价格)升序或盈利能力排序。
-
循环执行:
筛选交易:从交易池中取出下一笔最有价值的交易,在 EVM 中执行该交易。执行过程会传入当前的环境信息、交易数据和当前的状态数据库。
状态更新:如果交易执行成功,它会产生更新后的状态并累加到状态数据库中
Gas 追踪:节点会追踪已消耗的 Gas。由于区块有固定的 Gas 限制(例如主网约为 3000 万),节点会持续添加交易,直到交易池耗尽或达到 Gas 上限。
异常处理:如果某笔交易执行失败(例如因链上原因、Gas 耗尽或合约调用失败),节点会直接跳过它并继续尝试下一笔交易,而不会停止整个区块的构建。
- 状态生成与根计算
一旦交易填充完成,执行层需要生成最终的区块信息并计算相关的加密根:
-
默克尔化 (Merkleization):节点会计算并生成交易根 (Transactions Root)、收据根 (Receipts Root) 以及提款根 (Withdrawals Root)。这些根是通过对相应列表进行默克尔化处理得到的,并被添加到区块头中。
-
世界状态根 (State Root):在所有交易处理完毕后,执行层会计算世界状态根。这是对整个系统状态的加密承诺,反映了执行完该区块所有交易后的最终状态。
- 负载交付 (Payload Delivery)
最后共识层会再次通过 Engine API(调用 getPayload 方法)向执行层请求构建好的执行负载。执行层将组装好的负载(包含区块头、交易列表等)返回给共识层。共识层将该负载放入信标区块中,并向全网广播。
**共识层作会在该时隙(Slot)即将结束需要广播时,通过 getPayload 获取成果。**由于有空区块兜底,不存在“没东西交”的情况,只存在“交易填了多少”的区别。
失败交易的处理方式
如果交易由于 Gas 耗尽或合约报错而执行失败:
-
不予包含:如果 ApplyTransaction 执行报错,该交易的状态更改将不会被应用到执行层的本地状态中。
-
直接跳过:节点会直接跳过该交易并继续处理下一笔。
-
保留在池中或剔除:资料指出失败通常意味着交易无效或环境变化。这类交易不会进入当前的区块列表中,因此不会出现在该区块的交易树(Transaction Trie)中。
Verkle 树相比当前的 MPT 结构有哪些核心改进
Verkle 树(Verkle Tree,结合了“Vector Commitment”和“Merkle Tree”)是为替换以太坊当前的默克尔帕特里夏树(MPT)而设计的新型数据结构,旨在提高效率和可扩展性,核心改进主要体现在以下几个方面:
- 证明规模的大幅缩减,更省空间
MPT 的局限性:MPT 是 2 叉或 16 叉树结构,其证明(Witness)包含从根到叶子路径上每一层的所有兄弟节点哈希,导致证明数据量随树的增大而变得非常庞大,对于无状态客户端很不友好。
Verkle 的优化:Verkle 树使用**向量承诺(Vector Commitments)**代替简单的哈希。这使得证明只需要提供路径上的承诺数据,且通过聚合技术使证明过程变得非常紧凑。
- 树结构从“深”变“宽”
更宽的节点:MPT 通常是 2 或 16 叉树,而 Verkle 树采用了更宽的 n-ary 结构(目前提议为每个节点 256 个子节点)。由于宽度增加,树的深度显著降低,这不仅减小了证明长度,也优化了查找效率。
- 实现“无状态客户端”
无状态验证:由于证明数据量极小,可以将证明包含在区块中而不显著增加区块大小。
无需存储全量状态:这使得无状态客户端成为可能,即客户端无需存储完整的以太坊状态(State),仅凭区块中的证明即可验证状态转换的正确性。
执行层如何通过排序来最大化区块的收益
- 基于交易价值的排序
在实际操作中这种排序是基于手续费(Fee)进行的,交易中的 maxPriorityFeePerGas 字段代表了发送者愿意支付给验证者的最高“小费”,而 maxFeePerGas 则代表了其愿意为每单位 Gas支付的总最高费用。执行层通过优先选择这些费用更高的交易来提升区块的总收益。
- 贪婪算法填充过程
由于区块的 Gas 上限是固定的(例如主网约为 3000 万),节点会持续添加交易并追踪已消耗的 Gas,直到交易池排空或达到 Gas 限制为止。这种做法类似于“装箱问题”,确保在有限的区块空间内塞入价值最高的交易组合,
- 容错与效率优化
为了防止无效交易占用创收空间,执行层还具备以下逻辑:
-
跳过失败交易:如果某笔交易在 EVM 执行中失败(例如由于 Gas 耗尽或合约调用错误),节点不会停止构建,而是直接跳过该交易并尝试处理下一笔。
-
动态填充:这种机制确保了即使某些高价值交易失效,区块中剩余的可用 Gas 仍能被后续的有效、有收益的交易填充,从而保证了区块的整体盈利能力。
什么是 PBS 机制以及它如何改变区块构建
PBS 全称是提议者-构建者分离(Proposer-Builder Separation)。它是一种将区块链节点的两项核心任务——“构建区块”与“提议(广播)区块”进行解耦的机制。
在传统的区块构建流程中验证者同时承担这两项工作,在 PBS 框架下,出现了专门的外部构建者 (External Builders),他们专注于收集交易并构建最优化的区块。
构建者负责筛选、排序交易并最大化区块价值;提议者则只负责从多个构建者提供的备选区块中选出收益最高的一个,并将其签署后广播到网络中。
由于外部构建者可以使用极其复杂的算法和策略来提取最大可提取价值 (MEV),他们构建的区块通常比普通验证者节点自行构建的区块更有利可图。
无状态客户端如何验证区块的正确性
无状态客户端(Stateless Client)验证区块正确性的核心机制是依赖区块证据(Witness),而不是存储完整的以太坊状态数据库。其具体验证流程如下:
- 依赖区块证据
无状态客户端不存储完整的“世界状态树”(World State Trie),因此无法直接查询账户余额或合约代码。当一个区块被广播给无状态客户端时,它必须包含验证该区块内交易所必需的所有数据片段(如涉及账户的余额、Nonce 等)以及相关的加密证明,这些证明允许客户端在没有完整数据库的情况下,验证所提供的数据片段是否确实属于当前的状态根(State Root)。
- 验证执行过程
无状态客户端接收到区块和证据后,按以下步骤验证:
-
加载上下文:利用区块头中的上一个区块的状态根作为锚点。
-
构建局部状态:利用证据中提供的数据“填补”缺失的状态信息。
-
执行交易:在 EVM 中重新执行区块中的所有交易。由于证据提供了所有必要的数据输入,客户端可以像全节点一样计算出状态转换的结果。
-
计算并对比根哈希:执行完所有交易后,客户端会独立计算产生的新状态根。如果计算出的结果与区块头中提议的状态根一致,则该区块被视为有效。
以太坊中的四种MPT树分别存储什么样的数据内容
以太坊的执行层状态主要存储在四种改良后的**默克尔帕特里夏树(MPT)**结构中:
- 交易树 (Transaction Trie)
交易树负责存储特定区块内的所有交易数据。
存储内容:每个区块都有自己独立的交易树,存储该区块包含的所有交易。
键 (Key):交易在区块中的索引(Index)的 RLP 编码。
值 (Value):交易详情(T)的 RLP 编码,包含 Nonce、Gas 费用参数(如最高小费和最高总费用)、Gas 限制、发送者与接收者地址、转账金额 (Value)、输入数据 (Input Data) 以及数字签名 (v, r, s)。
特性:一旦区块执行完毕并固化,该区块的交易树内容便不可更改。
- 收据树 (Receipt Trie)
收据树用于验证区块中每笔交易的指令是否已被实际执行,并记录执行结果。
存储内容:存储与交易执行结果相关的 RLP 编码收据数据。
键 (Key):交易在区块中的索引。
值 (Value):交易收据,包含交易执行后的结果证明。
用途:主要用于索引历史交易结果而无需重新执行交易,同时也允许轻客户端通过默克尔证明验证交易结果。
- 世界状态树 (World State Trie)
世界状态树是以太坊的核心数据结构,代表了系统当前的全局状态。
存储内容:它将账户地址映射到其对应的账户状态。
键 (Key):经过 Keccak-256 哈希处理的 20 字节账户地址。
值 (Value):RLP 编码的账户对象,包含四个核心字段:Nonce(已发送交易数)、余额 (Balance)、代码哈希 (Code Hash)(合约代码的哈希,外部账户为空字符串哈希)以及存储根哈希 (Storage Root Hash)。
特性:它是随区块演化的持续结构,其根哈希(State Root)存储在每个区块头中,作为整个系统状态的加密承诺。
- 存储树 (Storage Trie)
存储树用于保存智能合约的持久化变量。
存储内容:每个合约账户都有自己独立的存储树,用于维护该合约特有的键值对状态。
引用方式:它不直接嵌套在世界状态树中,而是通过世界状态树中账户节点的 storageRoot 字段进行引用。
键 (Key):经过 Keccak-256 哈希处理的 256 位存储插槽(Storage Slot)索引。
值 (Value):对应插槽中存储数据的 RLP 编码值。
用途:外部账户 (EOA) 的存储树为空,只有合约账户会通过 SSTORE 和 SLOAD 指令操作该树。
mempool 交易准入还需要检查哪些条件
签名验证
Nonce 的正确性 (Nonce Correctness):节点会检查交易的 Nonce 是否与发送者账户的序列号相匹配。Nonce 是一个标量值,用于识别该账户成功发送的交易数量,它的存在是为了确保交易按顺序处理并防止重放攻击。
账户余额
Gas 限制与费用参数:交易本身必须包含合法的 gasLimit(交易愿意消耗的最大 Gas 单位)和费用字段(如 maxPriorityFeePerGas 和 maxFeePerGas),以便节点评估其有效性和盈利能力。
Nonce 在防止重放攻击中具体是如何起作用的
对于由同一发送者提交的每一笔新交易,其 Nonce 值都会依次增加。这意味着对于一个特定的账户地址,每一笔合法的交易都必须拥有一个独一无二的、按顺序排列的 Nonce 编号。在以太坊的世界状态树(World State Trie)中,每个账户对象都维护着一个当前的 Nonce 字段。
已经执行过的合法交易,并尝试再次发送到网络中以欺诈性地重复执行。Nonce 通过以下方式拦截这种攻击:
匹配规则:网络只接受 Nonce 值恰好等于该账户当前状态中记录的 Nonce 值的交易。
拦截重放:一旦一笔交易被纳入区块并成功执行,该账户在状态数据库中的 Nonce 就会增加 。如果攻击者尝试重放之前的那笔交易,由于该交易携带的是旧的 Nonce,节点会识别出该 Nonce 是“已使用过”的或“过时的”,从而直接拒绝该交易。
区块包含的 blob 的数量限制
每个区块允许包含的 blob 数量是由 EIP-4844 协议规范定义的,在区块构建的具体流程中,执行层(EL)的 commitTransactions 函数在尝试将携带 blob 的交易提交到区块时,会进行专门的检查。
构建逻辑:节点在填充交易(fillTransactions)时,虽然会从内存池中检索包括 blob 在内的所有待处理交易,但在最终确认包含之前,必须确保该区块内的 blob 总量不超过协议规定的上限。