ABI编码深度解析:Solidity如何将你的数据转换为字节

本文深入探讨了以太坊ABI编码机制,详细解释了Solidity函数调用和自定义结构体数据如何被编码成EVM可处理的十六进制字节。文章通过具体示例,包括函数选择器和参数的编码规则,展示了静态和动态类型数据的处理过程,并总结了ABI编码的核心原理。

当你在 Solidity 中调用 store({ number: 123, owner: "bob" }) 这样的函数时,你的钱包不会将“123”或“bob”发送到区块链。

相反,它发送的是一长串十六进制字节,以太坊虚拟机必须将它们解码回这些值。

这个过程由以太坊应用二进制接口 (ABI) 定义,它是一种描述数据结构和函数调用如何编码和解码的规范。

在这篇文章中,我们将通过手动拆解一个带有自定义结构体的函数调用,深入探讨 ABI 编码。你将确切了解 Solidity 如何将你的输入转换为字节,以及如何再次解码它们。

合作

我目前乐于接受区块链、智能合约和全栈系统方面的合作和开发项目,如果你正在构建有趣的东西,欢迎在 LinkedIn 上联系我。

ABI 编码深入探讨

在以太坊中,应用二进制接口 (ABI) 定义了在与智能合约交互时数据结构和函数是如何编码和解码的。无论是手动调用函数、分析交易 calldata,还是使用低级别的 call,理解 ABI 编码都至关重要。

让我们使用一个比简单的 uint256 函数稍复杂的例子来探索 ABI 编码:一个带有自定义结构体的合约。

pragma solidity 0.8.12;

contract Storage {
    struct my_storage_struct {
        uint256 number;
        string owner;
    }
    my_storage_struct my_storage;

    function store(my_storage_struct calldata new_storage) public {
        my_storage = new_storage;
    }
    function retrieve() public view returns (my_storage_struct memory){
        return my_storage;
    }
}

输出 ABI:

[\
 {\
  "inputs": [\
   {\
    "components": [\
     {\
      "internalType": "uint256",\
      "name": "number",\
      "type": "uint256"\
     },\
     {\
      "internalType": "string",\
      "name": "owner",\
      "type": "string"\
     }\
    ],\
    "internalType": "struct Storage.my_storage_struct",\
    "name": "new_storage",\
    "type": "tuple"\
   }\
  ],\
  "name": "store",\
  "outputs": [],\
  "stateMutability": "nonpayable",\
  "type": "function"\
 },\
 {\
  "inputs": [],\
  "name": "retrieve",\
  "outputs": [\
   {\
    "components": [\
     {\
      "internalType": "uint256",\
      "name": "number",\
      "type": "uint256"\
     },\
     {\
      "internalType": "string",\
      "name": "owner",\
      "type": "string"\
     }\
    ],\
    "internalType": "struct Storage.my_storage_struct",\
    "name": "",\
    "type": "tuple"\
   }\
  ],\
  "stateMutability": "view",\
  "type": "function"\
 }\
]

调用store(...)

记住我以便更快登录

当调用 store({ number: 123456789, owner: "bob"}) 时,ABI 编码过程如下所示:

  1. 函数选择器 (4 字节):

它是函数签名的 Keccak-256 哈希值的前 4 个字节。

签名:"store((uint256,string))"

哈希:0xddd456b3...

选择器:0xddd456b3

keccak256("store((uint256,string))") = 0xddd456b3
  1. 参数编码 (tuple):

由于我们正在传递一个带有字段 (uint256 number,string) 的结构体,ABI 将值编码为填充的 32 字节字:

2.1. number 的编码:0x00000000000000000000000000000000000000000000000000000000075bcd15

2.2. 字符串“bob”的编码:

在 ABI 编码中,字符串是动态大小的。它们被编码为指向偏移量的指针,后跟字符串长度和 UTF-8 字节。

  • 偏移量:相对于参数块的起始位置。0x40 (十进制 64),因为字符串在 2 个字(32 字节 * 2)之后开始。
0x0000000000000000000000000000000000000000000000000000000000000040
  • “bob”的长度:3
0x0000000000000000000000000000000000000000000000000000000000000003
  • 字节:0x626f62 (ASCII 表示 "bob"),填充至 32 字节
0x626f620000000000000000000000000000000000000000000000000000000000
  1. 最终的 calldata:
0xddd356b3
0000000000000000000000000000000000000000000000000000000000000020
00000000000000000000000000000000000000000000000000000000075bcd15
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000003
626f620000000000000000000000000000000000000000000000000000000000

调用retrieve()

当调用函数 retrieve() public view returns (my_storage_struct memory) 时,你将收到一个ABI 编码为元组的返回值:(uint256 number, string owner)

从智能合约的角度来看,解码会是这样:

(uint256 number, string memory owner) = abi.decode(returnData, (uint256, string));

// 你将得到
number = 123456789
owner = "bob"

总结

当你调用 Solidity 函数时,你发送给 EVM 的所有内容都会根据 ABI 规范转换为字节。

  1. calldata 的前 4 个字节函数选择器,计算方式为 keccak256(functionSignature)[0:4]
  2. 静态类型(例如 uint256addressbool)直接编码为 32 字节的字。
  3. 动态类型(例如 stringbytes、数组)被编码为指向其实际数据(在 calldata 中稍后出现)的偏移量指针
  4. 结构体 (tuples) 结合了这些规则:每个字段按顺序编码,遵循静态与动态布局。
  5. 返回值遵循相同的规则,只是从 returnData 解码时是相反的。

简而言之,ABI 编码就是将人类可读的 Solidity 调用转换为 EVM 可以处理的结构化十六进制数据。

一旦你理解了它,你就可以读取原始 calldata、解码 trace,甚至手动构造函数调用。

  • 原文链接: medium.com/@andrey_obruc...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Andrey Obruchkov
Andrey Obruchkov
江湖只有他的大名,没有他的介绍。