惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园_首页
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
ThreatConnect
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 聂微东
H
Help Net Security
T
Threat Research - Cisco Blogs
Blog — PlanetScale
Blog — PlanetScale
A
Arctic Wolf
G
Google Developers Blog
量子位
U
Unit 42
I
InfoQ
V
V2EX
F
Fox-IT International blog
P
Privacy & Cybersecurity Law Blog
V
Visual Studio Blog
J
Java Code Geeks
大猫的无限游戏
大猫的无限游戏
C
CERT Recently Published Vulnerability Notes
博客园 - 三生石上(FineUI控件)
T
The Exploit Database - CXSecurity.com
T
Tailwind CSS Blog
SecWiki News
SecWiki News
Know Your Adversary
Know Your Adversary
MyScale Blog
MyScale Blog
宝玉的分享
宝玉的分享
The Hacker News
The Hacker News
Project Zero
Project Zero
Application and Cybersecurity Blog
Application and Cybersecurity Blog
月光博客
月光博客
Recent Commits to openclaw:main
Recent Commits to openclaw:main
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
GRAHAM CLULEY
C
Cisco Blogs
I
Intezer
Simon Willison's Weblog
Simon Willison's Weblog
O
OpenAI News
Recorded Future
Recorded Future
T
Tenable Blog
W
WeLiveSecurity
腾讯CDC
Stack Overflow Blog
Stack Overflow Blog
T
The Blog of Author Tim Ferriss
www.infosecurity-magazine.com
www.infosecurity-magazine.com
D
Docker
C
Cybersecurity and Infrastructure Security Agency CISA
PCI Perspectives
PCI Perspectives

追梦人物的博客

0x05:Merkle Tree & Patricia Trie 0x04:ECDSA LeetCode 105 从前序与中序遍历构造二叉树 迭代算法原理 + 完整证明 0x03:Address 0x02:Secp256k1 - 以太坊设计与实现 0x00:专栏开篇 Bellman-Ford 算法原理及其在 DeFi 套利中的应用 迪杰斯特拉(Dijkstra)最短路径算法原理、实现与证明 实战:CEX-DEX 稳定币套利监控程序开发 CEX-DEX 稳定币套利模型 Uniswap 手续费和协议费机制剖析 Uniswap 流动性机制及相关数学原理分析 uv 替代 pyenv + pipx + poetry 环境管理实践 Rust 项目从创建到发布 VS Code 调试 Python 比特币跨市场套利的数学模型 ERC-20 相关知识点总结 Django 老项目如何从 SQLite 迁到 PostgreSQL 自动生成接口文档 单元测试 限制接口访问频率 API 版本管理 如何在 Windows 下搭建高效的 django 开发环境 拓展Python Markdown 加缓存为接口提速 基于 drf-haystack 实现文章搜索接口 评论接口 实现分类、标签、归档日期接口 在接口返回Markdown解析后的内容 文章详情接口 分页 使用视图集简化代码 用类视图实现首页 API 实现博客首页文章列表 API 初始化 RESTful API 风格的博客系统 django-rest-framework 是什么鬼? 结束 or 开始? Coverage.py 统计测试覆盖率 单元测试:测试评论应用 单元测试:测试 blog 应用 Django Haystack 全文检索与关键词高亮 Django 博客实现简单的全文搜索 开启 Django 博客的 RSS 功能 统计各个分类和标签下的文章数
0x01:RLP 编码 - 以太坊设计与实现
2026-01-20 · via 追梦人物的博客

作为专栏正文第一篇,我们从一个简单但在以太坊系统内广泛使用的工具——RLP(Recursive-Length prefix,递归长度前缀)编码开始。

对人类而言,一般使用的都是描述性很强的数据结构表示形式,比如下面这样的数据:

{
  "to": "abcdefg",
  "value": 100
}

但对以太坊系统而言,其磁盘存储和网络传输的,都是 0101 这样的二进制序列。所以,需要将描述性很强的数据结构表示形式转为二进制序列,这就是编码。

在以太坊设计初期,已有不少成熟的编码方案,但这些方案包含了大量以太坊并不需要的功能模块,显得较为 “臃肿”。以太坊需要的是:

  • 确定性。区块链依赖于数据的哈希值来唯一标识和验证数据,对同一数据,其描述性的结构可能不一样,但编码后的结果必须完全一致。
  • 紧凑性。区块链需要存储整个历史数据,数据也需要在网络中传播,越紧凑越节约系统资源。
  • 简洁性。设计越简洁,编码规则越易于理解和实现,也越不易出错。在区块链这样对安全性要求极高的系统中,简单即是美——代码越简单,漏洞越少,审计越容易。

