Skip to main content

执行

一些操作码和预编译已重新定价,以便更准确地反映其相对成本。详情见此处
在 Monad 中,与以太坊一样,交易在一个区块内按线性顺序排序。Monad 提供的保证是,每个区块结束时的结果就像交易被串行执行一样,即使实际上底层是并行完成的工作。Monad 通过将计算(可以并行完成)与提交(仍然串行完成)的关注点分开,优雅地处理相互依赖的交易。交易被乐观地并行计算(即假设执行期间读取的任何存储槽都是正确的),为每笔交易生成一个待定结果。待定结果由输入存储槽(及其值)和输出存储槽(及其值)的集合组成。待定结果按交易的原始顺序串行提交,在提交时检查每个输入的正确性。(如果输入被之前提交的待定结果之一变更过,则输入将不正确。)如果待定结果有任何不正确的输入,将被重新执行;在此之前不能提交其他待定结果。串行提交待定结果确保始终保持正确性。
最好用一个例子来说明。假设在区块开始时,Alice、Bob 和 Charlie 各自都有 100 USDC 余额。以下是前两笔交易:当交易 0 和 1 并行执行时,它们产生以下待定结果:现在我们串行提交待定结果。待定结果 0 被提交。当我们尝试提交待定结果 1 时,我们注意到其中一个输入是错误的 —— 期望 Bob 的余额为 100,但实际为 105。这会重新触发交易 1 的执行。在重新执行交易 1 之前不能提交其他交易。
(注:此问题询问的是此处讨论的重新执行。)虽然重新执行待定结果确实比立即提交它慢,但重新执行通常也比原始执行快得多,因为输入存储在缓存(RAM)中。还请注意每笔交易最多执行两次:初始一次,重新执行一次。更一般地,您可以将乐观并行执行视为两遍策略。第一遍并行开始执行许多交易,从而并行揭示许多存储槽依赖关系并将它们全部拉入缓存。第二遍串行遍历交易,要么立即提交待定结果,要么重新执行它(但从大多数存储槽已缓存的位置)。此策略结合来自 MonadDb 的高效 SSD 查找,交付出可以更高效地利用全部 SSD 吞吐量的工作负载。
不需要!与您的智能合约交互的交易表现得就像每笔交易都在串行执行。并行执行严格来说是一种实现细节。另外,重要的是要注意所有状态争用都是按槽评估的。例如,假设交易 1 涉及 Alice 向 Bob 发送 USDC,交易 2 涉及 Charlie 向 David 发送 USDC。两笔交易都涉及同一智能合约(USDC ERC-20 合约)并无关系;交易 1 中受影响的两个存储槽与交易 2 中受影响的两个存储槽完全独立。
MonadDb 原生存储 Merkle Patricia Trie 数据,而不是将 trie 嵌入到通用数据库(如 LevelDB 或 RocksDB)中,后者有自己的数据库条目到磁盘位置的映射逻辑。这消除了一层间接性,并大幅减少了查找一个值所需的 IOPS 和页读取次数。诸如在每个区块结束时重新计算 merkle 根之类的 trie 操作效率更高。MonadDb 通过使用 io_uring 实现异步 I/O 并绕过文件系统,进一步降低了延迟并提高了吞吐量。io_uring 是一种新的 linux 内核技术,允许执行线程在不阻塞或占用线程的情况下发出 I/O 请求。这允许许多 I/O 请求并行发出、由内核排序、并在返回时由第一个可用线程处理。最后,在 MonadDb 中,trie 中的每个节点都是版本化的,允许直观地维护 merkle trie 和高效的状态同步算法。在 statesync 期间仅发送必要的 trie 组件,使引导和恢复更快。
  1. 使用访问列表通常会增加交易的大小;长期来看,我们认为带宽是最大的瓶颈
  2. 在以太坊中,用户使用访问列表提交交易的工作流是:模拟交易,注意访问了哪些存储槽,然后使用访问列表中提到的这些槽提交交易。然而,世界状态可能在模拟和真实执行之间发生变化;我们认为系统应该在底层优雅地处理这一点。
  3. 这会破坏与不支持 EIP-2930 的现有钱包的集成。
  4. 请注意,EIP-2930 访问列表实际上是欠规范的,至少从预测状态争用的角度来看。如果两笔交易都从同一存储槽读取(但都不写入),那么就该存储槽而言,没有状态争用 —— 任一交易都无法使另一笔交易的计算无效。仅当较早的交易写入较晚的交易将读取的存储槽时,争用才会发生。EIP-2930 访问列表提到访问了哪些存储槽,但不注明交易是从该存储槽读取还是写入。

