以太坊源码研究之RLP编码

这是以太坊源码研究的第一篇文章。基本上来说,我写什么内容,说明我正好在学习什么内容,并没有固定的顺序。之所以先写RLP编码,是因为在一开始研究以太坊交易结构的时候,就遇上RLP编码了,所谓拣上不如撞上,就从它开始吧。

RLP(Recursive Length Prefix)是以太坊中广泛运用的一种编码方法,用于序列化以太坊中各类对象。RLP可以把任意嵌套的二进制数据数组,都编码成为一个“扁平”的无嵌套的byte数组。

任意嵌套的二进制数据数组,可能是一个空字符串“”,可能是一个整数比如36,也可能是一个非常复杂的数据结构,比如["cat",["puppy","cow"],"horse",[[]],"pig",[""],"sheep"]。对于这些千变万化的数据,RLP到底是怎么进行编码的呢?其主要规则如下:

  1. 如果是单个字节,并且值在[0x00, 0x7f]范围内,则RLP编码就是它自己。这个好理解,不用解释。

  2. 如果是长度不超过55的字符串,那么RLP编码的第一个字节用来指定字符串长度,其值为0x80加上字符串长度,之后再跟整个字符串。因此,第一个字节值范围从0x80到0xb7,空字符串时为0x80,长度55的字符串为0x80+0x37=0xb7。
    例:“dog"编码后为4个字节:[0x83, 'd', 'o', 'g']

  3. 如果是长度超过55的字符串,这时字符串的长度可能会很长,甚至超过一个字节。RLP规定这种情况下,从第二个字节开始存储字符串的长度,将长度所花的字节数再加上0xb7的和作为第一个字节,字符串长度之后再跟字符串的内容。
    例:"Lorem ipsum dolor sit amet, consectetur adipisicing elit"字符串一共56个字节,编码结果为58字节:[ 0xb8, 0x38, 'L', 'o', 'r', 'e', 'm', ' ', ... , 'e', 'l', 'i', 't' ]。其中,0x38即56为字符串长度,0xb8为0xb7+1,1就是0x38这个长度本身的长度也就是1个字节。

  4. 如果是长度不超过55的列表,那么RLP编码的第一个字节是0xc0加上列表长度,后跟列表内容。因此,第一个字节值范围从0xc0到0xf7。
    例: [ "cat", "dog" ]列表,编码后为[ 0xc8, 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g' ]。其中0xc8为0xc0+8,8就是两个字符串的总长度。

  5. 如果是长度超过55的列表,那么RLP编码的第一个字节是0xf7加上列表长度的字节长度,后跟列表总长度,再跟列表项目内容的串联。因此,第一个字节的范围[0xf8, 0xff]。

再举个稍微复杂的例子:[ [ ], [ [ ] ], [ [ ], [ [ ] ] ] ] 的编码结果是 [ 0xc7, 0xc0, 0xc1, 0xc0, 0xc3, 0xc0, 0xc1, 0xc0 ]。

编码过程用python语言来描述是下面这样的:

def rlp_encode(input):
    if isinstance(input,str):
        if len(input) == 1 and ord(input) < 0x80: return input
        else: return encode_length(len(input), 0x80) + input
    elif isinstance(input,list):
        output = ''
        for item in input: output += rlp_encode(item)
        return encode_length(len(output), 0xc0) + output

def encode_length(L,offset):
    if L < 56:
         return chr(L + offset)
    elif L < 256**8:
         BL = to_binary(L)
         return chr(len(BL) + offset + 55) + BL
    else:
         raise Exception("input too long")

def to_binary(x):
    if x == 0:
        return ''
    else: 
        return to_binary(int(x / 256)) + chr(x % 256)

下面再看解码。解码其实就是编码的逆过程,每次读入第一个字节时判断其类型,计算出长度和偏移量,再解构具体数字。其具体规则如下:

  1. 如果第一个字节的范围是[0x00,0x7f],则数据是一个字符串,且字符串本身就是第一个字节;

  2. 如果第一个字节的范围是[0x80,0xb7],则数据是一个字符串,其长度等于第一个字节减去0x80,字符串内容在第一个字节之后;

  3. 如果第一个字节的范围是[0xb8,0xbf],则数据是一个字符串,并且字节长度等于第一个字节减去0xb7,字符串长度在第一个字节之后,字符串跟随长度串;

  4. 如果第一个字节的范围是[0xc0,0xf7],则数据是一个列表,其总长度等于第一个字节减去0xc0,列表中各项目的RLP编码的串联在第一个字节之后;

  5. 如果第一个字节的范围是[0xf8,0xff],则数据是一个列表,字节长度等于第一个字节减去0xf7,列表总长度在第一个字节之后,再接下来是所有项目的RLP编码串联。

例子就不再举了。解码过程的python代码如下:

def rlp_decode(input):
    if len(input) == 0: return
    output = ''
    (offset, dataLen, type) = decode_length(input)
    if type is str: output = instantiate_str(substr(input, offset, dataLen))
    elif type is list: output = instantiate_list(substr(input, offset, dataLen))
    output + rlp_decode(substr(input, offset + dataLen))
    return output

def decode_length(input):
    length = len(input)
    if length == 0: raise Exception("input is null")
    prefix = ord(input[0])
    if prefix <= 0x7f: return (0, 1, str)
    elif prefix <= 0xb7 and length > prefix - 0x80:
        strLen = prefix - 0x80
        return (1, strLen, str)
    elif prefix <= 0xbf and length > prefix - 0xb7 and length > prefix - 0xb7 + to_integer(substr(input, 1, prefix - 0xb7)):
        lenOfStrLen = prefix - 0xb7
        strLen = to_integer(substr(input, 1, lenOfStrLen))
        return (1 + lenOfStrLen, strLen, str)
    elif prefix <= 0xf7 and length > prefix - 0xc0:
        listLen = prefix - 0xc0;
        return (1, listLen, list)
    elif prefix <= 0xff and length > prefix - 0xf7 and length > prefix - 0xf7 + to_integer(substr(input, 1, prefix - 0xf7)):
        lenOfListLen = prefix - 0xf7
        listLen = to_integer(substr(input, 1, lenOfListLen))
        return (1 + lenOfListLen, listLen, list)
    else:
        raise Exception("input don't conform RLP encoding form")

def to_integer(b):
    length = len(b)
    if length == 0: raise Exception("input is null")
    elif length == 1: return ord(b[0])
    else: return ord(substr(b, -1)) + to_integer(substr(b, 0, -1)) * 256

好,RLP编码的算法搞清楚了,接下来回头看以太坊源码里,RLP编码和解码分别是怎么实现的。我用的是以太坊的Go语言版本源码。实际的实现过程很全很细,这里只挑主干。先看rlp/encode.go中的编码过程:

func EncodeToBytes(val interface{}) ([]byte, error) {
    eb := encbufPool.Get().(*encbuf) //从encbuf池当中取出一个encbuf
    defer encbufPool.Put(eb) //运算完了要把encbuf放回池中
    eb.reset()
    if err := eb.encode(val); err != nil {  //调用实际的编码过程
        return nil, err
    }
    return eb.toBytes(), nil  //取得编码后的byte数组
}

encbuf是用来进行编码运算的结构,由于它会非常频繁地调用,为了提高效率,避免反复进行new和dispose,对其进行池化(顺便说一下,由此可见Go语言的效率当然远远超过python等脚本语言,直追C/C++):

var encbufPool = sync.Pool{
    New: func() interface{} { return &encbuf{sizebuf: make([]byte, 9)} },
}

下面重点解释encbuf这个结构。以下是我画的UML类图:
以太坊源码研究之RLP编码_第1张图片
encbuf类图

encbuf类有4个数据成员。str是字节数组,包含所有内容但除了列表头;lheads里存放了所有的列表头;lhsize是所有列表头的总大小;sizebuf是编码期间用到的辅助缓存。除此之外还有一大堆成员函数,篇幅原因这里不一一解释了。重点看一下encode函数:

func (w *encbuf) encode(val interface{}) error {
    rval := reflect.ValueOf(val)  //取得val的Value
    ti, err := cachedTypeInfo(rval.Type(), tags{})  //获取val的类型信息
    if err != nil {
        return err
    }
    return ti.writer(rval, w)  //最后写入w,之后可以用toBytes()函数取得字节数组
}

reflect类似于.net中的反射。注意这里调用了cachedTypeInfo函数,它声明在typecache.go文件内,其内部调用了genTypeInfo函数,然后通过makeDecoder、makeWriter,根据不同的数据类型找到不同的编码和解码函数。以下是makeWriter函数的实现:

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)
    }
}

