diff --git a/src/consensus_types/phase0.zig b/src/consensus_types/phase0.zig index cd1e6b397..d526902f9 100644 --- a/src/consensus_types/phase0.zig +++ b/src/consensus_types/phase0.zig @@ -19,7 +19,7 @@ pub const Checkpoint = ssz.FixedContainerType(struct { root: p.Root, }); -pub const Validator = ssz.FixedContainerType(struct { +pub const Validator = ssz.StructContainerType(struct { pubkey: p.BLSPubkey, withdrawal_credentials: p.Root, effective_balance: p.Gwei, diff --git a/src/persistent_merkle_tree/Node.zig b/src/persistent_merkle_tree/Node.zig index 59205755c..754909a4e 100644 --- a/src/persistent_merkle_tree/Node.zig +++ b/src/persistent_merkle_tree/Node.zig @@ -9,6 +9,9 @@ const Depth = @import("hashing").Depth; const Gindex = @import("gindex.zig").Gindex; hash: [32]u8, +// `left` and `right` are child node IDs for leaf/zero/regular branch nodes. +// For branch-struct nodes, `left` stores the high pointer bits and `right` +// stores the low pointer bits of the packed BranchStructRef pointer. left: Id, right: Id, state: State, @@ -34,8 +37,8 @@ pub const Error = error{ /// /// `[1, next_free]` /// -/// If the high bit is not set, the next two bits determine the `node_type` -/// The following 29 bits are used for the `ref_count`. +/// If the high bit is not set, the next 3 bits determine the `node_type` +/// The following 28 bits are used for the `ref_count`. /// /// `[0, node_type, ref_count]` pub const State = enum(u32) { @@ -45,14 +48,16 @@ pub const State = enum(u32) { pub const max_next_free = 0x7FFFFFFF; - // four types of nodes - const node_type = 0x60000000; + // five types of nodes, use 3 bits + const node_type = 0x70000000; pub const zero: State = @enumFromInt(0x00000000); - pub const leaf: State = @enumFromInt(0x20000000); - pub const branch_lazy: State = @enumFromInt(0x40000000); - pub const branch_computed: State = @enumFromInt(0x60000000); + pub const leaf: State = @enumFromInt(0x10000000); + pub const branch_lazy: State = @enumFromInt(0x20000000); + pub const branch_computed: State = @enumFromInt(0x30000000); + pub const branch_struct_lazy: State = @enumFromInt(0x40000000); + pub const branch_struct_computed: State = @enumFromInt(0x50000000); - pub const max_ref_count = 0x1FFFFFFF; + pub const max_ref_count = 0x0FFFFFFF; pub inline fn isFree(node: State) bool { return @intFromEnum(node) & @intFromEnum(free) != 0; @@ -75,7 +80,8 @@ pub const State = enum(u32) { } pub inline fn isBranch(node: State) bool { - return @intFromEnum(node) & @intFromEnum(branch_lazy) != 0; + const nt = @intFromEnum(node) & node_type; + return nt == @intFromEnum(branch_lazy) or nt == @intFromEnum(branch_computed); } pub inline fn isBranchLazy(node: State) bool { @@ -86,10 +92,27 @@ pub const State = enum(u32) { return @intFromEnum(node) & node_type == @intFromEnum(branch_computed); } + pub inline fn isBranchStruct(node: State) bool { + const nt = @intFromEnum(node) & node_type; + return nt == @intFromEnum(branch_struct_lazy) or nt == @intFromEnum(branch_struct_computed); + } + + pub inline fn isBranchStructLazy(node: State) bool { + return @intFromEnum(node) & node_type == @intFromEnum(branch_struct_lazy); + } + + pub inline fn isBranchStructComputed(node: State) bool { + return @intFromEnum(node) & node_type == @intFromEnum(branch_struct_computed); + } + pub inline fn setBranchComputed(node: *State) void { node.* = @enumFromInt(@intFromEnum(node.*) | @intFromEnum(branch_computed)); } + pub inline fn setBranchStructComputed(node: *State) void { + node.* = @enumFromInt(@intFromEnum(node.*) | @intFromEnum(branch_struct_computed)); + } + pub inline fn initRefCount(node: State) State { return node; } @@ -123,6 +146,15 @@ pub const Pool = struct { nodes: std.MultiArrayList(Node).Slice, next_free_node: Id, + pub const BranchStructRef = struct { + ptr: *anyopaque, + get_root: *const fn (ptr: *const anyopaque, out: *[32]u8) void, + // Proof generation may need a traversable tree even though branch-struct + // nodes are normally opaque to left/right navigation. + to_tree: *const fn (ptr: *const anyopaque, pool: *Pool) Error!Id, + deinit: *const fn (ptr: *anyopaque, allocator: Allocator) void, + }; + pub const free_bit: u32 = 0x80000000; pub const max_ref_count: u32 = 0x7FFFFFFF; @@ -246,6 +278,53 @@ pub const Pool = struct { return node_id; } + /// The pool allocates and owns a clone of `ptr`; the caller retains ownership of its data. + pub fn createBranchStruct(self: *Pool, comptime T: type, ptr: *const T) Error!Id { + const cloned = try T.init(self.allocator, ptr); + errdefer @constCast(cloned).deinit(self.allocator); + + const branch_struct_ref = try self.allocator.create(BranchStructRef); + errdefer self.allocator.destroy(branch_struct_ref); + + branch_struct_ref.* = .{ + .ptr = @ptrCast(@constCast(cloned)), + .get_root = struct { + fn call(ptr_erased: *const anyopaque, out: *[32]u8) void { + const typed_ptr: *const T = @ptrCast(@alignCast(ptr_erased)); + typed_ptr.getRoot(out); + } + }.call, + .to_tree = struct { + fn call(ptr_erased: *const anyopaque, pool: *Pool) Error!Id { + const typed_ptr: *const T = @ptrCast(@alignCast(ptr_erased)); + return try typed_ptr.toTree(pool); + } + }.call, + .deinit = struct { + fn call(ptr_erased: *anyopaque, allocator: Allocator) void { + const typed_ptr: *T = @ptrCast(@alignCast(ptr_erased)); + typed_ptr.deinit(allocator); + } + }.call, + }; + + const node_id = try self.create(); + const ptr_usize = @intFromPtr(branch_struct_ref); + // branch-struct nodes do not store child node IDs in left/right. Instead we + // pack the BranchStructRef pointer into those fields and rely on noChild() + // to keep generic traversal from treating them as normal branches. + const right_ptr_value: u32 = @intCast(ptr_usize & 0xFFFFFFFF); + self.nodes.items(.right)[@intFromEnum(node_id)] = @enumFromInt(right_ptr_value); + if (comptime @sizeOf(usize) == 8) { + const left_ptr_value: u32 = @intCast(ptr_usize >> 32); + self.nodes.items(.left)[@intFromEnum(node_id)] = @enumFromInt(left_ptr_value); + } else { + self.nodes.items(.left)[@intFromEnum(node_id)] = @enumFromInt(0); + } + self.nodes.items(.state)[@intFromEnum(node_id)] = State.branch_struct_lazy.initRefCount(); + return node_id; + } + /// Allocates nodes into the pool. /// /// All nodes are allocated with refcount=0. @@ -333,6 +412,39 @@ pub const Pool = struct { _ = try states[@intFromEnum(node_id)].incRefCount(); } + pub fn getStructPtr(self: *Pool, node_id: Id, comptime T: type) Error!*const T { + const state = self.nodes.items(.state)[@intFromEnum(node_id)]; + if (!state.isBranchStruct()) { + return Error.InvalidNode; + } + const struct_ref = self.getBranchStructRefUnsafe(node_id); + const ptr: *const T = @ptrCast(@alignCast(struct_ref.ptr)); + return ptr; + } + + pub fn getBranchStructRefUnsafe(self: *Pool, node_id: Id) *BranchStructRef { + const left_ptr_value: u32 = @intFromEnum(self.nodes.items(.left)[@intFromEnum(node_id)]); + const right_ptr_value: u32 = @intFromEnum(self.nodes.items(.right)[@intFromEnum(node_id)]); + // This reverses the packing performed in createBranchStruct(). Callers must + // ensure node_id is a branch-struct node before decoding pointer bits. + const ptr_int: usize = if (comptime @sizeOf(usize) == 8) + @as(u64, left_ptr_value) << 32 | @as(u64, right_ptr_value) + else + right_ptr_value; + return @ptrFromInt(ptr_int); + } + + pub fn materializeBranchStruct(self: *Pool, node_id: Id) Error!Id { + const state = self.nodes.items(.state)[@intFromEnum(node_id)]; + if (!state.isBranchStruct()) { + return Error.InvalidNode; + } + // Materialization is only for specialized flows such as proof generation. + // Generic tree navigation should keep branch-struct nodes opaque. + const struct_ref = self.getBranchStructRefUnsafe(node_id); + return try struct_ref.to_tree(struct_ref.ptr, self); + } + pub fn unref(self: *Pool, node_id: Id) void { const states = self.nodes.items(.state); const lefts = self.nodes.items(.left); @@ -381,6 +493,13 @@ pub const Pool = struct { } else { current = null; } + + if (states[@intFromEnum(id)].isBranchStruct()) { + const struct_ref = self.getBranchStructRefUnsafe(id); + struct_ref.deinit(struct_ref.ptr, self.allocator); + self.allocator.destroy(struct_ref); + } + // Return the node to the free list states[@intFromEnum(id)] = State.initNextFree(self.next_free_node); self.next_free_node = id; @@ -396,7 +515,8 @@ pub const Id = enum(u32) { /// Returns true if navigation to the child node is not possible pub inline fn noChild(node_id: Id, state: State) bool { - return state.isLeaf() or @intFromEnum(node_id) == 0; + // branch-struct nodes keep packed pointer bits in left/right, not child IDs. + return state.isLeaf() or state.isBranchStruct() or @intFromEnum(node_id) == 0; } /// Returns the root hash of the tree, computing any lazy branches as needed. @@ -410,6 +530,12 @@ pub const Id = enum(u32) { hashOne(hash, left, right); state.setBranchComputed(); } + + if (state.isBranchStructLazy()) { + const struct_ref = pool.getBranchStructRefUnsafe(node_id); + struct_ref.get_root(struct_ref.ptr, hash); + state.setBranchStructComputed(); + } return hash; } diff --git a/src/persistent_merkle_tree/proof.zig b/src/persistent_merkle_tree/proof.zig index 8d4b95042..547a5c9ff 100644 --- a/src/persistent_merkle_tree/proof.zig +++ b/src/persistent_merkle_tree/proof.zig @@ -14,6 +14,8 @@ pub const Error = error{ InvalidGindex, /// Witness list length does not match the gindex path length. InvalidWitnessLength, + /// Single-proof traversal encountered a branch-struct node after already materializing one. + NestedBranchStruct, }; pub const ProofType = enum { @@ -53,6 +55,24 @@ pub const SingleProof = struct { } }; +/// Proof traversal needs real left/right child nodes. For a branch-struct node, +/// materialize a temporary plain tree and keep its root alive until proof creation finishes. +fn materializeIfBranchStruct( + allocator: Allocator, + pool: *Node.Pool, + node_id: Node.Id, + temporary_roots: *std.ArrayListUnmanaged(Node.Id), +) (Node.Error || Error)!Node.Id { + if (!node_id.getState(pool).isBranchStruct()) { + return node_id; + } + + const materialized = try pool.materializeBranchStruct(node_id); + errdefer pool.unref(materialized); + temporary_roots.append(allocator, materialized) catch return error.OutOfMemory; + return materialized; +} + /// Produces a single Merkle proof for the node at `gindex`. pub fn createSingleProof( allocator: Allocator, @@ -67,6 +87,9 @@ pub fn createSingleProof( const path_len = gindex.pathLen(); var witnesses = try allocator.alloc([32]u8, path_len); errdefer allocator.free(witnesses); + // there should not be more than 1 branch-struct node on the path + var materialized_root: ?Node.Id = null; + defer if (materialized_root) |temp_root| pool.unref(temp_root); if (path_len == 0) { return SingleProof{ @@ -80,6 +103,15 @@ pub fn createSingleProof( for (0..path_len) |depth_idx| { const witness_index = path_len - 1 - depth_idx; + if (node_id.getState(pool).isBranchStruct()) { + if (materialized_root) |_| { + // Single proofs only support one branch-struct hop. If the + // materialized subtree would require another one, fail fast. + return error.NestedBranchStruct; + } + materialized_root = try pool.materializeBranchStruct(node_id); + node_id = materialized_root.?; + } if (path.left()) { const right_id = try node_id.getRight(pool); @@ -420,6 +452,7 @@ fn nodeToCompactMultiProof( node_id: Node.Id, bitlist: []const bool, bit_index: usize, + temporary_roots: *std.ArrayListUnmanaged(Node.Id), ) (Node.Error || Error)![][32]u8 { // If bit is 1, this node is a leaf in the proof if (bitlist[bit_index]) { @@ -428,13 +461,15 @@ fn nodeToCompactMultiProof( return leaves; } + const current = try materializeIfBranchStruct(allocator, pool, node_id, temporary_roots); + // Otherwise, recurse into children - const left_id = try node_id.getLeft(pool); - const left = try nodeToCompactMultiProof(allocator, pool, left_id, bitlist, bit_index + 1); + const left_id = try current.getLeft(pool); + const left = try nodeToCompactMultiProof(allocator, pool, left_id, bitlist, bit_index + 1, temporary_roots); defer allocator.free(left); - const right_id = try node_id.getRight(pool); - const right = try nodeToCompactMultiProof(allocator, pool, right_id, bitlist, bit_index + left.len * 2); + const right_id = try current.getRight(pool); + const right = try nodeToCompactMultiProof(allocator, pool, right_id, bitlist, bit_index + left.len * 2, temporary_roots); defer allocator.free(right); const result = try allocator.alloc([32]u8, left.len + right.len); @@ -452,8 +487,15 @@ pub fn createCompactMultiProof( ) (Node.Error || Error)![][32]u8 { const bitlist = try descriptorToBitlist(allocator, descriptor); defer allocator.free(bitlist); + var temporary_roots = std.ArrayListUnmanaged(Node.Id){}; + defer { + for (temporary_roots.items) |temp_root| { + pool.unref(temp_root); + } + temporary_roots.deinit(allocator); + } - return nodeToCompactMultiProof(allocator, pool, root, bitlist, 0); + return nodeToCompactMultiProof(allocator, pool, root, bitlist, 0, &temporary_roots); } /// Pointer to track position in bitlist and leaves during reconstruction diff --git a/src/ssz/root.zig b/src/ssz/root.zig index 4c6b9237c..47f6765f7 100644 --- a/src/ssz/root.zig +++ b/src/ssz/root.zig @@ -33,6 +33,7 @@ pub const VariableVectorType = types.VariableVectorType; pub const FixedContainerType = types.FixedContainerType; pub const VariableContainerType = types.VariableContainerType; +pub const StructContainerType = types.StructContainerType; pub const getPathGindex = types.getPathGindex; @@ -42,6 +43,7 @@ pub const HasherData = hasher.HasherData; const tree_view = @import("tree_view/root.zig"); pub const ContainerTreeView = tree_view.ContainerTreeView; +pub const StructContainerTreeView = tree_view.StructContainerTreeView; pub const ArrayBasicTreeView = tree_view.ArrayBasicTreeView; pub const ArrayCompositeTreeView = tree_view.ArrayCompositeTreeView; pub const ListBasicTreeView = tree_view.ListBasicTreeView; diff --git a/src/ssz/tree_view/container.zig b/src/ssz/tree_view/container.zig index 1e6cedd88..fcf50c039 100644 --- a/src/ssz/tree_view/container.zig +++ b/src/ssz/tree_view/container.zig @@ -427,6 +427,215 @@ pub fn ContainerTreeView(comptime ST: type) type { return TreeView; } +pub fn StructContainerTreeView(comptime ST: type) type { + const T = ST.Type; + + const TreeView = struct { + allocator: Allocator, + pool: *Node.Pool, + root: Node.Id, + value: T, + changed: std.StaticBitSet(ST.chunk_count), + + pub const SszType = ST; + + const Self = @This(); + + pub fn init(allocator: Allocator, pool: *Node.Pool, root: Node.Id) !*Self { + try pool.ref(root); + errdefer pool.unref(root); + + const ptr = try allocator.create(Self); + errdefer allocator.destroy(ptr); + + try ST.tree.toValue(root, pool, &ptr.value); + + ptr.allocator = allocator; + ptr.pool = pool; + ptr.root = root; + ptr.changed = std.StaticBitSet(ST.chunk_count).initEmpty(); + return ptr; + } + + pub fn clone(self: *Self, opts: CloneOpts) !*Self { + try self.commit(); + + try self.pool.ref(self.root); + errdefer self.pool.unref(self.root); + + const ptr = try self.allocator.create(Self); + errdefer self.allocator.destroy(ptr); + + ptr.allocator = self.allocator; + ptr.pool = self.pool; + ptr.root = self.root; + ptr.changed = std.StaticBitSet(ST.chunk_count).initEmpty(); + + if (opts.transfer_cache) { + ptr.value = self.value; + } else { + try ST.tree.toValue(self.root, self.pool, &ptr.value); + } + + return ptr; + } + + pub fn deinit(self: *Self) void { + self.pool.unref(self.root); + self.allocator.destroy(self); + } + + pub fn commit(self: *Self) !void { + if (self.changed.count() == 0) { + return; + } + + const new_root = try ST.tree.fromValue(self.pool, &self.value); + try self.pool.ref(new_root); + self.pool.unref(self.root); + self.root = new_root; + self.changed = std.StaticBitSet(ST.chunk_count).initEmpty(); + } + + pub fn getRoot(self: *const Self) Node.Id { + return self.root; + } + + pub fn hashTreeRootInto(self: *Self, out: *[32]u8) !void { + try self.commit(); + out.* = self.root.getRoot(self.pool).*; + } + + pub fn hashTreeRoot(self: *Self) !*const [32]u8 { + try self.commit(); + return self.root.getRoot(self.pool); + } + + pub fn getFieldRoot(self: *Self, comptime field_name: []const u8) !*const [32]u8 { + const ChildST = ST.getFieldType(field_name); + const field_value = try self.get(field_name); + const node = try ChildST.tree.fromValue(self.pool, &field_value); + return node.getRoot(self.pool); + } + + pub fn deserialize(allocator: Allocator, pool: *Node.Pool, bytes: []const u8) !*Self { + const root = try ST.tree.deserializeFromBytes(pool, bytes); + return try Self.init(allocator, pool, root); + } + + pub fn fromValue(allocator: Allocator, pool: *Node.Pool, value: *const ST.Type) !*Self { + const root = try ST.tree.fromValue(pool, value); + errdefer pool.unref(root); + return try Self.init(allocator, pool, root); + } + + pub fn toValue(self: *Self, allocator: Allocator, out: *ST.Type) !void { + _ = allocator; + try self.commit(); + try ST.clone(&self.value, out); + } + + pub fn Field(comptime field_name: []const u8) type { + const ChildST = ST.getFieldType(field_name); + return ChildST.Type; + } + + pub fn get(self: *Self, comptime field_name: []const u8) !Field(field_name) { + return @field(self.value, field_name); + } + + pub fn set(self: *Self, comptime field_name: []const u8, value: Field(field_name)) !void { + @field(self.value, field_name) = value; + self.changed.set(comptime std.meta.fieldIndex(T, field_name).?); + } + + pub fn getValue(self: *Self, allocator: Allocator, comptime field_name: []const u8, out: *Field(field_name)) !void { + _ = allocator; + out.* = try self.get(field_name); + } + + pub fn setValue(self: *Self, comptime field_name: []const u8, value: *const Field(field_name)) !void { + try self.set(field_name, value.*); + } + + pub fn serializeIntoBytes(self: *Self, out: []u8) !usize { + try self.commit(); + return ST.serializeIntoBytes(&self.value, out); + } + + pub fn serializedSize(_: *const Self) usize { + return ST.fixed_size; + } + }; + + assertTreeViewType(TreeView); + return TreeView; +} + +test "StructContainerTreeView" { + const Validator = StructContainerType(struct { + pubkey: ByteVectorType(48), + withdrawal_credentials: ByteVectorType(32), + effective_balance: UintType(64), + slashed: BoolType(), + activation_eligibility_epoch: UintType(64), + activation_epoch: UintType(64), + exit_epoch: UintType(64), + withdrawable_epoch: UintType(64), + }); + + var pool = try Node.Pool.init(std.testing.allocator, 1000); + defer pool.deinit(); + + const validator_value: Validator.Type = .{ + .pubkey = [_]u8{0} ** 48, + .withdrawal_credentials = [_]u8{1} ** 32, + .effective_balance = 32000000000, + .slashed = false, + .activation_eligibility_epoch = 0, + .activation_epoch = 0, + .exit_epoch = 18446744073709551615, + .withdrawable_epoch = 18446744073709551615, + }; + + const root_node = try Validator.tree.fromValue(&pool, &validator_value); + var committed_root: ?Node.Id = null; + { + var validator_view = try StructContainerTreeView(Validator).init(std.testing.allocator, &pool, root_node); + defer validator_view.deinit(); + + var tree_root: [32]u8 = undefined; + try validator_view.hashTreeRootInto(&tree_root); + var out: [32]u8 = undefined; + try Validator.hashTreeRoot(&validator_value, &out); + try std.testing.expectEqualSlices(u8, out[0..], tree_root[0..]); + + try std.testing.expectEqualSlices(u8, ([_]u8{0} ** 48)[0..], &(try validator_view.get("pubkey"))); + try std.testing.expectEqual(32000000000, try validator_view.get("effective_balance")); + try std.testing.expectEqual(false, try validator_view.get("slashed")); + + try validator_view.set("pubkey", [_]u8{2} ** 48); + try validator_view.set("effective_balance", 32100000000); + try validator_view.set("slashed", true); + try validator_view.commit(); + + try std.testing.expect(root_node.getState(&pool).isFree()); + + try std.testing.expectEqualSlices(u8, ([_]u8{2} ** 48)[0..], &(try validator_view.get("pubkey"))); + try std.testing.expectEqual(32100000000, try validator_view.get("effective_balance")); + try std.testing.expectEqual(true, try validator_view.get("slashed")); + + committed_root = validator_view.getRoot(); + try std.testing.expect(!committed_root.?.getState(&pool).isFree()); + } + + try std.testing.expect(committed_root.?.getState(&pool).isFree()); + const reuse_hash: [32]u8 = [_]u8{9} ** 32; + const reused = try pool.createLeaf(&reuse_hash); + defer pool.unref(reused); + try std.testing.expectEqual(committed_root.?, reused); +} + test "ContainerTreeView" { const Foo = FixedContainerType(struct { a: UintType(64), @@ -503,12 +712,14 @@ test "ContainerTreeView" { const FixedContainerType = @import("../type/container.zig").FixedContainerType; const VariableContainerType = @import("../type/container.zig").VariableContainerType; +const StructContainerType = @import("../type/container.zig").StructContainerType; const UintType = @import("../type/uint.zig").UintType; const ByteVectorType = @import("../type/byte_vector.zig").ByteVectorType; const ByteListType = @import("../type/byte_list.zig").ByteListType; const FixedListType = @import("../type/list.zig").FixedListType; const VariableListType = @import("../type/list.zig").VariableListType; const FixedVectorType = @import("../type/vector.zig").FixedVectorType; +const BoolType = @import("../type/bool.zig").BoolType; const Checkpoint = FixedContainerType(struct { epoch: UintType(64), diff --git a/src/ssz/tree_view/root.zig b/src/ssz/tree_view/root.zig index 99fa4da5d..0abf6a048 100644 --- a/src/ssz/tree_view/root.zig +++ b/src/ssz/tree_view/root.zig @@ -1,6 +1,7 @@ const std = @import("std"); pub const ContainerTreeView = @import("container.zig").ContainerTreeView; +pub const StructContainerTreeView = @import("container.zig").StructContainerTreeView; pub const ArrayBasicTreeView = @import("array_basic.zig").ArrayBasicTreeView; pub const ArrayCompositeTreeView = @import("array_composite.zig").ArrayCompositeTreeView; pub const ListBasicTreeView = @import("list_basic.zig").ListBasicTreeView; diff --git a/src/ssz/type/container.zig b/src/ssz/type/container.zig index e4315b22f..8bc1c3d4a 100644 --- a/src/ssz/type/container.zig +++ b/src/ssz/type/container.zig @@ -12,7 +12,9 @@ const maxChunksToDepth = @import("hashing").maxChunksToDepth; const Node = @import("persistent_merkle_tree").Node; const Gindex = @import("persistent_merkle_tree").Gindex; const Depth = @import("persistent_merkle_tree").Depth; +const proof = @import("persistent_merkle_tree").proof; const ContainerTreeView = @import("../tree_view/root.zig").ContainerTreeView; +const StructContainerTreeView = @import("../tree_view/root.zig").StructContainerTreeView; pub fn FixedContainerType(comptime ST: type) type { const ssz_fields = switch (@typeInfo(ST)) { @@ -763,6 +765,118 @@ pub fn VariableContainerType(comptime ST: type) type { }; } +/// Fixed-size container type that uses `StructContainerTreeView` for tree-view operations. +pub fn StructContainerType(comptime ST: type) type { + const FixedCT = FixedContainerType(ST); + + return struct { + pub const kind = TypeKind.container; + pub const Fields: type = ST; + pub const fields: []const std.builtin.Type.StructField = FixedCT.fields; + pub const Type: type = FixedCT.Type; + pub const TreeView: type = StructContainerTreeView(@This()); + pub const fixed_size: usize = FixedCT.fixed_size; + pub const field_offsets: [fields.len]usize = FixedCT.field_offsets; + pub const chunk_count: usize = fields.len; + pub const chunk_depth: Depth = maxChunksToDepth(chunk_count); + pub const default_value: Type = FixedCT.default_value; + pub const default_root: [32]u8 = FixedCT.default_root; + pub const serialized = FixedCT.serialized; + const Self = @This(); + + /// Required interface for branch-struct nodes. + pub const WrappedT = struct { + value: Type, + + pub fn getRoot(self: *const WrappedT, out: *[32]u8) void { + hashTreeRoot(&self.value, out) catch unreachable; + } + + pub fn toTree(self: *const WrappedT, pool: *Node.Pool) !Node.Id { + return try FixedCT.tree.fromValue(pool, &self.value); + } + + pub fn init(allocator: std.mem.Allocator, wrapped: *const WrappedT) !*const WrappedT { + const ptr = try allocator.create(WrappedT); + errdefer allocator.destroy(ptr); + try clone(&wrapped.value, &ptr.value); + return ptr; + } + + pub fn deinit(self: *WrappedT, allocator: std.mem.Allocator) void { + allocator.destroy(self); + } + }; + + pub fn equals(a: *const Type, b: *const Type) bool { + return FixedCT.equals(a, b); + } + + pub fn clone(value: *const Type, out: anytype) !void { + return FixedCT.clone(value, out); + } + + pub fn hashTreeRoot(value: *const Type, out: *[32]u8) !void { + return FixedCT.hashTreeRoot(value, out); + } + + pub fn serializeIntoBytes(value: *const Type, out: []u8) usize { + return FixedCT.serializeIntoBytes(value, out); + } + + pub fn deserializeFromBytes(data: []const u8, out: *Type) !void { + return FixedCT.deserializeFromBytes(data, out); + } + + pub const tree = struct { + pub fn deserializeFromBytes(pool: *Node.Pool, data: []const u8) !Node.Id { + if (data.len != fixed_size) { + return error.InvalidSize; + } + + var wrapped: WrappedT = undefined; + try Self.deserializeFromBytes(data, &wrapped.value); + return try pool.createBranchStruct(WrappedT, &wrapped); + } + + pub fn toValue(node: Node.Id, pool: *Node.Pool, out: *Type) !void { + const wrapped = try pool.getStructPtr(node, WrappedT); + try clone(&wrapped.value, out); + } + + pub fn fromValue(pool: *Node.Pool, value: *const Type) !Node.Id { + const wrapped = WrappedT{ .value = value.* }; + return try pool.createBranchStruct(WrappedT, &wrapped); + } + + pub fn serializeIntoBytes(node: Node.Id, pool: *Node.Pool, out: []u8) !usize { + const wrapped = try pool.getStructPtr(node, WrappedT); + return Self.serializeIntoBytes(&wrapped.value, out); + } + }; + + pub fn serializeIntoJson(writer: anytype, in: *const Type) !void { + return FixedCT.serializeIntoJson(writer, in); + } + + pub fn deserializeFromJson(source: *std.json.Scanner, out: *Type) !void { + return FixedCT.deserializeFromJson(source, out); + } + + pub fn getFieldIndex(comptime name: []const u8) usize { + return FixedCT.getFieldIndex(name); + } + + pub fn getFieldType(comptime name: []const u8) type { + return FixedCT.getFieldType(name); + } + + pub fn getFieldGindex(comptime name: []const u8) Gindex { + return FixedCT.getFieldGindex(name); + } + }; +} + const UintType = @import("uint.zig").UintType; const BoolType = @import("bool.zig").BoolType; const ByteVectorType = @import("byte_vector.zig").ByteVectorType; @@ -961,6 +1075,71 @@ test "FixedContainerType - tree.deserializeFromBytes" { try std.testing.expectEqualSlices(u8, node2.getRoot(&pool), node.getRoot(&pool)); } +test "StructContainerType - single proof materializes a temporary tree for traversal" { + const StructValidator = StructContainerType(struct { + pubkey: ByteVectorType(48), + withdrawal_credentials: ByteVectorType(32), + effective_balance: UintType(64), + slashed: BoolType(), + activation_eligibility_epoch: UintType(64), + activation_epoch: UintType(64), + exit_epoch: UintType(64), + withdrawable_epoch: UintType(64), + }); + const TreeValidator = FixedContainerType(struct { + pubkey: ByteVectorType(48), + withdrawal_credentials: ByteVectorType(32), + effective_balance: UintType(64), + slashed: BoolType(), + activation_eligibility_epoch: UintType(64), + activation_epoch: UintType(64), + exit_epoch: UintType(64), + withdrawable_epoch: UintType(64), + }); + + const validator_value: StructValidator.Type = .{ + .pubkey = [_]u8{0} ** 48, + .withdrawal_credentials = [_]u8{1} ** 32, + .effective_balance = 32000000000, + .slashed = false, + .activation_eligibility_epoch = 1, + .activation_epoch = 2, + .exit_epoch = 3, + .withdrawable_epoch = 4, + }; + + var pool = try Node.Pool.init(std.testing.allocator, 256); + defer pool.deinit(); + + const struct_root = try StructValidator.tree.fromValue(&pool, &validator_value); + defer pool.unref(struct_root); + + const tree_root = try TreeValidator.tree.fromValue(&pool, &validator_value); + defer pool.unref(tree_root); + + const gindex = Gindex.fromDepth(StructValidator.chunk_depth, 2); + + var struct_proof = try proof.createSingleProof(std.testing.allocator, &pool, struct_root, gindex); + defer struct_proof.deinit(std.testing.allocator); + + var tree_proof = try proof.createSingleProof(std.testing.allocator, &pool, tree_root, gindex); + defer tree_proof.deinit(std.testing.allocator); + + try std.testing.expectEqualSlices(u8, &tree_proof.leaf, &struct_proof.leaf); + try std.testing.expectEqual(@as(usize, tree_proof.witnesses.len), struct_proof.witnesses.len); + for (tree_proof.witnesses, struct_proof.witnesses) |expected, actual| { + try std.testing.expectEqualSlices(u8, &expected, &actual); + } + + var pool2 = try Node.Pool.init(std.testing.allocator, 256); + defer pool2.deinit(); + + const reconstructed = try proof.createNodeFromSingleProof(&pool2, gindex, struct_proof.leaf, struct_proof.witnesses); + defer pool2.unref(reconstructed); + + try std.testing.expectEqualSlices(u8, struct_root.getRoot(&pool), reconstructed.getRoot(&pool2)); +} + test "FixedContainerType - serializeIntoBytes (uint64 + ByteVector32)" { const allocator = std.testing.allocator; const Container = FixedContainerType(struct { diff --git a/src/ssz/type/root.zig b/src/ssz/type/root.zig index 1579021e4..78a0ec72e 100644 --- a/src/ssz/type/root.zig +++ b/src/ssz/type/root.zig @@ -27,6 +27,7 @@ pub const VariableVectorType = @import("vector.zig").VariableVectorType; pub const FixedContainerType = @import("container.zig").FixedContainerType; pub const VariableContainerType = @import("container.zig").VariableContainerType; +pub const StructContainerType = @import("container.zig").StructContainerType; const chunk = @import("chunk.zig"); pub const BYTES_PER_CHUNK: usize = chunk.BYTES_PER_CHUNK; diff --git a/src/state_transition/test_utils/generate_state.zig b/src/state_transition/test_utils/generate_state.zig index bab4cadee..f200475e0 100644 --- a/src/state_transition/test_utils/generate_state.zig +++ b/src/state_transition/test_utils/generate_state.zig @@ -142,8 +142,8 @@ pub fn generateElectraState(allocator: Allocator, pool: *Node.Pool, chain_config var validators = try beacon_state.validators(); for (next_sync_committee_indices, 0..next_sync_committee_indices.len) |index, i| { var validator = try validators.get(@intCast(index)); - var pubkey_view = try validator.get("pubkey"); - _ = try pubkey_view.getAllInto(next_sync_committee_pubkeys[i][0..]); + const pubkey = try validator.get("pubkey"); + next_sync_committee_pubkeys[i] = pubkey; next_sync_committee_pubkeys_slices[i] = try bls.PublicKey.uncompress(&next_sync_committee_pubkeys[i]); }