Documentation Index Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt
Use this file to discover all available pages before exploring further.
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
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
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
Error Cause Fix InputTooShortNot enough bytes for declared length Provide complete data InvalidRemainderExtra bytes after value (non-stream) Use stream=true or trim input NonCanonicalSizeNon-minimal length encoding Use canonical encoding LeadingZerosLength has leading zero bytes Remove leading zeros from length InvalidLengthList payload length mismatch Fix list item encodings RecursionDepthExceededNested > 32 levels deep Reduce nesting depth UnexpectedInputInvalid prefix or format Check input is valid RLP
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