diff --git a/build.zig b/build.zig index 448a6a1..66e1f7f 100644 --- a/build.zig +++ b/build.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const Builder = @import("std").Build; pub fn build(b: *Builder) void { @@ -32,7 +33,51 @@ pub fn build(b: *Builder) void { tests_tests.root_module.addImport("ssz.zig", mod); const run_tests_tests = b.addRunArtifact(tests_tests); + // Poseidon hasher build options + const poseidon_enabled = b.option(bool, "poseidon", "Enable Poseidon2 hash support") orelse false; + if (poseidon_enabled) { + std.log.info("Poseidon2 enabled (koalabear, Poseidon2-24 Plonky3)", .{}); + } + + // Create build options + const options = b.addOptions(); + options.addOption(bool, "poseidon_enabled", poseidon_enabled); + + // Poseidon2 implementation via hash-zig dependency + const hashzig_module = if (poseidon_enabled) blk: { + const hashzig_dep = b.dependency("hash_zig", .{ + .target = target, + .optimize = optimize, + }); + break :blk hashzig_dep.module("hash-zig"); + } else null; + + // Add build options and poseidon import to all artifacts + mod.addOptions("build_options", options); + if (hashzig_module) |pm| mod.addImport("hash_zig", pm); + + lib.root_module.addOptions("build_options", options); + if (hashzig_module) |pm| lib.root_module.addImport("hash_zig", pm); + + main_tests.root_module.addOptions("build_options", options); + if (hashzig_module) |pm| main_tests.root_module.addImport("hash_zig", pm); + + tests_tests.root_module.addOptions("build_options", options); + if (hashzig_module) |pm| tests_tests.root_module.addImport("hash_zig", pm); + const test_step = b.step("test", "Run library tests"); test_step.dependOn(&run_main_tests.step); test_step.dependOn(&run_tests_tests.step); + // Optional Poseidon validation suite (only when Poseidon is enabled) + if (poseidon_enabled) { + const plonky3_validation_tests = b.addTest(.{ + .root_source_file = .{ .cwd_relative = "src/poseidon_plonky3_validation.zig" }, + .optimize = optimize, + .target = target, + }); + plonky3_validation_tests.root_module.addOptions("build_options", options); + if (hashzig_module) |pm| plonky3_validation_tests.root_module.addImport("hash_zig", pm); + const run_plonky3_validation_tests = b.addRunArtifact(plonky3_validation_tests); + test_step.dependOn(&run_plonky3_validation_tests.step); + } } diff --git a/build.zig.zon b/build.zig.zon index f335c95..83d6442 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,4 +3,10 @@ .fingerprint = 0x1d34bd0ceb1dfc2d, .version = "0.0.9", .paths = .{""}, + .dependencies = .{ + .hash_zig = .{ + .url = "https://github.com/blockblaz/hash-zig/archive/2bca6541a933a2bbe4630f41c29f61b3f84ad7e0.tar.gz", + .hash = "hash_zig-1.1.3-POmurOPmCgCkbtpq_c62mZqLuHzVeaLZ2X23fxsoHVrI", + }, + }, } diff --git a/src/beacon_tests.zig b/src/beacon_tests.zig index 5a60c27..66260ca 100644 --- a/src/beacon_tests.zig +++ b/src/beacon_tests.zig @@ -4,6 +4,7 @@ const serialize = libssz.serialize; const deserialize = libssz.deserialize; const hashTreeRoot = libssz.hashTreeRoot; const std = @import("std"); +const build_options = @import("build_options"); const ArrayList = std.ArrayList; const expect = std.testing.expect; @@ -53,6 +54,7 @@ test "Validator struct serialization" { } test "Validator struct hash tree root" { + if (build_options.poseidon_enabled) return; const validator = Validator{ .pubkey = [_]u8{0x01} ** 48, .withdrawal_credentials = [_]u8{0x02} ** 32, @@ -116,6 +118,7 @@ test "Individual Validator serialization and hash" { try expect(std.mem.eql(u8, list.items, &expected_validator_bytes)); // Test hash tree root + if (build_options.poseidon_enabled) return; var hash: [32]u8 = undefined; try hashTreeRoot(Validator, validator, &hash, std.testing.allocator); @@ -190,6 +193,7 @@ test "List[Validator] serialization and hash tree root" { } // Test hash tree root + if (build_options.poseidon_enabled) return; var hash1: [32]u8 = undefined; try hashTreeRoot(ValidatorList, validator_list, &hash1, std.testing.allocator); @@ -279,6 +283,8 @@ test "BeamBlockBody with validator array - full cycle" { try expect(orig.withdrawable_epoch == deser.withdrawable_epoch); } + if (build_options.poseidon_enabled) return; + // Test hash tree root consistency var hash_original: [32]u8 = undefined; try hashTreeRoot(BeamBlockBody, beam_block_body, &hash_original, std.testing.allocator); @@ -377,6 +383,8 @@ test "Zeam-style List/Bitlist usage with tree root stability" { try expect(std.mem.eql(u8, state_serialized.items, &expected_zeam_state_bytes)); + if (build_options.poseidon_enabled) return; + // Test hash tree root determinism and validate against expected hashes var body_hash1: [32]u8 = undefined; var body_hash2: [32]u8 = undefined; @@ -450,6 +458,8 @@ test "BeamState with historical roots - comprehensive test" { const expected_comprehensive_beam_state_bytes = [_]u8{ 0x39, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x9C, 0x00, 0x00, 0x00, 0xE8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; try expect(std.mem.eql(u8, serialized_data.items, &expected_comprehensive_beam_state_bytes)); + if (build_options.poseidon_enabled) return; + // Test hash tree root calculation var original_hash: [32]u8 = undefined; try hashTreeRoot(BeamState, beam_state, &original_hash, std.testing.allocator); @@ -524,6 +534,8 @@ test "BeamState with empty historical roots" { const expected_empty_beam_state_bytes = [_]u8{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; try expect(std.mem.eql(u8, serialized_data.items, &expected_empty_beam_state_bytes)); + if (build_options.poseidon_enabled) return; + // Test hash tree root calculation var original_hash: [32]u8 = undefined; try hashTreeRoot(SimpleBeamState, beam_state, &original_hash, std.testing.allocator); @@ -591,6 +603,8 @@ test "BeamState with maximum historical roots" { const expected_max_beam_state_bytes_start = [_]u8{ 0x3F, 0x42, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00 }; try expect(std.mem.eql(u8, serialized_data.items[0..12], &expected_max_beam_state_bytes_start)); + if (build_options.poseidon_enabled) return; + // Test hash tree root calculation var original_hash: [32]u8 = undefined; try hashTreeRoot(MaxBeamState, beam_state, &original_hash, std.testing.allocator); @@ -671,6 +685,8 @@ test "BeamState historical roots access and comparison" { const expected_access_beam_state_bytes = [_]u8{ 0x31, 0xD4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0xEF, 0xBE, 0xAD, 0xDE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x34, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x56, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x78, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0x9A, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC, 0xBC }; try expect(std.mem.eql(u8, serialized_data.items, &expected_access_beam_state_bytes)); + if (build_options.poseidon_enabled) return; + // Test hash tree root calculation var original_hash: [32]u8 = undefined; try hashTreeRoot(AccessBeamState, beam_state, &original_hash, std.testing.allocator); @@ -741,6 +757,8 @@ test "SimpleBeamState with empty historical roots" { const expected_simple_beam_state_bytes = [_]u8{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; try expect(std.mem.eql(u8, serialized_data.items, &expected_simple_beam_state_bytes)); + if (build_options.poseidon_enabled) return; + // Test hash tree root calculation var original_hash: [32]u8 = undefined; try hashTreeRoot(SimpleBeamState, beam_state, &original_hash, std.testing.allocator); diff --git a/src/lib.zig b/src/lib.zig index 381f648..a367395 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -6,8 +6,20 @@ pub const utils = @import("./utils.zig"); pub const zeros = @import("./zeros.zig"); const ArrayList = std.ArrayList; const builtin = std.builtin; +const build_options = @import("build_options"); const sha256 = std.crypto.hash.sha2.Sha256; -const hashes_of_zero = zeros.hashes_of_zero; + +// Configure the hasher based on build options +pub const Hasher = if (build_options.poseidon_enabled) blk: { + const hash_zig = @import("hash_zig"); + const poseidon2 = hash_zig.poseidon2; + const poseidon_wrapper = @import("./poseidon_wrapper.zig"); + const Poseidon2Type = poseidon2.Poseidon2KoalaBear24Plonky3; + // Wrap with SHA256-compatible API + break :blk poseidon_wrapper.PoseidonHasher(Poseidon2Type); +} else sha256; + +const hashes_of_zero = zeros.buildZeroHashes(Hasher, 32, 256); const Allocator = std.mem.Allocator; /// Number of bytes per chunk. @@ -505,7 +517,7 @@ 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{}); + var hasher = Hasher.init(Hasher.Options{}); hasher.update(root[0..]); var tmp = [_]u8{0} ** 32; @@ -515,13 +527,14 @@ pub fn mixInLength2(root: [32]u8, length: usize, out: *[32]u8) void { } fn mixInLength(root: [32]u8, length: [32]u8, out: *[32]u8) void { - var hasher = sha256.init(sha256.Options{}); + var hasher = Hasher.init(Hasher.Options{}); hasher.update(root[0..]); hasher.update(length[0..]); hasher.final(out[0..]); } test "mixInLength" { + if (build_options.poseidon_enabled) return; var root: [32]u8 = undefined; var length: [32]u8 = undefined; var expected: [32]u8 = undefined; @@ -535,7 +548,7 @@ test "mixInLength" { } fn mixInSelector(root: [32]u8, comptime selector: usize, out: *[32]u8) void { - var hasher = sha256.init(sha256.Options{}); + 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); @@ -544,6 +557,7 @@ fn mixInSelector(root: [32]u8, comptime selector: usize, out: *[32]u8) void { } test "mixInSelector" { + if (build_options.poseidon_enabled) return; var root: [32]u8 = undefined; var expected: [32]u8 = undefined; var mixin: [32]u8 = undefined; @@ -671,7 +685,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(Hasher, chunks, null, &out); try std.testing.expect(std.mem.eql(u8, out[0..], zero_chunk[0..])); } @@ -680,22 +694,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(Hasher, 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 = Hasher.init(Hasher.Options{}); hasher.update(leaf1[0..]); hasher.update(leaf1[0..]); hasher.final(&internal_left); - hasher = sha256.init(sha256.Options{}); + hasher = Hasher.init(Hasher.Options{}); hasher.update(leaf1[0..]); hasher.update(leaf2[0..]); hasher.final(&internal_right); - hasher = sha256.init(sha256.Options{}); + hasher = Hasher.init(Hasher.Options{}); hasher.update(internal_left[0..]); hasher.update(internal_right[0..]); hasher.final(&root); @@ -710,7 +724,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(Hasher, chunks, null, &out); try std.testing.expect(std.mem.eql(u8, out[0..], expected[0..])); @@ -719,7 +733,7 @@ test "merkleize a boolean" { chunks = try pack(bool, true, &list2); expected[0] = 1; - try merkleize(sha256, chunks, null, &out); + try merkleize(Hasher, chunks, null, &out); try std.testing.expect(std.mem.eql(u8, out[0..], expected[0..])); } @@ -764,7 +778,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 @@ -776,13 +790,13 @@ 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); @@ -792,7 +806,7 @@ pub fn hashTreeRoot(comptime T: type, value: T, out: *[32]u8, allctr: Allocator) try hashTreeRoot(@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, } @@ -807,7 +821,7 @@ 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); + try merkleize(Hasher, chunks, null, &tmp); mixInLength2(tmp, value.len, out); }, // use bitlist @@ -821,7 +835,7 @@ pub fn hashTreeRoot(comptime T: type, value: T, out: *[32]u8, allctr: Allocator) try hashTreeRoot(@TypeOf(item), item, &tmp, allctr); try chunks.append(tmp); } - try merkleize(sha256, chunks.items, null, &tmp); + try merkleize(Hasher, chunks.items, null, &tmp); mixInLength2(tmp, chunks.items.len, out); }, } @@ -837,7 +851,7 @@ pub fn hashTreeRoot(comptime T: type, value: T, out: *[32]u8, allctr: Allocator) try hashTreeRoot(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) { diff --git a/src/poseidon_plonky3_validation.zig b/src/poseidon_plonky3_validation.zig new file mode 100644 index 0000000..e3e4064 --- /dev/null +++ b/src/poseidon_plonky3_validation.zig @@ -0,0 +1,116 @@ +//! Cross-validation: SSZ Poseidon2-24 wrapper vs Plonky3 reference outputs +//! +//! This test verifies that the SSZ Poseidon2 wrapper produces IDENTICAL outputs +//! to Plonky3's reference implementation for the same 64-byte inputs. + +const std = @import("std"); +const build_options = @import("build_options"); + +test "SSZ Poseidon2-24 matches Plonky3 reference outputs" { + if (!build_options.poseidon_enabled) return; + + const hash_zig = @import("hash_zig"); + const poseidon_wrapper = @import("./poseidon_wrapper.zig"); + const Hasher = poseidon_wrapper.PoseidonHasher(hash_zig.poseidon2.Poseidon2KoalaBear24Plonky3); + + // Test 1: All zeros (64 bytes) + { + var hasher = Hasher.init(.{}); + const input = [_]u8{0x00} ** 64; + hasher.update(&input); + + var output: [32]u8 = undefined; + hasher.final(&output); + + const expected = [_]u8{ 0xe4, 0xcb, 0xc9, 0x51, 0xcc, 0xd0, 0xf9, 0x07, 0xe1, 0xca, 0x89, 0x29, 0xc0, 0xa8, 0x70, 0x76, 0xf7, 0x8d, 0x75, 0x7a, 0xda, 0x87, 0xd4, 0x35, 0xd3, 0x86, 0xcc, 0x62, 0xd0, 0x64, 0x5a, 0x13 }; + try std.testing.expectEqualSlices(u8, &expected, &output); + } + + // Test 2: All 0x01 bytes + { + var hasher = Hasher.init(.{}); + const input = [_]u8{0x01} ** 64; + hasher.update(&input); + + var output: [32]u8 = undefined; + hasher.final(&output); + + const expected = [_]u8{ 0xb3, 0x16, 0xc9, 0x34, 0x81, 0x0a, 0x37, 0x73, 0x93, 0x89, 0x61, 0x7a, 0x5e, 0x9d, 0xc8, 0x6f, 0x75, 0x28, 0xd4, 0x27, 0x22, 0x8f, 0xf3, 0x57, 0x9d, 0xfb, 0xff, 0x5c, 0xef, 0x08, 0x1f, 0x00 }; + try std.testing.expectEqualSlices(u8, &expected, &output); + } + + // Test 3: All 0x42 bytes + { + var hasher = Hasher.init(.{}); + const input = [_]u8{0x42} ** 64; + hasher.update(&input); + + var output: [32]u8 = undefined; + hasher.final(&output); + + const expected = [_]u8{ 0x78, 0xae, 0xf5, 0x68, 0xa5, 0x4c, 0xf6, 0x59, 0x2f, 0x82, 0x6d, 0x1e, 0x5f, 0x8f, 0x5e, 0x68, 0x95, 0x94, 0xc6, 0x09, 0x25, 0x87, 0xce, 0x6d, 0x16, 0xd2, 0xb2, 0x21, 0xdb, 0x21, 0x3c, 0x1c }; + try std.testing.expectEqualSlices(u8, &expected, &output); + } + + // Test 4: Sequential bytes (0..63) + { + var hasher = Hasher.init(.{}); + var input: [64]u8 = undefined; + for (0..64) |i| { + input[i] = @intCast(i); + } + hasher.update(&input); + + var output: [32]u8 = undefined; + hasher.final(&output); + + const expected = [_]u8{ 0x29, 0x43, 0x5f, 0x44, 0xc0, 0xab, 0xbb, 0x1e, 0x3b, 0x42, 0x73, 0x2c, 0xfb, 0xac, 0x95, 0x67, 0xb1, 0xa6, 0x4b, 0x6d, 0xb9, 0x51, 0x6a, 0x23, 0xdd, 0x01, 0x03, 0x1d, 0x15, 0xf4, 0x3a, 0x63 }; + try std.testing.expectEqualSlices(u8, &expected, &output); + } + + // Test 5: SSZ pattern - hash two 32-byte nodes (0xAA || 0xBB) + { + var hasher = Hasher.init(.{}); + const left_node = [_]u8{0xAA} ** 32; + const right_node = [_]u8{0xBB} ** 32; + + hasher.update(&left_node); + hasher.update(&right_node); + + var output: [32]u8 = undefined; + hasher.final(&output); + + const expected = [_]u8{ 0xec, 0x3e, 0x77, 0x40, 0x7c, 0x50, 0xf7, 0x7a, 0x63, 0x98, 0xdb, 0x56, 0x94, 0x82, 0x6e, 0x21, 0xfb, 0xb8, 0x7f, 0x29, 0x92, 0x59, 0x3e, 0x59, 0x6c, 0xc9, 0x37, 0x7a, 0x50, 0x54, 0xdf, 0x56 }; + try std.testing.expectEqualSlices(u8, &expected, &output); + } + + // Test 6: Last byte boundary (63 bytes 0xFF, 1 byte 0x01) + { + var hasher = Hasher.init(.{}); + var input: [64]u8 = undefined; + @memset(input[0..63], 0xFF); + input[63] = 0x01; + hasher.update(&input); + + var output: [32]u8 = undefined; + hasher.final(&output); + + const expected = [_]u8{ 0xd2, 0xe5, 0x8c, 0x51, 0x39, 0xb5, 0x91, 0x64, 0xd2, 0xdb, 0x26, 0x49, 0x32, 0x50, 0x7d, 0x4e, 0x6d, 0xac, 0xef, 0x30, 0x76, 0x83, 0x12, 0x67, 0x4a, 0x9c, 0x70, 0x35, 0x87, 0xdf, 0xa9, 0x64 }; + try std.testing.expectEqualSlices(u8, &expected, &output); + } + + // Test 7: Last byte boundary variant (63 bytes 0xFF, 1 byte 0x02) + { + var hasher = Hasher.init(.{}); + var input: [64]u8 = undefined; + @memset(input[0..63], 0xFF); + input[63] = 0x02; + hasher.update(&input); + + var output: [32]u8 = undefined; + hasher.final(&output); + + const expected = [_]u8{ 0xc7, 0xed, 0x40, 0x1c, 0x2c, 0x03, 0x7e, 0x29, 0x3d, 0xb7, 0x76, 0x3f, 0xf2, 0xa7, 0x49, 0x39, 0xec, 0x47, 0x52, 0x3e, 0x5c, 0xeb, 0xad, 0x34, 0xe7, 0x4b, 0x00, 0x74, 0xf5, 0x01, 0xd4, 0x43 }; + try std.testing.expectEqualSlices(u8, &expected, &output); + } +} diff --git a/src/poseidon_wrapper.zig b/src/poseidon_wrapper.zig new file mode 100644 index 0000000..bd5dd7c --- /dev/null +++ b/src/poseidon_wrapper.zig @@ -0,0 +1,190 @@ +//! Provides a SHA256-compatible API wrapper for Poseidon2 hash function. +//! This allows Poseidon2 to be used as a drop-in replacement for SHA256 +//! in merkleization and hash tree root operations. +//! +//! IMPORTANT: This is a specialized wrapper for SSZ merkleization, which always +//! provides exactly 64 bytes (two 32-byte nodes). It is NOT a general-purpose +//! hash function: it enforces the fixed 64-byte input length and intentionally +//! does not implement any padding scheme. + +const std = @import("std"); + +/// Creates a hasher type that wraps a Poseidon2 instance with SHA256-like API +pub fn PoseidonHasher(comptime Poseidon2Type: type) type { + // SSZ compression in this codebase is always: + // H: {0,1}^512 -> {0,1}^256 + // i.e. exactly 64 bytes in, 32 bytes out. + const BUFFER_SIZE = 64; + + // Poseidon2-24 state width. + const WIDTH = 24; + + // Compile-time safety: verify Poseidon2Type has the required interface + comptime { + if (!@hasDecl(Poseidon2Type, "Field")) { + @compileError("Poseidon2Type must have a 'Field' declaration"); + } + if (!@hasDecl(Poseidon2Type, "permutation")) { + @compileError("Poseidon2Type must have a 'permutation' function"); + } + if (!@hasDecl(Poseidon2Type, "WIDTH")) { + @compileError("Poseidon2Type must expose a WIDTH constant"); + } + if (Poseidon2Type.WIDTH != WIDTH) { + @compileError(std.fmt.comptimePrint( + "PoseidonHasher requires width-{d} Poseidon2, got width-{d}", + .{ WIDTH, Poseidon2Type.WIDTH }, + )); + } + } + + // We encode 64 bytes as 22 limbs of 24 bits each (little-endian within each limb), + // which are always < 2^24 < p (KoalaBear prime), avoiding lossy modular reduction: + // 64 bytes = 21*3 + 1 => 22 limbs, fits in a single width-24 permutation. + const LIMBS = 22; + + const FIELD_ELEM_SIZE = 4; // u32 = 4 bytes + const OUTPUT_FIELD_ELEMS = 8; // 8 u32s = 32 bytes + + return struct { + const Self = @This(); + + // Accumulated input bytes + buffer: [BUFFER_SIZE]u8, + buffer_len: usize, + + /// Options struct for compatibility with std.crypto.hash API + pub const Options = struct {}; + + /// Initialize a new hasher instance + pub fn init(_: Options) Self { + return .{ + .buffer = undefined, + .buffer_len = 0, + }; + } + + /// Update the hasher with new data + /// Note: This accumulates data. SSZ compression requires exactly 64 bytes, + /// so we buffer until we have enough data. + pub fn update(self: *Self, data: []const u8) void { + // Enforce the 64-byte limit explicitly + std.debug.assert(self.buffer_len + data.len <= BUFFER_SIZE); + + // Copy data into buffer + @memcpy(self.buffer[self.buffer_len..][0..data.len], data); + self.buffer_len += data.len; + } + + /// Finalize the hash and write the result to out + pub fn final(self: *Self, out: []u8) void { + std.debug.assert(out.len == 32); + // Enforce exact length: SSZ internal nodes and mix-in-length always pass 64 bytes. + std.debug.assert(self.buffer_len == BUFFER_SIZE); + + // Byte -> 24-bit limb packing (injective for fixed 64-byte inputs). + var limbs: [LIMBS]u32 = undefined; + for (0..(LIMBS - 1)) |i| { + const j = i * 3; + limbs[i] = @as(u32, self.buffer[j]) | + (@as(u32, self.buffer[j + 1]) << 8) | + (@as(u32, self.buffer[j + 2]) << 16); + } + limbs[LIMBS - 1] = @as(u32, self.buffer[63]); + + // Build Poseidon2 state: 22 limbs + 2 zero lanes. + var state: [WIDTH]Poseidon2Type.Field = undefined; + for (0..LIMBS) |i| { + state[i] = Poseidon2Type.Field.fromU32(limbs[i]); + } + state[22] = Poseidon2Type.Field.zero; + state[23] = Poseidon2Type.Field.zero; + + // TruncatedPermutation semantics (no feed-forward): permute, then squeeze. + Poseidon2Type.permutation(state[0..]); + + // Squeeze first 8 lanes as 32 bytes, little-endian u32 per lane. + for (0..OUTPUT_FIELD_ELEMS) |i| { + const v = state[i].toU32(); + std.mem.writeInt(u32, out[i * FIELD_ELEM_SIZE ..][0..FIELD_ELEM_SIZE], v, .little); + } + + // Reset buffer for potential reuse. + self.buffer_len = 0; + } + + /// Convenience helper used by some generic code (e.g. zero-hash builders). + pub fn finalResult(self: *Self) [32]u8 { + var out: [32]u8 = undefined; + self.final(out[0..]); + return out; + } + }; +} + +test "PoseidonHasher basic API" { + // This test just verifies the API compiles and runs. + const hash_zig = @import("hash_zig"); + const Hasher = PoseidonHasher(hash_zig.poseidon2.Poseidon2KoalaBear24Plonky3); + + var hasher = Hasher.init(.{}); + const data = [_]u8{0x01} ** 64; + hasher.update(data[0..]); + + var output: [32]u8 = undefined; + hasher.final(output[0..]); + + // Just verify we got some output (not all zeros) + var has_nonzero = false; + for (output) |byte| { + if (byte != 0) { + has_nonzero = true; + break; + } + } + try std.testing.expect(has_nonzero); +} + +test "PoseidonHasher deterministic" { + // Verify same input produces same output + const hash_zig = @import("hash_zig"); + const Hasher = PoseidonHasher(hash_zig.poseidon2.Poseidon2KoalaBear24Plonky3); + + var hasher1 = Hasher.init(.{}); + var hasher2 = Hasher.init(.{}); + + const data = [_]u8{0x42} ** 64; + hasher1.update(data[0..]); + hasher2.update(data[0..]); + + var output1: [32]u8 = undefined; + var output2: [32]u8 = undefined; + hasher1.final(output1[0..]); + hasher2.final(output2[0..]); + + try std.testing.expectEqualSlices(u8, &output1, &output2); +} + +test "PoseidonHasher different inputs produce different outputs" { + // Verify different inputs produce different outputs + const hash_zig = @import("hash_zig"); + const Hasher = PoseidonHasher(hash_zig.poseidon2.Poseidon2KoalaBear24Plonky3); + + var hasher1 = Hasher.init(.{}); + var hasher2 = Hasher.init(.{}); + + const data1 = [_]u8{0x01} ** 64; + const data2 = [_]u8{0x02} ** 64; + + hasher1.update(data1[0..]); + hasher2.update(data2[0..]); + + var output1: [32]u8 = undefined; + var output2: [32]u8 = undefined; + hasher1.final(output1[0..]); + hasher2.final(output2[0..]); + + // Verify outputs are different + const are_equal = std.mem.eql(u8, &output1, &output2); + try std.testing.expect(!are_equal); +} diff --git a/src/tests.zig b/src/tests.zig index 117dd95..a600752 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -7,6 +7,7 @@ const chunkCount = libssz.chunkCount; const hashTreeRoot = libssz.hashTreeRoot; const isFixedSizeObject = libssz.isFixedSizeObject; const std = @import("std"); +const build_options = @import("build_options"); const ArrayList = std.ArrayList; const expect = std.testing.expect; const expectError = std.testing.expectError; @@ -529,6 +530,8 @@ const d_bits = bytesToBits(16, d_bytes); const e_bits = bytesToBits(16, e_bytes); test "calculate the root hash of a boolean" { + // SHA-specific expected vectors; skip when Poseidon is enabled. + if (build_options.poseidon_enabled) return; var expected = [_]u8{1} ++ [_]u8{0} ** 31; var hashed: [32]u8 = undefined; try hashTreeRoot(bool, true, &hashed, std.testing.allocator); @@ -540,6 +543,7 @@ test "calculate the root hash of a boolean" { } test "calculate root hash of an array of two Bitvector[128]" { + if (build_options.poseidon_enabled) return; 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); @@ -559,6 +563,7 @@ test "calculate the root hash of an array of integers" { } test "calculate root hash of an array of three Bitvector[128]" { + if (build_options.poseidon_enabled) return; 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); @@ -578,6 +583,7 @@ test "calculate root hash of an array of three Bitvector[128]" { } test "calculate the root hash of an array of five Bitvector[128]" { + if (build_options.poseidon_enabled) return; 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); @@ -616,6 +622,7 @@ const Fork = struct { }; test "calculate the root hash of a structure" { + if (build_options.poseidon_enabled) return; var hashed: [32]u8 = undefined; const fork = Fork{ .previous_version = [_]u8{ 0x9c, 0xe2, 0x5d, 0x26 }, @@ -629,6 +636,7 @@ test "calculate the root hash of a structure" { } test "calculate the root hash of an Optional" { + if (build_options.poseidon_enabled) return; var hashed: [32]u8 = undefined; var payload: [64]u8 = undefined; const v: ?u32 = null; @@ -647,6 +655,7 @@ test "calculate the root hash of an Optional" { } test "calculate the root hash of an union" { + if (build_options.poseidon_enabled) return; const Payload = union(enum) { int: u64, boolean: bool, @@ -919,6 +928,7 @@ test "structs with nested fixed/variable size u8 array" { } test "slice hashtree root composite type" { + if (build_options.poseidon_enabled) return; const Root = [32]u8; const RootsList = []Root; const test_root = [_]u8{23} ** 32; @@ -938,6 +948,7 @@ test "slice hashtree root composite type" { } test "slice hashtree root simple type" { + if (build_options.poseidon_enabled) return; const DynamicRoot = []u8; // merkelizes as List[u8,33] as dynamic data length is mixed in as bounded type var test_root = [_]u8{23} ** 33; @@ -955,6 +966,7 @@ test "slice hashtree root simple type" { } test "List tree root calculation" { + if (build_options.poseidon_enabled) return; const ListU64 = utils.List(u64, 1024); var empty_list = try ListU64.init(std.testing.allocator); @@ -1275,6 +1287,7 @@ test "serialize max/min integer values" { } test "Empty List hash tree root" { + if (build_options.poseidon_enabled) return; const ListU32 = utils.List(u32, 100); var empty_list = try ListU32.init(std.testing.allocator); defer empty_list.deinit(); @@ -1293,6 +1306,7 @@ test "Empty List hash tree root" { } test "Empty BitList(<=256) hash tree root" { + if (build_options.poseidon_enabled) return; const BitListLen100 = utils.Bitlist(100); var empty_list = try BitListLen100.init(std.testing.allocator); defer empty_list.deinit(); @@ -1310,6 +1324,7 @@ test "Empty BitList(<=256) hash tree root" { } test "Empty BitList (>256) hash tree root" { + if (build_options.poseidon_enabled) return; const BitListLen100 = utils.Bitlist(2570); var empty_list = try BitListLen100.init(std.testing.allocator); defer empty_list.deinit(); @@ -1327,6 +1342,7 @@ test "Empty BitList (>256) hash tree root" { } test "List at maximum capacity" { + if (build_options.poseidon_enabled) return; const ListU8 = utils.List(u8, 4); var full_list = try ListU8.init(std.testing.allocator); defer full_list.deinit(); @@ -1355,6 +1371,8 @@ test "List at maximum capacity" { } test "Array hash tree root" { + // SHA-specific expected vectors; skip when Poseidon is enabled. + if (build_options.poseidon_enabled) return; const data: [4]u32 = .{ 1, 2, 3, 4 }; var hash: [32]u8 = undefined; @@ -1394,6 +1412,8 @@ test "Large Bitvector serialization and hash" { try expect(list.items[32] & 0x01 == 0x01); // bit 256 -> LSB of byte 32 try expect(list.items[63] & 0x80 == 0x80); // bit 511 -> MSB of byte 63 + if (build_options.poseidon_enabled) return; + // Test hash tree root var hash: [32]u8 = undefined; try hashTreeRoot(LargeBitvec, data, &hash, std.testing.allocator); @@ -1407,6 +1427,7 @@ test "Large Bitvector serialization and hash" { } test "Bitlist edge cases" { + if (build_options.poseidon_enabled) return; const TestBitlist = utils.Bitlist(100); // All false @@ -1448,6 +1469,7 @@ test "Bitlist edge cases" { } test "Bitlist trailing zeros optimization" { + if (build_options.poseidon_enabled) return; const TestBitlist = utils.Bitlist(256); // Test case 1: 8 false bits - should result in one 0x00 byte after pack_bits @@ -1497,6 +1519,8 @@ test "Bitlist trailing zeros optimization" { } test "uint256 hash tree root" { + // SHA-specific expected vectors; skip when Poseidon is enabled. + if (build_options.poseidon_enabled) return; const data: u256 = 0x0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF; var hash: [32]u8 = undefined; @@ -1511,6 +1535,7 @@ test "uint256 hash tree root" { } test "Single element List" { + if (build_options.poseidon_enabled) return; const ListU64 = utils.List(u64, 10); var single = try ListU64.init(std.testing.allocator); defer single.deinit(); @@ -1529,6 +1554,7 @@ test "Single element List" { } test "Nested structure hash tree root" { + if (build_options.poseidon_enabled) return; const Inner = struct { a: u32, b: u64, @@ -1577,6 +1603,8 @@ test "serialize negative i8 and i16" { } test "Zero-length array" { + // SHA-specific expected vectors; skip when Poseidon is enabled. + if (build_options.poseidon_enabled) return; const empty: [0]u32 = .{}; var list = ArrayList(u8).init(std.testing.allocator); diff --git a/src/utils.zig b/src/utils.zig index d5ddaf4..2a82f55 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -7,7 +7,7 @@ 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 Hasher = lib.Hasher; const hashes_of_zero = @import("./zeros.zig").hashes_of_zero; // SSZ specification constants @@ -167,7 +167,7 @@ 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); + try lib.merkleize(Hasher, chunks, chunks_for_max_capacity, &tmp); lib.mixInLength2(tmp, items.len, out); }, else => { @@ -180,7 +180,7 @@ pub fn List(comptime T: type, comptime N: usize) type { } // 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); + try lib.merkleize(Hasher, chunks.items, N, &tmp); lib.mixInLength2(tmp, items.len, out); }, } @@ -351,7 +351,7 @@ 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); + try lib.merkleize(Hasher, chunks, chunk_count_limit, &tmp); lib.mixInLength2(tmp, bit_length, out); }