mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
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.
191 lines
6.1 KiB
Go
191 lines
6.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-KEM precompile (FIPS 203).
|
|
//
|
|
// testdata/acvp_fips203.json holds a subset of the NIST ACVP-Server demo
|
|
// vectors:
|
|
//
|
|
// encap (ek, m) -> (c, k). The precompile's encapsulation is exactly
|
|
// this function with m supplied by deriveSeed, so feeding NIST's
|
|
// m must reproduce NIST's ciphertext and shared secret byte for
|
|
// byte. A round-trip test cannot see an encapsulation that is
|
|
// wrong in a way decapsulation undoes; this can.
|
|
//
|
|
// keyGen (d, z) -> (ek, dk). Gives a NIST-issued key pair, so the
|
|
// precompile can be driven against a key it did not generate and
|
|
// the result decapsulated with the matching NIST private key.
|
|
|
|
package mlkem
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/luxfi/crypto/mlkem"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type acvpEncap struct {
|
|
ParameterSet string `json:"parameterSet"`
|
|
ModeByte string `json:"modeByte"`
|
|
TcID int `json:"tcId"`
|
|
Ek string `json:"ek"`
|
|
M string `json:"m"`
|
|
C string `json:"c"`
|
|
K string `json:"k"`
|
|
}
|
|
|
|
type acvpKeyGen struct {
|
|
ParameterSet string `json:"parameterSet"`
|
|
ModeByte string `json:"modeByte"`
|
|
TcID int `json:"tcId"`
|
|
D string `json:"d"`
|
|
Z string `json:"z"`
|
|
Ek string `json:"ek"`
|
|
Dk string `json:"dk"`
|
|
}
|
|
|
|
type acvpFIPS203 struct {
|
|
Source string `json:"source"`
|
|
Encap []acvpEncap `json:"encap"`
|
|
KeyGen []acvpKeyGen
|
|
}
|
|
|
|
func loadACVP(t *testing.T) acvpFIPS203 {
|
|
t.Helper()
|
|
b, err := os.ReadFile(filepath.Join("testdata", "acvp_fips203.json"))
|
|
require.NoError(t, err)
|
|
var f acvpFIPS203
|
|
require.NoError(t, json.Unmarshal(b, &f))
|
|
require.NotEmpty(t, f.Encap)
|
|
require.NotEmpty(t, f.KeyGen)
|
|
return f
|
|
}
|
|
|
|
// fixedReader hands back exactly the bytes it was given, so an
|
|
// encapsulation can be pinned to a chosen m.
|
|
type fixedReader struct {
|
|
b []byte
|
|
n int
|
|
}
|
|
|
|
func (r *fixedReader) Read(p []byte) (int, error) {
|
|
n := copy(p, r.b[r.n:])
|
|
r.n += n
|
|
return n, nil
|
|
}
|
|
|
|
func modeByte(t *testing.T, s string) uint8 {
|
|
t.Helper()
|
|
switch s {
|
|
case "0x00":
|
|
return ModeMLKEM512
|
|
case "0x01":
|
|
return ModeMLKEM768
|
|
case "0x02":
|
|
return ModeMLKEM1024
|
|
}
|
|
t.Fatalf("unknown ML-KEM mode byte %q", s)
|
|
return 0
|
|
}
|
|
|
|
// Encapsulation reproduces the ciphertext and shared secret NIST
|
|
// published, for every parameter set the precompile offers.
|
|
func TestKAT_EncapsulateMatchesFIPS203(t *testing.T) {
|
|
for _, v := range loadACVP(t).Encap {
|
|
t.Run(v.ParameterSet, func(t *testing.T) {
|
|
mode := modeByte(t, v.ModeByte)
|
|
pubSize, ctSize, ssSize, _, kemMode, err := getModeParams(mode)
|
|
require.NoError(t, err)
|
|
|
|
ek := mustHex(t, v.Ek)
|
|
require.Equal(t, pubSize, len(ek),
|
|
"the package's public-key size constant disagrees with FIPS 203")
|
|
|
|
pk, err := mlkem.PublicKeyFromBytes(ek, kemMode)
|
|
require.NoError(t, err)
|
|
|
|
ct, ss, err := pk.Encapsulate(&fixedReader{b: mustHex(t, v.M)})
|
|
require.NoError(t, err)
|
|
require.Equalf(t, mustHex(t, v.C), ct, "tc %d: ciphertext differs from the NIST vector", v.TcID)
|
|
require.Equalf(t, mustHex(t, v.K), ss, "tc %d: shared secret differs from the NIST vector", v.TcID)
|
|
require.Equal(t, ctSize, len(ct), "ciphertext size constant")
|
|
require.Equal(t, ssSize, len(ss), "shared-secret size constant")
|
|
})
|
|
}
|
|
}
|
|
|
|
// Driven against a NIST-issued encapsulation key, the precompile's output
|
|
// decapsulates under the matching NIST private key to exactly the shared
|
|
// secret the precompile returned. That is the whole contract: a contract
|
|
// on chain and a counterparty off chain agree on a key.
|
|
func TestKAT_PrecompileOutputDecapsulatesUnderNISTKey(t *testing.T) {
|
|
caller := common.HexToAddress("0x000000000000000000000000000000000000c0de")
|
|
for _, v := range loadACVP(t).KeyGen {
|
|
t.Run(v.ParameterSet, func(t *testing.T) {
|
|
mode := modeByte(t, v.ModeByte)
|
|
_, ctSize, ssSize, _, kemMode, err := getModeParams(mode)
|
|
require.NoError(t, err)
|
|
|
|
ek, dk := mustHex(t, v.Ek), mustHex(t, v.Dk)
|
|
|
|
in := append([]byte{OpEncapsulate, mode}, bytes.Repeat([]byte{0x5A}, SeedSize)...)
|
|
in = append(in, ek...)
|
|
|
|
out, _, err := MLKEMPrecompile.Run(nil, caller, ContractAddress,
|
|
in, MLKEMPrecompile.RequiredGas(in), true)
|
|
require.NoError(t, err)
|
|
require.Len(t, out, ctSize+ssSize)
|
|
|
|
ct, ss := out[:ctSize], out[ctSize:]
|
|
|
|
sk, err := mlkem.PrivateKeyFromBytes(dk, kemMode)
|
|
require.NoError(t, err)
|
|
recovered, err := sk.Decapsulate(ct)
|
|
require.NoError(t, err)
|
|
require.Equalf(t, recovered, ss,
|
|
"tc %d: the shared secret the precompile returned is not the one its ciphertext carries", v.TcID)
|
|
})
|
|
}
|
|
}
|
|
|
|
// The precompile's randomness is deriveSeed(caller, raw), consumed
|
|
// directly as FIPS 203's m. Pin that: recomputing the derivation by hand
|
|
// and encapsulating with it must land on the precompile's exact output.
|
|
// A change to the label, to the order of the inputs, or to the inclusion
|
|
// of the caller shows up here and nowhere else -- every round-trip test
|
|
// keeps passing through all three.
|
|
func TestKAT_PrecompileSeedIsTheDerivedSeed(t *testing.T) {
|
|
caller := common.HexToAddress("0x00000000000000000000000000000000deadbeef")
|
|
v := loadACVP(t).KeyGen[1] // ML-KEM-768
|
|
mode := modeByte(t, v.ModeByte)
|
|
_, ctSize, _, _, kemMode, err := getModeParams(mode)
|
|
require.NoError(t, err)
|
|
|
|
var raw [SeedSize]byte
|
|
for i := range raw {
|
|
raw[i] = byte(i * 7)
|
|
}
|
|
ek := mustHex(t, v.Ek)
|
|
|
|
in := append([]byte{OpEncapsulate, mode}, raw[:]...)
|
|
in = append(in, ek...)
|
|
out, _, err := MLKEMPrecompile.Run(nil, caller, ContractAddress,
|
|
in, MLKEMPrecompile.RequiredGas(in), true)
|
|
require.NoError(t, err)
|
|
|
|
m := deriveSeed(caller, raw)
|
|
pk, err := mlkem.PublicKeyFromBytes(ek, kemMode)
|
|
require.NoError(t, err)
|
|
ct, ss, err := pk.Encapsulate(&fixedReader{b: m[:]})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, ct, out[:ctSize], "ciphertext is not the one deriveSeed's m produces")
|
|
require.Equal(t, ss, out[ctSize:], "shared secret is not the one deriveSeed's m produces")
|
|
}
|