本文详细解释了以太坊calldata的工作原理,包括EVM如何通过函数选择器确定要调用的函数,如何计算4字节选择器,参数如何在32字节槽中编码,合约字节码的结构,以及calldata在EVM中的执行过程。文章通过实例展示了calldata的构成,包括函数选择器和ABI编码的参数,并解释了EVM如何解析和执行这些数据。
当你向智能合约发送交易时,你发送的不是纯文本的“命令”。 你发送的是一个精确的字节序列,calldata,它准确地告诉 EVM 要执行哪个函数以及使用哪些参数。 你通过 ethers.js、Foundry 或 MetaMask 发出的每个函数调用最终都会转换为这个 calldata。 它以一个 4 字节的函数选择器开始(从函数签名派生),后跟 ABI 编码的参数。 在这篇文章中,我们将逐步分解 calldata,你将学习: 1. EVM 如何确定要调用哪个函数 2. 如何计算 4 字节选择器 3. 参数如何以 32 字节槽进行编码 4. 合约字节码是什么样的 5. 当执行 calldata 时,EVM 内部会发生什么 最后,你将能够读取交易的
data字段,并准确理解它在做什么——没有黑盒,只有字节。协作
我目前对区块链、智能合约和全栈系统领域的合作和开发项目持开放态度,如果你正在构建有趣的东西,请随时在 LinkedIn 上联系
什么是 Calldata
在我们理解了 EVM 的堆栈(来自之前的文章)之后,让我们深入了解 calldata。calldata 是 EVM 中的一个只读数据位置,用于保存外部函数调用的输入参数。它随交易一起传递,并且在执行期间无法修改。 当一个交易被发送到智能合约时,交易中的
**data**字段告诉合约要运行哪个函数以及使用哪些参数。data字段的前 4 个字节被称为 函数选择器。这个值告诉 EVM 要调用合约中的哪个函数。 (你可以在 4byte-directory 中搜索著名的选择器)EVM 如何确定要调用哪个函数
函数选择器是函数签名的 Keccak-256 哈希的前 4 个字节。 例如:
function set(uint256 x)
"set(uint256)"keccak256("set(uint256)")0x60fe47b1...0x60fe47b1因此,如果你在 calldata 的开头看到 0x60fe47b1,你就知道这是一个对 set(uint256) 的调用。
示例交易 Calldata:
假设你使用值 69420 调用此函数。
set(69420)
Calldata 看起来会是这样:
0x60fe47b1
0000000000000000000000000000000000000000000000000000000000010f2c
分解:
0x60fe47b1 → 函数选择器 (set(uint256))0x...010f2c → 69420,编码为填充的 uint256通用 Calldata 布局:
免费加入 Medium 以获取此作者的更新。 对于任何函数调用:
<4 bytes> 函数选择器(keccak256 的前 4 个字节)
<32 bytes> 参数 1(填充)
<32 bytes> 参数 2(填充)
...
需要了解的注意事项
fallback() 或 receive()。ethers.js、Foundry)可以解码/编码它。eth_sendTransaction),你必须自己构建此 calldata。Foundry 中的示例
cast calldata "set(uint256)" 69420
## 输出:
## 0x60fe47b1000000000000000000000000000000000000000000000000000000010f2c
当你用 Solidity 编写智能合约时,你真正创建的是一个高级蓝图,以太坊虚拟机 (EVM) 最终会将其作为称为 操作码 的低级指令执行。 让我们逐步了解 Solidity 代码是如何编译的以及 EVM 如何处理它。
pragma solidity >=0.4.16 <0.9.0;
contract MiniExample {
uint data;
function set(uint x) public {
data = x;
}
function get() public view returns (uint) {
return data;
}
}
这很容易阅读,但 EVM 不理解 Solidity。它需要 字节码,它由 Solidity 编译器 (solc) 生成。编译过程产生:
如果合约有一个构造函数,那么该逻辑会被捆绑到 部署字节码 中,它只运行一次。部署后,链上存储的内容称为 运行时字节码。
上面合约的编译字节码如下所示
6080604052348015600e575f5ffd5b506101298061001c5f395ff3fe6080604052348015600e575f5ffd5b50600436106030575f3560e01c806360fe47b11460345780636d4ce63c14604c575b5f5ffd5b604a60048036038101906046919060a9565b6066565b005b6052606f565b604051605d919060dc565b60405180910390f35b805f8190555050565b5f5f54905090565b5f5ffd5b5f819050919050565b608b81607b565b81146094575f5ffd5b50565b5f8135905060a3816084565b92915050565b5f6020828403121560bb5760ba6077565b5b5f60c6848285016097565b91505092915050565b60d681607b565b82525050565b5f60208201905060ed5f83018460cf565b9291505056fea2646970667358221220ec163686bf86159ebb242a8ca38f68fe4e9bf9be12def4ec8af94737310b0c6364736f6c634300081e0033
这个十六进制字符串是合约逻辑的直接表示。EVM 将其解释为操作码列表。例如:
60 → PUSH180 → 将值 0x80 推送到堆栈上52 → MSTORE(将值存储在内存中)所以前几个操作是:
[00] PUSH1 80 // 将 1 字节的值 80 推送到堆栈上
[02] PUSH1 40 // 将 1 字节的值 40 推送到堆栈上
[04] MSTORE // 内存存储
[05] CALLVALUE // 从调用中获取已存入的值
[06] DUP1
[07] ISZERO // 一个条件操作码
[08] PUSH1 0xR // 推送 2 字节
[0b] JUMPI // 跳转到堆栈上的另一个位置
...
...
[138]
每个操作码都是 EVM 执行的简单指令,程序计数器 (PC) 逐步执行列表中的每一项。 当你发送交易时会发生什么: 智能合约调用是通过 交易 发出的,其中包括以下字段:
{
"to": "0x8a19ba...",
"from": "0xf9db21...",
"value": "0x0",
"gasPrice": 700000,
"gasLimit": 210000,
"data": "0x60fe47b10000000000000000000000000000000000000000000000000000000000010f2c"
}
以下是 EVM 内部发生的情况:
v、r 和 s 值验证签名。Calldata 在此流程中的工作方式:
让我们看一下交易中的 data 字段:
0x60fe47b10000000000000000000000000000000000000000000000000000000000010f2c
0x60fe47b1
→ 这是 函数选择器,set(uint256) 的哈希值0x...010f2c,等于十进制的 69420此数据告诉 EVM:“调用 set 函数,值为 69420。”
在合约的字节码中,函数选择器被匹配,并在正确的代码段开始执行。
理解 calldata 可以让你透视调用合约时真正发生的事情。 无论你是调试失败的交易还是构建自定义工具,这些知识都会将原始十六进制转换为可读的逻辑。
- 原文链接: medium.com/@andrey_obruc...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!