mirror of
https://github.com/luxfi/precompile.git
synced 2026-07-27 03:33:45 +00:00
fhe: close confidentiality hole — public-key-only, ACL-gated fail-closed decrypt
The precompile derived the FHE secret key from a fixed in-source seed (public secret = anyone decrypts offline) + performFHEDecrypt ignored caller (no ACL). FIX: remove the secret key entirely (keygen seed/secretKey/decryptor gone); public+bootstrap keys installed via SetNetworkKeys (F-Chain DKG); decrypt + sealOutput are now ACL-gated (owner-only) and FAIL CLOSED (ErrThresholdDecryptionRequired) — never return plaintext from a local key. Encrypt + all homomorphic compute ops (the 46 callers' safe path) unchanged (public bootstrap key). Real threshold decrypt = F-Chain via Warp (follow-up). Security tests prove: unauthorized decrypt rejected, no plaintext ever leaked.
This commit is contained in:
+117
-34
@@ -180,6 +180,21 @@ var (
|
||||
// expected match — consistent with every other precompile in the tree.
|
||||
ErrInsufficientGas = fmt.Errorf("insufficient gas for FHE operation: %w", contract.ErrOutOfGas)
|
||||
ErrInvalidCiphertext = errors.New("invalid ciphertext handle")
|
||||
|
||||
// ErrACLUnauthorized is returned when a caller requests an operation on a
|
||||
// ciphertext handle it is not authorized for (it is not the owner). The
|
||||
// caller is rejected outright — it is never handed any plaintext-shaped
|
||||
// output.
|
||||
ErrACLUnauthorized = errors.New("FHE ACL: caller not authorized for ciphertext handle")
|
||||
|
||||
// ErrThresholdDecryptionRequired is the fail-closed result of decrypt and
|
||||
// sealOutput. The precompile holds NO secret key — plaintext can be
|
||||
// recovered ONLY through the F-Chain (ThresholdVM) threshold-decryption
|
||||
// ceremony, where no single party can decrypt. That async Warp round-trip
|
||||
// (requestDecryption → threshold combine → fulfill) is not yet wired into
|
||||
// this precompile, so these ops fail closed rather than ever returning
|
||||
// plaintext from a local key. See LP-134.
|
||||
ErrThresholdDecryptionRequired = errors.New("FHE decrypt requires F-Chain threshold ceremony (no local secret key)")
|
||||
)
|
||||
|
||||
// FHEContract implements the main FHE precompile
|
||||
@@ -997,6 +1012,19 @@ func (c *FHEContract) handleAsEuint256(state contract.AccessibleState, caller co
|
||||
|
||||
// === Utility Handlers ===
|
||||
|
||||
// handleDecrypt is the EVM gateway to F-Chain threshold decryption. It is NOT
|
||||
// a local decrypt: the precompile holds no secret key. A decrypt is a gated
|
||||
// REQUEST:
|
||||
//
|
||||
// 1. ACL gate — the caller must be authorized for the handle (owner). An
|
||||
// unauthorized caller is rejected with ErrACLUnauthorized; it never
|
||||
// receives plaintext-shaped output.
|
||||
// 2. Threshold ceremony — plaintext is revealed only via the F-Chain
|
||||
// threshold-decryption protocol (requestDecryption → combine → fulfill),
|
||||
// where no single party can decrypt. That async Warp round-trip is not yet
|
||||
// wired into this precompile, so the request FAILS CLOSED here.
|
||||
//
|
||||
// Under no circumstances does this return plaintext recovered from a local key.
|
||||
func (c *FHEContract) handleDecrypt(state contract.AccessibleState, caller common.Address, data []byte, gas uint64, readOnly bool) ([]byte, uint64, error) {
|
||||
if len(data) < 32 {
|
||||
return nil, gas, ErrInvalidInput
|
||||
@@ -1006,10 +1034,16 @@ func (c *FHEContract) handleDecrypt(state contract.AccessibleState, caller commo
|
||||
}
|
||||
|
||||
handle := common.BytesToHash(data[:32])
|
||||
gasLeft := gas - GasDecryptRequest
|
||||
|
||||
result := performFHEDecrypt(state.GetStateDB(), handle, caller)
|
||||
if !isAuthorized(state.GetStateDB(), handle, caller) {
|
||||
return nil, gasLeft, ErrACLUnauthorized
|
||||
}
|
||||
|
||||
return result.Bytes(), gas - GasDecryptRequest, nil
|
||||
// Authorized — but decryption is a threshold ceremony on the F-Chain, not a
|
||||
// local operation. Fail closed until that integration lands. NEVER return
|
||||
// plaintext from a local key.
|
||||
return nil, gasLeft, ErrThresholdDecryptionRequired
|
||||
}
|
||||
|
||||
func (c *FHEContract) handleVerify(state contract.AccessibleState, caller common.Address, data []byte, gas uint64, readOnly bool) ([]byte, uint64, error) {
|
||||
@@ -1028,6 +1062,13 @@ func (c *FHEContract) handleVerify(state contract.AccessibleState, caller common
|
||||
return result.Bytes(), gas - GasEncrypt, nil
|
||||
}
|
||||
|
||||
// handleSealOutput re-encrypts (seals) a ciphertext to a caller-provided public
|
||||
// key so the caller can decrypt it off-chain. That re-encryption requires the
|
||||
// network secret key (which the precompile does NOT hold) or a threshold
|
||||
// re-encryption ceremony on the F-Chain (not yet wired). It is therefore
|
||||
// ACL-gated and fails closed — it must NEVER hand the raw network-key
|
||||
// ciphertext to the caller, which (combined with any key leak) would expose the
|
||||
// plaintext. Same confidentiality posture as decrypt.
|
||||
func (c *FHEContract) handleSealOutput(state contract.AccessibleState, caller common.Address, data []byte, gas uint64, readOnly bool) ([]byte, uint64, error) {
|
||||
if len(data) < 64 {
|
||||
return nil, gas, ErrInvalidInput
|
||||
@@ -1037,11 +1078,15 @@ func (c *FHEContract) handleSealOutput(state contract.AccessibleState, caller co
|
||||
}
|
||||
|
||||
handle := common.BytesToHash(data[:32])
|
||||
publicKey := data[32:]
|
||||
gasLeft := gas - GasEncrypt
|
||||
|
||||
result := performFHESealOutput(state.GetStateDB(), handle, publicKey, caller)
|
||||
if !isAuthorized(state.GetStateDB(), handle, caller) {
|
||||
return nil, gasLeft, ErrACLUnauthorized
|
||||
}
|
||||
|
||||
return result, gas - GasEncrypt, nil
|
||||
// Authorized — but sealing to the caller's key is a threshold
|
||||
// re-encryption performed via the F-Chain, not locally. Fail closed.
|
||||
return nil, gasLeft, ErrThresholdDecryptionRequired
|
||||
}
|
||||
|
||||
// Ciphertext storage layout in StateDB:
|
||||
@@ -1138,7 +1183,7 @@ func performFHEOperation(stateDB contract.StateDB, op string, handle1, handle2 c
|
||||
if op == "lt" || op == "gt" || op == "eq" || op == "ne" || op == "le" || op == "ge" {
|
||||
resultType = TypeEbool
|
||||
}
|
||||
return storeCiphertext(stateDB, result, resultType)
|
||||
return storeCiphertextOwned(stateDB, result, resultType, caller)
|
||||
}
|
||||
|
||||
// CPU fallback
|
||||
@@ -1186,7 +1231,7 @@ func performFHEOperation(stateDB contract.StateDB, op string, handle1, handle2 c
|
||||
resultType = TypeEbool
|
||||
}
|
||||
|
||||
return storeCiphertext(stateDB, result, resultType)
|
||||
return storeCiphertextOwned(stateDB, result, resultType, caller)
|
||||
}
|
||||
|
||||
// fheOpGPU attempts GPU-accelerated FHE operations.
|
||||
@@ -1278,7 +1323,7 @@ func performFHESelect(stateDB contract.StateDB, condition, ifTrue, ifFalse commo
|
||||
return common.Hash{}
|
||||
}
|
||||
|
||||
return storeCiphertext(stateDB, result, trueType)
|
||||
return storeCiphertextOwned(stateDB, result, trueType, caller)
|
||||
}
|
||||
|
||||
// performFHEUnaryOperation executes FHE unary operations using real TFHE library
|
||||
@@ -1302,7 +1347,7 @@ func performFHEUnaryOperation(stateDB contract.StateDB, op string, handle common
|
||||
return common.Hash{}
|
||||
}
|
||||
|
||||
return storeCiphertext(stateDB, result, ctType)
|
||||
return storeCiphertextOwned(stateDB, result, ctType, caller)
|
||||
}
|
||||
|
||||
// encryptValue encrypts a plaintext value using real TFHE library
|
||||
@@ -1311,7 +1356,7 @@ func encryptValue(stateDB contract.StateDB, value uint64, ctType uint8, caller c
|
||||
if ct == nil {
|
||||
return common.Hash{}
|
||||
}
|
||||
return storeCiphertext(stateDB, ct, ctType)
|
||||
return storeCiphertextOwned(stateDB, ct, ctType, caller)
|
||||
}
|
||||
|
||||
// encryptAddress encrypts an address using real TFHE library
|
||||
@@ -1322,7 +1367,7 @@ func encryptAddress(stateDB contract.StateDB, addr common.Address, caller common
|
||||
if ct == nil {
|
||||
return common.Hash{}
|
||||
}
|
||||
return storeCiphertext(stateDB, ct, TypeEaddress)
|
||||
return storeCiphertextOwned(stateDB, ct, TypeEaddress, caller)
|
||||
}
|
||||
|
||||
// generateEncryptedRandom generates random encrypted value using real TFHE library
|
||||
@@ -1333,7 +1378,7 @@ func generateEncryptedRandom(stateDB contract.StateDB, ctType uint8, caller comm
|
||||
if ct == nil {
|
||||
return common.Hash{}
|
||||
}
|
||||
return storeCiphertext(stateDB, ct, ctType)
|
||||
return storeCiphertextOwned(stateDB, ct, ctType, caller)
|
||||
}
|
||||
|
||||
// performFHEScalarOperation executes FHE scalar operations using real TFHE library
|
||||
@@ -1363,7 +1408,7 @@ func performFHEScalarOperation(stateDB contract.StateDB, op string, handle commo
|
||||
return common.Hash{}
|
||||
}
|
||||
|
||||
return storeCiphertext(stateDB, result, ctType)
|
||||
return storeCiphertextOwned(stateDB, result, ctType, caller)
|
||||
}
|
||||
|
||||
// performFHEShiftOperation executes FHE shift operations using real TFHE library
|
||||
@@ -1391,7 +1436,7 @@ func performFHEShiftOperation(stateDB contract.StateDB, op string, handle common
|
||||
return common.Hash{}
|
||||
}
|
||||
|
||||
return storeCiphertext(stateDB, result, ctType)
|
||||
return storeCiphertextOwned(stateDB, result, ctType, caller)
|
||||
}
|
||||
|
||||
// performFHECast executes type casting using real TFHE library
|
||||
@@ -1406,7 +1451,7 @@ func performFHECast(stateDB contract.StateDB, handle common.Hash, toType uint8,
|
||||
return common.Hash{}
|
||||
}
|
||||
|
||||
return storeCiphertext(stateDB, result, toType)
|
||||
return storeCiphertextOwned(stateDB, result, toType, caller)
|
||||
}
|
||||
|
||||
// encryptBigIntValue encrypts a big.Int value for types > 64 bits
|
||||
@@ -1415,32 +1460,70 @@ func encryptBigIntValue(stateDB contract.StateDB, value *big.Int, ctType uint8,
|
||||
if ct == nil {
|
||||
return common.Hash{}
|
||||
}
|
||||
return storeCiphertext(stateDB, ct, ctType)
|
||||
return storeCiphertextOwned(stateDB, ct, ctType, caller)
|
||||
}
|
||||
|
||||
// performFHEDecrypt decrypts a ciphertext (returns as big.Int bytes)
|
||||
func performFHEDecrypt(stateDB contract.StateDB, handle common.Hash, caller common.Address) *big.Int {
|
||||
ct, ctType, ok := getCiphertext(stateDB, handle)
|
||||
if !ok {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
|
||||
return tfheDecrypt(ct, ctType)
|
||||
}
|
||||
|
||||
// performFHEVerify verifies and stores an input ciphertext
|
||||
// performFHEVerify verifies and stores an input ciphertext, recording the
|
||||
// submitting caller as its owner.
|
||||
func performFHEVerify(stateDB contract.StateDB, inputHandle []byte, ctType uint8, caller common.Address) common.Hash {
|
||||
if !tfheVerify(inputHandle, ctType) {
|
||||
return common.Hash{}
|
||||
}
|
||||
return storeCiphertext(stateDB, inputHandle, ctType)
|
||||
return storeCiphertextOwned(stateDB, inputHandle, ctType, caller)
|
||||
}
|
||||
|
||||
// performFHESealOutput seals output for a specific public key
|
||||
func performFHESealOutput(stateDB contract.StateDB, handle common.Hash, publicKey []byte, caller common.Address) []byte {
|
||||
ct, ctType, ok := getCiphertext(stateDB, handle)
|
||||
if !ok {
|
||||
return nil
|
||||
// === Access Control List (ACL) ===
|
||||
//
|
||||
// Decryption and sealing are gated by ownership. Every ciphertext created
|
||||
// through this precompile records its creator as owner; only the owner may
|
||||
// request decryption/seal of that handle. There is no separate ACL precompile
|
||||
// in this tree, so ownership is tracked here, co-located with the ciphertext
|
||||
// storage it governs. Decrypt/seal additionally fail closed (threshold ceremony
|
||||
// required), so this gate is defense-in-depth that also pre-positions the
|
||||
// authorization check for when F-Chain threshold wiring lands.
|
||||
|
||||
// aclOwnerPrefix namespaces owner slots so they never collide with ciphertext
|
||||
// data/meta slots in the precompile's state trie.
|
||||
var aclOwnerPrefix = []byte("fhe/acl/owner/v1")
|
||||
|
||||
// ownerSlot is the deterministic state slot holding the owner of a handle.
|
||||
func ownerSlot(handle common.Hash) common.Hash {
|
||||
return keccak256Hash(aclOwnerPrefix, handle.Bytes())
|
||||
}
|
||||
|
||||
// setOwner records owner for handle on first creation. First-writer-wins: a
|
||||
// content-addressed handle cannot have its ownership reassigned by a later
|
||||
// creator producing identical ciphertext bytes.
|
||||
func setOwner(stateDB contract.StateDB, handle common.Hash, owner common.Address) {
|
||||
slot := ownerSlot(handle)
|
||||
if stateDB.GetState(ContractAddress, slot) != (common.Hash{}) {
|
||||
return
|
||||
}
|
||||
return tfheSealOutput(ct, publicKey, ctType)
|
||||
stateDB.SetState(ContractAddress, slot, common.BytesToHash(owner.Bytes()))
|
||||
}
|
||||
|
||||
// getOwner returns the recorded owner of handle, or the zero address if none.
|
||||
func getOwner(stateDB contract.StateDB, handle common.Hash) common.Address {
|
||||
return common.BytesToAddress(stateDB.GetState(ContractAddress, ownerSlot(handle)).Bytes())
|
||||
}
|
||||
|
||||
// isAuthorized reports whether caller may decrypt/seal handle. An unknown or
|
||||
// unowned handle is never authorized (fail closed).
|
||||
func isAuthorized(stateDB contract.StateDB, handle common.Hash, caller common.Address) bool {
|
||||
owner := getOwner(stateDB, handle)
|
||||
if owner == (common.Address{}) {
|
||||
return false
|
||||
}
|
||||
return owner == caller
|
||||
}
|
||||
|
||||
// storeCiphertextOwned persists a ciphertext and records its creating caller as
|
||||
// owner. Production ciphertext creation goes through this wrapper so every
|
||||
// handle is ACL-owned at creation.
|
||||
func storeCiphertextOwned(stateDB contract.StateDB, ct []byte, ctType uint8, owner common.Address) common.Hash {
|
||||
handle := storeCiphertext(stateDB, ct, ctType)
|
||||
if handle != (common.Hash{}) {
|
||||
setOwner(stateDB, handle, owner)
|
||||
}
|
||||
return handle
|
||||
}
|
||||
|
||||
+249
-159
@@ -1,12 +1,25 @@
|
||||
// Copyright (C) 2019-2024, Lux Partners Limited. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Pure Go FHE implementation using github.com/luxfi/fhe.
|
||||
// GPU acceleration available via github.com/luxfi/accel when CGO enabled.
|
||||
// FHE operations for the EVM gateway precompile, backed by github.com/luxfi/fhe.
|
||||
//
|
||||
// Confidentiality model (LP-134, threshold FHE):
|
||||
//
|
||||
// The precompile holds ONLY PUBLIC key material — the network encryption
|
||||
// public key and the bootstrap/evaluation key — both published by the
|
||||
// F-Chain (ThresholdVM) DKG. The FHE secret key is Shamir-shared across the
|
||||
// F-Chain validator set and is NEVER reconstructed by any single party, so
|
||||
// the precompile CANNOT (and MUST NOT) decrypt locally. Decryption is a
|
||||
// threshold ceremony on the F-Chain; see contract.go handleDecrypt.
|
||||
//
|
||||
// This file therefore contains NO secret key, NO decryptor, and NO in-source
|
||||
// keygen seed. Key material is installed via SetNetworkKeys (from the F-Chain
|
||||
// DKG output, loaded by Configure) and is identical on every validator. Until
|
||||
// keys are installed, every key-dependent op fails closed (returns nil) rather
|
||||
// than fabricating a key — there is no insecure default.
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"math/big"
|
||||
"sync"
|
||||
@@ -15,52 +28,93 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// Singleton TFHE components
|
||||
tfheOnce sync.Once
|
||||
evaluator *fhe.BitwiseEvaluator
|
||||
encryptor *fhe.BitwiseEncryptor
|
||||
decryptor *fhe.BitwiseDecryptor
|
||||
secretKey *fhe.SecretKey
|
||||
publicKey *fhe.PublicKey
|
||||
params fhe.Parameters
|
||||
initErr error
|
||||
// params is the public FHE parameter set. Deterministic and key-independent,
|
||||
// so it is safe to derive in-process; it must match the params the F-Chain
|
||||
// DKG used to generate the installed keys.
|
||||
params fhe.Parameters
|
||||
paramsOnce sync.Once
|
||||
paramsErr error
|
||||
|
||||
// Network PUBLIC key material, installed via SetNetworkKeys from the F-Chain
|
||||
// DKG. There is deliberately NO secret key and NO decryptor here.
|
||||
keysMu sync.RWMutex
|
||||
publicKey *fhe.PublicKey
|
||||
bootstrapKey *fhe.BootstrapKey
|
||||
encryptor *fhe.BitwisePublicEncryptor // public-key encryption only
|
||||
evaluator *fhe.BitwiseEvaluator // homomorphic compute (bootstrap key only)
|
||||
)
|
||||
|
||||
// fheKeygenSeed is the domain-separated seed for deterministic FHE keygen.
|
||||
// All validators must use the same seed to produce identical keys.
|
||||
// Changing this value invalidates all existing ciphertexts on-chain.
|
||||
var fheKeygenSeed = sha256.Sum256([]byte("LUX_FHE_KEYGEN_v1"))
|
||||
|
||||
// Initialize TFHE components with deterministic keygen for consensus.
|
||||
func initTFHE() error {
|
||||
tfheOnce.Do(func() {
|
||||
var err error
|
||||
|
||||
// Create parameters
|
||||
params, err = fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
||||
if err != nil {
|
||||
initErr = err
|
||||
return
|
||||
}
|
||||
|
||||
// Generate keys deterministically from a fixed seed so every
|
||||
// validator produces identical FHE keys. Using crypto/rand here
|
||||
// would make each node's keys different, breaking consensus.
|
||||
kg, err := fhe.NewKeyGeneratorFromSeed(params, fheKeygenSeed[:])
|
||||
if err != nil {
|
||||
initErr = err
|
||||
return
|
||||
}
|
||||
secretKey, publicKey = kg.GenKeyPair()
|
||||
bsk := kg.GenBootstrapKey(secretKey)
|
||||
|
||||
// Create operators
|
||||
encryptor = fhe.NewBitwiseEncryptor(params, secretKey)
|
||||
decryptor = fhe.NewBitwiseDecryptor(params, secretKey)
|
||||
evaluator = fhe.NewBitwiseEvaluator(params, bsk, secretKey)
|
||||
// ensureParams initializes the public FHE parameter set exactly once.
|
||||
func ensureParams() (fhe.Parameters, error) {
|
||||
paramsOnce.Do(func() {
|
||||
params, paramsErr = fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
||||
})
|
||||
return params, paramsErr
|
||||
}
|
||||
|
||||
return initErr
|
||||
// SetNetworkKeys installs the F-Chain DKG-published PUBLIC key material:
|
||||
// the encryption public key (pk) and the bootstrap/evaluation key (bsk).
|
||||
//
|
||||
// The corresponding FHE secret key is threshold-shared on the F-Chain and is
|
||||
// NEVER held by the precompile, so installing these keys grants the ability to
|
||||
// encrypt and to homomorphically compute, but NOT to decrypt. Decryption stays
|
||||
// a threshold ceremony.
|
||||
//
|
||||
// Determinism: every validator installs the identical published bundle, so the
|
||||
// derived encryptor/evaluator behave identically across the network. Passing a
|
||||
// nil key clears the corresponding capability (fail closed).
|
||||
func SetNetworkKeys(pk *fhe.PublicKey, bsk *fhe.BootstrapKey) error {
|
||||
p, err := ensureParams()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keysMu.Lock()
|
||||
defer keysMu.Unlock()
|
||||
|
||||
publicKey = pk
|
||||
bootstrapKey = bsk
|
||||
|
||||
if pk != nil {
|
||||
encryptor = fhe.NewBitwisePublicEncryptor(p, pk)
|
||||
} else {
|
||||
encryptor = nil
|
||||
}
|
||||
if bsk != nil {
|
||||
// The evaluator needs only the bootstrap key; its sk parameter is
|
||||
// deprecated and ignored by luxfi/fhe. Pass nil — the precompile has
|
||||
// no secret key to give.
|
||||
evaluator = fhe.NewBitwiseEvaluator(p, bsk, nil)
|
||||
} else {
|
||||
evaluator = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasNetworkKeys reports whether public key material is installed. Used by the
|
||||
// VM/operator to detect an inert (fail-closed) FHE precompile.
|
||||
func HasNetworkKeys() bool {
|
||||
keysMu.RLock()
|
||||
defer keysMu.RUnlock()
|
||||
return encryptor != nil && evaluator != nil
|
||||
}
|
||||
|
||||
func getEvaluator() *fhe.BitwiseEvaluator {
|
||||
keysMu.RLock()
|
||||
defer keysMu.RUnlock()
|
||||
return evaluator
|
||||
}
|
||||
|
||||
func getEncryptor() *fhe.BitwisePublicEncryptor {
|
||||
keysMu.RLock()
|
||||
defer keysMu.RUnlock()
|
||||
return encryptor
|
||||
}
|
||||
|
||||
func getPublicKey() *fhe.PublicKey {
|
||||
keysMu.RLock()
|
||||
defer keysMu.RUnlock()
|
||||
return publicKey
|
||||
}
|
||||
|
||||
// fheTypeToTFHEType converts FHE type constant to TFHE FheUintType
|
||||
@@ -136,9 +190,14 @@ func deserializeCiphertext(data []byte) *fhe.Ciphertext {
|
||||
}
|
||||
|
||||
// FHE Operations - Binary Arithmetic
|
||||
//
|
||||
// Every compute op runs under the PUBLIC bootstrap key only; no secret key is
|
||||
// involved and no plaintext is ever exposed. When no bootstrap key is installed
|
||||
// the op fails closed (returns nil).
|
||||
|
||||
func tfheAdd(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -148,7 +207,7 @@ func tfheAdd(lhs, rhs []byte, fheType uint8) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.Add(ctLhs, ctRhs)
|
||||
result, err := ev.Add(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -157,7 +216,8 @@ func tfheAdd(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheSub(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -167,7 +227,7 @@ func tfheSub(lhs, rhs []byte, fheType uint8) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.Sub(ctLhs, ctRhs)
|
||||
result, err := ev.Sub(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -176,7 +236,8 @@ func tfheSub(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheMul(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -188,7 +249,7 @@ func tfheMul(lhs, rhs []byte, fheType uint8) []byte {
|
||||
|
||||
// TFHE multiplication using schoolbook algorithm.
|
||||
// This is O(n^2) in bootstrapping operations for n-bit integers.
|
||||
result, err := evaluator.Mul(ctLhs, ctRhs)
|
||||
result, err := ev.Mul(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -197,7 +258,8 @@ func tfheMul(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheDiv(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -208,7 +270,7 @@ func tfheDiv(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
// TFHE division using binary long division
|
||||
result, err := evaluator.Div(ctLhs, ctRhs)
|
||||
result, err := ev.Div(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -217,7 +279,8 @@ func tfheDiv(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheRem(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -228,7 +291,7 @@ func tfheRem(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
// TFHE remainder operation
|
||||
result, err := evaluator.Rem(ctLhs, ctRhs)
|
||||
result, err := ev.Rem(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -240,7 +303,8 @@ func tfheRem(lhs, rhs []byte, fheType uint8) []byte {
|
||||
// These return encrypted boolean (single encrypted bit)
|
||||
|
||||
func tfheLt(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -250,7 +314,7 @@ func tfheLt(lhs, rhs []byte, fheType uint8) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.Lt(ctLhs, ctRhs)
|
||||
result, err := ev.Lt(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -261,7 +325,8 @@ func tfheLt(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheLe(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -271,7 +336,7 @@ func tfheLe(lhs, rhs []byte, fheType uint8) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.Le(ctLhs, ctRhs)
|
||||
result, err := ev.Le(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -281,7 +346,8 @@ func tfheLe(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheGt(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -291,7 +357,7 @@ func tfheGt(lhs, rhs []byte, fheType uint8) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.Gt(ctLhs, ctRhs)
|
||||
result, err := ev.Gt(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -301,7 +367,8 @@ func tfheGt(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheGe(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -311,7 +378,7 @@ func tfheGe(lhs, rhs []byte, fheType uint8) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.Ge(ctLhs, ctRhs)
|
||||
result, err := ev.Ge(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -321,7 +388,8 @@ func tfheGe(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheEq(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -331,7 +399,7 @@ func tfheEq(lhs, rhs []byte, fheType uint8) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.Eq(ctLhs, ctRhs)
|
||||
result, err := ev.Eq(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -341,7 +409,8 @@ func tfheEq(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheNe(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -352,11 +421,11 @@ func tfheNe(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
// NE(a, b) = (a < b) OR (a > b)
|
||||
ltResult, err := evaluator.Lt(ctLhs, ctRhs)
|
||||
ltResult, err := ev.Lt(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
gtResult, err := evaluator.Gt(ctLhs, ctRhs)
|
||||
gtResult, err := ev.Gt(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -365,7 +434,7 @@ func tfheNe(lhs, rhs []byte, fheType uint8) []byte {
|
||||
ltBits := fhe.WrapBoolCiphertext(ltResult)
|
||||
gtBits := fhe.WrapBoolCiphertext(gtResult)
|
||||
|
||||
neResult, err := evaluator.Or(ltBits, gtBits)
|
||||
neResult, err := ev.Or(ltBits, gtBits)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -377,7 +446,8 @@ func tfheNe(lhs, rhs []byte, fheType uint8) []byte {
|
||||
// FHE Operations - Bitwise
|
||||
|
||||
func tfheAnd(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -387,7 +457,7 @@ func tfheAnd(lhs, rhs []byte, fheType uint8) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.And(ctLhs, ctRhs)
|
||||
result, err := ev.And(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -396,7 +466,8 @@ func tfheAnd(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheOr(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -406,7 +477,7 @@ func tfheOr(lhs, rhs []byte, fheType uint8) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.Or(ctLhs, ctRhs)
|
||||
result, err := ev.Or(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -415,7 +486,8 @@ func tfheOr(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheXor(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -425,7 +497,7 @@ func tfheXor(lhs, rhs []byte, fheType uint8) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.Xor(ctLhs, ctRhs)
|
||||
result, err := ev.Xor(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -434,7 +506,8 @@ func tfheXor(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheNot(ct []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -444,12 +517,13 @@ func tfheNot(ct []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
// Not returns *BitCiphertext directly (no error)
|
||||
result := evaluator.Not(ctIn)
|
||||
result := ev.Not(ctIn)
|
||||
return serializeBitCiphertext(result)
|
||||
}
|
||||
|
||||
func tfheNeg(ct []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -459,7 +533,7 @@ func tfheNeg(ct []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
// Negation: negate the value (two's complement)
|
||||
result, err := evaluator.Neg(ctIn)
|
||||
result, err := ev.Neg(ctIn)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -470,7 +544,8 @@ func tfheNeg(ct []byte, fheType uint8) []byte {
|
||||
// FHE Operations - Selection and Cast
|
||||
|
||||
func tfheSelect(control, ifTrue, ifFalse []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -483,7 +558,7 @@ func tfheSelect(control, ifTrue, ifFalse []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
// Select: if control then ifTrue else ifFalse
|
||||
result, err := evaluator.Select(ctControl, ctTrue, ctFalse)
|
||||
result, err := ev.Select(ctControl, ctTrue, ctFalse)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -492,7 +567,8 @@ func tfheSelect(control, ifTrue, ifFalse []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheCast(ct []byte, fromType, toType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -503,7 +579,7 @@ func tfheCast(ct []byte, fromType, toType uint8) []byte {
|
||||
|
||||
targetType := fheTypeToTFHEType(toType)
|
||||
// CastTo returns *BitCiphertext directly (no error)
|
||||
result := evaluator.CastTo(ctIn, targetType)
|
||||
result := ev.CastTo(ctIn, targetType)
|
||||
|
||||
return serializeBitCiphertext(result)
|
||||
}
|
||||
@@ -511,7 +587,8 @@ func tfheCast(ct []byte, fromType, toType uint8) []byte {
|
||||
// FHE Operations - Min/Max
|
||||
|
||||
func tfheMin(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -522,12 +599,12 @@ func tfheMin(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
// Min = (lhs < rhs) ? lhs : rhs
|
||||
ltResult, err := evaluator.Lt(ctLhs, ctRhs)
|
||||
ltResult, err := ev.Lt(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.Select(ltResult, ctLhs, ctRhs)
|
||||
result, err := ev.Select(ltResult, ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -536,7 +613,8 @@ func tfheMin(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheMax(lhs, rhs []byte, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -547,12 +625,12 @@ func tfheMax(lhs, rhs []byte, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
// Max = (lhs > rhs) ? lhs : rhs
|
||||
gtResult, err := evaluator.Gt(ctLhs, ctRhs)
|
||||
gtResult, err := ev.Gt(ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.Select(gtResult, ctLhs, ctRhs)
|
||||
result, err := ev.Select(gtResult, ctLhs, ctRhs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -560,79 +638,71 @@ func tfheMax(lhs, rhs []byte, fheType uint8) []byte {
|
||||
return serializeBitCiphertext(result)
|
||||
}
|
||||
|
||||
// FHE Operations - Encryption/Decryption
|
||||
// FHE Operations - Encryption / Input handling
|
||||
//
|
||||
// Note: there is intentionally NO local decrypt and NO local seal here. Both
|
||||
// require the secret key, which the precompile does not hold; they are handled
|
||||
// as ACL-gated, fail-closed threshold requests in contract.go.
|
||||
|
||||
func tfheVerify(ct []byte, fheType uint8) bool {
|
||||
// Basic validation - check ciphertext can be deserialized
|
||||
// Basic validation - check ciphertext can be deserialized. No key needed:
|
||||
// input ciphertexts are encrypted off-chain under the network public key.
|
||||
return deserializeBitCiphertext(ct) != nil
|
||||
}
|
||||
|
||||
func tfheDecrypt(ct []byte, fheType uint8) *big.Int {
|
||||
if err := initTFHE(); err != nil {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
|
||||
ctIn := deserializeBitCiphertext(ct)
|
||||
if ctIn == nil {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
|
||||
plaintext := decryptor.DecryptUint64(ctIn)
|
||||
return new(big.Int).SetUint64(plaintext)
|
||||
}
|
||||
|
||||
// tfheTrivialEncrypt encrypts a PUBLIC plaintext (an on-chain calldata value)
|
||||
// under the network PUBLIC key. The plaintext is already public on-chain, so no
|
||||
// confidentiality is created or lost here — this only lifts a known value into
|
||||
// the ciphertext domain so it can be combined with confidential ciphertexts.
|
||||
// Fails closed (nil) when no public key is installed.
|
||||
func tfheTrivialEncrypt(plaintext *big.Int, toType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
enc := getEncryptor()
|
||||
if enc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
targetType := fheTypeToTFHEType(toType)
|
||||
// Use encryptor for now (trivial encryption would be noiseless)
|
||||
ct := encryptor.EncryptUint64(plaintext.Uint64(), targetType)
|
||||
ct, err := enc.EncryptUint64(plaintext.Uint64(), targetType)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return serializeBitCiphertext(ct)
|
||||
}
|
||||
|
||||
func tfheSealOutput(ct, pk []byte, fheType uint8) []byte {
|
||||
// Seal output for a specific public key
|
||||
// In production, this would re-encrypt under the given public key
|
||||
// For now, just return the ciphertext with a header
|
||||
result := make([]byte, len(ct)+len(pk)+8)
|
||||
binary.BigEndian.PutUint32(result[0:4], uint32(len(pk)))
|
||||
binary.BigEndian.PutUint32(result[4:8], uint32(len(ct)))
|
||||
copy(result[8:8+len(pk)], pk)
|
||||
copy(result[8+len(pk):], ct)
|
||||
return result
|
||||
}
|
||||
|
||||
func tfheRandom(fheType uint8, seed uint64) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
pk := getPublicKey()
|
||||
if pk == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p, err := ensureParams()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate random bytes based on seed
|
||||
targetType := fheTypeToTFHEType(fheType)
|
||||
|
||||
// Create deterministic seed bytes
|
||||
// Deterministic seed bytes → public-key RNG. No secret key involved.
|
||||
seedBytes := make([]byte, 32)
|
||||
binary.BigEndian.PutUint64(seedBytes[24:], seed)
|
||||
|
||||
rng := fhe.NewFheRNG(params, secretKey, seedBytes)
|
||||
ct := rng.RandomUint(targetType)
|
||||
rng := fhe.NewFheRNGPublic(p, pk, seedBytes)
|
||||
ct, err := rng.RandomUint(targetType)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return serializeBitCiphertext(ct)
|
||||
}
|
||||
|
||||
func tfheGetNetworkPublicKey() []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
pk := getPublicKey()
|
||||
if pk == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if publicKey == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := publicKey.MarshalBinary()
|
||||
data, err := pk.MarshalBinary()
|
||||
if err != nil {
|
||||
// Returning random bytes here would be a consensus violation:
|
||||
// each node would return different data for the same call.
|
||||
@@ -645,7 +715,8 @@ func tfheGetNetworkPublicKey() []byte {
|
||||
// === Shift Operations ===
|
||||
|
||||
func tfheShl(ct []byte, shift int, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -655,12 +726,13 @@ func tfheShl(ct []byte, shift int, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
// Shl returns *BitCiphertext directly
|
||||
result := evaluator.Shl(ctIn, shift)
|
||||
result := ev.Shl(ctIn, shift)
|
||||
return serializeBitCiphertext(result)
|
||||
}
|
||||
|
||||
func tfheShr(ct []byte, shift int, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -670,12 +742,13 @@ func tfheShr(ct []byte, shift int, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
// Shr returns *BitCiphertext directly
|
||||
result := evaluator.Shr(ctIn, shift)
|
||||
result := ev.Shr(ctIn, shift)
|
||||
return serializeBitCiphertext(result)
|
||||
}
|
||||
|
||||
func tfheRotl(ct []byte, shift int, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -688,10 +761,10 @@ func tfheRotl(ct []byte, shift int, fheType uint8) []byte {
|
||||
numBits := ctIn.NumBits()
|
||||
shift = shift % numBits
|
||||
|
||||
leftPart := evaluator.Shl(ctIn, shift)
|
||||
rightPart := evaluator.Shr(ctIn, numBits-shift)
|
||||
leftPart := ev.Shl(ctIn, shift)
|
||||
rightPart := ev.Shr(ctIn, numBits-shift)
|
||||
|
||||
result, err := evaluator.Or(leftPart, rightPart)
|
||||
result, err := ev.Or(leftPart, rightPart)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -700,7 +773,8 @@ func tfheRotl(ct []byte, shift int, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheRotr(ct []byte, shift int, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -713,10 +787,10 @@ func tfheRotr(ct []byte, shift int, fheType uint8) []byte {
|
||||
numBits := ctIn.NumBits()
|
||||
shift = shift % numBits
|
||||
|
||||
rightPart := evaluator.Shr(ctIn, shift)
|
||||
leftPart := evaluator.Shl(ctIn, numBits-shift)
|
||||
rightPart := ev.Shr(ctIn, shift)
|
||||
leftPart := ev.Shl(ctIn, numBits-shift)
|
||||
|
||||
result, err := evaluator.Or(leftPart, rightPart)
|
||||
result, err := ev.Or(leftPart, rightPart)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -727,7 +801,8 @@ func tfheRotr(ct []byte, shift int, fheType uint8) []byte {
|
||||
// === Scalar Operations ===
|
||||
|
||||
func tfheScalarAdd(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -736,7 +811,7 @@ func tfheScalarAdd(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.ScalarAdd(ctIn, scalar)
|
||||
result, err := ev.ScalarAdd(ctIn, scalar)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -745,7 +820,8 @@ func tfheScalarAdd(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheScalarSub(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -760,7 +836,7 @@ func tfheScalarSub(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
mask := uint64((1 << numBits) - 1)
|
||||
negScalar := (^scalar + 1) & mask
|
||||
|
||||
result, err := evaluator.ScalarAdd(ctIn, negScalar)
|
||||
result, err := ev.ScalarAdd(ctIn, negScalar)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -769,7 +845,8 @@ func tfheScalarSub(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheScalarMul(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -778,7 +855,7 @@ func tfheScalarMul(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.ScalarMul(ctIn, scalar)
|
||||
result, err := ev.ScalarMul(ctIn, scalar)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -787,7 +864,9 @@ func tfheScalarMul(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheScalarDiv(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
enc := getEncryptor()
|
||||
if ev == nil || enc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -801,11 +880,15 @@ func tfheScalarDiv(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
// For scalar division, encrypt the scalar and use encrypted division
|
||||
// For scalar division, encrypt the scalar (a public value) under the
|
||||
// network public key and use encrypted division.
|
||||
targetType := fheTypeToTFHEType(fheType)
|
||||
ctScalar := encryptor.EncryptUint64(scalar, targetType)
|
||||
ctScalar, err := enc.EncryptUint64(scalar, targetType)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.Div(ctIn, ctScalar)
|
||||
result, err := ev.Div(ctIn, ctScalar)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -814,7 +897,9 @@ func tfheScalarDiv(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
}
|
||||
|
||||
func tfheScalarRem(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
enc := getEncryptor()
|
||||
if ev == nil || enc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -828,11 +913,15 @@ func tfheScalarRem(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
// For scalar remainder, encrypt the scalar and use encrypted rem
|
||||
// For scalar remainder, encrypt the scalar (a public value) under the
|
||||
// network public key and use encrypted rem.
|
||||
targetType := fheTypeToTFHEType(fheType)
|
||||
ctScalar := encryptor.EncryptUint64(scalar, targetType)
|
||||
ctScalar, err := enc.EncryptUint64(scalar, targetType)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := evaluator.Rem(ctIn, ctScalar)
|
||||
result, err := ev.Rem(ctIn, ctScalar)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -842,12 +931,13 @@ func tfheScalarRem(ct []byte, scalar uint64, fheType uint8) []byte {
|
||||
|
||||
// tfheMaxValue returns an encrypted max value (all bits set)
|
||||
func tfheMaxValue(fheType uint8) []byte {
|
||||
if err := initTFHE(); err != nil {
|
||||
ev := getEvaluator()
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
targetType := fheTypeToTFHEType(fheType)
|
||||
maxVal := evaluator.MaxValue(targetType)
|
||||
maxVal := ev.MaxValue(targetType)
|
||||
return serializeBitCiphertext(maxVal)
|
||||
}
|
||||
|
||||
|
||||
+17
-14
@@ -68,15 +68,17 @@ func (s *testStateDB) RevertToSnapshot(int) {}
|
||||
|
||||
var _ contract.StateDB = (*testStateDB)(nil)
|
||||
|
||||
// TestTFHEInitialization tests that the TFHE components initialize correctly
|
||||
// TestTFHEInitialization tests that the precompile installs PUBLIC key material
|
||||
// only. There is deliberately no secret key and no decryptor in the precompile;
|
||||
// decryption is an F-Chain threshold ceremony.
|
||||
func TestTFHEInitialization(t *testing.T) {
|
||||
err := initTFHE()
|
||||
require.NoError(t, err, "TFHE initialization should succeed")
|
||||
require.NotNil(t, evaluator, "evaluator should be initialized")
|
||||
require.NotNil(t, encryptor, "encryptor should be initialized")
|
||||
require.NotNil(t, decryptor, "decryptor should be initialized")
|
||||
require.NotNil(t, secretKey, "secretKey should be initialized")
|
||||
require.NotNil(t, publicKey, "publicKey should be initialized")
|
||||
require.NoError(t, err, "network key installation should succeed")
|
||||
require.NotNil(t, evaluator, "evaluator (bootstrap key) should be installed")
|
||||
require.NotNil(t, encryptor, "public-key encryptor should be installed")
|
||||
require.NotNil(t, publicKey, "network public key should be installed")
|
||||
require.NotNil(t, bootstrapKey, "network bootstrap key should be installed")
|
||||
require.True(t, HasNetworkKeys(), "precompile should report installed keys")
|
||||
}
|
||||
|
||||
// TestFheTypeMapping tests FHE type constant to TFHE type mapping
|
||||
@@ -695,18 +697,19 @@ func TestFHEVerify(t *testing.T) {
|
||||
require.False(t, invalid)
|
||||
}
|
||||
|
||||
// TestDeterministicKeygen verifies that initTFHE produces identical keys
|
||||
// on repeated invocations (via the fixed seed), which is the core consensus fix.
|
||||
func TestDeterministicKeygen(t *testing.T) {
|
||||
// initTFHE uses sync.Once, so we can only verify the keys are non-nil
|
||||
// and that the public key serializes deterministically.
|
||||
// TestNetworkPublicKeyStable verifies the precompile publishes a stable network
|
||||
// PUBLIC key (so all validators encrypt under the same key) and that the key is
|
||||
// the public half only — the secret key lives off-precompile, on the F-Chain
|
||||
// threshold set.
|
||||
func TestNetworkPublicKeyStable(t *testing.T) {
|
||||
err := initTFHE()
|
||||
require.NoError(t, err)
|
||||
|
||||
pk1 := tfheGetNetworkPublicKey()
|
||||
require.NotNil(t, pk1)
|
||||
require.Greater(t, len(pk1), 0)
|
||||
|
||||
// Second call must return identical bytes (same singleton).
|
||||
// Repeated reads return identical bytes (same installed public key).
|
||||
pk2 := tfheGetNetworkPublicKey()
|
||||
require.Equal(t, pk1, pk2, "public key must be deterministic across calls")
|
||||
require.Equal(t, pk1, pk2, "published network public key must be stable across calls")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
//go:build cgo
|
||||
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/fhe"
|
||||
)
|
||||
|
||||
// Test-only threshold-FHE simulation.
|
||||
//
|
||||
// In production the precompile holds ONLY public key material (installed via
|
||||
// SetNetworkKeys) and CANNOT decrypt — decryption is an F-Chain threshold
|
||||
// ceremony where no single party holds the secret key. To unit-test the
|
||||
// encrypt/compute path end to end, the test harness stands in for the DKG: it
|
||||
// generates a throwaway keypair, installs the PUBLIC half into the precompile,
|
||||
// and keeps the SECRET half HERE (in the test process only, never in the
|
||||
// precompile) to verify results out of band — exactly what a threshold
|
||||
// decryptor does, minus the share distribution.
|
||||
//
|
||||
// initTFHE and tfheDecrypt are the shims the op-level tests call. They exist
|
||||
// ONLY in the test build. The precompile itself has no secret key, no
|
||||
// decryptor, and no keygen seed.
|
||||
|
||||
var (
|
||||
testKeyOnce sync.Once
|
||||
testDecryptor *fhe.BitwiseDecryptor // secret key lives ONLY here, in the test
|
||||
testKeyErr error
|
||||
)
|
||||
|
||||
// initTFHE installs a throwaway network keypair's PUBLIC half into the
|
||||
// precompile and stashes the SECRET half in a test-only decryptor. Idempotent.
|
||||
func initTFHE() error {
|
||||
testKeyOnce.Do(func() {
|
||||
params, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
||||
if err != nil {
|
||||
testKeyErr = err
|
||||
return
|
||||
}
|
||||
kg := fhe.NewKeyGenerator(params)
|
||||
sk, pk := kg.GenKeyPair()
|
||||
bsk := kg.GenBootstrapKey(sk)
|
||||
if err := SetNetworkKeys(pk, bsk); err != nil {
|
||||
testKeyErr = err
|
||||
return
|
||||
}
|
||||
// The secret key NEVER enters the precompile — it lives only in this
|
||||
// test-side decryptor, mirroring the F-Chain threshold decryptor.
|
||||
testDecryptor = fhe.NewBitwiseDecryptor(params, sk)
|
||||
})
|
||||
return testKeyErr
|
||||
}
|
||||
|
||||
// tfheDecrypt is the test-side (threshold-stand-in) decryptor. The precompile
|
||||
// has no decrypt capability; this uses the test-held secret key to confirm that
|
||||
// encrypt/compute produced the right plaintext.
|
||||
func tfheDecrypt(ct []byte, fheType uint8) *big.Int {
|
||||
bc := deserializeBitCiphertext(ct)
|
||||
if bc == nil || testDecryptor == nil {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
return new(big.Int).SetUint64(testDecryptor.DecryptUint64(bc))
|
||||
}
|
||||
+14
-1
@@ -88,6 +88,19 @@ func (*configurator) Configure(chainConfig precompileconfig.ChainConfig, cfg pre
|
||||
err, GasMul, MinSafeGasMulRatio, minGasLimit)
|
||||
}
|
||||
|
||||
_ = config // NetworkKeyPath and CoprocessorEndpoint are reserved for future use
|
||||
// Network FHE keys are installed in-process via SetNetworkKeys by the node's
|
||||
// VM layer, which shares the F-Chain (ThresholdVM) DKG's live public key
|
||||
// material (encryption + bootstrap keys) with this precompile. The secret
|
||||
// key is threshold-shared on the F-Chain and is never held here. Until keys
|
||||
// are installed the precompile runs key-less: encrypt/compute fail closed
|
||||
// and decrypt/seal require the F-Chain threshold ceremony — no insecure
|
||||
// default key is ever fabricated.
|
||||
//
|
||||
// Keys are passed as live objects rather than loaded from NetworkKeyPath:
|
||||
// luxfi/fhe v1.8.2 cannot serialize a BootstrapKey for cross-process
|
||||
// transport (its MarshalBinary omits the KSK field and gob-fails on the
|
||||
// blind-rotation key set). NetworkKeyPath/CoprocessorEndpoint remain
|
||||
// reserved for when that upstream serialization is complete.
|
||||
_ = config
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
//go:build cgo
|
||||
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Confidentiality regression tests for the FHE gateway precompile (LP-134).
|
||||
//
|
||||
// These tests pin the launch-safety invariant that the original code violated:
|
||||
// the precompile held a deterministic in-source secret key and returned real
|
||||
// plaintext from a local decrypt, so anyone could read anyone's ciphertexts.
|
||||
// After the fix the precompile holds NO secret key; decrypt/seal are ACL-gated
|
||||
// and fail closed (threshold ceremony required) and NEVER return plaintext from
|
||||
// a local key. Each test fails on the vulnerable code and passes on the fix.
|
||||
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// aclTestState is a minimal AccessibleState wrapping a StateDB. The FHE
|
||||
// handlers only touch GetStateDB(); the other accessors are never called on
|
||||
// the decrypt/seal paths.
|
||||
type aclTestState struct{ db contract.StateDB }
|
||||
|
||||
func (s *aclTestState) GetStateDB() contract.StateDB { return s.db }
|
||||
func (s *aclTestState) GetBlockContext() contract.BlockContext { return nil }
|
||||
func (s *aclTestState) GetConsensusContext() context.Context { return nil }
|
||||
func (s *aclTestState) GetChainConfig() precompileconfig.ChainConfig { return nil }
|
||||
func (s *aclTestState) GetPrecompileEnv() contract.PrecompileEnvironment { return nil }
|
||||
|
||||
var _ contract.AccessibleState = (*aclTestState)(nil)
|
||||
|
||||
var (
|
||||
ownerAddr = common.HexToAddress("0x00000000000000000000000000000000000000aa")
|
||||
attackerAddr = common.HexToAddress("0x00000000000000000000000000000000000000bb")
|
||||
|
||||
selDecrypt = []byte{0x12, 0x3d, 0x4c, 0x87} // decrypt(bytes32)
|
||||
selSealOutput = []byte{0x56, 0x7a, 0x11, 0x98} // sealOutput(bytes32,bytes)
|
||||
)
|
||||
|
||||
// TestDecrypt_RejectsUnauthorizedCaller proves the ACL gate: a caller that does
|
||||
// not own a handle is rejected — never handed plaintext-shaped output. On the
|
||||
// vulnerable code this returned the real plaintext (42).
|
||||
func TestDecrypt_RejectsUnauthorizedCaller(t *testing.T) {
|
||||
require.NoError(t, initTFHE())
|
||||
db := newTestStateDB()
|
||||
|
||||
handle := encryptValue(db, 42, TypeEuint8, ownerAddr)
|
||||
require.NotEqual(t, common.Hash{}, handle)
|
||||
|
||||
input := append(append([]byte{}, selDecrypt...), handle.Bytes()...)
|
||||
st := &aclTestState{db: db}
|
||||
|
||||
ret, _, err := FHEPrecompile.Run(st, attackerAddr, ContractAddress, input, GasDecryptRequest, false)
|
||||
require.ErrorIs(t, err, ErrACLUnauthorized, "unauthorized decrypt must be rejected by the ACL")
|
||||
require.Empty(t, ret, "rejected decrypt must return no bytes")
|
||||
require.NotEqual(t, big.NewInt(42).Bytes(), ret, "must never leak the plaintext")
|
||||
}
|
||||
|
||||
// TestDecrypt_OwnerFailsClosed_NeverLocalPlaintext proves that even the
|
||||
// authorized owner cannot extract plaintext locally: decryption requires the
|
||||
// F-Chain threshold ceremony, so the request fails closed. The precompile has
|
||||
// no secret key, so there is no local plaintext to return.
|
||||
func TestDecrypt_OwnerFailsClosed_NeverLocalPlaintext(t *testing.T) {
|
||||
require.NoError(t, initTFHE())
|
||||
db := newTestStateDB()
|
||||
|
||||
handle := encryptValue(db, 42, TypeEuint8, ownerAddr)
|
||||
require.NotEqual(t, common.Hash{}, handle)
|
||||
|
||||
input := append(append([]byte{}, selDecrypt...), handle.Bytes()...)
|
||||
st := &aclTestState{db: db}
|
||||
|
||||
ret, _, err := FHEPrecompile.Run(st, ownerAddr, ContractAddress, input, GasDecryptRequest, false)
|
||||
require.ErrorIs(t, err, ErrThresholdDecryptionRequired,
|
||||
"owner decrypt must fail closed pending the F-Chain threshold ceremony")
|
||||
require.Empty(t, ret, "fail-closed decrypt must return no bytes")
|
||||
require.NotEqual(t, big.NewInt(42).Bytes(), ret, "must never return local plaintext")
|
||||
}
|
||||
|
||||
// TestSealOutput_FailsClosed proves sealOutput is ACL-gated and fails closed,
|
||||
// and that it NEVER hands the raw network-key ciphertext back to the caller
|
||||
// (which on the vulnerable code it did, via a plain concat with the pubkey).
|
||||
func TestSealOutput_FailsClosed(t *testing.T) {
|
||||
require.NoError(t, initTFHE())
|
||||
db := newTestStateDB()
|
||||
|
||||
handle := encryptValue(db, 7, TypeEuint8, ownerAddr)
|
||||
require.NotEqual(t, common.Hash{}, handle)
|
||||
rawCt, _, ok := getCiphertext(db, handle)
|
||||
require.True(t, ok)
|
||||
|
||||
sealPubKey := make([]byte, 32) // caller-provided seal target key
|
||||
input := append(append(append([]byte{}, selSealOutput...), handle.Bytes()...), sealPubKey...)
|
||||
st := &aclTestState{db: db}
|
||||
|
||||
// Unauthorized caller: rejected by the ACL.
|
||||
ret, _, err := FHEPrecompile.Run(st, attackerAddr, ContractAddress, input, GasEncrypt, false)
|
||||
require.ErrorIs(t, err, ErrACLUnauthorized)
|
||||
require.Empty(t, ret)
|
||||
|
||||
// Authorized owner: still fails closed (threshold re-encryption required),
|
||||
// and crucially never returns the raw ciphertext.
|
||||
ret, _, err = FHEPrecompile.Run(st, ownerAddr, ContractAddress, input, GasEncrypt, false)
|
||||
require.ErrorIs(t, err, ErrThresholdDecryptionRequired)
|
||||
require.Empty(t, ret)
|
||||
require.NotEqual(t, rawCt, ret, "must never leak the raw network-key ciphertext")
|
||||
}
|
||||
|
||||
// TestSafePath_EncryptComputeStillWorks proves the 46 callers' safe path
|
||||
// (encrypt-with-public-key + homomorphic compute) is intact. The result is
|
||||
// verified OUT OF BAND with the test-held secret key (the threshold stand-in);
|
||||
// the precompile itself never decrypts.
|
||||
func TestSafePath_EncryptComputeStillWorks(t *testing.T) {
|
||||
require.NoError(t, initTFHE())
|
||||
db := newTestStateDB()
|
||||
|
||||
h10 := encryptValue(db, 10, TypeEuint8, ownerAddr)
|
||||
h3 := encryptValue(db, 3, TypeEuint8, ownerAddr)
|
||||
require.NotEqual(t, common.Hash{}, h10)
|
||||
require.NotEqual(t, common.Hash{}, h3)
|
||||
|
||||
sum := performFHEOperation(db, "add", h10, h3, ownerAddr)
|
||||
require.NotEqual(t, common.Hash{}, sum, "homomorphic add must succeed under public keys")
|
||||
|
||||
ct, _, ok := getCiphertext(db, sum)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, uint64(13), tfheDecrypt(ct, TypeEuint8).Uint64(),
|
||||
"encrypt+compute must produce the correct plaintext when decrypted out-of-band")
|
||||
}
|
||||
|
||||
// TestNoNetworkKeys_FailsClosed proves there is no insecure default key: with no
|
||||
// key material installed, encrypt and compute fail closed (empty handle) rather
|
||||
// than fabricating a key. Snapshots and restores the suite keyset so other
|
||||
// tests are unaffected.
|
||||
func TestNoNetworkKeys_FailsClosed(t *testing.T) {
|
||||
require.NoError(t, initTFHE())
|
||||
keysMu.RLock()
|
||||
savedPk, savedBsk := publicKey, bootstrapKey
|
||||
keysMu.RUnlock()
|
||||
defer func() { require.NoError(t, SetNetworkKeys(savedPk, savedBsk)) }()
|
||||
|
||||
require.NoError(t, SetNetworkKeys(nil, nil))
|
||||
require.False(t, HasNetworkKeys(), "precompile must report no keys after clearing")
|
||||
|
||||
db := newTestStateDB()
|
||||
require.Equal(t, common.Hash{}, encryptValue(db, 5, TypeEuint8, ownerAddr),
|
||||
"encrypt must fail closed with no public key")
|
||||
|
||||
// Even with two pre-stored ciphertexts, compute fails closed with no
|
||||
// bootstrap key.
|
||||
require.NoError(t, SetNetworkKeys(savedPk, savedBsk)) // briefly to stage inputs
|
||||
a := encryptValue(db, 1, TypeEuint8, ownerAddr)
|
||||
b := encryptValue(db, 2, TypeEuint8, ownerAddr)
|
||||
require.NoError(t, SetNetworkKeys(nil, nil))
|
||||
require.Equal(t, common.Hash{}, performFHEOperation(db, "add", a, b, ownerAddr),
|
||||
"compute must fail closed with no bootstrap key")
|
||||
}
|
||||
Reference in New Issue
Block a user