区块链常用序列化分析

  • parity开源解析 parity-scale-codec

    用于Parity Substrate框架中的类型的SCALE(Simple Concatenated Aggregate Little-Endian)数据格式的Rust实现。

    SCALE是一种轻量级格式,允许编码(和解码),使其非常适合资源受限的执行环境,如区块链运行时和低功耗,低内存设备。

    它是一种极轻量级的编码格式,专为资源受限的执行上下文(如Substrate运行时)中的高性能,无副本编码和数据解码而设计。它 不以任何方式自我描述,并假设解码上下文具有关于编码数据的所有类型知识。

使用方法

  1. toml引用

[dependencies.codec]

default-features = false

features = ['derive']

package = 'parity-scale-codec'

version = '1.0.5'

  1. 需要对结构体声名宏:#[derive(Encode, Decode)]

  2. 序列化调用x.encode(),反序列化调用 x::decoce

代码使用示例:


use codec::{Encode, Decode};



#[derive(Debug, Encode, Decode)]

struct Test {

    a:u64,

    b:Vec,

}



let t = Test{a:1,b:vec![1,1,1,1,2,1,1,1,1,1,2,1,1,1]};

let ten = t.encode();

println!("{:?}",t.encode());

let tdec = Test::decode(&mut &ten[..]).unwrap();

println!("{:?}",tdec);

输出结果:

[1, 0, 0, 0, 0, 0, 0, 0, 56, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1]

Test { a: 1, b: [1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1] }

  1. 可以对结构体字段声明#[codec(compact)] 在序列化的时候对其使用压缩

代码使用示例:


#[derive(Debug, Encode, Decode)]

struct TestCompact {

    #[codec(compact)]

    a: u64,

    b: Vec,

}

let tc = TestCompact { a: 1, b: vec![1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1] };

let tcen = tc.encode();

println!("{:?}", tc.encode());

let tcdec = TestCompact::decode(&mut &tcen[..]).unwrap();

println!("{:?}", tcdec);

输出结果:

[4, 56, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1]

Test { a: 1, b: [1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1] }

对比输出结果可以看出,同样的u64值为1的字段,在压缩前占用 [1, 0, 0, 0, 0, 0, 0, 0] 而压缩后只占用 [4],所以在存储和传输的时候对字段启用压缩是很好的解决方案.

parity-scale-codec 不能对有符号的数据编码解码

  • Rust标准解析库 serde

Serde是一个有效和一般地对Rust数据结构进行序列化和反序列化的框架。

Serde生态系统由知道如何序列化和反序列化自身的数据结构以及知道如何序列化和反序列化其他事物的数据格式组成。Serde提供了这两个组相互交互的层,允许使用任何支持的数据格式序列化和反序列化任何支持的数据结构。

设计

许多其他语言依赖运行时反射来序列化数据,而Serde则建立在Rust强大的特征系统上。知道如何序列化和反序列化自身的数据结构是实现Serde SerializeDeserializetraits(或使用Serde的derive属性在编译时自动生成实现)的数据结构。这避免了反射或运行时类型信息的任何开销。事实上,在许多情况下,Rust编译器可以完全优化数据结构和数据格式之间的交互,使Serde序列化与手写序列化器执行相同的速度,以便选择特定的数据结构和数据格式。

数据格式

以下是社区为Serde实施的部分数据格式列表。

  • JSON,许多HTTP API使用的无处不在的JavaScript Object Notation。

  • Bincode,一种紧凑的二进制格式,用于Servo渲染引擎中的IPC。

  • CBOR,一种简洁的二进制对象表示,专为小消息大小而设计,无需进行版本协商。

  • YAML,一种流行的人性化配置语言,不是标记语言。

  • MessagePack,一种类似于紧凑型JSON的高效二进制格式。

  • TOML,Cargo使用的最小配置格式。

  • Pickle,Python世界中常见的一种格式。

  • RON,生锈的对象表示法。

  • BSON,MongoDB使用的数据存储和网络传输格式。

  • Avro,Apache Hadoop中使用的二进制格式,支持模式定义。

  • JSON5,JSON的超集,包括ES5的一些产品。

  • 明信片,一种no_std和嵌入式系统友好的紧凑二进制格式。

  • URL,x-www-form-urlencoded格式。

  • Envy,一种将环境变量反序列化为Rust结构的方法。 (仅反序列化)

  • Envy Store,一种将AWS参数存储参数反序列化为Rust结构的方法。(仅反序列化)

数据结构

开箱即用,Serde能够以上述任何格式序列化和反序列化常见的Rust数据类型。例如String&strusizeVecHashMap在所有支持。

