Files
zeekay ca0bf6b303 precompile: seven packages to 96-99%, from lanes killed before they could commit
slhdsa 97.9 · mlkem 97.4 · mldsa 96.8 · xwing 96.2 · bls12381 97.2 ·
kzg4844 97.8 · v3 98.7. All build, vet and gofmt clean, all green under
-count=1, and preserved here rather than lost to a session limit.

v3's lane did report before it died: 78.1% to 98.7%, 70 of 70 mutations caught,
four of which survived a first pass and needed tests written. The two that
matter are rounding: burn rounding principal UP, and fee growth rounding UP,
neither of which any behaviour test could see -- a Q128.128 off-by-one is
2^-128 of a token and floors to zero through every payout path, so it took an
exact-value assertion. It also found that a full drain of a pool with
overlapping positions REVERTS rather than paying out, because deposits round
down once per position while payouts round down once per step over combined
liquidity, and floor(a+b) >= floor(a)+floor(b). Custody holds; not one unbacked
wei leaves. Liveness, not theft.

The other six carry no report -- their lanes were killed mid-run. Build, vet,
gofmt and the suite are all I can vouch for.
2026-08-28 13:27:00 -07:00

222 lines
8.1 KiB
Go

// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//
// NIST ACVP known-answer coverage for the ML-DSA precompile (FIPS 204).
//
// testdata/acvp_fips204.json holds a subset of the NIST ACVP-Server demo
// vectors. Two kinds are used, and they answer two different questions:
//
// keyGen seed -> pk. Anchors the wrapped library's key derivation to
// bytes NIST published. A round-trip test cannot see a keygen
// that is wrong the same way in both directions; this can.
//
// sigVer pk, message, signature, testPassed. These exercise
// ML-DSA.Verify_internal -- the raw interface, with no context
// prefix. The precompile calls the EXTERNAL interface bound to
// precompileCtx, so a NIST-valid internal signature must be
// refused here. That is the property, not an accident.
package mldsa
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/luxfi/geth/common"
"github.com/stretchr/testify/require"
)
type acvpKeyGen struct {
ParameterSet string `json:"parameterSet"`
ModeByte string `json:"modeByte"`
TcID int `json:"tcId"`
Seed string `json:"seed"`
Pk string `json:"pk"`
}
type acvpSigVer struct {
ParameterSet string `json:"parameterSet"`
ModeByte string `json:"modeByte"`
TcID int `json:"tcId"`
Pk string `json:"pk"`
Message string `json:"message"`
Signature string `json:"signature"`
TestPassed bool `json:"testPassed"`
}
type acvpFIPS204 struct {
Source string `json:"source"`
KeyGen []acvpKeyGen `json:"keyGen"`
SigVer []acvpSigVer `json:"sigVer"`
}
func loadACVP(t *testing.T) acvpFIPS204 {
t.Helper()
b, err := os.ReadFile(filepath.Join("testdata", "acvp_fips204.json"))
require.NoError(t, err)
var f acvpFIPS204
require.NoError(t, json.Unmarshal(b, &f))
require.NotEmpty(t, f.KeyGen)
require.NotEmpty(t, f.SigVer)
return f
}
// run drives the precompile over a frame and returns the verdict byte.
func run(t *testing.T, input []byte) byte {
t.Helper()
gas := MLDSAVerifyPrecompile.RequiredGas(input)
ret, _, err := MLDSAVerifyPrecompile.Run(
nil, common.Address{}, ContractMLDSAVerifyAddress, input, gas, true)
require.NoError(t, err)
require.Len(t, ret, 32)
return ret[31]
}
// Key derivation reproduces the bytes NIST published, and the size
// constants this package hard-codes are the sizes NIST published.
func TestKAT_KeyGenMatchesFIPS204(t *testing.T) {
for _, v := range loadACVP(t).KeyGen {
t.Run(v.ParameterSet, func(t *testing.T) {
mode := modeByteFromString(t, v.ModeByte)
pk, _ := signFromSeed(t, mode, mustHex(t, v.Seed), pinnedMessage, nil)
require.Equalf(t, mustHex(t, v.Pk), pk,
"tc %d: derived public key differs from the NIST vector", v.TcID)
declared, _, _, _, err := getModeParams(mode)
require.NoError(t, err)
require.Equal(t, declared, len(pk),
"the package's public-key size constant disagrees with FIPS 204")
})
}
}
// A signature under a NIST-anchored key verifies through the precompile,
// and every single-byte corruption of the frame is refused.
func TestKAT_PrecompileUnderNISTAnchoredKey(t *testing.T) {
for _, v := range loadACVP(t).KeyGen {
t.Run(v.ParameterSet, func(t *testing.T) {
mode := modeByteFromString(t, v.ModeByte)
pk, sig := signFromSeed(t, mode, mustHex(t, v.Seed), pinnedMessage, precompileCtx)
require.Equal(t, mustHex(t, v.Pk), pk, "key is not the NIST one")
require.Equal(t, byte(1), run(t, buildPinnedInput(mode, pk, pinnedMessage, sig)),
"a good signature under the NIST key must verify")
// Corrupt one byte at the front, middle and end of the signature.
for _, at := range []int{0, len(sig) / 2, len(sig) - 1} {
bad := append([]byte{}, sig...)
bad[at] ^= 0x01
require.Equalf(t, byte(0), run(t, buildPinnedInput(mode, pk, pinnedMessage, bad)),
"signature byte %d flipped and still accepted", at)
}
// Corrupt the message.
msg := append([]byte{}, pinnedMessage...)
msg[0] ^= 0x01
require.Equal(t, byte(0), run(t, buildPinnedInput(mode, pk, msg, sig)),
"message flipped and still accepted")
// Corrupt the key.
badPk := append([]byte{}, pk...)
badPk[len(badPk)-1] ^= 0x01
require.Equal(t, byte(0), run(t, buildPinnedInput(mode, badPk, pinnedMessage, sig)),
"public key flipped and still accepted")
// An all-zero signature of the right length is refused too.
require.Equal(t, byte(0), run(t, buildPinnedInput(mode, pk, pinnedMessage, make([]byte, len(sig)))),
"all-zero signature accepted")
// So is an all-zero key.
require.Equal(t, byte(0), run(t, buildPinnedInput(mode, make([]byte, len(pk)), pinnedMessage, sig)),
"all-zero public key accepted")
})
}
}
// The precompile binds precompileCtx. NIST's sigVer vectors are signatures
// over the internal interface, which carries no context at all; the ones
// NIST marks valid are genuinely valid ML-DSA signatures, and the
// precompile must still refuse every one of them. If the precompile ever
// verified the internal interface, or verified with an empty context, an
// off-chain ML-DSA signature over an unrelated protocol would satisfy an
// on-chain verify.
func TestKAT_PrecompileRefusesInternalInterfaceSignatures(t *testing.T) {
f := loadACVP(t)
anyValid := false
for _, v := range f.SigVer {
mode := modeByteFromString(t, v.ModeByte)
pk, msg, sig := mustHex(t, v.Pk), mustHex(t, v.Message), mustHex(t, v.Signature)
require.Equal(t, byte(0), run(t, buildPinnedInput(mode, pk, msg, sig)),
"%s tc %d (NIST testPassed=%v) accepted: the precompile is not bound to its context",
v.ParameterSet, v.TcID, v.TestPassed)
anyValid = anyValid || v.TestPassed
}
require.True(t, anyValid,
"the vector set carries no NIST-valid signature, so it proves nothing")
}
// The context is load-bearing and exact. The same key and message signed
// under a different context -- including the empty one, and one that
// differs by a single byte -- must not verify.
func TestKAT_ContextIsExact(t *testing.T) {
seed := mustHex(t, loadACVP(t).KeyGen[0].Seed)
mode := ModeMLDSA44
near := append([]byte{}, precompileCtx...)
near[len(near)-1] ^= 0x01
for name, ctx := range map[string][]byte{
"nil": nil,
"empty": {},
"off by one bit": near,
"truncated": precompileCtx[:len(precompileCtx)-1],
"other scheme": []byte("lux-evm-precompile-slhdsa-v1"),
} {
pk, sig := signFromSeed(t, mode, seed, pinnedMessage, ctx)
require.Equalf(t, byte(0), run(t, buildPinnedInput(mode, pk, pinnedMessage, sig)),
"a signature under context %q verified", name)
}
pk, sig := signFromSeed(t, mode, seed, pinnedMessage, precompileCtx)
require.Equal(t, byte(1), run(t, buildPinnedInput(mode, pk, pinnedMessage, sig)),
"the precompile's own context must verify")
}
// Mode confusion is structural for ML-DSA: no two parameter sets frame to
// the same length, so a frame built for one mode cannot be re-labelled as
// another. Assert both halves of that -- the sizes are distinct, and
// re-labelling is refused rather than misparsed.
func TestKAT_ModeConfusionIsStructural(t *testing.T) {
modes := []uint8{ModeMLDSA44, ModeMLDSA65, ModeMLDSA87}
seen := map[int]uint8{}
for _, m := range modes {
pkSize, sigSize, _, _, err := getModeParams(m)
require.NoError(t, err)
frameLen := ModeByte + pkSize + MessageLenSize + sigSize
prev, dup := seen[frameLen]
require.Falsef(t, dup, "modes 0x%02x and 0x%02x share a frame length", prev, m)
seen[frameLen] = m
}
seed := mustHex(t, loadACVP(t).KeyGen[0].Seed)
for _, signing := range modes {
pk, sig := signFromSeed(t, signing, seed, pinnedMessage, precompileCtx)
good := buildPinnedInput(signing, pk, pinnedMessage, sig)
for _, claimed := range modes {
if claimed == signing {
continue
}
relabelled := append([]byte{}, good...)
relabelled[0] = claimed
gas := MLDSAVerifyPrecompile.RequiredGas(relabelled)
_, _, err := MLDSAVerifyPrecompile.Run(
nil, common.Address{}, ContractMLDSAVerifyAddress, relabelled, gas, true)
require.Errorf(t, err,
"a 0x%02x frame re-labelled 0x%02x was parsed instead of refused", signing, claimed)
}
}
}