mirror of
https://github.com/luxfi/fhe.git
synced 2026-07-26 23:16:08 +00:00
705 lines
20 KiB
Go
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
|
||
|
|
}
|