Elrond序列化格式
原文:https://docs.elrond.com/developers/developer-reference/elrond-serialization-format
Elrond智能合约如何序列化参数、结果和存储
在Elrond,与智能合约交互的所有数据都有特定的序列化格式。序列化格式是任何项目的核心,因为进入和退出合约的所有值都表示为字节数组,需要根据一致的规范进行解释。
在 Rust 中,elrond-codeccrate(crate、 repo 、 doc )专门处理这种格式。Mandos的 Go 和 Rust 实现都有一个序列化为这种格式的组件。DApp 开发者在后端与智能合约交互时需要注意这种格式。
理
我们希望这种格式具有一定的可读性,并尽可能容易地与区块链生态系统的其他部分进行交互。这就是为什么我们为所有的数字类型选择了大端表示法。
更重要的是,这种格式需要尽可能紧凑,因为每增加一个字节需要额外的费用。
顶层与嵌套对象的概念
格式化程序有一个核心好处:我们知道进入合约的字节数组的大小。所有参数都有一个已知的字节大小,我们通常在将值本身加载到合约中之前了解存储值的长度。这直接给了我们一些额外的数据,使我们可以少编码。
假设我们有一个 int32 类型的参数。在智能合约调用期间,我们希望将值“5”传输给它。一个标准的反序列化器可能希望我们发送完整的 4 个字节0x00000005
,但是显然不需要前导零。这是一个单一的论点,我们知道在哪里停止,没有阅读太多的风险。所以送0x05
就够了。我们节省了 3 个字节。这里我们说整数是以它的顶级形式来表示的,它独立存在,可以更简洁地表示。
但是现在想象一个参数反序列化为 int32 的向量。这些数字一个接一个地被序列化。我们不再可能拥有可变长度的整数,因为我们不知道一个数在哪里开始,一个数在哪里结束。我们应该把0x0101
理解为[1, 1]
还是[257]
?因此,解决方案是始终以完整的 4 字节形式表示每个整数。[1, 1]
因此被表示为0x0000000100000001
,而[257]
被表示为0x00000101
,不再有歧义。这里的整数被认为是嵌套形式。这意味着,因为它们是一个更大结构的一部分,它们的表示长度必须从编码中显而易见。
**但是向量本身呢?它的表示形式的长度必须总是 4 字节的倍数,所以从表示形式中,我们总是可以通过将字节数除以 4 来推导出向量的长度。如果编码的字节长度不能被 4 整除,这是一个反序列化错误。因为向量是顶级的,我们不必担心编码它的长度,但是如果向量本身嵌入到一个更大的结构中,这可能是一个问题。例如,如果参数是 int32 的向量的向量,每个嵌套向量也需要在数据之前对其长度进行编码。
关于零值的一个注记
我们习惯于将数字 0 写成“0”或“0x00”,但仔细想想,我们并不需要 1 个字节来表示它,0 字节或“空字节数组”也一样可以表示数字 0。其实就像在0x0005
中,前导 0 字节是多余的,那么0x00
字节就像一个不必要的前导 0。
也就是说,这种格式总是将任何类型的零编码为空字节数组。
每种类型如何序列化
等宽数字
小数字可以存储在最多 64 位的变量中。
铁锈种类 : u8
、u16
、u32
、usize
、u64
、i8
、i16
、i32
、isize
、i64
。
Top-encoding :和所有数值类型一样,可以适合它们的二进制补码的最小字节数,大端表示。
嵌套编码:类型的固定宽度 big endian 编码,使用 2 的补码。
重要
关于类型usize
和isize
的说明:这些特定于 Rust 的类型具有底层架构的宽度,即在 32 位系统上是 32,在 64 位系统上是 64。然而,智能合约总是在 wasm32 架构上运行,所以这些类型总是分别与u32
和i32
相同。
即使在 64 位系统上模拟智能合约执行,它们仍然必须在 32 位上序列化。
例题
类型 | 数字 | 顶级编码 | 嵌套编码 |
---|---|---|---|
u8 |
0 |
0x |
0x00 |
u8 |
1 |
0x01 |
0x01 |
u8 |
0x11 |
0x11 |
0x11 |
u8 |
255 |
0xFF |
0xFF |
u16 |
0 |
0x |
0x0000 |
u16 |
0x11 |
0x11 |
0x0011 |
u16 |
0x1122 |
0x1122 |
0x1122 |
u32 |
0 |
0x |
0x00000000 |
u32 |
0x11 |
0x11 |
0x00000011 |
u32 |
0x1122 |
0x1122 |
0x00001122 |
u32 |
0x112233 |
0x112233 |
0x00112233 |
u32 |
0x11223344 |
0x11223344 |
0x11223344 |
u64 |
0 |
0x |
0x0000000000000000 |
u64 |
0x11 |
0x11 |
0x0000000000000011 |
u64 |
0x1122 |
0x1122 |
0x0000000000001122 |
u64 |
0x112233 |
0x112233 |
0x0000000000112233 |
u64 |
0x11223344 |
0x11223344 |
0x0000000011223344 |
u64 |
0x1122334455 |
0x1122334455 |
0x0000001122334455 |
u64 |
0x112233445566 |
0x112233445566 |
0x0000112233445566 |
u64 |
0x11223344556677 |
0x11223344556677 |
0x0011223344556677 |
u64 |
0x1122334455667788 |
0x1122334455667788 |
0x1122334455667788 |
usize |
0 |
0x |
0x00000000 |
usize |
0x11 |
0x11 |
0x00000011 |
usize |
0x1122 |
0x1122 |
0x00001122 |
usize |
0x112233 |
0x112233 |
0x00112233 |
usize |
0x11223344 |
0x11223344 |
0x11223344 |
i8 |
0 |
0x |
0x00 |
i8 |
1 |
0x01 |
0x01 |
i8 |
-1 |
0xFF |
0xFF |
i8 |
127 |
0x7F |
0x7F |
i8 |
-128 |
0x80 |
0x80 |
i16 |
-0x11 |
0xEF |
0xEF |
i16 |
-1 |
0xFF |
0xFFFF |
i16 |
-0x11 |
0xEF |
0xFFEF |
i16 |
-0x1122 |
0xEEDE |
0xEEDE |
i32 |
-1 |
0xFF |
0xFFFFFFFF |
i32 |
-0x11 |
0xEF |
0xFFFFFFEF |
i32 |
-0x1122 |
0xEEDE |
0xFFFFEEDE |
i32 |
-0x112233 |
0xEEDDCD |
0xFFEEDDCD |
i32 |
-0x11223344 |
0xEEDDCCBC |
0xEEDDCCBC |
i64 |
-1 |
0xFF |
0xFFFFFFFFFFFFFFFF |
i64 |
-0x11 |
0xEF |
0xFFFFFFFFFFFFFFEF |
i64 |
-0x1122 |
0xEEDE |
0xFFFFFFFFFFFFEEDE |
i64 |
-0x112233 |
0xEEDDCD |
0xFFFFFFFFFFEEDDCD |
i64 |
-0x11223344 |
0xEEDDCCBC |
0xFFFFFFFFEEDDCCBC |
i64 |
-0x1122334455 |
0xEEDDCCBBAB |
0xFFFFFFEEDDCCBBAB |
i64 |
-0x112233445566 |
0xEEDDCCBBAA9A |
0xFFFFEEDDCCBBAA9A |
i64 |
-0x11223344556677 |
0xEEDDCCBBAA9989 |
0xFFEEDDCCBBAA9989 |
i64 |
-0x1122334455667788 |
0xEEDDCCBBAA998878 |
0xEEDDCCBBAA998878 |
isize |
0 |
0x |
0x00000000 |
isize |
-1 |
0xFF |
0xFFFFFFFF |
isize |
-0x11 |
0xEF |
0xFFFFFFEF |
isize |
-0x1122 |
0xEEDE |
0xFFFFEEDE |
isize |
-0x112233 |
0xEEDDCD |
0xFFEEDDCD |
isize |
-0x11223344 |
0xEEDDCCBC |
0xEEDDCCBC |
任意宽度(大)的数字
对于大多数智能合约应用程序,需要大于最大 uint64 值的数字。例如,EGLD 余额表示为具有 18 位小数的定点十进制数。这意味着我们使用数字 10 18 来表示 1 个 EGLD,这已经超过了常规 64 位整数的容量。
铁锈种类 : BigUint
,BigInt
,
重要
这些类型由Elrond虚拟机管理,在许多情况下,合约看不到数据,只有一个句柄。
这是为了减轻智能合约的负担。
Top-encoding :和所有数值类型一样,可以适合它们的二进制补码的最小字节数,大端表示。
嵌套编码:由于这些类型是可变长度的,我们需要对它们的长度进行编码,以便解码者知道何时停止解码。编码数字的长度总是在前面,为 4 个字节(usize
/ u32
)。接下来我们编码:
- 对于
BigUint
大端字节 - 对于
BigInt
,可以明确表示该数的最短二进制补数。正数必须总是有最高有效位0
,而负数必须有最高有效位1
。参见下面的例子。
例题
类型 | 数字 | 顶级编码 | 嵌套编码 | 说明 |
---|---|---|---|---|
BigUint |
0 |
0x |
0x00000000 |
0 的长度被认为是0 。 |
BigUint |
1 |
0x01 |
0x0000000101 |
1 可以用 1 个字节来表示,所以长度为 1。 |
BigUint |
256 |
0x0100 |
0x000000020100 |
256 是占用 2 个字节的最小数字。 |
BigInt |
0 |
0x |
0x00000000 |
有符号的0 也表示为零长度字节。 |
BigInt |
1 |
0x01 |
0x0000000101 |
带符号的1 也表示为 1 个字节。 |
BigInt |
-1 |
0x01FF |
0x00000001FF |
如果FF``-1 的最短二进制补码表示。最高有效位是 1。 |
BigUint |
127 |
0x7F |
0x000000017F |
|
BigInt |
127 |
0x7F |
0x000000017F |
|
BigUint |
128 |
0x80 |
0x0000000180 |
|
BigInt |
128 |
0x0080 |
0x000000020080 |
这个数字的最高有效位是 1,所以为了避免歧义,需要在前面加上一个额外的0 字节。 |
BigInt |
255 |
0x00FF |
0x0000000200FF |
同上。 |
BigInt |
256 |
0x0100 |
0x000000020100 |
256 需要 2 个字节来表示,其中 MSB 为 0,不再需要前置一个0 字节。 |
布尔值被序列化为一个字节(u8
),它可以取值1
或0
。
铁锈类型 : bool
值
类型 | 价值 | 顶级编码 | 嵌套编码 |
---|---|---|---|
bool |
true |
0x01 |
0x01 |
bool |
false |
0x |
0x00 |
物品清单
这是一个总括术语,指各种项目类型的所有列表或数组。它们都以相同的方式序列化。
铁锈种类 : &[T]
、Vec<T>
、Box<[T]>
、LinkedList<T>
、VecMapper<T>
等。
顶层编码:项目的所有嵌套编码,串联。
嵌套编码:首先是列表的长度,编码在 4 个字节上(usize
/ u32
)。然后,将项目的所有嵌套编码串联起来。
例题
类型 | 价值 | 顶级编码 | 嵌套编码 | 说明 |
---|---|---|---|---|
Vec<u8> |
vec![1, 2] |
0x0102 |
0x00000002 0102 |
长度= 2 |
Vec<u16> |
vec![1, 2] |
0x00010002 |
0x00000002 00010002 |
长度= 2 |
Vec<u16> |
vec![] |
0x |
0x00000000 |
长度= 0 |
Vec<u32> |
vec![7] |
0x00000007 |
0x00000001 00000007 |
长度= 1 |
Vec< Vec<u32>> |
vec![ vec![7]] |
0x00000001 00000007 |
0x00000001 00000001 00000007 |
有一个元素,它是一个向量。在这两种情况下,内部 Vec 都需要嵌套编码在较大的 Vec 中。 |
Vec<&[u8]> |
vec![ &[7u8][..]] |
0x00000001 07 |
0x00000001 00000001 07 |
同上,但是内部列表是一个简单的字节列表。 |
Vec< BigUint> |
vec![ 7u32.into()] |
0x00000001 07 |
0x00000001 00000001 07 |
嵌套时需要对它们的长度进行编码。7 的编码方式与长度为 1 的字节列表相同,所以同上。 |
数组和元组
这些类型与上一节中的列表的唯一区别是它们的长度在编译时是已知的。因此,永远不需要对它们的长度进行编码。
铁锈种类 : [T; N]
、Box<[T; N]>
、(T1, T2, ... , TN)
。
顶层编码:项目的所有嵌套编码,串联。
嵌套编码:项目的所有嵌套编码,串联。
例题
类型 | 价值 | 顶级编码 | 嵌套编码 |
---|---|---|---|
[u8; 2] |
[1, 2] |
0x0102 |
0x0102 |
[u16; 2] |
[1, 2] |
0x00010002 |
0x00010002 |
(u8, u16, u32) |
[1u8, 2u16, 3u32] |
0x01000200000003 |
0x01000200000003 |
字节片和 ASCII 字符串
列表类型的一个特例,它们的行为遵循与项目列表相同的规则。
重要
从序列化的角度来看,字符串被视为一系列字节。使用 Unicode 字符串虽然通常是编程中的一个好习惯,但往往会给智能合约增加不必要的开销。区别在于 Unicode 字符串在输入和连接时得到验证。
我们认为最佳实践是在前端使用 Unicode,但在智能合约级别上保持所有消息和错误消息为 ASCII 格式。
铁锈种类 : BoxedBytes
、&[u8]
、Vec<u8>
、String
、&str
。
Top-encoding :字节片,原样。
嵌套编码:4 个字节上的字节片长度,后面是原样的字节片。
例题
类型 | 价值 | 顶级编码 | 嵌套编码 | 说明 |
---|---|---|---|---|
&'static [u8] |
b"abc" |
0x616263 |
0x00000003616263 |
ASCII 字符串是缓冲区的常规字节片。 |
BoxedBytes |
BoxedBytes::from( b"abc") |
0x616263 |
0x00000003616263 |
BoxedBytes 只是优化的拥有的不能增长的字节片。 |
Vec<u8> |
b"abc".to_vec() |
0x616263 |
0x00000003616263 |
使用Vec 作为可以增长的缓冲区。 |
&'static str |
"abc" |
0x616263 |
0x00000003616263 |
Unicode 字符串(切片)。 |
String |
"abc".to_string() |
0x616263 |
0x00000003616263 |
Unicode 字符串(自有)。 |
选项
一个Option
代表一个可选值:每个选项要么是Some
并且包含一个值,要么是None
,并且不包含值。
铁锈种类 : Option<T>
。
顶层编码:如果Some
,一个0x01
字节被编码,其后是编码值。如果None
,什么都不会被编码。
嵌套编码:如果Some
,一个0x01
字节被编码,其后是编码值。如果None
,一个0x00
字节被编码。
例题
类型 | 价值 | 顶级编码 | 嵌套编码 | 说明 |
---|---|---|---|---|
Option<u16> |
Some(5) |
0x010005 |
0x010005 |
|
Option<u16> |
Some(0) |
0x010000 |
0x010000 |
|
Option<u16> |
None |
0x |
0x00 |
请注意,对于任何类型,Some 的编码都不同于None |
Option< BigUint> |
Some( BigUint::from( 0x1234u32)) |
0x01 00000002 1234 |
0x01 00000002 1234 |
Some 值是嵌套编码的。对于一个BigUint ,这增加了长度,这里是2 。 |
自定义结构
在库的合约中定义的任何结构,如果用TopEncode
、TopDecode
、NestedEncode
、NestedDecode
中的一个或全部进行注释,都可以成为可序列化的。
示例实现:
#[derive(TopEncode, TopDecode, NestedEncode, NestedDecode)]
pub struct Struct {
pub int: u16,
pub seq: Vec<u8>,
pub another_byte: u8,
pub uint_32: u32,
pub uint_64: u64,
}
顶层编码:所有字段一个接一个嵌套编码。
嵌套编码:相同,所有字段一个接一个嵌套编码。
示例值
Struct {
int: 0x42,
seq: vec![0x1, 0x2, 0x3, 0x4, 0x5],
another_byte: 0x6,
uint_32: 0x12345,
uint_64: 0x123456789,
}
它将被编码(顶层编码和嵌套编码)为:0x004200000005010203040506000123450000000123456789
。
解释:
[
/* int */ 0, 0x42,
/* seq length */ 0, 0, 0, 5,
/* seq contents */ 1, 2, 3, 4, 5,
/* another_byte */ 6,
/* uint_32 */ 0x00, 0x01, 0x23, 0x45,
/* uint_64 */ 0x00, 0x00, 0x00, 0x01, 0x23, 0x45, 0x67, 0x89
]
自定义枚举
在库的合约中定义的任何枚举,如果用以下任意一个或全部进行注释,都可以成为可序列化的:TopEncode
、TopDecode
、NestedEncode
、NestedDecode
。
一个简单的枚举示例:
摘自 elrond 编解码器测试的示例。
#[derive(TopEncode, TopDecode, NestedEncode, NestedDecode)]
enum DayOfWeek {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
更复杂的枚举示例:
#[derive(TopEncode, TopDecode, NestedEncode, NestedDecode)]
enum EnumWithEverything {
Default,
Today(DayOfWeek),
Write(Vec<u8>, u16),
Struct {
int: u16,
seq: Vec<u8>,
another_byte: u8,
uint_32: u32,
uint_64: u64,
},
}
嵌套编码:首先对判别式进行编码。判别式是变量的索引,从0
开始。然后,该变量中的字段(如果有)被一个接一个地嵌套编码。
顶层编码:与嵌套编码相同,但是有一个额外的规则:如果判别式是0
(第一个变体)并且没有字段,则不编码任何东西。
示例值
以下示例摘自 elrond-codec 测试。
| 价值 | 顶部编码字节 | 嵌套编码字节 | |
DayOfWeek::Monday
|
/* nothing */
|
/* discriminant */ 0,
DayOfWeek::Tuesday
|
/* discriminant */ 1,
EnumWithEverything::Default
|
/* nothing */
|
/* discriminant */ 0,
EnumWithEverything::Today(
DayOfWeek::Monday
)
|
/* discriminant */ 1,
/* DayOfWeek discriminant */ 0
EnumWithEverything::Today(
DayOfWeek::Friday
)
|
/* discriminant */ 1,
/* DayOfWeek discriminant */ 4
EnumWithEverything::Write(
Vec::new(),
0,
)
|
/* discriminant */ 2,
/* vec length */ 0, 0, 0, 0,
/* u16 */ 0, 0,
EnumWithEverything::Write(
[1, 2, 3].to_vec(),
4
)
|
/* discriminant */ 2,
/* vec length */ 0, 0, 0, 3,
/* vec contents */ 1, 2, 3,
/* an extra 16 */ 0, 4,
EnumWithEverything::Struct {
int: 0x42,
seq: vec![0x1, 0x2, 0x3, 0x4, 0x5],
another_byte: 0x6,
uint_32: 0x12345,
uint_64: 0x123456789,
};
|
/* discriminant */ 3,
/* int */ 0, 0x42,
/* seq length */ 0, 0, 0, 5,
/* seq contents */ 1, 2, 3, 4, 5,
/* another_byte */ 6,
/* uint_32 */ 0x00, 0x01, 0x23, 0x45,
/* uint_64 */ 0x00, 0x00, 0x00, 0x01,
0x23, 0x45, 0x67, 0x89,
|**