本文介绍了以太坊的访问列表交易(Access List Transaction),它是EIP-2930在柏林硬分叉中引入的。访问列表通过预先声明交易将访问的地址和存储槽来优化gas消耗并提高可预测性。文章还演示了如何使用eth_createAccessList RPC方法生成访问列表,以及如何在Go语言中构建和广播EIP-2930交易。
在理解了传统交易之后,以太坊演进的下一步就是访问列表交易,它由柏林硬分叉期间的 EIP-2930 引入。
这种交易类型 (
0x01) 通过添加访问列表扩展了传统格式 —— 访问列表是你交易打算触及的一组预定义的地址和存储槽。通过预先声明这些,以太坊客户端可以“预热”这些存储位置,降低初始读取的成本,并使复杂合约交互的 gas 使用更加可预测。
访问列表交易还改进了签名的表示方式:传统交易中的旧
v值被yParity字段(0x0或0x1)取代,同时chainId移至其自己的字段中,以提高清晰度和重放保护。在本文中,你将学习:
1. 访问列表如何优化 gas 并提高可预测性。
2. 如何使用
eth_createAccessListRPC 方法生成它们。3. 如何在 Polygon Amoy 测试网上用 Go 构建和广播 EIP-2930 交易。
最后,你将了解访问列表的工作原理,并且它将是 EIP-1559 交易的介绍。
这个想法很简单:
你预先包含一个地址列表和你知道你的交易将触及的特定存储槽(键)。
不在列表中的访问仍然允许,但它们会花费更多的 gas。此外,访问列表交易包含 yParity 参数。此参数的返回值可以是 0x0 或 0x1。这是 secp256k1 签名的 y 值的奇偶校验(0 表示偶数,1 表示奇数)。
这是与 Ethereum 的 Berlin 硬分叉 一起引入的。
使用 RLP 编码的交易如下所示:
0x01 || rlp([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, signatureYParity, signatureR, signatureS]).
{
nonce: "0x0", // 发送者在此之前进行的交易数量。
gasPrice: "0x09184e72a000", // 发送者提供的 gas 价格,单位为 wei。
gasLimit: "0x21000", // 发送者提供的最大 gas 量。
to: "0x...", // 接收者的地址。在合约创建交易中不使用。
value: "0x0", // 转移的价值,单位为 wei。
data: "0x...", // 用于定义合约创建和交互。
r: "0x...", // ECDSA 签名 r。
s: "0x...", // ECDSA 签名 s。
y_parity: "0x1" // secp256k1 签名的 y 值的奇偶校验。
chainId: "0x...", // 交易的链 ID。
accessList: [ // 交易计划访问的地址和存储键的列表。
{
"address": "0x...",
"storageKeys": ["..."]
}
],
}
注意:传统交易中的
v签名组件已演变:从组合恢复和chain_id,现在是y_parity(0/1) 用于纯公钥恢复,因为chain_id移动到新交易类型中的其自身字段。
访问列表解释
在 EIP-2930 之前,每次你的交易与智能合约交互或触及当前交易中尚未访问的存储槽时,都会产生“冷”访问成本,即更高的 gas 费用。随后在同一交易中对相同地址或存储槽的访问将是“热”的,这意味着它们成本更低。
问题是对于复杂的交易,尤其是那些涉及多个合约调用或大量存储读取/写入的交易,这些初始的“冷”访问可能会累加,从而使交易更加昂贵。
访问列表地址“预热”这些指定的位置。这意味着当交易实际执行时,对这些预声明项目的初始访问会产生折扣后的前期成本(因为将它们包含在列表中),然后在执行期间花费低得多的“热”访问费用。
访问列表的主要挑战是知道_什么_放在其中。一个复杂的交易可能会触及许多地址和存储槽。手动确定这些几乎是不可能的。
eth_createAccessList RPC 方法的用武之地。此方法由 Geth 等以太坊客户端提供,允许你模拟一个交易并取回该交易将生成的精确 accessList。它是准备优化交易的宝贵工具。例如:假设我们要将 token 转移给某人,并且我们需要了解将调用哪些存储(你可以使用此 faucet 在 Polygon Amoy 网络上获取 LINK token),我们可以使用此调用:
curl https://polygon-amoy.drpc.org \
-X POST \
-H "Content-Type: application/json" \
-d '{"method": "eth_createAccessList", "params": [{"from": "<your-sending-address>","gas": "0x50000", "gasPrice": "0x6fee2d00","to": "0x0Fd9e8d3aF1aaee056EB9e802c3A762a667b1904", "data": "0xa9059cbb0000000000000000000000008056361b1c1361436D61D187d761233b42d1c20e0000000000000000000000000000000000000000000000000DE0B6B3A7640000"}, "pending"], "id": 1, "jsonrpc": "2.0"}'
响应将是:
{"id":1,"jsonrpc":"2.0","result":{"accessList":[{"address":"0x0fd9e8d3af1aaee056eb9e802c3a762a667b1904","storageKeys":["0xf9a42dc9f268c1720e130da18118febc65e0ca534e035a0e39d30cf8daea5f0a","0x0c2d31ae2b93233fa550fc5df04cd7b0b742c0821a2494f91cd79ca74a9e2e48"]}],"gasUsed":"0xd1a4"}}
注意:在之前的帖子中,我们学习了如何构建数据,此数据表示对 LINK token 上的 transfer 方法的调用
意味着这些是将用于 transfer 调用的存储地址。
forge cli 工具检查存储,并在已部署的智能合约上使用 eth_getStorageAt(<slot_num>)。为了得到这个,我们可以使用下一行:
forge inspect srv/Storage.sol:Storage storage
注意:存储合约来自之前的博客文章,请确保你已阅读之前的文章并了解
forgecli 工具。
输出将如下所示:
按 Enter 键或单击以全尺寸查看图像

