什么是递归长度前缀 (RLP) 序列化

本文介绍了以太坊中使用的 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")
 }
}

测试 RLP 编码

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
}

测试 RLP 解码

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

注意事项

  • RLP 编码结构,而不是类型。你可以决定字节是否代表“字符串”、“地址”或“uint”。
  • 零整数编码为空字节 ⇒ RLP 0x80
  • 拒绝非规范形式(整数中的前导零,过长的长度编码)。
  • 字符串 vs 字节:在 Go 中,string → UTF-8 字节;对于精确控制,首选 []byte

总结

RLP 是以太坊的基础编码格式:一种简单、与类型无关的方式,可以将嵌套结构转换为字节。它不关心数据类型,只关心结构和长度。你在以太坊上看到的每一个交易、收据和区块头最终都是通过这同样几个规则进行序列化的。

你很少需要手动编写编码器或解码器,但是了解 RLP 的工作原理可以帮助你读取原始交易数据、调试节点跟踪,并推断执行客户端在底层实际交换的内容。

资源

  • 带有测试的 RLP 代码可以在 这里 找到
  • 原文链接: medium.com/@andrey_obruc...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Andrey Obruchkov
Andrey Obruchkov
江湖只有他的大名,没有他的介绍。