本文介绍了以太坊智能合约中的字节码、ABI以及如何将字节码反编译为可读的Solidity代码。
由 Alchemy 撰写
由 Brady Werkheiser 审核
发布于 2023 年 7 月 10 日,阅读需 4 分钟
通过智能合约发布到以太坊区块链的原始数据是字节码,或者说是十六进制字符的长字符串。虽然开发者使用人类可读的 Solidity 代码编写和阅读智能合约,但这不是发布到区块链的文本。
同样,每个智能合约的“调用”,或对智能合约发布的外部可见函数之一发出的请求,都以原始字节码或“二进制文件”的形式存在。
以下面的智能合约为例,它被上传到以太坊主网,具有以下(Solidity 编码)结构:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Foo {
function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
}
假设用户想要使用参数 69 和 true 调用函数 baz。
以下是在字节码中实际传输的请求的样子:
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450
...
很难阅读,对吧?
在本文中,我们将讨论为什么 以太坊虚拟机 将所有内容编码为字节码,了解 ABI 是什么以及如何使用它,并掌握一些基本工具将字节码反编译回人类可读的 Solidity。
注意: 本文中的示例借用自官方的 Solidity ABI 文档。
因为在以太坊区块链上存储数据的成本非常高,并且上传的每个字节的数据都需要复制到区块链上的所有完整节点,所以编写和读取原始字节码比上传 Solidity 代码更具成本效益。
解析和存储人类可读的代码可能需要付出高出一个数量级的数据成本,当智能合约在主网上已经要花费数千美元时,这是一个问题。
当智能合约发布时,它们会在发布到以太坊之前自动转换为字节码。但是 - 一旦它们发布到网络上,个人如何知道如何与智能合约交互?几乎不可能查看一长串字节码并了解哪些函数可以调用。
应用程序二进制接口,或 ABI 就是答案。
ABI 是一个人类可读的公共方法列表,用于描述可以对任何特定智能合约进行的调用以及每次调用将返回的内容。
有了 ABI,智能合约的用户不需要读取字节码,就可以将他们在字节码中的调用转换为与智能合约交互。
ABI 与传统 Web2 架构中的 API(应用程序编程接口)极其相似。但是,主要区别在于 Solidity ABI 使 用户可以访问以二进制编码的智能合约中的方法,而 API 使用户可以访问来自在线服务器端点的方法。
因为它们旨在供人类使用和阅读,所以智能合约开发者不会将智能合约的 ABI 发布到区块链,因为这将非常昂贵。
相反,你可以从以下位置获取 ABI:
从智能合约开发者处获得的合约的公开可用源代码,可用于生成 ABI。
如果智能合约在 Etherscan 上经过验证,则从 Etherscan 合约信息中获取。
从智能合约字节码中反向工程 ABI(不推荐)。
ABI 通常作为 Solidity 智能合约的公共函数声明的 JSON 格式编码发布。
以下面的智能合约的函数定义为例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Test {
constructor() { b = hex"12345678901234567890123456789012"; }
event Event(uint indexed a, bytes32 b);
event Event2(uint indexed a, bytes32 b);
error InsufficientBalance(uint256 available, uint256 required);
function foo(uint a) public { emit Event(a, b); }
bytes32 b;
}
相应的 JSON 编码如下所示:
[{\
"type":"error",\
"inputs": [{"name":"available","type":"uint256"},{"name":"required","type":"uint256"}],\
"name":"InsufficientBalance"\
}, {\
"type":"event",\
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],\
"name":"Event"\
}, {\
"type":"event",\
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],\
"name":"Event2"\
}, {\
"type":"function",\
"inputs": [{"name":"a","type":"uint256"}],\
"name":"foo",\
"outputs": []\
}]
虽然你不想手动解析 Solidity 二进制文件以调用函数,因为它很复杂、不直观,并且你可能会犯很多错误,但了解二进制文件在 Solidity 中是如何形成的会非常有帮助,这样你就可以快速浏览调用数据或仔细检查值。
我们将在下一节中链接几个工具,它们应该可以为你处理大部分转录工作。
以上面的例子为例。
假设用户想要使用参数 69 和 true 调用智能合约中的函数 baz。以下是请求在字节码中的样子,总共 68 个字节:
0xcdcd77c0000000000000000000000000000000000000000000000000000000000000004500
...
在这种情况下,0xcdcd77c 通过导出签名 baz(uint32,bool) 的 ASCII 形式的 Keccak 哈希的前 4 个字节来标识方法 baz。
0x00000000000000000000000000000000000000000000000000000000000000045
标识第一个参数 69,这是一个填充为 32 字节的 uint32 值。填充只是意味着添加 0 以保证整个字符串的长度为 32 字节(在本例中),无论实际数字有多大。
0x00000000000000000000000000000000000000000000000000000000000000045
第二个参数是 true,这是一个填充为 32 字节的 bool 值:
0x0000000000000000000000000000000000000000000000000000000000000001
对于包含动态类型的参数,编码看起来略有不同,因为与地址、bool 或 uint32 等静态类型就地编码不同,动态类型在单独分配的位置进行编码。
事件是智能合约在执行方法调用时发布的日志,事件以二进制数据的形式发布。
事件可以接收参数,这有助于指定事件将输出的内容。这些参数可以被索引,这意味着可以使用该索引参数作为过滤器来搜索事件。这些索引参数在 Solidity 术语中也称为主题!
粗略地说,Solidity 事件遵循以下结构:
address:合约的地址
topics[n]:0 - 4 个 topics,或 indexed 参数
任意长度的二进制数据,可以根据 ABI 进行解析。
有各种可用的 EVM 反编译器可以帮助你检索更易读的 Solidity 二进制文件版本,包括 EtherVM 反编译器 和 Panoramix 反编译器。
这些 EVM 反编译器不会返回原始源代码的完美重现(可能会删除名称或其他重要信息以最小化二进制文件大小),但它们应该让你对允许的 ABI 请求有一个高层次的理解。
Supercharged | Alchemy | Substack
注册 Alchemy 大学并开始免费学习 Solidity 开发 立即注册
📚 目录
分享:
\
\
学习 Solidity\
\
Solidity 中的 selfdestruct 是什么?\
\
它是什么,它是如何工作的,以及如何开始使用
\
\
学习 Solidity\
\
学习 Solidity 开发的 7 门最佳课程 (2025)\
\
探索学习 Solidity 开发的最佳免费和付费课程
\
\
学习 Solidity\
\
什么是 Solidity 数组?\
\
你的 Solidity 数组入门指南——函数、声明和故障排除
Alchemy 将最强大的 web3 开发者产品和工具与资源、社区和传奇的支持相结合。
- 原文链接: alchemy.com/overviews/so...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!