Skip to main content

Try it Live

Run RLP examples in the interactive playground

RLP Encoding

Methods for encoding bytes, lists, and nested structures into RLP format.

Overview

RLP encoding converts arbitrary nested data structures into a compact byte representation. The encoder handles three main cases:
  • Bytes - Raw byte arrays (strings in RLP terminology)
  • Lists - Arrays of encodable items
  • Nested - Lists containing other lists
All encoding operations are deterministic and canonical, ensuring the same input always produces identical output.

RLP Encoding Tree Diagram

RLP encodes nested structures hierarchically:
Input Data Structure:
┌─────────────────────────────┐
│ List                         │
│ ├─ Bytes: [0x7f]           │
│ ├─ Bytes: [0x01, 0x02]     │
│ └─ List                      │
│    ├─ Bytes: [0x03]        │
│    └─ Bytes: [0x04, 0x05]  │
└─────────────────────────────┘

    RLP Encoding

Output RLP Bytes:
┌──────────────────────────────────────────┐
│ 0xc6               (list prefix + length) │
│   0x7f             (single byte [0x7f])  │
│   0x82 0x01 0x02   (bytes: length + data)│
│   0xc4             (nested list prefix)  │
│     0x03           (single byte [0x03])  │
│     0x82 0x04 0x05 (bytes: length + data)│
└──────────────────────────────────────────┘

RLP Encoding Algorithm

  1. Determine Data Type
    • Is input a single byte < 0x80? Encode as-is
    • Is input bytes? Encode with length prefix
    • Is input array? Encode each element recursively
  2. Encode Bytes
    • If length = 1 and byte < 0x80: no prefix
    • If length ≤ 55: prefix = 0x80 + length
    • If length > 55: prefix = 0xb7 + (length of length) + length
  3. Encode Lists
    • Recursively encode each element
    • Concatenate all encoded elements
    • Calculate total payload length
    • If length ≤ 55: prefix = 0xc0 + length
    • If length > 55: prefix = 0xf7 + (length of length) + length
  4. Return RLP Bytes
    • Prefix + (length if needed) + data

encode

General-purpose encoding method that accepts bytes, lists, or RLP data structures.

Signature

function encode(data: Encodable): Uint8Array

type Encodable =
  | Uint8Array
  | BrandedRlp
  | Array<Uint8Array | BrandedRlp | any>
Parameters:
  • data: Encodable - Data to encode (Uint8Array, RlpData, or array)
Returns:
  • Uint8Array - RLP-encoded bytes
Throws:
  • Error('UnexpectedInput') - Invalid encodable data type
Source: encode.js:38-59

Usage

import { Rlp } from 'tevm'

// Encode bytes
const bytes = new Uint8Array([1, 2, 3])
const encoded = Rlp.encode(bytes)
// => Uint8Array([0x83, 1, 2, 3])

// Encode list
const list = [new Uint8Array([1]), new Uint8Array([2])]
const encoded = Rlp.encode(list)
// => Uint8Array([0xc4, 0x01, 0x02])

// Encode nested structures
const nested = [
  new Uint8Array([1]),
  [new Uint8Array([2]), new Uint8Array([3])]
]
const encoded = Rlp.encode(nested)

// Encode RLP data structure
const data = Rlp(new Uint8Array([1, 2, 3]))
const encoded = Rlp.encode(data)

// Instance method
const rlpData = Rlp(new Uint8Array([1, 2, 3]))
const encoded = rlpData.encode()

Algorithm

The encode method dispatches to specialized encoders based on input type:
  1. Uint8Array → Uses encodeBytes for string encoding
  2. BrandedRlp (bytes) → Uses encodeBytes on value
  3. BrandedRlp (list) → Uses encodeList on value
  4. Array → Uses encodeList for list encoding
This automatic dispatch simplifies encoding of complex structures:
// These are equivalent
const manual = Rlp.encodeList([
  Rlp.encodeBytes(new Uint8Array([1])),
  Rlp.encodeBytes(new Uint8Array([2]))
])

const automatic = Rlp.encode([
  new Uint8Array([1]),
  new Uint8Array([2])
])

encodeBytes

Encodes a byte array according to RLP string rules.

Signature

function encodeBytes(bytes: Uint8Array): Uint8Array
Parameters:
  • bytes: Uint8Array - Byte array to encode
Returns:
  • Uint8Array - RLP-encoded bytes
Source: encodeBytes.js:32-53

Usage

import { Rlp } from 'tevm'

// Single byte < 0x80 (no prefix)
const single = new Uint8Array([0x7f])
const encoded = Rlp.encodeBytes(single)
// => Uint8Array([0x7f])

// Short string (0-55 bytes)
const short = new Uint8Array([1, 2, 3])
const encoded = Rlp.encodeBytes(short)
// => Uint8Array([0x83, 1, 2, 3])
// 0x83 = 0x80 + 3

// Empty string
const empty = Bytes()
const encoded = Rlp.encodeBytes(empty)
// => Uint8Array([0x80])

