diff --git a/README.md b/README.md index 64140ba..cd89ec4 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,31 @@ Supported types: * `List[N]` * `Bitlist[N]` +## Using Custom Hash Functions + +ssz.zig is hash-function agnostic. Pass your hasher as a type parameter: + +```zig +const std = @import("std"); +const ssz = @import("ssz.zig"); + +// Using SHA256 (from stdlib) +const Sha256 = std.crypto.hash.sha2.Sha256; +try ssz.hashTreeRoot(Sha256, MyType, value, &root, allocator); + +// Using a custom hasher (must implement init/update/final API) +const MyHasher = ...; // Your hasher type +try ssz.hashTreeRoot(MyHasher, MyType, value, &root, allocator); +``` + +**Required Hasher API:** +```zig +pub const Options = struct {}; +pub fn init(_: Options) Self; +pub fn update(self: *Self, data: []const u8) void; +pub fn final(self: *Self, out: *[Self.digest_length]u8) void; // out size matches 32 bytes for SSZ +``` + ## Contributing Simply create an issue or a PR. diff --git a/src/beacon_tests.zig b/src/beacon_tests.zig index 5a60c27..0f4803d 100644 --- a/src/beacon_tests.zig +++ b/src/beacon_tests.zig @@ -6,6 +6,7 @@ const hashTreeRoot = libssz.hashTreeRoot; const std = @import("std"); const ArrayList = std.ArrayList; const expect = std.testing.expect; +const Sha256 = std.crypto.hash.sha2.Sha256; // Beacon chain Validator struct for compatibility testing const Validator = struct { @@ -65,7 +66,7 @@ test "Validator struct hash tree root" { }; var hash: [32]u8 = undefined; - try hashTreeRoot(Validator, validator, &hash, std.testing.allocator); + try hashTreeRoot(Sha256, Validator, validator, &hash, std.testing.allocator); // Validate against expected hash const expected_validator_hash = [_]u8{ 0x70, 0x68, 0xE5, 0x06, 0xCB, 0xFF, 0xCD, 0x31, 0xBD, 0x2D, 0x13, 0x42, 0x5E, 0x4F, 0xDE, 0x98, 0x6E, 0xF3, 0x5E, 0x6F, 0xB5, 0x0F, 0x35, 0x9D, 0x7A, 0x26, 0xB6, 0x33, 0x2E, 0xE2, 0xCB, 0x94 }; @@ -73,7 +74,7 @@ test "Validator struct hash tree root" { // Hash should be deterministic for the same validator var hash2: [32]u8 = undefined; - try hashTreeRoot(Validator, validator, &hash2, std.testing.allocator); + try hashTreeRoot(Sha256, Validator, validator, &hash2, std.testing.allocator); try expect(std.mem.eql(u8, &hash, &hash2)); // Different validator should produce different hash @@ -89,7 +90,7 @@ test "Validator struct hash tree root" { }; var hash3: [32]u8 = undefined; - try hashTreeRoot(Validator, validator2, &hash3, std.testing.allocator); + try hashTreeRoot(Sha256, Validator, validator2, &hash3, std.testing.allocator); try expect(!std.mem.eql(u8, &hash, &hash3)); } @@ -116,12 +117,12 @@ test "Individual Validator serialization and hash" { try expect(std.mem.eql(u8, list.items, &expected_validator_bytes)); // Test hash tree root - var hash: [32]u8 = undefined; - try hashTreeRoot(Validator, validator, &hash, std.testing.allocator); + var root: [32]u8 = undefined; + try hashTreeRoot(Sha256, Validator, validator, &root, std.testing.allocator); // Validate against expected hash const expected_validator_hash = [_]u8{ 0x70, 0x68, 0xE5, 0x06, 0xCB, 0xFF, 0xCD, 0x31, 0xBD, 0x2D, 0x13, 0x42, 0x5E, 0x4F, 0xDE, 0x98, 0x6E, 0xF3, 0x5E, 0x6F, 0xB5, 0x0F, 0x35, 0x9D, 0x7A, 0x26, 0xB6, 0x33, 0x2E, 0xE2, 0xCB, 0x94 }; - try expect(std.mem.eql(u8, &hash, &expected_validator_hash)); + try expect(std.mem.eql(u8, &root, &expected_validator_hash)); } test "List[Validator] serialization and hash tree root" { @@ -191,14 +192,14 @@ test "List[Validator] serialization and hash tree root" { // Test hash tree root var hash1: [32]u8 = undefined; - try hashTreeRoot(ValidatorList, validator_list, &hash1, std.testing.allocator); + try hashTreeRoot(Sha256, ValidatorList, validator_list, &hash1, std.testing.allocator); // Validate against expected hash const expected_validator_list_hash = [_]u8{ 0x54, 0x80, 0xF8, 0x35, 0xD7, 0x52, 0xF7, 0x27, 0xC8, 0xF1, 0xE9, 0xCC, 0x0F, 0x84, 0x2B, 0x25, 0x76, 0xA5, 0x1A, 0xD2, 0xB7, 0xB5, 0x10, 0xF1, 0xA5, 0x39, 0xF7, 0xD8, 0xD0, 0x87, 0xC3, 0xC2 }; try expect(std.mem.eql(u8, &hash1, &expected_validator_list_hash)); var hash2: [32]u8 = undefined; - try hashTreeRoot(ValidatorList, deserialized_list, &hash2, std.testing.allocator); + try hashTreeRoot(Sha256, ValidatorList, deserialized_list, &hash2, std.testing.allocator); // Hash should be the same for original and deserialized lists try expect(std.mem.eql(u8, &hash1, &hash2)); @@ -281,21 +282,21 @@ test "BeamBlockBody with validator array - full cycle" { // Test hash tree root consistency var hash_original: [32]u8 = undefined; - try hashTreeRoot(BeamBlockBody, beam_block_body, &hash_original, std.testing.allocator); + try hashTreeRoot(Sha256, BeamBlockBody, beam_block_body, &hash_original, std.testing.allocator); // Validate against expected hash const expected_beam_block_body_hash = [_]u8{ 0x34, 0xF2, 0xBC, 0x58, 0xA0, 0xBF, 0x20, 0x72, 0x43, 0xF8, 0xC2, 0x5E, 0x0F, 0x83, 0x5E, 0x36, 0x90, 0x73, 0xD5, 0xAC, 0x97, 0x1E, 0x9A, 0x53, 0x71, 0x14, 0xA0, 0xFD, 0x1C, 0xC8, 0xD8, 0xE4 }; try expect(std.mem.eql(u8, &hash_original, &expected_beam_block_body_hash)); var hash_deserialized: [32]u8 = undefined; - try hashTreeRoot(BeamBlockBody, deserialized_body, &hash_deserialized, std.testing.allocator); + try hashTreeRoot(Sha256, BeamBlockBody, deserialized_body, &hash_deserialized, std.testing.allocator); // Hashes should be identical for original and deserialized data try expect(std.mem.eql(u8, &hash_original, &hash_deserialized)); // Test hash determinism var hash_duplicate: [32]u8 = undefined; - try hashTreeRoot(BeamBlockBody, beam_block_body, &hash_duplicate, std.testing.allocator); + try hashTreeRoot(Sha256, BeamBlockBody, beam_block_body, &hash_duplicate, std.testing.allocator); try expect(std.mem.eql(u8, &hash_original, &hash_duplicate)); } @@ -383,10 +384,10 @@ test "Zeam-style List/Bitlist usage with tree root stability" { var state_hash1: [32]u8 = undefined; var state_hash2: [32]u8 = undefined; - try hashTreeRoot(ZeamBeamBlockBody, body, &body_hash1, std.testing.allocator); - try hashTreeRoot(ZeamBeamBlockBody, body, &body_hash2, std.testing.allocator); - try hashTreeRoot(BeamState, state, &state_hash1, std.testing.allocator); - try hashTreeRoot(BeamState, state, &state_hash2, std.testing.allocator); + try hashTreeRoot(Sha256, ZeamBeamBlockBody, body, &body_hash1, std.testing.allocator); + try hashTreeRoot(Sha256, ZeamBeamBlockBody, body, &body_hash2, std.testing.allocator); + try hashTreeRoot(Sha256, BeamState, state, &state_hash1, std.testing.allocator); + try hashTreeRoot(Sha256, BeamState, state, &state_hash2, std.testing.allocator); // Validate against expected hashes const expected_zeam_body_hash = [_]u8{ 0xAA, 0x2C, 0x76, 0x39, 0x96, 0xA6, 0xDD, 0x26, 0x25, 0x13, 0x12, 0x8D, 0xEA, 0xDF, 0xCB, 0x69, 0xF1, 0xEC, 0xEB, 0x60, 0xA8, 0xFF, 0xAC, 0xC7, 0xA7, 0xE4, 0x28, 0x3C, 0x74, 0xAA, 0x6A, 0xE4 }; @@ -452,7 +453,7 @@ test "BeamState with historical roots - comprehensive test" { // Test hash tree root calculation var original_hash: [32]u8 = undefined; - try hashTreeRoot(BeamState, beam_state, &original_hash, std.testing.allocator); + try hashTreeRoot(Sha256, BeamState, beam_state, &original_hash, std.testing.allocator); // Validate against expected hash const expected_comprehensive_beam_state_hash = [_]u8{ 0xBD, 0x36, 0x59, 0x5E, 0x3B, 0x4A, 0x51, 0x9C, 0xF3, 0x5F, 0x4F, 0x96, 0x88, 0x9E, 0x86, 0x10, 0xFF, 0x45, 0x20, 0x49, 0x15, 0xAE, 0x96, 0x2E, 0xF4, 0x0C, 0x81, 0x6B, 0xF7, 0x45, 0x4A, 0x17 }; @@ -488,7 +489,7 @@ test "BeamState with historical roots - comprehensive test" { // Test hash tree root consistency var deserialized_hash: [32]u8 = undefined; - try hashTreeRoot(BeamState, deserialized_state, &deserialized_hash, std.testing.allocator); + try hashTreeRoot(Sha256, BeamState, deserialized_state, &deserialized_hash, std.testing.allocator); // Verify hash tree roots are identical try expect(std.mem.eql(u8, &original_hash, &deserialized_hash)); @@ -526,7 +527,7 @@ test "BeamState with empty historical roots" { // Test hash tree root calculation var original_hash: [32]u8 = undefined; - try hashTreeRoot(SimpleBeamState, beam_state, &original_hash, std.testing.allocator); + try hashTreeRoot(Sha256, SimpleBeamState, beam_state, &original_hash, std.testing.allocator); // Validate against actual hash const expected_empty_beam_state_hash = [_]u8{ 0x58, 0xD2, 0x2B, 0xA0, 0x04, 0x45, 0xE8, 0xB7, 0x39, 0x5E, 0xC3, 0x93, 0x92, 0x45, 0xC6, 0xF1, 0x5A, 0x29, 0x91, 0xA5, 0x70, 0x3F, 0xC5, 0x05, 0x88, 0x10, 0x57, 0xDE, 0x9D, 0xF3, 0x64, 0x10 }; @@ -547,7 +548,7 @@ test "BeamState with empty historical roots" { // Test hash tree root consistency var deserialized_hash: [32]u8 = undefined; - try hashTreeRoot(SimpleBeamState, deserialized_state, &deserialized_hash, std.testing.allocator); + try hashTreeRoot(Sha256, SimpleBeamState, deserialized_state, &deserialized_hash, std.testing.allocator); // Verify hash tree roots are identical try expect(std.mem.eql(u8, &original_hash, &deserialized_hash)); @@ -593,7 +594,7 @@ test "BeamState with maximum historical roots" { // Test hash tree root calculation var original_hash: [32]u8 = undefined; - try hashTreeRoot(MaxBeamState, beam_state, &original_hash, std.testing.allocator); + try hashTreeRoot(Sha256, MaxBeamState, beam_state, &original_hash, std.testing.allocator); // Validate against actual hash const expected_max_beam_state_hash = [_]u8{ 0x3F, 0xFC, 0x7A, 0xA4, 0x85, 0x21, 0xD4, 0x02, 0x36, 0x46, 0x19, 0x2E, 0x8D, 0x73, 0xBC, 0x11, 0x3D, 0x1D, 0xE7, 0xF4, 0xDE, 0xC4, 0xD9, 0x6E, 0x94, 0x52, 0xD2, 0xCB, 0x95, 0xE3, 0x22, 0x9A }; @@ -619,7 +620,7 @@ test "BeamState with maximum historical roots" { // Test hash tree root consistency var deserialized_hash: [32]u8 = undefined; - try hashTreeRoot(MaxBeamState, deserialized_state, &deserialized_hash, std.testing.allocator); + try hashTreeRoot(Sha256, MaxBeamState, deserialized_state, &deserialized_hash, std.testing.allocator); try expect(std.mem.eql(u8, &original_hash, &deserialized_hash)); } @@ -673,7 +674,7 @@ test "BeamState historical roots access and comparison" { // Test hash tree root calculation var original_hash: [32]u8 = undefined; - try hashTreeRoot(AccessBeamState, beam_state, &original_hash, std.testing.allocator); + try hashTreeRoot(Sha256, AccessBeamState, beam_state, &original_hash, std.testing.allocator); // Validate against expected hash const expected_access_beam_state_hash = [_]u8{ 0x22, 0x3E, 0xCB, 0xDD, 0x62, 0x46, 0x7F, 0x7F, 0x0F, 0xA8, 0x2C, 0x91, 0x54, 0x1F, 0xF4, 0xEA, 0xBF, 0x92, 0xB6, 0xB7, 0x67, 0x57, 0x02, 0x67, 0x16, 0xEF, 0x3A, 0xB0, 0x96, 0x4E, 0x91, 0x9E }; @@ -707,7 +708,7 @@ test "BeamState historical roots access and comparison" { // Test hash tree root consistency var deserialized_hash: [32]u8 = undefined; - try hashTreeRoot(AccessBeamState, deserialized_state, &deserialized_hash, std.testing.allocator); + try hashTreeRoot(Sha256, AccessBeamState, deserialized_state, &deserialized_hash, std.testing.allocator); try expect(std.mem.eql(u8, &original_hash, &deserialized_hash)); } @@ -743,7 +744,7 @@ test "SimpleBeamState with empty historical roots" { // Test hash tree root calculation var original_hash: [32]u8 = undefined; - try hashTreeRoot(SimpleBeamState, beam_state, &original_hash, std.testing.allocator); + try hashTreeRoot(Sha256, SimpleBeamState, beam_state, &original_hash, std.testing.allocator); // Validate against actual hash const expected_simple_beam_state_hash = [_]u8{ 0x58, 0xD2, 0x2B, 0xA0, 0x04, 0x45, 0xE8, 0xB7, 0x39, 0x5E, 0xC3, 0x93, 0x92, 0x45, 0xC6, 0xF1, 0x5A, 0x29, 0x91, 0xA5, 0x70, 0x3F, 0xC5, 0x05, 0x88, 0x10, 0x57, 0xDE, 0x9D, 0xF3, 0x64, 0x10 }; @@ -763,7 +764,7 @@ test "SimpleBeamState with empty historical roots" { // Test hash tree root consistency var deserialized_hash: [32]u8 = undefined; - try hashTreeRoot(SimpleBeamState, deserialized_state, &deserialized_hash, std.testing.allocator); + try hashTreeRoot(Sha256, SimpleBeamState, deserialized_state, &deserialized_hash, std.testing.allocator); // Verify hash tree roots are identical try expect(std.mem.eql(u8, &original_hash, &deserialized_hash)); @@ -775,7 +776,7 @@ test "hashTreeRoot for pointer types" { // Test pointer size .one - SUPPORTED { var value: u32 = 8; - try hashTreeRoot(*u32, &value, &hash, std.testing.allocator); + try hashTreeRoot(Sha256, *u32, &value, &hash, std.testing.allocator); var deserialized: u32 = undefined; try deserialize(u32, &hash, &deserialized, std.testing.allocator); @@ -786,7 +787,7 @@ test "hashTreeRoot for pointer types" { { var values = [4]u8{ 0xAA, 0xBB, 0xCC, 0xDD }; const values_ptr: *[4]u8 = &values; - try hashTreeRoot(*[4]u8, values_ptr, &hash, std.testing.allocator); + try hashTreeRoot(Sha256, *[4]u8, values_ptr, &hash, std.testing.allocator); var deserialized: [4]u8 = undefined; try deserialize([4]u8, &hash, &deserialized, std.testing.allocator); @@ -797,13 +798,13 @@ test "hashTreeRoot for pointer types" { { var values = [4]u8{ 0xAA, 0xBB, 0xCC, 0xDD }; const values_ptr: [*]u8 = &values; - try std.testing.expectError(error.UnSupportedPointerType, hashTreeRoot([*]u8, values_ptr, &hash, std.testing.allocator)); + try std.testing.expectError(error.UnSupportedPointerType, hashTreeRoot(Sha256, [*]u8, values_ptr, &hash, std.testing.allocator)); } // Test pointer size .c - should return error { var values = [4]u8{ 0xAA, 0xBB, 0xCC, 0xDD }; const values_ptr: [*c]u8 = &values; - try std.testing.expectError(error.UnSupportedPointerType, hashTreeRoot([*c]u8, values_ptr, &hash, std.testing.allocator)); + try std.testing.expectError(error.UnSupportedPointerType, hashTreeRoot(Sha256, [*c]u8, values_ptr, &hash, std.testing.allocator)); } } diff --git a/src/lib.zig b/src/lib.zig index e3e6003..00195cb 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -6,14 +6,13 @@ pub const utils = @import("./utils.zig"); pub const zeros = @import("./zeros.zig"); const ArrayList = std.ArrayList; const builtin = std.builtin; -const sha256 = std.crypto.hash.sha2.Sha256; -const hashes_of_zero = zeros.hashes_of_zero; const Allocator = std.mem.Allocator; +const Sha256 = std.crypto.hash.sha2.Sha256; /// Number of bytes per chunk. const BYTES_PER_CHUNK = 32; -pub fn serializedFixedSize(comptime T: type) !usize { +pub fn serializedFixedSize(T: type) !usize { const info = @typeInfo(T); return switch (info) { .int => @sizeOf(T), @@ -40,7 +39,7 @@ pub fn serializedFixedSize(comptime T: type) !usize { // Determine the serialized size of an object so that // the code serializing of variable-size objects can // determine the offset to the next object. -pub fn serializedSize(comptime T: type, data: T) !usize { +pub fn serializedSize(T: type, data: T) !usize { // Check for custom serializedSize method first for List types if (comptime std.meta.hasFn(T, "serializedSize")) { return data.serializedSize(); @@ -96,7 +95,7 @@ pub fn serializedSize(comptime T: type, data: T) !usize { } /// Returns true if an object is of fixed size -pub fn isFixedSizeObject(comptime T: type) !bool { +pub fn isFixedSizeObject(T: type) !bool { if (comptime std.meta.hasFn(T, "isFixedSizeObject")) { return T.isFixedSizeObject(); } @@ -122,7 +121,7 @@ pub fn isFixedSizeObject(comptime T: type) !bool { /// Provides the generic serialization of any `data` var to SSZ. The /// serialization is written to the `ArrayList` `l`. -pub fn serialize(comptime T: type, data: T, l: *ArrayList(u8)) !void { +pub fn serialize(T: type, data: T, l: *ArrayList(u8)) !void { // shortcut if the type implements its own encode method if (comptime std.meta.hasFn(T, "sszEncode")) { return data.sszEncode(l); @@ -316,7 +315,7 @@ pub fn serialize(comptime T: type, data: T, l: *ArrayList(u8)) !void { /// Takes a byte array containing the serialized payload of type `T` (with /// possible trailing data) and deserializes it into the `T` object pointed /// at by `out`. -pub fn deserialize(comptime T: type, serialized: []const u8, out: *T, allocator: ?std.mem.Allocator) !void { +pub fn deserialize(T: type, serialized: []const u8, out: *T, allocator: ?std.mem.Allocator) !void { // shortcut if the type implements its own decode method if (comptime std.meta.hasFn(T, "sszDecode")) { return T.sszDecode(serialized, out, allocator); @@ -517,8 +516,8 @@ pub fn deserialize(comptime T: type, serialized: []const u8, out: *T, allocator: } } -pub fn mixInLength2(root: [32]u8, length: usize, out: *[32]u8) void { - var hasher = sha256.init(sha256.Options{}); +pub fn mixInLength2(Hasher: type, root: [Hasher.digest_length]u8, length: usize, out: *[Hasher.digest_length]u8) void { + var hasher = Hasher.init(Hasher.Options{}); hasher.update(root[0..]); var tmp = [_]u8{0} ** 32; @@ -527,8 +526,8 @@ pub fn mixInLength2(root: [32]u8, length: usize, out: *[32]u8) void { hasher.final(out[0..]); } -fn mixInLength(root: [32]u8, length: [32]u8, out: *[32]u8) void { - var hasher = sha256.init(sha256.Options{}); +fn mixInLength(Hasher: type, root: [Hasher.digest_length]u8, length: [32]u8, out: *[Hasher.digest_length]u8) void { + var hasher = Hasher.init(Hasher.Options{}); hasher.update(root[0..]); hasher.update(length[0..]); hasher.final(out[0..]); @@ -542,13 +541,13 @@ test "mixInLength" { _ = try std.fmt.hexToBytes(root[0..], "2279cf111c15f2d594e7a0055e8735e7409e56ed4250735d6d2f2b0d1bcf8297"); _ = try std.fmt.hexToBytes(length[0..], "deadbeef00000000000000000000000000000000000000000000000000000000"); _ = try std.fmt.hexToBytes(expected[0..], "0b665dda6e4c269730bc4bbe3e990a69d37fa82892bac5fe055ca4f02a98c900"); - mixInLength(root, length, &mixin); + mixInLength(Sha256, root, length, &mixin); try std.testing.expect(std.mem.eql(u8, mixin[0..], expected[0..])); } -fn mixInSelector(root: [32]u8, comptime selector: usize, out: *[32]u8) void { - var hasher = sha256.init(sha256.Options{}); +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..]); var tmp = [_]u8{0} ** 32; std.mem.writeInt(@TypeOf(selector), tmp[0..@sizeOf(@TypeOf(selector))], selector, std.builtin.Endian.little); @@ -562,14 +561,14 @@ test "mixInSelector" { var mixin: [32]u8 = undefined; _ = try std.fmt.hexToBytes(root[0..], "2279cf111c15f2d594e7a0055e8735e7409e56ed4250735d6d2f2b0d1bcf8297"); _ = try std.fmt.hexToBytes(expected[0..], "c483cb731afcfe9f2c596698eaca1c4e0dcb4a1136297adef74c31c268966eb5"); - mixInSelector(root, 25, &mixin); + mixInSelector(Sha256, root, 25, &mixin); try std.testing.expect(std.mem.eql(u8, mixin[0..], expected[0..])); } /// Calculates the number of leaves needed for the merkelization /// of this type. -pub fn chunkCount(comptime T: type) usize { +pub fn chunkCount(T: type) usize { const info = @typeInfo(T); switch (info) { .int, .bool => return 1, @@ -591,7 +590,7 @@ pub fn chunkCount(comptime T: type) usize { const chunk = [BYTES_PER_CHUNK]u8; const zero_chunk: chunk = [_]u8{0} ** BYTES_PER_CHUNK; -pub fn pack(comptime T: type, values: T, l: *ArrayList(u8)) ![]chunk { +pub fn pack(T: type, values: T, l: *ArrayList(u8)) ![]chunk { try serialize(T, values, l); const padding_size = (BYTES_PER_CHUNK - l.items.len % BYTES_PER_CHUNK) % BYTES_PER_CHUNK; _ = try l.writer().write(zero_chunk[0..padding_size]); @@ -636,7 +635,10 @@ test "pack string" { } // merkleize recursively calculates the root hash of a Merkle tree. -pub fn merkleize(hasher: type, chunks: []chunk, limit: ?usize, out: *[32]u8) anyerror!void { +pub fn merkleize(Hasher: type, chunks: []chunk, limit: ?usize, out: *[Hasher.digest_length]u8) anyerror!void { + // Generate zero hashes for this hasher type at comptime + const hashes_of_zero = comptime zeros.buildHashesOfZero(Hasher, 32, 256); + // Calculate the number of chunks to be padded, check the limit if (limit != null and chunks.len > limit.?) { return error.ChunkSizeExceedsLimit; @@ -652,24 +654,21 @@ pub fn merkleize(hasher: type, chunks: []chunk, limit: ?usize, out: *[32]u8) any // Merkleize the left side. If the number of chunks // isn't enough to fill the entire width, complete // with zeroes. - var digest = hasher.init(hasher.Options{}); + var digest = Hasher.init(Hasher.Options{}); var buf: [32]u8 = undefined; const split = if (size / 2 < chunks.len) size / 2 else chunks.len; - try merkleize(hasher, chunks[0..split], size / 2, &buf); + try merkleize(Hasher, chunks[0..split], size / 2, &buf); digest.update(buf[0..]); // Merkleize the right side. If the number of chunks only // covers the first half, directly input the hashed zero- // filled subtrie. if (size / 2 < chunks.len) { - try merkleize(hasher, chunks[size / 2 ..], size / 2, &buf); + try merkleize(Hasher, chunks[size / 2 ..], size / 2, &buf); digest.update(buf[0..]); } else { // Use depth-based indexing for zero hashes // For a subtree of size/2 leaves, we need the zero hash at depth log2(size/2) - // hashes_of_zero[0] = single zero chunk (depth 0) - // hashes_of_zero[1] = hash of 2 zero chunks (depth 1) - // hashes_of_zero[d] = hash of 2^d zero chunks (depth d) const subtree_size = size / 2; const depth = std.math.log2_int(usize, subtree_size); digest.update(hashes_of_zero[depth][0..]); @@ -684,7 +683,7 @@ test "merkleize an empty slice" { defer list.deinit(); const chunks = &[0][32]u8{}; var out: [32]u8 = undefined; - try merkleize(sha256, chunks, null, &out); + try merkleize(Sha256, chunks, null, &out); try std.testing.expect(std.mem.eql(u8, out[0..], zero_chunk[0..])); } @@ -693,22 +692,22 @@ test "merkleize a string" { defer list.deinit(); const chunks = try pack([]const u8, "a" ** 100, &list); var out: [32]u8 = undefined; - try merkleize(sha256, chunks, null, &out); + try merkleize(Sha256, chunks, null, &out); // Build the expected tree const leaf1 = [_]u8{0x61} ** 32; // "0xaaaaa....aa" 32 times var leaf2: [32]u8 = [_]u8{0x61} ** 4 ++ [_]u8{0} ** 28; var root: [32]u8 = undefined; var internal_left: [32]u8 = undefined; var internal_right: [32]u8 = undefined; - var hasher = sha256.init(sha256.Options{}); + var hasher = Sha256.init(Sha256.Options{}); hasher.update(leaf1[0..]); hasher.update(leaf1[0..]); hasher.final(&internal_left); - hasher = sha256.init(sha256.Options{}); + hasher = Sha256.init(Sha256.Options{}); hasher.update(leaf1[0..]); hasher.update(leaf2[0..]); hasher.final(&internal_right); - hasher = sha256.init(sha256.Options{}); + hasher = Sha256.init(Sha256.Options{}); hasher.update(internal_left[0..]); hasher.update(internal_right[0..]); hasher.final(&root); @@ -723,7 +722,7 @@ test "merkleize a boolean" { var chunks = try pack(bool, false, &list); var expected = [_]u8{0} ** BYTES_PER_CHUNK; var out: [BYTES_PER_CHUNK]u8 = undefined; - try merkleize(sha256, chunks, null, &out); + try merkleize(Sha256, chunks, null, &out); try std.testing.expect(std.mem.eql(u8, out[0..], expected[0..])); @@ -732,7 +731,7 @@ test "merkleize a boolean" { chunks = try pack(bool, true, &list2); expected[0] = 1; - try merkleize(sha256, chunks, null, &out); + try merkleize(Sha256, chunks, null, &out); try std.testing.expect(std.mem.eql(u8, out[0..], expected[0..])); } @@ -765,10 +764,10 @@ fn packBits(bits: []const bool, l: *ArrayList(u8)) ![]chunk { return std.mem.bytesAsSlice(chunk, l.items); } -pub fn hashTreeRoot(comptime T: type, value: T, out: *[32]u8, allctr: Allocator) !void { +pub fn hashTreeRoot(Hasher: type, T: type, value: T, out: *[Hasher.digest_length]u8, allctr: Allocator) !void { // Check if type has its own hashTreeRoot method at compile time if (comptime std.meta.hasFn(T, "hashTreeRoot")) { - return value.hashTreeRoot(out, allctr); + return value.hashTreeRoot(Hasher, out, allctr); } const type_info = @typeInfo(T); @@ -777,7 +776,7 @@ pub fn hashTreeRoot(comptime T: type, value: T, out: *[32]u8, allctr: Allocator) var list = ArrayList(u8).init(allctr); defer list.deinit(); const chunks = try pack(T, value, &list); - try merkleize(sha256, chunks, null, out); + try merkleize(Hasher, chunks, null, out); }, .array => |a| { // Check if the child is a basic type. If so, return @@ -789,30 +788,30 @@ pub fn hashTreeRoot(comptime T: type, value: T, out: *[32]u8, allctr: Allocator) var list = ArrayList(u8).init(allctr); defer list.deinit(); const chunks = try pack(T, value, &list); - try merkleize(sha256, chunks, null, out); + try merkleize(Hasher, chunks, null, out); }, .bool => { var list = ArrayList(u8).init(allctr); defer list.deinit(); const chunks = try packBits(value[0..], &list); - try merkleize(sha256, chunks, chunkCount(T), out); + try merkleize(Hasher, chunks, chunkCount(T), out); }, .array => { var chunks = ArrayList(chunk).init(allctr); defer chunks.deinit(); var tmp: chunk = undefined; for (value) |item| { - try hashTreeRoot(@TypeOf(item), item, &tmp, allctr); + try hashTreeRoot(Hasher, @TypeOf(item), item, &tmp, allctr); try chunks.append(tmp); } - try merkleize(sha256, chunks.items, null, out); + try merkleize(Hasher, chunks.items, null, out); }, else => return error.NotSupported, } }, .pointer => |ptr| { switch (ptr.size) { - .one => try hashTreeRoot(ptr.child, value.*, out, allctr), + .one => try hashTreeRoot(Hasher, ptr.child, value.*, out, allctr), .slice => { switch (@typeInfo(ptr.child)) { .int => { @@ -820,8 +819,8 @@ pub fn hashTreeRoot(comptime T: type, value: T, out: *[32]u8, allctr: Allocator) defer list.deinit(); const chunks = try pack(T, value, &list); var tmp: chunk = undefined; - try merkleize(sha256, chunks, null, &tmp); - mixInLength2(tmp, value.len, out); + try merkleize(Hasher, chunks, null, &tmp); + mixInLength2(Hasher, tmp, value.len, out); }, // use bitlist .bool => return error.UnSupportedPointerType, @@ -831,11 +830,11 @@ pub fn hashTreeRoot(comptime T: type, value: T, out: *[32]u8, allctr: Allocator) defer chunks.deinit(); var tmp: chunk = undefined; for (value) |item| { - try hashTreeRoot(@TypeOf(item), item, &tmp, allctr); + try hashTreeRoot(Hasher, @TypeOf(item), item, &tmp, allctr); try chunks.append(tmp); } - try merkleize(sha256, chunks.items, null, &tmp); - mixInLength2(tmp, chunks.items.len, out); + try merkleize(Hasher, chunks.items, null, &tmp); + mixInLength2(Hasher, tmp, chunks.items.len, out); }, } }, @@ -847,18 +846,18 @@ pub fn hashTreeRoot(comptime T: type, value: T, out: *[32]u8, allctr: Allocator) defer chunks.deinit(); var tmp: chunk = undefined; inline for (str.fields) |f| { - try hashTreeRoot(f.type, @field(value, f.name), &tmp, allctr); + try hashTreeRoot(Hasher, f.type, @field(value, f.name), &tmp, allctr); try chunks.append(tmp); } - try merkleize(sha256, chunks.items, null, out); + try merkleize(Hasher, chunks.items, null, out); }, // An optional is a union with `None` as first value. .optional => |opt| if (value != null) { var tmp: chunk = undefined; - try hashTreeRoot(opt.child, value.?, &tmp, allctr); - mixInSelector(tmp, 1, out); + try hashTreeRoot(Hasher, opt.child, value.?, &tmp, allctr); + mixInSelector(Hasher, tmp, 1, out); } else { - mixInSelector(zero_chunk, 0, out); + mixInSelector(Hasher, zero_chunk, 0, out); }, .@"union" => |u| { if (u.tag_type == null) { @@ -867,8 +866,8 @@ pub fn hashTreeRoot(comptime T: type, value: T, out: *[32]u8, allctr: Allocator) inline for (u.fields, 0..) |f, index| { if (@intFromEnum(value) == index) { var tmp: chunk = undefined; - try hashTreeRoot(f.type, @field(value, f.name), &tmp, allctr); - mixInSelector(tmp, index, out); + try hashTreeRoot(Hasher, f.type, @field(value, f.name), &tmp, allctr); + mixInSelector(Hasher, tmp, index, out); } } }, diff --git a/src/tests.zig b/src/tests.zig index 0a402ea..6055aa3 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -10,8 +10,9 @@ const std = @import("std"); const ArrayList = std.ArrayList; const expect = std.testing.expect; const expectError = std.testing.expectError; -const sha256 = std.crypto.hash.sha2.Sha256; -const hashes_of_zero = libssz.zeros.hashes_of_zero; +const Sha256 = std.crypto.hash.sha2.Sha256; +const zeros = @import("zeros.zig"); +const hashes_of_zero = zeros.hashes_of_zero; test "serializes uint8" { const data: u8 = 0x55; @@ -531,22 +532,22 @@ const e_bits = bytesToBits(16, e_bytes); test "calculate the root hash of a boolean" { var expected = [_]u8{1} ++ [_]u8{0} ** 31; var hashed: [32]u8 = undefined; - try hashTreeRoot(bool, true, &hashed, std.testing.allocator); + try hashTreeRoot(Sha256, bool, true, &hashed, std.testing.allocator); try expect(std.mem.eql(u8, hashed[0..], expected[0..])); expected = hashes_of_zero[0]; - try hashTreeRoot(bool, false, &hashed, std.testing.allocator); + try hashTreeRoot(Sha256, bool, false, &hashed, std.testing.allocator); try expect(std.mem.eql(u8, hashed[0..], expected[0..])); } test "calculate root hash of an array of two Bitvector[128]" { const deserialized: [2][128]bool = [2][128]bool{ a_bits, b_bits }; var hashed: [32]u8 = undefined; - try hashTreeRoot(@TypeOf(deserialized), deserialized, &hashed, std.testing.allocator); + try hashTreeRoot(Sha256, @TypeOf(deserialized), deserialized, &hashed, std.testing.allocator); var expected: [32]u8 = undefined; const expected_preimage = a_bytes ++ empty_bytes ++ b_bytes ++ empty_bytes; - sha256.hash(expected_preimage[0..], &expected, sha256.Options{}); + Sha256.hash(expected_preimage[0..], &expected, Sha256.Options{}); try expect(std.mem.eql(u8, hashed[0..], expected[0..])); } @@ -554,22 +555,22 @@ test "calculate root hash of an array of two Bitvector[128]" { test "calculate the root hash of an array of integers" { var expected = [_]u8{ 0xef, 0xbe, 0xad, 0xde, 0xfe, 0xca, 0xfe, 0xca } ++ [_]u8{0} ** 24; var hashed: [32]u8 = undefined; - try hashTreeRoot([2]u32, [_]u32{ 0xdeadbeef, 0xcafecafe }, &hashed, std.testing.allocator); + try hashTreeRoot(Sha256, [2]u32, [_]u32{ 0xdeadbeef, 0xcafecafe }, &hashed, std.testing.allocator); try expect(std.mem.eql(u8, hashed[0..], expected[0..])); } test "calculate root hash of an array of three Bitvector[128]" { const deserialized: [3][128]bool = [3][128]bool{ a_bits, b_bits, c_bits }; var hashed: [32]u8 = undefined; - try hashTreeRoot(@TypeOf(deserialized), deserialized, &hashed, std.testing.allocator); + try hashTreeRoot(Sha256, @TypeOf(deserialized), deserialized, &hashed, std.testing.allocator); var left: [32]u8 = undefined; var expected: [32]u8 = undefined; const preimg1 = a_bytes ++ empty_bytes ++ b_bytes ++ empty_bytes; const preimg2 = c_bytes ++ empty_bytes ** 3; - sha256.hash(preimg1[0..], &left, sha256.Options{}); - sha256.hash(preimg2[0..], &expected, sha256.Options{}); - var digest = sha256.init(sha256.Options{}); + Sha256.hash(preimg1[0..], &left, Sha256.Options{}); + Sha256.hash(preimg2[0..], &expected, Sha256.Options{}); + var digest = Sha256.init(Sha256.Options{}); digest.update(left[0..]); digest.update(expected[0..]); digest.final(&expected); @@ -580,7 +581,7 @@ test "calculate root hash of an array of three Bitvector[128]" { test "calculate the root hash of an array of five Bitvector[128]" { const deserialized = [5][128]bool{ a_bits, b_bits, c_bits, d_bits, e_bits }; var hashed: [32]u8 = undefined; - try hashTreeRoot(@TypeOf(deserialized), deserialized, &hashed, std.testing.allocator); + try hashTreeRoot(Sha256, @TypeOf(deserialized), deserialized, &hashed, std.testing.allocator); var internal_nodes: [64]u8 = undefined; var left: [32]u8 = undefined; @@ -590,21 +591,21 @@ test "calculate the root hash of an array of five Bitvector[128]" { const preimg3 = e_bytes ++ empty_bytes ** 3; const preimg4 = empty_bytes ** 4; - sha256.hash(preimg1[0..], &left, sha256.Options{}); - sha256.hash(preimg2[0..], internal_nodes[0..32], sha256.Options{}); - var digest = sha256.init(sha256.Options{}); + Sha256.hash(preimg1[0..], &left, Sha256.Options{}); + Sha256.hash(preimg2[0..], internal_nodes[0..32], Sha256.Options{}); + var digest = Sha256.init(Sha256.Options{}); digest.update(left[0..]); digest.update(internal_nodes[0..32]); digest.final(internal_nodes[0..32]); - sha256.hash(preimg3[0..], &left, sha256.Options{}); - sha256.hash(preimg4[0..], internal_nodes[32..], sha256.Options{}); - digest = sha256.init(sha256.Options{}); + Sha256.hash(preimg3[0..], &left, Sha256.Options{}); + Sha256.hash(preimg4[0..], internal_nodes[32..], Sha256.Options{}); + digest = Sha256.init(Sha256.Options{}); digest.update(left[0..]); digest.update(internal_nodes[32..]); digest.final(internal_nodes[32..]); - sha256.hash(internal_nodes[0..], &expected, sha256.Options{}); + Sha256.hash(internal_nodes[0..], &expected, Sha256.Options{}); try expect(std.mem.eql(u8, hashed[0..], expected[0..])); } @@ -624,7 +625,7 @@ test "calculate the root hash of a structure" { }; var expected: [32]u8 = undefined; _ = try std.fmt.hexToBytes(expected[0..], "58316a908701d3660123f0b8cb7839abdd961f71d92993d34e4f480fbec687d9"); - try hashTreeRoot(Fork, fork, &hashed, std.testing.allocator); + try hashTreeRoot(Sha256, Fork, fork, &hashed, std.testing.allocator); try expect(std.mem.eql(u8, hashed[0..], expected[0..])); } @@ -636,13 +637,13 @@ test "calculate the root hash of an Optional" { var expected: [32]u8 = undefined; _ = try std.fmt.hexToBytes(payload[0..], "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); - sha256.hash(payload[0..], expected[0..], sha256.Options{}); - try hashTreeRoot(?u32, v, &hashed, std.testing.allocator); + Sha256.hash(payload[0..], expected[0..], Sha256.Options{}); + try hashTreeRoot(Sha256, ?u32, v, &hashed, std.testing.allocator); try expect(std.mem.eql(u8, hashed[0..], expected[0..])); _ = try std.fmt.hexToBytes(payload[0..], "efbeadde000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000"); - sha256.hash(payload[0..], expected[0..], sha256.Options{}); - try hashTreeRoot(?u32, u, &hashed, std.testing.allocator); + Sha256.hash(payload[0..], expected[0..], Sha256.Options{}); + try hashTreeRoot(Sha256, ?u32, u, &hashed, std.testing.allocator); try expect(std.mem.eql(u8, hashed[0..], expected[0..])); } @@ -655,14 +656,14 @@ test "calculate the root hash of an union" { var payload: [64]u8 = undefined; _ = try std.fmt.hexToBytes(payload[0..], "d2040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); var exp1: [32]u8 = undefined; - sha256.hash(payload[0..], exp1[0..], sha256.Options{}); - try hashTreeRoot(Payload, Payload{ .int = 1234 }, &out, std.testing.allocator); + Sha256.hash(payload[0..], exp1[0..], Sha256.Options{}); + try hashTreeRoot(Sha256, Payload, Payload{ .int = 1234 }, &out, std.testing.allocator); try expect(std.mem.eql(u8, out[0..], exp1[0..])); var exp2: [32]u8 = undefined; _ = try std.fmt.hexToBytes(payload[0..], "01000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000"); - sha256.hash(payload[0..], exp2[0..], sha256.Options{}); - try hashTreeRoot(Payload, Payload{ .boolean = true }, &out, std.testing.allocator); + Sha256.hash(payload[0..], exp2[0..], Sha256.Options{}); + try hashTreeRoot(Sha256, Payload, Payload{ .boolean = true }, &out, std.testing.allocator); try expect(std.mem.eql(u8, out[0..], exp2[0..])); } @@ -927,6 +928,7 @@ test "slice hashtree root composite type" { var hash_root: [32]u8 = undefined; try hashTreeRoot( + Sha256, RootsList, &roots_list, &hash_root, @@ -944,6 +946,7 @@ test "slice hashtree root simple type" { var hash_root: [32]u8 = undefined; try hashTreeRoot( + Sha256, DynamicRoot, &test_root, &hash_root, @@ -969,8 +972,8 @@ test "List tree root calculation" { var empty_hash: [32]u8 = undefined; var filled_hash: [32]u8 = undefined; - try hashTreeRoot(ListU64, empty_list, &empty_hash, std.testing.allocator); - try hashTreeRoot(ListU64, list_with_items, &filled_hash, std.testing.allocator); + try hashTreeRoot(Sha256, ListU64, empty_list, &empty_hash, std.testing.allocator); + try hashTreeRoot(Sha256, ListU64, list_with_items, &filled_hash, std.testing.allocator); try expect(std.mem.eql(u8, &filled_hash, &list_with_items_expected)); try expect(!std.mem.eql(u8, &empty_hash, &filled_hash)); @@ -982,7 +985,7 @@ test "List tree root calculation" { try same_content_list.append(456); var same_content_hash: [32]u8 = undefined; - try hashTreeRoot(ListU64, same_content_list, &same_content_hash, std.testing.allocator); + try hashTreeRoot(Sha256, ListU64, same_content_list, &same_content_hash, std.testing.allocator); try expect(std.mem.eql(u8, &filled_hash, &same_content_hash)); } @@ -1002,8 +1005,8 @@ test "Bitlist tree root calculation" { var empty_hash: [32]u8 = undefined; var filled_hash: [32]u8 = undefined; - try hashTreeRoot(TestBitlist, empty_bitlist, &empty_hash, std.testing.allocator); - try hashTreeRoot(TestBitlist, filled_bitlist, &filled_hash, std.testing.allocator); + try hashTreeRoot(Sha256, TestBitlist, empty_bitlist, &empty_hash, std.testing.allocator); + try hashTreeRoot(Sha256, TestBitlist, filled_bitlist, &filled_hash, std.testing.allocator); try expect(!std.mem.eql(u8, &empty_hash, &filled_hash)); @@ -1015,7 +1018,7 @@ test "Bitlist tree root calculation" { try same_content_bitlist.append(true); var same_content_hash: [32]u8 = undefined; - try hashTreeRoot(TestBitlist, same_content_bitlist, &same_content_hash, std.testing.allocator); + try hashTreeRoot(Sha256, TestBitlist, same_content_bitlist, &same_content_hash, std.testing.allocator); try expect(std.mem.eql(u8, &filled_hash, &same_content_hash)); } @@ -1029,7 +1032,7 @@ test "List of composite types tree root" { try pastry_list.append(Pastry{ .name = "muffin", .weight = 30 }); var hash1: [32]u8 = undefined; - try hashTreeRoot(ListOfPastry, pastry_list, &hash1, std.testing.allocator); + try hashTreeRoot(Sha256, ListOfPastry, pastry_list, &hash1, std.testing.allocator); var pastry_list2 = try ListOfPastry.init(std.testing.allocator); defer pastry_list2.deinit(); @@ -1037,13 +1040,13 @@ test "List of composite types tree root" { try pastry_list2.append(Pastry{ .name = "muffin", .weight = 30 }); var hash2: [32]u8 = undefined; - try hashTreeRoot(ListOfPastry, pastry_list2, &hash2, std.testing.allocator); + try hashTreeRoot(Sha256, ListOfPastry, pastry_list2, &hash2, std.testing.allocator); try expect(std.mem.eql(u8, &hash1, &hash2)); try pastry_list2.append(Pastry{ .name = "bagel", .weight = 25 }); var hash3: [32]u8 = undefined; - try hashTreeRoot(ListOfPastry, pastry_list2, &hash3, std.testing.allocator); + try hashTreeRoot(Sha256, ListOfPastry, pastry_list2, &hash3, std.testing.allocator); try expect(!std.mem.eql(u8, &hash1, &hash3)); } @@ -1280,7 +1283,7 @@ test "Empty List hash tree root" { defer empty_list.deinit(); var hash: [32]u8 = undefined; - try hashTreeRoot(ListU32, empty_list, &hash, std.testing.allocator); + try hashTreeRoot(Sha256, ListU32, empty_list, &hash, std.testing.allocator); // Updated to correct SSZ-compliant hash that uses max capacity for merkleization const zig_expected = [_]u8{ @@ -1298,7 +1301,7 @@ test "Empty BitList(<=256) hash tree root" { defer empty_list.deinit(); var hash: [32]u8 = undefined; - try hashTreeRoot(BitListLen100, empty_list, &hash, std.testing.allocator); + try hashTreeRoot(Sha256, BitListLen100, empty_list, &hash, std.testing.allocator); const zig_expected = [_]u8{ 0xf5, 0xa5, 0xfd, 0x42, 0xd1, 0x6a, 0x20, 0x30, @@ -1315,7 +1318,7 @@ test "Empty BitList (>256) hash tree root" { defer empty_list.deinit(); var hash: [32]u8 = undefined; - try hashTreeRoot(BitListLen100, empty_list, &hash, std.testing.allocator); + try hashTreeRoot(Sha256, BitListLen100, empty_list, &hash, std.testing.allocator); const zig_expected = [_]u8{ 0x79, 0x29, 0x30, 0xbb, 0xd5, 0xba, 0xac, 0x43, @@ -1342,7 +1345,7 @@ test "List at maximum capacity" { // Test hash tree root at capacity var hash: [32]u8 = undefined; - try hashTreeRoot(ListU8, full_list, &hash, std.testing.allocator); + try hashTreeRoot(Sha256, ListU8, full_list, &hash, std.testing.allocator); // Python reference: List[uint8, 4] with [1,2,3,4] const expected = [_]u8{ @@ -1358,7 +1361,7 @@ test "Array hash tree root" { const data: [4]u32 = .{ 1, 2, 3, 4 }; var hash: [32]u8 = undefined; - try hashTreeRoot([4]u32, data, &hash, std.testing.allocator); + try hashTreeRoot(Sha256, [4]u32, data, &hash, std.testing.allocator); // Python reference: Vector[uint32, 4] with [1,2,3,4] // For basic types packed in one chunk, hash is the serialized data @@ -1396,7 +1399,7 @@ test "Large Bitvector serialization and hash" { // Test hash tree root var hash: [32]u8 = undefined; - try hashTreeRoot(LargeBitvec, data, &hash, std.testing.allocator); + try hashTreeRoot(Sha256, LargeBitvec, data, &hash, std.testing.allocator); const expected = [_]u8{ 0x1d, 0x83, 0x09, 0x11, 0x4a, 0xfe, 0xf7, 0x14, 0x89, 0xbe, 0x68, 0xd4, 0x5e, 0x18, 0xc3, 0x39, @@ -1417,7 +1420,7 @@ test "Bitlist edge cases" { } var hash1: [32]u8 = undefined; - try hashTreeRoot(TestBitlist, all_false, &hash1, std.testing.allocator); + try hashTreeRoot(Sha256, TestBitlist, all_false, &hash1, std.testing.allocator); const expected_false = [_]u8{ 0x02, 0xc8, 0xc1, 0x5f, 0xed, 0x3f, 0x1b, 0x86, @@ -1435,7 +1438,7 @@ test "Bitlist edge cases" { } var hash2: [32]u8 = undefined; - try hashTreeRoot(TestBitlist, all_true, &hash2, std.testing.allocator); + try hashTreeRoot(Sha256, TestBitlist, all_true, &hash2, std.testing.allocator); // Python reference: Bitlist[100] with 50 true bits const expected_true = [_]u8{ @@ -1458,7 +1461,7 @@ test "Bitlist trailing zeros optimization" { } var hash1: [32]u8 = undefined; - try hashTreeRoot(TestBitlist, eight_false, &hash1, std.testing.allocator); + try hashTreeRoot(Sha256, TestBitlist, eight_false, &hash1, std.testing.allocator); // Expected hash for 8 false bits in Bitlist[256] // This should keep one zero byte and not remove all then add back a chunk @@ -1482,7 +1485,7 @@ test "Bitlist trailing zeros optimization" { } var hash2: [32]u8 = undefined; - try hashTreeRoot(TestBitlist, pattern, &hash2, std.testing.allocator); + try hashTreeRoot(Sha256, TestBitlist, pattern, &hash2, std.testing.allocator); // Expected hash for [T,F,T,F...F] (16 bits total) // First byte is 0x05, second byte is 0x00 @@ -1500,7 +1503,7 @@ test "uint256 hash tree root" { const data: u256 = 0x0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF; var hash: [32]u8 = undefined; - try hashTreeRoot(u256, data, &hash, std.testing.allocator); + try hashTreeRoot(Sha256, u256, data, &hash, std.testing.allocator); const expected = [_]u8{ 0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01, 0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01, @@ -1517,7 +1520,7 @@ test "Single element List" { try single.append(42); var hash: [32]u8 = undefined; - try hashTreeRoot(ListU64, single, &hash, std.testing.allocator); + try hashTreeRoot(Sha256, ListU64, single, &hash, std.testing.allocator); const expected = [_]u8{ 0x54, 0xd7, 0x76, 0x7c, 0xc1, 0xdd, 0xd2, 0xf6, @@ -1547,7 +1550,7 @@ test "Nested structure hash tree root" { }; var hash: [32]u8 = undefined; - try hashTreeRoot(Outer, data, &hash, std.testing.allocator); + try hashTreeRoot(Sha256, Outer, data, &hash, std.testing.allocator); const expected = [_]u8{ 0x4e, 0xbe, 0x9c, 0x7f, 0x41, 0x63, 0xd9, 0x34, @@ -1585,7 +1588,7 @@ test "Zero-length array" { try expect(list.items.len == 0); var hash: [32]u8 = undefined; - try hashTreeRoot([0]u32, empty, &hash, std.testing.allocator); + try hashTreeRoot(Sha256, [0]u32, empty, &hash, std.testing.allocator); // Should be the zero chunk try expect(std.mem.eql(u8, &hash, &([_]u8{0} ** 32))); } diff --git a/src/utils.zig b/src/utils.zig index d5ddaf4..2b6df82 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -7,7 +7,6 @@ const deserialize = lib.deserialize; const isFixedSizeObject = lib.isFixedSizeObject; const ArrayList = std.ArrayList; const Allocator = std.mem.Allocator; -const sha256 = std.crypto.hash.sha2.Sha256; const hashes_of_zero = @import("./zeros.zig").hashes_of_zero; // SSZ specification constants @@ -16,7 +15,7 @@ const chunk = [BYTES_PER_CHUNK]u8; const zero_chunk: chunk = [_]u8{0} ** BYTES_PER_CHUNK; /// Implements the SSZ `List[N]` container. -pub fn List(comptime T: type, comptime N: usize) type { +pub fn List(T: type, comptime N: usize) type { // Compile-time check: List[bool, N] is not allowed, use Bitlist[N] instead if (T == bool) { @compileError("List[bool, N] is not supported. Use Bitlist(" ++ std.fmt.comptimePrint("{}", .{N}) ++ ") instead for boolean lists."); @@ -154,7 +153,7 @@ pub fn List(comptime T: type, comptime N: usize) type { return lib.serializedSize(@TypeOf(inner_slice), inner_slice); } - pub fn hashTreeRoot(self: *const Self, out: *[32]u8, allctr: Allocator) !void { + pub fn hashTreeRoot(self: *const Self, Hasher: type, out: *[32]u8, allctr: Allocator) !void { const items = self.constSlice(); switch (@typeInfo(Item)) { @@ -167,21 +166,21 @@ pub fn List(comptime T: type, comptime N: usize) type { const items_per_chunk = BYTES_PER_CHUNK / bytes_per_item; const chunks_for_max_capacity = (N + items_per_chunk - 1) / items_per_chunk; var tmp: chunk = undefined; - try lib.merkleize(sha256, chunks, chunks_for_max_capacity, &tmp); - lib.mixInLength2(tmp, items.len, out); + try lib.merkleize(Hasher, chunks, chunks_for_max_capacity, &tmp); + lib.mixInLength2(Hasher, tmp, items.len, out); }, else => { var chunks = ArrayList(chunk).init(allctr); defer chunks.deinit(); var tmp: chunk = undefined; for (items) |item| { - try lib.hashTreeRoot(Item, item, &tmp, allctr); + try lib.hashTreeRoot(Hasher, Item, item, &tmp, allctr); try chunks.append(tmp); } // Always use N (max capacity) for merkleization, even when empty // This ensures proper tree depth according to SSZ specification - try lib.merkleize(sha256, chunks.items, N, &tmp); - lib.mixInLength2(tmp, items.len, out); + try lib.merkleize(Hasher, chunks.items, N, &tmp); + lib.mixInLength2(Hasher, tmp, items.len, out); }, } } @@ -324,7 +323,7 @@ pub fn Bitlist(comptime N: usize) type { return (self.length + 7 + 1) / 8; } - pub fn hashTreeRoot(self: *const Self, out: *[32]u8, allctr: Allocator) !void { + pub fn hashTreeRoot(self: *const Self, Hasher: type, out: *[32]u8, allctr: Allocator) !void { const bit_length = self.length; var bitfield_bytes = ArrayList(u8).init(allctr); @@ -351,8 +350,8 @@ pub fn Bitlist(comptime N: usize) type { // Use chunk_count limit as per SSZ specification const chunk_count_limit = (N + 255) / 256; - try lib.merkleize(sha256, chunks, chunk_count_limit, &tmp); - lib.mixInLength2(tmp, bit_length, out); + try lib.merkleize(Hasher, chunks, chunk_count_limit, &tmp); + lib.mixInLength2(Hasher, tmp, bit_length, out); } /// Validates that the bitlist is correctly formed diff --git a/src/zeros.zig b/src/zeros.zig index 1ecca76..ceb0e86 100644 --- a/src/zeros.zig +++ b/src/zeros.zig @@ -4,7 +4,7 @@ const std = @import("std"); /// Generic function to build zero hashes for any hash function /// HashType should be a hash type like std.crypto.hash.sha2.Sha256 /// digest_length is the output size of the hash in bytes -pub fn buildZeroHashes(comptime HashType: type, comptime digest_length: usize, comptime depth: usize) [depth][digest_length]u8 { +pub fn buildHashesOfZero(comptime HashType: type, comptime digest_length: usize, comptime depth: usize) [depth][digest_length]u8 { @setEvalBranchQuota(10000000); var ret: [depth][digest_length]u8 = undefined; @@ -26,4 +26,4 @@ pub fn buildZeroHashes(comptime HashType: type, comptime digest_length: usize, c } // SHA256 zero hashes (the default for SSZ) -pub const hashes_of_zero = buildZeroHashes(std.crypto.hash.sha2.Sha256, 32, 256); +pub const hashes_of_zero = buildHashesOfZero(std.crypto.hash.sha2.Sha256, 32, 256);