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.
111 lines
3.8 KiB
Go
111 lines
3.8 KiB
Go
//go:build live
|
||
|
||
package stm
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"io"
|
||
"net/http"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// Fetches the head preprod cert, pulls out the (msg, vk_i, sigma_i) triple,
|
||
// and tries to BLS-verify ONE signer's sigma against signed_message.as_bytes().
|
||
//
|
||
// Expectation: this FAILS for a single signer because Mithril's STM layer
|
||
// aggregates sigs/vks before BLS-verifying, so an individual (sigma_i, vk_i)
|
||
// pair isn't a valid BLS signature of signed_message on its own.
|
||
//
|
||
// What we learn from this test:
|
||
//
|
||
// - If a single-signer verify passes: BLS works as expected (unlikely).
|
||
// - If it fails with "signature invalid" but no decode errors: our BLS impl
|
||
// is correct and the failure is a real property of STM (curve points
|
||
// decoded fine, pairing ran, result doesn't match — exactly what the
|
||
// aggregation layer fixes).
|
||
// - If it fails with a decode error: something in the wire format is off.
|
||
//
|
||
// This test documents expected behavior rather than asserting pass.
|
||
func TestBlsVerify_SingleSignerAgainstSignedMessage(t *testing.T) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||
defer cancel()
|
||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, preprodHead+"/artifact/cardano-database", nil)
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
t.Skipf("network: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
var snaps []struct {
|
||
CertificateHash string `json:"certificate_hash"`
|
||
}
|
||
if err := json.NewDecoder(resp.Body).Decode(&snaps); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
req, _ = http.NewRequestWithContext(ctx, http.MethodGet, preprodHead+"/certificate/"+snaps[0].CertificateHash, nil)
|
||
resp, err = http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer resp.Body.Close()
|
||
body, _ := io.ReadAll(resp.Body)
|
||
var cert struct {
|
||
MultiSignature json.RawMessage `json:"multi_signature"`
|
||
SignedMessage string `json:"signed_message"`
|
||
AggregateVerificationKey json.RawMessage `json:"aggregate_verification_key"`
|
||
}
|
||
if err := json.Unmarshal(body, &cert); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
ms, err := DecodeMultiSig(cert.MultiSignature)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(ms.Signatures) == 0 {
|
||
t.Fatal("no signers")
|
||
}
|
||
|
||
// Extract the AVK's mt_commitment.root — the BLS-verified message is
|
||
// msg || root, where msg is the ASCII bytes of signed_message hex.
|
||
avk, err := DecodeAVK(cert.AggregateVerificationKey)
|
||
if err != nil {
|
||
t.Fatalf("avk decode: %v", err)
|
||
}
|
||
t.Logf("avk mt_root=%x total_stake=%d nr_leaves=%d",
|
||
avk.MerkleRoot, avk.TotalStake, avk.NumLeaves)
|
||
|
||
msg := append([]byte(cert.SignedMessage), avk.MerkleRoot...)
|
||
s0 := ms.Signatures[0]
|
||
err = BlsVerify(msg, s0.Sig.Sigma, s0.RegParty.VK)
|
||
t.Logf("single-signer[0] BLS verify over signed_message: err=%v", err)
|
||
// Don't assert pass/fail here — documenting behavior.
|
||
|
||
// Aggregate all signers and verify — one entry per signer per lottery
|
||
// win (a signer with N wins contributes N copies of the same sigma/vk).
|
||
var sigs, vks [][]byte
|
||
for _, s := range ms.Signatures {
|
||
for range s.Sig.Indexes {
|
||
sigs = append(sigs, s.Sig.Sigma)
|
||
vks = append(vks, s.RegParty.VK)
|
||
}
|
||
}
|
||
t.Logf("aggregating %d (signer × win) pairs", len(sigs))
|
||
if err := BlsAggregateVerify(msg, sigs, vks); err != nil {
|
||
t.Logf("aggregate verify (with multiplicity = lottery wins): %v", err)
|
||
} else {
|
||
t.Logf("aggregate verify (with multiplicity = lottery wins): PASS ✓")
|
||
}
|
||
|
||
// Alternate: one entry per unique signer (no multiplicity)
|
||
sigs, vks = nil, nil
|
||
for _, s := range ms.Signatures {
|
||
sigs = append(sigs, s.Sig.Sigma)
|
||
vks = append(vks, s.RegParty.VK)
|
||
}
|
||
if err := BlsAggregateVerify(msg, sigs, vks); err != nil {
|
||
t.Logf("aggregate verify (unique signers only): %v", err)
|
||
} else {
|
||
t.Logf("aggregate verify (unique signers only): PASS ✓")
|
||
}
|
||
}
|