阅读提示

如果对编码细节不感兴趣,可将 RLP 视为一个 “黑盒工具”:只需了解它能把数据编码成一串二进制数字,且通过解码能将这串二进制数字还原为人类可读懂的描述性数据即可,后续相关细节内容可直接跳过。待有实际需求(比如某些场景下需要自行解码交易数据)时,再回头研究具体原理即可。

编码规则概要

RLP 编码的核心思想是用"长度前缀"来标记数据的类型和长度。它将数据分为三类:单字节、字符串、列表,每类采用不同的编码规则。

单字节编码

对于单个字节,且值在 [0x00, 0x7f] 范围内,编码即原字节本身(不加任何前缀)。

示例:

  • 0x000x00
  • 0x7f0x7f

字符串编码

字符串编码分为三种情况:

空字符串

空字符串编码为 0x80

短字符串(0-55 字节)

字符串长度在 0-55 字节时:

  • 前缀 = 0x80 + 字符串长度
  • 后跟字符串内容

示例:

  • "dog" (长度 3) → 0x83 + 0x64 + 0x6F + 0x67

其中:0x83 是 RLP 前缀,0x64/0x6F/0x67 分别是字符 d/o/g 本身的 16 进制值。

长字符串(56 字节以上)

字符串长度 ≥ 56 字节时:

  • 前缀 = 0xb7 + 长度本身的字节数
  • 后跟长度的编码(大端序)
  • 最后跟字符串内容

示例:

  • 字符串长度为 1024(需要 2 字节表示)
  • 前缀 = 0xb7 + 2 = 0xb9
  • 后跟长度 1024 的编码:0x04 + 0x00
  • 最后跟 1024 字节的字符串内容

列表编码

列表编码是指将列表中所有元素递归编码后拼接,再对总长度加前缀。

空列表

空列表编码为 0xc0

短列表(总载荷 0-55 字节)

列表所有元素编码后的总长度在 0-55 字节时:

  • 前缀 = 0xc0 + 总长度
  • 后跟各元素的编码

示例:

  • ["cat", "dog"] → 编码后总长度 8(cat 和 dog 均为短字符串,RLP 编码后都占 4 字节)
  • "cat"0x83 + 0x63 + 0x61+ 0x74
  • "dog"0x83 + 0x64 + 0x6f + 0x67
  • 列表编码 → 0xc8 + 0x83636174 + 0x83646f67

长列表(总载荷 56 字节以上)

列表所有元素编码后的总长度 ≥ 56 字节时:

  • 前缀 = 0xf7 + 长度本身的字节数
  • 后跟长度的编码(大端序)
  • 最后跟各元素的编码

