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:
parent
5294cf0bfa
commit
12373af0b1
8 changed files with 905 additions and 90 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 " +
|
||||
|
|
|
|||
Reference in a new issue