Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
网络与协议指南阅读约 12 分钟

Monad 交易生命周期:异步执行、回执状态与排序

了解 Monad 的异步执行:交易回执、状态和排序与经典 EVM 链有何不同,并附带轮询脚本。

TL;DR

Monad 的异步执行模型将交易排序和提交与执行解耦。与经典 EVM 链不同,交易可以包含在区块中并在其执行(及其依赖项的执行)完成之前出现在回执中。本指南解释了该机制、如何解读回执状态和排序,并提供了一个可复现的轮询脚本,用于在 Monad 端点上观察生命周期。

直接回答:Monad 异步执行带来了哪些变化

在经典 EVM 链上,区块中的交易按顺序执行,并且只有在执行完成后才会生成区块。Monad 则相反:它首先对区块进行排序和提交,然后异步并行执行交易,使用乐观推测和依赖跟踪。因此,交易被包含在区块中的时刻与其实际执行的时刻并不相同。当你查询 eth_getTransactionReceipt 时,可能会看到带有 status 字段的回执,该字段反映了最终结果,但该交易或其依赖项的执行可能仍在进行中。本指南解释了该机制、延迟执行下回执字段的含义,以及如何构建不假设立即顺序执行的稳健集成代码。

此行为的权威来源是 Monad 关于交易生命周期的文档异步执行。截至撰写本文时,Monad 处于测试网/开发网阶段;主网行为可能有所不同。请始终根据实时文档和你所针对的具体端点进行验证。本指南提供了一种可复现的方法,用于在你自己的端点上观察生命周期。

Monad 的执行模型:先排序,后执行

Monad 使用流水线架构,将区块生产与执行分离。共识层在执行之前就交易和区块的顺序(规范顺序)达成一致。这通常被描述为“先共识,后执行”(参见 Monad 的论坛帖子)。然后异步执行,在依赖关系允许的情况下并行执行多个区块。

为了确保安全,Monad 使用带有推测的乐观执行。当提议一个区块时,执行层基于当前状态推测性地执行交易,即使某些依赖项(同一区块中较早的交易或来自先前区块的交易)尚未完成执行。依赖跟踪确保读取由另一笔交易写入的状态的交易会等待该写入可用。如果推测执行结果不正确(例如,因为依赖项的实际结果与推测状态不同),则回滚并重新执行。这种协调是内部的,不应影响最终提交的结果,该结果遵循规范顺序。

对于集成者来说,关键要点是交易的规范顺序在提交时固定,但交易的执行可能会滞后。这意味着当你通过 eth_getBlockByNumber 或类似方法在区块中看到交易时,其执行可能尚未完成。回执(如果可用)反映了该执行的结果,但其可用性并不保证所有先前交易都已执行。

交易提交与包含 vs. 执行

在 Monad 上提交交易使用标准的 eth_sendRawTransaction 方法(或你所用库的等效方法)。交易被广播到网络,并最终包含在区块中。包含意味着交易是规范顺序的一部分,并具有区块号和交易索引。然而,包含并不意味着执行已经发生。

要检查包含情况,可以使用 eth_getTransactionByHash。一旦交易被包含,此方法将返回交易详细信息,包括 blockNumberblockHash。但交易的 status(成功/失败)只有在执行后才知道。为此,你需要 eth_getTransactionReceipt

回执包含 status 字段(0x1 表示成功,0x0 表示失败)和 logs(用于事件)。在 Monad 上,回执可能在交易实际执行之前就可用,因为回执是作为区块元数据的一部分生成的。但是,statuslogs 反映了执行和协调后的最终结果。实际上,你可能会看到带有 blockNumber 的回执,但交易的执行仍在进行中。这与经典链不同,在经典链上,回执的可用性意味着执行完成。

延迟执行下的回执状态语义

当你在 Monad 上调用 eth_getTransactionReceipt 时,返回的对象包含标准字段:transactionHashtransactionIndexblockHashblockNumberfromtocumulativeGasUsedgasUsedcontractAddresslogslogsBloomstatuseffectiveGasPricestatus 字段尤其重要:它指示交易一旦执行将成功还是失败。但由于执行是异步的,带有 status: 0x1 的回执并不意味着状态更改已经应用——仅表示执行引擎已确定结果。

对于依赖其他交易的交易(例如,读取先前交易写入状态的合约调用),回执可能在这些依赖项执行之前不可用。实际上,你可能会看到回执出现晚于区块包含,或者你可能会看到带有 status 的回执,如果推测执行被回滚,该状态稍后可能会更改(尽管这应该很少见,并且不属于公共 API 保证的一部分)。

Monad 的文档指出执行是确定性的,最终状态与规范顺序匹配。因此,即使执行仍在进行中,你也可以信任回执中的 statuslogs 作为最终结果。但是,你不应假设交易的效果在回执可用后立即可见于状态查询(例如 eth_getBalanceeth_call)。回执可用性与状态最终确定之间可能存在延迟。

排序保证与 Nonce 管理

Monad 保留由共识层确定的交易规范顺序。这意味着对于给定账户,交易按 nonce 排序,最终状态反映该顺序。然而,由于执行是异步的,你不能假设较低 nonce 的交易在较高 nonce 交易被包含之前已执行。排序仅在状态级别保证,而非执行时间线。

这对 nonce 管理有影响。如果你从同一账户发送多笔交易,你仍然必须像在任何 EVM 链上一样正确递增 nonce。但你不应依赖先前交易的执行完成后再发送下一笔。相反,你应该跟踪 nonce,并使用适当的区块参数(例如 'pending' 或 'latest')调用 eth_getTransactionCount 来确定下一个可用 nonce。有关更深入的讨论,请参阅我们的指南 并发下的 EVM nonce 管理

在构建等待回执的循环时,你应该轮询 eth_getTransactionReceipt,直到它返回非空结果。然而,由于执行可能滞后,如果你的应用程序依赖交易效果,你可能还需要等待交易效果在状态中可见。例如,如果你发送转账然后想检查接收者的余额,你应该轮询 eth_getBalance 直到它反映预期值,而不是假设回执可用后立即更新。

实际示例:使用 Node.js 脚本观察生命周期

以下脚本向 Monad 端点发送一笔简单的转账交易(你必须提供端点 URL 和带有资金的私钥)。然后每秒轮询 eth_getTransactionReceipt,最多 30 秒,每次轮询打印区块号、状态和日志数量。这允许你观察回执相对于包含何时可用。请针对 Monad 测试网/开发网端点运行;不要假设主网可用性。

限制: 此脚本仅用于教育目的。确切的时序和行为可能因端点和网络阶段而异。请始终根据实时 Monad 文档和你的端点行为进行验证。

const { Web3 } = require('web3');

// Configuration - replace with your endpoint and private key
const RPC_URL = 'https://your-monad-endpoint.example.com';
const PRIVATE_KEY = '0x...';
const TO_ADDRESS = '0x...';

const web3 = new Web3(RPC_URL);

async function main() {
  const account = web3.eth.accounts.privateKeyToAccount(PRIVATE_KEY);
  web3.eth.accounts.wallet.add(account);

  // Get nonce
  const nonce = await web3.eth.getTransactionCount(account.address, 'pending');

  // Build transaction
  const tx = {
    from: account.address,
    to: TO_ADDRESS,
    value: web3.utils.toWei('0.001', 'ether'),
    gas: 21000,
    gasPrice: await web3.eth.getGasPrice(),
    nonce: nonce
  };

  // Sign and send
  const signedTx = await web3.eth.accounts.signTransaction(tx, PRIVATE_KEY);
  const txHash = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  console.log('Transaction hash:', txHash);

  // Poll for receipt
  const timeout = 30000; // 30 seconds
  const start = Date.now();
  let receipt = null;
  while (Date.now() - start < timeout) {
    receipt = await web3.eth.getTransactionReceipt(txHash);
    if (receipt) {
      console.log(`Poll at ${Date.now() - start}ms: blockNumber=${receipt.blockNumber}, status=${receipt.status}, logs=${receipt.logs.length}`);
      // Optionally break when status is defined
      if (receipt.status !== undefined) break;
    } else {
      console.log(`Poll at ${Date.now() - start}ms: receipt not yet available`);
    }
    await new Promise(resolve => setTimeout(resolve, 1000));
  }

  if (!receipt) {
    console.log('Timeout: receipt not available within 30s');
  }
}

main().catch(console.error);

结果表:填写你的观察结果

针对你的 Monad 端点运行脚本并记录时间。此表将帮助你了解特定网络上包含与执行之间的关系。

  • 回执可用时间: 从发送交易到首次非空回执响应的时间。
  • 状态定义时间: 回执的 status 字段不再为 undefined 的时间(如果适用)。
  • 首次回执的区块号: 回执首次出现时的区块号。
  • 直到状态确定的轮询次数: 获得明确状态所需的轮询次数。
| Metric | Value (seconds) |
|--------|-----------------|
| Time to first receipt | |
| Time to status defined | |
| Block number at first receipt | |
| Polls until status | |

故障排除:常见陷阱与修复

在集成 Monad 的异步执行时,你可能会遇到源于假设立即执行的问题。以下是常见陷阱及解决方法。

  • 包含后回执不可用: 如果你在区块中看到交易但 eth_getTransactionReceipt 返回 null,则表示执行尚未完成。等待并再次轮询。不要假设交易失败。
  • 状态字段为 null: 如果执行仍在进行中,某些 RPC 实现可能会返回带有 status: null 的回执。将其视为“未知”,而不是失败。
  • 回执后状态未更新: 如果你在收到回执后立即查询余额或调用合约,状态可能尚未反映交易的效果。轮询状态直到符合预期。
  • Nonce 过低或过高: 由于执行是异步的,如果你依赖最新已执行的 nonce,你可能会发送具有过高 nonce 的交易。使用带有 'pending' 的 eth_getTransactionCount 获取下一个预期 nonce。
  • 事件日志缺失: 如果你索引尚未执行的交易的日志,你可能会错过它们。确保在回执可用且执行完成后轮询日志。
  • 回滚的交易: 如果交易回滚,回执将具有 status: 0x0。这是最终结果。但是,回滚可能延迟发生,因此请为延迟失败做好准备。

异步执行的限制与权衡

Monad 的异步执行通过流水线提供了更高的吞吐量,但也为开发人员带来了复杂性。主要权衡是你不能依赖同步执行语义。这会影响调试、事件索引以及任何假设立即状态更改的逻辑。

此外,随着 Monad 向主网迈进,回执可用性和状态语义的确切行为可能会演变。文档是主要来源,但你应该针对目标端点进行测试。例如,某些端点可能仅在执行完成后返回回执,而其他端点可能更早返回。这尚未标准化。

对于历史数据,请注意 Monad 的归档节点可能具有不同的行为。有关更多信息,请参阅我们的指南 通过 RPC 查询 Monad 历史状态

后续步骤与进一步阅读

要在 Monad 上构建可靠的应用程序,你需要了解其独特的交易生命周期。首先阅读官方 Monad 交易生命周期异步执行 文档。然后,在测试网上试验上述脚本。

有关 Monad 特定指导,请探索我们的其他资源:

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始