共识

Leader 时间表由确定性、按质押权重加权的过程构建,每个 epoch 计算一次:
  • epoch 大约每 4 小时 12 分钟(50,000 个区块)发生一次。验证者的质押权重被锁定提前一个 epoch(即 epoch N+1 的任何变化必须在 epoch N 开始之前注册)。
  • 在每个 epoch 开始时,每个验证者基于对质押权重运行确定性伪随机函数来计算 leader 时间表。由于函数是确定性的,每个人都会得出相同的 leader 时间表。
客户端代码库有一个称为 ACTIVE_VALSET_SIZE 的参数,当前设置为 200。因此,排名前 200 的 validator(按质押权重排序)可以直接参与共识。此参数可能会随时间变化。参与无需许可;只需成为前 ACTIVE_VALSET_SIZE 名 validator 之一即可。

Raptorcast

请查看这篇博文!
请参见这篇博文中的讨论。选择 UDP,接受其损耗特性但通过添加额外的数据完整性(Raptor 码)和消息认证(对 merkle 根的签名)来缓解,因为这些策略在 UDP 上的组合比使用 TCP 效率高得多。
以太坊使用 libp2p。它基于 gossip(每个节点将其消息传播到一组对等节点),效率低得多(更多重复消息以及更曲折的传播过程)。因此,以太坊为区块在网络中的传播预算了数秒。Solana 使用 Turbine。Turbine 和 RaptorCast 在使用擦除码、将数据包切分为 MTU 大小的块、并通过广播树发送交易以提高效率方面相似。一些差异包括:
  • Monad 使用 Raptor 码,而 Turbine 使用 Reed-Solomon
  • Monad 使用两级广播树,每隔一个 validator 作为一级节点,而 Solana 使用更深、结构更少的广播树,一级节点更少,确定广播树的逻辑更复杂。Solana 没有 Monad 那样的区块交付 BFT 保证。
在 L2 中只有 1 个 sequencer,因此没有用于共识的区块传播概念。Sequencer 只是偶尔将交易批次推送到 L1。

Mempool

例如,如果我的 EOA 当前 nonce 为 0,然后我发送一笔 nonce 为 3 的交易,然后我再发送 nonce 为 0、1、2 的交易。nonce 为 3 的交易会被执行还是被丢弃?答:交易 3 会被执行。

区块状态与最终确认

一旦收到新区块提议,节点就可以开始推测性执行。执行一个区块只是生成新的状态和新的 merkle trie 根 —— 但官方指针仍然指向旧的。这就像从老师那里收到一份即将确定的可能作业 —— 您可以在新纸上开始做,如果结果不需要这份作业就扔掉。
一旦区块进入 Finalized 状态,该区块推测执行的 merkle 根就成为该区块的官方本地 merkle 根。该 merkle 根还要再过 D=3 个区块才会被共识验证,但它在本地已知,因为在固定交易顺序下状态是确定性的。如果您想确保您的本地节点没有出现计算错误(例如由于宇宙射线),您可以等待 D=3 个区块以获得延迟的 merkle 根,这会使相关区块进入 Verified 状态。

RPC

由于 Monad 的高吞吐量,全节点不提供对任意旧状态的访问,因为这需要过多存储。有关更完整的讨论,请参见历史数据编写智能合约时,建议使用事件记录以后需要的任何状态,或使用智能合约索引器在链下计算它。

特性

是的,支持 EIP-7702。请注意,在账户根据 EIP-7702 进行委托时,它们在 Monad 的储备余额规则下的处理会略有变化。您可以在此处了解更多。
是的,它是遵循 EIP-7951 位于 0x0100 的预编译。这使得可以使用 P256 曲线在链上验证 WebAuthn/passkey 签名。有关使用详情和 Solidity 示例,请参见预编译

杂项

Monad 的核心目标是去中心化。如果运行节点非常昂贵,只有拥有大量质押的专业 validator 公司才能证明成本合理。使节点经济实惠对于任何人都能运行全节点以及支持大型 validator 集至关重要 —— 100-200 节点的主网首日目标只是一个起点。
Rust 和 C++ 都是很好的高性能语言。C++ 被选中用于数据库和执行系统,以更精细地控制文件系统,并使用 io_uringboost::fibers 等库。Rust 被选中用于共识,以利用更严格的内存安全性,因为共识关注的是稍高级别的系统工程问题。