// Long string (56+ bytes)
const long = new Uint8Array(60).fill(0x42)
const encoded = Rlp.encodeBytes(long)
// => Uint8Array([0xb8, 60, ...long])
// 0xb8 = 0xb7 + 1 (length needs 1 byte)

String Encoding Rules

RLP string encoding has three cases based on byte length:

1. Single Byte < 0x80

For a single byte with value less than 0x80 (128), the byte encodes as itself with no prefix:
// Byte value 0x7f (127)
const input = new Uint8Array([0x7f])
const encoded = Rlp.encodeBytes(input)
// => Uint8Array([0x7f])

// Byte value 0x00
const zero = new Uint8Array([0x00])
const encoded = Rlp.encodeBytes(zero)
// => Uint8Array([0x00])

2. Short String (0-55 bytes)

For strings of 0-55 bytes, prefix with 0x80 + length:
// Empty string
const empty = Bytes()
const encoded = Rlp.encodeBytes(empty)
// => Uint8Array([0x80])
// 0x80 = 0x80 + 0

// 3 bytes
const bytes = new Uint8Array([1, 2, 3])
const encoded = Rlp.encodeBytes(bytes)
// => Uint8Array([0x83, 1, 2, 3])
// 0x83 = 0x80 + 3

// 55 bytes (maximum for short form)
const max = new Uint8Array(55).fill(0x42)
const encoded = Rlp.encodeBytes(max)
// => Uint8Array([0xb7, ...max])
// 0xb7 = 0x80 + 55

3. Long String (56+ bytes)

For strings of 56+ bytes, use long form: [0xb7 + length_of_length, ...length_bytes, ...bytes]
// 56 bytes (minimum for long form)
const min = new Uint8Array(56).fill(0x42)
const encoded = Rlp.encodeBytes(min)
// => Uint8Array([0xb8, 56, ...min])
// 0xb8 = 0xb7 + 1 (length needs 1 byte)
// 56 = length value

// 256 bytes (length needs 2 bytes)
const large = new Uint8Array(256).fill(0x42)
const encoded = Rlp.encodeBytes(large)
// => Uint8Array([0xb9, 0x01, 0x00, ...large])
// 0xb9 = 0xb7 + 2 (length needs 2 bytes)
// [0x01, 0x00] = 256 in big-endian

// 65536 bytes (length needs 3 bytes)
const huge = new Uint8Array(65536).fill(0x42)
const encoded = Rlp.encodeBytes(huge)
// => Uint8Array([0xba, 0x01, 0x00, 0x00, ...huge])
// 0xba = 0xb7 + 3

encodeList

Encodes a list of RLP-encodable items.

Signature

function encodeList(items: Encodable[]): Uint8Array

type Encodable =
  | Uint8Array
  | BrandedRlp
  | Array<any>
Parameters:
  • items: Encodable[] - Array of items to encode
Returns:
  • Uint8Array - RLP-encoded list
Source: encodeList.js:33-63

Usage

import { Rlp } from 'tevm'

// Empty list
const empty = []
const encoded = Rlp.encodeList(empty)
// => Uint8Array([0xc0])

// Simple list
const list = [new Uint8Array([1]), new Uint8Array([2])]
const encoded = Rlp.encodeList(list)
// => Uint8Array([0xc4, 0x01, 0x02])
// 0xc4 = 0xc0 + 4 (2 items, each 1 byte encoded)

// Nested list
const nested = [
  new Uint8Array([1]),
  [new Uint8Array([2]), new Uint8Array([3])]
]
const encoded = Rlp.encodeList(nested)

// List with various types
const mixed = [
  new Uint8Array([0x7f]),           // Single byte
  new Uint8Array([1, 2, 3]),         // Short string
  [],                                 // Empty list
  [new Uint8Array([4])]              // Nested list
]
const encoded = Rlp.encodeList(mixed)

List Encoding Rules

RLP list encoding has two cases based on total payload length:

1. Short List (< 56 bytes total)

For lists with total payload < 56 bytes, prefix with 0xc0 + total_length:
// Empty list
const empty = []
const encoded = Rlp.encodeList(empty)
// => Uint8Array([0xc0])
// 0xc0 = 0xc0 + 0

// List with 2 single bytes
const list = [new Uint8Array([0x01]), new Uint8Array([0x02])]
const encoded = Rlp.encodeList(list)
// => Uint8Array([0xc2, 0x01, 0x02])
// 0xc2 = 0xc0 + 2 (each item is 1 byte)

// List with short strings
const strings = [
  new Uint8Array([1, 2]),     // Encodes as [0x82, 1, 2] (3 bytes)
  new Uint8Array([3, 4, 5])   // Encodes as [0x83, 3, 4, 5] (4 bytes)
]
const encoded = Rlp.encodeList(strings)
// => Uint8Array([0xc7, 0x82, 1, 2, 0x83, 3, 4, 5])
// 0xc7 = 0xc0 + 7 (3 + 4 bytes)

2. Long List (56+ bytes total)

