Files
fhe/types/ciphertext_header.go
Hanzo AI d5d2f4083d feat(types): FHECiphertextHeader + FHEPrecompileArtifact
LP-137-FHE-TYPING. New package github.com/luxfi/fhe/types wrapping every
FHE buffer with the metadata required for safe dispatch:

- FHEScheme (uint32 enum): TFHE, FHEW, CKKS, BFV, BGV.
- FHECiphertextHeader (144-byte struct): ParamsHash, KeyID, CircuitID,
  Scheme, Level, N, ModulusCount, Domain, Reserved. Digest() returns
  SHA-256 of canonical 144-byte little-endian encoding; deterministic
  across runs. MatchesContext() rejects N + Domain mismatch.
- FHEPrecompileArtifact (232-byte struct): seven 32-byte digest fields
  (ParamsHash, KeyRoot, InputCiphertextRoot, OutputCiphertextRoot,
  CircuitRoot, ThresholdTranscriptRoot, AttestationRoot) + OpCount +
  FailedCount. Digest() is the contribution to fchain_fhe_root.
  IsThreshold / IsAttested flag-helpers on the *Root fields.

Layout byte-stable with the C++ mirror at luxcpp/fhe/include/lux/fhe/types.
Cross-language byte image tests verify both struct types.

14 Go tests pass.
2026-04-27 12:47:39 -07:00

118 lines
3.9 KiB
Go

// 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
}