以太坊源碼分析--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編碼

相關文章
相關標籤/搜索