Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Currently supported types:
* **tagged** unions
* `List[N]`
* `Bitlist[N]`
* `ProgressiveList[T]`
* `ProgressiveBitlist`

Ziglang has the limitation that it's not possible to determine which union field is active without tags.

Expand All @@ -42,6 +44,8 @@ Supported types:
* optionals
* `List[N]`
* `Bitlist[N]`
* `ProgressiveList[T]`
* `ProgressiveBitlist`

## Merkelization (experimental)

Expand All @@ -59,6 +63,52 @@ Supported types:
* unions
* `List[N]`
* `Bitlist[N]`
* `ProgressiveList[T]`
* `ProgressiveBitlist`

## 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.

## Using Custom Hash Functions

Expand Down
104 changes: 103 additions & 1 deletion src/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,30 @@ test "mixInLength" {
try std.testing.expect(std.mem.eql(u8, mixin[0..], expected[0..]));
}

fn mixInActiveFields(Hasher: type, root: [Hasher.digest_length]u8, active_fields: chunk, out: *[Hasher.digest_length]u8) void {
var hasher = Hasher.init(Hasher.Options{});
hasher.update(root[0..]);
hasher.update(active_fields[0..]);
hasher.final(out[0..]);
}

/// A struct opts in to EIP-7495 `ProgressiveContainer(active_fields=[1] * N)`
/// merkleization, as mandated by EIP-7688, by declaring
/// `pub const ssz_progressive_container = true;`. Serialization is unchanged.
pub fn isProgressiveContainer(T: type) bool {
return @hasDecl(T, "ssz_progressive_container") and T.ssz_progressive_container;
}

/// pack_bits(active_fields) for `n` all-active fields. EIP-7495 caps
/// active_fields at 256 bits, so the result is always a single chunk.
fn activeFieldsChunk(comptime n: usize) chunk {
if (n == 0) @compileError("a progressive container needs at least one field");
if (n > 256) @compileError("a progressive container may have at most 256 fields");
var c: chunk = zero_chunk;
for (0..n) |i| c[i / 8] |= @as(u8, 1) << @truncate(i % 8);
return c;
}

fn mixInSelector(Hasher: type, root: [Hasher.digest_length]u8, comptime selector: usize, out: *[Hasher.digest_length]u8) void {
var hasher = Hasher.init(Hasher.Options{});
hasher.update(root[0..]);
Expand Down Expand Up @@ -898,6 +922,79 @@ test "merkleize a bytes16 vector with one element" {
// try std.testing.expect(std.mem.eql(u8, out[0..], expected[0..]));
}

// merkleizeProgressive recursively calculates the root hash of an EIP-7916
// progressive Merkle tree: a 0-terminated sequence of binary subtrees with leaf
// counts 1, 4, 16, 64, ... Callers start the recursion with `num_leaves = 1`.
//
// Trailing zero chunks are not padding here, so `chunks` must hold exactly
// ceil(serialized_len / BYTES_PER_CHUNK) entries.
pub fn merkleizeProgressive(Hasher: type, chunks: []chunk, num_leaves: usize, out: *[Hasher.digest_length]u8) anyerror!void {
// The 0-terminator.
if (chunks.len == 0) {
@memset(out[0..], 0);
return;
}

// `merkleize` zero-pads the left subtree up to `num_leaves`.
const split = @min(num_leaves, chunks.len);
var buf: [Hasher.digest_length]u8 = undefined;
var digest = Hasher.init(Hasher.Options{});

try merkleize(Hasher, chunks[0..split], num_leaves, &buf);
digest.update(buf[0..]);
try merkleizeProgressive(Hasher, chunks[split..], num_leaves * 4, &buf);
digest.update(buf[0..]);

digest.final(out);
}

test "merkleizeProgressive of an empty slice is the zero chunk" {
const chunks = &[0][32]u8{};
var out: [32]u8 = undefined;
try merkleizeProgressive(Sha256, chunks, 1, &out);
try std.testing.expectEqualSlices(u8, zero_chunk[0..], out[0..]);
}

test "merkleizeProgressive subtree layout" {
var chunks: [5]chunk = undefined;
for (0..5) |i| chunks[i] = [_]u8{@intCast(i + 1)} ** 32;

var got: [32]u8 = undefined;
var expected: [32]u8 = undefined;
var hasher = Sha256.init(Sha256.Options{});

// A single chunk fills the 1-leaf subtree, terminated by the zero chunk.
try merkleizeProgressive(Sha256, chunks[0..1], 1, &got);
hasher.update(chunks[0][0..]);
hasher.update(zero_chunk[0..]);
hasher.final(&expected);
try std.testing.expectEqualSlices(u8, expected[0..], got[0..]);

// Five chunks fill the 1-leaf and the 4-leaf subtrees exactly.
try merkleizeProgressive(Sha256, chunks[0..5], 1, &got);
var second: [32]u8 = undefined;
try merkleize(Sha256, chunks[1..5], 4, &second);
var right: [32]u8 = undefined;
hasher = Sha256.init(Sha256.Options{});
hasher.update(second[0..]);
hasher.update(zero_chunk[0..]);
hasher.final(&right);
hasher = Sha256.init(Sha256.Options{});
hasher.update(chunks[0][0..]);
hasher.update(right[0..]);
hasher.final(&expected);
try std.testing.expectEqualSlices(u8, expected[0..], got[0..]);
}

test "merkleizeProgressive is sensitive to trailing zero chunks" {
var chunks = [_]chunk{ [_]u8{0xAA} ** 32, zero_chunk };
var one: [32]u8 = undefined;
var two: [32]u8 = undefined;
try merkleizeProgressive(Sha256, chunks[0..1], 1, &one);
try merkleizeProgressive(Sha256, chunks[0..2], 1, &two);
try std.testing.expect(!std.mem.eql(u8, one[0..], two[0..]));
}

fn packBits(bits: []const bool, l: *ArrayList(u8), allocator: Allocator) ![]chunk {
var byte: u8 = 0;
for (bits, 0..) |bit, bitidx| {
Expand Down Expand Up @@ -1002,7 +1099,12 @@ pub fn hashTreeRoot(Hasher: type, T: type, value: T, out: *[Hasher.digest_length
try hashTreeRoot(Hasher, f.type, @field(value, f.name), &tmp, allocator);
try chunks.append(allocator, tmp);
}
try merkleize(Hasher, chunks.items, null, out);
if (comptime isProgressiveContainer(T)) {
try merkleizeProgressive(Hasher, chunks.items, 1, &tmp);
mixInActiveFields(Hasher, tmp, comptime activeFieldsChunk(str.fields.len), out);
} else {
try merkleize(Hasher, chunks.items, null, out);
}
},
// An optional is a union with `None` as first value.
.optional => |opt| if (value != null) {
Expand Down
Loading
Loading