2 个版本

0.7.3 2020 年 10 月 31 日
0.7.2 2020 年 10 月 31 日

#1459编码

Apache-2.0

59KB
1K SLoC

borsh

用于哈希的二进制对象表示序列化器

npm npm Crates.io version Download Join the community on Discord Apache 2.0 License Travis Build

网站 | 示例 | 功能 | 基准测试 | 规范 | 发布

为什么我们需要另一种序列化格式?Borsh 是第一个优先考虑以下对安全关键项目至关重要的特性的序列化器

  • 一致的、明确的二进制表示
    • 一致性意味着对象和它们的二进制表示之间存在双射映射。没有两个二进制表示可以反序列化为同一个对象。这对于使用二进制表示来计算哈希的应用程序非常有用;
    • Borsh 随附一个完整的规范,可用于其他语言中的实现;
  • 安全。Borsh 实现使用安全的编码实践。在 Rust 中,Borsh 几乎只使用安全代码,只有一个例外是使用 unsafe 以避免耗尽攻击;
  • 速度。在 Rust 中,Borsh 通过放弃使用 Serde 而实现高性能,在某些情况下比 bincode 更快;这也减少了代码大小。

示例

use borsh::{BorshSerialize, BorshDeserialize};

#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug)]
struct A {
    x: u64,
    y: String,
}

#[test]
fn test_simple_struct() {
    let a = A {
        x: 3301,
        y: "liber primus".to_string(),
    };
    let encoded_a = a.try_to_vec().unwrap();
    let decoded_a = A::try_from_slice(&encoded_a).unwrap();
    assert_eq!(a, decoded_a);
}

功能

放弃使用 Serde 允许 borsh 拥有一些当前对于 serde 兼容序列化器不可用的功能。目前我们支持两个功能:borsh_initborsh_skip(前者在 Serde 中不可用)。

borsh_init 允许在反序列化后自动运行初始化函数。这对于设计为严格不可变的对象非常有用。使用示例

#[derive(BorshSerialize, BorshDeserialize)]
#[borsh_init(init)]
struct Message {
    message: String,
    timestamp: u64,
    public_key: CryptoKey,
    signature: CryptoSignature
    hash: CryptoHash
}

impl Message {
    pub fn init(&mut self) {
        self.hash = CryptoHash::new().write_string(self.message).write_u64(self.timestamp);
        self.signature.verify(self.hash, self.public_key);
    }
}

borsh_skip 允许跳过序列化/反序列化字段,假设它们实现了 Default 特性,类似于 #[serde(skip)]

#[derive(BorshSerialize, BorshDeserialize)]
struct A {
    x: u64,
    #[borsh_skip]
    y: f32,
}

基准测试

我们对区块链项目最关注的对象进行了以下基准测试:区块、区块头、交易、账户。我们从 nearprotocol 区块链中获取了对象结构。我们使用 Criterion 构建以下图表。

基准测试在 Google Cloud n1-standard-2 (2 vCPUs, 7.5 GB 内存) 上运行。

区块头序列化速度与区块头大小(以字节为单位)的对比(大小仅大致对应序列化复杂性,导致图形不光滑)

ser_header

区块头反序列化速度与区块头大小(以字节为单位)的对比

ser_header

区块序列化速度与区块大小(以字节为单位)的对比

ser_header

区块反序列化速度与区块大小(以字节为单位)的对比

ser_header

完整报告请见此处

规范

简而言之,Borsh 是一种非自描述的二进制序列化格式。它被设计成将任何对象序列化为一组规范且确定的字节。

基本原则

  • 整数是低位字节序;
  • 动态容器的大小(如向量、字符串)在值之前以 u32 的形式写入;
  • 所有无序容器(哈希表/集合)按键的字典顺序排序(在值相同的情况下按值排序);
  • 结构体按结构体中的字段顺序序列化;
  • 枚举使用 u8 为枚举序号,然后存储枚举值内部的数据(如果有的话)。

形式规范

非正式类型Rust EBNF * 伪代码
整数 integer_type: ["u8" | "u16" | "u32" | "u64" | "u128" | "i8" | "i16" | "i32" | "i64" | "i128" ] little_endian(x)
浮点数 float_type: ["f32" | "f64" ] err_if_nan(x)
little_endian(x as integer_type)
单元 unit_type: "()"我们不写入任何内容
固定大小数组 array_type: '[' ident ';' literal ']' for el in x
  repr(el as ident)
动态大小数组 vec_type: "Vec<" ident '>" repr(len() as u32)
for el in x
  repr(el as ident)
结构体 struct_type: "struct" ident fieldsrepr(fields)
字段 fields: [named_fields | unnamed_fields]
命名字段 named_fields: '{' ident_field0 ':' ident_type0 ',' ident_field1 ':' ident_type1 ',' ... '}' repr(ident_field0 as ident_type0)
repr(ident_field1 as ident_type1)
...
未命名字段 unnamed_fields: '(' ident_type0 ',' ident_type1 ',' ... ')'repr(x.0 as type0)
repr(x.1 as type1)
...
枚举 enum: 'enum' ident '{' variant0 ',' variant1 ',' ... '}'
variant: ident [ fields ] ?
假设 X 是枚举采用的变体的数量。
repr(X as u8)
repr(x.X as fieldsX)
HashMaphashmap: "HashMap<" ident0, ident1 ">"repr(x.len() as u32)
for (k, v) in x.sorted_by_key() {
  repr(k as ident0)
  repr(v as ident1)
}
HashSethashset: "HashSet<" ident ">"repr(x.len() as u32)
for el in x.sorted() {
 repr(el as ident)
}
Option option_type: "Option<" ident '>" if x.is_some() {
  repr(1 as u8)
  repr(x.unwrap() as ident)
} else {
  repr(0 as u8)
}
字符串 string_type: "String" encoded = utf8_encoding(x) as Vec<u8>
repr(encoded.len() as u32)
repr(encoded as Vec<u8>)

注意

  • 一些 Rust 语法部分尚未形式化,如枚举和变体。我们从 syn 类型 反向推导 Rust 语法的 EBNF 形式;
  • 我们不得不扩展EBNF的重复,而不是将它们定义为 [ ident_field ':' ident_type ',' ] *,而是将它们定义为 ident_field0 ':' ident_type0 ',' ident_field1 ':' ident_type1 ',' ...,这样我们就可以在伪代码中引用单个元素;
  • 我们使用 repr() 函数来表示我们将给定元素的表示写入一个假想的缓冲区。

发布

在您将更改合并到主分支并提高所有三个crate的版本后,是时候正式发布新版本了。

确保 borshborsh-deriveborsh-derive-internal 都有新的crate版本。然后导航到每个文件夹并按顺序运行(以下顺序)

cd ../borsh-derive-internal; cargo publish
cd ../borsh-derive; cargo publish
cd ../borsh; cargo publish

确保您在主分支上,然后标记代码并推送标签

git tag -a v9.9.9 -m "My superawesome change."
git push origin v9.9.9

依赖项

~1.5MB
~37K SLoC