Skip to content

Eip 7916 progressive list - #68

Open
omerfirmak wants to merge 2 commits into
blockblaz:masterfrom
omerfirmak:eip-7916-progressive-list
Open

Eip 7916 progressive list#68
omerfirmak wants to merge 2 commits into
blockblaz:masterfrom
omerfirmak:eip-7916-progressive-list

Conversation

@omerfirmak

Copy link
Copy Markdown
Contributor

No description provided.

omerfirmak and others added 2 commits August 19, 2026 15:42
Add merkleizeProgressive() to lib.zig: a 0-terminated sequence of binary
subtrees with leaf counts 1, 4, 16, 64, ..., reusing merkleize() for each
fixed subtree.

Refactor List/Bitlist into ListImpl/BitlistImpl taking a `limit: ?usize`,
where null means progressive. List(T, N) and Bitlist(N) become thin
wrappers, so their behaviour is unchanged. New public types:
ProgressiveList(T), ProgressiveByteList and ProgressiveBitlist.

Serialization is byte-identical to the bounded variants. Differences are
confined to the capacity checks, which become no-ops, and merkleization:

 - maxInLength() returns error.NoMaxInLengthAvailable, since there is no
   static bound to check a payload against. This also changes
   Bitlist(N).maxInLength() from usize to !usize.
 - chunkCountLimit() is a @CompileError, as a progressive tree has no
   fixed depth and so cannot be wrapped in TreeHasher.
 - ProgressiveBitlist does not trim trailing zero bytes before hashing.
   Bitlist(N) can, because zero chunks are padding in a fixed-depth tree,
   but dropping a chunk shifts the progressive subtree layout.

Expected roots in the tests were generated by an independent Python
transcription of the EIP pseudocode, covering chunk counts that straddle
every subtree boundary plus deep cases at 1250 chunks and 20000 bits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A struct opts in by declaring `pub const ssz_progressive_container = true`.
Serialization is untouched; hashTreeRoot becomes

  hash(merkleize_progressive(field_roots), pack_bits(active_fields))

Only the all-active form EIP-7688 mandates is supported: active_fields is
derived from the field count, so a field cannot be marked inactive.

Expected roots in the tests come from eth-remerkleable, the reference
implementation execution-specs uses. The ProgressiveList vectors added in
the previous commit were also cross-checked against it and all match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@omerfirmak
omerfirmak requested a review from gballet as a code owner August 19, 2026 13:57

@gballet gballet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a big fan of adding stuff that isn't part of the spec, because I have done such things in the past and removing them is a pain.

Nonetheless, I wouldn't mind adding an "experimental" flag to it. But let's not try to over-optimize by factoring the code with List[N] because this is guaranteed to bite later. So duplicate the code for progressive containers and leave List[N] untouched please. If it's approved, we can factor the code then.

But this eip is giving RLP vibes, i don't think we should do that as a protocol.

Comment thread README.md
Comment on lines +69 to +111
## Progressive types (EIP-7916)

`ProgressiveList(T)` and `ProgressiveBitlist` implement
[EIP-7916](https://eips.ethereum.org/EIPS/eip-7916). They serialize exactly like
`List(T, N)` and `Bitlist(N)`, but carry no capacity limit and merkleize with
`merkleizeProgressive`: a 0-terminated sequence of binary subtrees whose leaf
counts grow 1, 4, 16, 64, ... This costs fewer hashes for short lists and keeps
generalized indices stable as the list grows.

```zig
const Transactions = ssz.utils.ProgressiveList(u64);
var txs = try Transactions.init(allocator);
defer txs.deinit();
try txs.append(42);
try ssz.hashTreeRoot(Sha256, Transactions, txs, &root, allocator);
```

`ProgressiveByteList` is an alias for `ProgressiveList(u8)`.

A struct opts in to EIP-7495 / EIP-7688 `ProgressiveContainer(active_fields=[1] * N)`
merkleization by declaring a marker. Serialization is unchanged; only the root
differs, becoming `hash(merkleize_progressive(field_roots), pack_bits(active_fields))`.

```zig
pub const ExecutionPayload = struct {
pub const ssz_progressive_container = true;
parent_hash: [32]u8,
// ...
};
```

Only the all-active form EIP-7688 mandates is supported; `active_fields` is
derived from the field count, so there is no way to mark a field inactive.

Two consequences of having no `N`:

* `maxInLength` returns `error.NoMaxInLengthAvailable`, so `deserialize` cannot
reject an oversized payload up front. Decoding still allocates only in
proportion to the input, but callers that relied on `N` as a cheap sanity
bound should enforce their own context-specific limit, as the EIP recommends.
* `TreeHasher` cannot wrap a progressive type: a progressive tree has no fixed
depth, so the power-of-two Merkle cache does not apply. Using it is a compile
error.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a typical artifact of claude, this document is specific to that PR and should not clutter the README. At the very worst, it should be added as a header comment to the ProgressiveList objects themselves.

Comment thread src/utils.zig

/// Backing implementation of `Bitlist` and `ProgressiveBitlist`. `limit` is the
/// maximum bit count, or `null` for a progressive bitlist.
fn BitlistImpl(comptime limit: ?usize) type {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather have duplicated code than this, until progressive lists are scheduled in the fork.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants