本文介绍了以太坊中使用的 RLP (Recursive-Length Prefix) 编码,它是一种用于序列化数据结构的紧凑、标准化的方法。文章详细解释了 RLP 的编码逻辑、规范形式,并提供了在 Go 语言中实现 RLP 编码器和解码器的示例代码,包括单元测试。
以太坊的每一个交易、收据和区块头最终都会变成一系列嵌套的字节列表,所有这些都使用相同的简单规则集进行编码,称为 RLP(递归长度前缀)。RLP 不编码数据类型或模式。它只保留结构。一切都被视为原始字节,协议需要决定这些字节的含义。
这使得 RLP 轻量且具有确定性,非常适合共识和客户端通信,但在实际操作之前也会令人困惑。在这篇文章中,我们将解开它的编码逻辑,解释规范形式,并在 Go 中构建一个最小的编码器和解码器。
递归长度前缀 (RLP) 是以太坊执行层中使用的序列化格式。它旨在以紧凑、标准化的方式编码数据结构,从而使执行客户端可以高效地通过网络共享数据。它不关心一个值是字符串、浮点数还是地址,它只看到原始二进制数据。如何解释该二进制数据(作为字符串、数字等)留给使用 RLP 的协议。唯一的例外是正整数,必须将其编码为大端二进制,且没有任何前导零。这意味着数字 0 被视为空字节数组,如果反序列化一个具有额外前导零的数字,则认为该数字无效。**](https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/#definition).
golang 中 RLP 编码的示例:
_注意_: 完整的 GitHub 代码 + 测试引用位于文章底部。
func RlpEncode(input any) []byte {
switch v := input.(type) {
case string:
data := []byte(v)
if len(data) == 1 && data[0] < 0x80 {
return data
}
return append(encodeLength(len(data), 0x80), data...)
case []byte:
if len(v) == 1 && v[0] < 0x80 {
return v
}
return append(encodeLength(len(v), 0x80), v...)
default:
// 处理任何类型的切片(例如,[]string,[]int,[]any)
reflectedValue := reflect.ValueOf(input)
kind := reflectedValue.Kind()
if reflectedValue.Kind() == reflect.Slice {
var output []byte
for i := 0; i < reflectedValue.Len(); i++ {
item := reflectedValue.Index(i).Interface()
output = append(output, RlpEncode(item)...)
}
return append(encodeLength(len(output), 0xc0), output...)
}
// 处理所有整数类型(有符号和无符号)
if isIntegerKind(kind) {
n := toInt(reflectedValue)
if n == 0 {
return []byte{0x80}
}
return encodeInteger(n)
}
panic(fmt.Sprintf("unsupported type: %T", input))
}
}
func encodeLength(length int, offset int) []byte {
if length < 56 {
return []byte{byte(length + offset)}
}
l := big.NewInt(int64(length))
limit := new(big.Int).Lsh(big.NewInt(1), 64) // 2^64
// 不允许长度超过 2^64
if l.Cmp(limit) >= 0 {
panic("input too long")
}
bl := toBinary(length)
return append([]byte{byte(len(bl) + offset + 55)}, bl...)
}
func toBinary(x int) []byte {
if x == 0 {
return []byte{}
}
var buf bytes.Buffer
for x > 0 {
buf.WriteByte(byte(x & 0xff))
x >>= 8
}
// 反转以生成大端
b := buf.Bytes()
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return b
}
func encodeInteger(n int) []byte {
if n < 0 {
panic("RLP only supports unsigned integers")
}
buf := toBinary(n)
if len(buf) == 1 && buf[0] < 0x80 {
return buf
}
return append(encodeLength(len(buf), 0x80), buf...)
}
func isIntegerKind(kind reflect.Kind) bool {
switch kind {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return true
default:
return false
}
}
func toInt(v reflect.Value) int {
// 转换为 int(如果需要更大的范围,可以使用 int64)
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return int(v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return int(v.Uint())
default:
panic("not an integer kind")
}
}
func TestRLPEncoding(t *testing.T) {
tests := map[string]struct {
input any
expected []byte
}{
"string dog": {
input: "dog",
expected: []byte{0x83, 'd', 'o', 'g'},
},
"list [cat, dog]": {
input: []string{"cat", "dog"},
expected: []byte{0xc8, 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g'},
},
"bytes": {
input: []byte("dog"),
expected: []byte{0x83, 'd', 'o', 'g'},
},
"empty string": {
input: "",
expected: []byte{0x80},
},
"empty list": {
input: []any{},
expected: []byte{0xc0},
},
"integer 0": {
input: 0,
expected: []byte{0x80},
},
"integer 15": {
input: 15,
expected: []byte{0x0f},
},
"integer 1024": {
input: 1024,
expected: []byte{0x82, 0x04, 0x00},
},
"byte 0x00": {
input: []byte{0x00},
expected: []byte{0x00},
},
"byte 0x0f": {
input: []byte{0x0f},
expected: []byte{0x0f},
},
"bytes 0x04 0x00": {
input: []byte{0x04, 0x00},
expected: []byte{0x82, 0x04, 0x00},
},
"set theoretical representation [ [], [[]], [ [], [[]] ] ]": {
input: []any{
[]any{},
[]any{[]any{}},
[]any{
[]any{},
[]any{[]any{}},
},
},
expected: []byte{0xc7, 0xc0, 0xc1, 0xc0, 0xc3, 0xc0, 0xc1, 0xc0},
},
"long string Lorem ipsum...": {
input: "Lorem ipsum dolor sit amet, consectetur adipisicing elit",
expected: append([]byte{0xb8, 0x38}, []byte("Lorem ipsum dolor sit amet, consectetur adipisicing elit")...),
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
result := RlpEncode(tt.input)
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("Encode(%v) = %v, want %v", tt.input, result, tt.expected)
t.Errorf("Encode(%v) = %x, want %x", tt.input, result, tt.expected)
}
})
}
}
_注意: RLP 旨在编码结构_, 而不是数据类型, 因此它不对诸如“浮点数”、“布尔值”或“有符号整数”之类的内容进行任何假设。
你可以在此处找到关于 RLP 解码规则的信息。
golang 中 RLP 解码的示例:
// RlpDecode 将 RLP 编码的字节切片解码为 Go 值。
// 它返回 []byte 或表示列表的 []any。
func RlpDecode(input []byte) (interface{}, error) {
val, _, err := decodeItem(input)
return val, err
}
// decodeItem 处理单个 RLP 值,该值可以是:
// - 单个字节
// - 字符串(短或长)
// - 列表(短或长)
func decodeItem(data []byte) (any, int, error) {
if len(data) == 0 {
return nil, 0, errors.New("empty input")
}
prefix := data[0]
switch {
// Case 1: 单个字节(0x00 到 0x7f) - 值是字节本身
case prefix <= 0x7f:
return data[:1], 1, nil
// Case 2: 短字符串(0x80 到 0xb7)
// 第一个字节 = 0x80 + 字符串的长度
case prefix <= 0xb7:
strLen := int(prefix - 0x80)
if len(data) < 1+strLen {
return nil, 0, errors.New("short string too short")
}
return data[1 : 1+strLen], 1 + strLen, nil
// Case 3: 长字符串(0xb8 到 0xbf)
// 第一个字节 = 0xb7 + 长度的长度 (lenOfLen)
// 下一个 lenOfLen 字节 = 字符串的实际长度
case prefix <= 0xbf:
lenOfLen := int(prefix - 0xb7)
if len(data) < 1+lenOfLen {
return nil, 0, errors.New("long string length prefix too short")
}
strLen := decodeLength(data[1 : 1+lenOfLen])
if len(data) < 1+lenOfLen+strLen {
return nil, 0, errors.New("long string too short")
}
return data[1+lenOfLen : 1+lenOfLen+strLen], 1 + lenOfLen + strLen, nil
// Case 4: 短列表(0xc0 到 0xf7)
// 第一个字节 = 0xc0 + 编码项的总有效载荷长度
case prefix <= 0xf7:
listLen := int(prefix - 0xc0)
if len(data) < 1+listLen {
return nil, 0, errors.New("short list too short")
}
items, err := decodeList(data[1 : 1+listLen])
return items, 1 + listLen, err
// Case 5: 长列表(0xf8 到 0xff)
// 第一个字节 = 0xf7 + 长度的长度 (lenOfLen)
// 下一个 lenOfLen 字节 = 列表有效载荷的实际长度
default:
lenOfLen := int(prefix - 0xf7)
if len(data) < 1+lenOfLen {
return nil, 0, errors.New("long list length prefix too short")
}
listLen := decodeLength(data[1 : 1+lenOfLen])
if len(data) < 1+lenOfLen+listLen {
return nil, 0, errors.New("long list too short")
}
items, err := decodeList(data[1+lenOfLen : 1+lenOfLen+listLen])
return items, 1 + lenOfLen + listLen, err
}
}
// decodeList 遍历表示列表有效载荷的字节切片,
// 递归解码列表中的每个 RLP 项。
func decodeList(data []byte) ([]any, error) {
// 应该返回一个空切片而不是 nil
if len(data) == 0 {
return []any{}, nil // 返回空切片而不是 nil
}
var result []any
for len(data) > 0 {
val, consumed, err := decodeItem(data)
if err != nil {
return nil, err
}
result = append(result, val)
data = data[consumed:]
}
return result, nil
}
// decodeLength 将大端字节切片解释为整数长度。
// 这用于长度本身被编码的长字符串/列表。
func decodeLength(b []byte) int {
n := 0
for _, by := range b {
// 左移并添加下一个字节(大端)
n = (n << 8) + int(by)
}
return n
}
func TestRLPDecoding(t *testing.T) {
tests := map[string]struct {
input []byte
expected any
}{
"string dog": {
input: []byte{0x83, 'd', 'o', 'g'},
expected: []byte("dog"),
},
"list [cat, dog]": {
input: []byte{0xc8, 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g'},
expected: []any{
[]byte("cat"),
[]byte("dog"),
},
},
"bytes": {
input: []byte{0x83, 'd', 'o', 'g'},
expected: []byte("dog"),
},
"empty string": {
input: []byte{0x80},
expected: []byte{},
},
"empty list": {
input: []byte{0xc0},
expected: []any{},
},
"integer 0": {
input: []byte{0x80},
expected: []byte{},
},
"integer 15": {
input: []byte{0x0f},
expected: []byte{0x0f},
},
"integer 1024": {
input: []byte{0x82, 0x04, 0x00},
expected: []byte{0x04, 0x00},
},
"byte 0x00": {
input: []byte{0x00},
expected: []byte{0x00},
},
"byte 0x0f": {
input: []byte{0x0f},
expected: []byte{0x0f},
},
"bytes 0x04 0x00": {
input: []byte{0x82, 0x04, 0x00},
expected: []byte{0x04, 0x00},
},
"set theoretical representation [ [], [[]], [ [], [[]] ] ]": {
input: []byte{0xc7, 0xc0, 0xc1, 0xc0, 0xc3, 0xc0, 0xc1, 0xc0},
expected: []any{
[]any{},
[]any{[]any{}},
[]any{
[]any{},
[]any{[]any{}},
},
},
},
"long string Lorem ipsum...": {
input: append([]byte{0xb8, 0x38}, []byte("Lorem ipsum dolor sit amet, consectetur adipisicing elit")...),
expected: []byte("Lorem ipsum dolor sit amet, consectetur adipisicing elit"),
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
val, err := RlpDecode(tt.input)
if err != nil {
t.Fatalf("RlpDecode failed: %v", err)
}
switch expected := tt.expected.(type) {
case []byte:
actual, ok := val.([]byte)
if !ok {
t.Fatalf("Expected []byte, got %T", val)
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Decoded []byte = %x, want %x", actual, expected)
}
case []any:
actual, ok := val.([]any)
if !ok {
t.Fatalf("Expected []any, got %T", val)
}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Decoded list = %#v\nExpected list = %#v", actual, expected)
}
default:
t.Fatalf("Unsupported expected type: %T", expected)
}
})
}
}
0x80。string → UTF-8 字节;对于精确控制,首选 []byte。RLP 是以太坊的基础编码格式:一种简单、与类型无关的方式,可以将嵌套结构转换为字节。它不关心数据类型,只关心结构和长度。你在以太坊上看到的每一个交易、收据和区块头最终都是通过这同样几个规则进行序列化的。
你很少需要手动编写编码器或解码器,但是了解 RLP 的工作原理可以帮助你读取原始交易数据、调试节点跟踪,并推断执行客户端在底层实际交换的内容。
- 原文链接: medium.com/@andrey_obruc...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!