Skip to content
Merged
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
12 changes: 12 additions & 0 deletions src/ssz/tree_view/array_basic.zig
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,17 @@ pub fn ArrayBasicTreeView(comptime ST: type) type {
pub fn getAllInto(self: *Self, values: []Element) ![]Element {
return try Chunks.getAllInto(&self.base_view, length, values);
}

/// Serialize the tree view into a provided buffer.
/// Returns the number of bytes written.
pub fn serializeIntoBytes(self: *Self, out: []u8) !usize {
try self.commit();
return try ST.tree.serializeIntoBytes(self.base_view.data.root, self.base_view.pool, out);
}

/// Get the serialized size of this tree view.
pub fn serializedSize(_: *Self) usize {
return ST.fixed_size;
}
};
}
22 changes: 22 additions & 0 deletions src/ssz/tree_view/array_composite.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const Depth = hashing.Depth;
const Node = @import("persistent_merkle_tree").Node;
const Gindex = @import("persistent_merkle_tree").Gindex;
const isBasicType = @import("../type/type_kind.zig").isBasicType;
const isFixedType = @import("../type/type_kind.zig").isFixedType;

const type_root = @import("../type/root.zig");
const chunkDepth = type_root.chunkDepth;
Expand Down Expand Up @@ -83,5 +84,26 @@ pub fn ArrayCompositeTreeView(comptime ST: type) type {
_ = allocator;
return error.NotImplemented;
}

/// Serialize the tree view into a provided buffer.
/// Returns the number of bytes written.
pub fn serializeIntoBytes(self: *Self, out: []u8) !usize {
try self.commit();
if (comptime isFixedType(ST)) {
return try ST.tree.serializeIntoBytes(self.base_view.data.root, self.base_view.pool, out);
} else {
return try ST.tree.serializeIntoBytes(self.base_view.allocator, self.base_view.data.root, self.base_view.pool, out);
}
}

/// Get the serialized size of this tree view.
pub fn serializedSize(self: *Self) !usize {
try self.commit();
if (comptime isFixedType(ST)) {
return ST.tree.serializedSize(self.base_view.data.root, self.base_view.pool);
} else {
return try ST.tree.serializedSize(self.base_view.allocator, self.base_view.data.root, self.base_view.pool);
}
}
};
}
22 changes: 22 additions & 0 deletions src/ssz/tree_view/container.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const Allocator = std.mem.Allocator;
const Node = @import("persistent_merkle_tree").Node;
const Gindex = @import("persistent_merkle_tree").Gindex;
const isBasicType = @import("../type/type_kind.zig").isBasicType;
const isFixedType = @import("../type/type_kind.zig").isFixedType;
const tree_view_root = @import("root.zig");
const TreeViewData = tree_view_root.TreeViewData;
const BaseTreeView = tree_view_root.BaseTreeView;
Expand Down Expand Up @@ -91,5 +92,26 @@ pub fn ContainerTreeView(comptime ST: type) type {
try self.base_view.setChildData(child_gindex, value.base_view.data);
}
}

/// Serialize the tree view into a provided buffer.
/// Returns the number of bytes written.
pub fn serializeIntoBytes(self: *Self, out: []u8) !usize {
try self.commit();
if (comptime isFixedType(ST)) {
return try ST.tree.serializeIntoBytes(self.base_view.data.root, self.base_view.pool, out);
} else {
return try ST.tree.serializeIntoBytes(self.base_view.allocator, self.base_view.data.root, self.base_view.pool, out);
}
}

/// Get the serialized size of this tree view.
pub fn serializedSize(self: *Self) !usize {
try self.commit();
if (comptime isFixedType(ST)) {
return ST.tree.serializedSize(self.base_view.data.root, self.base_view.pool);
} else {
return try ST.tree.serializedSize(self.base_view.allocator, self.base_view.data.root, self.base_view.pool);
}
}
};
}
13 changes: 13 additions & 0 deletions src/ssz/tree_view/list_basic.zig
Original file line number Diff line number Diff line change
Expand Up @@ -160,5 +160,18 @@ pub fn ListBasicTreeView(comptime ST: type) type {
errdefer self.base_view.pool.unref(length_node);
try self.base_view.setChildNode(@enumFromInt(3), length_node);
}

/// Serialize the tree view into a provided buffer.
/// Returns the number of bytes written.
pub fn serializeIntoBytes(self: *Self, out: []u8) !usize {
try self.commit();
return try ST.tree.serializeIntoBytes(self.base_view.allocator, self.base_view.data.root, self.base_view.pool, out);
}

/// Get the serialized size of this tree view.
pub fn serializedSize(self: *Self) !usize {
try self.commit();
return try ST.tree.serializedSize(self.base_view.allocator, self.base_view.data.root, self.base_view.pool);
}
};
}
14 changes: 14 additions & 0 deletions src/ssz/tree_view/list_composite.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const Depth = hashing.Depth;
const Node = @import("persistent_merkle_tree").Node;
const Gindex = @import("persistent_merkle_tree").Gindex;
const isBasicType = @import("../type/type_kind.zig").isBasicType;
const isFixedType = @import("../type/type_kind.zig").isFixedType;

