Skip to main content

Try it Live

Run RLP examples in the interactive playground

    Decoding Algorithm

    RLP decoder uses the first byte (prefix) to determine data type and length:

    Prefix Ranges

    // Prefix byte determines encoding type:
    // 0x00-0x7f: Single byte (value itself)
    // 0x80-0xb7: Short string (0-55 bytes)
    // 0xb8-0xbf: Long string (56+ bytes)
    // 0xc0-0xf7: Short list (0-55 bytes total)
    // 0xf8-0xff: Long list (56+ bytes total)
    

    Single Byte (0x00-0x7f)

    Bytes with value < 0x80 encode as themselves:
    import { Rlp } from 'tevm'
    
    const encoded = new Uint8Array([0x7f])
    const result = Rlp.decode(encoded)
    // => { data: { type: 'bytes', value: Uint8Array([0x7f]) }, remainder: Uint8Array([]) }
    
    const zero = new Uint8Array([0x00])
    const result = Rlp.decode(zero)
    // => { data: { type: 'bytes', value: Uint8Array([0x00]) }, remainder: Uint8Array([]) }
    

    Short String (0x80-0xb7)

    Length encoded in prefix: length = prefix - 0x80
    import { Rlp } from 'tevm'
    
    // Empty string
    const empty = new Uint8Array([0x80])
    const result = Rlp.decode(empty)
    // => { data: { type: 'bytes', value: Uint8Array([]) }, remainder: Uint8Array([]) }
    
    // 3-byte string
    const bytes = new Uint8Array([0x83, 1, 2, 3])
    const result = Rlp.decode(bytes)
    // 0x83 - 0x80 = 3 bytes
    // => { data: { type: 'bytes', value: Uint8Array([1, 2, 3]) }, remainder: Uint8Array([]) }
    

    Long String (0xb8-0xbf)

    Length-of-length encoding: lengthOfLength = prefix - 0xb7
    import { Rlp } from 'tevm'
    
    // 56-byte string (minimum long form)
    const min = new Uint8Array([0xb8, 56, ...Array(56).fill(0x42)])
    const result = Rlp.decode(min)
    // 0xb8 - 0xb7 = 1 (length needs 1 byte)
    // Next byte: 56 = actual length
    
    // 256-byte string (length needs 2 bytes)
    const large = new Uint8Array([0xb9, 0x01, 0x00, ...Array(256).fill(0x42)])
    const result = Rlp.decode(large)
    // 0xb9 - 0xb7 = 2 (length needs 2 bytes)
    // Next 2 bytes: [0x01, 0x00] = 256
    

    Short List (0xc0-0xf7)

    Total payload length in prefix: length = prefix - 0xc0
    import { Rlp } from 'tevm'
    
    // Empty list
    const empty = new Uint8Array([0xc0])
    const result = Rlp.decode(empty)
    // => { data: { type: 'list', value: [] }, remainder: Uint8Array([]) }
    
    // List with 2 single bytes
    const list = new Uint8Array([0xc2, 0x01, 0x02])
    const result = Rlp.decode(list)
    // 0xc2 - 0xc0 = 2 bytes total payload
    // => {
    //   data: {
    //     type: 'list',
    //     value: [
    //       { type: 'bytes', value: Uint8Array([1]) },
    //       { type: 'bytes', value: Uint8Array([2]) }
    //     ]
    //   },
    //   remainder: Uint8Array([])
    // }
    

    Long List (0xf8-0xff)

    Length-of-length encoding: lengthOfLength = prefix - 0xf7
    import { Rlp } from 'tevm'
    
    // List with 60 bytes total payload
    const items = Array(30).fill([0x01, 0x02]).flat()
    const long = new Uint8Array([0xf8, 60, ...items])
    const result = Rlp.decode(long)
    // 0xf8 - 0xf7 = 1 (length needs 1 byte)
    // Next byte: 60 = payload length
    
    // List with 256 bytes total payload
    const large = new Uint8Array([0xf9, 0x01, 0x00, ...Array(256).fill(0x01)])
    const result = Rlp.decode(large)
    // 0xf9 - 0xf7 = 2 (length needs 2 bytes)
    // Next 2 bytes: [0x01, 0x00] = 256
    

    Usage Patterns

    Extract Transaction Data

    Decode transaction bytes and extract fields:
    import { Rlp } from 'tevm'
    
    // Legacy transaction RLP
    const txBytes = new Uint8Array([...])  // RLP-encoded transaction
    const result = Rlp.decode(txBytes)
    
    if (result.data.type === 'list') {
      const [nonce, gasPrice, gas, to, value, data, v, r, s] = result.data.value
    
      // Each field is a bytes data structure
      if (nonce.type === 'bytes') {
        console.log('Nonce:', nonce.value)
      }
      if (to.type === 'bytes') {
        console.log('To:', to.value)
      }
    }
    

    Stream Decoding

    Decode multiple RLP values from a stream:
    import { Rlp } from 'tevm'
    
    function* decodeStream(bytes: Uint8Array) {
      let remainder = bytes
    
      while (remainder.length > 0) {
        const result = Rlp.decode(remainder, true)
        yield result.data
        remainder = result.remainder
      }
    }
    
    // Use stream decoder
    const stream = new Uint8Array([0x01, 0x02, 0x03, 0x04])
    for (const data of decodeStream(stream)) {
      console.log('Decoded:', data)
    }
    // Outputs each byte as separate data structure
    

    Validate and Extract

    Decode with validation and type checking:
    import { Rlp } from 'tevm'
    
    function decodeTransaction(bytes: Uint8Array) {
      const result = Rlp.decode(bytes)
    
      // Must be a list
      if (result.data.type !== 'list') {
        throw new Error('Transaction must be RLP list')
      }
    
      // Must have 9 fields (legacy tx)
      if (result.data.value.length !== 9) {
        throw new Error('Invalid transaction field count')
      }
    
      // Must consume all bytes
      if (result.remainder.length > 0) {
        throw new Error('Extra data after transaction')
      }
    
      return result.data
    }
    

    Recursive Flattening

    Flatten nested lists to extract all byte values:
    import { Rlp } from 'tevm'
    
    const nested = new Uint8Array([...])  // Deeply nested RLP
    const result = Rlp.decode(nested)
    
    // Flatten recursively extracts all bytes
    const allBytes = Rlp.flatten(result.data)
    // => Array of { type: 'bytes', value: Uint8Array }
    
    for (const item of allBytes) {
      console.log('Bytes:', item.value)
    }
    

    Canonical Validation

    RLP decoder enforces canonical encoding rules to prevent malleability:

    Non-canonical Single Byte

    Single bytes < 0x80 must not have a length prefix:
    import { Rlp } from 'tevm'
    
    // Invalid: single byte 0x7f with prefix
    const invalid = new Uint8Array([0x81, 0x7f])
    try {
      Rlp.decode(invalid)
    } catch (error) {
      // Error: NonCanonicalSize
      // Single byte < 0x80 should not be prefixed
    }
    
    // Valid: 0x7f encodes as itself
    const valid = new Uint8Array([0x7f])
    const result = Rlp.decode(valid)  // OK
    

    Non-canonical Short Form

    Strings < 56 bytes must use short form:
    import { Rlp } from 'tevm'
    
    // Invalid: 3-byte string using long form
    const invalid = new Uint8Array([0xb8, 0x03, 1, 2, 3])
    try {
      Rlp.decode(invalid)
    } catch (error) {
      // Error: NonCanonicalSize
      // String < 56 bytes should use short form
    }
    
    // Valid: 3-byte string in short form
    const valid = new Uint8Array([0x83, 1, 2, 3])
    const result = Rlp.decode(valid)  // OK
    

    Leading Zeros

    Length encodings must not have leading zeros:
    import { Rlp } from 'tevm'
    
    // Invalid: length with leading zero
    const invalid = new Uint8Array([0xb9, 0x00, 0x38, ...Array(56).fill(0x42)])
    try {
      Rlp.decode(invalid)
    } catch (error) {
      // Error: LeadingZeros
      // Length encoding has leading zeros
    }
    
    // Valid: minimal length encoding
    const valid = new Uint8Array([0xb8, 56, ...Array(56).fill(0x42)])
    const result = Rlp.decode(valid)  // OK
    

    Length Mismatches

    Declared length must match actual data:
    import { Rlp } from 'tevm'
    
    // Invalid: declared 5 bytes but only 3 provided
    const invalid = new Uint8Array([0x85, 1, 2, 3])
    try {
      Rlp.decode(invalid)
    } catch (error) {
      // Error: InputTooShort
      // Expected 6 bytes, got 4
    }
    
    // Invalid: list length mismatch
    const invalid = new Uint8Array([0xc5, 0x01, 0x02])  // Says 5 bytes but only 2
    try {
      Rlp.decode(invalid)
    } catch (error) {
      // Error: InputTooShort
    }
    

    Error Handling

    Comprehensive error handling for malformed input:
    import { Rlp } from 'tevm'
    
    // Empty input
    try {
      Rlp.decode(Bytes())
    } catch (error) {
      console.error('InputTooShort: Cannot decode empty input')
    }
    
    // Extra data (non-stream mode)
    try {
      const bytes = new Uint8Array([0x01, 0x02])
      Rlp.decode(bytes, false)
    } catch (error) {
      console.error('InvalidRemainder: Extra data after decoded value: 1 bytes')
    }
    
    // Recursion depth exceeded
    try {
      // Create deeply nested structure (> 32 levels)
      let nested = new Uint8Array([0xc0])  // Empty list
      for (let i = 0; i < 40; i++) {
        nested = new Uint8Array([0xc1, ...nested])  // Wrap in list
      }
      Rlp.decode(nested)
    } catch (error) {
      console.error('RecursionDepthExceeded: Maximum recursion depth 32 exceeded')
    }
    
    // Incomplete data
    try {
      const incomplete = new Uint8Array([0x83, 1, 2])  // Says 3 bytes, only has 2
      Rlp.decode(incomplete)
    } catch (error) {
      console.error('InputTooShort: Expected 4 bytes, got 3')
    }
    

    Error Types Reference

    ErrorCauseFix
    InputTooShortNot enough bytes for declared lengthProvide complete data
    InvalidRemainderExtra bytes after value (non-stream)Use stream=true or trim input
    NonCanonicalSizeNon-minimal length encodingUse canonical encoding
    LeadingZerosLength has leading zero bytesRemove leading zeros from length
    InvalidLengthList payload length mismatchFix list item encodings
    RecursionDepthExceededNested > 32 levels deepReduce nesting depth
    UnexpectedInputInvalid prefix or formatCheck input is valid RLP

    Performance

    Depth Limiting

    Maximum recursion depth is 32 to prevent stack overflow:
    import { Rlp } from 'tevm'
    
    // This is fine (depth = 3)
    const shallow = [[[new Uint8Array([1])]]]
    const encoded = Rlp.encode(shallow)
    const decoded = Rlp.decode(encoded)  // OK
    
    // This will fail (depth > 32)
    const deep = Array(40).fill(null).reduce(
      (acc) => [acc],
      new Uint8Array([1])
    )
    const encoded = Rlp.encode(deep)
    try {
      Rlp.decode(encoded)
    } catch (error) {
      // RecursionDepthExceeded
    }
    

    Stream Mode Efficiency

    Use stream mode to avoid re-parsing when decoding multiple values:
    import { Rlp } from 'tevm'
    
    // Inefficient: decode + re-decode remainder
    const data = new Uint8Array([0x01, 0x02, 0x03])
    const first = Rlp.decode(data.slice(0, 1))
    const second = Rlp.decode(data.slice(1, 2))
    const third = Rlp.decode(data.slice(2, 3))
    
    // Efficient: stream mode
    let remainder = data
    const values = []
    while (remainder.length > 0) {
      const result = Rlp.decode(remainder, true)
      values.push(result.data)
      remainder = result.remainder
    }
    

    Round-trip Encoding

    Decode and re-encode produces identical bytes (for canonical input):
    import { Rlp } from 'tevm'
    
    // Original data
    const original = new Uint8Array([0x83, 1, 2, 3])
    
    // Decode
    const decoded = Rlp.decode(original)
    
    // Re-encode
    const reencoded = Rlp.encode(decoded.data)
    
    // Should match original (if canonical)
    function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
      if (a.length !== b.length) return false
      return a.every((byte, i) => byte === b[i])
    }
    
    console.log(bytesEqual(original, reencoded))  // true
    
    Validation ensures malformed RLP can’t cause issues downstream. The performance cost is negligible compared to security benefits.

    See Also