{"content":{"title":"深度解析：在发送1个DAI时发生了什么","body":"> * 原文[链接](https://www.notonlyowner.com/learn/what-happens-when-you-send-one-dai)，作者：[tincho](https://twitter.com/tinchoabbate)\r\n> * 译文出自：[登链翻译计划](https://github.com/lbc-team/Pioneer)\r\n> * 译者：[翻译小组](https://learnblockchain.cn/people/412)  校对：[Tiny 熊](https://learnblockchain.cn/people/15)\r\n> * 本文永久链接：[learnblockchain.cn/article…](https://learnblockchain.cn/article/5805)\r\n\r\n\r\n\r\n你有 1 个 [DAI](https://makerdao.world/en/learn/Dai/)， 使用钱包（如[Metamask](https://metamask.io/)）发送1个DAI到`0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045`（就是 vitalik.eth），点击发送。\r\n\r\n一段时间后，钱包显示交易已被确认。突然，vitalik.eth 现在有了1个DAI的财富。这背后到底发生了什么？\r\n\r\n让我们回放一下。并以慢动作回放。\r\n\r\n准备好了吗？\r\n\r\n\r\n\r\n\r\n\r\n## 构建交易\r\n\r\n[钱包](https://learnblockchain.cn/tags/%E9%92%B1%E5%8C%85)是便于向以太坊网络发送交易的软件。\r\n\r\n交易只是告诉以太坊网络，你作为一个用户，想要执行一个行动的一种方式。在此案例中，这将是向Vitalik发送1个DAI。而钱包（如Metamask）有助于以一种相对简单的方式建立这种交易。\r\n\r\n让我们先来看看钱包将建立的交易，可以被表示为一个带有字段和相应数值的对象。\r\n\r\n我们的交易开始时看起来像这样：\r\n\r\n```json\r\n{\r\n    \"to\": \"0x6b175474e89094c44da98b954eedeac495271d0f\",\r\n    // [...]\r\n}\r\n```\r\n\r\n其中字段*to*说明目标地址。在此案例中，`0x6b175474e89094c44da98b954eedeac495271d0f`是DAI智能合约的地址。\r\n\r\n等等，什么？\r\n\r\n我们不是应该发送1个DAI给Vitalik吗？`to`不应该是Vitalik的地址吗？\r\n\r\n嗯，不是。要发送DAI，必须制作一个交易，执行存储在区块链（以太坊数据库的花哨名称）中的一段代码，将更新DAI的记录余额。执行这种更新的逻辑和相关存储都保存在以太坊数据库中的一个不可改变的公共计算机程序中 - DAI智能合约。\r\n\r\n因此，你想建立一个交易，告诉合约 `嘿，伙计，更新你的内部余额，从我的余额中取出1个DAI，并添加1个DAI到Vitalik的余额`。在以太坊的行话中，`hey buddy `这句话翻译为在交易的`to`字段中设置DAI的地址。\r\n\r\n然而，\"to\" 字段是不够的。从你喜欢的钱包的用户界面中提供的信息，钱包会要求你填写其他几个字段，以建立一个格式良好的交易：\r\n\r\n```json\r\n{\r\n    \"to\": \"0x6b175474e89094c44da98b954eedeac495271d0f\",\r\n    \"amount\": 0,\r\n    \"chainId\": 31337,\r\n    \"nonce\": 0,\r\n    // [...]\r\n}\r\n```\r\n\r\n所以你给 Vitalik 发送1个DAI，你既没有使用Vitalik的地址，也没有在`amount`字段里填上`1`。这就是生活的艰难（而我们只是在热身）。`amount`字段实际上包含在交易中，表示你在交易中发送多少ETH（以太坊的原始货币）。由于你现在不想发送ETH，那么钱包会正确地将该字段设置为0。\r\n\r\n至于 \"chainId\"，它是一个指定交易执行的链的字段。对于以太坊 Mainnet，它是1。然而，由于我将在mainnet的本地Fork上运行这个实验，我将使用其链ID：31337，[其他链有其他标识符](https://chainlist.org/)。\r\n\r\n那 \"nonce\" 字段呢？那是一个数字，每次你向网络发送交易时都应该增加。它是一种防御机制，以避免重放问题。钱包通常为你设置这个数字。为了做到这一点，他们会查询网络，询问你的账户最新使用的nonce是什么，然后相应地设置当前交易的nonce。在上面的例子中，它被设置为0，尽管在现实中它将取决于你的账户所执行的交易数量。\r\n\r\n我刚才说，钱包 \"查询网络\"。我的意思是，钱包执行对以太坊节点的只读调用，而节点则回答所要求的数据。从以太坊节点读取数据有多种方式，这取决于节点的位置，以及它所暴露的API种类。\r\n\r\n让我们想象一下，钱包可以直接网络访问一个以太坊节点。更常见的是，钱包与第三方供应商(如Infura、Alchemy、QuickNode和许多其他供应商)交互。与节点交互的请求遵循一个特殊的协议来执行远程调用。这种协议被称为[JSON-RPC](https://www.jsonrpc.org/)。\r\n\r\n一个试图获取账户nonce的钱包请求将类似于这样：\r\n\r\n```\r\nPOST / HTTP/1.1\r\nconnection: keep-alive\r\nContent-Type: application/json\r\ncontent-length: 124\r\n\r\n{\r\n    \"jsonrpc\":\"2.0\",\r\n    \"method\":\"eth_getTransactionCount\",\r\n    \"params\":[\"0x6fC27A75d76d8563840691DDE7a947d7f3F179ba\",\"latest\"],\r\n    \"id\":6\r\n}\r\n---\r\nHTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 42\r\n\r\n{\"jsonrpc\":\"2.0\",\"id\":6,\"result\":\"0x0\"}\r\n```\r\n\r\n其中`0x6fC27A75d76d8563840691DDE7a947d7f3F179ba`将是发起者的账户。从响应中你可以看到，它的nonce是0。\r\n\r\n钱包使用网络请求（在此案例中，通过HTTP）来获取数据，请求节点暴露的JSON-RPC端点。上面我只包括了一个，但实际上钱包可以查询任何他们需要的数据来建立一个交易。如果在现实生活中，你注意到有更多的网络请求来查询其他东西，请不要惊讶。例如，下面是一个本地测试节点在几分钟内收到的 Metamask 流量快照：\r\n\r\n\r\n\r\n![本地网络中Metamak流量的Wireshark快照](https://img.learnblockchain.cn/pics/20230516092242.png)\r\n\r\n### 交易的数据字段\r\n\r\nDAI是一个智能合约。它的主要逻辑在以太坊主网的地址`0x6b175474e89094c44da98b954eedeac495271d0f`实现。\r\n\r\n更具体地说，DAI是一个符合ERC20标准的同质代币 -- 一种特殊的合约类型。意思是DAI至少实现[ERC20规范](https://eips.ethereum.org/EIPS/eip-20)中详述的接口。用(有点牵强的)web2术语来说，DAI是一个运行在以太坊上的不可变的开源网络服务。鉴于它遵循ERC20规范，我们有可能提前知道(不一定要看源代码)与它交互的确切暴露的接口。\r\n\r\n简短的附带说明：不是所有的ERC20代币都是这样。实现某种接口（有利于交互和集成），单不能保证具体的行为。不过，在这个练习中，我们可以安全地假设DAI在行为上是相当标准的ERC20代币。\r\n\r\n在DAI智能合约中，有许多功能（源代码可在[这里](https://library.dedaub.com/contracts/Ethereum/6b175474e89094c44da98b954eedeac495271d0f/source)），其中许多直接来自ERC20规范。特别值得注意的是[外部`转移`（external transferr）](https://library.dedaub.com/contracts/Ethereum/6b175474e89094c44da98b954eedeac495271d0f/source?line=122) 函数。\r\n\r\n```\r\ncontract Dai is LibNote {\r\n    ...\r\n    function transfer(address dst, uint wad) external returns (bool) {\r\n        ...\r\n    }\r\n}\r\n```\r\n\r\n这个函数允许任何持有DAI代币的人将其中一部分转账到另一个以太坊账户。它的签名是`transfer(address,uint256)`。其中第一个参数是接收方账户的地址，第二个参数是无符号整数，代表要转账的代币数量。\r\n\r\n现在我们不关注该函数行为的具体细节。相信我，你会了解到的，该函数将发送方的余额减去所传递的金额，然后相应地增加接收方的金额。\r\n\r\n这一点很重要，因为当建立一个交易与智能合约交互时，人们应该知道合约的哪个函数要被执行。以及要传递哪些参数。这就像在web2中，你想向一个网络API发送一个POST请求。你很可能需要在请求中指定确切的URL和它的参数。这也是一样的。我们想转移1个DAI，所以我们必须知道如何在交易中指定它应该在DAI智能合约上执行`转移`功能。\r\n\r\n幸运的是，这是非常直接和直观的。\r\n\r\n哈哈，我开玩笑。不是的。\r\n\r\n下面是你在交易中必须包含的内容，以发送1个DAI给维塔利克（记住，地址`0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045`）：\r\n\r\n```json\r\n{\r\n    // [...]\r\n    \"data\": \"0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000\"\r\n}\r\n```\r\n\r\n让我解释一下。\r\n\r\n为了简化集成，并有一个标准化的方式来与智能合约交互，以太坊生态系统采用（某种形式）的 \"合约ABI规范\"(ABI代表应用二进制接口)。在普通使用场景中，我强调，在普通使用场景中，为了执行智能合约功能，你必须首先按照[合约ABI规范](https://docs.soliditylang.org/en/latest/abi-spec.html)对调用进行编码。更高级的使用场景可能不遵循这个规范，但我们*肯定*不会进入这个兔子洞。我只想说，用[Solidity](https://soliditylang.org/)编程的常规智能合约，如DAI，通常遵循合约ABI规范。\r\n\r\n你可以看到上面是用DAI的`transfer(address,uint256)`函数将1个DAI转移到地址`0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045`的ABI编码的结果字节。\r\n\r\n现在有很多工具可以对交易进行ABI编码（如：https://chaintool.tech/calldata），而且大多数钱包都以某种方式实现ABI编码来与合约交互。为了这个例子，我们可以用一个叫做 [cast](https://learnblockchain.cn/docs/foundry/i18n/zh/reference/cast/cast-calldata.html) 的命令行工具来验证上面的字节序列是否正确，它能够用特定的参数对调用进行ABI-编码：\r\n\r\n```bash\r\n$ cast calldata \"transfer(address,uint256)\" 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 1000000000000000000\r\n\r\n0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000\r\n```\r\n\r\n有什么困扰你的吗？有什么问题吗？\r\n\r\n哦，对不起，是的。那个100000000000000。说实话，我真的很想在这里为你提供一个更有力的论据。很多ERC20代币都用18位小数表示。比如说DAI。\r\n\r\n在合约里我们只能使用无符号整数。因此，1个DAI实际上被存储为1 * 10^18 - 这是100000000000000。\r\n\r\n现在我们有一个漂亮的ABI编码的字节序列，包含在交易的`data`字段中。现在看来是这样的：\r\n\r\n```json\r\n{\r\n    \"to\": \"0x6b175474e89094c44da98b954eedeac495271d0f\",\r\n    \"amount\": 0,\r\n    \"chainId\": 31337,\r\n    \"nonce\": 0,\r\n    \"data\": \"0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000\"\r\n}\r\n```\r\n\r\n一旦我们进入交易的实际执行阶段，我们将重新审视这个`data`字段的内容。\r\n\r\n### Gas \r\n\r\n下一步是决定为交易支付多少钱。因为请记住，所有交易都必须向花费时间和资源来执行和验证它们的节点网络支付费用。\r\n\r\n\r\n\r\n执行交易的费用是以ETH支付的。而ETH的最终数额将取决于你的交易消耗了多少净 [Gas](https://ethereum.org/en/developers/docs/gas/#what-is-gas)（也就是计算成本有多高），你愿意为每个Gas单位的花费支付多少钱，以及网络愿意接受的最低数额。\r\n\r\n从用户的角度来看，通常是，支付的越多，交易的速度就越快。因此，如果你想在下一个区块中向Vitalik支付1个DAI，你可能需要设置一个更高的费用，而不是你愿意等待几分钟（或更长的时间），直到Gas更便宜。\r\n\r\n不同的钱包可能采取不同的方法来决定支付多少Gas费。我不知道有什么单一的机制被所有人使用。确定正确费用的策略可能涉及从节点查询与Gas有关的信息（如网络接受的最低基本费用）。\r\n\r\n例如，在下面的请求中，你可以看到Metamask浏览器插件在建立交易时向本地测试节点发送请求，以获取Gas费数据：\r\n\r\n![Metamask流量查询一个节点的Gas相关数据](https://img.learnblockchain.cn/pics/20230516092249.png)\r\n\r\n而简化后的请求-响应看起来像：\r\n\r\n```json\r\nPOST / HTTP/1.1\r\nContent-Type: application/json\r\nContent-Length: 99\r\n\r\n{\r\n    \"id\":3951089899794639,\r\n    \"jsonrpc\":\"2.0\",\r\n    \"method\":\"eth_feeHistory\",\r\n    \"params\":[\"0x1\",\"0x1\",[10,20,30]]\r\n}\r\n---\r\nHTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 190\r\n\r\n{\r\n    \"jsonrpc\":\"2.0\",\r\n    \"id\":3951089899794639,\r\n    \"result\":{\r\n        \"oldestBlock\":\"0x1\",\r\n        \"baseFeePerGas\":[\"0x342770c0\",\"0x2da4d8cd\"],\r\n        \"gasUsedRatio\":[0.0007],\r\n        \"reward\":[[\"0x59682f00\",\"0x59682f00\",\"0x59682f00\"]]\r\n    }\r\n}\r\n```\r\n\r\n`eth_feeHistory`端点被一些节点暴露出来，允许查询交易费用数据。如果你很好奇，可以阅读[这里](https://docs.infura.io/infura/networks/ethereum/json-rpc-methods/eth_feehistory)或[这里](https://composer.alchemyapi.io/?share=eJwdyEEKgCAQBdC7.LULCyTwBK1atomIoSaKUkMnIqK7J_0e78G40OphtYJnuULcfjuWJUwNOYZF9jAz12uSEG8oHBTJtbSfnGA7FDrfTsJJMrrSKKNVZXr07wcDJR59)玩玩它，或者看看规范[这里](https://github.com/ethereum/execution-apis/blob/main/src/eth/fee_market.json#L14-L94)。\r\n\r\n流行的钱包也使用更复杂的链外服务来获取Gas交易成本来估计，并向用户建议合理的价值。这里有一个例子，一个钱包请求了一个网络服务的公共端点，并收到了一堆有用的Gas相关数据：\r\n\r\n![Wireshark流量包括eth_feeHistory请求](https://img.learnblockchain.cn/pics/20230516092303.png)\r\n\r\n看一下响应片段：\r\n\r\n![Wireshark流量包括eth_feeHistory响应](https://img.learnblockchain.cn/pics/20230516092307.png)\r\n\r\n很酷，对吗？\r\n\r\n无论如何，希望你能熟悉设置Gas费用价格并不简单，它是建立一个成功交易的基本步骤。即使你想做的只是发送1个DAI。[这里](https://www.blocknative.com/blog/eip-1559-fees)是一个有趣的介绍性指南，可以深入挖掘其中的一些机制，在交易中设置更准确的费用。\r\n\r\n在一些初步的背景下，现在让我们回到实际的交易。有三个与Gas有关的字段需要设置：\r\n\r\n```json\r\n{\r\n    \"maxPriorityFeePerGas\": ...,\r\n    \"maxFeePerGas\": ...,\r\n    \"gasLimit\": ...,\r\n}\r\n```\r\n\r\n钱包将使用一些提到的机制来为你填写前两个字段。有趣的是，每当钱包UI让你在某个版本的 \"慢速\"、\"常规 \"或 \"快速\"交易中进行选择时，它实际上是在试图决定什么值最适合这些确切的参数。现在你可以更好地理解上面从钱包收到的JSON格式的响应内容了。\r\n\r\n为了确定第三个字段的值，即GasLimit，有一个方便的机制，钱包可以用来在真正提交交易之前模拟交易。这使他们能够确切的估计一笔交易会消耗多少Gas，从而设定一个合理的GasLimit。\r\n\r\n为什么不直接设置一个巨大的GasLimit？当然是为了保护你的资金。智能合约可能有任意的逻辑，你是为其执行付费的人。通过在交易开始时就选择一个合理的GasLimit，你可以保护自己，避免在Gas费用中耗尽你账户的所有ETH资金的尴尬情况。\r\n\r\n可以通过节点的 \"eth_estimateGas\" 端点进行Gas估算。在发送1个DAI之前，钱包可以利用这一机制来模拟你的交易，并确定你的DAI转账的正确GasLimit。来自钱包的请求-回应可能是这样的：\r\n\r\n```json\r\nPOST / HTTP/1.1\r\nContent-Type: application/json\r\n\r\n{\r\n    \"id\":2697097754525,\r\n    \"jsonrpc\":\"2.0\",\r\n    \"method\":\"eth_estimateGas\",\r\n    \"params\":[\r\n        {\r\n            \"from\":\"0x6fC27A75d76d8563840691DDE7a947d7f3F179ba\",\r\n            \"value\":\"0x0\",\r\n            \"data\":\"0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000\",\r\n            \"to\":\"0x6b175474e89094c44da98b954eedeac495271d0f\"\r\n        }\r\n    ]\r\n}\r\n---\r\nHTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"jsonrpc\":\"2.0\",\"id\":2697097754525,\"result\":\"0x8792\"}\r\n```\r\n\r\n在响应中，你可以看到，转账将需要大约 34706 个Gas单位。\r\n\r\n让我们把这些信息纳入交易的有效载荷中：\r\n\r\n```json\r\n{\r\n    \"to\": \"0x6b175474e89094c44da98b954eedeac495271d0f\",\r\n    \"amount\": 0,\r\n    \"chainId\": 31337,\r\n    \"nonce\": 0,\r\n    \"data\": \"0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000\",\r\n    \"maxPriorityFeePerGas\": 2000000000,\r\n    \"maxFeePerGas\": 120000000000,\r\n    \"gasLimit\": 40000\r\n}\r\n```\r\n\r\n\r\n\r\n记住，\"maxPriorityFeePerGas \"和 \"maxFeePerGas \"最终将取决于发送交易时的网络条件。上面我只是为了这个例子而设置了一些任意的值。至于为GasLimit设置的值，我只是把估计值增加了一点，以提交执行交易的可能性。\r\n\r\n### 访问列表和交易类型\r\n\r\n\r\n\r\n让我们简单评论一下在你的交易中设置的另外两个字段。\r\n\r\n首先，`accessList`字段。高级使用场景或边缘场景可能需要交易提前指定要访问的账户地址和合约的存储槽，从而使交易的成本降低一些。\r\n\r\n然而，提前建立这样的列表可能并不直接，目前节省的Gas可能并不那么显著。特别是对于简单的交易，如发送1个DAI。因此，我们可以直接将其设置为一个空的列表。尽管记住它确实存在[有原因](https://eips.ethereum.org/EIPS/eip-2930)，而且它在未来可能变得更有意义。\r\n\r\n第二，[交易类型](https://eips.ethereum.org/EIPS/eip-2718)。它在 \"type\" 字段中被指定。类型是交易内部内容的一个指标。我们的将是一个类型2的交易--因为它遵循[这里](https://eips.ethereum.org/EIPS/eip-1559#abstract)指定的格式。\r\n\r\n```\r\n{\r\n    \"to\": \"0x6b175474e89094c44da98b954eedeac495271d0f\",\r\n    \"amount\": 0,\r\n    \"chainId\": 31337,\r\n    \"nonce\": 0,\r\n    \"data\": \"0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000\",\r\n    \"maxPriorityFeePerGas\": 2000000000,\r\n    \"maxFeePerGas\": 120000000000,\r\n    \"gasLimit\": 40000,\r\n    \"accessList\": [],\r\n    \"type\": 2\r\n}\r\n```\r\n\r\n### 签署交易\r\n\r\n节点如何知道是*你的*账户，而不是其他人的账户在发送交易？\r\n\r\n我们已经来到了建立有效交易的关键步骤：签名。\r\n\r\n一旦钱包收集了足够的信息来建立交易，并且你点击发送，它将对你的交易进行数字签名。如何签名？使用你的账户的私钥(你的钱包可以访问)，和一个涉及椭圆曲线的加密算法，称为[ECDSA](https://en.wikipedia.org/wiki/ECDSA)。\r\n\r\n对于好奇的人来说，实际上被签署的是交易类型和 [RLP编码](https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/) 内容之间的串联的`keccak256`哈希值。\r\n\r\n```\r\nkeccak256(0x02 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, amount, data, accessList]))\r\n```\r\n\r\n虽然你不应该有那么多的密码学知识来理解这个。简单地说，这个过程是对交易的密封。它通过在上面盖上一个只有你的私钥才能产生的聪明的印章，使其具有防篡改性。从现在开始，任何能够访问该签名交易的人（例如，以太坊节点）都可以通过密码学来验证是你的账户产生了该交易。\r\n\r\n明确一下：签名不是加密。你的交易始终是明文的。一旦它们被公开，任何人都可以从它们的内容中获得其含义。\r\n\r\n\r\n\r\n签署交易的过程中，毫不奇怪，会产生一个签名。在实践中是一堆奇怪的不可读的值，你通常会发现它们被称为`v`，`r`和`s`。如果你想更深入地了解这些实际代表的内容，以及它们对还原你的账户地址的重要性，互联网是你的朋友。\r\n\r\n你可以通过查看[@ethereumjs/tx](https://github.com/ethereumjs/ethereumjs-monorepo/tree/master/packages/tx)软件包来更好地了解签名实现时的样子。也可以使用[ethers](https://docs.ethers.io/v5/single-page/)包中的一些实用工具。作为一个极其简化的例子，签署交易以发送1个DAI可以是这样的：\r\n\r\n```javascript\r\nconst { FeeMarketEIP1559Transaction } = require(\"@ethereumjs/tx\");\r\n\r\nconst txData = {\r\n    to: \"0x6b175474e89094c44da98b954eedeac495271d0f\",\r\n    amount: 0,\r\n    chainId: 31337,\r\n    nonce: 0,\r\n    data: \"0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000\",\r\n    maxPriorityFeePerGas: ethers.utils.parseUnits('2', 'gwei').toNumber(),\r\n    maxFeePerGas: ethers.utils.parseUnits('120', 'gwei').toNumber(),\r\n    gasLimit: 40000,\r\n    accessList: [],\r\n    type: 2,\r\n};\r\n\r\nconst tx = FeeMarketEIP1559Transaction.fromTxData(txData);\r\nconst signedTx = tx.sign(Buffer.from(process.env.PRIVATE_KEY, 'hex'));\r\n\r\nconsole.log(signedTx.v.toString('hex'));\r\n// 1\r\n\r\nconsole.log(signedTx.r.toString('hex'));\r\n// 57d733933b12238a2aeb0069b67c6bc58ca8eb6827547274b3bcf4efdad620a\r\n\r\nconsole.log(signedTx.s.toString('hex'));\r\n// e49937ec81db89ce70ebec5e51b839c0949234d8aad8f8b55a877bd78cc293\r\n```\r\n\r\n由此产生的对象将看起来像：\r\n\r\n```json\r\n{\r\n    \"to\": \"0x6b175474e89094c44da98b954eedeac495271d0f\",\r\n    \"amount\": 0,\r\n    \"chainId\": 31337,\r\n    \"nonce\": 0,\r\n    \"data\": \"0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000\",\r\n    \"maxPriorityFeePerGas\": 2000000000,\r\n    \"maxFeePerGas\": 120000000000,\r\n    \"gasLimit\": 40000,\r\n    \"accessList\": [],\r\n    \"type\": 2,\r\n    \"v\": 1,\r\n    \"r\": \"57d733933b12238a2aeb0069b67c6bc58ca8eb6827547274b3bcf4efdad620a\",\r\n    \"s\": \"e49937ec81db89ce70ebec5e51b839c0949234d8aad8f8b55a877bd78cc293\",\r\n}\r\n```\r\n\r\n### 序列化\r\n\r\n下一步是*序列化*签名的交易。这意味着将上面的漂亮对象编码成一个二进制字节序列，这样它就可以被发送到以太坊网络并被接收的节点消费。\r\n\r\n以太坊选择的编码方法被称为[RLP](https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/)。交易的编码方式如下：\r\n\r\n```\r\n0x02 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, v, r, s])\r\n```\r\n\r\n其中初始字节是交易类型。\r\n\r\n在前面的代码片段的基础上，你可以实际看到序列化的交易这样添加：\r\n\r\n```json\r\nconsole.log(signedTx.serialize().toString('hex'));\r\n// 02f8b1827a69808477359400851bf08eb000829c40946b175474e89094c44da98b954eedeac495271d0f80b844a9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000c001a0057d733933b12238a2aeb0069b67c6bc58ca8eb6827547274b3bcf4efdad620a9fe49937ec81db89ce70ebec5e51b839c0949234d8aad8f8b55a877bd78cc293\r\n```\r\n\r\n这就是在我的以太坊主网上的本地 Fork 中向Vitalik发送 1 个DAI的实际有效载荷。\r\n\r\n### 提交交易\r\n\r\n一旦建立、签署和序列化，该交易必须被发送到一个以太坊节点。\r\n\r\n节点会提供方便的JSON-RPC端点，节点可以在那里接收交易请求。\r\n\r\n发送交易使用`eth_sendRawTransaction`。下面是一个钱包在提交交易时使用的网络流量：\r\n\r\n![Wireshark使用eth_sendRawTransaction方法发送原始交易的流量](https://img.learnblockchain.cn/pics/20230516092314.png)\r\n\r\n总结的请求-响应看起来像：\r\n\r\n```json\r\nPOST / HTTP/1.1\r\nContent-Type: application/json\r\nContent-Length: 446\r\n\r\n{\r\n    \"id\":4264244517200,\r\n    \"jsonrpc\":\"2.0\",\r\n    \"method\":\"eth_sendRawTransaction\",\r\n    \"params\":[\"0x02f8b1827a69808477359400851bf08eb000829c40946b175474e89094c44da98b954eedeac495271d0f80b844a9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000c001a0057d733933b12238a2aeb0069b67c6bc58ca8eb6827547274b3bcf4efdad620a9fe49937ec81db89ce70ebec5e51b839c0949234d8aad8f8b55a877bd78cc293\"]\r\n}\r\n---\r\nHTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 114\r\n\r\n{\r\n    \"jsonrpc\":\"2.0\",\r\n    \"id\":4264244517200,\r\n    \"result\":\"0xbf77c4a9590389b0189494aeb2b2d68dc5926a5e20430fb5bc3c610b59db3fb5\"\r\n}\r\n```\r\n\r\n响应中包含的结果包含交易的哈希值：`bf77c4a9590389b0189494aeb2b2d68dc5926a5e20430fb5bc3c610b59db3fb5`.这个32字节长的十六进制字符序列是所提交交易的唯一标识符。\r\n\r\n## 节点接收\r\n\r\n我们应该如何去弄清楚当以太坊节点收到序列化的签名交易时会发生什么？\r\n\r\n有些人可能会在Twitter上询问，有些人可能会阅读一些Medium文章。其他的人甚至可能会阅读文档，或者看[视频](https://www.youtube.com/watch?v=uZ7vkmUNTPA)\r\n\r\n\r\n\r\n只有一个地方可以找到真相：在源码。让我们用[go-ethereum v1.10.18](https://github.com/ethereum/go-ethereum/tree/v1.10.18)（又名Geth），一个流行的以太坊节点的实现（一旦以太坊转向Proof-of-Stake，就是 \"执行客户端\"）。从现在开始，我将包括Geth的源代码链接，以便你能跟随。\r\n\r\n在收到对其`eth_sendRawTransaction`[端点](https://github.com/ethereum/go-ethereum/blob/v1.10.18/internal/ethapi/api.go#L1698-L1706)的JSON-RPC调用后，该节点需要对请求正文中包含的序列化交易进行分析。所以[它开始对交易进行反序列化](https://github.com/ethereum/go-ethereum/blob/v1.10.18/internal/ethapi/api.go#L1702)。从现在开始，节点将能更容易地访问交易的字段。\r\n\r\n在这一点上，节点已经开始验证交易了。[首先](https://github.com/ethereum/go-ethereum/blob/v1.10.18/internal/ethapi/api.go#L1623-L1625)，确保交易的费用（即价格 * GasLimit）不超过节点愿意接受的最大限度（显然，[默认情况下，这是一个以太币](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/ethconfig/config.go#L95)）。还有[然后](https://github.com/ethereum/go-ethereum/blob/v1.10.18/internal/ethapi/api.go#L1626-L1629)，确保交易是受重放保护的（按照[EIP155](https://eips.ethereum.org/EIPS/eip-155)--记得我们在交易中设置的`链ID`字段吗？），或者节点愿意接受不受保护的交易。\r\n\r\n接下来的步骤包括[发送](https://github.com/ethereum/go-ethereum/blob/v1.10.18/internal/ethapi/api.go#L1630)[交易](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/api_backend.go#L245-L247)[到](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L843-L850)[交易池](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L226-L271)（又称mempool）。简单地说，这个池子代表了节点在某个特定时刻所知道的交易集合。就只有节点所知，这些还没有被纳入区块链。\r\n\r\n在*真正*将交易纳入池中之前，节点[检查](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L896-L901)它是否已经知道它。而且它的ECDSA签名是[有效的](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L902-L910)。否则就抛弃该交易。\r\n\r\n然后[沉重的 mempool ](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L648-L753)开始。正如你可能注意到的，有很多琐碎的逻辑来确保交易池是“快乐和健康”的。\r\n\r\n这里有相当多的重要[验证](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L586-L646)。例如，GasLimit [低于区块 GasLimit](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L604-L607)，或者交易的大小[不超过](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L595-L598)[允许的最大](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L49-L53)，或者 nonce 是[预期的](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L628-L631)，或者发送方有[足够的资金](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L632-L636)来支付潜在的成本（即价值 + GasLimit*价格），等等。\r\n\r\n虽然我们可以继续下去，但我们在这里不是要成为mempool专家。即使我们想这样做，我们也需要考虑，只要他们遵循网络共识规则，每个节点运营商可能采取不同的方法来管理mempool。这意味着执行特殊的验证或遵循自定义的交易优先级规则。为了只发送1个DAI，我们可以将 mempool 视为一组急切等待被拾取并被纳入区块的交易。\r\n\r\n在成功地将交易添加到池中（并做内部[记录](https://github.com/ethereum/go-ethereum/blob/v1.10.18/internal/ethapi/api.go#L1633-L1645)的事情），节点[返回交易哈希值](https://github.com/ethereum/go-ethereum/blob/v1.10.18/internal/ethapi/api.go#L1646)。这正是我们之前在JSON-RPC请求-响应中看到的返回内容😎。\r\n\r\n### 检查 mempool\r\n\r\n如果你通过 Metamask 或任何默认连接到传统节点的类似钱包发送交易，在某些时候，它将“降落”在公共节点的mempools上。你可以通过自己检查mempools来确保这一点。\r\n\r\n\r\n\r\n有一个方便的端点，一些节点暴露了出来，叫做`eth_newPendingTransactionFilter`。它也许是[frontrunning（抢跑）](https://www.paradigm.xyz/2020/08/ethereum-is-a-dark-forest) [bots](https://arxiv.org/pdf/1904.05234.pdf) 的好朋友。定期查询这个端点可以让我们在交易被纳入链中之前观察到，现在 1个 DAI 进入了本地测试节点的mempool中。\r\n\r\n在Javascript代码中，这可以通过以下方式完成：\r\n\r\n```\r\nconst hre = require(\"hardhat\");\r\n\r\nhre.ethers.provider.on('pending', async function (tx) {\r\n    // do something with the transaction\r\n});\r\n```\r\n\r\n要看到实际的`eth_newPendingTransactionFilter`调用，我们可以直接检查网络流量：\r\n\r\n![Wireshark流量与JSON-RPC调用订阅待处理交易](https://img.learnblockchain.cn/pics/20230516092321.png)\r\n\r\n从现在开始，脚本将（自动）轮询mempool中的变化。这是随后的许多周期性调用中的第一个，检查变化：\r\n\r\n![Wireshark流量与JSON-RPC调用询问mempool中的变化](https://img.learnblockchain.cn/pics/20230516092326.png)\r\n\r\n在收到交易后，节点最终用它的哈希值来响应：\r\n\r\n![Wireshark流量与JSON-RPC调用回答检测到的交易哈希](https://img.learnblockchain.cn/pics/20230516092330.png)\r\n\r\n总结的请求 - 响应看起来像：\r\n\r\n```json\r\nPOST / HTTP/1.1\r\nContent-Type: application/json\r\ncontent-length: 74\r\n\r\n{\r\n    \"jsonrpc\":\"2.0\",\r\n    \"method\":\"eth_getFilterChanges\",\r\n    \"params\":[\"0x1\"],\r\n    \"id\":58\r\n}\r\n---\r\nHTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 105\r\n\r\n{\r\n    \"jsonrpc\":\"2.0\",\r\n    \"id\":58,\r\n    \"result\":[\"0xbf77c4a9590389b0189494aeb2b2d68dc5926a5e20430fb5bc3c610b59db3fb5\"]\r\n}\r\n```\r\n\r\n\r\n\r\n早些时候我说过 \"传统节点\"，但没有解释太多。我的意思是，有一些[更专业的节点](https://github.com/flashbots/mev-geth)具有私人内存池的特点。它们允许用户在交易被纳入区块之前，从公众那里 \"隐藏 \"交易。\r\n\r\n不管具体情况如何，这种机制通常包括在交易发起者和区块构建者之间建立私人通道。[Flashbots保护服务](https://docs.flashbots.net/flashbots-protect/rpc/quick-start)就是一个明显的例子。实际的结果是，即使你用上面的方法来监控mempools，你也不能通过私人通道来获取那些进入区块构建者的交易。\r\n\r\n假设发送1个DAI的交易是通过普通通道提交给网络的，没有利用这种服务。\r\n\r\n## 传播\r\n\r\n为了使交易被包含在区块中，它需要以某种方式到达能够建立和提出交易的节点。在[工作量证明](https://ethereum.org/en/developers/docs/consensus-mechanisms/pow/)以太坊中，这些节点被称为矿工。在[Proof-of-Stake](https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/)以太坊中，称为验证者。虽然现实往往更复杂一些。请注意，可能有一些方法可以将[区块构建](https://github.com/ethereum/builder-specs)外包给专业服务。\r\n\r\n作为一个普通用户，你应该不需要知道这些区块生产者是谁，也不需要知道他们在哪里。相反，你可以简单地发送一个有效的交易到网络中的任何常规节点，让它包括在交易池中，并让点对点协议做他们的事情。\r\n\r\n有[一些这样的p2p协议](https://github.com/ethereum/devp2p/)将以太坊节点相互连接。除其他事项外，它们允许频繁地[交换交易](https://github.com/ethereum/devp2p/blob/master/caps/eth.md#transaction-exchange)。\r\n\r\n从一开始，所有节点都与他们的对等节点（默认情况下，[最多50个对等节点（Peers）](https://github.com/ethereum/go-ethereum/blob/v1.10.18/node/defaults.go#L64)）一起[监听和广播](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/handler.go#L524-L528)交易。\r\n\r\n一旦一个交易到达mempool，它就会被[发送给所有尚未知道该交易的连接对等节点](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/handler.go#L603-L607)。\r\n\r\n为了提高效率，只有一个随机的连接节点子集（[平方根](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/handler.go#L621-L622) 🤓）被[发送完整交易](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/peer.go#L183-L198)。其余的是[只发送交易哈希](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/peer.go#L213-L223)。如果需要的话，这些节点可以请求返回完整的交易。\r\n\r\n一个交易不能永远停留在一个节点的mempool中。如果它没有被其他原因首先被丢弃（例如，池子满了，交易价格低了，或者它被更高的nonce/价格的新交易取代）的话，它可能在一定时间后（默认为[3小时](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L184)）被自动[删除](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L390-L396)。\r\n\r\n\r\n\r\n在mempool中被认为可以被区块构建者拾取和处理的有效交易被[跟踪](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L1338-L1345)在一个[待处理交易](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L254)的列表中。这个数据结构[可以被区块构建者查询](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/tx_pool.go#L536)，以获得被允许进入链上的可处理交易。\r\n\r\n## 工作准备和交易纳入\r\n\r\n交易应该在浏览了mempools之后到达一个挖矿节点（至少在写这篇文章的时候）。这种类型的节点是特别重的多任务处理器。对于熟悉Golang的人来说，这意味着在挖矿相关的逻辑中，有相当多的go例程和通道。对于那些不熟悉Golang的人来说，这意味着矿工的常规操作不能像我想的那样被线性解释。\r\n\r\n本节的目标有两个方面。首先，了解我们的交易是如何以及何时被矿工从mempool中提取的。第二，找出交易的执行在哪一点上开始。\r\n\r\n当节点的挖矿组件被初始化时，至少有两件相关的事情发生。第一，它[开始监听](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L280)新交易到达mempool的情况。第二，[一些基本的循环](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L293-L296)被触发了。\r\n\r\n在Geth的行话中，用交易建立一个区块并将其密封的行为被称为 \"提交工作（committing work）\"。因此，我们想了解这是在什么情况下发生的。\r\n\r\n重点是[\"新工作\"loop](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L531-L627)。这是一个独立的例程，当节点收到[不同类型的通知](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L456-L477)时，会触发工作提交。该触发器需要[发送一个工作要求](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L436)到该节点的另一个活跃监听器（运行在矿工的[\"main\" loop](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L456-L459)中）。当[收到](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L533)这样的工作要求时，[提交工作开始](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L534)。\r\n\r\n节点开始进行一些[初始准备](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L1114-L1117)。主要包括建立区块头。这包括[寻找父区块](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L982-L989)，确保[正在建立的区块的时间戳](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L990-L998)是正确的，[设置区块编号](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L1003)，[GasLimit](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L1004)，[coinbase地址](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L1006)和[基本费用](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L1017)等任务。\r\n\r\n之后，共识引擎被调用，进行区块头的[\"共识准备\"](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L1024-L1027)。这[计算出正确的区块难度](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/consensus.go#L587)（[取决于](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/consensus.go#L337-L351)当前的网络版本）。如果你听说过以太坊的 \"难度炸弹\"，你就知道了。\r\n\r\n> 译者注： TheMerge 之后已经没有难度炸弹了。\r\n\r\n接下来，区块密封上下文[被创建](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L1031-L1035)。撇开其他动作，这包括[获取最后的已知状态](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L758-L768)。这是正在建立的区块中的第一个交易将被执行的状态。这可能是我们的交易发送1个DAI。\r\n\r\n在准备好区块后，它就开始[填充交易](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L1128)。\r\n\r\n我们达到了这里：到目前为止，我们的未决（pending）交易只是舒适地坐在节点的内存池中，与其他交易一起[被拾起](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L1063)。\r\n\r\n默认情况下，[交易在一个区块内按价格和nonce排序](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L1071-L1082)。对于我们的情况，交易在区块中的位置实际上是无关的。\r\n\r\n\r\n\r\n现在开始按顺序[执行这些交易](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L849)。一个交易[被执行](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L901-L904)之后，每个交易都建立在前一个交易的结果状态之上。\r\n\r\n## 执行\r\n\r\n一个以太坊交易可以被认为是一个状态转换。\r\n\r\n状态0：你有100个DAI，Vitalik也有100个。\r\n\r\n交易：你发送1个DAI给Vitalik。\r\n\r\n状态1：你有99个DAI，而Vitalik有101个。\r\n\r\n因此，执行交易需要*对区块链的当前状态应用*一系列的操作。产生一个新的（不同的）状态作为结果。这将被认为是新的当前状态，直到有另一个交易进来。\r\n\r\n在现实中，这更有趣（也更复杂）。让我们来看看。\r\n\r\n### 准备工作（第一部分）\r\n\r\n用Geth的行话说，矿工在区块中[提交交易](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L835)。提交交易的行为是在一个[环境](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L87)中进行的。这种环境包含一个特定的[状态](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L90)（先不管其他的）。\r\n\r\n因此，简而言之，提交一个交易本质上是：[(1)](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L836)记住当前的状态，[(2)](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L838)通过应用交易来修改它，[(3)](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L839-L842)根据交易的成功，要么接受新状态，要么回滚到原来的状态。\r\n\r\n有趣的事情发生在（2）：[应用交易](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L144)。\r\n\r\n首先要注意的是，[交易被变成了一个 消息“Message”](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L145)。如果你熟悉Solidity，在那里你通常会写诸如`msg.data`或`msg.sender`这样的东西，最后在Geth的代码中读到 `message `就是欢迎你进入友好之地的标志。\r\n\r\n当检查[消息](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/types/transaction.go#L563)时，会很快就会注意到它与交易的至少一个区别。一条信息有[一个`from`字段](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/types/transaction.go#L565)!这个字段是签名者的以太坊地址，它是由交易中的[公共签名](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/types/transaction.go#L612)[衍生出来的](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/types/transaction_signing.go#L195)（还记得奇怪的`v`、`r`和`s`字段吗？）。\r\n\r\n现在，执行的环境[被进一步准备](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L149-L151)。首先，[与区块相关的环境被创建](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/evm.go#L58-L69)，其中包括区块编号、时间戳、coinbase地址和区块GasLimit等内容。然后...\r\n\r\n[野兽走了进来](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L151)，它就是以太坊虚拟机。\r\n\r\n以太坊虚拟机（EVM），负责执行交易的[基于堆栈的](https://en.wikipedia.org/wiki/Stack_machine)[256位](https://en.wikipedia.org/wiki/256-bit_computing)计算引擎，我们可以期待它做什么？\r\n\r\nEVM是一台机器。作为一台机器，它有一套可以执行的指令（又称操作码）。该指令集多年来一直在变化。因此，一定会有段代码告诉EVM今天应该使用哪些操作码。当EVM[实例化解释器](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L137)时，它[选择正确的操作代码集](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L72-L101)，取决于正在使用的版本。\r\n\r\n最后，在[真正的执行](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L101)之前有[两个最后步骤](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L97-L98)。EVM的交易上下文被创建（在你的Solidity智能合约中使用过`tx.origin`或`tx.gasPrice`吗？），EVM被赋予访问当前状态的权限。\r\n\r\n### 准备工作(第二部分)\r\n\r\n\r\n\r\n现在轮到EVM执行[状态转换](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L181)了。给定一个信息、一个环境和原始状态，它将使用一组有限的指令来转移到一个新的状态。其中，维塔利克有1个额外的DAI💰。\r\n\r\n在应用状态转换之前，EVM必须确保它遵守[特定的共识规则](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L279-L284)。让我们看看这一点是如何做到的。\r\n\r\n验证开始于Geth所说的[\"预检查\"](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L287)，它包括：\r\n\r\n1. 验证信息的nonce。它[必须与](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L218-L224)信息的 \"from\"地址的nonce匹配。此外，它[必须不是可能的最大nonce](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L225-L228)（通过检查nonce +1是否会导致溢出）。\r\n2. 确保与信息的`from`地址相对应的账户[没有代码](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L229-L233)。也就是说，交易起源是一个外部拥有的账户（EOA）。从而遵守[EIP 3607](https://eips.ethereum.org/EIPS/eip-3607)规范。\r\n3. 验证交易中设置的 `maxFeePerGas`（Geth中的 `gasFeeCap`）和 `maxPriorityFeePerGas`（Geth中的 `gasTipCap`）字段是[在预期范围内](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L239-L246)。此外，优先权费用[不大于](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L247-L250)最大费用。并且`maxFeePerGas`[大于](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L253)当前区块的基本费用。\r\n4. [购买Gas](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L259)，检查账户是否[能够支付](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L201-L203)它打算消费的所有Gas。而且该区块中还有[足够的Gas](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L204-L206)来处理这笔交易。最后让账户[提前支付](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L210)Gas费用（别担心，以后还有退款机制）。\r\n\r\n接下来，EVM[核算交易消耗的 \"内在 (intrinsic) Gas\"](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L305-L313)。在计算内在Gas时，有几个因素需要考虑。首先，[交易是否是合约创建](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L124)。我们这里不是，所以Gas 初始为21000 个单位](https://github.com/ethereum/go-ethereum/blob/v1.10.18/params/protocol_params.go#L32)。之后，信息的 \"数据 \"字段中的[非零字节的数量](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L124)也被考虑在内。每个非零字节收取[16个单位](https://github.com/ethereum/go-ethereum/blob/v1.10.18/params/protocol_params.go#L88)（遵循[本规范](https://eips.ethereum.org/EIPS/eip-2028)）。每个零字节只收取[4个单位](https://github.com/ethereum/go-ethereum/blob/v1.10.18/params/protocol_params.go#L34)的费用。最后，[如果我们提供访问列表](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L124)，一些更多的Gas将被提前计算。\r\n\r\n我们将交易的`value`字段设置为零。如果我们指定一个正值，现在将是EVM检查[发送方账户是否真的有足够的余额](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L316-L318)以执行ETH转账的时刻。此外，如果我们设置了访问列表，[现在它们将被初始化为状态](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L320-L323)。\r\n\r\n正在执行的交易并不是在创建一个合约。EVM知道它[因为`to`字段不是零](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L302)。因此，它将起者的账户nonce[增加](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L332)1，并[执行一个调用](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L333)。\r\n\r\n调用将从`from`到`to`信息的地址，传递`data`，没有value，以及消耗内在Gas后剩下的任何Gas。\r\n\r\n\r\n\r\n### 调用\r\n\r\nDAI智能合约存储在地址`0x6b175474e89094c44da98b954eedeac495271d0f`。这就是我们在交易的`to`字段中设置的地址。这个初始调用是为了让EVM执行存储在它那里的任何代码，逐个操作码执行。\r\n\r\n操作码是EVM的指令，用十六进制数字表示，范围从00到FF。尽管它们通常用它们的名字来指代。例如，`00`是`STOP`，`FF`是`SELFDESTRUCT`。一个方便的操作码列表可以在[evm.codes](https://www.evm.codes/)上找到。\r\n\r\n那么DAI的操作码到底是什么？很高兴你这么问：\r\n\r\n![DAI智能合约的EVM操作码](https://img.learnblockchain.cn/pics/20230516092339.png)\r\n\r\n不要惊慌。现在要想弄清这一切还为时尚早。\r\n\r\n让我们慢慢开始，把[初始调用](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L168)分解。它的[简要文档](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L164-L167)提供了一个很好的总结：\r\n\r\n```go\r\n// Call executes the contract associated with the addr with the given input as\r\n// parameters. It also handles any necessary value transfer required and takes\r\n// the necessary steps to create accounts and reverses the state in case of an\r\n// execution error or failed value transfer.\r\nfunc (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {\r\n    ...\r\n}\r\n```\r\n\r\n首先，逻辑[检查](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L169-L172)是否已经触及调用深度限制。这个限制是[设置为1024](https://github.com/ethereum/go-ethereum/blob/v1.10.18/params/protocol_params.go#L75)，这意味着在一个交易中最多只能有1024个嵌套调用。[这里](https://medium.com/arbitrary-execution/testing-the-limits-of-evm-stack-depth-c40ba55ca78e)是一篇有趣的文章，可以阅读关于EVM这种行为背后的一些推理和微妙之处。稍后我们将探讨如何增加/减少调用深度。\r\n\r\n相关的附带说明：调用深度限制 *并不是* EVM的堆栈大小限制 -- 堆栈大小（巧合？）也[是1024个元素](https://github.com/ethereum/go-ethereum/blob/v1.10.18/params/protocol_params.go#L79)。\r\n\r\n下一步是[确保](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L173-L176)，如果在调用中指定了一个正的 value ，那么发送方有足够的余额来执行转移（[执行](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L196)几步之后）。我们可以忽略这一点，因为我们调用的value是零。此外，一个[当前状态的快照](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L177)被拍摄下来。这允许在失败时[轻松恢复](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L235-L236)任何状态变化。\r\n\r\n我们知道DAI的地址指的是一个有代码的账户。因此，它[必须已经存在](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L180)于以太坊的状态空间里。\r\n\r\n然而，让我们暂时想象一下，这不是一个发送1个DAI的交易。假设它是一个没有价值的垃圾交易，目标是一个新的地址。相应的账户将需要被[添加到状态](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L194)。然而，如果该账户缺只是空的呢？除了浪费节点的磁盘空间之外，似乎没有理由对它进行跟踪。[EIP 158](https://eips.ethereum.org/EIPS/eip-158)对以太坊协议进行了一些修改，以帮助避免这种情况的发生。这就是为什么你在调用任何账户时看到[这个`if`](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L181)条件。\r\n\r\n我们知道的另一件事是，DAI[不是预编译合约](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L214-L231)。什么是预编译合约？下面是[以太坊黄皮书](https://ethereum.github.io/yellowpaper/paper.pdf#section.8)提供的内容：\r\n\r\n> [......]初步的架构预备代码片段，以后可能成为原生扩展。地址1到9的合约分别执行椭圆曲线公钥恢复函数、SHA2 256位Hash方案、RIPEMD 160位Hash方案、身份函数、任意精度的模块化指数、椭圆曲线加法、椭圆曲线标量乘法、椭圆曲线配对检查以及BLAKE2压缩函数F。\r\n\r\n简而言之，在以太坊的状态下，（到目前为止）有9个不同的特殊合约。这些账户（范围从[0x0000000000000000000000000001](https://etherscan.io/address/0x0000000000000000000000000000000000000001)到[0x0000000000000000000009](https://etherscan.io/address/0x0000000000000000000000000000000000000009)）开箱即包含执行黄皮书中提到的操作的必要代码。当然，你可以[在Geth的代码中自己检查其实现](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/contracts.go#L45-L93)。\r\n\r\n\r\n\r\n为了给预编译合约的故事增添一些色彩，请注意，在以太坊主网中，所有这些账户的余额至少有 *1wei*。这是[故意的](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L407-L408)（至少在用户开始错误地发送以太币之前）。看，[这里有一个近5年的交易](https://etherscan.io/tx/0xbdba0ec52ae2c9785468a453745b9f7024af6e165887a0e9038a5c2d2fea3909)向`0x0000000000000000000000000000000009`预编译的账户发送了1wei。\r\n\r\n不管怎样。在意识到调用的目标地址并不对应于预编译的合约后，节点[从状态中读取账户的代码](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L219)。然后[确保它是不空的](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L220-L221)。最后，[命令EVM](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L228)使用它的解释器，用给定的输入（交易的`data`字段的内容）来运行该代码。\r\n\r\n### 解释器(第一部分)\r\n\r\n现在是EVM实际执行DAI代码的时候了。为了完成这个任务，EVM手头有几个元素。它有[一个堆栈](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L141)，可以容纳[多达1024个元素](https://github.com/ethereum/go-ethereum/blob/v1.10.18/params/protocol_params.go#L79)(尽管只有前16个元素可以通过可用的操作码直接访问)；它有一个易失性的[读/写内存空间](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L140)；它有一个[程序计数器](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L150)；它有一个特殊的只读内存空间，叫做calldata[保存调用的输入数据](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L164)。还有一写其他东西。\r\n\r\n\r\n\r\n像往常一样，在进入多汁的东西之前有一些必要的设置和验证。首先，[调用深度递增](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L118-L120)1。其次，如果有必要，[只读模式被设置](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L122-L127)。我们的调用不是只读的（见[这里](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L228)传递的`false`参数）。否则一些EVM操作将不被允许。这包括改变状态的EVM指令[`SSTORE`](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L524-L527), [`CREATE`](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L580-L583), [`CREATE2`](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L626-L629), [`SELFDESTRUCT`](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L826-L829), [`CALL`](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L679-L681) 有正值，和[`LOG`](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L846-L848) 。\r\n\r\n解释器现在进入了[执行循环](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L181-L244)。它包括按顺序[执行](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L239)DAI代码中[由程序计数器](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L188-L189)和当前EVM指令集所指示的操作码。目前我们使用的是[伦敦指令集](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/jump_table.go#L94-L99)--这是在解释器第一次实例化时[在跳转表中配置的](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L74-L75)。\r\n\r\n循环还负责[保持一个健康的堆栈](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L191-L196)（避免上下溢出）。并花费每个操作的[固定Gas成本](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L197-L199)，以及适当时的[动态Gas成本](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L221-L225)。这些动态成本[包括](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L201-L217)，例如，EVM内存的扩展（阅读更多关于内存扩展成本的计算[这里](https://www.evm.codes/about#memoryexpansion)）。请注意，Gas是在执行操作码之前（--而不是之后）消耗的。\r\n\r\n\r\n\r\n每个可能指令的实际行为可以在[这个Geth文件](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go)中找到实现。只要略微浏览一下，就可以开始看到这些指令是如何与堆栈、内存、Calldata和状态一起工作的。\r\n\r\n在这一点上，我们需要直接跳到DAI的操作码中，并为我们的交易跟踪它们的执行。然而，我不认为这是处理这个问题的最好方法。我宁愿先从EVM和Geth中走出来，然后进入Solidity 领地。这应该给我们一个更有价值的关于ERC20转移操作的高级行为的概述。\r\n\r\n### Solidity 执行\r\n\r\nDAI智能合约是用[Solidity](https://soliditylang.org/)编码的。它是一种面向对象的高级语言，当被编译时，输出EVM字节码，能够在EVM兼容的链上部署智能合约(在我们的例子中是以太坊)。\r\n\r\nDAI的源代码可以找到[在区块浏览器中验证](https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F#code)，或[在GitHub](https://github.com/makerdao/dss/blob/master/src/dai.sol)。为了便于参考，我将会指向第一个。\r\n\r\n在我们开始之前，让我们始终牢记，EVM 对 Solidity 一无所知。它对其变量、函数、合约的布局、ABI编码等一无所知。以太坊区块链存储的是普通的EVM 字节码，而不是花哨的Solidity代码。\r\n\r\n你可能会问，为什么当你去任何区块浏览器时，他们会在以太坊地址上向你显示Solidity代码。嗯，这只是一个幌子。在大多数区块浏览器中，人们可以上传Solidity 源代码，而浏览器则负责用特定的编译器设置来编译该源代码。如果编译器产生的输出与区块链上的指定地址存储的内容相匹配，那么合约的源代码就被称为 \"验证\"。从那时起，任何导航到该地址的人都会看到该地址的Solidity代码，而不是只看到存储在该地址的EVM字节码。\r\n\r\n上述情况的一个非微不足道的后果是，在某种程度上，我们相信区块浏览器会向我们展示合法的代码（这不一定是真的，即使是[意外](https://samczsun.com/hiding-in-plain-sight/)）。不过这可能有[替代方案](https://sourcify.dev/)--除非每次你想读一个合约时，都要对照自己的节点来验证源代码。\r\n\r\n无论如何，现在回到DAI的Solidity代码。\r\n\r\n在DAI的智能合约上（用Solidity [v0.5.12](https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F#code#L6)编译），让我们专注于[函数](https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F#code#L122)的执行：`transfer`。\r\n\r\n```\r\nfunction transfer(address dst, uint wad) external returns (bool) {\r\n    return transferFrom(msg.sender, dst, wad);\r\n}\r\n```\r\n\r\n当`transfer`运行时，它将[调用另一个名为`transferFrom`的函数](https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F#code#L123)，然后返回后者返回的任何布尔标志。`transfer`的第一个和第二个参数（这里称为`dst`和`wad`）被直接传递给`transferFrom`。这个函数另外读取发起者的地址（在`msg.sender`中作为一个[Solidity全局变量](https://learnblockchain.cn/docs/solidity/units-and-global-variables.html#index-2)）。\r\n\r\n对于我们的例子，这些将是传递给`transferFrom`的值：\r\n\r\n```\r\nreturn transferFrom(\r\n    msg.sender, // 0x6fC27A75d76d8563840691DDE7a947d7f3F179ba (my address on the local testing node)\r\n    dst,        // 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 (Vitalik's address)\r\n    wad         // 1000000000000000000 (1 DAI in wei units)\r\n);\r\n```\r\n\r\n让我们看看[`transferFrom`函数](https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F#code#L125)，然后：\r\n\r\n```\r\nfunction transferFrom(address src, address dst, uint wad) public returns (bool) {\r\n   ...\r\n}\r\n```\r\n\r\n首先，发起者的[余额被检查](https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F#code#L128)与被转移的金额相对照。\r\n\r\n```\r\nrequire(balanceOf[src] >= wad, \"Dai/insufficient-balance\");\r\n```\r\n\r\n这很简单：你转移的DAI不能多于你的余额。如果我没有1个DAI，执行将在这一点上停止，返回一个错误信息。请注意，每个地址的余额都在智能合约的存储中被跟踪。在一个[名为`balanceOf`的 Map 的数据结构中](https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F#code#L90)。如果你至少有1个DAI，我可以向你保证你的账户地址在那里的某个地方有记录。\r\n\r\n第二，代币[allowances 被验证](https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F#code#L129)。\r\n\r\n```solidity\r\n// don't bother too much about this :)\r\nif (src != msg.sender && allowance[src][msg.sender] != uint(-1)) {\r\n    require(allowance[src][msg.sender] >= wad, \"Dai/insufficient-allowance\");\r\n    allowance[src][msg.sender] = sub(allowance[src][msg.sender], wad);\r\n}\r\n```\r\n\r\n\r\n\r\n这与我们现在没有关系。因为我们没有代表另一个账户执行转账。虽然要注意到这是所有ERC20代币应该实现的机制--DAI不是例外。实质上，你可以授权其他账户从你的账户转账DAI代币。\r\n\r\n\r\n\r\n第三，实际进行[余额互换](https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F#code#L133)。\r\n\r\n```\r\nbalanceOf[src] = sub(balanceOf[src], wad);\r\nbalanceOf[dst] = add(balanceOf[dst], wad);\r\n```\r\n\r\n\r\n\r\n当发送1个DAI时，发送方的余额减少了100000000000000，接收方的余额增加了100000000000000。这些操作是在`balanceOf`数据结构上进行读写的。值得注意的是使用了两个特殊的函数`add`和`sub`来进行计算。\r\n\r\n为什么不简单地使用`+`和`-`运算符？\r\n\r\n记住：这个合约是用Solidity 0.5.12编译的。在那个时候，编译器并没有像今天这样包括上/下溢检查。因此，开发者必须记住（或被提醒😛），在适当的地方自己实现它们。因此在DAI合约中使用了`add`和`sub`。它们只是自定义的内部函数，用于执行加法和减法，并带有约束检查以避免算术问题。\r\n\r\n```\r\nfunction add(uint x, uint y) internal pure returns (uint z) {\r\n    require((z = x + y) >= x);\r\n}\r\n\r\nfunction sub(uint x, uint y) internal pure returns (uint z) {\r\n    require((z = x - y) <= x);\r\n}\r\n```\r\n\r\n`add`函数将`x`和`y`相加，如果运算结果小于`x`，则停止执行（从而防止整数溢出）。\r\n\r\n`sub`函数从`x`中减去`y`，如果操作的结果大于`x`，则停止执行（从而防止整数下溢）。\r\n\r\n第四，[触发](https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F#code#L135)一个`转移`事件（正如[ERC20规范所建议的](https://eips.ethereum.org/EIPS/eip-20#events)）。\r\n\r\n```\r\nemit Transfer(src, dst, wad);\r\n```\r\n\r\n一个事件是一个记录操作。在事件中发出的数据后可以从读取区块链的链外服务中获取，但绝不会被其他合约获取。\r\n\r\n在我们的转账操作中，发出的事件似乎记录了三个元素。发起者的地址（`0x6fC27A75d76d8563840691DDE7a947d7f3F179ba`），接收者的地址（`0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045`），和发送金额（`100000000000000`）。\r\n\r\n前两个对应于事件声明中标记为 “indexed” 的参数。索引参数有利于数据的检索，允许过滤任何相应的记录值。除非事件被标记为 \"匿名\"，否则事件的标识符也会作为一个主题被包含。\r\n\r\n因此，更具体地说，我们正在处理的`Transfer`事件实际上记录了3个主题（事件的标识符、发起者的地址和接受者的地址）和1个值（转移的DAI数量）。一旦我们涉及到低级别的EVM的东西，我们将涵盖关于这个事件的更多细节。\r\n\r\n在函数的最后，布尔值 \"true \"被[返回](https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F#code#L136)（正如[ERC20规范所建议的](https://eips.ethereum.org/EIPS/eip-20#methods)）。\r\n\r\n```\r\nreturn true;\r\n```\r\n\r\n这是一种信号，表明转移被成功执行。这个布尔标志被传递给启动调用的`transfer`函数（它也简单地返回它）。\r\n\r\n这就是了!如果你曾经发送过DAI，这就是你所执行的逻辑。这就是你花钱让一个全球去中心化的节点网络为你做的工作。\r\n\r\n等一下。我可能偏得有点远了。因为正如我之前告诉你的，EVM对Solidity一无所知。节点不执行Solidity。它们执行的是EVM的字节码。\r\n\r\n是时候进行真正的交易了。\r\n\r\n### EVM执行\r\n\r\n在这一节中，将变得相当技术化。我假设你对EVM的字节码比较熟悉。如果你不熟悉，我强烈建议你阅读[这个专栏](https://learnblockchain.cn/column/22)或[这个系列](https://learnblockchain.cn/article/4622)。在那里，你会发现本节中的很多概念都有单独和更深入的解释。\r\n\r\nDAI的原始字节码是很难阅读的 -- 我们已经在上一节见证了它。研究它的一个更漂亮的方法是使用反汇编的版本。你可以在[这里](https://library.dedaub.com/contracts/Ethereum/6b175474e89094c44da98b954eedeac495271d0f/disassembled)找到Dai的反汇编字节码(为了便于参考，我已经把它提取到[这个gist](https://gist.github.com/tinchoabbate/508b2b51df7043a524be7debb738992c)中)。\r\n\r\n#### 空闲内存指针和调用的值\r\n\r\n如果你已经熟悉 Solidity 编译器，前三条指令不应该感到惊讶。它只是在初始化空闲内存指针。\r\n\r\n```\r\n0x0: PUSH1     0x80\r\n0x2: PUSH1     0x40\r\n0x4: MSTORE    \r\n```\r\n\r\nSolidity 编译器为内部的东西保留了从`0x00`到`0x80`的内存插槽。所以 `空闲内存指针 `是一个指向EVM内存中第一个可以自由使用的插槽的指针。它存储在`0x40`，初始化时指向`0x80`。\r\n\r\n请记住，所有EVM操作码在Geth中都有对应的实现。例如，你可以[真正看到](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L505-L506)`MSTORE`的实现是如何弹出两个堆栈元素并向EVM内存写入一个32字节的字：\r\n\r\n```\r\nfunc opMstore(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {\r\n    // pop value of the stack\r\n    mStart, val := scope.Stack.pop(), scope.Stack.pop()\r\n    scope.Memory.Set32(mStart.Uint64(), &val)\r\n    return nil, nil\r\n}\r\n```\r\n\r\n\r\n\r\n在DAI的字节码中，接下来的EVM指令确保调用不持有任何价值。如果它有，执行将在`REVERT`指令处停止。注意使用`CALLVALUE`指令（[在此实现](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L277-L281)来读取当前调用的Value。\r\n\r\n```\r\n0x5: CALLVALUE \r\n0x6: DUP1      \r\n0x7: ISZERO    \r\n0x8: PUSH2     0x10\r\n0xb: JUMPI     \r\n0xc: PUSH1     0x0\r\n0xe: DUP1      \r\n0xf: REVERT\r\n```\r\n\r\n我们的调用没有持有任何值（交易的`value`字段被设置为零）--所以我们可以继续。\r\n\r\n#### 验证calldata（第一部分）\r\n\r\n接下来：由编译器引入的另一个检查。这一次，它要弄清楚calldata的大小（通过`CALLDATASIZE`指令获得--[在这里实现](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L294-L297)是否低于4字节（见下面的`0x4`和`LT`指令）。在此案例中，它将跳到`0x142`位置。在`0x146`位置的`REVERT`指令上停止执行。\r\n\r\n```\r\n0x10: JUMPDEST\r\n0x11: POP       \r\n0x12: PUSH1     0x4\r\n0x14: CALLDATASIZE\r\n0x15: LT        \r\n0x16: PUSH2     0x142\r\n0x19: JUMPI\r\n\r\n...\r\n\r\n0x142: JUMPDEST  \r\n0x143: PUSH1     0x0\r\n0x145: DUP1      \r\n0x146: REVERT \r\n```\r\n\r\n这意味着在DAI智能合约中，calldata的大小被强制要求为*至少*4字节。这是因为Solidity使用的ABI编码机制用其签名的keccak256哈希值的前四个字节来识别函数(通常称为 \"函数选择器\" - [见规范 - Solidity 文档](https://learnblockchain.cn/docs/solidity/abi-spec.html))。\r\n\r\n如果calldata没有至少4个字节，就不可能识别出该函数。所以编译器引入了必要的EVM指令，以在此案例中提前失败。这就是你在上面目睹的情况。\r\n\r\n为了调用`transfer(address,uint256)`函数，calldata的前四个字节必须与函数的选择器匹配。这四个字节是：\r\n\r\n```\r\n$ cast sig \"transfer(address,uint256)\"\r\n0xa9059cbb\r\n```\r\n\r\n与我们之前建立的交易的`data`字段的前4个字节完全相同：\r\n\r\n```\r\n0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000\r\n```\r\n\r\n现在calldata的长度已经得到验证，是时候使用它了。请看下面如何将前4个字节的calldata放在堆栈的顶部（这里需要注意的主要EVM指令是`CALLDATALOAD`，[这里](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L283-L292)实现）。\r\n\r\n```\r\n0x1a: PUSH1     0x0\r\n0x1c: CALLDATALOAD\r\n0x1d: PUSH1     0xe0\r\n0x1f: SHR       \r\n```\r\n\r\n实际上`CALLDATALOAD`将[32字节的calldata](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L286)推到堆栈中。它需要用[`SHR`指令](https://www.evm.codes/#1c)来截断，以保留前四个字节。\r\n\r\n#### 函数调度器\r\n\r\n不要试图逐行理解下面的内容。相反，注意突出的高级模式就好。我会添加一些分界线，使之更加清晰：\r\n\r\n```\r\n0x20: DUP1\r\n0x21: PUSH4     0x7ecebe00\r\n0x26: GT        \r\n0x27: PUSH2     0xb8\r\n0x2a: JUMPI\r\n0x2b: DUP1      \r\n0x2c: PUSH4     0xa9059cbb\r\n0x31: GT        \r\n0x32: PUSH2     0x7c\r\n0x35: JUMPI\r\n0x36: DUP1      \r\n0x37: PUSH4     0xa9059cbb\r\n0x3c: EQ        \r\n0x3d: PUSH2     0x6b4\r\n0x40: JUMPI\r\n0x41: DUP1      \r\n0x42: PUSH4     0xb753a98c\r\n0x47: EQ        \r\n0x48: PUSH2     0x71a\r\n0x4b: JUMPI\r\n```\r\n\r\n一些被推送到堆栈的十六进制值有4个字节长，这并不是巧合。这些确实是[函数选择器](https://learnblockchain.cn/article/5197)。\r\n\r\n上面这组指令是Solidity编译器产生的字节码的一个常见结构。它通常被称为 \"函数调度器\"。它类似于一个if-else或switch流程。它只是试图将calldata的前四个字节与合约的函数的已知选择器集合相匹配。一旦它找到一个匹配项，执行将跳到字节码的另一个部分。在那里，该特定函数的指令被放置在这个部分。\r\n\r\n按照上述逻辑，EVM 将 calldata 的前四个字节与 ERC20 `transfer`函数的选择器相匹配：`0xa9059cbb`。并跳转到字节码位置`0x6b4`。这就是告诉 EVM 开始执行DAI的转移。\r\n\r\n#### 验证calldata(第二部分)\r\n\r\n在匹配了选择器和跳转后，现在EVM要开始运行与函数有关的具体代码了。但在跳转到其细节之前，它需要以某种方式记住位置，一旦所有与功能相关的逻辑被执行，在哪里继续执行。\r\n\r\n做到这一点的方法是简单地保持堆栈中适当的字节码位置。请看下面正在推送的`0x700`值。它将在堆栈中徘徊，直到在某个时间点（稍后）被检索到，并被用来跳回以结束执行。\r\n\r\n```\r\n0x6b4: JUMPDEST  \r\n0x6b5: PUSH2     0x700\r\n```\r\n\r\n现在让我们更具体地了解一下`transfer`函数。\r\n\r\n编译器嵌入了一些逻辑，以确保calldata的大小对一个有两个`address`和`uint256`类型的参数的函数是正确的。对于`transfer`函数，至少是68字节（4字节用于选择器+64字节用于两个ABI编码的参数）。\r\n\r\n```\r\n0x6b8: PUSH1     0x4\r\n0x6ba: DUP1      \r\n0x6bb: CALLDATASIZE\r\n0x6bc: SUB       \r\n0x6bd: PUSH1     0x40\r\n0x6bf: DUP2      \r\n0x6c0: LT        \r\n0x6c1: ISZERO    \r\n0x6c2: PUSH2     0x6ca\r\n0x6c5: JUMPI     \r\n0x6c6: PUSH1     0x0\r\n0x6c8: DUP1      \r\n0x6c9: REVERT\r\n```\r\n\r\n如果calldata的大小更小，执行将在位置`0x6c9`的`REVERT`处停止。由于我们的交易的calldata已经被正确的ABI编码，因此有适当的长度，执行会跳到位置`0x6ca`。\r\n\r\n#### 读取参数\r\n\r\n\r\n\r\n下一步是让EVM读取calldata中提供的两个参数。这些是20字节长的地址`0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045`和数字`100000000000000`（十六进制的`0x0de0b6b3a7640000`）。两者都是以32个字节为一组的ABI编码。因此，需要进行一些基本的操作来读取正确的数值，并把它们放在堆栈的顶部。\r\n\r\n```\r\n0x6ca: JUMPDEST  \r\n0x6cb: DUP2      \r\n0x6cc: ADD       \r\n0x6cd: SWAP1     \r\n0x6ce: DUP1      \r\n0x6cf: DUP1      \r\n0x6d0: CALLDATALOAD\r\n0x6d1: PUSH20    0xffffffffffffffffffffffffffffffffffffffff\r\n0x6e6: AND       \r\n0x6e7: SWAP1     \r\n0x6e8: PUSH1     0x20\r\n0x6ea: ADD       \r\n0x6eb: SWAP1     \r\n0x6ec: SWAP3     \r\n0x6ed: SWAP2     \r\n0x6ee: SWAP1     \r\n0x6ef: DUP1      \r\n0x6f0: CALLDATALOAD\r\n0x6f1: SWAP1     \r\n0x6f2: PUSH1     0x20\r\n0x6f4: ADD       \r\n0x6f5: SWAP1     \r\n0x6f6: SWAP3     \r\n0x6f7: SWAP2     \r\n0x6f8: SWAP1     \r\n0x6f9: POP       \r\n0x6fa: POP       \r\n0x6fb: POP       \r\n0x6fc: PUSH2     0x1df4\r\n0x6ff: JUMP\r\n```\r\n\r\n为了更加直观，在依次应用上述指令集后（直到`0x6fb`），堆栈顶部看起来像这样：\r\n\r\n```\r\n0x0000000000000000000000000000000000000000000000000de0b6b3a7640000\r\n0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045\r\n```\r\n\r\n这就是EVM如何迅速地从calldata中提取两个参数，将它们放在堆栈中供将来使用。\r\n\r\n上面的最后两条指令（字节码位置`0x6fc`和`0x6ff`）只是让执行跳到位置`0x1df4`。让我们在那里继续。\r\n\r\n#### `transfer`函数\r\n\r\n在简单的Solidity分析中，我们看到`transfer(address,uint256)`函数是一个包装器，调用更复杂的`transferFrom(address,address,uint256)`函数。编译器将这种内部调用翻译成[这些EVM指令](https://gist.github.com/tinchoabbate/508b2b51df7043a524be7debb738992c#file-dai-asm-L3346-L3353)：\r\n\r\n```\r\n0x1df4: JUMPDEST  \r\n0x1df5: PUSH1     0x0\r\n0x1df7: PUSH2     0x1e01\r\n0x1dfa: CALLER    \r\n0x1dfb: DUP5      \r\n0x1dfc: DUP5      \r\n0x1dfd: PUSH2     0xa25\r\n0x1e00: JUMP\r\n```\r\n\r\n首先注意推送值`0x1e01`的指令。这就是指示EVM `记住 `它应该跳回的确切位置，以便在即将到来的内部调用后继续执行。\r\n\r\n然后，注意`CALLER`的使用（因为在Solidity中，内部调用使用`msg.sender`）。以及两个`DUP5`指令。这些都是把`transferFrom`的三个必要参数放在堆栈的顶部：调用者的地址，接收者的地址，以及要转移的金额。后两个参数已经在堆栈的某处，因此使用了 `DUP5`。现在堆栈的顶部有所有必要的参数：\r\n\r\n```\r\n0x0000000000000000000000000000000000000000000000000de0b6b3a7640000\r\n0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045\r\n0x0000000000000000000000006fc27a75d76d8563840691dde7a947d7f3f179ba\r\n```\r\n\r\n最后，在指令`0x1dfd`和`0x1e00`之后，执行跳转到位置`0xa25`。在那里EVM将开始执行与 `transferFrom `函数对应的指令。\r\n\r\n#### `transferFrom` 函数\r\n\r\n首先需要检查的是发送方是否有足够的DAI余额--否则将被回退。发送方的余额被保存在合约存储器中。然后需要的基本EVM指令是`SLOAD`。然而，`SLOAD`需要知道什么存储槽需要被读取。对于映射（在DAI智能合约中保存账户余额的Solidity数据结构的类型），这不是那么直接的告诉。\r\n\r\n我不会在这里深入研究合约存储中Solidity状态变量的内部布局。你可以阅读它[这里是v0.5.15](https://docs.soliditylang.org/en/v0.5.15/miscellaneous.html#mappings-and-dynamic-arrays)。我只想说，给定映射`balanceOf`的键地址`k`，它相应的`uint256`值将被保存在存储槽`keccak256(k . p)`，其中`p`是映射本身的槽位置，`.`是连接。你可以自己做数学题。\r\n\r\n> 参考[状态变量的存储空间](https://learnblockchain.cn/docs/solidity/internals/layout_in_storage.html#id2) 。\r\n\r\n为了简单起见，我们只强调几个需要发生的操作。EVM必须 i) 计算映射的存储槽，ii)读取数值，iii)将其与要转账的数量（已经在堆栈中的数值）进行比较。因此，我们应该看到像 \"SHA3 \"这样的指令用于散列，\"SLOAD\" 用于读取存储，\"LT \"用于比较。\r\n\r\n```\r\n0xa25: JUMPDEST  \r\n0xa26: PUSH1     0x0\r\n0xa28: DUP2      \r\n0xa29: PUSH1     0x2\r\n0xa2b: PUSH1     0x0\r\n0xa2d: DUP7      \r\n0xa2e: PUSH20    0xffffffffffffffffffffffffffffffffffffffff\r\n0xa43: AND       \r\n0xa44: PUSH20    0xffffffffffffffffffffffffffffffffffffffff\r\n0xa59: AND       \r\n0xa5a: DUP2      \r\n0xa5b: MSTORE    \r\n0xa5c: PUSH1     0x20\r\n0xa5e: ADD       \r\n0xa5f: SWAP1     \r\n0xa60: DUP2      \r\n0xa61: MSTORE    \r\n0xa62: PUSH1     0x20\r\n0xa64: ADD       \r\n0xa65: PUSH1     0x0\r\n0xa67: SHA3      --> calculating storage slot\r\n0xa68: SLOAD     --> reading storage\r\n0xa69: LT        --> comparing balance against amount\r\n0xa6a: ISZERO    \r\n0xa6b: PUSH2     0xadc\r\n0xa6e: JUMPI    \r\n```\r\n\r\n如果发送方没有足够的DAI，执行将[在`0xa6f`处继续，最后在`0xadb`处碰到`REVERT`](https://gist.github.com/tinchoabbate/508b2b51df7043a524be7debb738992c#file-dai-asm-L1583-L1620)。由于我没有忘记在我的发送方账户余额中装入1个DAI，那么让我们继续到`0xadc`位置。\r\n\r\n下面一组指令对应于EVM验证调用者是否与发起者的地址相符（记得合约中的`if (src != msg.sender ...) { ...}`合约中的代码段）。\r\n\r\n```\r\n0xadc: JUMPDEST  \r\n0xadd: CALLER    \r\n0xade: PUSH20    0xffffffffffffffffffffffffffffffffffffffff\r\n0xaf3: AND       \r\n0xaf4: DUP5      \r\n0xaf5: PUSH20    0xffffffffffffffffffffffffffffffffffffffff\r\n0xb0a: AND       \r\n0xb0b: EQ        \r\n0xb0c: ISZERO    \r\n0xb0d: DUP1      \r\n0xb0e: ISZERO    \r\n0xb0f: PUSH2     0xbb4\r\n0xb12: JUMPI\r\n...\r\n0xbb4: JUMPDEST  \r\n0xbb5: ISZERO    \r\n0xbb6: PUSH2     0xdb2\r\n0xbb9: JUMPI\r\n```\r\n\r\n既然不匹配，就在`0xdb2`位置继续执行。\r\n\r\n下面这段代码没有让你想起什么吗？检查一下正在使用的指令。同样，不要单独一行行理解。用你的直觉来发现高级模式和最相关的指令。\r\n\r\n```\r\n0xdb2: JUMPDEST  \r\n0xdb3: PUSH2     0xdfb\r\n0xdb6: PUSH1     0x2\r\n0xdb8: PUSH1     0x0\r\n0xdba: DUP7      \r\n0xdbb: PUSH20    0xffffffffffffffffffffffffffffffffffffffff\r\n0xdd0: AND       \r\n0xdd1: PUSH20    0xffffffffffffffffffffffffffffffffffffffff\r\n0xde6: AND       \r\n0xde7: DUP2      \r\n0xde8: MSTORE    \r\n0xde9: PUSH1     0x20\r\n0xdeb: ADD       \r\n0xdec: SWAP1     \r\n0xded: DUP2      \r\n0xdee: MSTORE    \r\n0xdef: PUSH1     0x20\r\n0xdf1: ADD       \r\n0xdf2: PUSH1     0x0\r\n0xdf4: SHA3      \r\n0xdf5: SLOAD     \r\n0xdf6: DUP4      \r\n0xdf7: PUSH2     0x1e77\r\n0xdfa: JUMP\r\n```\r\n\r\n感觉它类似于从存储器中读取映射，那是因为它就是这样! 上面是EVM从`balanceOf`映射中读取发送方的余额。\r\n\r\n然后执行跳转到`0x1e77`的位置，这里是`sub`函数的主体。\r\n\r\n`sub`函数将两个数字相减，在整数下溢时恢复到原状。我这里没有写字节码，尽管你可以在[这里](https://gist.github.com/tinchoabbate/508b2b51df7043a524be7debb738992c#file-dai-asm-L3444-L3465) 找到他。算术运算的结果被保存在堆栈中。\r\n\r\n\r\n\r\n回到对应于`transferFrom`函数主体的指令，现在减法的结果将被写入存储空间-更新`balanceOf`映射。试着注意下面的计算，以获得映射项的适当存储槽，这通过`SSTORE`指令的执行。这条指令是有效地将数据写入状态的指令--也就是更新合约的存储。\r\n\r\n```\r\n0xdfb: JUMPDEST  \r\n0xdfc: PUSH1     0x2\r\n0xdfe: PUSH1     0x0\r\n0xe00: DUP7      \r\n0xe01: PUSH20    0xffffffffffffffffffffffffffffffffffffffff\r\n0xe16: AND       \r\n0xe17: PUSH20    0xffffffffffffffffffffffffffffffffffffffff\r\n0xe2c: AND       \r\n0xe2d: DUP2      \r\n0xe2e: MSTORE    \r\n0xe2f: PUSH1     0x20\r\n0xe31: ADD       \r\n0xe32: SWAP1     \r\n0xe33: DUP2      \r\n0xe34: MSTORE    \r\n0xe35: PUSH1     0x20\r\n0xe37: ADD       \r\n0xe38: PUSH1     0x0\r\n0xe3a: SHA3      \r\n0xe3b: DUP2      \r\n0xe3c: SWAP1     \r\n0xe3d: SSTORE \r\n```\r\n\r\n一组相当类似的操作码被运行以更新接收者的账户余额。首先[是从存储中的`balanceOf`映射中读取](https://gist.github.com/tinchoabbate/508b2b51df7043a524be7debb738992c#file-dai-asm-L1884-L1907)。然后[使用`add`函数](https://gist.github.com/tinchoabbate/508b2b51df7043a524be7debb738992c#file-dai-asm-L3467-L3487)将余额加到正在转移的金额上。最后，结果被[写到适当的存储槽](https://gist.github.com/tinchoabbate/508b2b51df7043a524be7debb738992c#file-dai-asm-L1908-L1929)。\r\n\r\n#### 事件记录（Log）\r\n\r\n在合约的代码中，`Transfer`事件是在更新余额之后发出的。因此，在分析的字节码中必须有一组指令来处理这种带有适当数据的事件。\r\n\r\n然而，事件是另一个属于Solidity的幻想世界的东西。在EVM世界中，事件对应于记录操作。\r\n\r\n记录是通过可用的`LOG`指令集进行的。有几个变体，取决于有多少个主题要被记录。在DAI的案例中，我们已经注意到，发出的`Transfer`事件有3个主题。\r\n\r\n那么找到一组运行`LOG3`指令的指令就不奇怪了。\r\n\r\n```\r\n0xeca: POP       \r\n0xecb: DUP3      \r\n0xecc: PUSH20    0xffffffffffffffffffffffffffffffffffffffff\r\n0xee1: AND \r\n0xee2: DUP5\r\n0xee3: PUSH20    0xffffffffffffffffffffffffffffffffffffffff\r\n0xef8: AND       \r\n0xef9: PUSH32    0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\r\n0xf1a: DUP5      \r\n0xf1b: PUSH1     0x40\r\n0xf1d: MLOAD     \r\n0xf1e: DUP1      \r\n0xf1f: DUP3      \r\n0xf20: DUP2      \r\n0xf21: MSTORE    \r\n0xf22: PUSH1     0x20\r\n0xf24: ADD       \r\n0xf25: SWAP2     \r\n0xf26: POP       \r\n0xf27: POP       \r\n0xf28: PUSH1     0x40\r\n0xf2a: MLOAD     \r\n0xf2b: DUP1      \r\n0xf2c: SWAP2     \r\n0xf2d: SUB       \r\n0xf2e: SWAP1     \r\n0xf2f: LOG3      \r\n```\r\n\r\n在这些指令中，至少有一个值是突出的：`0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef`.这是该事件的主要标识符。也叫主题0。它是编译器在编译时计算的一个静态值（嵌入在合约的运行时字节码中）。如前所述，事件签名的哈希值：\r\n\r\n```\r\n$ cast keccak \"Transfer(address,address,uint256)\"\r\n0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\r\n```\r\n\r\n就在到达`LOG3`指令之前，堆栈看起来像这样：\r\n\r\n```\r\n0x0000000000000000000000000000000000000000000000000000000000000080\r\n0x0000000000000000000000000000000000000000000000000000000000000020\r\n0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef -- topic 0 (event identifier)\r\n0x0000000000000000000000006fc27a75d76d8563840691dde7a947d7f3F179ba -- topic 1 (sender's address)\r\n0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045 -- topic 2 (receiver's address)\r\n```\r\n\r\n那么转移的金额在哪里？在内存中! 在到达 `LOG3 `之前，EVM 首先被指示将金额存储在内存中。这样它就可以在以后被记录指令消耗掉。如果你看一下`0xf21`位置，你会看到`MSTORE`指令负责这样做。\r\n\r\n所以一旦到达`LOG3`，EVM 就可以安全的从内存中抓取实际记录的数值，从偏移量`0x80`开始，读取`0x20`字节（上面的前两个堆栈元素）。\r\n\r\n另一种理解日志的方法是看它的[在Geth中的实现](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L844-L869)。在那里你会发现一个负责处理所有日志指令的单一函数。你可以看到 i) 一个空的主题数组[被初始化](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L849)，ii) 内存偏移量和数据大小[从堆栈中读取](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L850-L851)，iii) 主题[从堆栈中读取](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L853)并[插入数组中](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L854)，iv) 值[从内存中读取](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L857)，v) 包含它被发出的地址、主题和值的日志[被附加](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L858-L865)。\r\n\r\n这些日志后来是如何还原的，我们很快就会发现。\r\n\r\n#### 返回值\r\n\r\n`transferFrom`函数的最后一件事是返回布尔值`true`。这就是为什么在`LOG3`之后的第一条指令只是将`0x1`的值推到堆栈中。\r\n\r\n```\r\n0xf30: PUSH1    0x1\r\n```\r\n\r\n接下来的指令准备让堆栈退出`transferFrom`函数，回到它的包装`transfer`函数。请记住，这个下一步跳转的位置已经存储在堆栈中了--这就是为什么你在下面的操作码中没有看到它。\r\n\r\n```\r\n0xf32: SWAP1     \r\n0xf33: POP       \r\n0xf34: SWAP4     \r\n0xf35: SWAP3     \r\n0xf36: POP       \r\n0xf37: POP       \r\n0xf38: POP       \r\n0xf39: JUMP\r\n```\r\n\r\n回到`transfer`函数中，要做的就是为最后的跳转准备堆栈。到一个执行将被结束的位置。这个即将跳转的位置之前也已经存储在堆栈中了（还记得被推送的`0x700`值吗？）\r\n\r\n```\r\n0x1e01: JUMPDEST  \r\n0x1e02: SWAP1     \r\n0x1e03: POP       \r\n0x1e04: SWAP3     \r\n0x1e05: SWAP2     \r\n0x1e06: POP       \r\n0x1e07: POP       \r\n0x1e08: JUMP \r\n```\r\n\r\n剩下的就是为最后一条指令准备堆栈：`RETURN`。这条指令负责从内存中读取一些数据，并将其传回给原始调用者。\r\n\r\n对于DAI转账，返回的数据将简单地包括由`transfer`函数返回的`true`布尔标志。记住，这个值已经放在堆栈里了。\r\n\r\n\r\n\r\nEVM开始抓取第一个可用的空闲内存位置。这是通过读取空闲内存的指针来完成的：\r\n\r\n```\r\n0x700: JUMPDEST  \r\n0x701: PUSH1     0x40\r\n0x703: MLOAD\r\n```\r\n\r\n接下来，必须用`MSTORE`将该值存储在内存中。虽然不是那么直接地告诉你，下面的指令只是编译器认为最适合为`MSTORE`操作准备堆栈的指令。\r\n\r\n```\r\n0x704: DUP1      \r\n0x705: DUP3      \r\n0x706: ISZERO    \r\n0x707: ISZERO    \r\n0x708: ISZERO    \r\n0x709: ISZERO    \r\n0x70a: DUP2      \r\n0x70b: MSTORE\r\n```\r\n\r\n`RETURN` 指令从内存中复制返回的数据。所以它需要被告知要读取多少内存，以及从哪里开始。下面的指令简单的告诉EVM从内存中读取并返回`0x20`字节，从空闲内存指针开始。\r\n\r\n```\r\n0x70c: PUSH1     0x20\r\n0x70e: ADD       \r\n0x70f: SWAP2     \r\n0x710: POP       \r\n0x711: POP       \r\n0x712: PUSH1     0x40\r\n0x714: MLOAD     \r\n0x715: DUP1      \r\n0x716: SWAP2     \r\n0x717: SUB       \r\n0x718: SWAP1     \r\n0x719: RETURN \r\n```\r\n\r\n返回值 \"0x0000000000000000000000000000000000000000000000000001\"（对应于布尔值 \"true\"）。\r\n\r\n执行停止。\r\n\r\n### 解释器 (第二部分)\r\n\r\n字节码的执行已经结束。解释器必须停止[迭代](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L181)。在Geth中，它是这样做的：\r\n\r\n```\r\n// interpreter's execution loop\r\nfor { \r\n    ...\r\n    // execute the operation\r\n    res, err = operation.execute(&pc, in, callContext)\r\n    if err != nil {\r\n        break\r\n    }\r\n    ...\r\n}\r\n```\r\n\r\n这意味着 `RETURN `操作码的执行应该以某种方式返回一个错误。即使是像我们这样成功的执行。事实上，[它确实](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/instructions.go#L807)。尽管它作为一个标志--当它与成功执行`RETURN`操作码所返回的标志相匹配时，错误实际上被[删除](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/interpreter.go#L246-L248)。\r\n\r\n### Gas 退款和付款\r\n\r\n随着解释器运行的结束，我们[回到了最初触发它的调用](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L228)中。该运行[被成功完成](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L235-L243)。因此，返回的数据和任何剩余的Gas被简单地[返回](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/vm/evm.go#L244)。\r\n\r\n调用也完成了。执行是在包裹状态转换之后进行的。\r\n\r\n首先[提供Gas退款](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L339-L342)。它被添加到交易中任何剩余的Gas中。退款金额的上限是所使用Gas的1/5（由于[EIP 3529](https://eips.ethereum.org/EIPS/eip-3529)）。所有现在可用的Gas（剩余的加上退还的）被[以ETH形式支付](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L364-L366)到发起者的账户，按照发起者在交易中最初设定的费用价格。所有剩余的Gas被[重新添加](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L370)到区块中的可用Gas中，以便后续交易可以消耗这些Gas。\r\n\r\n然后[向coinbase地址](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L343-L347)（PoW中的矿工地址，PoS中的验证者地址）支付最初承诺小费。有趣的是，对执行过程中使用的所有Gas 都进行支付。即使其中一些后来被退还了。此外，请注意[这里](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L345)*有效的小费是如何计算的。不仅注意到它被 \"maxPriorityFeePerGas\" 交易字段所限制。但更重要的是，意识到它不包括基本费用（base-fee）!这没有错 -- 以太坊喜欢[看着ETH燃烧](https://watchtheburn.com/)。\r\n\r\n最后[执行结果被包裹在一个更漂亮的结构中](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L349-L353)。包括使用的Gas，任何可能中止执行的EVM错误（在我们的例子中没有），以及从EVM返回的数据。\r\n\r\n### 建立交易收据\r\n\r\n代表执行结果的结构现在被传递 [向上](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L101) [返回](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_transition.go#L181) 。在这一点上，Geth 对执行状态做了[一些内部清理](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L109)。一旦完成，它就会[累积交易中使用的Gas](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L113)（包括退款）。\r\n\r\n\r\n\r\n最重要的是，现在是创建交易收据的时候了。收据是一个总结与交易执行有关的数据的对象。它包括的信息有：[执行状态](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L118-L122)（成功/失败），[交易的哈希值](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L123)，[使用的Gas 单位](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L124)，[创建合约的地址](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L128)（在我们的例子中没有），[发出的日志](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L132)，[交易的bloom 过滤器](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L133)，和[其他](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/state_processor.go#L134-L136)。\r\n\r\n我们很快就会检索到我们交易的收据的全部内容。\r\n\r\n如果你想深入了解交易的日志和bloom filter的作用，请查看[noxx的文章](https://noxx.substack.com/i/55077458/bloom-filters)。\r\n\r\n## 挖掘区块\r\n\r\n后续交易的执行继续发生，直到区块的空间耗尽。\r\n\r\n这时节点会调用共识引擎来[最终完成](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L1155)该区块。在PoW中，这需要[积累挖矿奖励](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/consensus.go#L595)（向coinbase地址发放ETH的[全额奖励](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/consensus.go#L645)，以及[其他区块](https://ethereum.org/en/glossary/#ommer)的[部分奖励](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/consensus.go#L645)）并相应地[更新区块的最终状态根](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/consensus.go#L596)。\r\n\r\n接下来，实际的区块[被组装](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/consensus.go#L606)，将[所有数据放在正确的位置](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/types/block.go#L200-L228)。包括头的[交易哈希](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/types/block.go#L206)或[收据哈希](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/types/block.go#L214)等信息。\r\n\r\n现在为真正的PoW挖矿做好了所有准备。[一个新的 \"任务\"](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L1162)被创建并[推送给正确的监听者](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L648)。委托给共识引擎的[挖掘任务](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L668)开始。\r\n\r\n我不会详细解释PoW的实际开采是如何进行的。互联网上已经有很多关于它的内容。只需注意，在Geth中，这涉及一个[多线程的尝试和错误过程](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/sealer.go#L99)，以[找到一个数字](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/sealer.go#L182)，[满足一个必要条件](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/sealer.go#L182)。不用说，一旦以太坊切换到权益证明，挖掘过程的处理方式将有很大不同。\r\n\r\n挖出的区块被[推送到适当的channel](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/sealer.go#L112)和[在结果循环中接收](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L687)。其中[收据和日志会相应地更新](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L712-L732)，并在其被有效挖出后提供最新的区块数据。\r\n\r\n区块最后被[写入链中](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L733-L734)，置于链的顶端。\r\n\r\n## 广播区块\r\n\r\n下一步是[向整个网络宣布](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L742-L743)，一个新的区块已经被开采出来。同时，该区块在内部[存储到待定区块集合](https://github.com/ethereum/go-ethereum/blob/v1.10.18/miner/worker.go#L745-L746)。耐心地等待其他节点的确认。\r\n\r\n\r\n\r\n公告已经完成[发布一个特定的事件](https://github.com/ethereum/go-ethereum/blob/v1.10.18/event/event.go#L97)，被[挖掘的广播循环(loop)](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/handler.go#L647)接收。在那里，该区块被完全[传播到一个子网的对等节点](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/handler.go#L652)，并以较轻的方式[提供给其他人](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/handler.go#L653)。\r\n\r\n更具体地说，广播需要[向连接的对等节点的平方根发送块数据](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/handler.go#L586-L590)。在内部实现是[将数据推送到块通道（ channel） 队列](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/peer.go#L291)，直到其通过[p2p层](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/peer.go#L281)[发送](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/broadcast.go#L46)。p2p消息被识别为`NewBlockMsg`。其余节点的收到一个[包括区块哈希的轻量级通告](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/handler.go#L597)。\r\n\r\n请注意，这只对PoW有效。在权益证明中，[区块传播将发生在共识引擎上](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/handler.go#L562-L572)。\r\n\r\n## 验证区块\r\n\r\n对等节点不断[监听消息](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/handler.go#L186)。每种类型的[可能的消息都有一个相关的处理程序](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/handler.go#L167-L182)，一旦收到相应的消息，就立即[调用](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/handler.go#L215)。\r\n\r\n因此，在得到带有区块数据的 \"NewBlockMsg \"消息时，[其相应的处理程序](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/handlers.go#L328)被执行。处理程序[对消息进行解码](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/handlers.go#L329-L333)并对传播的块运行一些[早期验证](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/handlers.go#L334-L344)。这些验证包括对报头数据的初步[理智检查](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/types/block.go#L128-L143)，主要是确保它们被填充和约束。以及对区块的[uncle](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/handlers.go#L337-L340)和[交易](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/handlers.go#L341-L344)哈希值进行验证。\r\n\r\n然后[发送消息的对等节点被标记为](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/handlers.go#L349)拥有该区块。从而避免了以后将区块传播回给它。\r\n\r\n最后，数据包被[向下传递](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/protocols/eth/handlers.go#L351)到[第二个处理程序](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/handler_eth.go#L67-L68)，在那里区块将被[入队导入](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/handler_eth.go#L123-L124)到链的本地副本。入队是通过[向相应的通道（channel）直接发送导入请求](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/fetcher/block_fetcher.go#L268)完成的。当请求被[拾取](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/fetcher/block_fetcher.go#L420)时，它就会触发[实际的入队操作](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/fetcher/block_fetcher.go#L429)。最后[推送](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/fetcher/block_fetcher.go#L796)区块数据到队列中。\r\n\r\n该区块现在在本地队列中，准备被处理。这个队列在节点的区块提取器主循环中被[定期读取](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/fetcher/block_fetcher.go#L354)。当区块到达前面时，节点将拾取它并[尝试导入](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/fetcher/block_fetcher.go#L376)。\r\n\r\n在实际插入候选块之前，至少有两个值得强调的验证。\r\n\r\n首先，本地链必须已经[包括被传播块的父节点](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/fetcher/block_fetcher.go#L848-L853)。\r\n\r\n\r\n\r\n第二，区块的头[必须是有效的](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/fetcher/block_fetcher.go#L855)。这些验证是真正的验证。意思是说，那些对共识真正重要的，并且在以太坊的[黄皮书](https://ethereum.github.io/yellowpaper/paper.pdf#subsection.4.3)中被指定。因此，它们是[由共识引擎处理](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/consensus.go#L263)。\r\n\r\n举例来说，引擎会检查[区块的工作证明是否有效](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/consensus.go#L310)，或者区块的时间戳是否[不在过去](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/consensus.go#L274)或[不在未来太远](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/consensus.go#L270)，或者区块高度是否[已经正确增加](https://github.com/ethereum/go-ethereum/blob/v1.10.18/consensus/ethash/consensus.go#L305)，等等。\r\n\r\n在验证了它符合共识规则之后，整个区块被进一步[传播到一个对等节点的子集](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/fetcher/block_fetcher.go#L859)。然后才是[实际的导入运行](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/fetcher/block_fetcher.go#L871)。\r\n\r\n在导入过程中会有很多事情发生。所以我将直接切入正题。\r\n\r\n在[几个额外的验证](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/blockchain.go#L1414-L1595)之后，[父区块状态被检索](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/blockchain.go#L1603)。这是新区块的第一个交易将被执行的状态。以它为参考点，[整个区块被处理](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/blockchain.go#L1632)。如果你曾经听说过所有以太坊节点都要执行和验证每一笔交易，现在你可以确定了。之后，[完成后状态被验证](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/blockchain.go#L1654)（见如何[这里](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/block_validator.go#L81)）。最后，[该区块被写入](https://github.com/ethereum/go-ethereum/blob/v1.10.18/core/blockchain.go#L1669-L1674)到本地链。\r\n\r\n成功导入继续[发布](https://github.com/ethereum/go-ethereum/blob/v1.10.18/eth/fetcher/block_fetcher.go#L877)该区块到其他节点的对等物上（不是完全广播）。\r\n\r\n整个验证过程被复制到所有收到区块的节点。很大一部分会接受它进入他们的本地链，以后会有更多的区块到达，插入到它的上面。\r\n\r\n## 检索交易\r\n\r\n在包含交易的区块上挖出几个区块后，就可以开始安全地假设交易确实已经被确认。\r\n\r\n从链上找回交易是非常简单的。我们所需要的是它的哈希值。方便的是，在我们第一次提交交易的时候就已经获得了它。\r\n\r\n交易本身的数据，加上区块的哈希值和编号，总是可以在节点的`eth_getTransactionByHash`端点上检索到。不出所料，它现在返回：\r\n\r\n```json\r\n{\r\n    \"hash\": \"0xbf77c4a9590389b0189494aeb2b2d68dc5926a5e20430fb5bc3c610b59db3fb5\",\r\n    \"type\": 2,\r\n    \"accessList\": [],\r\n    \"blockHash\": \"0xe880ba015faa9aeead0c41e26c6a62ba4363822ddebde6dd77a759a753ad2db2\",\r\n    \"blockNumber\": 15166167,\r\n    \"transactionIndex\": 0,\r\n    \"confirmations\": 6,\r\n    \"from\": \"0x6fC27A75d76d8563840691DDE7a947d7f3F179ba\",\r\n    \"maxPriorityFeePerGas\": 2000000000,\r\n    \"maxFeePerGas\": 120000000000,\r\n    \"gasLimit\": 40000,\r\n    \"to\": \"0x6B175474E89094C44Da98b954EedeAC495271d0F\",\r\n    \"value\": 0,\r\n    \"nonce\": 0,\r\n    \"data\": \"0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000\",\r\n    \"r\": \"0x057d733933b12238a2aeb0069b67c6bc58ca8eb6827547274b3bcf4efdad620a\",\r\n    \"s\": \"0x00e49937ec81db89ce70ebec5e51b839c0949234d8aad8f8b55a877bd78cc293\",\r\n    \"v\": 1,\r\n    \"creates\": null,\r\n    \"chainId\": 31337\r\n}\r\n```\r\n\r\n交易的收据可以在`eth_getTransactionReceipt`端点请求。根据你运行这个查询的节点，你可能还会得到预期交易收据数据之外的额外信息。这是我从mainnet的本地分叉中得到的交易收据：\r\n\r\n```json\r\n{\r\n    \"to\": \"0x6B175474E89094C44Da98b954EedeAC495271d0F\",\r\n    \"from\": \"0x6fC27A75d76d8563840691DDE7a947d7f3F179ba\",\r\n    \"contractAddress\": null,\r\n    \"transactionIndex\": 0,\r\n    \"gasUsed\": 34706,\r\n    \"logsBloom\": \"0x00000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000008000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000010000000000000004000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000002000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000\",\r\n    \"blockHash\": \"0x8b6d44d6cf39d01181b90677f8a77a2605d6e70c40d649eda659499063a19c77\",\r\n    \"transactionHash\": \"0xbf77c4a9590389b0189494aeb2b2d68dc5926a5e20430fb5bc3c610b59db3fb5\",\r\n    \"logs\": [\r\n        {\r\n            \"transactionIndex\": 0,\r\n            \"blockNumber\": 15166167,\r\n            \"transactionHash\": \"0xbf77c4a9590389b0189494aeb2b2d68dc5926a5e20430fb5bc3c610b59db3fb5\",\r\n            \"address\": \"0x6B175474E89094C44Da98b954EedeAC495271d0F\",\r\n            \"topics\": [\r\n                \"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\",\r\n                \"0x0000000000000000000000006fc27a75d76d8563840691dde7a947d7f3f179ba\",\r\n                \"0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045\"\r\n            ],\r\n            \"data\": \"0x0000000000000000000000000000000000000000000000000de0b6b3a7640000\",\r\n            \"logIndex\": 0,\r\n            \"blockHash\": \"0x8b6d44d6cf39d01181b90677f8a77a2605d6e70c40d649eda659499063a19c77\"\r\n        }\r\n    ],\r\n    \"blockNumber\": 15166167,\r\n    \"confirmations\": 6, // number of blocks I waited before fetching the receipt\r\n    \"cumulativeGasUsed\": 34706,\r\n    \"effectiveGasPrice\": 9661560402,\r\n    \"type\": 2,\r\n    \"byzantium\": true,\r\n    \"status\": 1\r\n}\r\n```\r\n\r\n你看到了吗？它说`\"status\"：1`.\r\n\r\n这只能说明一件事：成功\r\n\r\n## 后记\r\n\r\n这个故事绝对可以有更多的内容。\r\n\r\n从某种意义上说，它是永无止境的。总会有一个更多的注意事项。还有一个附带说明。一个替代的执行路径。另一个节点的实现。另一个我可能已经跳过的EVM指令。另一个以不同方式处理事情的钱包。所有的事情都会使我们更接近于找到当你发送1个DAI时发生的 \"真正真相\"。\r\n\r\n幸运的是，这不是我打算做的事。我希望最后的+1万字没有让你这么想😛。请允许我在这里阐明一些情况。\r\n\r\n事后看来，这篇文章是混合了好奇心和挫折感的副产品。\r\n\r\n好奇是因为我做以太坊智能合约安全已经超过4年了，但我还没有像我希望的那样花更多的时间来手动深入探索基础层的复杂性。我真的想获得第一手的经验，看看以太坊本身的实际实现。但智能合约总是被挡在中间。现在我终于找到了更多的和平时光，这似乎是回归本源并开始这次冒险的正确时机。\r\n\r\n\r\n\r\n但是好奇心是不够的。我需要一个借口。一个触发点。我知道我所想的会很艰难。所以我需要一个足够强大的理由，不仅是为了开始工作。而且，更重要的是，每当我觉得自己厌倦了试图从以太坊的代码中找出意义时，就会重新开始。\r\n\r\n我在我没有注意的地方找到了它。我在挫折中发现了它。\r\n\r\n沮丧的是，我们在汇款时已经习惯了绝对令人震惊的缺乏透明度的情况。如果你曾经需要在一个资本管制日益严格的发展中国家进行汇款，毫无疑问，你会感受到我的心情。所以我想提醒自己，我们可以做得更好。我决定用文字来表达我的挫折感。\r\n\r\n这篇文章也给我提了个醒。如果你能避开那些模糊的东西、价格、JPEG中的猴子、Ponzis、Rugpulls和盗窃，这里仍然有价值。这不是 \"神奇 \"的互联网货币。这里有真正的数学、密码学和计算机科学。作为开放源码，你可以看到每一块的移动。你几乎可以触摸到它们。不管是哪一天，也不管是什么时候。无论你是谁。无论你来自哪里。\r\n\r\n\r\n\r\n本翻译由 [DeCert.me](https://decert.me/) 协助支持， DeCert.me 的口号是`码一个未来`，愿景是为所有开发者构建可信的技能履历"},"author":{"user":"https://learnblockchain.cn/people/412","address":null},"history":"QmdvxFpybVuz2oGfFNkKtxqAQ6Z6pgwf7zkQMo1f4H3isZ","timestamp":1684221619,"version":1}