Files

118 lines
3.9 KiB
Go
Raw Permalink Normal View History

// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package types
import (
"crypto/sha256"
"encoding/binary"
lattice "github.com/luxfi/lattice/v7/types"
)
// FHECiphertextHeader wraps every FHE buffer with the metadata required for
// safe dispatch.
//
// Without this header, two ciphertexts produced under different parameter
// sets, evaluation keys, or domains can be combined silently and produce
// either undecryptable ciphertexts or an outright privacy break (key
// reuse across circuits).
//
// Layout is byte-stable across Go and C++. Field order MUST NOT change
// without updating the C++ mirror and matching layout tests.
//
// Total size: 144 bytes.
type FHECiphertextHeader struct {
// ParamsHash is the SHA-256 of the canonical encoding of the full
// parameter set (q, p, N, log_p, sigma, dnum, etc.).
// 32 bytes, offset 0.
ParamsHash [32]byte
// KeyID is the SHA-256 of the public/evaluation-key material that
// produced or will operate on this ciphertext.
// 32 bytes, offset 32.
KeyID [32]byte
// CircuitID is the SHA-256 of the circuit / policy program under
// which this ciphertext was produced. Used to bind ciphertexts to
// their authorised computation set.
// 32 bytes, offset 64.
CircuitID [32]byte
// Scheme identifies the FHE scheme.
// 4 bytes, offset 96.
Scheme FHEScheme
// Level is the modulus-switching level (multiplicative depth budget).
// 4 bytes, offset 100.
Level uint32
// N is the polynomial ring degree.
// 4 bytes, offset 104.
N uint32
// ModulusCount is the number of RNS moduli currently active.
// 4 bytes, offset 108.
ModulusCount uint32
// Domain is the polynomial domain the ciphertext data lives in.
// 1 byte, offset 112.
Domain lattice.PolyDomain
// _pad pads to the next 8-byte boundary so the trailing 32-byte
// fields stay aligned at offsets that are multiples of 8.
// 7 bytes, offset 113.
_pad [7]uint8
// Reserved provides 24 bytes for forward-compatible extension fields
// (e.g. attestation hashes, transcript pointers). Initialised to zero
// and ignored by Digest() unless promoted to a named field.
// 24 bytes, offset 120.
Reserved [24]byte
}
// canonicalEncoding returns the deterministic byte representation used for
// hashing. Field order matches struct layout; integers use little-endian.
func (h *FHECiphertextHeader) canonicalEncoding() []byte {
buf := make([]byte, 144)
copy(buf[0:32], h.ParamsHash[:])
copy(buf[32:64], h.KeyID[:])
copy(buf[64:96], h.CircuitID[:])
binary.LittleEndian.PutUint32(buf[96:100], uint32(h.Scheme))
binary.LittleEndian.PutUint32(buf[100:104], h.Level)
binary.LittleEndian.PutUint32(buf[104:108], h.N)
binary.LittleEndian.PutUint32(buf[108:112], h.ModulusCount)
buf[112] = uint8(h.Domain)
// bytes 113..119 are pad, fixed zero
copy(buf[120:144], h.Reserved[:])
return buf
}
// Digest returns the SHA-256 of the canonical encoding of the header.
//
// Determinism: same field values always yield the same digest, across
// processes and runs. This is the value bound into precompile artifacts and
// threshold transcripts.
func (h *FHECiphertextHeader) Digest() [32]byte {
return sha256.Sum256(h.canonicalEncoding())
}
// MatchesContext reports whether this ciphertext header is compatible with
// the given NTTContext, i.e. the polynomial degree and domain agree.
//
// Use this at the kernel boundary BEFORE dispatching a forward/inverse NTT
// or a pointwise multiply: if the ciphertext is in PolyDomainNTTMontgomery
// but the kernel's OutputDomain is PolyDomainNTTStandard, the dispatch is
// silently corrupting and must be rejected.
func (h *FHECiphertextHeader) MatchesContext(ctx *lattice.NTTContext) bool {
if ctx == nil {
return false
}
if h.N != ctx.N {
return false
}
// The header's Domain describes the *current* domain of the buffer.
// A kernel with InputDomain == h.Domain accepts this ciphertext.
return h.Domain == ctx.InputDomain
}