本文是Solidity ABI编码系列文章的第二部分,深入探讨了Solidity中复杂数据结构(如结构体、数组和嵌套类型)的ABI编码机制。文章详细解释了静态结构体、动态结构体和嵌套动态类型结构体的编码过程,通过分步骤的示例,展示了如何确定结构体类型、创建头尾布局、编码头部和尾部,以及如何将它们组合起来生成最终的calldata。文章旨在帮助读者掌握Solidity ABI编码中的递归模式。
注意: 在开始之前阅读上一部分内容:
通过 本系列文章的第一部分,你已经内化了 ABI 编码的基础知识。
我们现在准备更深入地研究 Solidity 开发者每天使用的复杂结构:结构体、数组和深度嵌套类型。
本文的这一部分不仅仅是添加更多类型,而是关于解锁 ABI 编码中的递归模式。
在这一部分中,我们将主要关注 solidity 结构体是如何编码的。
为了演示逐步编码,我们将使用 3 种不同类型的结构体示例:
在继续结构体类型中复杂的编码机制之前,让我们首先学习一些必要的知识:
Solidity 中的 struct
(结构体) 是一种用户定义的类型,它将多个变量组合在一个名称下。结构体中的每个字段可以是不同的类型,结构体的编码完全取决于这些字段的组成。
我们如何知道一个结构体的类型?
正如我们在第 1 部分学到的,在编码时,理解我们正在编码的参数类型至关重要。规则取决于我们编码的类型。
但是如果一个结构体可以同时具有静态和动态类型呢?
ABI 规范 编码过程根据以下内容将类型分为静态和动态结构体:
例如:
struct ABC{
uint256 a;
address b;
bool c;
}
struct XYZ{
uint256 x;
address y;
bytes c;
}
注意: 为了 ABI 的目的,结构体总是被视为元组。
编码的基本规律将与我们在第 1 部分中讨论的相同,我们所需要做的就是:
让我们现在开始编码。
让我们从一个仅包含静态类型的结构体开始 - 所有字段在编译时都具有固定的大小。
struct StaticStruct {
uint256 id;
bool isActive;
address owner;
}
function encodeStaticStruct(StaticStruct memory s) public pure returns (bytes memory) {
return msg.data;
}
// Arguments : [10, true, "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"]
我们现在应用 ABI 编码的基本规律,正如我们在第 1 部分中定义的那样。
步骤 1:确定结构体类型并可视化其头-尾布局
我们有一个单一的结构体 s
,其所有类型都是静态的。这使得我们的结构体成为一个静态类型变量。
因此,参数元组看起来像:
tuple(s.id, s.isActive, s.owner)
or
tuple(10, true, 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4)
因为所有字段都是静态的,所以没有尾部部分。
布局很简单:
encoded = [head(id)][head(isActive)][head(owner)]
没有动态类型 → 没有偏移量 → 没有尾部部分。
步骤 2:开始编码头和尾
让我们现在单独编码每个头。
回想一下,静态类型头的编码是编码值本身。
000000000000000000000000000000000000000000000000000000000000000a
uint8
→ 变为 0x01
0000000000000000000000000000000000000000000000000000000000000001
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
address
是 20 字节0000000000000000000000005B38Da6a701c568545dCfcB03FcB875f56beddC4
步骤 3:组合所有编码后的头
将它们放在一起:
Function-Signature: 118e7fac
------------
[000]: 000000000000000000000000000000000000000000000000000000000000000a
[020]: 0000000000000000000000000000000000000000000000000000000000000001
[040]: 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
这很简单,因为 StaticStruct 是一个简单的静态类型。
现在让我们进入复杂的动态结构体世界。
对于动态结构体,让我们考虑以下示例。
struct DynamicStruct {
uint256 id;
string name;
bytes data;
}
function encodeDynamicStruct(DynamicStruct memory s) public pure returns (bytes memory) {
return msg.data;
}
// Arguments: [10, "DecipherClub", "0xabcd1234"]
步骤 1:确定结构体类型并可视化其头-尾布局
我们正在传递一个单一的结构体 s
,这次,它包含静态和动态字段:
id
→ 静态 ( uint256
)name
→ 动态 ( string
)data
→ 动态 ( bytes
)这使得整个结构体成为一个动态类型( 因为 data
是一个动态类型)。
这表明 encodeDynamicStruct
函数具有一个动态类型参数。
回想一下,编码规则表明对于动态类型变量,我们有:
这意味着我们最初的 头-尾 布局看起来像这样
encoding = [head(s)[tail(s)]
where s is the DynamicStruct type passed as argument
编码 head(s)
正如我们所知,动态类型的头的编码应该简单地指向尾部开始的位置。
由于此函数采用一个动态结构体,因此编码后的 calldata 的前 32 个字节是指向结构体实际编码开始位置的偏移量 - tuple(id, name, data)
布局的开头。
因此,我们有:
offset = 0x20 = 32 bytes
encode(head(s)):
[000]: 0000000000000000000000000000000000000000000000000000000000000020
编码 tail(s)
现在事情变得有趣了。
动态类型的尾部是值本身的编码。
在这种情况下,“s” 的尾部是多个参数的元组(id、name、data)。
这意味着:
s.id
、s.name
和 s.data
视为常规函数参数换句话说,结构体的尾部本身就是一个微型的 ABI 编码的 payload,由每个成员的头-尾布局组成。
我们现在讨论的是嵌套编码。编码中的编码。
现在从元组的角度考虑参数,这意味着:
tuple(s.id, s.name, s.data)
or
tuple(10, "DecipherClub", "0xabcd1234")
头-尾布局看起来像这样:
encoded = [head(id)][head(name)][head(data)][tail(name)][tail(data)]
// Note: no tail for id because id is static type
开始编码头和尾
我们现在单独编码结构体值的头-尾。
让我们计算每个头:
head(id)
= 值 10 → 左侧填充的 uint256
:encode(head(id)):
[020]: 000000000000000000000000000000000000000000000000000000000000000a**
head(name)
= 指向 tail(name) 的偏移量
tail(name)
的偏移量,请记住:
name
之前的参数是静态类型 - 因此我们不需要为其尾部保存任何空间(因为静态类型没有任何尾部)tail(name)
从结构体编码开始的 96 字节后开始,即十六进制中的 0x60
编码为:
encode(head(name)):
[040]: 0000000000000000000000000000000000000000000000000000000000000060
head(data)
= 指向 tail(data) 的偏移量 这意味着,head(data)
将包含 0xa0 作为偏移量。
head(data)
的偏移量,请记住:
data
的尾部的总空间是:
name → 字符串类型,它将占用两个 32 字节的槽:
请记住,动态类型存储它们的长度 + 编码数据。
encode(head(data)):
[060]: 00000000000000000000000000000000000000000000000000000000000000a0
我们现在完成了 head
编码,所以让我们继续进行尾部编码。
id
是一个静态 uint256 类型id
将没有尾部。0xc
4465636970686572436C7562
(在此处验证 here )tail(name)
的编码将是:encode(tail(name)):
[080]: 000000000000000000000000000000000000000000000000000000000000000c
[0a0]: 4465636970686572436c75620000000000000000000000000000000000000000
abcd1234
encode(tail(name)):
[0c0]: 0000000000000000000000000000000000000000000000000000000000000004
[0e0]: abcd123400000000000000000000000000000000000000000000000000000000
最后一步:组合所有内容
markdown
CopyEdit
Function Selector: 2b01f4b4
------------
[000]: 0000000000000000000000000000000000000000000000000000000000000020 ← 顶层偏移量
[020]: 000000000000000000000000000000000000000000000000000000000000000a ← id
[040]: 0000000000000000000000000000000000000000000000000000000000000060 ← 指向 tail(name) 的偏移量
[060]: 00000000000000000000000000000000000000000000000000000000000000a0 ← 指向 tail(data) 的偏移量
[080]: 000000000000000000000000000000000000000000000000000000000000000c ← tail(name) 长度
[0a0]: 4465636970686572436c75620000000000000000000000000000000000000000 ← tail(name) 数据
[0c0]: 0000000000000000000000000000000000000000000000000000000000000004 ← tail(data) 长度
[0e0]: abcd123400000000000000000000000000000000000000000000000000000000 ← tail(data) 数据
这是我们目前最先进的结构体——它包括了动态类型 和 内部的 动态数组。
但是,我们将遵循类似的规则来编码它。
struct SuperDynamicStruct {
uint256 id;
string label;
address[] signers;
bytes metadata;
}
function encodeSuperDynamicStruct(SuperDynamicStruct memory s) public pure returns (bytes memory) {
return msg.data;
}
// Arguments: [99, "DecipherClub", [0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835Cb2], "0xbeefcafebabe"]
步骤 1:确定结构体类型并可视化其头-尾布局
该结构体包含:
id
→ 静态 ( uint256
)label
→ 动态 ( string
)signers
→ 动态 ( address[]
)metadata
→ 动态 ( bytes
)→ 因为它包含动态字段,所以该结构体是一个 动态类型。
初始的头-尾布局如下所示:
encoding = [head(s)[tail(s)]
where s is the SuperDynamicStruct type passed as argument
遵循我们之前为“DynamicStruct”遵循的类似规则集,我们有
前 32 个字节表示结构体编码开始处的 偏移量。
encode(head(s)):
[000]: 0000000000000000000000000000000000000000000000000000000000000020
步骤 2:将结构体的尾部视为一个元组
一旦我们跳转到该偏移量,我们就进入了结构体主体,从概念上讲:
encode(tail(s)) = encode( tuple(s.id, s.label, s.signers, s.metadata) )
or
encode( tuple(99, "DecipherClub", [address, address], 0xbeefcafebabe) )
我们现在再次应用我们的头-尾逻辑:
encoded = [head(id)][head(label)][head(signers)][head(metadata)][tail(label)][tail(signers)][tail(metadata)]
这意味着现在我们需要单独解决所有这些问题才能获得最终的编码值。
但是,嘿,我们之前已经做过了。没大不了的,anon.
步骤 3:编码头
head(id)
→ 99 → 静态 → 左填充 uint256:[020]: 0000000000000000000000000000000000000000000000000000000000000063
head(label)
→ 应该提供到 tail(label)
的偏移量
tail(label)
从字节 0x80
(128) 开始head(label)
→ 应该提供到 tail(label) 的偏移量
注意:
label
之前的参数是静态类型 - 因此我们不需要为其尾部保存任何空间(因为静态类型没有任何尾部)
encode(head(label)):
[040]: 0000000000000000000000000000000000000000000000000000000000000080
head(signers)
→ 到 tail(signers) 的偏移量
signers
之前的参数是 id 和 label。0xc0
encode(head(signers)):
[060]: 00000000000000000000000000000000000000000000000000000000000000c0
head(metadata)
→ 到 tail(metadata) 的偏移量
signers
之前的参数是 id 和 label 以及 signers 数组:
0x120
(以十六进制表示)0x120
作为其偏移量。[080]: 0000000000000000000000000000000000000000000000000000000000000120
到目前为止,我们已经包含了所有的头部编码,并且我们的整体 calldata 看起来像这样:
Function Selector: d291d14f
----------------------------
[000]: 0000000000000000000000000000000000000000000000000000000000000020 ← head(s)
[020]: 0000000000000000000000000000000000000000000000000000000000000063 ← id
[040]: 0000000000000000000000000000000000000000000000000000000000000080 ← 到 label 的偏移量
[060]: 00000000000000000000000000000000000000000000000000000000000000c0 ← 到 signers 的偏移量
[080]: 0000000000000000000000000000000000000000000000000000000000000120 ← 到 metadata 的偏移量
步骤 4:编码尾部
现在是时候编码所有尾部了。
DecipherClub
"4465636970686572436C7562
(12 字节)我们之前已经做过了。
encode(tail(label)):
[0a0]: 000000000000000000000000000000000000000000000000000000000000000c
[0c0]: 4465636970686572436c75620000000000000000000000000000000000000000
[0e0]: 0000000000000000000000000000000000000000000000000000000000000002
[100]: 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
[120]: 000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2
0xbeefcafebabe
= 6 字节beefcafebabe
提供encode(tail(metadata)):
[140]: 0000000000000000000000000000000000000000000000000000000000000006
[160]: beefcafebabe0000000000000000000000000000000000000000000000000000
最后一步:组合所有内容
Function Selector: d291d14f
----------------------------
[000]: 0000000000000000000000000000000000000000000000000000000000000020 ← head(s)
[020]: 0000000000000000000000000000000000000000000000000000000000000063 ← id
[040]: 0000000000000000000000000000000000000000000000000000000000000080 ← 到 label 的偏移量
[060]: 00000000000000000000000000000000000000000000000000000000000000c0 ← 到 signers 的偏移量
[080]: 0000000000000000000000000000000000000000000000000000000000000120 ← 到 metadata 的偏移量
[0a0]: 000000000000000000000000000000000000000000000000000000000000000c ← tail(label) 长度
[0c0]: 4465636970686572436c75620000000000000000000000000000000000000000 ← tail(label) 值
[0e0]: 0000000000000000000000000000000000000000000000000000000000000002 ← tail(signers) 长度
[100]: 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 ← signer[0]
[120]: 000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2 ← signer[1]
[140]: 00000000000000000000000000000000000000000
- 原文链接: decipherclub.com/solidit...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!