然后使用 curl 调用 eth_getStorageAt(<slot_num>) 以获取已部署合约的存储地址。
例子
package main
const (
NodeRPCURL = "https://polygon-amoy.drpc.org"
AmoyChainID = 80002 // Polygon Amoy 测试网链 ID
ValueToSend = 0.01e18 // 0.01 ETH
)
func main() {
acc2Addr, acc2Priv := account.GetAccount(2)
to := lo.ToPtr(common.HexToAddress("0x0fd9e8d3af1aaee056eb9e802c3a762a667b1904"))
ctx := context.Background()
client, err := ethclient.Dial(NodeRPCURL)
if err != nil {
log.Fatal("Failed to connect to Ethereum node:", err)
}
// Nonce
nonce, err := client.PendingNonceAt(ctx, lo.FromPtr(acc2Addr))
if err != nil {
log.Fatal("Failed to fetch nonce:", err)
}
// Gas price
gasPrice, err := client.SuggestGasPrice(ctx)
if err != nil {
log.Fatal("Failed to fetch gas price:", err)
}
accessList := types.AccessList{
{
Address: common.Address(common.HexToAddress("0x0fd9e8d3af1aaee056eb9e802c3a762a667b1904")),
StorageKeys: []common.Hash{common.HexToHash("0xf9a42dc9f268c1720e130da18118febc65e0ca534e035a0e39d30cf8daea5f0a"), common.HexToHash("0x0c2d31ae2b93233fa550fc5df04cd7b0b742c0821a2494f91cd79ca74a9e2e48")},
},
}
gasLimit, err := client.EstimateGas(ctx, ethereum.CallMsg{
From: *acc2Addr,
To: to,
Data: common.FromHex("0xa9059cbb0000000000000000000000008056361b1c1361436D61D187d761233b42d1c20e000000000000000000000000000000000000000000000000016345785D8A0000"),
AccessList: accessList,
})
if err != nil {
log.Fatal("Failed to fetch gas limit:", err)
}
chainID := big.NewInt(AmoyChainID) // 使用你的链 ID (80002 = Polygon Mumbai 测试网)
// 构建 AccessListTx
txData := types.AccessListTx{
ChainID: chainID,
Nonce: nonce,
GasPrice: gasPrice,
Gas: gasLimit,
To: to,
Data: common.FromHex("0xa9059cbb0000000000000000000000008056361b1c1361436D61D187d761233b42d1c20e000000000000000000000000000000000000000000000000016345785D8A0000"),
AccessList: accessList,
}
tx := types.NewTx(&txData)
// 签名
signedTx, err := types.SignTx(tx, types.NewEIP2930Signer(chainID), acc2Priv)
if err != nil {
log.Fatal("Failed to sign tx:", err)
}
// 广播
err = client.SendTransaction(ctx, signedTx)
if err != nil {
log.Fatal("Broadcast failed:", err)
}
fmt.Println("Access List Transaction Sent!")
fmt.Println("Tx hash:", signedTx.Hash().Hex())
// 可选地等待挖矿
time.Sleep(10 * time.Second)
receipt, err := client.TransactionReceipt(ctx, signedTx.Hash())
if err != nil {
fmt.Println("Tx not mined yet.")
} else {
fmt.Println("Mined in block:", receipt.BlockNumber)
}
}
你将看到类似于以下的内容:
按 Enter 键或单击以全尺寸查看图像

注意:我鼓励你使用你的钱包以前没有遇到过的 token 地址自己尝试一下,然后在没有访问列表的情况下执行此操作,并查看费用差异。
EIP-2930 引入了访问列表交易,以使合约执行期间的 gas 成本更具可预测性和效率。
通过提前声明地址和存储槽,这些交易“预热”存储访问,从而减少了最初的冷访问 gas 惩罚。
它们还通过将 chainId 从签名中分离出来并将传统的 v 值替换为 yParity,从而简化了签名恢复,从而使以太坊的签名格式现代化。
访问列表交易充当旧的传统(类型 0x0)格式和较新的 EIP-1559(类型 0x2)模型之间的桥梁,为更优化和透明的交易层铺平了道路。
- 原文链接: medium.com/@andrey_obruc...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!