Files
math/codec/cmd/emit_codec_kat/main.go
T
zeekay 946b9f6fd2 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

139 lines
4.3 KiB
Go

// Copyright (c) 2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
//
// emit_codec_kat โ€” produces the canonical cross-runtime KAT bundle
// for luxfi/math/codec. The C++ side at luxcpp/crypto/math/test/
// codec_cross_runtime_test.cpp reads the same JSON and asserts
// byte-equal Reader behavior on every entry.
//
// LP-107 Phase 7: Go emits โ†’ C++ verifies. This is the first
// cross-runtime release gate for the math substrate.
//
// Usage:
//
// go run ./cmd/emit_codec_kat --out testdata/codec_kat.json
package main
import (
"crypto/sha256"
"encoding/binary"
"flag"
"fmt"
"log"
"github.com/luxfi/math/codec"
"github.com/luxfi/math/params"
)
func main() {
out := flag.String("out", "codec_kat.json", "output JSON path")
flag.Parse()
header := params.KATHeader{
ParameterSet: "math.codec.v1",
ModulusID: params.ModPulsarQ, // any valid ID; codec is modulus-independent
BackendID: params.BackendPureGo,
HashSuiteID: params.HashBLAKE3,
ImplementationName: "luxfi/math/codec",
ImplementationVersion: "v0.2.0",
}
entries := []codec.KATEntry{}
// Entry 1: happy-path uint64 slice with 3 elements.
{
payload := []uint64{0xdeadbeef, 0xcafebabe, 0x1122334455667788}
input := codec.MakeUvarintFrame(uint64(len(payload)), payload)
// Output is the same uint64 stream emitted as little-endian bytes
// (canonical wire form). Verifier replays the input through
// ReadUint64Slice and re-serializes the result; bytes must match.
var output []byte
for _, v := range payload {
b := make([]byte, 8)
binary.LittleEndian.PutUint64(b, v)
output = append(output, b...)
}
entries = append(entries, mkEntry(header,
"ReadUint64Slice/happy-path/3-elements", input, output))
}
// Entry 2: empty slice (length 0).
{
input := codec.MakeUvarintFrame(0, nil)
entries = append(entries, mkEntry(header,
"ReadUint64Slice/empty", input, nil))
}
// Entry 3: huge length attack input (regression for lattice issue #4).
// The Reader MUST reject this; verifier records the rejection
// distinctly via Output = "REJECTED" sentinel.
{
input := codec.MakeUvarintFrame(70_368_955_777_453, nil)
entries = append(entries, mkRejectEntry(header,
"ReadUint64Slice/reject/lattice-issue-4-70T", input))
}
// Entry 4: uint16 happy-path with 5 elements.
{
// Manually emit varint(5) then 10 bytes of payload.
input := []byte{}
// uvarint(5):
input = append(input, 5)
// 5 little-endian uint16 values: 0x0102, 0x0304, 0x0506, 0x0708, 0x090A.
input = append(input, 0x02, 0x01, 0x04, 0x03, 0x06, 0x05, 0x08, 0x07, 0x0A, 0x09)
// Output is the values re-serialized.
output := []byte{0x02, 0x01, 0x04, 0x03, 0x06, 0x05, 0x08, 0x07, 0x0A, 0x09}
entries = append(entries, mkEntry(header,
"ReadUint16Slice/happy-path/5-elements", input, output))
}
// Entry 5: uint32 happy-path with 4 elements.
{
input := []byte{4}
// 4 little-endian uint32 values: 0x12345678, 0x87654321, 0x00112233, 0xFFEEDDCC.
input = append(input,
0x78, 0x56, 0x34, 0x12,
0x21, 0x43, 0x65, 0x87,
0x33, 0x22, 0x11, 0x00,
0xCC, 0xDD, 0xEE, 0xFF)
output := input[1:] // payload bytes
entries = append(entries, mkEntry(header,
"ReadUint32Slice/happy-path/4-elements", input, output))
}
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)
}
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[:]),
}
}
// mkRejectEntry records an input that the Reader MUST reject. The
// canonical "output" for a rejected input is the literal byte string
// "REJECTED" so cross-runtime verifiers can compare a single value.
func mkRejectEntry(h params.KATHeader, name string, input []byte) codec.KATEntry {
const sentinel = "REJECTED"
digest := sha256.Sum256([]byte(sentinel))
return codec.KATEntry{
Header: h,
Test: name,
InputHex: codec.HexEncode(input),
OutputHex: codec.HexEncode([]byte(sentinel)),
OutputSHA256: codec.HexEncode(digest[:]),
}
}