跳转至

Elrond序列化格式

原文:https://docs.elrond.com/developers/developer-reference/elrond-serialization-format

Elrond智能合约如何序列化参数、结果和存储

在Elrond,与智能合约交互的所有数据都有特定的序列化格式。序列化格式是任何项目的核心,因为进入和退出合约的所有值都表示为字节数组,需要根据一致的规范进行解释。

在 Rust 中,elrond-codeccrate(craterepodoc )专门处理这种格式。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 位的变量中。

铁锈种类 : u8u16u32usizeu64i8i16i32isizei64

Top-encoding :和所有数值类型一样,可以适合它们的二进制补码的最小字节数,大端表示。

嵌套编码:类型的固定宽度 big endian 编码,使用 2 的补码。

重要

关于类型usizeisize的说明:这些特定于 Rust 的类型具有底层架构的宽度,即在 32 位系统上是 32,在 64 位系统上是 64。然而,智能合约总是在 wasm32 架构上运行,所以这些类型总是分别与u32i32相同。

即使在 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 位整数的容量。

铁锈种类 : BigUintBigInt

重要

这些类型由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),它可以取值10

铁锈类型 : 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

自定义结构

在库的合约中定义的任何结构,如果用TopEncodeTopDecodeNestedEncodeNestedDecode中的一个或全部进行注释,都可以成为可序列化的。

示例实现:

#[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
] 

自定义枚举

在库的合约中定义的任何枚举,如果用以下任意一个或全部进行注释,都可以成为可序列化的:TopEncodeTopDecodeNestedEncodeNestedDecode

一个简单的枚举示例:

摘自 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, 

|**



回到顶部