以太坊源码分析--RLP编码

RLP(Recursive Length Prefix),递归长度前缀编码,它是以太坊序列化所采用的序列化和反序列化的主要方式。区块、交易等数据结构在 网络传输和持久化时会先通过RLP编码后再存储到数据库中。rlp适用于任意的二进制数据数组的编码,在以太坊中,rpl接受的数据分为两类:1.字节数组 2.类list数据结构。html

以太坊中rlp的具体定义和规则咱们能够在黄皮书中找到(Appendix B. Recursive Length Prefix):数据库

  • 序列化定义

1532341583673

* **O** 全部byte的集合
* **B** 全部可能字节数组
* **L** 不仅单一节点的树形结构(好比结构体或者树节点分支节点,非叶子节点)
* **T** 全部字节数组的树形结构组合
  • 序列化处理

1532341606589
经过两个子函数定义RLP分别处理上面说的两种数据类型数组

  • Rb(x)字节数组序列化处理规则
    1532341627515缓存

    • 若是字节数组只包含一个字节(对于 [0x00, 0x7f] 范围内的单个字节),并且这个字节的大小小于128,那么不对数据进行处理,处理结果就是原数据,好比:a的编码是97
    • 若是字节数组的长度小于56,那么处理结果就等于在原始数据前面加上(128+字节数据的长度)的前缀,好比abc编码结果是131 97 98 99,其中131=128+len("abc"),97 98 99依次是a b c
    • 若是不是上面两种状况,那么处理结果就等于在原始数据前面加上原始数据长度的大端表示,而后在前面加上(183 + 原始数据大端表示的长度),好比编码下面这段字符串The length of this sentence is more than 55 bytes, I know it because I pre-designed it编码结果以下184 86 84 104 101 32 108 101 110 103 116 104 32 111 102 32 116 104 105 115 32 115 101 110 116 101 110 99 101 32 105 115 32 109 111 114 101 32 116 104 97 110 32 53 53 32 98 121 116 101 115 44 32 73 32 107 110 111 119 32 105 116 32 98 101 99 97 117 115 101 32 73 32 112 114 101 45 100 101 115 105 103 110 101 100 32 105 116,其中前三个字节的计算方式以下:1. 184 = 183 + 1,由于数组长度86编码后仅占用一个字节; 2. 86即数组长度86
