diff --git a/build.zig b/build.zig index 10099b770..398bd5aa4 100644 --- a/build.zig +++ b/build.zig @@ -321,6 +321,7 @@ pub fn build(b: *Builder) !void { zeam_spectests.addImport("build_options", build_options_module); zeam_spectests.addImport("@zeam/state-transition", zeam_state_transition); zeam_spectests.addImport("@zeam/node", zeam_beam_node); + zeam_spectests.addImport("@zeam/xmss", zeam_xmss); // Add the cli executable const cli_exe = b.addExecutable(.{ @@ -572,6 +573,10 @@ pub fn build(b: *Builder) !void { spectests.root_module.addImport("@zeam/metrics", zeam_metrics); spectests.root_module.addImport("@zeam/state-transition", zeam_state_transition); spectests.root_module.addImport("ssz", ssz); + spectests.root_module.addImport("@zeam/xmss", zeam_xmss); + + spectests.step.dependOn(&build_rust_lib_steps.step); + addRustGlueLib(b, spectests, target, prover); manager_tests.step.dependOn(&build_rust_lib_steps.step); diff --git a/pkgs/key-manager/src/lib.zig b/pkgs/key-manager/src/lib.zig index ce3e933d3..5b6be884f 100644 --- a/pkgs/key-manager/src/lib.zig +++ b/pkgs/key-manager/src/lib.zig @@ -4,9 +4,105 @@ const types = @import("@zeam/types"); const zeam_utils = @import("@zeam/utils"); const zeam_metrics = @import("@zeam/metrics"); const Allocator = std.mem.Allocator; +const JsonValue = std.json.Value; + +pub const XmssTestConfig = struct { + scheme: xmss.HashSigScheme, + signature_ssz_len: usize, + allow_placeholder_aggregated_proof: bool, + + pub fn fromLeanEnv(lean_env: ?[]const u8) XmssTestConfig { + const scheme = schemeFromLeanEnv(lean_env); + return .{ + .scheme = scheme, + .signature_ssz_len = xmss.signatureSszLenForScheme(scheme), + .allow_placeholder_aggregated_proof = scheme == .@"test", + }; + } +}; + +pub const FixtureKeyError = error{ + DuplicateKeyIndex, + InvalidKeyFile, + InvalidKeyIndex, + InvalidPublicKey, + NoKeysFound, +}; + +fn schemeFromLeanEnv(lean_env: ?[]const u8) xmss.HashSigScheme { + const env = lean_env orelse return .prod; + if (std.ascii.eqlIgnoreCase(env, "test")) return .@"test"; + return .prod; +} + +fn parseKeyIndex(file_name: []const u8) !usize { + if (!std.mem.endsWith(u8, file_name, ".json")) { + return FixtureKeyError.InvalidKeyIndex; + } + const stem = file_name[0 .. file_name.len - ".json".len]; + if (stem.len == 0) { + return FixtureKeyError.InvalidKeyIndex; + } + return std.fmt.parseInt(usize, stem, 10) catch FixtureKeyError.InvalidKeyIndex; +} + +const fixture_key_file_max_bytes: usize = 2 * 1024 * 1024; + +fn readPublicKeyFromJson( + allocator: Allocator, + dir: std.fs.Dir, + file_name: []const u8, +) !types.Bytes52 { + const payload = dir.readFileAlloc(allocator, file_name, fixture_key_file_max_bytes) catch { + return FixtureKeyError.InvalidKeyFile; + }; + defer allocator.free(payload); + + var parsed = std.json.parseFromSlice(JsonValue, allocator, payload, .{ .ignore_unknown_fields = true }) catch { + return FixtureKeyError.InvalidKeyFile; + }; + defer parsed.deinit(); + + const obj = switch (parsed.value) { + .object => |map| map, + else => return FixtureKeyError.InvalidKeyFile, + }; + const pub_val = obj.get("public") orelse return FixtureKeyError.InvalidKeyFile; + const pub_hex = switch (pub_val) { + .string => |s| s, + else => return FixtureKeyError.InvalidKeyFile, + }; + + return parsePublicKeyHex(pub_hex); +} + +fn parsePublicKeyHex(input: []const u8) !types.Bytes52 { + const public_key_hex_len: usize = 2 * @sizeOf(types.Bytes52); + const hex_str = if (std.mem.startsWith(u8, input, "0x")) input[2..] else input; + if (hex_str.len != public_key_hex_len) { + return FixtureKeyError.InvalidPublicKey; + } + var bytes: types.Bytes52 = undefined; + _ = std.fmt.hexToBytes(&bytes, hex_str) catch { + return FixtureKeyError.InvalidPublicKey; + }; + return bytes; +} const KeyManagerError = error{ ValidatorKeyNotFound, + PrivateKeyMissing, + PublicKeyBufferTooSmall, +}; + +const PublicKeyEntry = struct { + bytes: types.Bytes52, + public_key: xmss.PublicKey, +}; + +const KeyEntry = union(enum) { + keypair: xmss.KeyPair, + public_key: PublicKeyEntry, }; const CachedKeyPair = struct { @@ -53,7 +149,7 @@ fn getOrCreateCachedKeyPair( } pub const KeyManager = struct { - keys: std.AutoHashMap(usize, xmss.KeyPair), + keys: std.AutoHashMap(usize, KeyEntry), allocator: Allocator, owns_keypairs: bool, @@ -61,24 +157,72 @@ pub const KeyManager = struct { pub fn init(allocator: Allocator) Self { return Self{ - .keys = std.AutoHashMap(usize, xmss.KeyPair).init(allocator), + .keys = std.AutoHashMap(usize, KeyEntry).init(allocator), .allocator = allocator, .owns_keypairs = true, }; } pub fn deinit(self: *Self) void { - if (self.owns_keypairs) { - var it = self.keys.iterator(); - while (it.next()) |entry| { - entry.value_ptr.deinit(); - } - } + self.clearEntries(); self.keys.deinit(); } pub fn addKeypair(self: *Self, validator_id: usize, keypair: xmss.KeyPair) !void { - try self.keys.put(validator_id, keypair); + if (self.keys.getPtr(validator_id)) |entry| { + self.deinitEntry(entry); + entry.* = .{ .keypair = keypair }; + return; + } + try self.keys.put(validator_id, .{ .keypair = keypair }); + } + + pub fn addPublicKey(self: *Self, validator_id: usize, pubkey_bytes: types.Bytes52) !void { + var public_key = try xmss.PublicKey.fromBytes(pubkey_bytes[0..]); + errdefer public_key.deinit(); + + const entry_value = PublicKeyEntry{ .bytes = pubkey_bytes, .public_key = public_key }; + if (self.keys.getPtr(validator_id)) |entry| { + self.deinitEntry(entry); + entry.* = .{ .public_key = entry_value }; + return; + } + try self.keys.put(validator_id, .{ .public_key = entry_value }); + } + + pub fn loadLeanSpecKeys(self: *Self, keys_root: []const u8, lean_env: ?[]const u8) !void { + const scheme_dir_name = switch (schemeFromLeanEnv(lean_env)) { + .@"test" => "test_scheme", + .prod => "prod_scheme", + }; + const scheme_dir_path = try std.fs.path.join(self.allocator, &.{ keys_root, scheme_dir_name }); + defer self.allocator.free(scheme_dir_path); + try self.loadKeysFromDir(scheme_dir_path); + } + + pub fn loadKeysFromDir(self: *Self, keys_dir_path: []const u8) !void { + var dir = try std.fs.cwd().openDir(keys_dir_path, .{ .iterate = true }); + defer dir.close(); + + self.clearEntries(); + + var it = dir.iterate(); + while (try it.next()) |entry| { + if (entry.kind != .file) continue; + const index = parseKeyIndex(entry.name) catch continue; + const pubkey = try readPublicKeyFromJson(self.allocator, dir, entry.name); + if (self.keys.get(index) != null) { + return FixtureKeyError.DuplicateKeyIndex; + } + self.addPublicKey(index, pubkey) catch |err| switch (err) { + error.OutOfMemory => return err, + else => return FixtureKeyError.InvalidPublicKey, + }; + } + + if (self.keys.count() == 0) { + return FixtureKeyError.NoKeysFound; + } } pub fn loadFromKeypairDir(_: *Self, _: []const u8) !void { @@ -109,8 +253,17 @@ pub const KeyManager = struct { validator_index: usize, buffer: []u8, ) !usize { - const keypair = self.keys.get(validator_index) orelse return KeyManagerError.ValidatorKeyNotFound; - return try keypair.pubkeyToBytes(buffer); + const entry = self.keys.getPtr(validator_index) orelse return KeyManagerError.ValidatorKeyNotFound; + return switch (entry.*) { + .keypair => |keypair| try keypair.pubkeyToBytes(buffer), + .public_key => |pubkey| blk: { + if (buffer.len < pubkey.bytes.len) { + return KeyManagerError.PublicKeyBufferTooSmall; + } + @memcpy(buffer[0..pubkey.bytes.len], pubkey.bytes[0..]); + break :blk pubkey.bytes.len; + }, + }; } /// Extract all validator public keys into an array @@ -136,8 +289,11 @@ pub const KeyManager = struct { self: *const Self, validator_index: usize, ) !*const xmss.HashSigPublicKey { - const keypair = self.keys.get(validator_index) orelse return KeyManagerError.ValidatorKeyNotFound; - return keypair.public_key; + const entry = self.keys.getPtr(validator_index) orelse return KeyManagerError.ValidatorKeyNotFound; + return switch (entry.*) { + .keypair => |keypair| keypair.public_key, + .public_key => |pubkey| pubkey.public_key.handle, + }; } /// Sign an attestation and return the raw signature handle (for aggregation) @@ -148,7 +304,11 @@ pub const KeyManager = struct { allocator: Allocator, ) !xmss.Signature { const validator_index: usize = @intCast(attestation.validator_id); - const keypair = self.keys.get(validator_index) orelse return KeyManagerError.ValidatorKeyNotFound; + const entry = self.keys.getPtr(validator_index) orelse return KeyManagerError.ValidatorKeyNotFound; + const keypair = switch (entry.*) { + .keypair => |*kp| kp, + .public_key => return KeyManagerError.PrivateKeyMissing, + }; const signing_timer = zeam_metrics.lean_pq_signature_attestation_signing_time_seconds.start(); var message: [32]u8 = undefined; @@ -160,6 +320,25 @@ pub const KeyManager = struct { return signature; } + + fn clearEntries(self: *Self) void { + var it = self.keys.iterator(); + while (it.next()) |entry| { + self.deinitEntry(entry.value_ptr); + } + self.keys.clearRetainingCapacity(); + } + + fn deinitEntry(self: *Self, entry: *KeyEntry) void { + switch (entry.*) { + .keypair => |*keypair| { + if (self.owns_keypairs) { + keypair.deinit(); + } + }, + .public_key => |*pubkey| pubkey.public_key.deinit(), + } + } }; pub fn getTestKeyManager( diff --git a/pkgs/spectest/src/fixture_kind.zig b/pkgs/spectest/src/fixture_kind.zig index 05735085a..bcb99ccf6 100644 --- a/pkgs/spectest/src/fixture_kind.zig +++ b/pkgs/spectest/src/fixture_kind.zig @@ -1,11 +1,13 @@ pub const FixtureKind = enum { state_transition, fork_choice, + verify_signatures, pub fn runnerModule(self: FixtureKind) []const u8 { return switch (self) { .state_transition => "state_transition", .fork_choice => "fork_choice", + .verify_signatures => "verify_signatures", }; } @@ -13,8 +15,9 @@ pub const FixtureKind = enum { return switch (self) { .state_transition => "state_transition", .fork_choice => "fc", + .verify_signatures => "verify_signatures", }; } }; -pub const all = [_]FixtureKind{ .state_transition, .fork_choice }; +pub const all = [_]FixtureKind{ .state_transition, .fork_choice, .verify_signatures }; diff --git a/pkgs/spectest/src/json_expect.zig b/pkgs/spectest/src/json_expect.zig index 15c811166..f633a948e 100644 --- a/pkgs/spectest/src/json_expect.zig +++ b/pkgs/spectest/src/json_expect.zig @@ -245,6 +245,23 @@ pub fn expectArrayValue( }; } +pub fn expectArrayField( + comptime FixtureError: type, + obj: std.json.ObjectMap, + field_names: []const []const u8, + context: Context, + label: []const u8, +) FixtureError!std.json.Array { + const value = getField(obj, field_names) orelse { + std.debug.print( + "fixture {s} case {s}{}: missing field {s}\n", + .{ context.fixture_label, context.case_name, context.formatStep(), label }, + ); + return FixtureError.InvalidFixture; + }; + return expectArrayValue(FixtureError, value, context, label); +} + pub fn appendBytesDataField( comptime FixtureError: type, comptime T: type, diff --git a/pkgs/spectest/src/runner/fork_choice_runner.zig b/pkgs/spectest/src/runner/fork_choice_runner.zig index 6bcb4550d..1f7309a3b 100644 --- a/pkgs/spectest/src/runner/fork_choice_runner.zig +++ b/pkgs/spectest/src/runner/fork_choice_runner.zig @@ -633,7 +633,10 @@ fn processBlockStep( } try types.sszClone(ctx.allocator, types.BeamState, parent_state_ptr.*, new_state_ptr); - state_transition.apply_transition(ctx.allocator, new_state_ptr, block, .{ .logger = ctx.fork_logger, .validateResult = false }) catch |err| { + state_transition.apply_transition(ctx.allocator, new_state_ptr, block, .{ + .logger = ctx.fork_logger, + .validateResult = false, + }) catch |err| { std.debug.print( "fixture {s} case {s}{}: state transition failed {s}\n", .{ fixture_path, case_name, formatStep(step_index), @errorName(err) }, diff --git a/pkgs/spectest/src/runner/state_transition_runner.zig b/pkgs/spectest/src/runner/state_transition_runner.zig index 25807049b..94bae3a49 100644 --- a/pkgs/spectest/src/runner/state_transition_runner.zig +++ b/pkgs/spectest/src/runner/state_transition_runner.zig @@ -275,7 +275,11 @@ fn runCase( } } - state_transition.apply_transition(allocator, &pre_state, block, .{ .logger = logger }) catch |err| { + const validate_result = expect_exception != null; + state_transition.apply_transition(allocator, &pre_state, block, .{ + .logger = logger, + .validateResult = validate_result, + }) catch |err| { encountered_error = true; if (expect_exception == null) { std.debug.print( diff --git a/pkgs/spectest/src/runner/verify_signatures_runner.zig b/pkgs/spectest/src/runner/verify_signatures_runner.zig new file mode 100644 index 000000000..9aa531aea --- /dev/null +++ b/pkgs/spectest/src/runner/verify_signatures_runner.zig @@ -0,0 +1,865 @@ +const std = @import("std"); + +const expect = @import("../json_expect.zig"); +const forks = @import("../fork.zig"); +const fixture_kind = @import("../fixture_kind.zig"); +const skip = @import("../skip.zig"); + +const Fork = forks.Fork; +const FixtureKind = fixture_kind.FixtureKind; + +pub const name = "verify_signatures"; + +pub const Handler = enum { + test_invalid_signatures, + test_valid_signatures, +}; + +pub const handlers = std.enums.values(Handler); + +pub fn handlerLabel(comptime handler: Handler) []const u8 { + return switch (handler) { + .test_invalid_signatures => "test_invalid_signatures", + .test_valid_signatures => "test_valid_signatures", + }; +} + +pub fn handlerPath(comptime handler: Handler) []const u8 { + return handlerLabel(handler); +} + +pub fn includeFixtureFile(file_name: []const u8) bool { + return std.mem.endsWith(u8, file_name, ".json"); +} + +pub fn baseRelRoot(comptime spec_fork: Fork) []const u8 { + const kind = FixtureKind.verify_signatures; + return std.fmt.comptimePrint( + "consensus/{s}/{s}/{s}", + .{ kind.runnerModule(), spec_fork.path, kind.handlerSubdir() }, + ); +} + +const types = @import("@zeam/types"); +const state_transition = @import("@zeam/state-transition"); +const key_manager = @import("@zeam/key-manager"); +const ssz = @import("ssz"); +const xmss = @import("@zeam/xmss"); + +// Signature structure constants from leansig +// path: 8 siblings, each is 8 u32 = 256 bytes +// rho: 7 u32 = 28 bytes +// hashes: 4 elements, each is 8 u32 = 128 bytes +// Total fixed size in fixture: 412 bytes, but actual SIGBYTES is 3112 bytes +// The remaining bytes are likely padding or additional OTS data + +const JsonValue = std.json.Value; +const Context = expect.Context; + +pub const RunnerError = error{ + IoFailure, +} || FixtureError; + +pub const FixtureError = error{ + InvalidFixture, + UnsupportedFixture, + FixtureMismatch, + SkippedFixture, +}; + +const read_max_bytes: usize = 16 * 1024 * 1024; // 16 MiB upper bound per fixture file. + +pub fn TestCase( + comptime spec_fork: Fork, + comptime rel_path: []const u8, +) type { + return struct { + payload: []u8, + + const Self = @This(); + + pub fn execute(allocator: std.mem.Allocator, dir: std.fs.Dir) RunnerError!void { + var tc = try Self.init(allocator, dir); + defer tc.deinit(allocator); + try tc.run(allocator); + } + + pub fn init(allocator: std.mem.Allocator, dir: std.fs.Dir) RunnerError!Self { + const payload = try loadFixturePayload(allocator, dir, rel_path); + return Self{ .payload = payload }; + } + + pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { + allocator.free(self.payload); + } + + pub fn run(self: *Self, allocator: std.mem.Allocator) RunnerError!void { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const arena_allocator = arena.allocator(); + + try runFixturePayload(spec_fork, arena_allocator, rel_path, self.payload); + } + }; +} + +fn loadFixturePayload( + allocator: std.mem.Allocator, + dir: std.fs.Dir, + rel_path: []const u8, +) RunnerError![]u8 { + const payload = dir.readFileAlloc(allocator, rel_path, read_max_bytes) catch |err| switch (err) { + error.FileTooBig => { + std.debug.print("spectest: fixture {s} exceeds allowed size\n", .{rel_path}); + return RunnerError.IoFailure; + }, + else => { + std.debug.print("spectest: failed to read {s}: {s}\n", .{ rel_path, @errorName(err) }); + return RunnerError.IoFailure; + }, + }; + return payload; +} + +pub fn runFixturePayload( + comptime spec_fork: Fork, + allocator: std.mem.Allocator, + fixture_label: []const u8, + payload: []const u8, +) FixtureError!void { + _ = spec_fork; + var parsed = std.json.parseFromSlice(JsonValue, allocator, payload, .{ .ignore_unknown_fields = true }) catch |err| { + std.debug.print("spectest: fixture {s} not valid JSON: {s}\n", .{ fixture_label, @errorName(err) }); + return FixtureError.InvalidFixture; + }; + defer parsed.deinit(); + + const root = parsed.value; + const obj = switch (root) { + .object => |map| map, + else => { + std.debug.print("spectest: fixture {s} must be JSON object\n", .{fixture_label}); + return FixtureError.InvalidFixture; + }, + }; + + var skipped_cases: usize = 0; + var it = obj.iterator(); + while (it.next()) |entry| { + const case_name = entry.key_ptr.*; + const case_value = entry.value_ptr.*; + const ctx = Context{ .fixture_label = fixture_label, .case_name = case_name }; + runCase(allocator, ctx, case_value) catch |err| switch (err) { + FixtureError.SkippedFixture => skipped_cases += 1, + FixtureError.UnsupportedFixture => { + std.debug.print( + "spectest: skipping unsupported case {s} in {s}\n", + .{ case_name, fixture_label }, + ); + }, + else => return err, + }; + } + + if (skipped_cases > 0) { + std.debug.print( + "spectest: skipped {d} case(s) in fixture {s}\n", + .{ skipped_cases, fixture_label }, + ); + } +} + +const AggregatedSignatureProof = struct { + participants: types.AggregationBits, + proof_data: []u8, + + pub fn deinit(self: *AggregatedSignatureProof, allocator: std.mem.Allocator) void { + self.participants.deinit(); + allocator.free(self.proof_data); + } +}; + +const ParsedSignedBlockWithAttestation = struct { + signed_block: types.SignedBlockWithAttestation, + attestation_proofs: []AggregatedSignatureProof, + + pub fn deinit(self: *ParsedSignedBlockWithAttestation, allocator: std.mem.Allocator) void { + for (self.attestation_proofs) |*proof| { + proof.deinit(allocator); + } + allocator.free(self.attestation_proofs); + self.signed_block.deinit(); + } +}; + +fn runCase( + allocator: std.mem.Allocator, + ctx: Context, + value: JsonValue, +) FixtureError!void { + const case_obj = switch (value) { + .object => |map| map, + else => { + std.debug.print("fixture {s} case {s}: expected object\n", .{ ctx.fixture_label, ctx.case_name }); + return FixtureError.InvalidFixture; + }, + }; + + const lean_env = blk: { + const lean_env_val = case_obj.get("leanEnv") orelse break :blk null; + const lean_env_str = switch (lean_env_val) { + .string => |s| s, + else => break :blk null, + }; + break :blk lean_env_str; + }; + const test_config = key_manager.XmssTestConfig.fromLeanEnv(lean_env); + const signature_ssz_len: usize = test_config.signature_ssz_len; + const allow_placeholder_aggregated_proof = test_config.allow_placeholder_aggregated_proof; + const signature_scheme = test_config.scheme; + + // Parse the anchorState to get validators + const anchor_state_value = case_obj.get("anchorState") orelse { + std.debug.print("fixture {s} case {s}: missing anchorState\n", .{ ctx.fixture_label, ctx.case_name }); + return FixtureError.InvalidFixture; + }; + + var anchor_state = try buildState(allocator, ctx, anchor_state_value); + defer anchor_state.deinit(); + + // Parse the signedBlockWithAttestation + const signed_block_value = case_obj.get("signedBlockWithAttestation") orelse { + std.debug.print("fixture {s} case {s}: missing signedBlockWithAttestation\n", .{ ctx.fixture_label, ctx.case_name }); + return FixtureError.InvalidFixture; + }; + + var parsed = try buildSignedBlockWithAttestation(allocator, ctx, signed_block_value); + defer parsed.deinit(allocator); + + // Determine if we expect failure based on test name/path + const expect_failure = std.mem.indexOf(u8, ctx.fixture_label, "invalid") != null or + std.mem.indexOf(u8, ctx.case_name, "invalid") != null; + + // Verify signatures + const verify_result = verifySignaturesWithFixtureProofs( + allocator, + &anchor_state, + &parsed.signed_block, + parsed.attestation_proofs, + signature_ssz_len, + signature_scheme, + allow_placeholder_aggregated_proof, + ); + + if (expect_failure) { + if (verify_result) |_| { + std.debug.print( + "fixture {s} case {s}: expected verification to fail but it succeeded\n", + .{ ctx.fixture_label, ctx.case_name }, + ); + return FixtureError.FixtureMismatch; + } else |_| { + // Expected failure + } + } else { + verify_result catch |err| { + std.debug.print( + "fixture {s} case {s}: signature verification failed with {s}\n", + .{ ctx.fixture_label, ctx.case_name, @errorName(err) }, + ); + return FixtureError.FixtureMismatch; + }; + } +} + +fn verifySignaturesWithFixtureProofs( + allocator: std.mem.Allocator, + state: *const types.BeamState, + signed_block: *const types.SignedBlockWithAttestation, + proofs: []const AggregatedSignatureProof, + signature_ssz_len: usize, + signature_scheme: xmss.HashSigScheme, + allow_placeholder_aggregated_proof: bool, +) !void { + const attestations = signed_block.message.block.body.attestations.constSlice(); + + if (attestations.len != proofs.len) { + return types.StateTransitionError.InvalidBlockSignatures; + } + + const validators = state.validators.constSlice(); + + for (attestations, proofs) |aggregated_attestation, proof| { + // Ensure the declared participants match the aggregated attestation bitfield. + if (aggregated_attestation.aggregation_bits.len() != proof.participants.len()) { + return types.StateTransitionError.InvalidBlockSignatures; + } + for (0..aggregated_attestation.aggregation_bits.len()) |i| { + if (try aggregated_attestation.aggregation_bits.get(i) != try proof.participants.get(i)) { + return types.StateTransitionError.InvalidBlockSignatures; + } + } + + var validator_indices = try types.aggregationBitsToValidatorIndices(&aggregated_attestation.aggregation_bits, allocator); + defer validator_indices.deinit(); + + for (validator_indices.items) |validator_index| { + if (validator_index >= validators.len) { + return types.StateTransitionError.InvalidValidatorId; + } + } + + // NOTE: leanSpec currently serializes a placeholder proof (`0x00`) in test mode. + // We accept the proof bytes and only validate participant bookkeeping. + if (allow_placeholder_aggregated_proof) { + if (proof.proof_data.len == 0) { + return types.StateTransitionError.InvalidBlockSignatures; + } + } else { + // Future: verify aggregated proofs against leanMultisig once the fixture format + // provides verifiable proof bytes in non-test environments. + } + } + + // Verify proposer attestation signature (standard XMSS signature) + const proposer_attestation = signed_block.message.proposer_attestation; + try verifySingleAttestationSignature( + allocator, + state, + @intCast(proposer_attestation.validator_id), + &proposer_attestation.data, + &signed_block.signature.proposer_signature, + signature_ssz_len, + signature_scheme, + ); +} + +fn verifySingleAttestationSignature( + allocator: std.mem.Allocator, + state: *const types.BeamState, + validator_index: usize, + attestation_data: *const types.AttestationData, + signature_bytes: *const types.SIGBYTES, + signature_ssz_len: usize, + signature_scheme: xmss.HashSigScheme, +) !void { + if (signature_ssz_len > signature_bytes.len) { + return types.StateTransitionError.InvalidBlockSignatures; + } + + const validators = state.validators.constSlice(); + if (validator_index >= validators.len) { + return types.StateTransitionError.InvalidValidatorId; + } + + const pubkey = validators[validator_index].getPubkey(); + + var message: [32]u8 = undefined; + try ssz.hashTreeRoot(types.AttestationData, attestation_data.*, &message, allocator); + + const epoch: u32 = @intCast(attestation_data.slot); + try xmss.verifySsz(pubkey, &message, epoch, signature_bytes.*[0..signature_ssz_len], signature_scheme); +} + +fn buildState( + allocator: std.mem.Allocator, + ctx: Context, + value: JsonValue, +) FixtureError!types.BeamState { + const pre_obj = try expect.expectObjectValue(FixtureError, value, ctx, "anchorState"); + + const config_obj = try expect.expectObject(FixtureError, pre_obj, &.{"config"}, ctx, "config"); + const genesis_time = try expect.expectU64Field(FixtureError, config_obj, &.{"genesisTime"}, ctx, "config.genesisTime"); + + const slot = try expect.expectU64Field(FixtureError, pre_obj, &.{"slot"}, ctx, "slot"); + + const header_obj = try expect.expectObject(FixtureError, pre_obj, &.{"latestBlockHeader"}, ctx, "latestBlockHeader"); + const latest_block_header = try parseBlockHeader(ctx, header_obj); + + const latest_justified = try parseCheckpoint(ctx, pre_obj, "latestJustified"); + const latest_finalized = try parseCheckpoint(ctx, pre_obj, "latestFinalized"); + + var historical = try types.HistoricalBlockHashes.init(allocator); + errdefer historical.deinit(); + if (pre_obj.get("historicalBlockHashes")) |val| { + try expect.appendBytesDataField(FixtureError, types.Root, &historical, ctx, val, "historicalBlockHashes"); + } + + var justified_slots = try types.JustifiedSlots.init(allocator); + errdefer justified_slots.deinit(); + if (pre_obj.get("justifiedSlots")) |val| { + try expect.appendBoolDataField(FixtureError, &justified_slots, ctx, val, "justifiedSlots"); + } + + var validators = try parseValidators(allocator, ctx, pre_obj); + errdefer validators.deinit(); + + var just_roots = try types.JustificationRoots.init(allocator); + errdefer just_roots.deinit(); + if (pre_obj.get("justificationsRoots")) |val| { + try expect.appendBytesDataField(FixtureError, types.Root, &just_roots, ctx, val, "justificationsRoots"); + } + + var just_validators = try types.JustificationValidators.init(allocator); + errdefer just_validators.deinit(); + if (pre_obj.get("justificationsValidators")) |val| { + try expect.appendBoolDataField(FixtureError, &just_validators, ctx, val, "justificationsValidators"); + } + + return types.BeamState{ + .config = .{ .genesis_time = genesis_time }, + .slot = slot, + .latest_block_header = latest_block_header, + .latest_justified = latest_justified, + .latest_finalized = latest_finalized, + .historical_block_hashes = historical, + .justified_slots = justified_slots, + .validators = validators, + .justifications_roots = just_roots, + .justifications_validators = just_validators, + }; +} + +fn parseValidators( + allocator: std.mem.Allocator, + ctx: Context, + pre_obj: std.json.ObjectMap, +) FixtureError!types.Validators { + var validators = try types.Validators.init(allocator); + errdefer validators.deinit(); + + if (pre_obj.get("validators")) |val| { + const validators_obj = try expect.expectObjectValue(FixtureError, val, ctx, "validators"); + if (validators_obj.get("data")) |data_val| { + const arr = try expect.expectArrayValue(FixtureError, data_val, ctx, "validators.data"); + for (arr.items, 0..) |item, idx| { + var base_label_buf: [64]u8 = undefined; + const base_label = std.fmt.bufPrint(&base_label_buf, "validators[{d}]", .{idx}) catch "validators"; + const validator_obj = try expect.expectObjectValue(FixtureError, item, ctx, base_label); + + var label_buf: [96]u8 = undefined; + const pubkey_label = std.fmt.bufPrint(&label_buf, "{s}.pubkey", .{base_label}) catch "validator.pubkey"; + const pubkey = try expect.expectBytesField(FixtureError, types.Bytes52, validator_obj, &.{"pubkey"}, ctx, pubkey_label); + + const validator_index: u64 = blk: { + if (validator_obj.get("index")) |index_value| { + var index_label_buf: [96]u8 = undefined; + const index_label = std.fmt.bufPrint(&index_label_buf, "{s}.index", .{base_label}) catch "validator.index"; + break :blk try expect.expectU64Value(FixtureError, index_value, ctx, index_label); + } + break :blk @as(u64, @intCast(idx)); + }; + + validators.append(.{ .pubkey = pubkey, .index = validator_index }) catch |err| { + std.debug.print( + "fixture {s} case {s}: validator #{} append failed: {s}\n", + .{ ctx.fixture_label, ctx.case_name, idx, @errorName(err) }, + ); + return FixtureError.InvalidFixture; + }; + } + } + } + + return validators; +} + +fn buildSignedBlockWithAttestation( + allocator: std.mem.Allocator, + ctx: Context, + value: JsonValue, +) FixtureError!ParsedSignedBlockWithAttestation { + const signed_block_obj = try expect.expectObjectValue(FixtureError, value, ctx, "signedBlockWithAttestation"); + + // Parse message + const message_obj = try expect.expectObject(FixtureError, signed_block_obj, &.{"message"}, ctx, "message"); + + // Parse block within message + const block_obj = try expect.expectObject(FixtureError, message_obj, &.{"block"}, ctx, "message.block"); + const block = try buildBlock(allocator, ctx, block_obj); + + // Parse proposerAttestation + const proposer_att_obj = try expect.expectObject(FixtureError, message_obj, &.{"proposerAttestation"}, ctx, "message.proposerAttestation"); + const proposer_attestation = try parseProposerAttestation(ctx, proposer_att_obj); + + // Parse signature section + const signature_obj = try expect.expectObject(FixtureError, signed_block_obj, &.{"signature"}, ctx, "signature"); + + // Parse attestation aggregated signature proofs + var attestation_proofs = std.ArrayList(AggregatedSignatureProof).init(allocator); + errdefer { + for (attestation_proofs.items) |*proof| proof.deinit(allocator); + attestation_proofs.deinit(); + } + if (signature_obj.get("attestationSignatures")) |att_sigs_val| { + const att_sigs_obj = try expect.expectObjectValue(FixtureError, att_sigs_val, ctx, "signature.attestationSignatures"); + if (att_sigs_obj.get("data")) |data_val| { + const arr = try expect.expectArrayValue(FixtureError, data_val, ctx, "signature.attestationSignatures.data"); + for (arr.items, 0..) |item, idx| { + var label_buf: [96]u8 = undefined; + const entry_label = std.fmt.bufPrint(&label_buf, "signature.attestationSignatures.data[{d}]", .{idx}) catch "signature.attestationSignatures.data"; + + const entry_obj = try expect.expectObjectValue(FixtureError, item, ctx, entry_label); + + const participants_val = entry_obj.get("participants") orelse { + std.debug.print( + "fixture {s} case {s}: missing participants in {s}\n", + .{ ctx.fixture_label, ctx.case_name, entry_label }, + ); + return FixtureError.InvalidFixture; + }; + var participants = try parseAggregationBits(allocator, ctx, participants_val, "participants"); + + const proof_val = entry_obj.get("proofData") orelse entry_obj.get("proof_data") orelse { + std.debug.print( + "fixture {s} case {s}: missing proofData in {s}\n", + .{ ctx.fixture_label, ctx.case_name, entry_label }, + ); + participants.deinit(); + return FixtureError.InvalidFixture; + }; + const proof_data = try parseByteListMiB(allocator, ctx, proof_val, "proofData"); + + attestation_proofs.append(.{ .participants = participants, .proof_data = proof_data }) catch |err| { + std.debug.print( + "fixture {s} case {s}: failed to append attestation proof: {s}\n", + .{ ctx.fixture_label, ctx.case_name, @errorName(err) }, + ); + participants.deinit(); + allocator.free(proof_data); + return FixtureError.InvalidFixture; + }; + } + } + } + + // Parse proposer_signature + const proposer_sig = try parseSignature(allocator, ctx, signature_obj, "proposerSignature"); + + var signatures = types.createBlockSignatures(allocator, block.body.attestations.len()) catch |err| { + std.debug.print( + "fixture {s} case {s}: unable to allocate signature groups: {s}\n", + .{ ctx.fixture_label, ctx.case_name, @errorName(err) }, + ); + return FixtureError.InvalidFixture; + }; + signatures.proposer_signature = proposer_sig; + + return ParsedSignedBlockWithAttestation{ + .signed_block = .{ + .message = .{ + .block = block, + .proposer_attestation = proposer_attestation, + }, + .signature = signatures, + }, + .attestation_proofs = attestation_proofs.toOwnedSlice() catch |err| { + std.debug.print( + "fixture {s} case {s}: unable to allocate attestation proof list: {s}\n", + .{ ctx.fixture_label, ctx.case_name, @errorName(err) }, + ); + return FixtureError.InvalidFixture; + }, + }; +} + +fn buildBlock( + allocator: std.mem.Allocator, + ctx: Context, + obj: std.json.ObjectMap, +) FixtureError!types.BeamBlock { + const slot = try expect.expectU64Field(FixtureError, obj, &.{"slot"}, ctx, "slot"); + const proposer_index = try expect.expectU64Field(FixtureError, obj, &.{ "proposer_index", "proposerIndex" }, ctx, "proposer_index"); + const parent_root = try expect.expectBytesField(FixtureError, types.Root, obj, &.{ "parent_root", "parentRoot" }, ctx, "parent_root"); + const state_root = try expect.expectBytesField(FixtureError, types.Root, obj, &.{ "state_root", "stateRoot" }, ctx, "state_root"); + + var attestations = try types.AggregatedAttestations.init(allocator); + errdefer attestations.deinit(); + + if (obj.get("body")) |body_val| { + const body_obj = try expect.expectObjectValue(FixtureError, body_val, ctx, "body"); + if (body_obj.get("attestations")) |att_val| { + const att_obj = try expect.expectObjectValue(FixtureError, att_val, ctx, "body.attestations"); + if (att_obj.get("data")) |data_val| { + const arr = try expect.expectArrayValue(FixtureError, data_val, ctx, "body.attestations.data"); + for (arr.items, 0..) |item, idx| { + var label_buf: [96]u8 = undefined; + const entry_label = std.fmt.bufPrint(&label_buf, "body.attestations.data[{d}]", .{idx}) catch "body.attestations.data"; + + const att_item_obj = try expect.expectObjectValue(FixtureError, item, ctx, entry_label); + + const bits_val = att_item_obj.get("aggregationBits") orelse { + std.debug.print( + "fixture {s} case {s}: missing aggregationBits in {s}\n", + .{ ctx.fixture_label, ctx.case_name, entry_label }, + ); + return FixtureError.InvalidFixture; + }; + var aggregation_bits = try parseAggregationBits(allocator, ctx, bits_val, "aggregationBits"); + errdefer aggregation_bits.deinit(); + + const data_obj = try expect.expectObject(FixtureError, att_item_obj, &.{"data"}, ctx, "attestation.data"); + const data = try parseAttestationData(ctx, data_obj); + + attestations.append(.{ .aggregation_bits = aggregation_bits, .data = data }) catch |err| { + std.debug.print( + "fixture {s} case {s}: failed to append attestation: {s}\n", + .{ ctx.fixture_label, ctx.case_name, @errorName(err) }, + ); + return FixtureError.InvalidFixture; + }; + } + } + } + } + + return types.BeamBlock{ + .slot = slot, + .proposer_index = proposer_index, + .parent_root = parent_root, + .state_root = state_root, + .body = .{ .attestations = attestations }, + }; +} + +fn parseAggregationBits( + allocator: std.mem.Allocator, + ctx: Context, + value: JsonValue, + label: []const u8, +) FixtureError!types.AggregationBits { + const obj = try expect.expectObjectValue(FixtureError, value, ctx, label); + const arr = try expect.expectArrayField(FixtureError, obj, &.{"data"}, ctx, label); + + var bits = try types.AggregationBits.init(allocator); + errdefer bits.deinit(); + + for (arr.items) |bit_val| { + const bit = switch (bit_val) { + .bool => |b| b, + else => { + std.debug.print( + "fixture {s} case {s}: {s} must contain booleans\n", + .{ ctx.fixture_label, ctx.case_name, label }, + ); + return FixtureError.InvalidFixture; + }, + }; + bits.append(bit) catch |err| { + std.debug.print( + "fixture {s} case {s}: failed to append {s} bit: {s}\n", + .{ ctx.fixture_label, ctx.case_name, label, @errorName(err) }, + ); + return FixtureError.InvalidFixture; + }; + } + + return bits; +} + +fn parseByteListMiB( + allocator: std.mem.Allocator, + ctx: Context, + value: JsonValue, + label: []const u8, +) FixtureError![]u8 { + const obj = try expect.expectObjectValue(FixtureError, value, ctx, label); + const text = try expect.expectStringField(FixtureError, obj, &.{"data"}, ctx, label); + + if (text.len < 2 or !std.mem.eql(u8, text[0..2], "0x")) { + std.debug.print( + "fixture {s} case {s}: field {s}.data missing 0x prefix\n", + .{ ctx.fixture_label, ctx.case_name, label }, + ); + return FixtureError.InvalidFixture; + } + + const body = text[2..]; + if (body.len % 2 != 0) { + std.debug.print( + "fixture {s} case {s}: field {s}.data has odd hex length\n", + .{ ctx.fixture_label, ctx.case_name, label }, + ); + return FixtureError.InvalidFixture; + } + + const out_len = body.len / 2; + const out = allocator.alloc(u8, out_len) catch |err| { + std.debug.print( + "fixture {s} case {s}: unable to allocate {d} bytes for {s}: {s}\n", + .{ ctx.fixture_label, ctx.case_name, out_len, label, @errorName(err) }, + ); + return FixtureError.InvalidFixture; + }; + errdefer allocator.free(out); + _ = std.fmt.hexToBytes(out, body) catch { + std.debug.print( + "fixture {s} case {s}: field {s}.data invalid hex\n", + .{ ctx.fixture_label, ctx.case_name, label }, + ); + return FixtureError.InvalidFixture; + }; + return out; +} + +fn parseProposerAttestation( + ctx: Context, + obj: std.json.ObjectMap, +) FixtureError!types.Attestation { + const validator_id = try expect.expectU64Field(FixtureError, obj, &.{"validatorId"}, ctx, "proposerAttestation.validatorId"); + + const data_obj = try expect.expectObject(FixtureError, obj, &.{"data"}, ctx, "proposerAttestation.data"); + const data = try parseAttestationData(ctx, data_obj); + + return types.Attestation{ + .validator_id = validator_id, + .data = data, + }; +} + +fn parseAttestationData( + ctx: Context, + obj: std.json.ObjectMap, +) FixtureError!types.AttestationData { + const slot = try expect.expectU64Field(FixtureError, obj, &.{"slot"}, ctx, "data.slot"); + + const head_obj = try expect.expectObject(FixtureError, obj, &.{"head"}, ctx, "data.head"); + const head = types.Checkpoint{ + .root = try expect.expectBytesField(FixtureError, types.Root, head_obj, &.{"root"}, ctx, "data.head.root"), + .slot = try expect.expectU64Field(FixtureError, head_obj, &.{"slot"}, ctx, "data.head.slot"), + }; + + const target_obj = try expect.expectObject(FixtureError, obj, &.{"target"}, ctx, "data.target"); + const target = types.Checkpoint{ + .root = try expect.expectBytesField(FixtureError, types.Root, target_obj, &.{"root"}, ctx, "data.target.root"), + .slot = try expect.expectU64Field(FixtureError, target_obj, &.{"slot"}, ctx, "data.target.slot"), + }; + + const source_obj = try expect.expectObject(FixtureError, obj, &.{"source"}, ctx, "data.source"); + const source = types.Checkpoint{ + .root = try expect.expectBytesField(FixtureError, types.Root, source_obj, &.{"root"}, ctx, "data.source.root"), + .slot = try expect.expectU64Field(FixtureError, source_obj, &.{"slot"}, ctx, "data.source.slot"), + }; + + return types.AttestationData{ + .slot = slot, + .head = head, + .target = target, + .source = source, + }; +} + +fn parseSignature( + allocator: std.mem.Allocator, + ctx: Context, + obj: std.json.ObjectMap, + field_name: []const u8, +) FixtureError!types.SIGBYTES { + const sig_value = obj.get(field_name) orelse { + std.debug.print( + "fixture {s} case {s}: missing field {s}\n", + .{ ctx.fixture_label, ctx.case_name, field_name }, + ); + return FixtureError.InvalidFixture; + }; + + // Re-serialize just the signature object and let Rust parse/SSZ-encode it. + var json_buf = std.ArrayList(u8).init(allocator); + defer json_buf.deinit(); + + std.json.stringify(sig_value, .{}, json_buf.writer()) catch |err| { + std.debug.print( + "fixture {s} case {s}: failed to stringify signature JSON: {s}\n", + .{ ctx.fixture_label, ctx.case_name, @errorName(err) }, + ); + return FixtureError.InvalidFixture; + }; + + var sig_bytes: types.SIGBYTES = std.mem.zeroes(types.SIGBYTES); + const written = xmss.signatureSszFromJson(json_buf.items, sig_bytes[0..]) catch { + std.debug.print( + "fixture {s} case {s}: Rust JSON→SSZ conversion failed\n", + .{ ctx.fixture_label, ctx.case_name }, + ); + return FixtureError.InvalidFixture; + }; + + if (written > sig_bytes.len) { + std.debug.print( + "fixture {s} case {s}: Rust JSON→SSZ wrote {d} bytes, max {d}\n", + .{ ctx.fixture_label, ctx.case_name, written, sig_bytes.len }, + ); + return FixtureError.InvalidFixture; + } + + return sig_bytes; +} + +fn parseU32ArrayN( + comptime N: usize, + ctx: Context, + obj: std.json.ObjectMap, + label: []const u8, +) FixtureError![N]u32 { + const data_arr = try expect.expectArrayField(FixtureError, obj, &.{"data"}, ctx, label); + if (data_arr.items.len != N) { + std.debug.print( + "fixture {s} case {s}: {s} length {d} != expected {d}\n", + .{ ctx.fixture_label, ctx.case_name, label, data_arr.items.len, N }, + ); + return FixtureError.InvalidFixture; + } + + var result: [N]u32 = undefined; + for (data_arr.items, 0..) |val, i| { + result[i] = @intCast(try expect.expectU64Value(FixtureError, val, ctx, label)); + } + return result; +} + +fn parseU32Array8( + ctx: Context, + obj: std.json.ObjectMap, + label: []const u8, +) FixtureError![8]u32 { + return parseU32ArrayN(8, ctx, obj, label); +} + +fn parseU32Array7( + ctx: Context, + obj: std.json.ObjectMap, + label: []const u8, +) FixtureError![7]u32 { + return parseU32ArrayN(7, ctx, obj, label); +} + +fn parseCheckpoint( + ctx: Context, + parent: std.json.ObjectMap, + field_name: []const u8, +) FixtureError!types.Checkpoint { + const cp_obj = try expect.expectObject(FixtureError, parent, &.{field_name}, ctx, field_name); + + var root_label_buf: [96]u8 = undefined; + const root_label = std.fmt.bufPrint(&root_label_buf, "{s}.root", .{field_name}) catch field_name; + var slot_label_buf: [96]u8 = undefined; + const slot_label = std.fmt.bufPrint(&slot_label_buf, "{s}.slot", .{field_name}) catch field_name; + + return types.Checkpoint{ + .root = try expect.expectBytesField(FixtureError, types.Root, cp_obj, &.{"root"}, ctx, root_label), + .slot = try expect.expectU64Field(FixtureError, cp_obj, &.{"slot"}, ctx, slot_label), + }; +} + +fn parseBlockHeader( + ctx: Context, + obj: std.json.ObjectMap, +) FixtureError!types.BeamBlockHeader { + return types.BeamBlockHeader{ + .slot = try expect.expectU64Field(FixtureError, obj, &.{"slot"}, ctx, "latestBlockHeader.slot"), + .proposer_index = try expect.expectU64Field(FixtureError, obj, &.{ "proposerIndex", "proposer_index" }, ctx, "latestBlockHeader.proposerIndex"), + .parent_root = try expect.expectBytesField(FixtureError, types.Root, obj, &.{ "parentRoot", "parent_root" }, ctx, "latestBlockHeader.parentRoot"), + .state_root = try expect.expectBytesField(FixtureError, types.Root, obj, &.{ "stateRoot", "state_root" }, ctx, "latestBlockHeader.stateRoot"), + .body_root = try expect.expectBytesField(FixtureError, types.Root, obj, &.{ "bodyRoot", "body_root" }, ctx, "latestBlockHeader.bodyRoot"), + }; +} diff --git a/pkgs/state-transition/src/lib.zig b/pkgs/state-transition/src/lib.zig index d2439ee8c..3f6b8f0c7 100644 --- a/pkgs/state-transition/src/lib.zig +++ b/pkgs/state-transition/src/lib.zig @@ -12,7 +12,9 @@ pub const apply_raw_block = transition.apply_raw_block; pub const StateTransitionError = transition.StateTransitionError; pub const StateTransitionOpts = transition.StateTransitionOpts; pub const verifySignatures = transition.verifySignatures; +pub const verifySignaturesWithScheme = transition.verifySignaturesWithScheme; pub const verifySingleAttestation = transition.verifySingleAttestation; +pub const verifySingleAttestationWithScheme = transition.verifySingleAttestationWithScheme; const mockImport = @import("./mock.zig"); pub const genMockChain = mockImport.genMockChain; diff --git a/pkgs/state-transition/src/transition.zig b/pkgs/state-transition/src/transition.zig index bcfda6ded..2772e2079 100644 --- a/pkgs/state-transition/src/transition.zig +++ b/pkgs/state-transition/src/transition.zig @@ -1,7 +1,6 @@ const std = @import("std"); const json = std.json; const types = @import("@zeam/types"); -const utils = types.utils; const params = @import("@zeam/params"); const zeam_utils = @import("@zeam/utils"); @@ -59,6 +58,20 @@ pub fn verifySignatures( allocator: Allocator, state: *const types.BeamState, signed_block: *const types.SignedBlockWithAttestation, +) !void { + return verifySignaturesWithScheme( + allocator, + state, + signed_block, + .prod, + ); +} + +pub fn verifySignaturesWithScheme( + allocator: Allocator, + state: *const types.BeamState, + signed_block: *const types.SignedBlockWithAttestation, + signature_scheme: xmss.HashSigScheme, ) !void { const attestations = signed_block.message.block.body.attestations.constSlice(); const signature_proofs = signed_block.signature.attestation_signatures.constSlice(); @@ -128,22 +141,29 @@ pub fn verifySignatures( // Verify proposer signature (still individual) const proposer_attestation = signed_block.message.proposer_attestation; - try verifySingleAttestation( + try verifySingleAttestationWithScheme( allocator, state, @intCast(proposer_attestation.validator_id), &proposer_attestation.data, &signed_block.signature.proposer_signature, + signature_scheme, ); } -pub fn verifySingleAttestation( +pub fn verifySingleAttestationWithScheme( allocator: Allocator, state: *const types.BeamState, validator_index: usize, attestation_data: *const types.AttestationData, signatureBytes: *const types.SIGBYTES, + signature_scheme: xmss.HashSigScheme, ) !void { + const signature_ssz_len = xmss.signatureSszLenForScheme(signature_scheme); + if (signature_ssz_len > signatureBytes.len) { + return StateTransitionError.InvalidBlockSignatures; + } + const validatorIndex = validator_index; const validators = state.validators.constSlice(); if (validatorIndex >= validators.len) { @@ -159,10 +179,27 @@ pub fn verifySingleAttestation( const epoch: u32 = @intCast(attestation_data.slot); - try xmss.verifySsz(pubkey, &message, epoch, signatureBytes); + try xmss.verifySsz(pubkey, &message, epoch, signatureBytes.*[0..signature_ssz_len], signature_scheme); _ = verification_timer.observe(); } +pub fn verifySingleAttestation( + allocator: Allocator, + state: *const types.BeamState, + validator_index: usize, + attestation_data: *const types.AttestationData, + signatureBytes: *const types.SIGBYTES, +) !void { + return verifySingleAttestationWithScheme( + allocator, + state, + validator_index, + attestation_data, + signatureBytes, + .prod, + ); +} + // TODO(gballet) check if beam block needs to be a pointer pub fn apply_transition(allocator: Allocator, state: *types.BeamState, block: types.BeamBlock, opts: StateTransitionOpts) !void { opts.logger.debug("applying state transition state-slot={d} block-slot={d}\n", .{ state.slot, block.slot }); diff --git a/pkgs/xmss/src/hashsig.zig b/pkgs/xmss/src/hashsig.zig index a490d103c..05eed6b4a 100644 --- a/pkgs/xmss/src/hashsig.zig +++ b/pkgs/xmss/src/hashsig.zig @@ -3,6 +3,21 @@ const Allocator = std.mem.Allocator; pub const aggregate = @import("aggregation.zig"); +pub const HashSigScheme = enum(u8) { + @"test" = 0, + prod = 1, +}; + +pub const PROD_SIGNATURE_SSZ_LEN: usize = 3112; +pub const TEST_SIGNATURE_SSZ_LEN: usize = 424; + +pub fn signatureSszLenForScheme(scheme: HashSigScheme) usize { + return switch (scheme) { + .@"test" => TEST_SIGNATURE_SSZ_LEN, + .prod => PROD_SIGNATURE_SSZ_LEN, + }; +} + /// Opaque pointer to the Rust KeyPair struct pub const HashSigKeyPair = opaque {}; @@ -104,8 +119,18 @@ extern fn hashsig_verify_ssz( epoch: u32, signature_bytes: [*]const u8, signature_len: usize, + scheme: HashSigScheme, ) i32; +/// Convert signature JSON (proposerSignature object) into SSZ bytes. +/// Returns number of bytes written, or 0 on error. +extern fn hashsig_signature_ssz_from_json( + signature_json_ptr: [*]const u8, + signature_json_len: usize, + out_ptr: [*]u8, + out_len: usize, +) usize; + pub const HashSigError = error{ KeyGenerationFailed, SigningFailed, VerificationFailed, InvalidSignature, SerializationFailed, InvalidMessageLength, DeserializationFailed, OutOfMemory }; /// Verify signature using SSZ-encoded bytes @@ -114,6 +139,7 @@ pub fn verifySsz( message: []const u8, epoch: u32, signature_bytes: []const u8, + scheme: HashSigScheme, ) HashSigError!void { if (message.len != 32) { return HashSigError.InvalidMessageLength; @@ -126,6 +152,7 @@ pub fn verifySsz( epoch, signature_bytes.ptr, signature_bytes.len, + scheme, ); switch (result) { @@ -136,6 +163,21 @@ pub fn verifySsz( } } +/// Fill `out` with SSZ signature bytes parsed from a signature JSON object. +pub fn signatureSszFromJson(signature_json: []const u8, out: []u8) HashSigError!usize { + const written = hashsig_signature_ssz_from_json( + signature_json.ptr, + signature_json.len, + out.ptr, + out.len, + ); + + if (written == 0) { + return HashSigError.DeserializationFailed; + } + return written; +} + /// Wrapper for the hash signature key pair pub const KeyPair = struct { handle: *HashSigKeyPair, @@ -508,6 +550,7 @@ test "HashSig: SSZ serialize and verify" { &message, epoch, sig_buffer[0..sig_size], + .prod, ); std.debug.print("Verification succeeded!\n", .{}); @@ -542,6 +585,7 @@ test "HashSig: verify fails with zero signature" { &message, epoch, &zero_sig_buffer, + .prod, ); try std.testing.expectError(HashSigError.InvalidSignature, invalid_signature_result); @@ -553,6 +597,7 @@ test "HashSig: verify fails with zero signature" { &invalid_message, epoch, signature_buffer[0..signature_size], + .prod, ); try std.testing.expectError(HashSigError.VerificationFailed, verification_failed_result); diff --git a/pkgs/xmss/src/lib.zig b/pkgs/xmss/src/lib.zig index ed18b1011..e4ea570a5 100644 --- a/pkgs/xmss/src/lib.zig +++ b/pkgs/xmss/src/lib.zig @@ -13,7 +13,12 @@ pub const KeyPair = hashsig.KeyPair; pub const Signature = hashsig.Signature; pub const PublicKey = hashsig.PublicKey; pub const HashSigError = hashsig.HashSigError; +pub const HashSigScheme = hashsig.HashSigScheme; +pub const PROD_SIGNATURE_SSZ_LEN = hashsig.PROD_SIGNATURE_SSZ_LEN; +pub const TEST_SIGNATURE_SSZ_LEN = hashsig.TEST_SIGNATURE_SSZ_LEN; +pub const signatureSszLenForScheme = hashsig.signatureSszLenForScheme; pub const verifySsz = hashsig.verifySsz; +pub const signatureSszFromJson = hashsig.signatureSszFromJson; pub const HashSigKeyPair = hashsig.HashSigKeyPair; pub const HashSigSignature = hashsig.HashSigSignature; pub const HashSigPublicKey = hashsig.HashSigPublicKey; diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 5529a0111..8a3577bcd 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -90,7 +90,7 @@ version = "0.1.0" source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=e4474138487eeb1ed7c2e1013674fe80ac9f3165#e4474138487eeb1ed7c2e1013674fe80ac9f3165" dependencies = [ "multilinear-toolkit", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-util 0.3.0", "tracing", "utils", ] @@ -100,7 +100,7 @@ name = "air" version = "0.3.0" source = "git+https://github.com/leanEthereum/multilinear-toolkit.git?branch=lean-vm-simple#e06cba2e214879c00c7fbc0e5b12908ddfcba588" dependencies = [ - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-field 0.3.0", ] [[package]] @@ -200,7 +200,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -211,7 +211,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -727,8 +727,8 @@ source = "git+https://github.com/leanEthereum/multilinear-toolkit.git?branch=lea dependencies = [ "fiat-shamir", "itertools 0.14.0", - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-field 0.3.0", + "p3-util 0.3.0", "rand 0.9.2", "rayon", "tracing", @@ -1289,7 +1289,7 @@ source = "git+https://github.com/leanEthereum/multilinear-toolkit.git?branch=lea dependencies = [ "air 0.3.0", "fiat-shamir", - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-field 0.3.0", ] [[package]] @@ -1801,7 +1801,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2016,7 +2016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2047,18 +2047,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "ethereum_ssz_derive" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78d247bc40823c365a62e572441a8f8b12df03f171713f06bc76180fcd56ab71" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "event-listener" version = "5.4.1" @@ -2169,8 +2157,8 @@ version = "0.1.0" source = "git+https://github.com/leanEthereum/fiat-shamir.git?branch=lean-vm-simple#9d4dc22f06cfa65f15bf5f1b07912a64c7feff0f" dependencies = [ "p3-challenger 0.3.0", - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-koala-bear 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-field 0.3.0", + "p3-koala-bear 0.3.0", "serde", ] @@ -2659,7 +2647,7 @@ name = "hashsig-glue" version = "0.1.0" dependencies = [ "ethereum_ssz", - "leansig 0.1.0 (git+https://github.com/leanEthereum/leanSig?rev=f10dcbefac2502d356d93f686e8b4ecd8dc8840a)", + "leansig", "rand 0.9.2", "rand_chacha 0.9.0", "serde", @@ -3295,10 +3283,10 @@ dependencies = [ "lookup", "multilinear-toolkit", "p3-challenger 0.3.0", - "p3-koala-bear 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-poseidon2 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-symmetric 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-koala-bear 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", "pest", "pest_derive", "rand 0.9.2", @@ -3320,10 +3308,10 @@ dependencies = [ "lookup", "multilinear-toolkit", "p3-challenger 0.3.0", - "p3-koala-bear 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-poseidon2 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-symmetric 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-koala-bear 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", "pest", "pest_derive", "rand 0.9.2", @@ -3347,10 +3335,10 @@ dependencies = [ "multilinear-toolkit", "num_enum", "p3-challenger 0.3.0", - "p3-koala-bear 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-poseidon2 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-symmetric 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-koala-bear 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", "pest", "pest_derive", "rand 0.9.2", @@ -3364,7 +3352,7 @@ dependencies = [ [[package]] name = "leansig" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leansig.git?rev=73bedc26ed961b110df7ac2e234dc11361a4bf25#73bedc26ed961b110df7ac2e234dc11361a4bf25" +source = "git+https://github.com/leanEthereum/leanSig?rev=73bedc26ed961b110df7ac2e234dc11361a4bf25#73bedc26ed961b110df7ac2e234dc11361a4bf25" dependencies = [ "dashmap", "ethereum_ssz", @@ -3381,27 +3369,6 @@ dependencies = [ "thiserror 2.0.17", ] -[[package]] -name = "leansig" -version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanSig?rev=f10dcbefac2502d356d93f686e8b4ecd8dc8840a#f10dcbefac2502d356d93f686e8b4ecd8dc8840a" -dependencies = [ - "dashmap", - "ethereum_ssz", - "ethereum_ssz_derive", - "num-bigint 0.4.6", - "num-traits", - "p3-baby-bear 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-field 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-koala-bear 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-symmetric 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "rand 0.9.2", - "rayon", - "serde", - "sha3", - "thiserror 2.0.17", -] - [[package]] name = "libc" version = "0.2.178" @@ -3890,8 +3857,8 @@ source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=e4474138487ee dependencies = [ "multilinear-toolkit", "p3-challenger 0.3.0", - "p3-koala-bear 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-koala-bear 0.3.0", + "p3-util 0.3.0", "rand 0.9.2", "tracing", "utils", @@ -4131,8 +4098,8 @@ dependencies = [ "backend", "constraints-folder", "fiat-shamir", - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-field 0.3.0", + "p3-util 0.3.0", "rayon", "sumcheck", "tracing", @@ -4143,8 +4110,8 @@ name = "multisig-glue" version = "0.1.0" dependencies = [ "ethereum_ssz", - "leansig 0.1.0 (git+https://github.com/leanEthereum/leansig.git?rev=73bedc26ed961b110df7ac2e234dc11361a4bf25)", - "p3-koala-bear 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "leansig", + "p3-koala-bear 0.3.0", "rec_aggregation", "whir-p3", ] @@ -4275,7 +4242,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5392,24 +5359,11 @@ name = "p3-baby-bear" version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" dependencies = [ - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-mds 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-monty-31 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-poseidon2 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-symmetric 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "rand 0.9.2", -] - -[[package]] -name = "p3-baby-bear" -version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" -dependencies = [ - "p3-field 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-mds 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-monty-31 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-poseidon2 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-symmetric 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", + "p3-field 0.3.0", + "p3-mds 0.3.0", + "p3-monty-31 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", "rand 0.9.2", ] @@ -5469,10 +5423,10 @@ name = "p3-challenger" version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" dependencies = [ - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-maybe-rayon 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-symmetric 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-field 0.3.0", + "p3-maybe-rayon 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", "tracing", ] @@ -5510,10 +5464,10 @@ source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple# dependencies = [ "itertools 0.14.0", "p3-challenger 0.3.0", - "p3-dft 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-matrix 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-dft 0.3.0", + "p3-field 0.3.0", + "p3-matrix 0.3.0", + "p3-util 0.3.0", "serde", ] @@ -5536,24 +5490,10 @@ version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" dependencies = [ "itertools 0.14.0", - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-matrix 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-maybe-rayon 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "tracing", -] - -[[package]] -name = "p3-dft" -version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" -dependencies = [ - "itertools 0.14.0", - "p3-field 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-matrix 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-maybe-rayon 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-util 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "spin 0.10.0", + "p3-field 0.3.0", + "p3-matrix 0.3.0", + "p3-maybe-rayon 0.3.0", + "p3-util 0.3.0", "tracing", ] @@ -5595,23 +5535,8 @@ source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple# dependencies = [ "itertools 0.14.0", "num-bigint 0.4.6", - "p3-maybe-rayon 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "paste", - "rand 0.9.2", - "serde", - "tracing", -] - -[[package]] -name = "p3-field" -version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" -dependencies = [ - "itertools 0.14.0", - "num-bigint 0.4.6", - "p3-maybe-rayon 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-util 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", + "p3-maybe-rayon 0.3.0", + "p3-util 0.3.0", "paste", "rand 0.9.2", "serde", @@ -5685,10 +5610,10 @@ name = "p3-interpolation" version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" dependencies = [ - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-matrix 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-maybe-rayon 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-field 0.3.0", + "p3-matrix 0.3.0", + "p3-maybe-rayon 0.3.0", + "p3-util 0.3.0", ] [[package]] @@ -5738,27 +5663,15 @@ source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple# dependencies = [ "itertools 0.14.0", "num-bigint 0.4.6", - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-monty-31 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-poseidon2 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-symmetric 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-field 0.3.0", + "p3-monty-31 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", "rand 0.9.2", "serde", ] -[[package]] -name = "p3-koala-bear" -version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" -dependencies = [ - "p3-field 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-monty-31 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-poseidon2 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-symmetric 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "rand 0.9.2", -] - [[package]] name = "p3-koala-bear" version = "0.4.1" @@ -5793,24 +5706,9 @@ version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" dependencies = [ "itertools 0.14.0", - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-maybe-rayon 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "rand 0.9.2", - "serde", - "tracing", - "transpose", -] - -[[package]] -name = "p3-matrix" -version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" -dependencies = [ - "itertools 0.14.0", - "p3-field 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-maybe-rayon 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-util 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", + "p3-field 0.3.0", + "p3-maybe-rayon 0.3.0", + "p3-util 0.3.0", "rand 0.9.2", "serde", "tracing", @@ -5848,11 +5746,6 @@ dependencies = [ "rayon", ] -[[package]] -name = "p3-maybe-rayon" -version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" - [[package]] name = "p3-maybe-rayon" version = "0.4.1" @@ -5877,22 +5770,10 @@ name = "p3-mds" version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" dependencies = [ - "p3-dft 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-symmetric 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "rand 0.9.2", -] - -[[package]] -name = "p3-mds" -version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" -dependencies = [ - "p3-dft 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-field 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-symmetric 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-util 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", + "p3-dft 0.3.0", + "p3-field 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", "rand 0.9.2", ] @@ -5932,11 +5813,11 @@ source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple# dependencies = [ "itertools 0.14.0", "p3-commit 0.3.0", - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-matrix 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-maybe-rayon 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-symmetric 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-field 0.3.0", + "p3-matrix 0.3.0", + "p3-maybe-rayon 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", "rand 0.9.2", "serde", "tracing", @@ -5970,40 +5851,17 @@ source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple# dependencies = [ "itertools 0.14.0", "num-bigint 0.4.6", - "p3-dft 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-matrix 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-maybe-rayon 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-mds 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-poseidon2 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-symmetric 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "paste", - "rand 0.9.2", - "serde", - "tracing", - "transpose", -] - -[[package]] -name = "p3-monty-31" -version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" -dependencies = [ - "itertools 0.14.0", - "num-bigint 0.4.6", - "p3-dft 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-field 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-matrix 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-maybe-rayon 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-mds 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-poseidon2 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-symmetric 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-util 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", + "p3-dft 0.3.0", + "p3-field 0.3.0", + "p3-matrix 0.3.0", + "p3-maybe-rayon 0.3.0", + "p3-mds 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", "paste", "rand 0.9.2", "serde", - "spin 0.10.0", "tracing", "transpose", ] @@ -6059,22 +5917,10 @@ name = "p3-poseidon2" version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" dependencies = [ - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-mds 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-symmetric 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "rand 0.9.2", -] - -[[package]] -name = "p3-poseidon2" -version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" -dependencies = [ - "p3-field 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-mds 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-symmetric 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", - "p3-util 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", + "p3-field 0.3.0", + "p3-mds 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", "rand 0.9.2", ] @@ -6122,17 +5968,7 @@ version = "0.3.0" source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple#4897086b6f460b969dc0baad5c4dff91a4eb1d67" dependencies = [ "itertools 0.14.0", - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "serde", -] - -[[package]] -name = "p3-symmetric" -version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" -dependencies = [ - "itertools 0.14.0", - "p3-field 0.3.0 (git+https://github.com/Plonky3/Plonky3.git?rev=a33a312)", + "p3-field 0.3.0", "serde", ] @@ -6181,14 +6017,6 @@ dependencies = [ "serde", ] -[[package]] -name = "p3-util" -version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" -dependencies = [ - "serde", -] - [[package]] name = "p3-util" version = "0.4.1" @@ -6909,14 +6737,14 @@ dependencies = [ "lean_compiler", "lean_prover", "lean_vm", - "leansig 0.1.0 (git+https://github.com/leanEthereum/leansig.git?rev=73bedc26ed961b110df7ac2e234dc11361a4bf25)", + "leansig", "lookup", "multilinear-toolkit", "p3-challenger 0.3.0", - "p3-koala-bear 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-poseidon2 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-symmetric 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-koala-bear 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", "rand 0.9.2", "serde", "serde_json", @@ -7438,7 +7266,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7975,7 +7803,7 @@ dependencies = [ "derive_more 2.1.0", "lookup", "multilinear-toolkit", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-util 0.3.0", "tracing", "utils", "whir-p3", @@ -7996,8 +7824,8 @@ dependencies = [ "backend", "constraints-folder", "fiat-shamir", - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-field 0.3.0", + "p3-util 0.3.0", "rayon", ] @@ -8086,7 +7914,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8652,10 +8480,10 @@ source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=e4474138487ee dependencies = [ "multilinear-toolkit", "p3-challenger 0.3.0", - "p3-koala-bear 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-poseidon2 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-symmetric 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-koala-bear 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", "tracing", "tracing-forest 0.3.0", "tracing-subscriber 0.3.22", @@ -8824,18 +8652,18 @@ source = "git+https://github.com/TomWambsgans/whir-p3?branch=lean-vm-simple#f74b dependencies = [ "itertools 0.14.0", "multilinear-toolkit", - "p3-baby-bear 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-baby-bear 0.3.0", "p3-challenger 0.3.0", "p3-commit 0.3.0", - "p3-dft 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-field 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-dft 0.3.0", + "p3-field 0.3.0", "p3-interpolation 0.3.0", - "p3-koala-bear 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-matrix 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-maybe-rayon 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-koala-bear 0.3.0", + "p3-matrix 0.3.0", + "p3-maybe-rayon 0.3.0", "p3-merkle-tree 0.3.0", - "p3-symmetric 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", "rand 0.9.2", "rayon", "thiserror 2.0.17", @@ -9228,11 +9056,11 @@ dependencies = [ "lookup", "multilinear-toolkit", "p3-challenger 0.3.0", - "p3-koala-bear 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-monty-31 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-poseidon2 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-symmetric 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", - "p3-util 0.3.0 (git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-vm-simple)", + "p3-koala-bear 0.3.0", + "p3-monty-31 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", "pest", "pest_derive", "rand 0.9.2", diff --git a/rust/hashsig-glue/Cargo.toml b/rust/hashsig-glue/Cargo.toml index 8cc235c5f..73223576d 100644 --- a/rust/hashsig-glue/Cargo.toml +++ b/rust/hashsig-glue/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] sha2 = "0.9" -leansig = { git = "https://github.com/leanEthereum/leanSig", rev = "f10dcbefac2502d356d93f686e8b4ecd8dc8840a" } +leansig = { git = "https://github.com/leanEthereum/leanSig", rev = "73bedc26ed961b110df7ac2e234dc11361a4bf25" } rand = "0.9.2" rand_chacha = "0.9.0" thiserror = "2.0.17" @@ -16,3 +16,4 @@ serde_json = "1.0" [lib] crate-type = ["staticlib"] name = "hashsig_glue" + diff --git a/rust/hashsig-glue/src/lib.rs b/rust/hashsig-glue/src/lib.rs index 2e40c07ab..cfb379c8f 100644 --- a/rust/hashsig-glue/src/lib.rs +++ b/rust/hashsig-glue/src/lib.rs @@ -2,14 +2,66 @@ use leansig::{signature::SignatureScheme, MESSAGE_LENGTH}; use rand::Rng; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; +use serde_json::Value; use sha2::{Digest, Sha256}; use std::ffi::CStr; use std::os::raw::c_char; use std::ptr; use std::slice; -pub type HashSigScheme = +const PROD_SIGNATURE_SSZ_LEN: usize = 3112; +const TEST_SIGNATURE_SSZ_LEN: usize = 424; + +#[repr(u8)] +enum HashSigSchemeId { + Test = 0, + Prod = 1, +} + +/// Production instantiation (LeanSpec `prod`). +pub type HashSigSchemeProd = leansig::signature::generalized_xmss::instantiations_poseidon_top_level::lifetime_2_to_the_32::hashing_optimized::SIGTopLevelTargetSumLifetime32Dim64Base8; + +/// Test instantiation matching LeanSpec `LEAN_ENV=test` constants. +/// +/// LeanSpec test config: +/// - MESSAGE_LENGTH=32 +/// - LOG_LIFETIME=8 +/// - DIMENSION=4 +/// - BASE=4 +/// - FINAL_LAYER=6 +/// - TARGET_SUM=6 +/// - PARAMETER_LEN=5 +/// - TWEAK_LEN_FE=2 +/// - MSG_LEN_FE=9 +/// - RAND_LEN_FE=7 +/// - HASH_LEN_FE=8 +/// - CAPACITY=9 +/// - POS_OUTPUT_LEN_PER_INV_FE=15 +/// - POS_INVOCATIONS=1 +pub type HashSigSchemeTest = leansig::signature::generalized_xmss::GeneralizedXMSSSignatureScheme< + leansig::symmetric::prf::shake_to_field::ShakePRFtoF<8, 7>, + leansig::inc_encoding::target_sum::TargetSumEncoding< + leansig::symmetric::message_hash::top_level_poseidon::TopLevelPoseidonMessageHash< + 15, + 1, + 15, + 4, + 4, + 6, + 2, + 9, + 5, + 7, + >, + 6, + >, + leansig::symmetric::tweak_hash::poseidon::PoseidonTweakHash<5, 8, 2, 9, 4>, + 8, +>; + +pub type HashSigScheme = HashSigSchemeProd; + pub type HashSigPrivateKey = ::SecretKey; pub type HashSigPublicKey = ::PublicKey; pub type HashSigSignature = ::Signature; @@ -476,12 +528,23 @@ pub unsafe extern "C" fn hashsig_verify_ssz( epoch: u32, signature_bytes: *const u8, signature_len: usize, + scheme_id: u8, ) -> i32 { if pubkey_bytes.is_null() || message.is_null() || signature_bytes.is_null() { return -1; } unsafe { + let expected_len = match scheme_id { + x if x == HashSigSchemeId::Test as u8 => TEST_SIGNATURE_SSZ_LEN, + x if x == HashSigSchemeId::Prod as u8 => PROD_SIGNATURE_SSZ_LEN, + _ => return -1, + }; + + if signature_len != expected_len { + return -1; + } + let pk_data = slice::from_raw_parts(pubkey_bytes, pubkey_len); let sig_data = slice::from_raw_parts(signature_bytes, signature_len); let msg_data = slice::from_raw_parts(message, MESSAGE_LENGTH); @@ -491,23 +554,239 @@ pub unsafe extern "C" fn hashsig_verify_ssz( Err(_) => return -1, }; - // Directly SSZ decode (leansig has SSZ support built-in) - let pk: HashSigPublicKey = match HashSigPublicKey::from_ssz_bytes(pk_data) { - Ok(pk) => pk, - Err(_) => return -1, - }; + fn verify_with_scheme( + pk_data: &[u8], + sig_data: &[u8], + epoch: u32, + message_array: &[u8; MESSAGE_LENGTH], + ) -> Result { + let pk = S::PublicKey::from_ssz_bytes(pk_data).map_err(|_| ())?; + let sig = S::Signature::from_ssz_bytes(sig_data).map_err(|_| ())?; + Ok(S::verify(&pk, epoch, message_array, &sig)) + } - let sig: HashSigSignature = match HashSigSignature::from_ssz_bytes(sig_data) { - Ok(sig) => sig, - Err(_) => return -1, + let attempt: Result = match scheme_id { + x if x == HashSigSchemeId::Test as u8 => { + verify_with_scheme::(pk_data, sig_data, epoch, message_array) + } + x if x == HashSigSchemeId::Prod as u8 => { + verify_with_scheme::(pk_data, sig_data, epoch, message_array) + } + _ => return -1, }; - let is_valid = ::verify(&pk, epoch, message_array, &sig); + match attempt { + Ok(true) => 1, + Ok(false) => 0, + Err(()) => -1, + } + } +} + +fn json_u32(value: &Value) -> Option { + match value { + Value::Number(n) => n.as_u64().and_then(|v| u32::try_from(v).ok()), + _ => None, + } +} + +fn json_get<'a>(obj: &'a Value, key: &str) -> Option<&'a Value> { + match obj { + Value::Object(map) => map.get(key), + _ => None, + } +} + +fn parse_u32_fixed_array(value: &Value, expected_len: usize) -> Option> { + let arr = match value { + Value::Array(a) => a, + _ => return None, + }; + if arr.len() != expected_len { + return None; + } + let mut out = Vec::with_capacity(expected_len); + for v in arr { + out.push(json_u32(v)?); + } + Some(out) +} + +fn parse_vec_of_u32x8(value: &Value) -> Option> { + let arr = match value { + Value::Array(a) => a, + _ => return None, + }; + + let mut out: Vec<[u32; 8]> = Vec::with_capacity(arr.len()); + for item in arr { + let data = json_get(item, "data")?; + let nums = parse_u32_fixed_array(data, 8)?; + let mut fixed = [0u32; 8]; + fixed.copy_from_slice(&nums); + out.push(fixed); + } + Some(out) +} + +fn write_u32_le(dst: &mut [u8], offset: usize, v: u32) -> Option<()> { + let end = offset.checked_add(4)?; + dst.get_mut(offset..end)?.copy_from_slice(&v.to_le_bytes()); + Some(()) +} + +/// Convert a signature JSON object into SSZ-encoded signature bytes. +/// +/// Expected JSON shape (object): +/// { "path": {"siblings": {"data": [ {"data": [u32;8]}, ... ]}}, +/// "rho": {"data": [u32;7]}, +/// "hashes": {"data": [ {"data": [u32;8]}, ... ]} } +/// +/// Returns number of bytes written, or 0 on error. +/// +/// # Safety +/// - `signature_json_ptr` must be either null or point to `signature_json_len` readable bytes. +/// - `out_ptr` must be either null or point to `out_len` writable bytes. +/// - Both buffers must be valid for the duration of the call and must not overlap in a way that +/// violates Rust aliasing rules. +#[no_mangle] +pub unsafe extern "C" fn hashsig_signature_ssz_from_json( + signature_json_ptr: *const u8, + signature_json_len: usize, + out_ptr: *mut u8, + out_len: usize, +) -> usize { + if signature_json_ptr.is_null() || out_ptr.is_null() { + return 0; + } - if is_valid { - 1 - } else { - 0 + let json_bytes = unsafe { slice::from_raw_parts(signature_json_ptr, signature_json_len) }; + let out = unsafe { slice::from_raw_parts_mut(out_ptr, out_len) }; + + let sig_val: Value = match serde_json::from_slice(json_bytes) { + Ok(v) => v, + Err(_) => return 0, + }; + + // Extract siblings + let path = match json_get(&sig_val, "path") { + Some(v) => v, + None => return 0, + }; + let siblings = match json_get(path, "siblings").and_then(|v| json_get(v, "data")) { + Some(v) => v, + None => return 0, + }; + let siblings_vec = match parse_vec_of_u32x8(siblings) { + Some(v) => v, + None => return 0, + }; + + // Extract rho + let rho = match json_get(&sig_val, "rho").and_then(|v| json_get(v, "data")) { + Some(v) => v, + None => return 0, + }; + let rho_vec = match parse_u32_fixed_array(rho, 7) { + Some(v) => v, + None => return 0, + }; + + // Extract hashes + let hashes = match json_get(&sig_val, "hashes").and_then(|v| json_get(v, "data")) { + Some(v) => v, + None => return 0, + }; + let hashes_vec = match parse_vec_of_u32x8(hashes) { + Some(v) => v, + None => return 0, + }; + + let sibling_size: usize = 8 * 4; + let hash_size: usize = 8 * 4; + let path_fixed_part: usize = 4; + let sig_fixed_part: usize = 36; + + let path_variable_size = siblings_vec.len().saturating_mul(sibling_size); + let path_total_size = match path_fixed_part.checked_add(path_variable_size) { + Some(v) => v, + None => return 0, + }; + + let hashes_size = hashes_vec.len().saturating_mul(hash_size); + + let total_size = match sig_fixed_part + .checked_add(path_total_size) + .and_then(|v| v.checked_add(hashes_size)) + { + Some(v) => v, + None => return 0, + }; + + if total_size > out_len { + return 0; + } + out[..total_size].fill(0); + + let offset_path: u32 = match u32::try_from(sig_fixed_part) { + Ok(v) => v, + Err(_) => return 0, + }; + let offset_hashes_u = match sig_fixed_part.checked_add(path_total_size) { + Some(v) => v, + None => return 0, + }; + let offset_hashes: u32 = match u32::try_from(offset_hashes_u) { + Ok(v) => v, + Err(_) => return 0, + }; + + // Signature fixed part + let mut write_pos: usize = 0; + if write_u32_le(out, write_pos, offset_path).is_none() { + return 0; + } + write_pos += 4; + for v in rho_vec { + if write_u32_le(out, write_pos, v).is_none() { + return 0; } + write_pos += 4; + } + if write_u32_le(out, write_pos, offset_hashes).is_none() { + return 0; } + write_pos += 4; + + // Path (HashTreeOpening) + let path_siblings_offset: u32 = 4; + if write_u32_le(out, write_pos, path_siblings_offset).is_none() { + return 0; + } + write_pos += 4; + + for sib in siblings_vec { + for v in sib { + if write_u32_le(out, write_pos, v).is_none() { + return 0; + } + write_pos += 4; + } + } + + // Hashes list + for h in hashes_vec { + for v in h { + if write_u32_le(out, write_pos, v).is_none() { + return 0; + } + write_pos += 4; + } + } + + if write_pos != total_size { + return 0; + } + + total_size }