A small, dependency-free Rust implementation of RLP (Recursive Length Prefix) — the serialization format used across Ethereum's execution layer for transactions, block headers, receipts, and trie nodes.
The encoder and decoder are verified line-by-line against the official
ethereum/ethereum-rlp Python reference and the
ethereum.org RLP specification,
including the strict canonical-form checks on decode.
- What is RLP?
- Features
- Installation
- Quick Start
- How It Maps to the Spec
- Project Structure
- Testing
- Running the Demo
- License
RLP encodes arbitrarily nested arrays of binary data. It only knows two things:
- byte strings (e.g.
"dog", an address, a hash) - lists of items (which may themselves contain byte strings or lists)
Everything else in Ethereum — integers, booleans, structs — is first reduced to those two primitives and then encoded. RLP says nothing about data types; it only encodes structure and length. The result is a compact, deterministic byte layout.
- Encoding of byte strings and arbitrarily nested lists.
- Decoding into a recursive
RlpItemtree. - Strict, canonical decoding — rejects malformed input exactly as the reference does:
- non-canonical single bytes (
0x00–0x7fwrapped in a length prefix) - non-minimal long-form lengths (leading zero, or long form used for
< 56bytes) - truncated input and trailing bytes
- non-canonical single bytes (
Encodabletrait for ergonomic typed encoding ofu8/u64/u128/usize,bool,&str/String, andVec/slices thereof — with correct big-endian, leading-zero-stripped integer rules (0→0x80).- Zero runtime dependencies (
hexis used only in dev/tests).
This is a Cargo workspace crate. Add it as a path or git dependency:
[dependencies]
rust-light-rlp = { path = "crates/light-rlp" }The package is named rust-light-rlp; the library is imported as light_rlp:
use light_rlp::{decode, encode, Encodable, RlpItem};use light_rlp::{decode, encode, RlpItem};
let item = RlpItem::Bytes(b"dog".to_vec());
let encoded = encode(&item);
assert_eq!(encoded, vec![0x83, 0x64, 0x6f, 0x67]);
assert_eq!(decode(&encoded).unwrap(), item);use light_rlp::{decode, encode, RlpItem};
let item = RlpItem::List(vec![
RlpItem::Bytes(b"cat".to_vec()),
RlpItem::Bytes(b"dog".to_vec()),
]);
let encoded = encode(&item);
assert_eq!(encoded, vec![0xc8, 0x83, 0x63, 0x61, 0x74, 0x83, 0x64, 0x6f, 0x67]);
assert_eq!(decode(&encoded).unwrap(), item);use light_rlp::Encodable;
assert_eq!(0u8.rlp_encode(), vec![0x80]); // integer 0 -> empty string
assert_eq!(1024u64.rlp_encode(), vec![0x82, 0x04, 0x00]); // big-endian, no leading zeros
assert_eq!(true.rlp_encode(), vec![0x01]);
assert_eq!("dog".rlp_encode(), vec![0x83, 0x64, 0x6f, 0x67]);Every rule in the Python reference (ethereum/ethereum-rlp, src/ethereum_rlp/rlp.py) has a
direct counterpart in this crate.
| RLP rule | Python reference | This crate |
|---|---|---|
Single byte [0x00, 0x7f] is its own encoding |
encode_bytes (len == 1 and b[0] < 0x80) |
encode/byte_string.rs |
String 0–55 bytes → 0x80 + len, then data |
encode_bytes (len < 0x38) |
encode/byte_string.rs |
String > 55 bytes → 0xb7 + len(len), big-endian len, data |
encode_bytes (else) |
encode/byte_string.rs |
List payload 0–55 bytes → 0xc0 + len, then payload |
encode_sequence (len < 0x38) |
encode/list.rs |
List payload > 55 bytes → 0xf7 + len(len), big-endian len, payload |
encode_sequence (else) |
encode/list.rs |
Integers: big-endian, no leading zeros (0 → empty string) |
encode (Uint → to_be_bytes) |
traits.rs |
true → 0x01, false → empty string |
encode (bool) |
traits.rs |
| Validation | Python reference | This crate |
|---|---|---|
| Prefix classification by first byte | decode / decode_item_length |
decode/dispatch.rs |
Reject single byte < 0x80 wrapped in length prefix |
decode_to_bytes (len == 1 and raw[0] < 0x80) |
decode/short.rs → NonCanonicalSingleByte |
| Reject leading-zero long length | decode_* (encoded[1] == 0) |
decode/long.rs → NonCanonicalLength |
Reject long form used for length < 56 |
decode_* (len < 0x38) |
decode/long.rs → NonCanonicalLength |
| Reject truncated / out-of-bounds input | decode_* (>= len(...)) |
LengthOutOfBounds / InputTooShort |
| Reject trailing bytes after the top-level item | decode_* (< len(...)) |
decode/mod.rs → TrailingBytes |
The error variants live in types.rs (RlpError).
rust-rlp-ssz/
├── Cargo.toml # workspace manifest
├── README.md
├── LICENSE
└── crates/
└── light-rlp/
├── Cargo.toml # package: rust-light-rlp (lib: light_rlp)
├── README.md
├── examples/
│ └── demo.rs # runnable encode/decode showcase
├── src/
│ ├── lib.rs # public re-exports
│ ├── types.rs # RlpItem, RlpError
│ ├── traits.rs # Encodable trait + typed impls
│ ├── encode/
│ │ ├── mod.rs # encode() dispatch
│ │ ├── byte_string.rs
│ │ └── list.rs
│ └── decode/
│ ├── mod.rs # decode() entry + trailing-byte check
│ ├── dispatch.rs # first-byte classification
│ ├── short.rs # short string / short list
│ └── long.rs # long string / long list + length parsing
└── tests/
└── spec_vectors.rs # spec conformance vectors
The suite covers unit tests in every module plus end-to-end spec vectors (47 tests total).
# Run everything
cargo test
# Just the spec-conformance vectors
cargo test --test spec_vectors
# A single test by name
cargo test encode_list_cat_dog -- --exactA self-contained example prints encode/decode roundtrips as hex and shows the strict decoder rejecting non-canonical input:
cargo run --example demoSample output:
=== rust-light-rlp :: encoding ===
string "dog" -> 0x83646f67
list ["cat", "dog"] -> 0xc88363617483646f67
u64 1024 -> 0x820400
bool true -> 0x01
=== strict decode rejects non-canonical input ===
0x8100 -> Err(NonCanonicalSingleByte) (0x00 wrapped, should be bare)
0xb837 -> Err(NonCanonicalLength) (long form for 55 bytes)
0x8080 -> Err(TrailingBytes) (trailing bytes)
Released under the MIT License. © 2026 ibrahimijai.