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:
zeekay
2026-06-27 17:39:49 -07:00
parent 8ef61eaeb1
commit 8c379b7b06
6 changed files with 631 additions and 208 deletions
+117 -34
View File
@@ -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
View File
@@ -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
View File
@@ -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")
}
+68
View File
@@ -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
View File
@@ -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
}
+166
View File
@@ -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")
}