区块链常用序列化分析

2020-05-27  本文已影响0人  幽客客

使用方法

  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<u8>,

}



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<u8>,

}

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 不能对有符号的数据编码解码

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

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

设计

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

数据格式

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

数据结构

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

使用方法

  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<u8>,

}

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<u8> = 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] }

所有上层类型的数据需要转成以上的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是一个对象编码规范。它是Proto3的一个子集,具有接口支持的扩展。其中Amino主要与Proto2兼容(但不与Proto2兼容)。

Amino目标

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]

img img * 为什么flatbuffers这么高效?

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

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

CKB 特点

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 实现的脚本,用于开发过程中方便向区块链发送测试数据。

上一篇下一篇

猜你喜欢

热点阅读