Try it Live
Run RLP examples in the interactive playground
RLP Usage Patterns
Real-world RLP usage patterns for Ethereum transactions, blocks, receipts, and other data structures.Overview
RLP is used throughout Ethereum for serializing structured data. This guide shows common patterns and best practices.Transaction Encoding
Legacy Ethereum transactions use RLP encoding with 9 fields.Basic Transaction
Copy
Ask AI
import { Rlp } from 'tevm'
// Transaction structure
interface Transaction {
nonce: bigint
gasPrice: bigint
gasLimit: bigint
to: Uint8Array // 20-byte address
value: bigint
data: Uint8Array
v: bigint
r: Uint8Array // 32-byte signature component
s: Uint8Array // 32-byte signature component
}
// Convert bigint to minimal big-endian bytes
function bigintToBytes(value: bigint): Uint8Array {
if (value === 0n) return Bytes()
const hex = value.toString(16)
const padded = hex.length % 2 ? '0' + hex : hex
const bytes = new Uint8Array(padded.length / 2)
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(padded.slice(i * 2, i * 2 + 2), 16)
}
return bytes
}
// Encode transaction
function encodeTransaction(tx: Transaction): Uint8Array {
const fields = [
bigintToBytes(tx.nonce),
bigintToBytes(tx.gasPrice),
bigintToBytes(tx.gasLimit),
tx.to,
bigintToBytes(tx.value),
tx.data,
bigintToBytes(tx.v),
tx.r,
tx.s
]
return Rlp.encode(fields)
}
// Example usage
const tx: Transaction = {
nonce: 0n,
gasPrice: 20_000_000_000n, // 20 gwei
gasLimit: 21000n,
to: new Uint8Array(20).fill(0x01),
value: 1_000_000_000_000_000_000n, // 1 ETH
data: Bytes(),
v: 27n,
r: Bytes32().fill(0x02),
s: Bytes32().fill(0x03)
}
const encoded = encodeTransaction(tx)
console.log('Encoded transaction:', encoded)
Signing Hash
Calculate transaction hash for signing:Copy
Ask AI
import { Rlp } from 'tevm'
import { keccak256 } from 'tevm/crypto'
function getSigningHash(tx: Omit<Transaction, 'v' | 'r' | 's'>): Uint8Array {
// For signing, use chainId in place of v, r, s
const chainId = 1n // Mainnet
const fields = [
bigintToBytes(tx.nonce),
bigintToBytes(tx.gasPrice),
bigintToBytes(tx.gasLimit),
tx.to,
bigintToBytes(tx.value),
tx.data,
bigintToBytes(chainId),
Bytes(), // Empty r
Bytes() // Empty s
]
const encoded = Rlp.encode(fields)
return keccak256(encoded)
}
Decoding Transaction
Extract transaction fields from RLP:Copy
Ask AI
import { Rlp } from 'tevm'
function decodeTransaction(bytes: Uint8Array): Transaction {
const result = Rlp.decode(bytes)
if (result.data.type !== 'list') {
throw new Error('Transaction must be RLP list')
}
if (result.data.value.length !== 9) {
throw new Error('Transaction must have 9 fields')
}
const fields = result.data.value
// Helper to extract bytes data
function getBytes(index: number): Uint8Array {
const field = fields[index]
if (!field || field.type !== 'bytes') {
throw new Error(`Field ${index} must be bytes`)
}
return field.value
}
// Helper to convert bytes to bigint
function bytesToBigint(bytes: Uint8Array): bigint {
if (bytes.length === 0) return 0n
return BigInt('0x' + Array(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join(''))
}
return {
nonce: bytesToBigint(getBytes(0)),
gasPrice: bytesToBigint(getBytes(1)),
gasLimit: bytesToBigint(getBytes(2)),
to: getBytes(3),
value: bytesToBigint(getBytes(4)),
data: getBytes(5),
v: bytesToBigint(getBytes(6)),
r: getBytes(7),
s: getBytes(8)
}
}
Block Encoding
Block headers are RLP-encoded lists of fields.Block Header
Copy
Ask AI
import { Rlp } from 'tevm'
interface BlockHeader {
parentHash: Uint8Array // 32 bytes
unclesHash: Uint8Array // 32 bytes
coinbase: Uint8Array // 20 bytes (miner address)
stateRoot: Uint8Array // 32 bytes
transactionsRoot: Uint8Array // 32 bytes
receiptsRoot: Uint8Array // 32 bytes
logsBloom: Uint8Array // 256 bytes
difficulty: bigint
number: bigint
gasLimit: bigint
gasUsed: bigint
timestamp: bigint
extraData: Uint8Array // Variable
mixHash: Uint8Array // 32 bytes
nonce: Uint8Array // 8 bytes
}
function encodeBlockHeader(header: BlockHeader): Uint8Array {
const fields = [
header.parentHash,
header.unclesHash,
header.coinbase,
header.stateRoot,
header.transactionsRoot,
header.receiptsRoot,
header.logsBloom,
bigintToBytes(header.difficulty),
bigintToBytes(header.number),
bigintToBytes(header.gasLimit),
bigintToBytes(header.gasUsed),
bigintToBytes(header.timestamp),
header.extraData,
header.mixHash,
header.nonce
]
return Rlp.encode(fields)
}
// Get block hash
function getBlockHash(header: BlockHeader): Uint8Array {
const encoded = encodeBlockHeader(header)
return keccak256(encoded)
}
Complete Block
Copy
Ask AI
import { Rlp } from 'tevm'
interface Block {
header: BlockHeader
transactions: Transaction[]
uncles: BlockHeader[]
}
function encodeBlock(block: Block): Uint8Array {
// Encode header
const headerFields = [
block.header.parentHash,
block.header.unclesHash,
// ... all header fields
]
// Encode transactions
const txs = block.transactions.map(tx => encodeTransaction(tx))
// Encode uncles
const uncles = block.uncles.map(uncle => {
const uncleFields = [
uncle.parentHash,
uncle.unclesHash,
// ... all uncle fields
]
return Rlp.encode(uncleFields)
})
// Combine into block
return Rlp.encode([
Rlp.encode(headerFields),
Rlp.encode(txs),
Rlp.encode(uncles)
])
}
Receipt Encoding
Transaction receipts use RLP encoding.Receipt Structure
Copy
Ask AI
import { Rlp } from 'tevm'
interface Log {
address: Uint8Array // 20 bytes
topics: Uint8Array[] // Each 32 bytes
data: Uint8Array
}
interface Receipt {
status: bigint // 1 = success, 0 = failure
gasUsed: bigint
logsBloom: Uint8Array // 256 bytes
logs: Log[]
}
function encodeLog(log: Log): Uint8Array {
return Rlp.encode([
log.address,
log.topics,
log.data
])
}
function encodeReceipt(receipt: Receipt): Uint8Array {
const logs = receipt.logs.map(log => encodeLog(log))
return Rlp.encode([
bigintToBytes(receipt.status),
bigintToBytes(receipt.gasUsed),
receipt.logsBloom,
Rlp.encode(logs)
])
}
Receipts Root
Calculate receipts Merkle root:Copy
Ask AI
import { Rlp } from 'tevm'
import { keccak256 } from 'tevm/crypto'
function getReceiptsRoot(receipts: Receipt[]): Uint8Array {
// Encode each receipt
const encoded = receipts.map(r => encodeReceipt(r))
// Build Merkle tree
// (simplified - actual implementation more complex)
const hashes = encoded.map(e => keccak256(e))
// Return root
return calculateMerkleRoot(hashes)
}
State Trie Nodes
State trie nodes use RLP encoding.Leaf Node
Copy
Ask AI
import { Rlp } from 'tevm'
// Leaf node: [encodedPath, value]
function encodeLeafNode(path: Uint8Array, value: Uint8Array): Uint8Array {
return Rlp.encode([path, value])
}
// Example: account state
const accountPath = new Uint8Array([0x20, 0x1f, 0x3a, 0x5b])
const accountValue = Rlp.encode([
bigintToBytes(nonce),
bigintToBytes(balance),
storageRoot,
codeHash
])
const leafNode = encodeLeafNode(accountPath, accountValue)
Branch Node
Copy
Ask AI
import { Rlp } from 'tevm'
// Branch node: [v0, v1, ..., v15, value]
function encodeBranchNode(branches: Uint8Array[], value?: Uint8Array): Uint8Array {
if (branches.length !== 16) {
throw new Error('Branch node must have 16 branches')
}
const fields = [...branches, value || Bytes()]
return Rlp.encode(fields)
}
Network Messages
DevP2P protocol uses RLP for message encoding.Status Message
Copy
Ask AI
import { Rlp } from 'tevm'
interface StatusMessage {
protocolVersion: bigint
networkId: bigint
totalDifficulty: bigint
bestHash: Uint8Array // 32 bytes
genesisHash: Uint8Array // 32 bytes
forkId: {
hash: Uint8Array // 4 bytes
next: bigint
}
}
function encodeStatus(msg: StatusMessage): Uint8Array {
return Rlp.encode([
bigintToBytes(msg.protocolVersion),
bigintToBytes(msg.networkId),
bigintToBytes(msg.totalDifficulty),
msg.bestHash,
msg.genesisHash,
Rlp.encode([
msg.forkId.hash,
bigintToBytes(msg.forkId.next)
])
])
}
GetBlockHeaders
Copy
Ask AI
import { Rlp } from 'tevm'
interface GetBlockHeaders {
requestId: bigint
startBlock: bigint | Uint8Array // Number or hash
maxHeaders: bigint
skip: bigint
reverse: boolean
}
function encodeGetBlockHeaders(msg: GetBlockHeaders): Uint8Array {
const startBlock = typeof msg.startBlock === 'bigint'
? bigintToBytes(msg.startBlock)
: msg.startBlock
return Rlp.encode([
bigintToBytes(msg.requestId),
[
startBlock,
bigintToBytes(msg.maxHeaders),
bigintToBytes(msg.skip),
bigintToBytes(msg.reverse ? 1n : 0n)
]
])
}
Optimistic Patterns
Pre-allocate Buffers
Copy
Ask AI
import { Rlp } from 'tevm'
// Calculate size first
const fields = [field1, field2, field3]
const totalSize = fields.reduce(
(sum, f) => sum + Rlp.getEncodedLength(f),
0
)
// Allocate once
const buffer = new Uint8Array(totalSize + 10) // +10 for list prefix
// Encode into buffer (conceptual)
let offset = 0
for (const field of fields) {
const encoded = Rlp.encode(field)
buffer.set(encoded, offset)
offset += encoded.length
}
Batch Encoding
Copy
Ask AI
import { Rlp } from 'tevm'
// Encode many transactions efficiently
function encodeBatch(transactions: Transaction[]): Uint8Array[] {
return transactions.map(tx => {
const fields = [
bigintToBytes(tx.nonce),
bigintToBytes(tx.gasPrice),
// ... other fields
]
return Rlp.encode(fields)
})
}
// Or encode as single list
function encodeAsBlock(transactions: Transaction[]): Uint8Array {
const encoded = transactions.map(tx => encodeTransaction(tx))
return Rlp.encode(encoded)
}
Caching
Copy
Ask AI
import { Rlp } from 'tevm'
// Cache encoded transactions
const cache = new Map<string, Uint8Array>()
function getEncodedTransaction(tx: Transaction): Uint8Array {
const key = getTxKey(tx)
let encoded = cache.get(key)
if (!encoded) {
encoded = encodeTransaction(tx)
cache.set(key, encoded)
}
return encoded
}
Error Handling
Validation
Copy
Ask AI
import { Rlp } from 'tevm'
function validateAndDecodeTransaction(bytes: Uint8Array): Transaction {
let result
try {
result = Rlp.decode(bytes)
} catch (error) {
throw new Error(`Invalid RLP: ${error.message}`)
}
if (result.remainder.length > 0) {
throw new Error('Extra data after transaction')
}
if (result.data.type !== 'list') {
throw new Error('Transaction must be list')
}
if (result.data.value.length !== 9) {
throw new Error(`Expected 9 fields, got ${result.data.value.length}`)
}
// Validate field types
for (let i = 0; i < 9; i++) {
const field = result.data.value[i]
if (!field || field.type !== 'bytes') {
throw new Error(`Field ${i} must be bytes`)
}
}
return decodeTransaction(bytes)
}
Safe Decoding
Copy
Ask AI
import { Rlp } from 'tevm'
function safeDecodeTransaction(bytes: Uint8Array): Transaction | null {
try {
return validateAndDecodeTransaction(bytes)
} catch (error) {
console.error('Failed to decode transaction:', error)
return null
}
}
// Use in application
const tx = safeDecodeTransaction(receivedBytes)
if (tx) {
processTransaction(tx)
} else {
console.error('Invalid transaction received')
}
Testing Patterns
Round-trip Testing
Copy
Ask AI
import { Rlp } from 'tevm'
test('transaction round-trip encoding', () => {
const original: Transaction = {
nonce: 5n,
gasPrice: 20_000_000_000n,
gasLimit: 21000n,
to: new Uint8Array(20).fill(0x01),
value: 1_000_000_000_000_000_000n,
data: Bytes(),
v: 27n,
r: Bytes32().fill(0x02),
s: Bytes32().fill(0x03)
}
// Encode
const encoded = encodeTransaction(original)
// Decode
const decoded = decodeTransaction(encoded)
// Should match
expect(decoded.nonce).toBe(original.nonce)
expect(decoded.gasPrice).toBe(original.gasPrice)
// ... check all fields
})
Known Vectors
Copy
Ask AI
import { Rlp } from 'tevm'
test('decodes known transaction', () => {
// Known valid transaction from Ethereum
const knownTx = '0xf86c...' // Full hex string
const bytes = hexToBytes(knownTx)
const decoded = decodeTransaction(bytes)
expect(decoded.nonce).toBe(0n)
expect(decoded.gasPrice).toBe(20_000_000_000n)
// ... verify known values
})