关于**大端小端**的理解能够参考[《理解字节序》](http://www.ruanyifeng.com/blog/2016/11/byte-order.html)比较浅显易懂。关于公式中的一些数学符号的解释:
* ||x|| 表明了求x的长度
* (a).(b,c).(d,e) = (a,b,c,d,e) 表明了concat的操做,也就是字符串的相加操做。 "hello "+"world" = "hello world"
* BE(x)函数表示**去掉了前导0的大端模式**。 好比4个字节的整形0x1234用大端模式来表示是 00 00 12 34 那么用BE函数处理以后返回的实际上是 12 34. 开头的多余的00被去掉了。
* ^ 符号表明而且的含义。
* ≡ ,恒等于
  • Rl(x) 其余类型(树型结构)数据序列化处理规则

1532341641762

* 若是链接后的字节长度小于56, 那么在链接后的结果前面加上(192 + 链接后的长度),组成最终的结果。**好比:["abc", "def"]的编码结果是200 131 97 98 99 131 100 101 102。其中abc的编码为131 97 98 99,def的编码为131 100 101 102。两个子字符串的编码后总长度是8,所以编码结果第一位计算得出:192 + 8 = 200**。
* 若是链接后的字节长度大于等于56, 那么就在链接后的结果前面先加上链接后的长度的大端模式,而后在前面加上(247 + 链接后长度的大端模式的长度)**好比:`["The length of this sentence is more than 55 bytes, ", "I know it because I pre-designed it"]`的编码结果是:`248 88 179 84 104 101 32 108 101 110 103 116 104 32 111 102 32 116 104 105 115 32 115 101 110 116 101 110 99 101 32 105 115 32 109 111 114 101 32 116 104 97 110 32 53 53 32 98 121 116 101 115 44 32 163 73 32 107 110 111 119 32 105 116 32 98 101 99 97 117 115 101 32 73 32 112 114 101 45 100 101 115 105 103 110 101 100 32 105 116`,其中前两个字节的计算方式以下:1. 248 = 247 +1; 2. 88 = 86 + 2,在`Rb(x)3`示例中,长度为86,而在此例中,因为有两个子字符串,每一个子字符串自己的长度的编码各占1字节,所以总共占2字节。第3个字节179依据`Rb(x)规则2`得出179 = 128 + 51 第55个字节163一样`Rb(x)2`得出163 = 128 + 35**

上面是一个递归的定义, 在求取s(x)的过程当中又调用了RLP方法,这样使得RLP可以处理递归的数据结构。经过一个复杂的例子来理解一下递归长度前缀:
`["abc",["The length of this sentence is more than 55 bytes, ", "I know it because I pre-designed it"]]`
编码后的结果:
`248 94 131 97 98 99 248 88 179 84 104 101 32 108 101 110 103 116 104 32 111 102 32 116 104 105 115 32 115 101 110 116 101 110 99 101 32 105 115 32 109 111 114 101 32 116 104 97 110 32 53 53 32 98 121 116 101 115 44 32 163 73 32 107 110 111 119 32 105 116 32 98 101 99 97 117 115 101 32 73 32 112 114 101 45 100 101 115 105 103 110 101 100 32 105 116`
列表第一项字符串abc依据`Rb(x)规则2`,编码结果为131 97 98 99,长度为4。
列表第二项也是一个列表项:
`["The length of this sentence is more than 55 bytes, ", "I know it because I pre-designed it"]`
根据`Rl(x)规则2`,结果为
`248 88 179 84 104 101 32 108 101 110 103 116 104 32 111 102 32 116 104 105 115 32 115 101 110 116 101 110 99 101 32 105 115 32 109 111 114 101 32 116 104 97 110 32 53 53 32 98 121 116 101 115 44 32 163 73 32 107 110 111 119 32 105 116 32 98 101 99 97 117 115 101 32 73 32 112 114 101 45 100 101 115 105 103 110 101 100 32 105 116`
长度为90,所以,整个列表的编码结果第二位是90 + 4 = 94, 占用1个字节,第一位247 + 1 = 248
  • 标量数据处理
    1532341655523

使用RLP处理标量数据,也就是基本的数据,RLP只可以用来处理正整数。 RLP只能处理大端模式处理后的整数。 也就是说若是是一个整数x,那么先使用BE(x)函数来把x转换成最简大端模式(去掉了开头的00),而后把BE(x)的结果当成是字节数组来进行编码。网络

上面就是RLP的编码定义,下面开始咱们来看一下以太坊中的实现源码。数据结构

代码文件结构

rlp
├── decode.go               // 解码器
├── decode_tail_test.go     // 解码示例
├── decode_test.go          // 解码器测试用例
├── doc.go                  // 文档代码
├── encode.go               // 编码器
├── encode_test.go          // 编码器测试用例
├── encoder_example_test.go // 编码器示例
├── raw.go                  // 处理编码后rlp数据,好比计算长度、分离、值计数等
├── raw_test.go             // raw测试用例
└── typecache.go            // 类型缓存,记录数据类型->编码器|解码器的映射

能够直接从示例 encoder_example_test.go 中来看,这个示例中实现了一个如何经过rlp编码struct的调用:多线程

type MyCoolType struct {
    Name string
    a, b uint
}

func (x *MyCoolType) EncodeRLP(w io.Writer) (err error) {
    if x == nil {
    // 结构体为空指针,编码{0, 0}
        err = Encode(w, []uint{0, 0})
    } else {
    // 编码指定的值
        err = Encode(w, []uint{x.a, x.b})
    }
    return err
}

func ExampleEncoder() {
    var t *MyCoolType 
    bytes, _ := EncodeToBytes(t)   // t MyCoolType为nil编码为字节数组
    fmt.Printf("%v → %X\n", t, bytes)

    t = &MyCoolType{Name: "foobar", a: 5, b: 6}
    bytes, _ = EncodeToBytes(t)    // t 为struct
    fmt.Printf("%v → %X\n", t, bytes)

    // Output:
    // <nil> → C28080
    // &{foobar 5 6} → C20506
}

经过上面的测试用例代码,能够看到EncodeToBytes就是编码的函数,往下走,看一下编码器具体实现 encode.go并发

var (
    EmptyString = []byte{0x80}
    EmptyList   = []byte{0xC0}
)

// Encoder is implemented by types that require custom
// encoding rules or want to encode private fields.
type Encoder interface {
    // EncodeRLP should write the RLP encoding of its receiver to w.
    // If the implementation is a pointer method, it may also be
    // called for nil pointers.
    //
    // Implementations should generate valid RLP. The data written is
    // not verified at the moment, but a future version might. It is
    // recommended to write only a single value but writing multiple
    // values or no value at all is also permitted.
    EncodeRLP(io.Writer) error
}

首先定义了空字符串和空列表的值,定义了Encoder接口,咱们能够看到上面的MyCoolType就实现了该接口的EncodeRLP方法,继续往下看EncodeToBytes的具体实现:函数

// EncodeToBytes 返回RLP编码后的值.
func EncodeToBytes(val interface{}) ([]byte, error) {
    eb := encbufPool.Get().(*encbuf)   // 从encbufPool池中获取encbuf实例
    defer encbufPool.Put(eb)           // 调用结束之后从新放入池中
    eb.reset()                         // 初始化encbuf
    if err := eb.encode(val); err != nil { // 对数据编码
        return nil, err
    }
    return eb.toBytes(), nil   //将编码后的数据和头部拼接成byte[]后返回
}

encbufPool是一个sync.Pool,能够经过一个资源池来提升编码效率,减小资源浪费。来看下encbuf的结构定义:源码分析

type encbuf struct {
    str     []byte      // 包含了除了列表的头部的全部的编码的内容
    lheads  []*listhead // 全部的列表头
    lhsize  int         // lheads的长度
    sizebuf []byte      // 9个字节大小的辅助buffer,用来处理uint的编码
}

type listhead struct {
    offset int // 记录了列表数据在str字段的起始位置
    size   int // 编码数据的总长度 (包括列表头)
}

encbuf看起来是在编码过程的一个buffer的做用,其定义了一些encode过程当中的操做方法,具体每一个函数的实现不作代码分析了,这里大略说一下每一个函数的做用:

  • encode(val interface{}) error 编码函数
  • (w *encbuf) encodeString(b []byte) 将编码后的原始数据链接到已编码内容以后
  • encodeStringHeader(size int) 将头部结构体中新编码后的元素和以前已经编码的内容链接
  • list() *listhead 保存每一个元素编码后的头部lheads信息
  • listEnd(lh *listhead) 编码链接后的长度lhsize统计
  • reset() encbuf 初始化
  • size() int 计算编码后内容和头部长度之和
  • toBytes() []byte 将每一个头部lheads链接到对应的编码数据后
  • toWriter(out io.Writer) (err error) 经过io流将编码后头部写到编码后,
  • Write(b []byte) (int, error) 实现io.Writer接口,以便于能够传入EncodeRLP

继续上面的EncodeToBytes函数,我来看一下eb.encode(val)编码函数具体作了什么:

func (w *encbuf) encode(val interface{}) error {
    rval := reflect.ValueOf(val)
    ti, err := cachedTypeInfo(rval.Type(), tags{})
    if err != nil {
        return err
    }
    return ti.writer(rval, w)
}

首先经过reflect反射机制获取编码值的类型,后和tags一块儿传入cachedTypeInfo,往下看cachedTypeInfo作了什么typecache.go

var (
    typeCacheMutex sync.RWMutex    // 读写锁
    typeCache      = make(map[typekey]*typeinfo)// 类型->编码|解码函数的映射,不一样的数据类型对应不一样的编码和解码方法
)

type typeinfo struct {
    decoder    // 解码
    writer     // 编码
}

type tags struct {
    nilOK bool     // 是否为空值
    tail bool      // 该字段是否含其余列表元素。它只能设置为最后一个字段,该字段必须是切片类型。
    ignored bool   // 是否忽略
}

type typekey struct {
    reflect.Type   // 数据类型
    tags   // 根据tags可能会生成不一样的解码器。
}

type decoder func(*Stream, reflect.Value) error

type writer func(reflect.Value, *encbuf) error

func cachedTypeInfo(typ reflect.Type, tags tags) (*typeinfo, error) {
    typeCacheMutex.RLock() // 加读锁
    info := typeCache[typekey{typ, tags}]  // 从缓存中获取编码解码器
    typeCacheMutex.RUnlock()
    if info != nil {
        return info, nil
    }
    // 缓存中没有时经过type和tags生成编码解码器
    typeCacheMutex.Lock()
    defer typeCacheMutex.Unlock()
    return cachedTypeInfo1(typ, tags)
}

cachedTypeInfo函数主要是从缓存中来根据数据类型来获取编码解码器,若是不存在时就经过cachedTypeInfo1 来建立一个对应类型的编码解码器,这里须要注意的typeCacheMutex 进程锁来避免多线程资源保护和互斥,下面继续看下cachedTypeInfo1如何根据typekey来建立typeinfo的:

func cachedTypeInfo1(typ reflect.Type, tags tags) (*typeinfo, error) {
    key := typekey{typ, tags}
    info := typeCache[key]
    if info != nil {
        // 另一个协程首先得到锁,再次验证避免多进程并发请求
        return info, nil
    }

    typeCache[key] = new(typeinfo)
    info, err := genTypeInfo(typ, tags)
    if err != nil {
        // 生成失败清除空间
        delete(typeCache, key)
        return nil, err
    }
    *typeCache[key] = *info
    return typeCache[key], err
}

该函数并非实际建立缓存的函数,其中调用了genTypeInfo函数,顺着往下看:

func genTypeInfo(typ reflect.Type, tags tags) (info *typeinfo, err error) {
    info = new(typeinfo)
    if info.decoder, err = makeDecoder(typ, tags); err != nil {
        return nil, err
    }
    if info.writer, err = makeWriter(typ, tags); err != nil {
        return nil, err
    }
    return info, nil
}

显而易见,这里分别调用makeDecodermakeWriter来建立解码和编码,咱们来看下编码器的实现encode.go:

// 经过类型和tags建立对应的具体编码函数
func makeWriter(typ reflect.Type, ts tags) (writer, error) {
    kind := typ.Kind()
    switch {
    case typ == rawValueType:
        return writeRawValue, nil
    case typ.Implements(encoderInterface):
        return writeEncoder, nil
    case kind != reflect.Ptr && reflect.PtrTo(typ).Implements(encoderInterface):
        return writeEncoderNoPtr, nil
    case kind == reflect.Interface:
        return writeInterface, nil
    case typ.AssignableTo(reflect.PtrTo(bigInt)):
        return writeBigIntPtr, nil
    case typ.AssignableTo(bigInt):
        return writeBigIntNoPtr, nil
    case isUint(kind):
        return writeUint, nil
    case kind == reflect.Bool:
        return writeBool, nil
    case kind == reflect.String:
        return writeString, nil
    case kind == reflect.Slice && isByte(typ.Elem()):
        return writeBytes, nil
    case kind == reflect.Array && isByte(typ.Elem()):
        return writeByteArray, nil
    case kind == reflect.Slice || kind == reflect.Array:
        return makeSliceWriter(typ, ts)
    case kind == reflect.Struct:
        return makeStructWriter(typ)
    case kind == reflect.Ptr:
        return makePtrWriter(typ)
    default:
        return nil, fmt.Errorf("rlp: type %v is not RLP-serializable", typ)
    }
}

这个函数很简单,经过type来设置对应的具体编码器,注意一下ts也就是tags参数,只有类型为slice和array时候才会用到,具体每一个类型对应的编码器实现就不一一分析了,很值得每个源码看一下,加深一下对上面黄皮书定义的规则的理解。对于结构体的编码在黄皮书中并无公式定义,咱们来看下源码了解一下:

type field struct {
    index int
    info  *typeinfo
}

func makeStructWriter(typ reflect.Type) (writer, error) {
    fields, err := structFields(typ) // 经过typ.NumField反射机制来解析struct结构,并获取每一个字段的解码器
    if err != nil {
        return nil, err
    }
    writer := func(val reflect.Value, w *encbuf) error {
        lh := w.list()
        for _, f := range fields {
          //f是field结构, f.info是typeinfo的指针, 因此f.info.writer就是调用字段的编码器方法
            if err := f.info.writer(val.Field(f.index), w); err != nil {
                return err
            }
        }
        w.listEnd(lh)
        return nil
    }
    return writer, nil
}

structFields 方法中经过reflect.Type的NumField获取其Field,而后调用cachedTypeInfo1来获取每一个Field的编码器,因此这是一个递归的调用。最后遍历Field的数组fields,调用f.info.writer具体的编码器来编码。
咱们继续回到encoder_example_test.go来看下示例中

var t *MyCoolType 
bytes, _ := EncodeToBytes(t)

这里的t是MyCoolType的一个指针,MyCoolType实现了EncodeRLP的接口,根据makeWriter中的case找到typ.Implements(encoderInterface)分支,调用了writeEncoder:

func writeEncoder(val reflect.Value, w *encbuf) error {
    return val.Interface().(Encoder).EncodeRLP(w)
}

这里直接调用了EncodeRLP(w)接口方法,从上面的示例代码中看,那么就是调用Encode(w, []uint{0, 0}),咱们上面分析过了,encbuf实现了io.Writer接口的Writer方法,这里的w就是encbuf:

func Encode(w io.Writer, val interface{}) error {
    if outer, ok := w.(*encbuf); ok {
       // 若是是*encbuf类型,则直接返回outer.encode
            return outer.encode(val)
    }
    
    eb := encbufPool.Get().(*encbuf)
    defer encbufPool.Put(eb)
    eb.reset()
    if err := eb.encode(val); err != nil {
        return err
    }
    w是io.Writer,eb.toWriter(w)流
    return eb.toWriter(w)
}

这样一个rlp的编码过程就解析完了,解码就是一个逆向的过程,不继续分析了,要理解黄皮书中的规则,仍是须要看下makeWriter中的每个类型对应的编码的具体实现代码。

转载请注明: 转载自Ryan是菜鸟 | LNMP技术栈笔记

若是以为本篇文章对您十分有益,何不 打赏一下

谢谢打赏

本文连接地址: 以太坊源码分析--RLP编码

相关文章
相关标签/搜索