Copilot AI Dec 24, 2025

Copy link

Choose a reason for hiding this comment

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

The import isFixedType is declared but never used in this file. It should be removed to clean up the code.

Suggested change
const isFixedType = @import("../type/type_kind.zig").isFixedType;

Copilot uses AI. Check for mistakes.

const type_root = @import("../type/root.zig");
const chunkDepth = type_root.chunkDepth;
Expand Down Expand Up @@ -179,5 +180,18 @@ pub fn ListCompositeTreeView(comptime ST: type) type {
errdefer self.base_view.pool.unref(length_node);
try self.base_view.setChildNode(@enumFromInt(3), length_node);
}

/// Serialize the tree view into a provided buffer.
/// Returns the number of bytes written.
pub fn serializeIntoBytes(self: *Self, out: []u8) !usize {
try self.commit();
return try ST.tree.serializeIntoBytes(self.base_view.allocator, self.base_view.data.root, self.base_view.pool, out);
}

/// Get the serialized size of this tree view.
pub fn serializedSize(self: *Self) !usize {
try self.commit();
return try ST.tree.serializedSize(self.base_view.allocator, self.base_view.data.root, self.base_view.pool);
}
};
}
142 changes: 142 additions & 0 deletions src/ssz/type/bit_list.zig
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,47 @@ pub fn BitListType(comptime _limit: comptime_int) type {
try pool.createLeafFromUint(value.bit_len),
);
}

/// Serialize BitList to bytes.
pub fn serializeIntoBytes(allocator: std.mem.Allocator, node: Node.Id, pool: *Node.Pool, out: []u8) !usize {
const bit_len = try length(node, pool);
const serialized_bit_len = bit_len + 1; // + 1 for padding bit
const byte_len = std.math.divCeil(usize, serialized_bit_len, 8) catch unreachable;
const chunk_count = (bit_len + 255) / 256;

if (chunk_count == 0) {
// Empty list - just set padding bit
out[0] = 1;
return 1;
}

const raw_byte_length = (bit_len + 7) / 8;

const nodes = try allocator.alloc(Node.Id, chunk_count);
defer allocator.free(nodes);
try node.getNodesAtDepth(pool, chunk_depth + 1, 0, nodes);

for (0..chunk_count) |i| {
const start_idx = i * 32;
const remaining_bytes = raw_byte_length - start_idx;
const bytes_to_copy = @min(remaining_bytes, 32);
if (bytes_to_copy > 0) {
@memcpy(out[start_idx..][0..bytes_to_copy], nodes[i].getRoot(pool)[0..bytes_to_copy]);
}
}

if (bit_len % 8 == 0) {
out[byte_len - 1] = 1;
} else {
out[byte_len - 1] |= @as(u8, 1) << @intCast((serialized_bit_len - 1) % 8);
}
return byte_len;
}

pub fn serializedSize(node: Node.Id, pool: *Node.Pool) !usize {
const bit_len = try length(node, pool);
return std.math.divCeil(usize, bit_len + 1, 8) catch unreachable;
}
};

