eth_getCode(address, blockParameter) 返回指定区块中部署在某个地址上的十六进制编码 EVM 字节码;如果该地址是外部拥有账户(EOA)、从未部署过或已自毁,则返回 '0x'。与 eth_getStorageAt 和 eth_call 等返回代码计算值的状态读取不同,eth_getCode 返回的是代码本身。区块参数至关重要,因为代码会在部署时以及通过升级代理发生变化;eth_getCode(addr, 'latest') 只有在解析出实现地址后,才能看到代理背后的实现代码。EIP-7702 委托指示符(0xef0100 + 委托地址)意味着简单的“长度 > 0 即合约”测试会错误分类被委托的 EOA。本指南涵盖地址推导、字节码前缀检测、代理槽位验证、编译产物比对,以及 eth_getCode 的客观局限性。
eth_getCode 返回什么,以及为什么区块参数很重要
以太坊 JSON-RPC 规范将 eth_getCode(address, blockParameter) 定义为返回指定区块中给定地址上的十六进制编码 EVM 字节码。返回值是以 0x 为前缀的 DATA 字符串;如果该地址是外部拥有账户(EOA)、不存在的地址或已自毁的合约,则返回 '0x'。这种歧义是根本性的:'0x' 并不能告诉你这三种情况中的哪一种。规范的方法定义记录在以太坊 JSON-RPC eth_getCode 参考中。
在实践中,区块参数并非可选。代码会在部署时以及通过升级代理发生变化,因此 eth_getCode(addr, 'latest') 反映当前代码,而 eth_getCode(addr, '0x...') 反映历史代码。如果你在调试“部署并使用”序列,'latest' 可能显示在收据区块尚不存在的代码。当可复现性很重要时,务必固定区块参数。
eth_getStorageAt 和 eth_call 等状态读取返回的是由该代码计算出的值,而不是代码本身。当你需要验证部署了什么时,使用 eth_getCode;当你需要检查存储槽或模拟调用时,使用状态读取。eth_getStorageAt 与槽位打包指南深入介绍了存储方面。
- eth_getCode 返回字节码;eth_getStorageAt 返回 32 字节存储字;eth_call 返回模拟调用的结果。
- '0x' 存在歧义:可能是 EOA、从未部署或已自毁。
- 固定区块参数,以获得可复现的字节码读取。
- 权威参考:https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getcode
通过字节码长度区分 EOA 与合约
成功部署后,合约的字节码长度大于零。EOA 没有代码,因此 eth_getCode 返回 '0x'(长度为零)。这是在代码中区分 EOA 与合约的标准启发式方法。然而,由于自毁合约和 EIP-7702 委托的存在,仅靠这一点并不充分。
在“部署并使用”序列中,'latest' 处的代码与收据区块处的代码并不相同。如果你在发送部署交易后立即查询 eth_getCode,代码可能在你预期的区块尚不可见。务必在收据区块或之后查询,并先验证交易状态。
对于可运行的示例,下面的 Node.js 代码片段检查代码长度并检测已知的 Solidity 字节码前缀(0x60806040)。它使用通用 JSON-RPC 端点;请将 URL 替换为你的提供商端点。OnFinality 的以太坊 RPC 节点指南(RPC Assistant)说明了如何获取端点。
const https = require('https');
const RPC_URL = 'https://your-ethereum-rpc-endpoint';
const ADDRESS = '0xYourContractAddress';
function rpcCall(method, params) {
return new Promise((resolve, reject) => {
const data = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
const req = https.request(RPC_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => resolve(JSON.parse(body)));
});
req.on('error', reject);
req.write(data);
req.end();
});
}
async function checkCode() {
const result = await rpcCall('eth_getCode', [ADDRESS, 'latest']);
const code = result.result;
const length = (code.length - 2) / 2; // bytes
console.log('Bytecode length:', length, 'bytes');
if (length === 0) {
console.log('No code: EOA, never-deployed, or self-destructed.');
return;
}
if (code.startsWith('0x60806040')) {
console.log('Detected Solidity 0.8.x bytecode prefix.');
}
if (code.startsWith('0xef0100')) {
console.log('EIP-7702 delegation designator detected.');
}
}
checkCode().catch(console.error);合约地址推导与回读
合约地址是确定性推导的:CREATE 使用 keccak256(rlp([sender, nonce]))[12:],而 CREATE2 使用 keccak256(0xff ++ sender ++ salt ++ keccak256(init_code))[12:]。你可以在部署前计算出预期地址,然后用 eth_getCode 回读以确认部署。这对于反事实部署以及验证工厂是否生成了预期地址非常有用。
回读地址并不能告诉你代码是否与你的源代码匹配。它只告诉你存在某些代码。要验证部署,请将字节码哈希与编译器产物进行比较,同时允许构造函数参数和不可变变量的替换。eth_getProof 账户与存储证明指南介绍了如何证明某个区块的账户状态。
如果你使用代理模式,你部署的地址是代理,而不是实现。eth_getCode(proxy, 'latest') 返回代理的字节码,而不是实现的字节码。你必须先解析实现地址,通常通过读取特定的存储槽来完成。
- CREATE 地址 = keccak256(rlp([sender, nonce]))[12:]。
- CREATE2 地址 = keccak256(0xff ++ sender ++ salt ++ keccak256(init_code))[12:]。
- 对代理调用 eth_getCode 返回代理字节码,而不是实现字节码。
- 使用 eth_getStorageAt 读取实现槽位(EIP-1967 通常为 0x360894...)。
代理模式与解析实现代码
可升级代理将实现地址存储在存储槽中。EIP-1967 标准定义了特定槽位:0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc。要读取实现代码,首先调用 eth_getStorageAt(proxy, slot, blockParameter) 获取实现地址,然后调用 eth_getCode(implementation, blockParameter)。
这个两步过程是必要的,因为 eth_getCode 不会跟随代理委托。它返回你查询的确切地址上的代码。如果你查询代理,你会得到代理的回退逻辑,而不是实现的函数。eth_call 状态覆盖模拟指南展示了如何直接针对实现模拟调用。
下面的 Node.js 示例验证代理的实现槽位,然后读取实现代码。请将 RPC URL 和代理地址替换为你自己的值。
const https = require('https');
const RPC_URL = 'https://your-ethereum-rpc-endpoint';
const PROXY = '0xYourProxyAddress';
const IMPLEMENTATION_SLOT = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc';
function rpcCall(method, params) {
return new Promise((resolve, reject) => {
const data = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
const req = https.request(RPC_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => resolve(JSON.parse(body)));
});
req.on('error', reject);
req.write(data);
req.end();
});
}
async function resolveImplementation() {
const slotResult = await rpcCall('eth_getStorageAt', [PROXY, IMPLEMENTATION_SLOT, 'latest']);
const implAddress = '0x' + slotResult.result.slice(26); // last 20 bytes
console.log('Implementation address:', implAddress);
const codeResult = await rpcCall('eth_getCode', [implAddress, 'latest']);
const code = codeResult.result;
console.log('Implementation bytecode length:', (code.length - 2) / 2, 'bytes');
console.log('First 10 bytes:', code.slice(0, 22));
}
resolveImplementation().catch(console.error);EIP-7702 委托指示符与错误分类风险
EIP-7702 引入了一种新的账户类型:将执行委托给合约的 EOA。委托指示符是特定的字节码前缀:0xef0100 后跟 20 字节的委托地址。这意味着被委托的 EOA 现在会返回以 0xef0100 开头的代码,因此简单的“长度 > 0 即合约”测试会将其错误分类为合约。该 EIP 被记录为待激活;行为可能因提供商和网络而异。
要正确分类账户,请检查 0xef0100 前缀。如果存在,该账户是带委托的 EOA,而不是合约。委托地址是接下来的 20 字节。然后你可以对委托地址调用 eth_getCode 来读取实际的实现代码。
这种区分对于假设任何有代码的地址都是合约的工具很重要。钱包、索引器和安全工具必须更新其启发式方法。权威参考是 EIP-7702 规范。
- 委托指示符:0xef0100 ++ 委托地址(20 字节)。
- 被委托的 EOA 会返回代码,但不是合约。
- 在应用“长度 > 0”启发式之前,检查 0xef0100 前缀。
- EIP-7702 被记录为待激活;请在目标网络上验证。
根据编译器产物验证已部署字节码
要验证已部署合约是否与预期源代码匹配,请将字节码哈希与编译器产物进行比较。产物包含创建字节码和已部署字节码。链上已部署字节码可能因构造函数参数替换(附加到创建字节码,而非已部署字节码)和不可变变量替换(嵌入在已部署字节码的特定位置)而与产物不同。
标准做法是从链上字节码中剥离 CBOR 元数据尾部,然后在应用已知不可变替换后,将剩余字节与产物的已部署字节码进行比较。元数据尾部是 solc 字节码末尾的 CBOR 编码映射;eth_getCode 不会修剪它。你必须自行修剪。
对于可复现的方法,请记录区块号、eth_getCode 结果、产物哈希和修剪后的字节码哈希。使用结果表跨端点进行比较。解码 revert 原因和自定义错误指南涵盖了相关的调试技术。
- 在与产物比较之前,剥离 CBOR 元数据尾部。
- 构造函数参数附加到创建字节码,而非已部署字节码。
- 不可变变量嵌入在已部署字节码中;在哈希之前应用替换。
- 记录区块号和端点以确保可复现性。
结果表:针对你的端点测量 eth_getCode 行为
由于提供商行为各异,请针对你自己的端点测量 eth_getCode。创建一个结果表,包含以下列:端点 URL、区块参数、地址、返回的字节码长度、前 10 字节,以及结果是否与已知产物匹配。对多个端点和区块参数运行相同查询,以检测不一致。
此方法由读者验证;它不声称 OnFinality 特定的数字。用它来比较提供商、确认区块参数语义,并检测缓存或负载均衡差异。RPC 定价页面说明了如何根据查询量选择套餐。
要进行完整测量,请包含一个历史区块和一个 'latest' 区块。如果字节码不同,你就检测到了升级或重组。在任何升级后,通过重新运行表格来重新验证。
- 列:端点、区块、地址、长度、前缀、产物匹配。
- 至少在两个端点运行,以检测不一致。
- 包含历史区块和最新区块,以检测升级。
- 每次升级或分叉后重新验证。
eth_getCode 的局限性与权衡
eth_getCode 不会修剪元数据。solc 字节码末尾的 CBOR 尾部会原样返回。如果你将原始字节码与产物比较,除非修剪尾部,否则会得到不匹配。这是验证工具中常见的假阴性来源。
eth_getCode 不会反编译。它返回原始字节。要理解代码做什么,你需要反编译器或源代码。它还会在不同分叉上返回不同字节,因此请固定区块参数并在升级后重新验证。重组可能会改变给定区块的代码;务必确认最终性。
最后,eth_getCode 不会跟随代理或 EIP-7702 委托。你必须自行解析实现地址或委托地址。这是一个有意的设计选择:RPC 方法返回确切地址上的代码,而不是委托后的有效代码。
- 不修剪元数据:包含 CBOR 尾部。
- 不反编译:仅原始字节。
- 依赖分叉:固定区块参数。
- 不解析代理或委托:手动解析地址。
排查常见的 eth_getCode 问题
如果 eth_getCode 对已部署合约返回 '0x',请检查区块参数。你可能在查询部署之前的区块。还要验证地址:拼写错误或校验和错误会返回 '0x'。如果合约已自毁,'0x' 是正确的。使用 eth_getTransactionReceipt 确认部署状态和区块号。
如果字节码长度不符合预期,请检查 EIP-7702 委托(0xef0100 前缀)或代理模式。如果你在查询代理,你看到的是代理字节码,而不是实现字节码。先解析实现槽位。如果你在与产物比较,请修剪 CBOR 元数据尾部并应用不可变替换。
如果不同端点的结果不同,你可能遇到了不同的分叉或缓存层。固定区块参数并比较区块哈希。使用 OnFinality Learn 中心获取有关状态读取和证明的相关指南。
- 已部署合约返回 '0x':检查区块参数和地址。
- 长度不符合预期:检查 EIP-7702 或代理模式。
- 产物不匹配:修剪 CBOR 尾部并应用不可变替换。
- 跨端点差异:固定区块参数并比较区块哈希。
后续步骤:将 eth_getCode 集成到你的工作流中
将 eth_getCode 集成到你的部署流水线中,以验证正确的字节码已在链上。部署后,在收据区块查询 eth_getCode,并将修剪后的字节码哈希与你的产物比较。存储区块号和哈希以供审计。对于可升级合约,解析实现槽位并单独验证实现代码。
对于生产监控,定期在 'latest' 重新查询 eth_getCode 并与预期哈希比较。对变化发出警报。这可以检测未经授权的升级和代理管理员变更。使用 API 服务在多个网络上自动执行这些检查。
要开始使用,请从 OnFinality 的以太坊网络页面获取端点,并运行本指南中的示例。有关定价和套餐详情,请参阅 RPC 定价。有关以太坊 RPC 方法的更广泛概述,请参阅以太坊 RPC 节点指南(RPC Assistant)。
- 在部署后于收据区块验证字节码哈希。
- 监控 'latest' 代码以发现未经授权的升级。
- 为代理合约解析实现槽位。
- 使用 API 服务自动执行检查。