Skip to main content

Try it Live

Run RLP examples in the interactive playground

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:
import { Rlp } from 'tevm'

// 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])

// Note: Single byte >= 0x80 still needs prefix
const high = new Uint8Array([0x80])
const encoded = Rlp.encodeBytes(high)
// => Uint8Array([0x81, 0x80])

2. Short String (0-55 bytes)

For strings of 0-55 bytes, prefix with 0x80 + length:
import { Rlp } from 'tevm'

// 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]
import { Rlp } from 'tevm'

// 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
The threshold of 56 bytes is chosen because lengths 0-55 fit in a single prefix byte (0x80-0xb7). Starting at 56, we need an additional byte to encode the length.

Usage Patterns

Transaction Field Encoding

Encode individual transaction fields:
import { Rlp } from 'tevm'

// Encode nonce (typically small number)
const nonce = new Uint8Array([0x00])
const encodedNonce = Rlp.encodeBytes(nonce)
// => Uint8Array([0x00])  (single byte < 0x80)

// Encode address (20 bytes)
const address = new Uint8Array(20).fill(0x01)
const encodedAddress = Rlp.encodeBytes(address)
// => Uint8Array([0x94, ...address])
// 0x94 = 0x80 + 20

// Encode signature r (32 bytes)
const r = Bytes32().fill(0x02)
const encodedR = Rlp.encodeBytes(r)
// => Uint8Array([0xa0, ...r])
// 0xa0 = 0x80 + 32

Contract Bytecode

Encode contract bytecode (often > 56 bytes):
import { Rlp } from 'tevm'

// Contract bytecode (example: 500 bytes)
const bytecode = new Uint8Array(500).fill(0x60)
const encoded = Rlp.encodeBytes(bytecode)
// => Uint8Array([0xb9, 0x01, 0xf4, ...bytecode])
// 0xb9 = 0xb7 + 2 (length needs 2 bytes)
// [0x01, 0xf4] = 500 in big-endian

Keccak256 Hash

Encode 32-byte hash values:
import { Rlp } from 'tevm'
import { keccak256 } from 'tevm/crypto'

const data = new Uint8Array([1, 2, 3, 4])
const hash = keccak256(data)  // 32 bytes

const encoded = Rlp.encodeBytes(hash)
// => Uint8Array([0xa0, ...hash])
// 0xa0 = 0x80 + 32

Empty Data

Encode empty byte arrays:
import { Rlp } from 'tevm'

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

// Use in transaction encoding
const tx = [
  nonce,
  gasPrice,
  gasLimit,
  to,
  value,
  Rlp.encodeBytes(Bytes()),  // Empty data
  v,
  r,
  s
]

Performance

When to Use encodeBytes

Use encodeBytes instead of generic encode when you know the input is bytes (not a list), to skip type dispatch overhead.
import { Rlp } from 'tevm'

// Direct encoding
const encoded = Rlp.encodeBytes(bytes)

// vs generic with dispatch overhead
const encoded = Rlp.encode(bytes)

Pre-calculate Sizes

For repeated encoding, pre-calculate buffer sizes:
import { Rlp } from 'tevm'

// Calculate size first
function calculateEncodedSize(bytes: Uint8Array): number {
  if (bytes.length === 1 && bytes[0]! < 0x80) {
    return 1
  }
  if (bytes.length <= 55) {
    return 1 + bytes.length
  }
  const lengthBytes = Math.ceil(Math.log2(bytes.length + 1) / 8)
  return 1 + lengthBytes + bytes.length
}

const size = calculateEncodedSize(data)
// Allocate buffer of exact size
const buffer = new Uint8Array(size)

Batch Encoding

Encode multiple byte arrays efficiently:
import { Rlp } from 'tevm'

const items = [
  new Uint8Array([1, 2, 3]),
  new Uint8Array([4, 5, 6]),
  new Uint8Array([7, 8, 9])
]

// Encode all at once as list
const encoded = Rlp.encode(items)

// vs encoding individually (less efficient)
const encoded = items.map(item => Rlp.encodeBytes(item))

See Also