RLP decoding parses compact byte representations back into nested data structures. The decoder performs comprehensive validation to ensure canonical encoding and prevent malformed input attacks.Key Features:
// 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)
import { Rlp } from 'tevm'// List with 60 bytes total payloadconst 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 payloadconst 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
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 decoderconst stream = new Uint8Array([0x01, 0x02, 0x03, 0x04])for (const data of decodeStream(stream)) { console.log('Decoded:', data)}// Outputs each byte as separate data structure
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}
Single bytes < 0x80 must not have a length prefix:
import { Rlp } from 'tevm'// Invalid: single byte 0x7f with prefixconst invalid = new Uint8Array([0x81, 0x7f])try { Rlp.decode(invalid)} catch (error) { // Error: NonCanonicalSize // Single byte < 0x80 should not be prefixed}// Valid: 0x7f encodes as itselfconst valid = new Uint8Array([0x7f])const result = Rlp.decode(valid) // OK
import { Rlp } from 'tevm'// Invalid: 3-byte string using long formconst 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 formconst valid = new Uint8Array([0x83, 1, 2, 3])const result = Rlp.decode(valid) // OK
Decode and re-encode produces identical bytes (for canonical input):
import { Rlp } from 'tevm'// Original dataconst original = new Uint8Array([0x83, 1, 2, 3])// Decodeconst decoded = Rlp.decode(original)// Re-encodeconst reencoded = Rlp.encode(decoded.data)// Should match original (if canonical)console.log(original.every((b, i) => b === reencoded[i])) // true// Compare bytesfunction 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
Non-canonical input will be normalized on re-encoding:
import { Rlp } from 'tevm'// This would fail to decode (non-canonical)// But if we had non-canonical that somehow got through:// const nonCanonical = new Uint8Array([0x81, 0x7f])// After decode and re-encode, becomes canonical:// const canonical = Rlp.encode(decoded.data)// => Uint8Array([0x7f]) // Canonical form