关键边界情况

  • 长度恰好 55:使用短编码规则(前缀 0x80 + 55 = 0xb7,此时前缀等于长字符串前缀的起始值,但由于长度本身不占用额外字节,仍属短编码)
  • 长度恰好 56:使用长编码规则(前缀 0xb7 + 1 = 0xb8,后跟 1 字节的长度 0x38
  • 空字符串 vs 单字节 0x00:空字符串编码为 0x80,单字节 0x00 编码为 0x00

整数类型的处理

以太坊中的整数需要先转换为字节串,再进行 RLP 编码:

  • 使用大端序(Big-Endian)编码
  • 去掉前导零(除了数字 0 本身编码为空字符串)。注意这里所说的前导零是指有效位以外的 0,例如 0x000418,去除前导 0 后是 0x0418,而不是 0x418,04 是一个整体有效位,占 1 个字节。

示例:

  • 整数 0 → 空字符串 → 0x80
  • 整数 1024 → 字节串 [0x04, 0x00]0x82 + 0x04 + 0x00
  • 整数 0x80 (128) → 字节串 [0x80]0x81 + 0x80

编码规则总结表

数据类型 长度范围 前缀计算方式 前缀范围
单字节 [0x00, 0x7f] 无前缀 N/A
空字符串 0 0x80 0x80
短字符串 1-55 0x80 + 长度 [0x81, 0xb7]
长字符串 ≥56 0xb7 + 长度字节数 [0xb8, 0xbf]
空列表 0 0xc0 0xc0
短列表 总载荷 0-55 0xc0 + 总长度 [0xc1, 0xf7]
长列表 总载荷 ≥56 0xf7 + 长度字节数 [0xf8, 0xff]

编码示例

我们通过一个完整的以太坊交易(Transaction)示例,展示 RLP 编码的过程。现阶段我们无需知道以太坊交易各个字段的具体含义,后续的专栏文章会进行详细讲解。

Transaction 结构

最简单的交易结构,包含以下字段:

[nonce, gasPrice, gasLimit, to, value, v, r, s]

示例交易

假设 Alice 向 Bob 转账 1 ETH,交易数据如下:

  • nonce: 0 (交易序号)
  • gasPrice: 0x4a817c800 (20 Gwei,即 20 × 10^9 wei,16 进制表示的整数)
  • gasLimit: 0x5208 (21000,16 进制表示的整数,标准 ETH 转账的 gas 限制)
  • to: 0x3535353535353535353535353535353535353535 (Bob 的地址,20 字节)
  • value: 0xde0b6b3a7640000 (10^18 wei,即 1ETH,16 进制表示的整数)
  • v: 0x1c (签名恢复标识,16 进制表示的整数)
  • r: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef (签名 r 值,32 字节)
  • s: 0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba (签名 s 值,32 字节)

编码步骤

说明:编码后的结果都以 16 进制表示,但为了简洁,省略掉了前缀 0x

步骤 1:对每个字段进行 RLP 编码

1. nonce = 0

  • 整数 0 转为字节串,去除前导 0 → 空字符串
  • 编码 → 80

2. gasPrice = 0x4a817c800

  • 整数,转为大端序字节串为:04 a8 17 c8 00 (5 字节)
  • 编码 → 85 + 04a817c800 = 8504a817c800

3. gasLimit = 0x5208

  • 整数,转为大端序字节串为:52 08 (2 字节)
  • 编码 → 82 + 5208 = 825208

4. to = 0x353535...3535 (20 字节)

  • 字节串:3535353535353535353535353535353535353535
  • 编码 → 94 + 地址字节串 = 943535353535353535353535353535353535353535

5. value = 0xde0b6b3a7640000

  • 整数,转为字节串:0d e0 b6 b3 a7 64 00 00] (8 字节)
  • 编码 → 88 + 0de0b6b3a7640000 = 880de0b6b3a7640000

6. v = 0x1c

  • 单字节 0x1c[0x00, 0x7f] 范围内
  • 编码 → 0x1c

7. r = 0x1234567890abcdef... (32 字节)

  • 字节串:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
  • 编码 → a0 + 32 字节 r 值 = a01234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

8. s = 0x9876543210fedcba... (32 字节)

  • 字节串:9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba
  • 编码 → a0 + 32 字节 s 值 = a09876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba

步骤 2:计算总长度

所有字段编码后的总长度 = 1 + 6 + 3 + 21 + 9 + 1 + 33 + 33 = 107 字节

步骤 3:对列表整体编码

