wrap: chain verify + manifest verify + LICENSE + final docs

- internal/chain: end-to-end chain verification. Walks head → genesis,
  verifies every cert (Ed25519 or STM as appropriate), and checks
  continuity at every boundary:
    epoch:    same or +1 from previous
    hash:     current.previous_hash == previous.hash
    AVK:      same epoch  → equal aggregate_verification_key
              new epoch   → matches previous.protocol_message.next_aggregate_verification_key
- cmd: 'verify chain' subcommand + 'verify manifest <dir>' for SHA-checking
  downloaded immutable files
- internal/manifest: per-file SHA-256 verification against the digests.json
  shipped in the snapshot's digests archive
- MCP: 8th tool 'mithril_verify_chain' for agent-driven full-chain verify
- README: complete rewrite — status table, architecture, gotchas, MCP
  tool surface, exit code contract, build instructions
- LICENSE: Apache-2.0 (matches upstream Mithril)

Verified end to end against live networks:
  preprod  chain  90 certs (89 STM + 1 genesis)  1124 wins   ✓
  mainnet  chain  89 certs (88 STM + 1 genesis)  210921 wins ✓

That's the wrap. Pure-Go consensus-correct Mithril client, single 10 MB
static binary, MCP-native, no CGo, no upstream Rust runtime.
This commit is contained in:
Sulkta 2026-04-23 16:15:47 -07:00
parent 5294cf0bfa
commit 12373af0b1
8 changed files with 905 additions and 90 deletions

View file

@ -26,6 +26,8 @@ import (
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/aggregator"
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/artifact"
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/chain"
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/manifest"
"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"
@ -93,7 +95,7 @@ Commands:
show Show detail for one snapshot (hash or "latest")
cert Show a certificate or walk the chain back to genesis
download Download a snapshot to a target directory
verify Verify a certificate (genesis Ed25519 working; STM BLS pending)
verify Verify certificates (genesis / head / chain / <hash>)
mcp Run as a Model Context Protocol server over stdio
info Show network + aggregator info
version Print version
@ -291,12 +293,95 @@ func cmdVerify(ctx context.Context, args []string) int {
return runVerifyGenesis(ctx, c, n, *asJSON)
case "head":
return runVerifyHead(ctx, c, n, *asJSON)
case "chain":
return runVerifyChain(ctx, n, *asJSON)
case "manifest":
return runVerifyManifest(rest[1:], *asJSON)
default:
// Treat as a literal cert hash: fetch + verify
return runVerifySingle(ctx, c, n, mode, *asJSON)
}
}
func runVerifyManifest(args []string, asJSON bool) int {
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "verify manifest: needs path to download dir (with digests/ + db/)")
return exitUsage
}
dir := args[0]
digestsPath, err := manifest.LocateDigests(filepath.Join(dir, "digests"))
if err != nil {
fmt.Fprintln(os.Stderr, "locate digests.json:", err)
return exitGeneric
}
entries, err := manifest.Load(digestsPath)
if err != nil {
fmt.Fprintln(os.Stderr, "load manifest:", err)
return exitIntegrity
}
res, err := manifest.Verify(entries, filepath.Join(dir, "db"))
if err != nil {
fmt.Fprintln(os.Stderr, "verify manifest:", err)
return exitGeneric
}
if asJSON {
code := emitJSON(res)
if !res.OK {
return exitIntegrity
}
return code
}
if !res.OK {
fmt.Fprintf(os.Stderr, "manifest verify FAILED: verified=%d/%d missing=%d mismatched=%d unverifiable=%d\n",
res.Verified, res.TotalEntries, len(res.Missing), len(res.Mismatched), len(res.Unverifiable))
return exitIntegrity
}
fmt.Printf("manifest verify ✓ %d/%d files match (extra: %d)\n",
res.Verified, res.TotalEntries, len(res.Extra))
return exitOK
}
func runVerifyChain(ctx context.Context, n networks.Network, asJSON bool) int {
c := aggregator.New(n.AggregatorURL)
snap, err := resolveSnapshot(ctx, c, "latest")
if err != nil {
fmt.Fprintln(os.Stderr, "resolve:", err)
return exitNetwork
}
res, err := chain.Verify(ctx, nil, n, snap.CertificateHash, 2048)
if err != nil {
fmt.Fprintln(os.Stderr, "chain verify:", err)
return exitNetwork
}
if asJSON {
code := emitJSON(res)
if !res.Verified {
return exitBadSig
}
return code
}
if !res.Verified {
fmt.Fprintf(os.Stderr, "chain verify FAILED at step %d (%s): %s\n",
res.FailureIndex, res.FailureKind, res.Error)
return exitBadSig
}
fmt.Printf("chain verify ✓ network=%s length=%d head=%s genesis=%s\n",
res.Network, res.Length, res.HeadHash, res.GenesisHash)
// Quick summary of STM/genesis breakdown
stmCount, genCount, totalWins := 0, 0, 0
for _, step := range res.Steps {
if step.Kind == "stm" {
stmCount++
totalWins += step.TotalWins
} else {
genCount++
}
}
fmt.Printf(" %d STM certs + %d genesis cert | %d total lottery wins across chain\n",
stmCount, genCount, totalWins)
return exitOK
}
func runVerifyGenesis(ctx context.Context, c *aggregator.Client, n networks.Network, asJSON bool) int {
// Find the head snapshot's cert, walk to genesis, verify Ed25519 on the genesis cert.
snap, err := resolveSnapshot(ctx, c, "latest")
@ -552,7 +637,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, 7)
fmt.Fprintf(os.Stderr, "[mcp] mithril-go %s MCP server ready on stdio (%d tools)\n", version, 8)
if err := s.Run(ctx); err != nil {
if err == context.Canceled || err == context.DeadlineExceeded {
return exitCanceled

View file

@ -5,6 +5,7 @@ import (
"fmt"
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/aggregator"
"git.sulkta.com/Sulkta-Coop/mithril-go/internal/chain"
"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"
@ -290,6 +291,39 @@ func registerMCPTools(s *mcp.Server) {
},
})
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 " +