Acceptance test report flagged three cosmetic JSON-schema gaps: 1. 'list -json' had no top-level 'count' — caller had to use .snapshots|length. Added 'count' alongside 'snapshots'. 2. 'verify -json chain' top-level 'kind' was reported null — actually a missing field rather than null. Chain results have per-step kind in .steps[]; chain-level kind would be misleading. Documented intent in README rather than adding the field. 3. MCP 'mithril_verify_certificate' returned 'cert_hash' but agents often look for 'hash'. Added 'hash' alias alongside 'cert_hash' in both genesis and STM result paths so either lookup works. End-to-end loop test on mainnet+preprod: full PASS (89-cert mainnet chain + 90-cert preprod chain both verified, MCP all 8 tools work, exit codes correct, manifest detection clean). v1-tag-able now.
376 lines
11 KiB
Go
376 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/aggregator"
|
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/chain"
|
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/mcp"
|
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/networks"
|
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/stm"
|
|
"git.sulkta.coop/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) {
|
|
name := mcp.ArgString(args, "network")
|
|
if name == "" {
|
|
name = "preprod"
|
|
}
|
|
n, ok := networks.ByName(name)
|
|
if !ok {
|
|
return networks.Network{}, nil, fmt.Errorf("unknown network: %s", name)
|
|
}
|
|
return n, aggregator.New(n.AggregatorURL), nil
|
|
}
|
|
|
|
// networkEnum is a shared JSON Schema fragment used across tools.
|
|
var networkEnum = map[string]any{
|
|
"type": "string",
|
|
"enum": []string{"mainnet", "preprod", "preview"},
|
|
"default": "preprod",
|
|
"description": "Cardano network",
|
|
}
|
|
|
|
func registerMCPTools(s *mcp.Server) {
|
|
s.RegisterTool(mcp.Tool{
|
|
Name: "mithril_info",
|
|
Description: "Get aggregator URL and genesis verification key for a Cardano network.",
|
|
InputSchema: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{"network": networkEnum},
|
|
},
|
|
Handler: func(ctx context.Context, args map[string]any) (any, error) {
|
|
n, _, err := networkArgOrDefault(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{
|
|
"network": n.Name,
|
|
"aggregator": n.AggregatorURL,
|
|
"genesis_verify_key": n.GenesisVerifyKey,
|
|
}, nil
|
|
},
|
|
})
|
|
|
|
s.RegisterTool(mcp.Tool{
|
|
Name: "mithril_list_snapshots",
|
|
Description: "List available cardano-database snapshots on a Mithril aggregator, newest first.",
|
|
InputSchema: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{"network": networkEnum},
|
|
},
|
|
Handler: func(ctx context.Context, args map[string]any) (any, error) {
|
|
_, c, err := networkArgOrDefault(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snaps, err := c.ListCardanoDBSnapshots(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"count": len(snaps), "snapshots": snaps}, nil
|
|
},
|
|
})
|
|
|
|
s.RegisterTool(mcp.Tool{
|
|
Name: "mithril_show_snapshot",
|
|
Description: "Fetch detail for a specific cardano-database snapshot. Pass 'latest' for the newest.",
|
|
InputSchema: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"network": networkEnum,
|
|
"hash": map[string]any{
|
|
"type": "string",
|
|
"description": "Snapshot hash or the literal 'latest'",
|
|
"default": "latest",
|
|
},
|
|
},
|
|
},
|
|
Handler: func(ctx context.Context, args map[string]any) (any, error) {
|
|
_, c, err := networkArgOrDefault(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hash := mcp.ArgString(args, "hash")
|
|
if hash == "" {
|
|
hash = "latest"
|
|
}
|
|
return resolveSnapshot(ctx, c, hash)
|
|
},
|
|
})
|
|
|
|
s.RegisterTool(mcp.Tool{
|
|
Name: "mithril_get_certificate",
|
|
Description: "Fetch a Mithril certificate by hash. Pass 'head' to resolve via the latest snapshot's certificate_hash.",
|
|
InputSchema: map[string]any{
|
|
"type": "object",
|
|
"required": []string{"hash"},
|
|
"properties": map[string]any{
|
|
"network": networkEnum,
|
|
"hash": map[string]any{
|
|
"type": "string",
|
|
"description": "Certificate hash or the literal 'head'",
|
|
},
|
|
},
|
|
},
|
|
Handler: func(ctx context.Context, args map[string]any) (any, error) {
|
|
_, c, err := networkArgOrDefault(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hash := mcp.ArgString(args, "hash")
|
|
if hash == "" {
|
|
return nil, fmt.Errorf("hash is required")
|
|
}
|
|
if hash == "head" {
|
|
snap, err := resolveSnapshot(ctx, c, "latest")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hash = snap.CertificateHash
|
|
}
|
|
return c.GetCertificate(ctx, hash)
|
|
},
|
|
})
|
|
|
|
s.RegisterTool(mcp.Tool{
|
|
Name: "mithril_walk_cert_chain",
|
|
Description: "Walk the certificate chain from head back to the genesis cert. Returns each cert's hash, epoch, and genesis-or-not role.",
|
|
InputSchema: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"network": networkEnum,
|
|
"head_hash": map[string]any{
|
|
"type": "string",
|
|
"description": "Starting certificate hash; defaults to the latest snapshot's cert",
|
|
},
|
|
"max_depth": map[string]any{
|
|
"type": "integer",
|
|
"default": 1024,
|
|
},
|
|
},
|
|
},
|
|
Handler: func(ctx context.Context, args map[string]any) (any, error) {
|
|
_, c, err := networkArgOrDefault(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
head := mcp.ArgString(args, "head_hash")
|
|
if head == "" {
|
|
snap, err := resolveSnapshot(ctx, c, "latest")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
head = snap.CertificateHash
|
|
}
|
|
maxDepth := 1024
|
|
if v, ok := args["max_depth"]; ok {
|
|
if f, ok := v.(float64); ok {
|
|
maxDepth = int(f)
|
|
}
|
|
}
|
|
chain, err := c.CertChain(ctx, head, maxDepth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
brief := make([]map[string]any, len(chain))
|
|
for i, ct := range chain {
|
|
brief[i] = map[string]any{
|
|
"hash": ct.Hash,
|
|
"previous_hash": ct.PreviousHash,
|
|
"epoch": ct.Epoch,
|
|
"is_genesis": ct.GenesisSignature != "",
|
|
}
|
|
}
|
|
return map[string]any{"length": len(brief), "certs": brief}, nil
|
|
},
|
|
})
|
|
|
|
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",
|
|
"hash": cert.Hash,
|
|
"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",
|
|
"hash": cert.Hash,
|
|
"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_chain",
|
|
Description: "End-to-end verification: walk from the latest snapshot's head cert back to genesis, verify every cert (Ed25519 or STM BLS as appropriate), and check epoch + hash + AVK continuity at every boundary. Returns a full step-by-step report.",
|
|
InputSchema: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"network": networkEnum,
|
|
"max_depth": map[string]any{
|
|
"type": "integer",
|
|
"default": 2048,
|
|
"description": "Safety cap on the chain walk length",
|
|
},
|
|
},
|
|
},
|
|
Handler: func(ctx context.Context, args map[string]any) (any, error) {
|
|
n, c, err := networkArgOrDefault(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
maxDepth := 2048
|
|
if v, ok := args["max_depth"]; ok {
|
|
if f, ok := v.(float64); ok {
|
|
maxDepth = int(f)
|
|
}
|
|
}
|
|
snap, err := resolveSnapshot(ctx, c, "latest")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return chain.Verify(ctx, nil, n, snap.CertificateHash, maxDepth)
|
|
},
|
|
})
|
|
|
|
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 " +
|
|
"baked-in genesis verification key. Returns verified: true|false with context.",
|
|
InputSchema: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{"network": networkEnum},
|
|
},
|
|
Handler: func(ctx context.Context, args map[string]any) (any, error) {
|
|
n, c, err := networkArgOrDefault(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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
|
|
}
|
|
if len(chain) == 0 {
|
|
return nil, fmt.Errorf("empty chain")
|
|
}
|
|
gen := chain[len(chain)-1]
|
|
if gen.GenesisSignature == "" {
|
|
return nil, fmt.Errorf("chain tail is not a genesis certificate")
|
|
}
|
|
vk, err := verify.DecodeGenesisVerifyKey(n.GenesisVerifyKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
verr := verify.GenesisFromJSON(vk, gen.SignedMessage, gen.GenesisSignature, gen.ProtocolMessage)
|
|
out := map[string]any{
|
|
"network": n.Name,
|
|
"cert_hash": gen.Hash,
|
|
"epoch": gen.Epoch,
|
|
"signed_message": gen.SignedMessage,
|
|
"chain_length": len(chain),
|
|
"verified": verr == nil,
|
|
}
|
|
if verr != nil {
|
|
out["error"] = verr.Error()
|
|
}
|
|
return out, nil
|
|
},
|
|
})
|
|
}
|