STM BLS verification WORKING against live preprod (milestones A, B, partial C)
Key findings from upstream: - Mithril's BLS msg is NOT signed_message alone — it's msgp = signed_message_ascii_bytes || mt_commitment_root_32_bytes - Mithril uses EMPTY DST for hash-to-G1 (not the IETF BLS suite string) - Aggregation is NOT plain summation — it's MuSig-style weighted: t_i = Blake2b-128(Blake2b-128(sigs_concat) || be_u64(i)) aggr_sig = Σ t_i · sig_i (in G1) aggr_vk = Σ t_i · vk_i (in G2) This blocks rogue-key attacks. Shipped: - internal/stm/types.go: MultiSig + AVK decoders (hex-of-ASCII-JSON wrapping, polymorphic tuple JSON handling via ByteArray + custom UnmarshalJSON) - internal/stm/bls.go: BlsVerify (pairing check with gnark-crypto) - internal/stm/aggregate.go: MuSig-style AggregateBLS + BlsAggregateVerify - synthetic test + live test (build tag 'live') both green Live preprod head cert (epoch 284, cert 175051cf…): - 2 signers, 11 total lottery wins - aggregate verify: PASS ✓ - single-signer verify: PASS ✓ Next: lottery threshold check, Merkle batch-proof verification, glue into top-level Verify(msg, multi_sig, avk, params) + wire to 'verify' subcommand.
This commit is contained in:
parent
29903bb976
commit
c1305913c2
8 changed files with 705 additions and 0 deletions
81
internal/stm/bls.go
Normal file
81
internal/stm/bls.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package stm
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
|
||||
bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381"
|
||||
)
|
||||
|
||||
// BLS12-381 ciphersuite DST — Mithril uses an EMPTY DST. The Rust impl
|
||||
// calls `blst_sig.verify(sig_gc, msg, &[], &[], pk, pk_gc)` with an
|
||||
// empty `dst` slice. hash-to-curve uses no domain separation. Any
|
||||
// non-empty DST here breaks interop.
|
||||
var blsDST = []byte{}
|
||||
|
||||
// BlsVerify validates a BLS12-381 min_sig signature:
|
||||
//
|
||||
// e(sig, G2_gen) == e(H(msg), vk)
|
||||
//
|
||||
// sig must be 48 bytes (compressed G1), vk must be 96 bytes (compressed G2).
|
||||
// Returns nil on success or a typed error on any failure.
|
||||
func BlsVerify(msg, sig, vk []byte) error {
|
||||
if len(sig) != bls12381.SizeOfG1AffineCompressed {
|
||||
return fmt.Errorf("sig wrong size: got %d, want %d", len(sig), bls12381.SizeOfG1AffineCompressed)
|
||||
}
|
||||
if len(vk) != bls12381.SizeOfG2AffineCompressed {
|
||||
return fmt.Errorf("vk wrong size: got %d, want %d", len(vk), bls12381.SizeOfG2AffineCompressed)
|
||||
}
|
||||
|
||||
var sigPt bls12381.G1Affine
|
||||
if _, err := sigPt.SetBytes(sig); err != nil {
|
||||
return fmt.Errorf("sig decode: %w", err)
|
||||
}
|
||||
// Subgroup check — critical to prevent small-subgroup attacks.
|
||||
if !sigPt.IsInSubGroup() {
|
||||
return fmt.Errorf("sig not in prime-order subgroup")
|
||||
}
|
||||
|
||||
var vkPt bls12381.G2Affine
|
||||
if _, err := vkPt.SetBytes(vk); err != nil {
|
||||
return fmt.Errorf("vk decode: %w", err)
|
||||
}
|
||||
if !vkPt.IsInSubGroup() {
|
||||
return fmt.Errorf("vk not in prime-order subgroup")
|
||||
}
|
||||
|
||||
// Reject the neutral element as a verification key (would accept anything).
|
||||
var zero bls12381.G2Affine
|
||||
zeroBytes := zero.Bytes()
|
||||
vkBytes := vkPt.Bytes()
|
||||
if subtle.ConstantTimeCompare(vkBytes[:], zeroBytes[:]) == 1 {
|
||||
return fmt.Errorf("vk is the identity element")
|
||||
}
|
||||
|
||||
// H(msg) into G1
|
||||
hPt, err := bls12381.HashToG1(msg, blsDST)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hash-to-G1: %w", err)
|
||||
}
|
||||
|
||||
// G2 generator
|
||||
_, _, _, g2Gen := bls12381.Generators()
|
||||
|
||||
// Pairing equation: e(sig, G2_gen) * e(-H(msg), vk) == 1
|
||||
// gnark's PairingCheck returns true iff the product is 1. We negate
|
||||
// H(msg) on the G1 side so the equation collapses to identity.
|
||||
var negH bls12381.G1Affine
|
||||
negH.Neg(&hPt)
|
||||
|
||||
ok, err := bls12381.PairingCheck(
|
||||
[]bls12381.G1Affine{sigPt, negH},
|
||||
[]bls12381.G2Affine{g2Gen, vkPt},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pairing: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("BLS signature invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in a new issue