1 个不稳定版本

0.7.2+serum.12021年1月15日

#2067编码


用于 serum-borsh-derive

Apache-2.0

31KB
710

borsh

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

网站 | 加入社区 | 实现 | 基准测试 | 规范

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

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

实现

平台 仓库 最新版本
Rust borsh-rs Latest released version
TypeScript, JavaScript borsh-js Latest released version
TypeScript borsh-ts Latest released version
Java, Kotlin, Scala, Clojure, 等 borshj
Go borsh-go Latest released version
Python borsh-construct-py Latest released version
Assemblyscript borsh-as Latest released version
C# Hexarc.Borsh Latest released version
C++ borsh-cpp (正在进行中)
C++20 borsh-cpp20 (正在进行中)
Elixir borsh-ex Latest released version

基准测试

我们对区块链项目最关心的对象进行了以下基准测试:区块、区块头部、交易、账户。我们采用了来自 NEAR 协议 区块链的对象结构。我们使用了 Criterion 来构建以下图表。

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

区块头部序列化速度与区块头部字节大小(大小仅大致对应序列化复杂性,导致图表不平滑)的关系

ser_header

区块头部反序列化速度与区块头部字节大小的关系

ser_header

区块序列化速度与区块字节大小的关系

ser_header

区块反序列化速度与区块字节大小的关系

ser_header

完整报告请见 此处

规范

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

一般原则

  • 整数采用小端序;
  • 动态容器的尺寸在值之前以 u32 的形式写入;
  • 所有无序容器(hashmap/hashset)按键的字典序排序(在值相同时以键的值作为解决冲突的依据);
  • 结构体按照结构体中字段的顺序进行序列化;
  • 枚举使用 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: "()" 我们不写入任何内容
布尔值 boolean_type: "bool" if x {
  repr(1 as u8)
} else {
  repr(0 as u8)
}
固定大小数组 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 fields repr(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)
HashMap hashmap: "HashMap<" ident0, ident1 ">" repr(x.len() as u32)
for (k, v) in x.sorted_by_key() {
  repr(k as ident0)
  repr(v as ident1)
}
HashSet hashset: "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()函数来表示我们将给定元素的表示写入一个假想的缓冲区。

依赖关系

~2MB
~43K SLoC