总长度 107 字节 ≥ 56,使用长列表编码:

  • 长度 107 需要用 1 字节表示(6b
  • 前缀 = f7 + 1 = f8
  • 后跟长度编码:6b
  • 最后拼接所有字段的编码

最终编码结果

注意:为了方便阅读,每个元素间用了空格隔开,实际结果中是没有空格的!

f86b 80 8504a817c800 825208 943535353535353535353535353535353535353535 880de0b6b3a7640000 1c a01234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef a09876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba

编码前后对比

字段 原始值 RLP 编码
nonce 0 80
gasPrice 0x4a817c800 (20 Gwei) 8504a817c800
gasLimit 0x5208 (21000) 825208
to 0x3535...3535 (20 字节) 943535...3535
value 0xde0b6b3a7640000 (1 ETH) 880de0b6b3a7640000
v 0x1c 1c
r 0x1234...cdef (32 字节) a01234...cdef
s 0x9876...dcba (32 字节) a09876...dcba

通过这个示例可以看到,RLP 将复杂的交易结构编码为一串紧凑的字节序列,每个部分都可以通过解析前缀长度来正确解码。

go-ethereum 中的实现

go-ethereum 的 RLP 编码位于 rlp package,本节深入分析其核心编码实现逻辑。

编码逻辑的实现思路很直接:对于简单的数据类型,例如整数或字符串,应用前文所述的编码规则进行编码;对于复合数据类型,逐一拆解为简单类型后按规则进行编码。

编码入口

编码的入口函数是 EncodeToBytes,它将任意值编码为 RLP 字节序列:

// rlp/encode.go

func EncodeToBytes(val interface{}) ([]byte, error) {
    buf := getEncBuffer()
    defer encBufferPool.Put(buf)

    if err := buf.encode(val); err != nil {
        return nil, err
    }
    return buf.makeBytes(), nil
}

入口函数十分简单,因为所有的编码的逻辑都封装在 buf.encode 中,buf 的类型是 encBuffer,负责存储编码全过程中编码值的中间结果,并在最后将这些中间结果转换为最终编码结果。

核心数据结构:encBuffer

encBuffer,定义如下:

// rlp/encbuffer.go

type encBuffer struct {
    str     []byte     // string data, contains everything except list headers
    lheads  []listhead // all list headers
    lhsize  int        // sum of sizes of all encoded list headers
    sizebuf [9]byte    // auxiliary buffer for uint encoding
}

各字段的作用:

  • str:存储所有简单类型(不含列表结构的前缀,但包含列表中的元素)编码后的结果
  • lheads:存储所有列表结构的信息,用于处理嵌套列表(下方的说明和后续给出的例子会帮助大家更好地理解其用途)
  • lhsize:所有列表结构编码前缀的总大小
  • sizebuf:辅助缓冲区,用于临时存储长度的编码

listhead 结构体的定义:

// rlp/encode.go

type listhead struct {
    offset int // index of this header in string data
    size   int // total size of encoded data (including list headers)
}

lheads 的存在是为了处理列表的嵌套。根据前文所述的列表编码规则:对列表所有元素编码后,根据结果长度生成前缀,前缀 + 所有元素编码结果作为列表的编码结果。如果包含嵌套列表,这就是一个递归过程。当遇到一个列表时,无法立即知道其前缀值,只有将内部元素全部编码完毕后才可计算。lheads 用来暂存当前列表在 str 中的位置以及内部元素编码后的总长度(内部元素全部编码完后进行更新),生成最终结果时,就能够根据这些信息,将列表的前缀值插入正确位置。

😣听着稍显复杂,不过不用着急,后面的例子会帮助大家更好地理解其用途。

字符串编码

encBuffer 定义了 encodeStringHeader 方法,实现了字符串编码的前缀计算逻辑,对应前文所述的字符串编码规则:

// rlp/encbuffer.go

func (buf *encBuffer) encodeStringHeader(size int) {
    if size < 56 {
        // 短字符串:前缀 = 0x80 + size
        buf.str = append(buf.str, 0x80+byte(size))
    } else {
        // 长字符串:前缀 = 0xB7 + 长度字节数
        sizesize := putint(buf.sizebuf[1:], uint64(size))
        buf.sizebuf[0] = 0xB7 + byte(sizesize)
        buf.str = append(buf.str, buf.sizebuf[:sizesize+1]...)
    }
}

这个函数精确实现了前文描述的规则:

  • 长度 < 56:使用 0x80 + size 作为单字节前缀
  • 长度 ≥ 56:使用 0xB7 + 长度字节数,后跟长度的编码

整数编码

writeUint64 方法负责将整数编码为 RLP 格式。整数编码首先将整数转为字节串,然后调用字符串编码逻辑:

// rlp/encbuffer.go

func (buf *encBuffer) writeUint64(i uint64) {
    if i == 0 {
        // 去除前导 0 后,等价于空字符串
        buf.str = append(buf.str, 0x80)
    } else if i < 128 {
        // fits single byte, no string header
        buf.str = append(buf.str, byte(i))
    } else {
        s := putint(buf.sizebuf[1:], i)
        buf.sizebuf[0] = 0x80 + byte(s)
        buf.str = append(buf.str, buf.sizebuf[:s+1]...)
    }
}

关键点:

  • 整数 0 编码为空字符串 → 0x80
  • 整数 < 128(0x80)编码为单字节本身,符合单字节规则
  • 整数 ≥ 128:先编码长度,再加上前缀 0x80 + 长度字节数

对于大整数,writeBigInt 方法使用类似的逻辑:

// rlp/encbuffer.go

func (buf *encBuffer) writeBigInt(i *big.Int) {
    bitlen := i.BitLen()
    if bitlen <= 64 {
        buf.writeUint64(i.Uint64())
        return
    }
    // 计算最小字节长度
    length := ((bitlen + 7) & -8) >> 3
    buf.encodeStringHeader(length)
    buf.str = append(buf.str, make([]byte, length)...)
    bytesBuf := buf.str[len(buf.str)-length:]
    math.ReadBits(i, bytesBuf)
}

使用 (bitlen + 7) & -8) >> 3 计算最小字节长度,math.ReadBits 将大整数按大端序写入字节切片。

