mirror of
https://github.com/luxfi/math.git
synced 2026-09-24 00:25:16 +00:00
LP-107 Phase 6.3-6.5: KAT extension — modarith / ntt / sample
Replicates the codec cross-runtime KAT pattern across the rest of the math substrate. Each package now ships: * kat.go — KATEntry/KATBundle aliases pointing at codec.KATEntry/ codec.KATBundle (one schema for the whole substrate). * cmd/emit_*_kat/main.go — Go-side emitter that produces the canonical bundle. * kat_test.go — Go replay test (TestKATBundle_RoundTrip*). * testdata/*_kat.json — emitted bundle, byte-stable across runs. modarith/cmd/emit_modarith_kat: ~300 entries - 100 MontMulMod cross-checked against MulMod - 100 AddMod across deterministic random pairs - 100 MontgomeryRoundTrip (ToMontgomery -> FromMontgomery) - Multiple moduli (PulsarQ, NTT998). ntt/cmd/emit_ntt_kat: ~50 entries - input: 256 random uint64 mod PulsarQ - output: NTT(input) byte-stream + SHA-256 commitment - Pulsar N=256 ring. sample/cmd/emit_sample_kat: ~30 entries - Uniform / Ternary / CenteredBinomial across param sweeps - Each entry records the seed + first 64 sampled values. Tests: GOWORK=off go test ./modarith/ ./ntt/ ./sample/ -> all PASS. C++ replay tests follow in luxcpp/crypto commit on this same branch.
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
// Copyright (c) 2026 Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
//
|
||||
// emit_sample_kat — produces the canonical cross-runtime KAT bundle
|
||||
// for luxfi/math/sample. The C++ side at luxcpp/crypto/math/test/
|
||||
// sample_cross_runtime_test.cpp reads the same JSON, drives a byte-
|
||||
// buffer reader with the recorded entropy, and asserts byte-equal
|
||||
// sample output.
|
||||
//
|
||||
// LP-107 Phase 6.5: Go emits → C++ verifies. Cross-runtime release gate
|
||||
// for primitive distribution samplers.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/emit_sample_kat --out testdata/sample_kat.json
|
||||
//
|
||||
// Each entry contains:
|
||||
// Test: "Uniform/q=PulsarQ/i=N" -> param: q
|
||||
// "Ternary/q=PulsarQ/density=0.50/i=N" -> param: q, density
|
||||
// "CenteredBinomial/q=PulsarQ/eta=2/i=N" -> param: q, eta
|
||||
// InputHex: raw entropy bytes (4096) from a SHA-256 hash chain
|
||||
// OutputHex: first 64 samples, packed little-endian (512 bytes)
|
||||
//
|
||||
// Entropy bytes are deterministic from the test name + index seed so
|
||||
// emit runs are byte-stable.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"github.com/luxfi/math/codec"
|
||||
"github.com/luxfi/math/params"
|
||||
"github.com/luxfi/math/sample"
|
||||
)
|
||||
|
||||
const (
|
||||
PulsarQ uint64 = 0x1000000004A01
|
||||
NTT998 uint64 = 998244353
|
||||
|
||||
NumSamples = 64
|
||||
EntropyBytes = 4096
|
||||
)
|
||||
|
||||
func main() {
|
||||
out := flag.String("out", "sample_kat.json", "output JSON path")
|
||||
flag.Parse()
|
||||
|
||||
header := params.KATHeader{
|
||||
ParameterSet: "math.sample.v1",
|
||||
ModulusID: params.ModPulsarQ,
|
||||
BackendID: params.BackendPureGo,
|
||||
HashSuiteID: params.HashBLAKE3,
|
||||
ImplementationName: "luxfi/math/sample",
|
||||
ImplementationVersion: "v1.3.0",
|
||||
}
|
||||
|
||||
entries := []codec.KATEntry{}
|
||||
|
||||
// 10 Uniform/PulsarQ entries.
|
||||
for i := 0; i < 10; i++ {
|
||||
seed := fmt.Sprintf("sample/Uniform/PulsarQ/i=%d", i)
|
||||
entropy := genEntropy(seed, EntropyBytes)
|
||||
dst := make([]uint64, NumSamples)
|
||||
if err := sample.Uniform(dst, PulsarQ, byteReader(entropy)); err != nil {
|
||||
log.Fatalf("Uniform[%d]: %v", i, err)
|
||||
}
|
||||
entries = append(entries, mkEntry(header,
|
||||
fmt.Sprintf("Uniform/q=PulsarQ/i=%d", i),
|
||||
entropy, packU64s(dst)))
|
||||
}
|
||||
|
||||
// 10 Uniform/NTT998 entries (smaller modulus, different mask).
|
||||
for i := 0; i < 10; i++ {
|
||||
seed := fmt.Sprintf("sample/Uniform/NTT998/i=%d", i)
|
||||
entropy := genEntropy(seed, EntropyBytes)
|
||||
dst := make([]uint64, NumSamples)
|
||||
if err := sample.Uniform(dst, NTT998, byteReader(entropy)); err != nil {
|
||||
log.Fatalf("Uniform/NTT998[%d]: %v", i, err)
|
||||
}
|
||||
entries = append(entries, mkEntry(header,
|
||||
fmt.Sprintf("Uniform/q=NTT998/i=%d", i),
|
||||
entropy, packU64s(dst)))
|
||||
}
|
||||
|
||||
// 5 Ternary entries at density=0.5.
|
||||
for i := 0; i < 5; i++ {
|
||||
seed := fmt.Sprintf("sample/Ternary/d=0.50/i=%d", i)
|
||||
entropy := genEntropy(seed, EntropyBytes)
|
||||
dst := make([]uint64, NumSamples)
|
||||
if err := sample.Ternary(dst, PulsarQ, 0.5, byteReader(entropy)); err != nil {
|
||||
log.Fatalf("Ternary[%d]: %v", i, err)
|
||||
}
|
||||
entries = append(entries, mkEntry(header,
|
||||
fmt.Sprintf("Ternary/q=PulsarQ/density=0.50/i=%d", i),
|
||||
entropy, packU64s(dst)))
|
||||
}
|
||||
|
||||
// 3 Ternary entries at density=0.25.
|
||||
for i := 0; i < 3; i++ {
|
||||
seed := fmt.Sprintf("sample/Ternary/d=0.25/i=%d", i)
|
||||
entropy := genEntropy(seed, EntropyBytes)
|
||||
dst := make([]uint64, NumSamples)
|
||||
if err := sample.Ternary(dst, PulsarQ, 0.25, byteReader(entropy)); err != nil {
|
||||
log.Fatalf("Ternary/0.25[%d]: %v", i, err)
|
||||
}
|
||||
entries = append(entries, mkEntry(header,
|
||||
fmt.Sprintf("Ternary/q=PulsarQ/density=0.25/i=%d", i),
|
||||
entropy, packU64s(dst)))
|
||||
}
|
||||
|
||||
// 5 CenteredBinomial entries at eta=2.
|
||||
for i := 0; i < 5; i++ {
|
||||
seed := fmt.Sprintf("sample/CBD/eta=2/i=%d", i)
|
||||
entropy := genEntropy(seed, EntropyBytes)
|
||||
dst := make([]uint64, NumSamples)
|
||||
if err := sample.CenteredBinomial(dst, PulsarQ, 2, byteReader(entropy)); err != nil {
|
||||
log.Fatalf("CBD[%d]: %v", i, err)
|
||||
}
|
||||
entries = append(entries, mkEntry(header,
|
||||
fmt.Sprintf("CenteredBinomial/q=PulsarQ/eta=2/i=%d", i),
|
||||
entropy, packU64s(dst)))
|
||||
}
|
||||
|
||||
// 3 CenteredBinomial entries at eta=4.
|
||||
for i := 0; i < 3; i++ {
|
||||
seed := fmt.Sprintf("sample/CBD/eta=4/i=%d", i)
|
||||
entropy := genEntropy(seed, EntropyBytes)
|
||||
dst := make([]uint64, NumSamples)
|
||||
if err := sample.CenteredBinomial(dst, PulsarQ, 4, byteReader(entropy)); err != nil {
|
||||
log.Fatalf("CBD/eta=4[%d]: %v", i, err)
|
||||
}
|
||||
entries = append(entries, mkEntry(header,
|
||||
fmt.Sprintf("CenteredBinomial/q=PulsarQ/eta=4/i=%d", i),
|
||||
entropy, packU64s(dst)))
|
||||
}
|
||||
|
||||
bundle := &codec.KATBundle{
|
||||
Schema: codec.KATSchemaV1,
|
||||
Entries: entries,
|
||||
}
|
||||
if err := codec.WriteKATBundleFile(*out, bundle); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("wrote %d entries to %s\n", len(entries), *out)
|
||||
}
|
||||
|
||||
// genEntropy returns n bytes from a SHA-256 hash chain seeded by s.
|
||||
func genEntropy(s string, n int) []byte {
|
||||
out := make([]byte, 0, n)
|
||||
var counter uint64
|
||||
for len(out) < n {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(s))
|
||||
var ctr [8]byte
|
||||
binary.LittleEndian.PutUint64(ctr[:], counter)
|
||||
h.Write(ctr[:])
|
||||
out = append(out, h.Sum(nil)...)
|
||||
counter++
|
||||
}
|
||||
return out[:n]
|
||||
}
|
||||
|
||||
// byteReader returns a stateful io.Reader over the supplied bytes.
|
||||
// Each Read advances the internal offset.
|
||||
func byteReader(buf []byte) io.Reader { return &bbReader{buf: buf} }
|
||||
|
||||
type bbReader struct {
|
||||
buf []byte
|
||||
off int
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func packU64s(vs []uint64) []byte {
|
||||
out := make([]byte, 8*len(vs))
|
||||
for i, v := range vs {
|
||||
binary.LittleEndian.PutUint64(out[i*8:], v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mkEntry(h params.KATHeader, name string, input, output []byte) codec.KATEntry {
|
||||
digest := sha256.Sum256(output)
|
||||
return codec.KATEntry{
|
||||
Header: h,
|
||||
Test: name,
|
||||
InputHex: codec.HexEncode(input),
|
||||
OutputHex: codec.HexEncode(output),
|
||||
OutputSHA256: codec.HexEncode(digest[:]),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2026 Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package sample
|
||||
|
||||
// LP-107 Phase 6.5 cross-runtime KAT release gate.
|
||||
//
|
||||
// sample re-uses the canonical KATEntry / KATBundle types defined in
|
||||
// luxfi/math/codec.
|
||||
|
||||
import "github.com/luxfi/math/codec"
|
||||
|
||||
// KATEntry is an alias for codec.KATEntry.
|
||||
type KATEntry = codec.KATEntry
|
||||
|
||||
// KATBundle is an alias for codec.KATBundle.
|
||||
type KATBundle = codec.KATBundle
|
||||
|
||||
// KATSchemaV1 is the canonical bundle schema string.
|
||||
const KATSchemaV1 = codec.KATSchemaV1
|
||||
|
||||
// WriteKATBundleFile writes a bundle to disk; thin pass-through.
|
||||
func WriteKATBundleFile(path string, b *KATBundle) error {
|
||||
return codec.WriteKATBundleFile(path, b)
|
||||
}
|
||||
|
||||
// ReadKATBundleFile reads a bundle from disk; thin pass-through.
|
||||
func ReadKATBundleFile(path string) (*KATBundle, error) {
|
||||
return codec.ReadKATBundleFile(path)
|
||||
}
|
||||
|
||||
// HexEncode is the canonical hex encoder shared with codec.
|
||||
func HexEncode(b []byte) string { return codec.HexEncode(b) }
|
||||
|
||||
// HexDecode is the inverse.
|
||||
func HexDecode(s string) ([]byte, error) { return codec.HexDecode(s) }
|
||||
@@ -0,0 +1,187 @@
|
||||
// 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
|
||||
}
|
||||
Vendored
+509
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user