Files
math/codec/kat_test.go
T
Hanzo AI 1f0036829a LP-107 Phase 7: cross-runtime KAT release gate
codec/kat.go: KATEntry + KATBundle types with canonical JSON
serialization. Header field embeds params.KATHeader so every entry
carries (parameter_set, modulus_id, backend_id, hash_suite_id,
implementation, version) — required by LP-107 §"Parameter registry"
for cross-runtime equality.

codec/cmd/emit_codec_kat/main.go: emits 5 canonical entries:
  1. ReadUint64Slice/happy-path/3-elements
  2. ReadUint64Slice/empty
  3. ReadUint64Slice/reject/lattice-issue-4-70T  (regression: huge
     length must be rejected by both runtimes)
  4. ReadUint16Slice/happy-path/5-elements
  5. ReadUint32Slice/happy-path/4-elements

Each entry records:
  - InputHex: the wire-format bytes the Reader consumes
  - OutputHex: the canonical output byte stream (or "REJECTED")
  - OutputSHA256: commitment for fast cross-runtime equality

codec/kat_test.go: TestKATBundle_RoundTrip replays every bundle
entry through the Go Reader and asserts byte-equality + SHA-256
commitment match. PASS in 0.29s.

codec/testdata/codec_kat.json: the Go-emitted bundle. The C++ side
at luxcpp/crypto/math/test/codec_cross_runtime_test.cpp loads the
same file and replays every entry; in this commit, all 5 entries
PASS byte-equal between runtimes.

This closes LP-107 Phase 7 for codec/. Subsequent commits extend the
gate to modarith/, ntt/, poly/, sample/ KATs.
2026-05-04 09:43:59 -07:00

144 lines
3.6 KiB
Go

// Copyright (c) 2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package codec
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"errors"
"path/filepath"
"testing"
)
// TestKATBundle_RoundTrip — round-trip the same Go-side KAT to
// validate the bundle format itself.
func TestKATBundle_RoundTrip(t *testing.T) {
path := filepath.Join("testdata", "codec_kat.json")
bundle, err := ReadKATBundleFile(path)
if err != nil {
t.Skipf("KAT bundle not present at %s; run cmd/emit_codec_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)
}
// Decode the input and replay it through Reader. Output must
// match the recorded OutputHex byte-for-byte (or be "REJECTED"
// sentinel for entries that are expected to reject).
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
}
// Reset reader for each entry.
r, err := NewReader(bytes.NewReader(input), DefaultLimitsLatticeWire)
if err != nil {
t.Errorf("entry[%d] NewReader: %v", i, err)
continue
}
actual, rejected := replayEntry(t, r, entry.Test)
if rejected {
if string(expected) != "REJECTED" {
t.Errorf("entry[%d] %q: expected non-rejection, got reject",
i, entry.Test)
}
continue
}
// Verify SHA-256 matches.
digest := sha256.Sum256(actual)
got := HexEncode(digest[:])
if 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 routes the entry's test name to the right Reader call.
// The case list MUST mirror cmd/emit_codec_kat/main.go.
func replayEntry(t *testing.T, r *Reader, name string) ([]byte, bool) {
t.Helper()
switch {
case startsWith(name, "ReadUint64Slice/"):
out, err := r.ReadUint64Slice()
if err != nil {
if errors.Is(err, ErrLimitExceeded) {
return nil, true // rejected
}
t.Errorf("%s: %v", name, err)
return nil, false
}
return uint64sToBytes(out), false
case startsWith(name, "ReadUint16Slice/"):
out, err := r.ReadUint16Slice()
if err != nil {
if errors.Is(err, ErrLimitExceeded) {
return nil, true
}
t.Errorf("%s: %v", name, err)
return nil, false
}
return uint16sToBytes(out), false
case startsWith(name, "ReadUint32Slice/"):
out, err := r.ReadUint32Slice()
if err != nil {
if errors.Is(err, ErrLimitExceeded) {
return nil, true
}
t.Errorf("%s: %v", name, err)
return nil, false
}
return uint32sToBytes(out), false
}
t.Errorf("unknown KAT test name: %s", name)
return nil, false
}
func startsWith(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
func uint64sToBytes(s []uint64) []byte {
b := make([]byte, len(s)*8)
for i, v := range s {
binary.LittleEndian.PutUint64(b[i*8:], v)
}
return b
}
func uint32sToBytes(s []uint32) []byte {
b := make([]byte, len(s)*4)
for i, v := range s {
binary.LittleEndian.PutUint32(b[i*4:], v)
}
return b
}
func uint16sToBytes(s []uint16) []byte {
b := make([]byte, len(s)*2)
for i, v := range s {
binary.LittleEndian.PutUint16(b[i*2:], v)
}
return b
}