列表编码

列表编码通过 listlistEnd 方法协作完成:

// rlp/encbuffer.go

// list adds a new list header to the header stack. It returns the index of the header.
func (buf *encBuffer) list() int {
    buf.lheads = append(buf.lheads, listhead{offset: len(buf.str), size: buf.lhsize})
    return len(buf.lheads) - 1
}

func (buf *encBuffer) listEnd(index int) {
    lh := &buf.lheads[index]
    lh.size = buf.size() - lh.offset - lh.size
    if lh.size < 56 {
        // 短列表规则,RLP 前缀仅占用 1 个字节
        buf.lhsize++ // length encoded into kind tag
    } else {
        // RLP 前缀占用 1 + intsize(lh.size) 个字节
        buf.lhsize += 1 + intsize(uint64(lh.size))
    }
}

list() 方法创建一个新的列表头,并将其添加到 lheads 栈中,返回该列表头的索引。listEnd(index) 方法在列表的所有元素编码完毕后调用,计算该列表的总大小(size),并更新 lhsize

编码示例

结合上面的数据结构和方法,我们以下方的 Person 结构体为例,详细追踪整个编码过程中代码的调用逻辑,理解这些数据结构和方法的实际作用。初始数据:

Person{
    Name:    "hello",
    Age:     33,
    Hobbies: []string{"basketball", "fishing"},
}

步骤 1:初始化编码缓冲区

buf := getEncBuffer()
// buf.str = []
// buf.lheads = []
// buf.lhsize = 0

步骤 2:编码结构体(视为列表)

通过反射得知 Personstruct,在 RLP 中被编码为列表。首先调用 list() 方法,表示开始进行列表结构的编码:

lh := buf.list()  // 返回 0
// buf.lheads = [{offset: 0, size: 0}]
// buf.lhsize = 0

由于列表在起始位置,所以其偏移(offset)为 0,size 目前未知。

步骤 3:编码 Name 字段

Name 字段值为 "hello",长度为 5,是短字符串。调用 encodeStringHeaderwriteString

buf.writeString("hello")
// buf.str = [0x85, 'h', 'e', 'l', 'l', 'o']

编码结果:0x85 (前缀 0x80 + 5) + "hello" = 8568656c6c6f

步骤 4:编码 Age 字段

Age 字段值为 33,是单字节整数且 < 128。调用 writeUint64(33)

buf.writeUint64(33)
// buf.str = [0x85, 'h', 'e', 'l', 'l', 'o', 0x21]

编码结果:0x21 (33 的十六进制)

步骤 5:编码 Hobbies 字段(嵌套列表)

Hobbies[]string 类型,需要编码为列表。

步骤 5.1:开始内层列表

lh_inner := buf.list()  // 返回 1
// buf.lheads = [{offset: 0, size: 0}, {offset: 7, size: 0}]

buf.str 已经存储了 Name 和 Age 编码的值,当前长度为 7,因此 offset = 7,表示内层列表是在 7 这个位置开始的,size 目前未知。

步骤 5.2:编码 "basketball"

buf.writeString("basketball")
// buf.str = [..., 0x8a, 'b', 'a', 's', 'k', 'e', 't', 'b', 'a', 'l', 'l']

"basketball" 长度为 10,前缀 = 0x80 + 10 = 0x8a

步骤 5.3:编码 "fishing"

buf.writeString("fishing")
// buf.str = [..., 0x87, 'f', 'i', 's', 'h', 'i', 'n', 'g']

"fishing" 长度为 7,前缀 = 0x80 + 7 = 0x87

此时 buf.str 的完整内容:

[0x85, 'h', 'e', 'l', 'l', 'o', 0x21, 0x8a, 'b', 'a', 's', 'k', 'e', 't', 'b', 'a', 'l', 'l', 0x87, 'f', 'i', 's', 'h', 'i', 'n', 'g']

总长度为 25 字节。

步骤 5.4:结束内层列表