For lists with total payload >= 56 bytes, use long form: [0xf7 + length_of_length, ...length_bytes, ...encoded_items]
// Create list with 60 bytes total payload
const items = Array({ length: 30 }, () => new Uint8Array([0x01, 0x02]))
const encoded = Rlp.encodeList(items)
// First bytes: [0xf8, 60, ...]
// 0xf8 = 0xf7 + 1 (length needs 1 byte)
// 60 = total payload length

// Large list with 256 bytes total
const large = Array({ length: 128 }, () => new Uint8Array([0x01, 0x02]))
const encoded = Rlp.encodeList(large)
// First bytes: [0xf9, 0x01, 0x00, ...]
// 0xf9 = 0xf7 + 2 (length needs 2 bytes)
// [0x01, 0x00] = 256 in big-endian

Algorithm Details

The encodeList implementation:
  1. Encode each item using encode() (dispatches to appropriate encoder)
  2. Calculate total length by summing encoded item lengths
  3. Choose encoding based on total length:
    • < 56 bytes: Short form with single prefix byte
    • >= 56 bytes: Long form with length-of-length encoding
  4. Concatenate prefix + encoded items into result buffer
// Conceptual implementation
function encodeList(items) {
  // Step 1: Encode each item
  const encodedItems = items.map(item => encode(item))

  // Step 2: Calculate total length
  const totalLength = encodedItems.reduce((sum, item) => sum + item.length, 0)

  // Step 3: Choose encoding
  if (totalLength < 56) {
    // Short list: [0xc0 + length, ...items]
    return concat([0xc0 + totalLength], ...encodedItems)
  } else {
    // Long list: [0xf7 + len_of_len, ...len_bytes, ...items]
    const lengthBytes = encodeLength(totalLength)
    return concat([0xf7 + lengthBytes.length], lengthBytes, ...encodedItems)
  }
}

Encoding Examples

Transaction Encoding

Ethereum transactions use RLP encoding for signing and broadcasting:
import { Rlp } from 'tevm'

// Legacy transaction: [nonce, gasPrice, gas, to, value, data, v, r, s]
const txData = [
  new Uint8Array([0x00]),                    // nonce
  new Uint8Array([0x04, 0xa8, 0x17, 0xc8]),  // gasPrice
  new Uint8Array([0x52, 0x08]),               // gas
  new Uint8Array(20).fill(0x01),              // to (address)
  new Uint8Array([0x00]),                    // value
  Bytes(),                         // data
  new Uint8Array([0x1b]),                    // v
  Bytes32().fill(0x02),              // r
  Bytes32().fill(0x03)               // s
]

const encoded = Rlp.encodeList(txData)
// Ready for broadcast or signature verification

Block Header Encoding

Block headers are RLP-encoded lists:
import { Rlp } from 'tevm'

// Simplified block header
const header = [
  Bytes32().fill(0x01),  // parentHash
  Bytes32().fill(0x02),  // uncleHash
  new Uint8Array(20).fill(0x03),  // coinbase
  Bytes32().fill(0x04),  // stateRoot
  Bytes32().fill(0x05),  // transactionsRoot
  Bytes32().fill(0x06),  // receiptsRoot
  new Uint8Array(256).fill(0x00), // logsBloom
  new Uint8Array([0x01]),         // difficulty
  new Uint8Array([0x01]),         // number
  new Uint8Array([0x5f, 0x5e, 0x100]), // gasLimit
  new Uint8Array([0x00]),         // gasUsed
  Bytes4(),              // timestamp
  Bytes()              // extraData
]

const encoded = Rlp.encodeList(header)

Nested Data Structures

RLP handles arbitrary nesting:
import { Rlp } from 'tevm'

// Deeply nested structure
const nested = [
  new Uint8Array([1]),
  [
    new Uint8Array([2]),
    [
      new Uint8Array([3]),
      [
        new Uint8Array([4])
      ]
    ]
  ]
]

const encoded = Rlp.encode(nested)

// List of lists
const matrix = [
  [new Uint8Array([1, 2, 3])],
  [new Uint8Array([4, 5, 6])],
  [new Uint8Array([7, 8, 9])]
]

const encoded = Rlp.encode(matrix)

Performance Considerations

Pre-sizing Buffers

For better performance when encoding many items, pre-calculate total size:
import { Rlp } from 'tevm'

// Calculate size without encoding
const items = [
  new Uint8Array([1, 2, 3]),
  new Uint8Array([4, 5, 6])
]

const size = Rlp.getEncodedLength(items)
console.log(`Will need ${size} bytes`)

// Then encode
const encoded = Rlp.encode(items)

Performance

Use specific encoders when type is known for better performance:
import { Rlp } from 'tevm'

// Generic encoder with type dispatch
const bytesEncoded = Rlp.encodeBytes(new Uint8Array([1, 2, 3]))
const listEncoded = Rlp.encodeList([bytes1, bytes2])

Avoiding Re-encoding

Cache encoded results when encoding the same data multiple times:
import { Rlp } from 'tevm'

const data = new Uint8Array([1, 2, 3])
const encoded = Rlp.encode(data)

// Reuse encoded result
for (let i = 0; i < 1000; i++) {
  // Use cached 'encoded' instead of re-encoding
  processEncoded(encoded)
}