You're halfway through writing a state proof verifier. The hash doesn't match. You check the tree structure, reread the spec, and eventually notice the documentation is pointing you at two completely different serialization formats depending on which layer of Ethereum you're touching.
Not a bug. The seam between two eras.
Ethereum runs RLP (Recursive Length Prefix) on the execution layer and SSZ (Simple Serialize) on the consensus layer, simultaneously, by design. They are not interchangeable, and they construct Merkle trees in fundamentally different ways. If you're building proofs, writing clients, or just trying to understand what a "state root" actually commits to, that difference is the whole game.
RLP: The Format That Grew Up Improvising
RLP was Ethereum's original answer to a simple question: how do you encode arbitrarily nested data into bytes? Every item gets prefixed with a length indicator. Lists are prefixed concatenations of their encoded items. Strings, integers, nested lists: one recursive rule handles all of it. Elegant in a minimal way.
The problem surfaces the moment you try to build a Merkle tree from it.
The execution layer uses a Modified Merkle Patricia Trie (MPT), where the tree is built over the RLP-encoded content of each node. A branch node encodes up to 16 children plus a value slot, all RLP-serialized together. The hash of that node is `keccak256(rlp(node))`, and if the encoded result is shorter than 32 bytes, the raw bytes are inlined rather than hashed. That inlining is a deliberate optimization, but it means the relationship between a node's hash and its position in the tree is not uniform. Some nodes are referenced by hash. Some are embedded inline. The boundary shifts with content size, like a pipe junction that changes diameter based on water pressure.
Try writing a proof verifier for that. Client teams have done it, but the code is dense. Every node in the proof path requires checking whether you're dealing with a hash reference or an inline node, and the two cases parse differently.
There's a deeper structural problem too. RLP has no fixed-size commitment scheme. Because field widths vary with content, there's no clean way to produce a Merkle proof covering exactly one field of a struct without re-serializing the whole thing. Want to prove a specific account's balance without revealing the nonce? In an MPT over RLP, you're committing to the entire account encoding. The proof is all-or-nothing at the leaf level. That is a serious design limitation, not a quirk.
SSZ: Designed With the Tree in Mind
SSZ starts from the opposite direction. Not "how do we encode data" but "how do we build a Merkle tree over structured data, and what encoding falls out of that?"
The answer is a binary Merkle tree where every leaf is exactly 32 bytes. Always.
For basic types, SSZ encodes in little-endian format padded to 32 bytes. A `uint64` becomes 8 bytes, zero-padded to 32. For composite types like structs, each field gets its own 32-byte chunk. The chunks are paired and hashed up a binary tree to a single 32-byte root: the `hash_tree_root` of the object.
Here's the concrete mechanism. Take a `BeaconBlockHeader` with five fields: `slot`, `proposer_index`, `parent_root`, `state_root`, and `body_root`. Each field becomes one 32-byte chunk. Five chunks, so the tree pads to eight leaves (next power of two). The three padding chunks are `0x00...00`. The tree hashes in pairs up four levels to a single root. Every field has a deterministic index. `slot` is always chunk index 0. `state_root` is always chunk index 3. Proving the value of `state_root` requires a Merkle branch of exactly three sibling hashes, regardless of what the other fields contain.
That predictability is not a small thing. You can write a generalized proof verifier that takes a leaf index, a value, and a branch, with no knowledge of the specific struct type. Proof path length is fixed by the number of fields, not by the content.
SSZ also handles lists through a construction called `mix_in_length`: the Merkle root of the list's chunks is combined with the actual length via `sha256(root || length)`. This closes a subtle vulnerability where a list of three items and a list of four items with a trailing zero could otherwise produce the same root.
The Practical Gap
Consider two developers both building light client proofs. Priya is proving an account balance on the execution layer. She traverses an MPT, handles inline nodes, parses RLP at each step. Her verifier runs to about 200 lines of careful code, and she spent two days tracking down a special case for nodes under 32 bytes.
Marcus is proving a validator balance on the consensus layer. He walks a binary SSZ tree with a fixed-depth path, verifies three `sha256` hashes against a known root, and ships. His verifier is 40 lines. Same cryptographic security guarantee.
Ask yourself: which codebase would you rather maintain at 2 a.m. when something breaks?
This is the engineering reality behind why Ethereum's long-term roadmap points toward SSZ becoming more dominant, including proposals to migrate execution layer state to SSZ-based structures over time.
What the Transition Means for Proof Schemes
The Beacon Chain's `BeaconState` is fully SSZ-encoded, which is precisely why EIP-4788 (the beacon root opcode) could expose a verifiable root to the EVM at all. Smart contracts can now verify consensus-layer state using on-chain Merkle proofs against that root. The proof format is simple enough to verify in a few hundred gas.
RLP-based MPT proofs cost more and break more easily. An Ethereum state proof for a single storage slot can require dozens of nodes, each needing RLP decoding, and the gas cost reflects that faithfully.
SSZ's fixed structure also makes cross-client proof generation reliable in a way RLP never quite managed. Because the tree structure of `BeaconState` is specified exactly, a proof generated by one client implementation is valid against a root produced by a completely different one. With RLP, subtle encoding differences (trailing zeros, integer minimality rules) have historically caused cross-client bugs that were miserable to diagnose. The format carried ambiguity in its bones.
The two formats will coexist for the foreseeable future. The execution layer's history is RLP all the way down, and migrating it is a multi-year project with real compatibility costs. If you're building anything that touches both layers, you need both parsers. Knowing why they differ, not just that they differ, is what lets you reason about which proofs are cheap, which are expensive, and where the footguns are hiding.
The footguns are mostly on the RLP side.