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

188 lines
6.2 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 SLH-DSA precompile (FIPS 205).
//
// testdata/acvp_fips205.json holds a subset of the NIST ACVP-Server demo
// sigVer vectors, restricted to signatureInterface=external and
// preHash=pure -- SLH-DSA.Verify(pk, M, sig, ctx), the exact function the
// precompile calls. Each vector carries its own context and NIST's verdict,
// so the wrapped verifier can be checked in both directions: it must accept
// what NIST accepts and refuse what NIST refuses. A round-trip test sees
// neither, because a verifier that is wrong in the same way as its signer
// still agrees with itself.
package slhdsa
import (
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/luxfi/crypto/slhdsa"
"github.com/luxfi/geth/common"
"github.com/stretchr/testify/require"
)
type acvpSigVer struct {
ParameterSet string `json:"parameterSet"`
ModeByte string `json:"modeByte"`
TcID int `json:"tcId"`
Pk string `json:"pk"`
Message string `json:"message"`
Context string `json:"context"`
Signature string `json:"signature"`
TestPassed bool `json:"testPassed"`
}
type acvpFIPS205 struct {
Source string `json:"source"`
SigVer []acvpSigVer `json:"sigVer"`
}
func loadACVP(t *testing.T) []acvpSigVer {
t.Helper()
b, err := os.ReadFile(filepath.Join("testdata", "acvp_fips205.json"))
require.NoError(t, err)
var f acvpFIPS205
require.NoError(t, json.Unmarshal(b, &f))
require.NotEmpty(t, f.SigVer)
return f.SigVer
}
func fromHex(t *testing.T, s string) []byte {
t.Helper()
b, err := hex.DecodeString(s)
require.NoError(t, err)
return b
}
func acvpMode(t *testing.T, s string) uint8 {
t.Helper()
switch s {
case "0x00":
return ModeSHA2_128s
case "0x02":
return ModeSHA2_192s
case "0x10":
return ModeSHAKE_128s
}
t.Fatalf("unknown SLH-DSA mode byte %q", s)
return 0
}
// verdict drives the precompile and returns its answer byte.
func verdict(t *testing.T, input []byte) byte {
t.Helper()
gas := SLHDSAVerifyPrecompile.RequiredGas(input)
ret, _, err := SLHDSAVerifyPrecompile.Run(nil, common.Address{},
ContractSLHDSAVerifyAddress, input, gas, true)
require.NoError(t, err)
require.Len(t, ret, 32)
return ret[31]
}
// The verifier the precompile calls agrees with NIST on every vector, in
// both directions. Accepting everything would pass a positive-only suite;
// refusing everything would pass a negative-only one. Requiring both, and
// requiring the vector set to contain both, is what makes this a test.
func TestKAT_VerifierAgreesWithFIPS205(t *testing.T) {
vectors := loadACVP(t)
seen := map[bool]int{}
for _, v := range vectors {
mode := acvpMode(t, v.ModeByte)
pubSize, sigSize, _, libMode, err := getModeParams(mode)
require.NoError(t, err)
pk := fromHex(t, v.Pk)
require.Equalf(t, pubSize, len(pk),
"%s: the package's public-key size constant disagrees with FIPS 205", v.ParameterSet)
pub, err := slhdsa.PublicKeyFromBytes(pk, libMode)
require.NoError(t, err)
got := pub.VerifySignatureCtx(
fromHex(t, v.Message), fromHex(t, v.Signature), fromHex(t, v.Context))
require.Equalf(t, v.TestPassed, got,
"%s tc %d: verifier disagrees with the NIST verdict", v.ParameterSet, v.TcID)
if v.TestPassed {
require.Equalf(t, sigSize, len(fromHex(t, v.Signature)),
"%s: the package's signature size constant disagrees with FIPS 205", v.ParameterSet)
}
seen[v.TestPassed]++
}
require.NotZero(t, seen[true], "no accepting vector: the suite proves only that it refuses")
require.NotZero(t, seen[false], "no refusing vector: the suite proves only that it accepts")
}
// The precompile substitutes its own context for whatever the caller
// intended, so a signature NIST calls valid under some other context must
// be refused here. Without that binding, a signature produced for an
// unrelated protocol -- a UTXO, a Warp message -- would satisfy an
// on-chain verify.
func TestKAT_PrecompileRefusesForeignContext(t *testing.T) {
accepted := 0
for _, v := range loadACVP(t) {
if !v.TestPassed {
continue
}
accepted++
mode := acvpMode(t, v.ModeByte)
require.NotEqual(t, precompileCtx, fromHex(t, v.Context),
"vector context collided with the precompile's own")
in := prepareInputWithMode(mode, fromHex(t, v.Pk), fromHex(t, v.Message), fromHex(t, v.Signature))
require.Equalf(t, byte(0), verdict(t, in),
"%s tc %d: a signature under a foreign context verified", v.ParameterSet, v.TcID)
}
require.NotZero(t, accepted, "no NIST-valid vector to offer, so nothing was proved")
}
// SHA2 and SHAKE at the same security level have identical key and
// signature sizes, so a frame built for one parses cleanly as the other.
// For these six pairs mode confusion is NOT caught by the length checks --
// only the hash family inside the verifier separates them. Assert both
// halves: the sizes really do collide, and the verifier still says no.
func TestKAT_ModeConfusionAcrossHashFamilies(t *testing.T) {
pairs := [][2]uint8{
{ModeSHA2_128s, ModeSHAKE_128s},
{ModeSHA2_128f, ModeSHAKE_128f},
{ModeSHA2_192s, ModeSHAKE_192s},
{ModeSHA2_192f, ModeSHAKE_192f},
{ModeSHA2_256s, ModeSHAKE_256s},
{ModeSHA2_256f, ModeSHAKE_256f},
}
for _, p := range pairs {
aPub, aSig, _, _, err := getModeParams(p[0])
require.NoError(t, err)
bPub, bSig, _, _, err := getModeParams(p[1])
require.NoError(t, err)
require.Equalf(t, aPub, bPub, "0x%02x/0x%02x public-key sizes", p[0], p[1])
require.Equalf(t, aSig, bSig, "0x%02x/0x%02x signature sizes", p[0], p[1])
}
swap := map[uint8]uint8{ModeSHA2_128s: ModeSHAKE_128s, ModeSHAKE_128s: ModeSHA2_128s}
offered := 0
for _, v := range loadACVP(t) {
if !v.TestPassed {
continue
}
mode := acvpMode(t, v.ModeByte)
other, ok := swap[mode]
if !ok {
continue
}
offered++
in := prepareInputWithMode(other, fromHex(t, v.Pk), fromHex(t, v.Message), fromHex(t, v.Signature))
require.Equalf(t, byte(0), verdict(t, in),
"%s tc %d verified as 0x%02x: the hash family is not separating the parameter sets",
v.ParameterSet, v.TcID, other)
}
require.Equal(t, 2, offered, "both hash families must supply a vector for this to mean anything")
}