mithril-go/internal/stm/bls_live_test.go
Kayos 32f0057700 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.
2026-04-23 15:53:00 -07:00

111 lines
3.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//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 ✓")
}
}