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.
143 lines
4.6 KiB
Go
143 lines
4.6 KiB
Go
package stm
|
||
|
||
import (
|
||
"encoding/binary"
|
||
"fmt"
|
||
"math/big"
|
||
|
||
bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381"
|
||
"golang.org/x/crypto/blake2b"
|
||
)
|
||
|
||
// AggregateBLS performs Mithril's MuSig-style weighted aggregation.
|
||
//
|
||
// For each signer i with signature σ_i and verification key vk_i:
|
||
//
|
||
// h₀ = Blake2b-128(σ_0 ‖ σ_1 ‖ … ‖ σ_{n-1})
|
||
// t_i = Blake2b-128(h₀ ‖ i_be_u64) // 128-bit scalar
|
||
// aggr_σ = Σ t_i · σ_i // in G1
|
||
// aggr_vk = Σ t_i · vk_i // in G2
|
||
//
|
||
// Matches mithril-stm::bls_multi_signature::signature::BlsSignature::aggregate.
|
||
// The per-signer scalar prevents rogue-key attacks where an adversary chooses
|
||
// their vk to cancel the contribution of honest signers.
|
||
//
|
||
// Single-signer short-circuit: when n == 1 the function returns the raw
|
||
// sig/vk unchanged, matching blst's `if vks.len() < 2 { return (vks[0], sigs[0]) }`.
|
||
func AggregateBLS(sigs, vks [][]byte) (aggSig, aggVK []byte, err error) {
|
||
if len(sigs) == 0 || len(sigs) != len(vks) {
|
||
return nil, nil, fmt.Errorf("sig/vk mismatch: %d/%d", len(sigs), len(vks))
|
||
}
|
||
if len(sigs) == 1 {
|
||
// No aggregation needed; blst returns the single contribution as-is.
|
||
return sigs[0], vks[0], nil
|
||
}
|
||
|
||
// Decode all points + subgroup-check up front.
|
||
sigPts := make([]bls12381.G1Affine, len(sigs))
|
||
vkPts := make([]bls12381.G2Affine, len(vks))
|
||
for i := range sigs {
|
||
if len(sigs[i]) != bls12381.SizeOfG1AffineCompressed {
|
||
return nil, nil, fmt.Errorf("sig[%d] size %d", i, len(sigs[i]))
|
||
}
|
||
if _, err := sigPts[i].SetBytes(sigs[i]); err != nil {
|
||
return nil, nil, fmt.Errorf("sig[%d] decode: %w", i, err)
|
||
}
|
||
if !sigPts[i].IsInSubGroup() {
|
||
return nil, nil, fmt.Errorf("sig[%d] not in subgroup", i)
|
||
}
|
||
if len(vks[i]) != bls12381.SizeOfG2AffineCompressed {
|
||
return nil, nil, fmt.Errorf("vk[%d] size %d", i, len(vks[i]))
|
||
}
|
||
if _, err := vkPts[i].SetBytes(vks[i]); err != nil {
|
||
return nil, nil, fmt.Errorf("vk[%d] decode: %w", i, err)
|
||
}
|
||
if !vkPts[i].IsInSubGroup() {
|
||
return nil, nil, fmt.Errorf("vk[%d] not in subgroup", i)
|
||
}
|
||
}
|
||
|
||
// Step 1: h₀ = Blake2b-128(σ_0 ‖ σ_1 ‖ … ‖ σ_{n-1})
|
||
//
|
||
// Note: the Rust impl updates the hasher with `sig.to_bytes()`, which is
|
||
// the compressed 48-byte encoding. We use the input bytes directly —
|
||
// they're the same thing, the signer ships compressed form on the wire.
|
||
base, err := blake2b.New(16, nil) // 128-bit output
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
for _, s := range sigs {
|
||
base.Write(s)
|
||
}
|
||
baseDigest := base.Sum(nil) // 16 bytes
|
||
|
||
// Step 2: scalars t_i = Blake2b-128(h₀ ‖ be_u64(i))
|
||
//
|
||
// Critical detail: the Rust uses `index.to_be_bytes()` where `index` is
|
||
// `usize`. On 64-bit platforms that's 8 BE bytes. blst_lib then treats
|
||
// the resulting 128-bit scalar as little-endian when doing the mult.
|
||
scalars := make([]*big.Int, len(sigs))
|
||
idxBuf := make([]byte, 8)
|
||
for i := range sigs {
|
||
h, err := blake2b.New(16, nil)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
h.Write(baseDigest)
|
||
binary.BigEndian.PutUint64(idxBuf, uint64(i))
|
||
h.Write(idxBuf)
|
||
digest := h.Sum(nil) // 16 bytes LE
|
||
// blst interprets the scalar blob as little-endian; gnark's
|
||
// ScalarMultiplication takes a big.Int (treated as absolute
|
||
// value mod r). Interpret as LE to match blst.
|
||
scalars[i] = new(big.Int).SetBytes(reverseBytes(digest))
|
||
}
|
||
|
||
// Step 3: aggr_sig = Σ t_i · σ_i
|
||
var aggSigJac bls12381.G1Jac
|
||
for i := range sigPts {
|
||
var scaled bls12381.G1Affine
|
||
scaled.ScalarMultiplication(&sigPts[i], scalars[i])
|
||
var jac bls12381.G1Jac
|
||
jac.FromAffine(&scaled)
|
||
aggSigJac.AddAssign(&jac)
|
||
}
|
||
var aggSigAff bls12381.G1Affine
|
||
aggSigAff.FromJacobian(&aggSigJac)
|
||
sigBytes := aggSigAff.Bytes()
|
||
|
||
// aggr_vk = Σ t_i · vk_i
|
||
var aggVKJac bls12381.G2Jac
|
||
for i := range vkPts {
|
||
var scaled bls12381.G2Affine
|
||
scaled.ScalarMultiplication(&vkPts[i], scalars[i])
|
||
var jac bls12381.G2Jac
|
||
jac.FromAffine(&scaled)
|
||
aggVKJac.AddAssign(&jac)
|
||
}
|
||
var aggVKAff bls12381.G2Affine
|
||
aggVKAff.FromJacobian(&aggVKJac)
|
||
vkBytes := aggVKAff.Bytes()
|
||
|
||
return sigBytes[:], vkBytes[:], nil
|
||
}
|
||
|
||
func reverseBytes(b []byte) []byte {
|
||
out := make([]byte, len(b))
|
||
for i, v := range b {
|
||
out[len(b)-1-i] = v
|
||
}
|
||
return out
|
||
}
|
||
|
||
// BlsAggregateVerify aggregates per-signer contributions (with the
|
||
// Mithril MuSig-style scalar weighting) and does one pairing check.
|
||
// Does NOT check lottery wins, Merkle membership, or the k-threshold —
|
||
// those are separate STM-layer checks.
|
||
func BlsAggregateVerify(msg []byte, sigs, vks [][]byte) error {
|
||
aggSig, aggVK, err := AggregateBLS(sigs, vks)
|
||
if err != nil {
|
||
return fmt.Errorf("aggregate: %w", err)
|
||
}
|
||
return BlsVerify(msg, aggSig, aggVK)
|
||
}
|