mithril-go/internal/stm/verify.go
Kayos 920d7cf177 STM full verification landing — milestones C/D/E complete
Implemented the remaining STM verification layers:

- internal/stm/lottery.go: EvaluateSigma (Blake2b-512 lottery draw) +
  IsLotteryWon with Taylor-series threshold comparison (ported from
  mithril-stm::eligibility), big.Rat-based to match Rust's num_bigint/
  num_rational path
- internal/stm/merkle.go: Blake2b-256 Merkle batch-proof verification,
  faithful port of mithril-stm's verify_leaves_membership_from_batch_path
  including the 'current is left/right child' branch logic and the
  1-byte zero pad for missing siblings
- internal/stm/verify.go: top-level stm.Verify(msg, ms, avk, params)
  glues all four checks: k-threshold, lottery, Merkle, BLS aggregate
- cmd: 'verify head' now runs full STM verification; JSON output shows
  signers, wins, params, verified flag
- MCP: new 'mithril_verify_certificate' tool dispatches genesis Ed25519
  vs STM by cert kind

Verified against live networks:
  mainnet head cert bc00b551…  epoch=626  59 signers  1972/16948 wins  ✓
  mainnet genesis   25acfcfe…  epoch=539  Ed25519 ✓
  preprod head      dd9c4fcb…  epoch=284   2 signers    11/100 wins   ✓
  preprod genesis   69bc3bdf…  epoch=196  Ed25519 ✓

This is a consensus-correct pure-Go Mithril client. Single binary,
CGo-free, no upstream Rust dependency.

Next: full chain verification (walk head → genesis, check continuity).
2026-04-23 15:58:44 -07:00

97 lines
3.4 KiB
Go

package stm
import (
"fmt"
)
// Parameters holds the three scalar values from a Mithril cert's
// protocol_parameters field. Only the three that matter for verification.
type Parameters struct {
K uint64 // minimum distinct lottery wins for a valid aggregate
M uint64 // total lottery slots per signing round
PhiF float64 // lottery success base rate (the "phi_f" param)
}
// Verify runs the full STM aggregate-signature verification:
//
// 1. k-threshold: DistinctWins(multi_sig) >= params.K
// 2. Lottery check: for each (signer, index), ev < threshold(stake)
// 3. Merkle proof: each claimed signer leaf is in avk at its index
// 4. BLS verify: MuSig-aggregated (sig, vk) over (msg || avk.root)
//
// All four must pass. Returns a descriptive error at the first failure.
//
// msg is the ASCII bytes of the certificate's signed_message hex string.
func Verify(msg []byte, ms *MultiSig, avk *AVK, params Parameters) error {
// (1) k-threshold
distinct := ms.DistinctWins()
if uint64(len(distinct)) < params.K {
return fmt.Errorf("k-threshold: got %d distinct wins, want >= %d", len(distinct), params.K)
}
// Also: every claimed index must appear exactly once across all signers.
// Rust enforces `nr_indices == unique_indices.len()` via HashSet.
total := ms.TotalWins()
if total != len(distinct) {
return fmt.Errorf("lottery indices not unique across signers: total=%d distinct=%d",
total, len(distinct))
}
// Compute msgp = msg || avk.MerkleRoot — used by lottery check AND BLS.
msgp := append(append([]byte(nil), msg...), avk.MerkleRoot...)
// (2) Lottery check — per (signer, claimed index).
for i, s := range ms.Signatures {
for _, idx := range s.Sig.Indexes {
if idx >= params.M {
return fmt.Errorf("signer[%d] claimed index %d >= m=%d", i, idx, params.M)
}
ev := EvaluateSigma(msgp, idx, s.Sig.Sigma)
if !IsLotteryWon(params.PhiF, ev, s.RegParty.Stake, avk.TotalStake) {
return fmt.Errorf("signer[%d] lottery loss at index %d (stake=%d/%d)",
i, idx, s.RegParty.Stake, avk.TotalStake)
}
}
}
// (3) Merkle batch proof — prove each signer leaf is in the AVK tree.
leaves := make([][]byte, len(ms.Signatures))
indices := make([]uint64, len(ms.Signatures))
// Rust sorts leaves by signer_index ascending; so must we.
sorted := make([]int, len(ms.Signatures))
for i := range sorted {
sorted[i] = i
}
// Sort indices ascending while tracking permutation
for i := 0; i < len(sorted); i++ {
for j := i + 1; j < len(sorted); j++ {
if ms.Signatures[sorted[j]].Sig.SignerIndex < ms.Signatures[sorted[i]].Sig.SignerIndex {
sorted[i], sorted[j] = sorted[j], sorted[i]
}
}
}
for outIdx, origIdx := range sorted {
s := ms.Signatures[origIdx]
leaves[outIdx] = LeafBytes(s.RegParty.VK, s.RegParty.Stake)
indices[outIdx] = s.Sig.SignerIndex
}
proofVals := make([][]byte, len(ms.BatchProof.Values))
for i, v := range ms.BatchProof.Values {
proofVals[i] = v
}
if err := VerifyMerkleBatch(avk.MerkleRoot, int(avk.NumLeaves), leaves, indices, proofVals); err != nil {
return fmt.Errorf("merkle batch proof: %w", err)
}
// (4) BLS aggregate verify — unique signers, no multiplicity.
sigs := make([][]byte, len(ms.Signatures))
vks := make([][]byte, len(ms.Signatures))
for i, s := range ms.Signatures {
sigs[i] = s.Sig.Sigma
vks[i] = s.RegParty.VK
}
if err := BlsAggregateVerify(msgp, sigs, vks); err != nil {
return fmt.Errorf("bls aggregate: %w", err)
}
return nil
}