使用方法

  1. toml引用

[dependencies]

    serde = "1.0.63"

    serde_derive = "1.0.27"

[dependencies.bincode]

features = ["i128"]

  1. 需要对结构体声名宏:#[derive(Serialize, Deserialize)]

  2. 序列化调用serialize(),反序列化调用 deserialize()

代码使用示例:


#[macro_use]

extern crate serde_derive;

extern crate bincode;

use bincode::{deserialize, serialize};

#[derive(Serialize, Deserialize, PartialEq, Debug)]

struct Test {

  a:u64,

  b:Vec,

}

fn main() {

  let t = Test{a:1,b:vec![1,1,1,1,2,1,1,1,1,1,2,1,1,1]};

  let encoded: Vec = serialize(&t).unwrap();

  println!("{:?}",encoded);

  let tdec :Test = deserialize( &encoded[..]).unwrap();

  println!("{:?}",tdec);

}

输出结果:

[1, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1]

Test { a: 1, b: [1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1] }

  • RLP编码和解码

    RLP(Recursive Length Prefix,递归的长度前缀)是一种编码规则,可用于编码任意嵌套的二进制数组数据。RLP编码的结果也是二进制序列。RLP主要用来序列化/反序列化数据。

    RLP编码的定义只处理以下2类底层数据:

    • 字符串(string)是指字节数组。例如,空串”“,再如单词”cat”,以及句子”Lorem ipsum dolor sit amet, consectetur adipisicing elit”等。

    • 列表(list)是一个可嵌套结构,里面可包含字符串和列表。例如,空列表[],再如一个包含两个字符串的列表[“cat”,”dog”],再比如嵌套列表的复杂列表[“cat”, [“puppy”, “cow”], “horse”, [[]], “pig”, [“”], “sheep”]。

所有上层类型的数据需要转成以上的2类数据,才能进行RLP编码。转换的规则RLP编码不统一规定,可以自定义转换规则。例如struct可以转成列表;int可以转成二进制序列(属于字符串这一类, 必须去掉首部0,必须用大端模式表示);map类型可以转换为由k和v组成的结构体、k按字典顺序排列的列表:[[k1,v1],[k2,v2]…] 等。

RLP编码规则

RLP编码的重点是给原始数据前面添加若干字节的前缀,而且这个前缀是和数据的长度相关的,并且是递归的;

RLP编码中的长度是数据的实际存储空间的字节大小,去掉首位0的正整数,用大端模式表示的二进制格式表示;RLP编码规定数据(字符串或列表)的长度的长度不得大于8字节。因为超过8字节后,一个字节的前缀就不能存储了。

  1. 如果字符串的长度是1个字节,并且它的值在[0x00, 0x7f] 范围之间,那么其RLP编码就是字符串本身。即前缀为空,用前缀代表字符串本身;否则,如果一个字符串的长度是0-55字节,其RLP编码是前缀跟上(拼接)字符串本身,前缀的值是0x80加上字符串的长度。由于在该规则下,字符串的最大长度是55,因此前缀的最大值是0x80+55=0xb7,所以在本规则下前缀(第一个字节)的取值范围是[0x80, 0xb7];
  1. 如果字符串的长度大于55个字节,其RLP编码是前缀跟上字符串的长度再跟上字符串本身。前缀的值是0xb7加上字符串长度的二进制形式的字节长度(即字符串长度的存储长度)。即用额外的空间存储字符串的长度,而前缀中只存字符串的长度的长度。例如一个长度是1024的字符串,字符串长度的二进制形式是\x04\x00,因此字符串长度的长度是2个字节,所以前缀应该是0xb7+2=0xb9,由此得到该字符串的RLP编码是\xb9\x04\x00再跟上字符串本身。因为字符串长度的长度最少需要1个字节存储,因此前缀的最小值是0xb7+1=0xb8;又由于长度的最大值是8个字节,因此前缀的最大值是0xb7+8=0xbf,因此在本规则下前缀的取值范围是[0xb8, 0xbf];
  1. 由于列表的任意嵌套的,因此列表的编码是递归的,先编码最里层列表,再逐步往外层列表编码。如果一个列表的总长度(payload,列表的所有项经过编码后拼接在一起的字节大小)是0-55字节,其RLP编码是前缀依次跟上列表中各项的RLP编码。前缀的值是0xc0加上列表的总长度。在本规则下前缀的取值范围是[0xc0, 0xf7]。本规则与规则2类似;如果一个列表的总长度大于55字节,它的RLP编码是前缀跟上列表的长度再依次跟上列表中各元素项的RLP编码。前缀的值是0xf7加上列表总长度的长度。编码的第一个字节的取值范围是[0xf8, 0xff]。本规则与规则3类似;

