mirror of
https://github.com/luxfi/math.git
synced 2026-07-27 03:38:49 +00:00
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.
144 lines
3.6 KiB
Go
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
|
|
}
|