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).
126 lines
3.3 KiB
Go
126 lines
3.3 KiB
Go
package stm
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"math"
|
|
"math/big"
|
|
|
|
"golang.org/x/crypto/blake2b"
|
|
)
|
|
|
|
// EvaluateSigma computes the 64-byte lottery evaluation for a given
|
|
// (msg, index, sigma). Mirrors Rust's evaluate_dense_mapping:
|
|
//
|
|
// ev = Blake2b-512( "map" || msg || le_u64(index) || sigma_bytes )
|
|
//
|
|
// The 64-byte output is the lottery draw, interpreted as a big unsigned
|
|
// integer in LSF/little-endian byte order per the Rust impl:
|
|
//
|
|
// rug::Integer::from_digits(&ev, Order::LsfLe)
|
|
// num_bigint::BigInt::from_bytes_le(Sign::Plus, &ev)
|
|
func EvaluateSigma(msg []byte, index uint64, sigma []byte) [64]byte {
|
|
h, _ := blake2b.New512(nil)
|
|
h.Write([]byte("map"))
|
|
h.Write(msg)
|
|
var idxBuf [8]byte
|
|
binary.LittleEndian.PutUint64(idxBuf[:], index)
|
|
h.Write(idxBuf[:])
|
|
h.Write(sigma)
|
|
var out [64]byte
|
|
copy(out[:], h.Sum(nil))
|
|
return out
|
|
}
|
|
|
|
// evAsBigInt converts the 64-byte ev output to a big.Int using LE byte
|
|
// order (matching the Rust `from_bytes_le`).
|
|
func evAsBigInt(ev [64]byte) *big.Int {
|
|
// big.Int.SetBytes is BE; so flip.
|
|
rev := make([]byte, len(ev))
|
|
for i := range ev {
|
|
rev[i] = ev[len(ev)-1-i]
|
|
}
|
|
return new(big.Int).SetBytes(rev)
|
|
}
|
|
|
|
// IsLotteryWon reports whether a signer with the given stake wins the
|
|
// lottery at the claimed index for the given ev.
|
|
//
|
|
// Predicate: p < 1 - (1 - phi_f)^w, where
|
|
//
|
|
// p = ev / 2^512
|
|
// w = stake / total_stake
|
|
// phi_f = protocol parameter in (0, 1]
|
|
//
|
|
// Equivalent reformulation (used here): `q < exp(-w * c)` where
|
|
// `q = 1/(1-p)` and `c = ln(1 - phi_f)`. Evaluated via Taylor series
|
|
// with early-stop on the error bound (constant M=3 from the Rust impl).
|
|
func IsLotteryWon(phiF float64, ev [64]byte, stake, totalStake uint64) bool {
|
|
if math.Abs(phiF-1.0) < 1e-15 {
|
|
return true
|
|
}
|
|
// Defensive: zero-stake or zero-total-stake produces nonsense (and
|
|
// totalStake==0 would panic at SetFrac). Guard at the lottery layer
|
|
// in addition to AVK-decode-time validation.
|
|
if stake == 0 || totalStake == 0 {
|
|
return false
|
|
}
|
|
|
|
// ev as big int (LE interpretation)
|
|
evInt := evAsBigInt(ev)
|
|
|
|
// evMax = 2^512
|
|
evMax := new(big.Int).Lsh(big.NewInt(1), 512)
|
|
|
|
// q = evMax / (evMax - ev) — a Ratio
|
|
denom := new(big.Int).Sub(evMax, evInt)
|
|
q := new(big.Rat).SetFrac(new(big.Int).Set(evMax), denom)
|
|
|
|
// c = ln(1 - phi_f); x = -w * c
|
|
cFloat := math.Log(1.0 - phiF)
|
|
c := new(big.Rat).SetFloat64(cFloat)
|
|
|
|
w := new(big.Rat).SetFrac(
|
|
new(big.Int).SetUint64(stake),
|
|
new(big.Int).SetUint64(totalStake),
|
|
)
|
|
|
|
x := new(big.Rat).Mul(w, c)
|
|
x.Neg(x)
|
|
|
|
return taylorCompare(1000, q, x)
|
|
}
|
|
|
|
// taylorCompare reports whether cmp < exp(x), using a Taylor series
|
|
// expansion with an early-stop error heuristic (M = 3).
|
|
func taylorCompare(bound int, cmp, x *big.Rat) bool {
|
|
newX := new(big.Rat).Set(x)
|
|
phi := new(big.Rat).SetInt64(1)
|
|
divisor := big.NewInt(1)
|
|
three := big.NewRat(3, 1)
|
|
absNewX := new(big.Rat)
|
|
errorTerm := new(big.Rat)
|
|
sum := new(big.Rat)
|
|
diff := new(big.Rat)
|
|
|
|
for i := 0; i < bound; i++ {
|
|
phi.Add(phi, newX)
|
|
divisor = new(big.Int).Add(divisor, big.NewInt(1))
|
|
// newX = newX * x / divisor
|
|
nx := new(big.Rat).Mul(newX, x)
|
|
nx.Quo(nx, new(big.Rat).SetInt(divisor))
|
|
newX = nx
|
|
|
|
absNewX.Abs(newX)
|
|
errorTerm.Mul(absNewX, three)
|
|
|
|
sum.Add(phi, errorTerm)
|
|
if cmp.Cmp(sum) > 0 {
|
|
return false
|
|
}
|
|
diff.Sub(phi, errorTerm)
|
|
if cmp.Cmp(diff) < 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|