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).
97 lines
3.4 KiB
Go
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
|
|
}
|