buf.listEnd(1)
// lh_inner.size = buf.size() - lh_inner.offset - lh_inner.size
//               = 25 - 7 - 0 = 18
// buf.lheads = [{offset: 0, size: 0}, {offset: 7, size: 18}]
// buf.lhsize = 1 (因为 18 < 56,列表头只占 1 字节)

内层列表的总长度(包括所有元素)为 18 字节,使用短列表编码:前缀 = 0xC0 + 18 = 0xD2

步骤 6:结束外层列表

buf.listEnd(0)
// lh.size = buf.size() - lh.offset - lh.size
//        = 25 - 0 - 1 = 24
// buf.lheads = [{offset: 0, size: 24}, {offset: 7, size: 18}]
// buf.lhsize = 2 (两个列表头各占 1 字节)

外层列表的总长度为 24 字节,使用短列表编码:前缀 = 0xC0 + 24 = 0xD8

步骤 7:生成最终结果

调用 buf.makeBytes() 生成最终编码结果:

// rlp/encbuffer.go

func (buf *encBuffer) makeBytes() []byte {
    out := make([]byte, buf.size())
    buf.copyTo(out)
    return out
}

func (buf *encBuffer) copyTo(dst []byte) {
    strpos := 0
    pos := 0
    for _, head := range buf.lheads {
        // write string data before header
        n := copy(dst[pos:], buf.str[strpos:head.offset])
        pos += n
        strpos += n
        // write the header
        enc := head.encode(dst[pos:])
        pos += len(enc)
    }
    // copy string data after the last list header
    copy(dst[pos:], buf.str[strpos:])
}

copyTo 方法的逻辑是遍历所有列表头,按照正确的顺序将字符串数据和列表头写入输出缓冲区。对于每个列表头:

  1. 将该列表头之前的字符串数据复制到输出
  2. 编码并写入该列表头
  3. 更新位置指针

listhead.encode 方法:

// rlp/encode.go

func (head *listhead) encode(buf []byte) []byte {
    return buf[:puthead(buf, 0xC0, 0xF7, uint64(head.size))]
}

func puthead(buf []byte, smalltag, largetag byte, size uint64) int {
    if size < 56 {
        buf[0] = smalltag + byte(size)
        return 1
    }
    sizesize := putint(buf[1:], size)
    buf[0] = largetag + byte(sizesize)
    return sizesize + 1
}

对于外层列表,head.size = 24puthead 生成前缀 0xC0 + 24 = 0xD8

对于内层列表,head.size = 18puthead 生成前缀 0xC0 + 18 = 0xD2

最终编码结果

D8                    → 外层列表前缀 (0xC0 + 24)
────────────────────────────────────────
85                    → "hello" 的前缀 (0x80 + 5)
68 65 6C 6C 6F        → "hello"
21                    → 33 (Age)
D2                    → 内层列表前缀 (0xC0 + 18)
────────────────────────────────────────
8A                    → "basketball" 的前缀 (0x80 + 10)
62 61 73 6B 65 74 62 61 6C 6C → "basketball"
87                    → "fishing" 的前缀 (0x80 + 7)
66 69 73 68 69 6E 67  → "fishing"

完整编码(十六进制):

D8 8568656C6C6F21 D2 8A6261736B657462616C6C87 66697368696E67

通过这个示例可以看到,go-ethereum 的 RLP 实现将复杂的嵌套结构编码为一串紧凑的字节序列。encBuffer 通过 str 存储所有字符串内容,lheads 存储列表头部信息,在 listEnd 时计算每个列表的总大小,最后通过 copyTo 将所有部分按正确顺序组装成最终结果。

解码是编码的逆过程,实现思路类似,关键是从字节序列的前缀中解析得到元素的类型和值的长度,具体可参考 decode.go 等文件中有关的代码,篇幅所限,这里不再赘述。

总结

RLP 编码规则相对简单,但在以太坊系统中应用极为广泛。系统内大量数据(如交易信息、区块数据等)的存储与网络传输,均需经过 RLP 编码处理,后续学习或开发中,我们会在多个核心模块中频繁接触到它。

在实际开发场景中,各主流编程语言都有成熟的 RLP 开源库,开发者基本无需手动编写编码逻辑,直接调用库中封装好的 API 即可完成操作,无需深入关注底层实现细节;仅在极少数特殊场景(如定制化编码需求、底层库适配问题)下,才需参考官方 RLP 规范手动实现编解码逻辑。

-- EOF --