我们从中抽选writeString来具体看看编码细节:

func writeString(val reflect.Value, w *encbuf) error {
    s := val.String()
    if len(s) == 1 && s[0] <= 0x7f {
        w.str = append(w.str, s[0])  //直接编,不需要头
    } else {
        w.encodeStringHeader(len(s)) //长度大于1,或者首字符大于0x7f
        w.str = append(w.str, s...)
    }
    return nil
}

Decode里相应的解码过程,其实就是编码过程的逆向工程,其算法过程这里不再详细介绍了。但需要注意的一点是,对于一个struct结构,它的数据成员可以带3个解码标志:"tail", ”nil" 和 "-"。"-"标志代表忽略本字段,"nil"标志代表如果这个字段的size为0则改变其值为nil。看下面的代码:

input := []byte{0xC1, 0x80}
var normalRules struct {
    String *string
}
Decode(bytes.NewReader(input), &normalRules)
fmt.Printf("normal: String = %q\n", *normalRules.String)  //normal: String = ""

var withEmptyOK struct {
    String *string `rlp:"nil"`
}
Decode(bytes.NewReader(input), &withEmptyOK)
fmt.Printf("with nil tag: String = %v\n", withEmptyOK.String) //with nil tag: String = 

“tail"标志一般用在数组字段,表示余下的内容都存入这里。看下面的示例代码:

type structWithTail struct {
    A, B uint
    C    []uint `rlp:"tail"`
}
var val structWithTail

err := Decode(bytes.NewReader([]byte{0xC4, 0x01, 0x02, 0x03, 0x04}), &val)
fmt.Printf("with 4 elements: err=%v val=%v\n", err, val)
// with 4 elements: err= val={1 2 [3 4]}

err = Decode(bytes.NewReader([]byte{0xC6, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06}), &val)
fmt.Printf("with 6 elements: err=%v val=%v\n", err, val)
// with 6 elements: err= val={1 2 [3 4 5 6]}

err = Decode(bytes.NewReader([]byte{0xC1, 0x01}), &val)
fmt.Printf("with 1 element: err=%q\n", err)
// with 1 element: err="rlp: too few elements for rlp.structWithTail"

全文结束。


说不出来写不出来,那就说明没学到家。

你可能感兴趣的:(以太坊源码研究之RLP编码)