mirror of
https://github.com/luxfi/math.git
synced 2026-07-27 03:38:49 +00:00
188 lines
4.4 KiB
Go
188 lines
4.4 KiB
Go
// Copyright (c) 2026 Lux Industries Inc.
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package sample
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const (
|
|
katNumSamples = 64
|
|
katPulsarQ = uint64(0x1000000004A01)
|
|
katNTT998 = uint64(998244353)
|
|
)
|
|
|
|
// TestKATBundle_RoundTrip — load the JSON KAT bundle emitted by
|
|
// cmd/emit_sample_kat, replay every entry through the Go-side
|
|
// samplers driven by a byte-buffer reader, and assert byte-equal
|
|
// sample output.
|
|
//
|
|
// Cross-runtime contract: the same JSON drives the C++ replay test in
|
|
// luxcpp/crypto/math/test/sample_cross_runtime_test.cpp.
|
|
func TestKATBundle_RoundTrip(t *testing.T) {
|
|
path := filepath.Join("testdata", "sample_kat.json")
|
|
bundle, err := ReadKATBundleFile(path)
|
|
if err != nil {
|
|
t.Skipf("KAT bundle not present at %s; run cmd/emit_sample_kat: %v",
|
|
path, err)
|
|
return
|
|
}
|
|
if bundle.Schema != KATSchemaV1 {
|
|
t.Fatalf("schema = %q, want %q", bundle.Schema, KATSchemaV1)
|
|
}
|
|
if len(bundle.Entries) == 0 {
|
|
t.Fatal("bundle has zero entries")
|
|
}
|
|
for i, entry := range bundle.Entries {
|
|
if err := entry.Header.Validate(); err != nil {
|
|
t.Errorf("entry[%d] header: %v", i, err)
|
|
}
|
|
input, err := HexDecode(entry.InputHex)
|
|
if err != nil {
|
|
t.Errorf("entry[%d] input_hex: %v", i, err)
|
|
continue
|
|
}
|
|
expected, err := HexDecode(entry.OutputHex)
|
|
if err != nil {
|
|
t.Errorf("entry[%d] output_hex: %v", i, err)
|
|
continue
|
|
}
|
|
actual, ok := replayEntry(t, entry.Test, input)
|
|
if !ok {
|
|
continue
|
|
}
|
|
digest := sha256.Sum256(actual)
|
|
if got := HexEncode(digest[:]); got != entry.OutputSHA256 {
|
|
t.Errorf("entry[%d] %q: SHA-256 mismatch:\n want %s\n got %s",
|
|
i, entry.Test, entry.OutputSHA256, got)
|
|
}
|
|
if !bytes.Equal(actual, expected) {
|
|
t.Errorf("entry[%d] %q: byte-stream mismatch", i, entry.Test)
|
|
}
|
|
}
|
|
}
|
|
|
|
// replayEntry parses the test name and dispatches to the right sampler.
|
|
// Test name format mirrors cmd/emit_sample_kat:
|
|
//
|
|
// Uniform/q=PulsarQ/i=N
|
|
// Uniform/q=NTT998/i=N
|
|
// Ternary/q=PulsarQ/density=0.50/i=N
|
|
// CenteredBinomial/q=PulsarQ/eta=2/i=N
|
|
func replayEntry(t *testing.T, name string, input []byte) ([]byte, bool) {
|
|
t.Helper()
|
|
r := newBBReader(input)
|
|
dst := make([]uint64, katNumSamples)
|
|
|
|
q := parseQ(name)
|
|
if q == 0 {
|
|
t.Errorf("%s: cannot parse q", name)
|
|
return nil, false
|
|
}
|
|
|
|
switch {
|
|
case strings.HasPrefix(name, "Uniform/"):
|
|
if err := Uniform(dst, q, r); err != nil {
|
|
t.Errorf("%s: Uniform: %v", name, err)
|
|
return nil, false
|
|
}
|
|
case strings.HasPrefix(name, "Ternary/"):
|
|
density, ok := parseDensity(name)
|
|
if !ok {
|
|
t.Errorf("%s: cannot parse density", name)
|
|
return nil, false
|
|
}
|
|
if err := Ternary(dst, q, density, r); err != nil {
|
|
t.Errorf("%s: Ternary: %v", name, err)
|
|
return nil, false
|
|
}
|
|
case strings.HasPrefix(name, "CenteredBinomial/"):
|
|
eta, ok := parseEta(name)
|
|
if !ok {
|
|
t.Errorf("%s: cannot parse eta", name)
|
|
return nil, false
|
|
}
|
|
if err := CenteredBinomial(dst, q, eta, r); err != nil {
|
|
t.Errorf("%s: CenteredBinomial: %v", name, err)
|
|
return nil, false
|
|
}
|
|
default:
|
|
t.Errorf("unknown KAT test name: %s", name)
|
|
return nil, false
|
|
}
|
|
out := make([]byte, len(dst)*8)
|
|
for i, v := range dst {
|
|
binary.LittleEndian.PutUint64(out[i*8:], v)
|
|
}
|
|
return out, true
|
|
}
|
|
|
|
func parseQ(name string) uint64 {
|
|
if strings.Contains(name, "q=PulsarQ") {
|
|
return katPulsarQ
|
|
}
|
|
if strings.Contains(name, "q=NTT998") {
|
|
return katNTT998
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func parseDensity(name string) (float64, bool) {
|
|
idx := strings.Index(name, "density=")
|
|
if idx < 0 {
|
|
return 0, false
|
|
}
|
|
rest := name[idx+len("density="):]
|
|
end := strings.IndexByte(rest, '/')
|
|
if end < 0 {
|
|
return 0, false
|
|
}
|
|
var d float64
|
|
if _, err := fmt.Sscanf(rest[:end], "%f", &d); err != nil {
|
|
return 0, false
|
|
}
|
|
return d, true
|
|
}
|
|
|
|
func parseEta(name string) (int, bool) {
|
|
idx := strings.Index(name, "eta=")
|
|
if idx < 0 {
|
|
return 0, false
|
|
}
|
|
rest := name[idx+len("eta="):]
|
|
end := strings.IndexByte(rest, '/')
|
|
if end < 0 {
|
|
return 0, false
|
|
}
|
|
var e int
|
|
if _, err := fmt.Sscanf(rest[:end], "%d", &e); err != nil {
|
|
return 0, false
|
|
}
|
|
return e, true
|
|
}
|
|
|
|
// bbReader is a stateful byte-buffer io.Reader.
|
|
type bbReader struct {
|
|
buf []byte
|
|
off int
|
|
}
|
|
|
|
func newBBReader(b []byte) *bbReader { return &bbReader{buf: b} }
|
|
|
|
func (r *bbReader) Read(p []byte) (int, error) {
|
|
if r.off >= len(r.buf) {
|
|
return 0, io.EOF
|
|
}
|
|
n := copy(p, r.buf[r.off:])
|
|
r.off += n
|
|
return n, nil
|
|
}
|