Skip to content

lambdaclass/librlp

librlp

A fast, modular RLP (Recursive Length Prefix) encoding and decoding library for Ethereum, written in Rust.

Built for no_std from day one — runs in zkVMs, WASM, and embedded targets. Up to 17x faster than ethrex-rlp and up to 1.4x faster than alloy-rlp on struct encode/decode. Fuzz-tested against both reference implementations.

Performance

Benchmarked against alloy-rlp v0.3, ethrex-rlp v9, and parity-rlp v0.6, cargo bench --release.

Apple M3 Max

Encode

Type librlp alloy-rlp ethrex-rlp parity-rlp vs alloy vs ethrex
SimpleStruct (2 hashes + u64) 17.0 ns 17.9 ns 155 ns 230 ns 1.05x 9.1x
Transaction (9 fields) 30.2 ns 32.2 ns 198 ns 1.07x 6.6x
BlockHeader (16 fields) 50.3 ns 52.8 ns 312 ns 1.05x 6.2x
Large payload (24 KB) 361 ns 318 ns 337 ns 0.88x 0.93x
Batch 1000 transactions 30.5 µs 32.2 µs 200 µs 1.06x 6.6x
Batch 1000 (buf reuse) 16.7 µs N/A N/A

Decode

Type librlp alloy-rlp ethrex-rlp parity-rlp vs alloy vs ethrex
SimpleStruct 40.8 ns 35.4 ns 59.4 ns 89.2 ns 0.87x 1.5x
Transaction 77.4 ns 95.9 ns 181 ns 1.24x 2.3x
BlockHeader 185 ns 229 ns 366 ns 1.24x 2.0x

Primitives

Type Op librlp alloy-rlp ethrex-rlp parity-rlp
u64 encode 16.1 ns 16.0 ns 21.0 ns 63.3 ns
u64 decode 3.9 ns 4.2 ns 6.2 ns
[u8; 32] encode 15.8 ns 15.6 ns 44.8 ns
[u8; 32] decode 11.3 ns 9.7 ns 11.2 ns
Vec<u8> 1 KB encode 34.8 ns 32.6 ns 64.3 ns
Vec<u8> 1 KB decode 36.1 ns 41.1 ns 45.1 ns

Lazy Decoding (RlpRef)

Benchmark Time Notes
Single field access 7.8 ns Parse header + skip to field
Full struct decode (librlp) 36.8 ns For comparison
Full struct decode (alloy) 44.4 ns For comparison
Iterate all items 58.5 ns Header-only scan

Nested Lists

Type librlp alloy-rlp vs alloy
Vec<Transaction> (100 items) encode 2.16 µs 2.68 µs 1.24x

AMD Ryzen 9 9950X3D

Encode

Type librlp alloy-rlp ethrex-rlp parity-rlp vs alloy vs ethrex
SimpleStruct (2 hashes + u64) 7.3 ns 9.5 ns 121 ns 189 ns 1.30x 16.6x
Transaction (9 fields) 17.5 ns 20.9 ns 154 ns 1.19x 8.8x
BlockHeader (16 fields) 21.2 ns 28.4 ns 248 ns 1.34x 11.7x
Large payload (24 KB) 134 ns 128 ns 142 ns 0.96x 1.06x
Batch 1000 transactions 16.6 µs 20.4 µs 165 µs 1.23x 10.0x
Batch 1000 (buf reuse) 12.2 µs N/A N/A

Decode

Type librlp alloy-rlp ethrex-rlp parity-rlp vs alloy vs ethrex
SimpleStruct 28.8 ns 22.9 ns 56.8 ns 84.9 ns 0.80x 2.0x
Transaction 62.3 ns 85.9 ns 171 ns 1.38x 2.7x
BlockHeader 117 ns 164 ns 349 ns 1.40x 3.0x

Primitives

Type Op librlp alloy-rlp ethrex-rlp parity-rlp
u64 encode 6.6 ns 6.4 ns 12.4 ns 28.9 ns
u64 decode 1.8 ns 3.7 ns 5.7 ns
[u8; 32] encode 4.5 ns 4.6 ns 31.6 ns
[u8; 32] decode 10.5 ns 9.1 ns 10.9 ns
Vec<u8> 1 KB encode 14.3 ns 14.4 ns 47.7 ns
Vec<u8> 1 KB decode 12.5 ns 21.8 ns 26.0 ns

Lazy Decoding (RlpRef)

Benchmark Time Notes
Single field access 11.9 ns Parse header + skip to field
Full struct decode (librlp) 31.5 ns For comparison
Full struct decode (alloy) 64.0 ns For comparison
Iterate all items 47.9 ns Header-only scan

Nested Lists

Type librlp alloy-rlp vs alloy
Vec<Transaction> (100 items) encode 1.63 µs 1.80 µs 1.11x
How
  • Single-pass RlpBuf — records list start positions during encoding, computes headers after all payload data is written, and assembles final output in one linear pass. No intermediate allocations for nested lists
  • Compile-time ENCODED_LEN — the derive macro const-folds field lengths at compile time. Only dynamic-length fields are measured at runtime
  • encode_unchecked — unsafe direct-write path that skips bounds checks after a single upfront length calculation. Used automatically by to_rlp() and encode_into()
  • get_unchecked in decode — after bounds validation, inner loops use unchecked indexing to eliminate redundant bounds checks in hot paths
  • Zero-copy RlpRef — parses only RLP headers without copying payload data. Field access skips over preceding items at header-scan speed
  • Aggressive inlining#[inline(always)] on all trait impls that cross crate boundaries

