Independent code audit (in-repo, fresh-eyes pass) flagged 0 critical, 4
high, 8 medium, 7 low. This commit addresses all 4 highs + the JSON
error-path inconsistency + the vestigial verify.STM stub.
HIGH fixes:
- cmd/mithril-go/main.go fetchCertRaw: missing status check let HTML 4xx/5xx
bodies fall through to confusing JSON-decode errors. Added explicit
StatusCode>=400 check + 16 MiB response body cap + Accept header.
- internal/artifact/download.go: SHA mismatch left .part on disk, causing
every retry to resume the corrupted bytes and fail SHA forever. Now
removes .part on hash mismatch so the next attempt starts clean.
- internal/stm/types.go DecodeAVK: rejects total_stake=0 and nr_leaves=0
at decode-time. internal/stm/lottery.go adds defensive guard for
stake==0 || totalStake==0 to prevent big.Rat.SetFrac panic (DoS vector
for the MCP server when fed crafted AVK).
- internal/stm/merkle.go: now requires (a) every proof value is exactly
32 bytes, (b) indices are STRICTLY ascending (no duplicates),
(c) every index is < nr_leaves, (d) all proof values are consumed by
the algorithm. Prevents parser-differential bugs vs upstream Rust.
JSON error-path wiring:
- cmd/mithril-go/json.go: replaced unused emitJSONErr with failure() helper
that routes errors to stdout-as-JSON when -json is set, else stderr-as-text.
Error envelope shape: {error: {code, kind, message}} where 'kind' is a
stable short string (network/integrity/verify/usage/internal) for agents
to branch on without parsing human text.
- All -json-supporting commands (info, list, show, cert, verify+subcommands)
now use failure() in error paths instead of bare fmt.Fprintln(stderr).
- Verified: 'verify -json deadbeef' on a bogus hash now emits valid JSON
to stdout with exit=3, instead of empty stdout + text on stderr.
Vestigial code:
- internal/verify/verify.go: removed STM() stub + ErrSTMNotImplemented.
Real STM verification has lived in internal/stm/verify.go since the
crypto sprint; the stub was dead code from milestone-by-milestone work.
Verification (still all green):
- preprod chain: 90 certs, 1124 wins ✓
- mainnet head: 59 signers, 1972 wins ✓
- preprod head: 2 signers, 11 wins ✓
- preprod genesis: Ed25519 ✓
- JSON error envelope on bogus hash: well-formed JSON, exit=3
- internal/stm unit test: PASS
Audit findings deferred to v1.0.2+: bubble-sort in stm.Verify (medium,
perf only at scale); int-vs-uint64 truncation guards on 32-bit targets
(medium, won't bite on 64-bit); tar mode-bit masking (medium, low impact
since archives are from trusted aggregator); no User-Agent header on
aggregator requests (low, op nicety); MCP scanner silent stop on >10 MiB
line (low, defensive).
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.
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).
- internal/mcp: minimal JSON-RPC 2.0 over newline-delimited JSON, stdio
transport. Handles initialize / tools/list / tools/call / ping /
notifications. No deps — stdlib only.
- cmd: 'mithril-go mcp' subcommand brings up the server. Tools:
mithril_info
mithril_list_snapshots
mithril_show_snapshot
mithril_get_certificate
mithril_walk_cert_chain
mithril_verify_genesis
- verified end-to-end against mainnet via tools/call: verify_genesis walks
the 89-cert chain and returns verified=true
Any MCP client (Claude Code, Cursor, Zed, etc.) can now point at this
binary and get a discoverable, typed tool surface.
Read the upstream Rust (mithril-stm) end to end for STM-side types,
wrote up what we need to port, library choices, gotchas to watch,
and milestone breakdown (A: decoder, B: BLS single verify, C: Merkle,
D: lottery threshold, E: full aggregate verify, F: chain verify).
Ready for a focused crypto sprint — genesis Ed25519 + M2M plumbing
are done and shipping.
- -json on info, list, show, cert (+ -chain): emit structured JSON ready
for jq / agent consumption
- exit codes are now stable + documented: 0/1/2/3/4/5/130 with distinct
meanings for network vs integrity vs signature failures
- help text enumerates the contract
- readme: machine-usage section explains both
MCP stdio server (for Claude Code / Cursor etc.) planned, not wired yet
- status table: what's working vs what's next
- sprint plan for genesis Ed25519 (wiring) and STM BLS (the real work)
- concrete pointers: upstream mithril-common for signed_message derivation,
blst Go bindings for BLS12-381
- aggregator.CertChain: walks previous_hash from head until genesis_signature
- cmd: 'cert' subcommand, -chain flag for full walk, 'head' shortcut resolves
latest snapshot's certificate_hash
- ProgressFn now signals both bytes-read and total-from-Content-Length so
percent is computed against the actual transfer size, not the uncompressed
target
- verified against preprod: 90-cert chain head→genesis, Ed25519 genesis cert
shape (64-byte sig over 32-byte signed_message, protocol_message carries
next_aggregate_verification_key for BLS), STM-signed non-genesis certs
pipeline is now verification-sprint ready
- module layout: cmd/mithril-go, internal/{aggregator,artifact,verify,networks}
- aggregator REST client, list command working against mainnet
- download/extract/verify stubbed
- no deps yet, pure stdlib