RLP解码规则

根据RLP编码规则和过程,RLP解码的输入一律视为二进制字符数组,其过程如下:

根据输入首字节数据,解码数据类型、实际数据长度和位置;根据类型和实际数据,解码不同类型的数据;继续解码剩余的数据;

总结

与其他序列化方法相比,RLP编码的优点在于使用了灵活的长度前缀来表示数据的实际长度,并且使用递归的方式能编码相当大的数据。

当接收或者解码经过RLP编码后的数据时,根据第1个字节就能推断数据的类型、大概长度和数据本身等信息。而其他的序列化方法, 不能根据第1个字节获得如此多的信息量。

代码使用示例:


type Tmp struct {

A uint64

B string

}

func TestRlp(t *testing.T){

var tt Tmp

tt.A = 1

tt.B= "ssssssssssss"

c,_ := rlp.EncodeToBytes(tt)

fmt.Println(c)

}

输出结果:

[206 1 140 115 115 115 115 115 115 115 115 115 115 115 115]

  • Amino编码规则

Amino是一个对象编码规范。它是Proto3的一个子集,具有接口支持的扩展。其中Amino主要与Proto2兼容(但不与Proto2兼容)。

Amino目标

  • 通过支持接口将奇偶校验引入逻辑对象和持久对象。

  • 具有唯一/确定性的价值编码。

  • 二进制字节必须可以使用模式进行解码。

  • 架构必须可升级。

  • 必须在没有模式的情况下解析足够的结构。

  • 编码器和解码器逻辑必须相当简单。

  • 序列化必须相当紧凑。

  • 必须保持足够兼容的JSON格式(但不能与JSON进行一般转换)

Amino vs JSON

JavaScript Object Notation(JSON)是人类可读的,结构良好的,非常适合与Javascript的互操作性,但效率很低。Protobuf3,BER,RLP都存在这些问题,因为我们需要更紧凑和有效的二进制编码标准。Amino为复杂对象(例如嵌入对象)提供了高效的二进制编码,这些对象可以自然地与您最喜爱的现代编程语 此外,Amino还具有完全兼容的JSON编码。


type Tmp struct {

A uint64

B string

}

func TestTmp(t *testing.T) {

tt := Tmp{

A: 1,

B: "ssssssssssss",

}

cdc := amino.NewCodec()

c, _ := cdc.MarshalBinaryLengthPrefixed(tt)

fmt.Println(c)

}

输出结果:

[16 8 1 18 12 115 115 115 115 115 115 115 115 115 115 115 115]

  • Nervos 序列化格式 flatbuffer

    flatbuffer是google的一个跨平台串行化库,开发这个最初是用在游戏项目中,github项目地址flatbuffersFlatBuffer提供了详细的使用文档,可以参考Google.github.io主页上的教程。

    flatbuffer vs JSON

    对于Json我们使用了这么长时间,目前几乎所有的数据传输格式都是Json,我们都知道Json是当今全服务平台的轻量级数据传输格式,Json量级轻,并且可读性强,使用友好却不过时,Json是语言独立的数据格式,但Json转换的时候却耗费较多的时间和内存,Facebook尝试过将所有的App中Json传输都换成使用Flatbuffers,而最终的效果可以参考一下这篇文章Improving Facebook's performance on Android with FlatBuffers,看起来这个确实很有前途。正如facebook展示的那样,遵循Android快速响应界面的16ms原则。

    如果你想把项目中所有的Json都替换为flatbuffers,首先要确认项目中真的需要这个,很多时候对性能的影响是潜移默化的,相比而言数据安全更加重要。

    下面介绍三个数据序列化的候选方案:

    Protocal Buffers:强大,灵活,但是对内存的消耗会比较大,并不是移动终端上的最佳选择。

    Nano-Proto-Buffers:基于Protocal,为移动终端做了特殊的优化,代码执行效率更高,内存使用效率更佳。

    FlatBuffers:这个开源库最开始是由Google研发的,专注于提供更优秀的性能。

    上面这些方案在性能方面的数据对比如下图所示:

img

img
* 为什么flatbuffers这么高效?

1.序列化数据访问不经过转换,即使用了分层数据。这样我们就不需要初始化解析器(没有复杂的字段映射)并且转换这些数据仍然需要时间。

2.flatbuffers不需要申请更多的空间,不需要分配额外的对象。

CKB 特点

  • 使用 Rust 作为主要开发语言 通过不同的功能模块实现一个紧凑,完整的区块链底层系统 基于 Cell 模型来扩展 UTXO

  • 模型并支持存储通用状态 基于 Cell 模型和 CKB-VM 实现智能合约,合约计算验证分离,计算发生在链下,验证则在链上

  • 支持智能合约的代码逻辑和状态分离 CKB-VM 使用 RISC-V 指令集,支持使用如 Ruby,Python 等高级语言来编写智能合约

  • 使用 Flatbuffer 实现数据结构的序列化,无需解析直接访问序列化数据以及基于 Zero-copy 的高效内存使用效率

  • 系统内部基于消息和 Channel 实现模块通讯机制,在高性能,以及底层存储访问和索引访问使用共享内存加锁实现 安全的 P2P

  • 网络实现,具有更好的网络安全性,对 DoS 和日蚀攻击等有更好的抵抗性 更高效的节点间发现,同步协议

CKB P2P Network 示意图

[图片上传失败...(image-e0b751-1590562140903)]

CKB 主要模块

SRC 模块

存储了 Main 函数,是整个项目的编译入口模块。

CORE 模块

用于保存 CKB 的核心数据结构的定义,包括 Block,Cell,Transaction 等核心数据结构。

SPEC 模块

链的共识配置,该配置会写入创世块。不同配置的节点直接无法通信。

SHARED 模块

用于保存各个模块公用的逻辑和代码。

DB 模块

封装了底层的数据持久化层,CKB 底层存储使用的是 KV 数据存储,对应的实现有两种,一种是基于 RocksDB 的实现,利用 RocksDB 将数据持久化到磁盘。另外一种实现是基于内存的模拟持久化实现,主要用于测试和开发等场景。

CHAIN 模块

实现了区块链数据结构。使用 DB 模块进行持久化。Chain 主要指责是记录和更新本地累计工作量最高的链,并维护链上数据的索引。在更新链时需要进行验证,并同时更新索引。

POOL 模块

Pool 模块的主要功能是实现交易池,CKB 的 Pool 的特点是根据网络状况动态调整出块时间,这样会更合理的利用网络资源和带宽。交易池的设计和实现的最大挑战是要同时兼顾多个目标并取得平衡。包括考虑交易的排序,依赖关系,以及整体性能,尤其是降低节点之间需要同步的数据并且合理的使用缓存。

PROTOCOL 模块

用于存放节点间消息的结构定义,以及消息体的 Builder。消息使用 Flatbuffers 序列化。

NETWORK 模块

点对的通讯的底层实现相关代码,对 Rust-libp2p 进行了封装,并将一些基础协议通过服务的方式暴露出来。通过对 Libp2p 的抽象实现未来可定制的 Libp2p 更加方便。

SYNC 模块

实现了 CKB 的网络同步协议,包括两个协议,分别是 Synchronizer 协议和 Relayer 协议。Synchronizer 协议的工作方式是 Head-first,更高效的利用网络带宽提升区块下载效率,用于节点之间高速下载区块数据。Relayer 协议是节点之间用来处理广播和转发新的交易。Sync 模块在 Bitcoin 的 Head-first 同步,Compact Block 等协议的基础上,结合了交易提交区,叔伯快统计等功能。

CKB-VM 模块

CKB-VM 是一个独立的实现。从实现角度,CKB-VM 是 RISC-V 的硬件 CPU 指令集的实现,所有的实现完全遵循 RISC-V 的标准。所以可以将 CKB-VM 当作一个 General Sandbox Runtime Module,CKB-VM 在项目中的作用是验证状态和执行智能合约,让整个系统的计算层保持了最大限度的灵活性,如通过更新 Cell 中存储的系统合约可以实现添加新的加密算法或其他功能等,并且 CKB-VM 的 Sandbox 的隔离性设计为执行合约提供了强大的运行安全保障。

MINER 模块

通过可插拔实现了不同的共识算法替换,目前为了开发方便,实现了 CPU 和 Cuckoo 两套内置共识算法,并且可方便增加外部实现的共识算法,如 ProgPow 算法。

NOTIFY 模块

是一套用于内部模块之间消息通讯的 Pub/Sub 模块。

DEVTOOLS 模块

包含用 Ruby 实现的脚本,用于开发过程中方便向区块链发送测试数据。

  • **GRIN 序列化格式 **rustc-serialize

    NOTE: This crate is deprecated in favor of serde. No new feature development will happen in this crate, although bug fixes proposed through PRs will still be merged. It is very highly recommended by the Rust Library Team that you use serde, not this crate.

你可能感兴趣的:(区块链常用序列化分析)