no_std Support

librlp is no_std + alloc by default. The std feature (enabled by default) only adds std::error::Error impls. Disable it for embedded, zkVM, or WASM targets:

[dependencies]
librlp = { version = "0.1", default-features = false }

Quick Start

[dependencies]
librlp = "0.1"
librlp-derive = "0.1"  # optional, for derive macros
use librlp::{RlpEncode, RlpDecode};
use librlp_derive::{RlpEncode, RlpDecode};

#[derive(RlpEncode, RlpDecode, PartialEq, Debug)]
struct Transaction {
    nonce: u64,
    to: [u8; 20],
    value: u64,
    data: Vec<u8>,
}

let tx = Transaction {
    nonce: 1,
    to: [0xAA; 20],
    value: 1_000_000,
    data: vec![],
};

// Encode
let encoded = tx.to_rlp();

// Decode
let mut buf: &[u8] = &encoded;
let decoded = Transaction::decode(&mut buf).unwrap();
assert_eq!(tx, decoded);

Lazy Decoding

RlpRef parses only RLP headers, deferring field decoding until accessed:

use librlp::{RlpEncode, RlpRef};

let encoded = (42u64, 100u64, vec![0u8; 1000]).to_rlp();
let r = RlpRef::new(&encoded).unwrap();

// Access only the first field — skips parsing the rest
let nonce: u64 = r.get(0).unwrap().decode().unwrap();

Buffer Reuse

encode_into reuses an existing Vec allocation across multiple encodes:

use librlp::RlpEncode;

let mut buf = Vec::new();
for tx in &transactions {
    tx.encode_into(&mut buf);
    send(&buf);
}

List Encoding

use librlp::encode::{encode_list_to_rlp, encode_list};
use librlp::decode::decode_list;
use librlp::RlpDecode;

let items = vec![1u64, 2, 3, 4, 5];

// Encode a list
let encoded = encode_list_to_rlp(&items);

// Decode a list
let mut buf: &[u8] = &encoded;
let decoded: Vec<u64> = decode_list(&mut buf).unwrap();

Derive Attributes

Attribute Target Effect
#[rlp(trailing)] struct Last Option<T> fields are omitted when None
#[rlp(skip)] field Field excluded from encoding; uses Default on decode
#[rlp(wrapper)] struct Transparent encoding of single-field structs
#[rlp(discriminant = N)] variant Enum variant discriminant for RLP encoding

Crates

Crate Description
librlp Core RlpEncode / RlpDecode traits, RlpRef lazy decoding, RlpBuf encoding buffer
librlp-derive #[derive(RlpEncode, RlpDecode)] proc macros

Dependency graph: librlp-derive is an optional dependency of librlp (re-exported via the librlp-derive feature).

Supported Types

RLP type Rust type Encode Decode
Single byte u8, bool Y Y
Integer u16, u32, u64, u128, usize Y Y
Byte string [u8; N], &[u8], Vec<u8>, &str, String Y Y
List structs via derive, tuples (up to 5) Y Y
Optional Option<T> Y Y
Unit () Y Y

Feature-gated types

Feature Types
bytes Bytes, BytesMut
alloy Address, B256, B512, Bloom, U64, U128, U256
ethereum-types H160, H256, H512, U256
ruint Uint<BITS, LIMBS>

Feature Flags

Feature Default Description
std Yes Enables std::error::Error impl
bytes No bytes crate (Bytes / BytesMut) support
alloy No alloy-primitives types (Address, B256, U256, etc.)
ethereum-types No ethereum-types support (H160, H256, U256, etc.)
ruint No ruint::Uint<BITS, LIMBS> support
librlp-derive No Re-exports derive macros from librlp-derive

Testing

cargo test --workspace                    # all tests, default features
cargo test --workspace --all-features     # all tests, all features
cargo test --workspace --no-default-features  # no_std tests

Miri (undefined behavior detection)

cargo +nightly miri test --workspace

Fuzzing

Five fuzz targets, differential-tested against alloy-rlp and ethrex-rlp:

Target Strategy
roundtrip encode -> decode -> equality for all primitive types
decode_arbitrary decode random bytes, assert no panics
differential compare encoding output against alloy-rlp and ethrex-rlp
canonical validate canonical encoding invariants
rlpref fuzz lazy RlpRef header parsing and field access
# Run all fuzz targets (30s each)
cd crates/librlp
for target in roundtrip decode_arbitrary differential canonical rlpref; do
    cargo +nightly fuzz run "$target" -- -max_total_time=30
done

Benchmarks

cargo bench --bench comparison    # vs alloy-rlp, ethrex-rlp, parity-rlp

CI

Every PR runs:

  1. Formatcargo fmt --check
  2. Clippy — 3 feature combinations (default, no-default, all-features) with -D warnings
  3. Tests — stable + nightly x 3 feature combinations (6 jobs)
  4. Miri — undefined behavior detection on nightly
  5. Fuzz smoke — each of 5 fuzz targets for 30 seconds

License

Licensed under either of

at your option.

About

Blazing fast RLP library in Rust with no-std support

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors