From 70df6a81041403e0df0a7e07c2f0473c364108f6 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Wed, 15 Jul 2026 08:03:12 -0400 Subject: [PATCH 1/4] fix(state-transition): group-check signature sets --- src/state_transition/utils/signature_sets.zig | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/src/state_transition/utils/signature_sets.zig b/src/state_transition/utils/signature_sets.zig index 34e189212..79867d812 100644 --- a/src/state_transition/utils/signature_sets.zig +++ b/src/state_transition/utils/signature_sets.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const types = @import("consensus_types"); pub const bls = @import("bls"); const PublicKey = bls.PublicKey; @@ -26,7 +27,7 @@ pub const AggregatedSignatureSet = struct { pub fn verifySingleSignatureSet(set: *const SingleSignatureSet) !bool { // All signatures are not trusted and must be group checked (p2.subgroup_check) const signature = try Signature.uncompress(&set.signature); - if (verify(&set.signing_root, &set.pubkey, &signature, .{})) { + if (verify(&set.signing_root, &set.pubkey, &signature, .{ .sig_groupcheck = true })) { return true; } else |_| { return false; @@ -36,7 +37,12 @@ pub fn verifySingleSignatureSet(set: *const SingleSignatureSet) !bool { pub fn verifyAggregatedSignatureSet(set: *const AggregatedSignatureSet) !bool { // All signatures are not trusted and must be group checked (p2.subgroup_check) const signature = try Signature.uncompress(&set.signature); - return fastAggregateVerify(&set.signing_root, set.pubkeys, &signature, .{}); + return fastAggregateVerify( + &set.signing_root, + set.pubkeys, + &signature, + .{ .sig_groupcheck = true }, + ); } pub fn createSingleSignatureSetFromComponents(pubkey: *const PublicKey, signing_root: Root, signature: BLSSignature) SingleSignatureSet { @@ -55,4 +61,55 @@ pub fn createAggregateSignatureSetFromComponents(pubkeys: []const PublicKey, sig }; } -// TODO: unit tests +test "signature sets reject signatures outside G2 subgroup" { + const ikm = [_]u8{1} ** 32; + const signing_root = [_]u8{2} ** 32; + const secret_key = try bls.SecretKey.keyGen(&ikm, null); + const public_key = secret_key.toPublicKey(); + const valid_signature = secret_key.sign(&signing_root, bls.DST, null); + const valid_signature_bytes = valid_signature.compress(); + + // Generated by adding [r]P to the valid signature, where P is the first decodable compressed + // curve point with an x-coordinate in the range 0...255. + var forged_signature_bytes: BLSSignature = undefined; + _ = try std.fmt.hexToBytes( + &forged_signature_bytes, + "99de7bff0cf5337f1d09fec80f5c9f5af01436b61656692b9520ca79f8211266" ++ + "8bbedce24f69d56c171a39b0425e508017c04962b77d77d80233bd92f45ba9bb" ++ + "93917964f576e8c5a1d2b9487c1e9b0d99021fb8ecfe5fec421d41c19674b609", + ); + const forged_signature = try Signature.uncompress(&forged_signature_bytes); + try std.testing.expect(!forged_signature.subgroupCheck()); + + // The forged signature adds a torsion component, so decoding succeeds but subgroup validation + // must reject it. + const public_keys = [_]PublicKey{public_key}; + + const single_forged = SingleSignatureSet{ + .pubkey = public_key, + .signing_root = signing_root, + .signature = forged_signature_bytes, + }; + try std.testing.expect(!try verifySingleSignatureSet(&single_forged)); + + const aggregate_forged = AggregatedSignatureSet{ + .pubkeys = &public_keys, + .signing_root = signing_root, + .signature = forged_signature_bytes, + }; + try std.testing.expect(!try verifyAggregatedSignatureSet(&aggregate_forged)); + + const single_valid = SingleSignatureSet{ + .pubkey = public_key, + .signing_root = signing_root, + .signature = valid_signature_bytes, + }; + try std.testing.expect(try verifySingleSignatureSet(&single_valid)); + + const aggregate_valid = AggregatedSignatureSet{ + .pubkeys = &public_keys, + .signing_root = signing_root, + .signature = valid_signature_bytes, + }; + try std.testing.expect(try verifyAggregatedSignatureSet(&aggregate_valid)); +} From c329b2a318936b8bd502d49b5af04eb3facd1c45 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Wed, 15 Jul 2026 08:25:38 -0400 Subject: [PATCH 2/4] refactor(state-transition): validate decoded signatures --- src/state_transition/utils/signature_sets.zig | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/state_transition/utils/signature_sets.zig b/src/state_transition/utils/signature_sets.zig index 79867d812..3628d6bd0 100644 --- a/src/state_transition/utils/signature_sets.zig +++ b/src/state_transition/utils/signature_sets.zig @@ -24,10 +24,18 @@ pub const AggregatedSignatureSet = struct { signature: BLSSignature, }; +fn uncompressAndGroupCheck(encoded: *const BLSSignature) !?Signature { + const signature = try Signature.uncompress(encoded); + signature.validate(false) catch |err| { + if (err == bls.BlstError.PointNotInGroup) return null; + return err; + }; + return signature; +} + pub fn verifySingleSignatureSet(set: *const SingleSignatureSet) !bool { - // All signatures are not trusted and must be group checked (p2.subgroup_check) - const signature = try Signature.uncompress(&set.signature); - if (verify(&set.signing_root, &set.pubkey, &signature, .{ .sig_groupcheck = true })) { + const signature = try uncompressAndGroupCheck(&set.signature) orelse return false; + if (verify(&set.signing_root, &set.pubkey, &signature, .{ .sig_groupcheck = false })) { return true; } else |_| { return false; @@ -35,13 +43,12 @@ pub fn verifySingleSignatureSet(set: *const SingleSignatureSet) !bool { } pub fn verifyAggregatedSignatureSet(set: *const AggregatedSignatureSet) !bool { - // All signatures are not trusted and must be group checked (p2.subgroup_check) - const signature = try Signature.uncompress(&set.signature); + const signature = try uncompressAndGroupCheck(&set.signature) orelse return false; return fastAggregateVerify( &set.signing_root, set.pubkeys, &signature, - .{ .sig_groupcheck = true }, + .{ .sig_groupcheck = false }, ); } @@ -81,8 +88,11 @@ test "signature sets reject signatures outside G2 subgroup" { const forged_signature = try Signature.uncompress(&forged_signature_bytes); try std.testing.expect(!forged_signature.subgroupCheck()); - // The forged signature adds a torsion component, so decoding succeeds but subgroup validation - // must reject it. + // Check the validation boundary directly because final verification also rejects this vector + // when subgroup checking is disabled. + try std.testing.expect((try uncompressAndGroupCheck(&forged_signature_bytes)) == null); + try std.testing.expect((try uncompressAndGroupCheck(&valid_signature_bytes)) != null); + const public_keys = [_]PublicKey{public_key}; const single_forged = SingleSignatureSet{ From 16f2de67f70cccc896bed5d3dc12a3b535e2dd06 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Wed, 15 Jul 2026 08:32:38 -0400 Subject: [PATCH 3/4] Revert "refactor(state-transition): validate decoded signatures" This reverts commit c329b2a318936b8bd502d49b5af04eb3facd1c45. --- src/state_transition/utils/signature_sets.zig | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/state_transition/utils/signature_sets.zig b/src/state_transition/utils/signature_sets.zig index 3628d6bd0..79867d812 100644 --- a/src/state_transition/utils/signature_sets.zig +++ b/src/state_transition/utils/signature_sets.zig @@ -24,18 +24,10 @@ pub const AggregatedSignatureSet = struct { signature: BLSSignature, }; -fn uncompressAndGroupCheck(encoded: *const BLSSignature) !?Signature { - const signature = try Signature.uncompress(encoded); - signature.validate(false) catch |err| { - if (err == bls.BlstError.PointNotInGroup) return null; - return err; - }; - return signature; -} - pub fn verifySingleSignatureSet(set: *const SingleSignatureSet) !bool { - const signature = try uncompressAndGroupCheck(&set.signature) orelse return false; - if (verify(&set.signing_root, &set.pubkey, &signature, .{ .sig_groupcheck = false })) { + // All signatures are not trusted and must be group checked (p2.subgroup_check) + const signature = try Signature.uncompress(&set.signature); + if (verify(&set.signing_root, &set.pubkey, &signature, .{ .sig_groupcheck = true })) { return true; } else |_| { return false; @@ -43,12 +35,13 @@ pub fn verifySingleSignatureSet(set: *const SingleSignatureSet) !bool { } pub fn verifyAggregatedSignatureSet(set: *const AggregatedSignatureSet) !bool { - const signature = try uncompressAndGroupCheck(&set.signature) orelse return false; + // All signatures are not trusted and must be group checked (p2.subgroup_check) + const signature = try Signature.uncompress(&set.signature); return fastAggregateVerify( &set.signing_root, set.pubkeys, &signature, - .{ .sig_groupcheck = false }, + .{ .sig_groupcheck = true }, ); } @@ -88,11 +81,8 @@ test "signature sets reject signatures outside G2 subgroup" { const forged_signature = try Signature.uncompress(&forged_signature_bytes); try std.testing.expect(!forged_signature.subgroupCheck()); - // Check the validation boundary directly because final verification also rejects this vector - // when subgroup checking is disabled. - try std.testing.expect((try uncompressAndGroupCheck(&forged_signature_bytes)) == null); - try std.testing.expect((try uncompressAndGroupCheck(&valid_signature_bytes)) != null); - + // The forged signature adds a torsion component, so decoding succeeds but subgroup validation + // must reject it. const public_keys = [_]PublicKey{public_key}; const single_forged = SingleSignatureSet{ From edad9317c0cb18d4d2f0a2b969e02dfdfa049268 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Wed, 15 Jul 2026 08:48:27 -0400 Subject: [PATCH 4/4] refactor(state-transition): simplify signature verification --- src/state_transition/utils/signature_sets.zig | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/state_transition/utils/signature_sets.zig b/src/state_transition/utils/signature_sets.zig index 79867d812..9630f9572 100644 --- a/src/state_transition/utils/signature_sets.zig +++ b/src/state_transition/utils/signature_sets.zig @@ -27,11 +27,8 @@ pub const AggregatedSignatureSet = struct { pub fn verifySingleSignatureSet(set: *const SingleSignatureSet) !bool { // All signatures are not trusted and must be group checked (p2.subgroup_check) const signature = try Signature.uncompress(&set.signature); - if (verify(&set.signing_root, &set.pubkey, &signature, .{ .sig_groupcheck = true })) { - return true; - } else |_| { - return false; - } + verify(&set.signing_root, &set.pubkey, &signature, .{ .sig_groupcheck = true }) catch return false; + return true; } pub fn verifyAggregatedSignatureSet(set: *const AggregatedSignatureSet) !bool {