pub fn serializeIntoJson(allocator: std.mem.Allocator, writer: anytype, in: *const Type) !void {
Expand Down Expand Up @@ -625,3 +666,104 @@ test "resize" {
try std.testing.expect(b.data.items.len == 1);
try std.testing.expect(b.data.items[0] == 13);
}

// Refer to https://github.com/ChainSafe/ssz/blob/f5ed0b457333749b5c3f49fa5eafa096a725f033/packages/ssz/test/unit/byType/bitList/valid.test.ts#L44-L69
test "BitListType - padding bit test cases" {
const allocator = std.testing.allocator;

const TestCase = struct {
bools: []const bool,
expected_hex: []const u8,
};

const test_cases = [_]TestCase{
.{ .bools = &[_]bool{}, .expected_hex = &[_]u8{0b1} },
.{ .bools = &[_]bool{true}, .expected_hex = &[_]u8{0b11} },
.{ .bools = &[_]bool{false}, .expected_hex = &[_]u8{0b10} },
.{ .bools = &[_]bool{ true, true, true }, .expected_hex = &[_]u8{0b1111} },
.{ .bools = &[_]bool{ false, false, false }, .expected_hex = &[_]u8{0b1000} },
.{ .bools = &[_]bool{ true, true, true, true, true, true, true, true }, .expected_hex = &[_]u8{ 0b11111111, 0b00000001 } },
.{ .bools = &[_]bool{ false, false, false, false, false, false, false, false }, .expected_hex = &[_]u8{ 0b00000000, 0b00000001 } },
};

const Bits = BitListType(8);

for (test_cases) |tc| {
var b: Bits.Type = try Bits.Type.fromBoolSlice(allocator, tc.bools);
defer b.deinit(allocator);

const serialized = try allocator.alloc(u8, Bits.serializedSize(&b));
defer allocator.free(serialized);
_ = Bits.serializeIntoBytes(&b, serialized);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The return value of Bits.serializeIntoBytes is ignored. According to the style guide (lines 51-56), return values should be asserted to ensure correctness. You should assert that the number of bytes written is equal to the size of the serialized slice.

        try std.testing.expectEqual(serialized.len, Bits.serializeIntoBytes(&b, serialized));
References
  1. Assert all function arguments and return values, pre/postconditions and invariants. A function must not operate blindly on data it has not checked. The purpose of a function is to increase the probability that a program is correct. Assertions within a function are part of how functions serve this purpose. The assertion density of the code must average a minimum of two assertions per function. (link)

try std.testing.expectEqualSlices(u8, tc.expected_hex, serialized);

var deserialized: Bits.Type = Bits.default_value;
try Bits.deserializeFromBytes(allocator, serialized, &deserialized);
defer deserialized.deinit(allocator);

var deserialized_bools = try allocator.alloc(bool, deserialized.bit_len);
defer allocator.free(deserialized_bools);
try deserialized.toBoolSlice(&deserialized_bools);
try std.testing.expectEqualSlices(bool, tc.bools, deserialized_bools);
}
}

// Refer to https://github.com/ChainSafe/ssz/blob/f5ed0b457333749b5c3f49fa5eafa096a725f033/packages/ssz/test/unit/byType/bitList/valid.test.ts#L5-L41
test "BitListType - tree roundtrip" {
const allocator = std.testing.allocator;

const Bits = BitListType(2048);

const TestCase = struct {
id: []const u8,
serialized: []const u8,
expected_root: [32]u8,
};

const test_cases = [_]TestCase{
.{
.id = "empty",
.serialized = &[_]u8{0x01},
.expected_root = [_]u8{ 0xe8, 0xe5, 0x27, 0xe8, 0x4f, 0x66, 0x61, 0x63, 0xa9, 0x0e, 0xf9, 0x00, 0xe0, 0x13, 0xf5, 0x6b, 0x0a, 0x4d, 0x02, 0x01, 0x48, 0xb2, 0x22, 0x40, 0x57, 0xb7, 0x19, 0xf3, 0x51, 0xb0, 0x03, 0xa6 },
},
.{
.id = "zero'ed 1 byte",
.serialized = &[_]u8{ 0x00, 0x10 },
.expected_root = [_]u8{ 0x07, 0xeb, 0x64, 0x02, 0x82, 0xe1, 0x6e, 0xea, 0x87, 0x30, 0x0c, 0x37, 0x4c, 0x48, 0x94, 0xad, 0x69, 0xb9, 0x48, 0xde, 0x92, 0x4a, 0x15, 0x8d, 0x2d, 0x18, 0x43, 0xb3, 0xcf, 0x01, 0x89, 0x8a },
},
.{
.id = "zero'ed 8 bytes",
.serialized = &[_]u8{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10 },
.expected_root = [_]u8{ 0x5c, 0x59, 0x7e, 0x77, 0xf8, 0x79, 0xe2, 0x49, 0xaf, 0x95, 0xfe, 0x54, 0x3c, 0xf5, 0xf4, 0xdd, 0x16, 0xb6, 0x86, 0x94, 0x8d, 0xc7, 0x19, 0x70, 0x74, 0x45, 0xa3, 0x2a, 0x77, 0xff, 0x62, 0x66 },
},
};

var pool = try Node.Pool.init(allocator, 1024);
defer pool.deinit();

for (test_cases) |tc| {
var value: Bits.Type = Bits.default_value;
try Bits.deserializeFromBytes(allocator, tc.serialized, &value);
defer value.deinit(allocator);

const tree_node = try Bits.tree.fromValue(allocator, &pool, &value);

var value_from_tree: Bits.Type = Bits.default_value;
try Bits.tree.toValue(allocator, tree_node, &pool, &value_from_tree);
defer value_from_tree.deinit(allocator);

try std.testing.expect(Bits.equals(&value, &value_from_tree));

const tree_size = try Bits.tree.serializedSize(tree_node, &pool);
try std.testing.expectEqual(tc.serialized.len, tree_size);

const tree_serialized = try allocator.alloc(u8, tree_size);
defer allocator.free(tree_serialized);
_ = try Bits.tree.serializeIntoBytes(allocator, tree_node, &pool, tree_serialized);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The return value of Bits.tree.serializeIntoBytes is ignored. According to the style guide (lines 51-56), return values should be asserted to ensure correctness. You should assert that the number of bytes written is equal to the size of the tree_serialized slice.

For example:

const written = try Bits.tree.serializeIntoBytes(allocator, tree_node, &pool, tree_serialized);
try std.testing.expectEqual(tree_serialized.len, written);
References
  1. Assert all function arguments and return values, pre/postconditions and invariants. A function must not operate blindly on data it has not checked. The purpose of a function is to increase the probability that a program is correct. Assertions within a function are part of how functions serve this purpose. The assertion density of the code must average a minimum of two assertions per function. (link)

try std.testing.expectEqualSlices(u8, tc.serialized, tree_serialized);

var hash_root: [32]u8 = undefined;
try Bits.hashTreeRoot(allocator, &value, &hash_root);
try std.testing.expectEqualSlices(u8, &tc.expected_root, &hash_root);
}
}
Loading