Files
fhe/lazy_carry.go
Zach Kelling 1ed73f84d7 fix: restore FHE Go implementation files
Restore core FHE Go files that were removed during cleanup.
These are required for precompile/fhe to build.
2026-01-03 19:11:38 -08:00

705 lines
20 KiB
Go

// Copyright (c) 2025, Lux Industries Inc
// SPDX-License-Identifier: BSD-3-Clause
// Package fhe provides lazy carry propagation for encrypted integers.
//
// Key Innovation: Defer carry propagation to reduce expensive programmable bootstrapping (PBS).
//
// - Traditional: Propagate carry after every add (expensive PBS per limb)
// - Lazy: Accumulate carries, propagate only when needed
// - Each limb stores value in extended range [0, 2^k * max_ops)
// - Propagate when: (1) overflow imminent, (2) comparison needed, (3) explicit request
//
// For euint256 with 8 x 32-bit limbs:
// - Traditional add: 7 PBS for carry chain
// - Lazy add (up to 16 ops): 0 PBS
// - Lazy propagate: 7 PBS (amortized over 16 ops)
//
// This provides 10-16x reduction in PBS operations for arithmetic-heavy workloads.
package fhe
import (
"fmt"
)
// LazyCarryConfig holds configuration for lazy carry propagation.
type LazyCarryConfig struct {
// MaxOpsBeforePropagate is the maximum number of additions before forcing propagation.
// Higher values amortize PBS cost better but increase per-limb noise.
// Recommended: 8-16 for 128-bit security, 4-8 for 256-bit security.
MaxOpsBeforePropagate int
// OverflowMargin is the safety margin before limb overflow.
// When remaining headroom < OverflowMargin, propagation is triggered.
OverflowMargin uint64
// PropagateOnCompare forces propagation before any comparison operation.
// Must be true for correct comparison results.
PropagateOnCompare bool
}
// DefaultLazyCarryConfig returns sensible defaults for EVM workloads.
func DefaultLazyCarryConfig() LazyCarryConfig {
return LazyCarryConfig{
MaxOpsBeforePropagate: 16,
OverflowMargin: 1 << 16, // 64K safety margin
PropagateOnCompare: true,
}
}
// LimbState tracks the state of a single limb in lazy carry representation.
type LimbState struct {
// Value is the encrypted limb value.
// In lazy mode, this may exceed the normal limb range.
Value *BitCiphertext
// AccumulatedCarry tracks how many carries have accumulated.
// Used to detect when propagation is needed.
AccumulatedCarry int
// LimbBits is the nominal bit width of this limb.
LimbBits int
// ExtendedBits is the actual storage width (LimbBits + headroom for carries).
ExtendedBits int
}
// LazyCarryInteger represents an encrypted integer with lazy carry propagation.
//
// Problem Statement:
// Traditional radix arithmetic propagates carries after every operation,
// requiring O(n) PBS calls for n-limb integers. This is the dominant cost
// in FHE arithmetic.
//
// Invariants:
// - Each limb stores a value in [0, 2^ExtendedBits)
// - The true value is Sum(limb[i] * 2^(i*LimbBits)) with carries applied
// - OpsWithoutPropagate tracks operations since last normalization
// - When OpsWithoutPropagate >= config.MaxOpsBeforePropagate, propagation is required
//
// Interface:
// - Add/Sub: O(n) parallel limb operations, 0 PBS
// - Propagate: O(n) sequential PBS calls (amortized)
// - Compare: Forces propagation, then O(n) comparisons
type LazyCarryInteger struct {
// Limbs are stored LSB-first (limbs[0] is least significant).
Limbs []*LimbState
// NumLimbs is the number of limbs.
NumLimbs int
// LimbBits is the nominal bit width per limb.
LimbBits int
// OpsWithoutPropagate counts additions since last carry propagation.
OpsWithoutPropagate int
// FheType is the underlying FHE integer type.
FheType FheUintType
// Config holds the lazy carry configuration.
Config LazyCarryConfig
}
// LazyCarryEvaluator performs operations on LazyCarryInteger values.
type LazyCarryEvaluator struct {
eval *BitwiseEvaluator
config LazyCarryConfig
}
// NewLazyCarryEvaluator creates a new evaluator for lazy carry integers.
func NewLazyCarryEvaluator(eval *BitwiseEvaluator, config LazyCarryConfig) *LazyCarryEvaluator {
return &LazyCarryEvaluator{
eval: eval,
config: config,
}
}
// FromBitCiphertext converts a BitCiphertext to lazy carry representation.
//
// For euint256: Creates 8 limbs of 32 bits each.
// For euint128: Creates 4 limbs of 32 bits each.
// For smaller types: Creates appropriate limb structure.
func (lce *LazyCarryEvaluator) FromBitCiphertext(bc *BitCiphertext) (*LazyCarryInteger, error) {
totalBits := bc.NumBits()
// Determine limb structure based on type
var limbBits, numLimbs int
switch bc.Type() {
case FheUint256:
limbBits = 32
numLimbs = 8
case FheUint160:
limbBits = 32
numLimbs = 5
case FheUint128:
limbBits = 32
numLimbs = 4
case FheUint64:
limbBits = 16
numLimbs = 4
case FheUint32:
limbBits = 8
numLimbs = 4
case FheUint16:
limbBits = 8
numLimbs = 2
case FheUint8:
limbBits = 4
numLimbs = 2
case FheUint4:
limbBits = 4
numLimbs = 1
default:
return nil, fmt.Errorf("unsupported type for lazy carry: %s", bc.Type())
}
// Verify consistency
if limbBits*numLimbs != totalBits {
return nil, fmt.Errorf("limb configuration mismatch: %d limbs * %d bits != %d total",
numLimbs, limbBits, totalBits)
}
// Extended bits provides headroom for carry accumulation
// With 16 ops max, we need log2(16) = 4 extra bits
extendedBits := limbBits + 4
// Create limbs by splitting the bit ciphertext
limbs := make([]*LimbState, numLimbs)
for i := 0; i < numLimbs; i++ {
startBit := i * limbBits
endBit := startBit + limbBits
// Extract bits for this limb
limbCipherBits := make([]*Ciphertext, extendedBits)
for j := 0; j < limbBits; j++ {
if startBit+j < len(bc.bits) {
limbCipherBits[j] = bc.bits[startBit+j]
} else {
// Pad with zero bits for extended range
limbCipherBits[j] = lce.eval.encryptBit(false)
}
}
// Pad remaining extended bits with zeros
for j := endBit - startBit; j < extendedBits; j++ {
limbCipherBits[j] = lce.eval.encryptBit(false)
}
limbs[i] = &LimbState{
Value: &BitCiphertext{
bits: limbCipherBits,
numBits: extendedBits,
fheType: bc.Type(), // Keep original type for reference
},
AccumulatedCarry: 0,
LimbBits: limbBits,
ExtendedBits: extendedBits,
}
}
return &LazyCarryInteger{
Limbs: limbs,
NumLimbs: numLimbs,
LimbBits: limbBits,
OpsWithoutPropagate: 0,
FheType: bc.Type(),
Config: lce.config,
}, nil
}
// ToBitCiphertext converts a LazyCarryInteger back to BitCiphertext.
// This forces carry propagation if needed.
func (lce *LazyCarryEvaluator) ToBitCiphertext(lci *LazyCarryInteger) (*BitCiphertext, error) {
// Force propagation to normalize
normalized, err := lce.Propagate(lci)
if err != nil {
return nil, fmt.Errorf("propagate for output: %w", err)
}
// Collect bits from all limbs
totalBits := normalized.NumLimbs * normalized.LimbBits
bits := make([]*Ciphertext, totalBits)
for i := 0; i < normalized.NumLimbs; i++ {
for j := 0; j < normalized.LimbBits; j++ {
bits[i*normalized.LimbBits+j] = normalized.Limbs[i].Value.bits[j]
}
}
return &BitCiphertext{
bits: bits,
numBits: totalBits,
fheType: normalized.FheType,
}, nil
}
// Add performs lazy addition without carry propagation.
//
// Complexity: O(numLimbs) parallel limb additions, 0 PBS.
// Each limb addition is O(limbBits) XOR/AND operations.
//
// Precondition: a and b have compatible types and limb structures.
// Postcondition: Result accumulates values; propagation deferred until needed.
func (lce *LazyCarryEvaluator) Add(a, b *LazyCarryInteger) (*LazyCarryInteger, error) {
if a.FheType != b.FheType {
return nil, fmt.Errorf("type mismatch: %s vs %s", a.FheType, b.FheType)
}
if a.NumLimbs != b.NumLimbs {
return nil, fmt.Errorf("limb count mismatch: %d vs %d", a.NumLimbs, b.NumLimbs)
}
// Check if propagation needed before this operation
if a.OpsWithoutPropagate >= lce.config.MaxOpsBeforePropagate ||
b.OpsWithoutPropagate >= lce.config.MaxOpsBeforePropagate {
var err error
a, err = lce.Propagate(a)
if err != nil {
return nil, fmt.Errorf("propagate a: %w", err)
}
b, err = lce.Propagate(b)
if err != nil {
return nil, fmt.Errorf("propagate b: %w", err)
}
}
// Perform lazy addition: just add limb values without carry propagation
result := &LazyCarryInteger{
Limbs: make([]*LimbState, a.NumLimbs),
NumLimbs: a.NumLimbs,
LimbBits: a.LimbBits,
OpsWithoutPropagate: max(a.OpsWithoutPropagate, b.OpsWithoutPropagate) + 1,
FheType: a.FheType,
Config: lce.config,
}
// Add each limb independently (no carry propagation!)
for i := 0; i < a.NumLimbs; i++ {
limbSum, err := lce.eval.Add(a.Limbs[i].Value, b.Limbs[i].Value)
if err != nil {
return nil, fmt.Errorf("limb %d add: %w", i, err)
}
result.Limbs[i] = &LimbState{
Value: limbSum,
AccumulatedCarry: a.Limbs[i].AccumulatedCarry + b.Limbs[i].AccumulatedCarry + 1,
LimbBits: a.LimbBits,
ExtendedBits: a.Limbs[i].ExtendedBits,
}
}
return result, nil
}
// Sub performs lazy subtraction without carry propagation.
func (lce *LazyCarryEvaluator) Sub(a, b *LazyCarryInteger) (*LazyCarryInteger, error) {
if a.FheType != b.FheType {
return nil, fmt.Errorf("type mismatch: %s vs %s", a.FheType, b.FheType)
}
if a.NumLimbs != b.NumLimbs {
return nil, fmt.Errorf("limb count mismatch: %d vs %d", a.NumLimbs, b.NumLimbs)
}
// Subtraction requires propagation for correctness (borrow handling)
aNorm, err := lce.Propagate(a)
if err != nil {
return nil, fmt.Errorf("propagate a: %w", err)
}
bNorm, err := lce.Propagate(b)
if err != nil {
return nil, fmt.Errorf("propagate b: %w", err)
}
// Convert to bit representation, subtract, convert back
aBits, err := lce.ToBitCiphertext(aNorm)
if err != nil {
return nil, fmt.Errorf("a to bits: %w", err)
}
bBits, err := lce.ToBitCiphertext(bNorm)
if err != nil {
return nil, fmt.Errorf("b to bits: %w", err)
}
diffBits, err := lce.eval.Sub(aBits, bBits)
if err != nil {
return nil, fmt.Errorf("subtract: %w", err)
}
return lce.FromBitCiphertext(diffBits)
}
// Propagate forces carry propagation through all limbs.
//
// Complexity: O(numLimbs) sequential carry operations.
// Each carry operation requires PBS for the carry extraction.
//
// Postcondition: All limbs are in normalized range [0, 2^LimbBits).
func (lce *LazyCarryEvaluator) Propagate(lci *LazyCarryInteger) (*LazyCarryInteger, error) {
if lci.OpsWithoutPropagate == 0 {
// Already propagated
return lci, nil
}
result := &LazyCarryInteger{
Limbs: make([]*LimbState, lci.NumLimbs),
NumLimbs: lci.NumLimbs,
LimbBits: lci.LimbBits,
OpsWithoutPropagate: 0, // Reset counter
FheType: lci.FheType,
Config: lci.Config,
}
// Carry propagation: sequential from LSB to MSB
var carry *BitCiphertext
for i := 0; i < lci.NumLimbs; i++ {
limbValue := lci.Limbs[i].Value
// Add incoming carry from previous limb
if carry != nil {
var err error
limbValue, err = lce.eval.Add(limbValue, carry)
if err != nil {
return nil, fmt.Errorf("limb %d carry add: %w", i, err)
}
}
// Extract carry: bits above LimbBits position
// The carry is the value >> LimbBits
normalizedValue, newCarry := lce.extractCarry(limbValue, lci.LimbBits)
result.Limbs[i] = &LimbState{
Value: normalizedValue,
AccumulatedCarry: 0,
LimbBits: lci.LimbBits,
ExtendedBits: lci.Limbs[i].ExtendedBits,
}
carry = newCarry
}
// Final carry is discarded (overflow)
return result, nil
}
// extractCarry splits an extended limb into normalized value and carry.
//
// For a limb with value V in [0, 2^ExtendedBits):
// - normalizedValue = V mod 2^LimbBits (lower LimbBits)
// - carry = V >> LimbBits (upper bits, to add to next limb)
func (lce *LazyCarryEvaluator) extractCarry(limb *BitCiphertext, limbBits int) (*BitCiphertext, *BitCiphertext) {
if limb.numBits <= limbBits {
// No extended bits, no carry - return empty carry
carryBitCount := 0
if limb.numBits > limbBits {
carryBitCount = limb.numBits - limbBits
}
zeroBits := make([]*Ciphertext, carryBitCount)
for i := range zeroBits {
zeroBits[i] = lce.eval.encryptBit(false)
}
return limb, &BitCiphertext{
bits: zeroBits,
numBits: carryBitCount,
fheType: limb.fheType,
}
}
// Lower bits: normalized value
normalizedBits := make([]*Ciphertext, limbBits)
copy(normalizedBits, limb.bits[:limbBits])
normalizedValue := &BitCiphertext{
bits: normalizedBits,
numBits: limbBits,
fheType: limb.fheType,
}
// Upper bits: carry (will be added to next limb)
carryBits := make([]*Ciphertext, limb.numBits-limbBits)
copy(carryBits, limb.bits[limbBits:])
carry := &BitCiphertext{
bits: carryBits,
numBits: limb.numBits - limbBits,
fheType: limb.fheType,
}
return normalizedValue, carry
}
// NeedsPropagation returns true if carry propagation is needed.
func (lci *LazyCarryInteger) NeedsPropagation() bool {
return lci.OpsWithoutPropagate >= lci.Config.MaxOpsBeforePropagate
}
// Eq compares two lazy carry integers for equality.
// Forces propagation for correct comparison.
func (lce *LazyCarryEvaluator) Eq(a, b *LazyCarryInteger) (*Ciphertext, error) {
if lce.config.PropagateOnCompare {
var err error
a, err = lce.Propagate(a)
if err != nil {
return nil, err
}
b, err = lce.Propagate(b)
if err != nil {
return nil, err
}
}
aBits, err := lce.ToBitCiphertext(a)
if err != nil {
return nil, err
}
bBits, err := lce.ToBitCiphertext(b)
if err != nil {
return nil, err
}
return lce.eval.Eq(aBits, bBits)
}
// Lt compares a < b (unsigned).
// Forces propagation for correct comparison.
func (lce *LazyCarryEvaluator) Lt(a, b *LazyCarryInteger) (*Ciphertext, error) {
if lce.config.PropagateOnCompare {
var err error
a, err = lce.Propagate(a)
if err != nil {
return nil, err
}
b, err = lce.Propagate(b)
if err != nil {
return nil, err
}
}
aBits, err := lce.ToBitCiphertext(a)
if err != nil {
return nil, err
}
bBits, err := lce.ToBitCiphertext(b)
if err != nil {
return nil, err
}
return lce.eval.Lt(aBits, bBits)
}
// ScalarAdd adds a plaintext scalar to a lazy carry integer.
// Uses limb-wise scalar addition without carry propagation.
func (lce *LazyCarryEvaluator) ScalarAdd(a *LazyCarryInteger, scalar uint64) (*LazyCarryInteger, error) {
// Check if propagation needed
if a.OpsWithoutPropagate >= lce.config.MaxOpsBeforePropagate {
var err error
a, err = lce.Propagate(a)
if err != nil {
return nil, fmt.Errorf("propagate: %w", err)
}
}
result := &LazyCarryInteger{
Limbs: make([]*LimbState, a.NumLimbs),
NumLimbs: a.NumLimbs,
LimbBits: a.LimbBits,
OpsWithoutPropagate: a.OpsWithoutPropagate + 1,
FheType: a.FheType,
Config: a.Config,
}
limbMask := uint64((1 << a.LimbBits) - 1)
for i := 0; i < a.NumLimbs; i++ {
scalarLimb := (scalar >> (i * a.LimbBits)) & limbMask
if scalarLimb == 0 {
// No change to this limb
result.Limbs[i] = &LimbState{
Value: a.Limbs[i].Value,
AccumulatedCarry: a.Limbs[i].AccumulatedCarry,
LimbBits: a.LimbBits,
ExtendedBits: a.Limbs[i].ExtendedBits,
}
continue
}
// Add scalar to this limb
limbSum, err := lce.eval.ScalarAdd(a.Limbs[i].Value, scalarLimb)
if err != nil {
return nil, fmt.Errorf("limb %d scalar add: %w", i, err)
}
result.Limbs[i] = &LimbState{
Value: limbSum,
AccumulatedCarry: a.Limbs[i].AccumulatedCarry + 1,
LimbBits: a.LimbBits,
ExtendedBits: a.Limbs[i].ExtendedBits,
}
}
return result, nil
}
// Mul multiplies two lazy carry integers.
// Forces propagation before multiplication for correctness.
func (lce *LazyCarryEvaluator) Mul(a, b *LazyCarryInteger) (*LazyCarryInteger, error) {
// Multiplication requires normalized inputs
aNorm, err := lce.Propagate(a)
if err != nil {
return nil, fmt.Errorf("propagate a: %w", err)
}
bNorm, err := lce.Propagate(b)
if err != nil {
return nil, fmt.Errorf("propagate b: %w", err)
}
// Convert to bit representation for multiplication
aBits, err := lce.ToBitCiphertext(aNorm)
if err != nil {
return nil, fmt.Errorf("a to bits: %w", err)
}
bBits, err := lce.ToBitCiphertext(bNorm)
if err != nil {
return nil, fmt.Errorf("b to bits: %w", err)
}
// Perform multiplication
prodBits, err := lce.eval.Mul(aBits, bBits)
if err != nil {
return nil, fmt.Errorf("multiply: %w", err)
}
// Convert back to lazy carry representation
return lce.FromBitCiphertext(prodBits)
}
// Copy creates a deep copy of a LazyCarryInteger.
func (lce *LazyCarryEvaluator) Copy(lci *LazyCarryInteger) *LazyCarryInteger {
result := &LazyCarryInteger{
Limbs: make([]*LimbState, lci.NumLimbs),
NumLimbs: lci.NumLimbs,
LimbBits: lci.LimbBits,
OpsWithoutPropagate: lci.OpsWithoutPropagate,
FheType: lci.FheType,
Config: lci.Config,
}
for i := 0; i < lci.NumLimbs; i++ {
bits := make([]*Ciphertext, lci.Limbs[i].Value.numBits)
copy(bits, lci.Limbs[i].Value.bits)
result.Limbs[i] = &LimbState{
Value: &BitCiphertext{
bits: bits,
numBits: lci.Limbs[i].Value.numBits,
fheType: lci.Limbs[i].Value.fheType,
},
AccumulatedCarry: lci.Limbs[i].AccumulatedCarry,
LimbBits: lci.Limbs[i].LimbBits,
ExtendedBits: lci.Limbs[i].ExtendedBits,
}
}
return result
}
// Zero returns a lazy carry integer initialized to zero.
func (lce *LazyCarryEvaluator) Zero(fheType FheUintType) (*LazyCarryInteger, error) {
zero := lce.eval.Zero(fheType)
return lce.FromBitCiphertext(zero)
}
// =========================================================================
// EVM-Specific Optimizations
// =========================================================================
// EVMLazyCarryConfig returns configuration optimized for EVM workloads.
//
// EVM characteristics:
// - Heavy uint256 arithmetic (ADD, SUB, MUL, DIV)
// - Frequent comparisons (LT, GT, EQ)
// - State reads/writes require normalized values
//
// This configuration balances:
// - PBS amortization (higher MaxOps = fewer PBS)
// - Memory overhead (higher MaxOps = more extended bits)
// - Latency (propagation adds latency when triggered)
func EVMLazyCarryConfig() LazyCarryConfig {
return LazyCarryConfig{
MaxOpsBeforePropagate: 12, // Tuned for typical EVM ADD sequences
OverflowMargin: 1 << 20,
PropagateOnCompare: true,
}
}
// BatchAdd performs multiple additions with minimal propagation.
//
// For n additions, this performs:
// - n * O(numLimbs) parallel limb additions
// - 1 final propagation (O(numLimbs) PBS)
//
// Amortized PBS cost: 1/n per addition vs 1 per addition for traditional.
func (lce *LazyCarryEvaluator) BatchAdd(values []*LazyCarryInteger) (*LazyCarryInteger, error) {
if len(values) == 0 {
return nil, fmt.Errorf("empty batch")
}
if len(values) == 1 {
return lce.Copy(values[0]), nil
}
// Accumulate all values
result := lce.Copy(values[0])
for i := 1; i < len(values); i++ {
var err error
result, err = lce.Add(result, values[i])
if err != nil {
return nil, fmt.Errorf("batch add %d: %w", i, err)
}
}
// Force final propagation for clean output
return lce.Propagate(result)
}
// PerformanceMetrics tracks lazy carry performance statistics.
type PerformanceMetrics struct {
TotalAdditions int
PropagationCount int
PBSOperations int // Estimated PBS calls
AmortizationRatio float64
TraditionalPBSEstimate int
}
// GetMetrics returns performance metrics for a lazy carry integer.
func (lci *LazyCarryInteger) GetMetrics() PerformanceMetrics {
// Traditional: 1 PBS per limb per addition for carry chain
traditionalPBS := lci.OpsWithoutPropagate * (lci.NumLimbs - 1)
// Lazy: PBS only on propagation (numLimbs - 1 carries)
lazyPBS := 0
if lci.OpsWithoutPropagate > 0 {
lazyPBS = lci.NumLimbs - 1 // Would need this many on next propagate
}
ratio := 0.0
if traditionalPBS > 0 {
ratio = float64(lazyPBS) / float64(traditionalPBS)
}
return PerformanceMetrics{
TotalAdditions: lci.OpsWithoutPropagate,
PropagationCount: 0, // This value, not counting historical
PBSOperations: lazyPBS,
AmortizationRatio: ratio,
TraditionalPBSEstimate: traditionalPBS,
}
}
// max returns the maximum of two ints.
func max(a, b int) int {
if a > b {
return a
}
return b
}