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).
This commit is contained in:
parent
c1305913c2
commit
5294cf0bfa
7 changed files with 647 additions and 16 deletions
|
|
@ -13,8 +13,10 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
|
|
@ -26,6 +28,7 @@ import (
|
|||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/artifact"
|
||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/mcp"
|
||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/networks"
|
||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/stm"
|
||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/verify"
|
||||
)
|
||||
|
||||
|
|
@ -336,17 +339,89 @@ func runVerifySingle(ctx context.Context, c *aggregator.Client, n networks.Netwo
|
|||
if cert.GenesisSignature != "" {
|
||||
return verifyGenesisCert(n, cert, asJSON)
|
||||
}
|
||||
// STM cert
|
||||
if asJSON {
|
||||
return emitJSON(map[string]any{
|
||||
"cert_hash": cert.Hash,
|
||||
"kind": "stm",
|
||||
"verified": false,
|
||||
"error": "STM BLS verification not yet implemented",
|
||||
})
|
||||
return verifySTMCert(ctx, c, n, hash, cert, asJSON)
|
||||
}
|
||||
|
||||
// verifySTMCert fetches the raw cert JSON (we need fields our minimal
|
||||
// Certificate struct doesn't capture — aggregate_verification_key, metadata
|
||||
// params), decodes, and runs the full STM verification.
|
||||
func verifySTMCert(ctx context.Context, c *aggregator.Client, n networks.Network, hash string, cert *aggregator.Certificate, asJSON bool) int {
|
||||
// Re-fetch as raw JSON to access the AVK + params fields.
|
||||
raw, err := fetchCertRaw(ctx, n.AggregatorURL, hash)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "fetch raw cert:", err)
|
||||
return exitNetwork
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "cert is STM-signed; STM verification not yet implemented")
|
||||
return exitGeneric
|
||||
ms, err := stm.DecodeMultiSig(raw.MultiSignature)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "decode multi_signature:", err)
|
||||
return exitIntegrity
|
||||
}
|
||||
avk, err := stm.DecodeAVK(raw.AggregateVerificationKey)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "decode avk:", err)
|
||||
return exitIntegrity
|
||||
}
|
||||
msg := []byte(cert.SignedMessage)
|
||||
params := stm.Parameters{K: raw.Metadata.Parameters.K, M: raw.Metadata.Parameters.M, PhiF: raw.Metadata.Parameters.PhiF}
|
||||
verr := stm.Verify(msg, ms, avk, params)
|
||||
if asJSON {
|
||||
out := map[string]any{
|
||||
"cert_hash": cert.Hash,
|
||||
"kind": "stm",
|
||||
"epoch": cert.Epoch,
|
||||
"signed_message": cert.SignedMessage,
|
||||
"signers": len(ms.Signatures),
|
||||
"total_wins": ms.TotalWins(),
|
||||
"distinct_wins": len(ms.DistinctWins()),
|
||||
"params": params,
|
||||
"verified": verr == nil,
|
||||
}
|
||||
if verr != nil {
|
||||
out["error"] = verr.Error()
|
||||
}
|
||||
code := emitJSON(out)
|
||||
if verr != nil {
|
||||
return exitBadSig
|
||||
}
|
||||
return code
|
||||
}
|
||||
if verr != nil {
|
||||
fmt.Fprintln(os.Stderr, "STM verify:", verr)
|
||||
return exitBadSig
|
||||
}
|
||||
fmt.Printf("STM cert %s epoch=%d signers=%d wins=%d/%d BLS+lottery+merkle ✓\n",
|
||||
cert.Hash, cert.Epoch, len(ms.Signatures), ms.TotalWins(), params.M)
|
||||
return exitOK
|
||||
}
|
||||
|
||||
type rawCert struct {
|
||||
MultiSignature json.RawMessage `json:"multi_signature"`
|
||||
AggregateVerificationKey json.RawMessage `json:"aggregate_verification_key"`
|
||||
Metadata struct {
|
||||
Parameters struct {
|
||||
K uint64 `json:"k"`
|
||||
M uint64 `json:"m"`
|
||||
PhiF float64 `json:"phi_f"`
|
||||
} `json:"parameters"`
|
||||
} `json:"metadata"`
|
||||
}
|
||||
|
||||
func fetchCertRaw(ctx context.Context, aggregatorURL, hash string) (*rawCert, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, aggregatorURL+"/certificate/"+hash, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var r rawCert
|
||||
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func verifyGenesisCert(n networks.Network, cert *aggregator.Certificate, asJSON bool) int {
|
||||
|
|
@ -477,7 +552,7 @@ func cmdMCP(ctx context.Context, args []string) int {
|
|||
Version: version,
|
||||
})
|
||||
registerMCPTools(s)
|
||||
fmt.Fprintf(os.Stderr, "[mcp] mithril-go %s MCP server ready on stdio (%d tools)\n", version, 6)
|
||||
fmt.Fprintf(os.Stderr, "[mcp] mithril-go %s MCP server ready on stdio (%d tools)\n", version, 7)
|
||||
if err := s.Run(ctx); err != nil {
|
||||
if err == context.Canceled || err == context.DeadlineExceeded {
|
||||
return exitCanceled
|
||||
|
|
|
|||
|
|
@ -7,9 +7,17 @@ import (
|
|||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/aggregator"
|
||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/mcp"
|
||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/networks"
|
||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/stm"
|
||||
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/verify"
|
||||
)
|
||||
|
||||
func errString(e error) string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return e.Error()
|
||||
}
|
||||
|
||||
// networkArgOrDefault pulls a "network" string from the args map, defaulting
|
||||
// to "preprod" if absent. Returns the resolved network + client.
|
||||
func networkArgOrDefault(args map[string]any) (networks.Network, *aggregator.Client, error) {
|
||||
|
|
@ -187,6 +195,101 @@ func registerMCPTools(s *mcp.Server) {
|
|||
},
|
||||
})
|
||||
|
||||
s.RegisterTool(mcp.Tool{
|
||||
Name: "mithril_verify_certificate",
|
||||
Description: "Verify a Mithril certificate. Genesis certs are checked with Ed25519; STM certs with full BLS12-381 aggregate + Merkle membership + lottery-win checks. Returns verified: true|false with context.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"network": networkEnum,
|
||||
"hash": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Certificate hash, 'head' for the latest snapshot's cert, or 'genesis' to walk to the chain root",
|
||||
"default": "head",
|
||||
},
|
||||
},
|
||||
},
|
||||
Handler: func(ctx context.Context, args map[string]any) (any, error) {
|
||||
n, c, err := networkArgOrDefault(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hash := mcp.ArgString(args, "hash")
|
||||
if hash == "" {
|
||||
hash = "head"
|
||||
}
|
||||
var cert *aggregator.Certificate
|
||||
switch hash {
|
||||
case "head":
|
||||
snap, err := resolveSnapshot(ctx, c, "latest")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err = c.GetCertificate(ctx, snap.CertificateHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "genesis":
|
||||
snap, err := resolveSnapshot(ctx, c, "latest")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chain, err := c.CertChain(ctx, snap.CertificateHash, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert = chain[len(chain)-1]
|
||||
default:
|
||||
cert, err = c.GetCertificate(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if cert.GenesisSignature != "" {
|
||||
vk, err := verify.DecodeGenesisVerifyKey(n.GenesisVerifyKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verr := verify.GenesisFromJSON(vk, cert.SignedMessage, cert.GenesisSignature, cert.ProtocolMessage)
|
||||
return map[string]any{
|
||||
"kind": "genesis",
|
||||
"cert_hash": cert.Hash,
|
||||
"epoch": cert.Epoch,
|
||||
"verified": verr == nil,
|
||||
"error": errString(verr),
|
||||
}, nil
|
||||
}
|
||||
// STM path
|
||||
raw, err := fetchCertRaw(ctx, n.AggregatorURL, cert.Hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ms, err := stm.DecodeMultiSig(raw.MultiSignature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
avk, err := stm.DecodeAVK(raw.AggregateVerificationKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params := stm.Parameters{K: raw.Metadata.Parameters.K, M: raw.Metadata.Parameters.M, PhiF: raw.Metadata.Parameters.PhiF}
|
||||
verr := stm.Verify([]byte(cert.SignedMessage), ms, avk, params)
|
||||
return map[string]any{
|
||||
"kind": "stm",
|
||||
"cert_hash": cert.Hash,
|
||||
"epoch": cert.Epoch,
|
||||
"signers": len(ms.Signatures),
|
||||
"total_wins": ms.TotalWins(),
|
||||
"distinct_wins": len(ms.DistinctWins()),
|
||||
"params_k": params.K,
|
||||
"params_m": params.M,
|
||||
"params_phi_f": params.PhiF,
|
||||
"verified": verr == nil,
|
||||
"error": errString(verr),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
s.RegisterTool(mcp.Tool{
|
||||
Name: "mithril_verify_genesis",
|
||||
Description: "Walk the certificate chain back to the genesis cert and verify its Ed25519 signature against the network's " +
|
||||
|
|
|
|||
Reference in a new issue