Eip 7916 progressive list - #68
Conversation
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>
gballet
left a comment
There was a problem hiding this comment.
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.
| ## 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. |
There was a problem hiding this comment.
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.
|
|
||
| /// Backing implementation of `Bitlist` and `ProgressiveBitlist`. `limit` is the | ||
| /// maximum bit count, or `null` for a progressive bitlist. | ||
| fn BitlistImpl(comptime limit: ?usize) type { |
There was a problem hiding this comment.
I would rather have duplicated code than this, until progressive lists are scheduled in the fork.
No description provided.