mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
The pre-2026 adversarial test sized the FHE Mul gas (750_000) against a
30M block gas limit, but the real Lux primary-network C-Chain genesis
gasLimit is 12_000_000 (genesis/configs/mainnet/cchain.json line 16).
At 750k gas/Mul + 12M block limit, 16 Muls fit per tx; at ~78s per Mul
on commodity ARM (Apple M1 Max, measured 2026-06-01), one tx stalls a
validator for >20 minutes against a 2s block-target. Sustained: chain
halt at <0.4 LUX cost.
Fix is two-axis and bench-driven:
1. Re-derive every FHE Gas* constant against measured wall-clock and a
1ms-per-(12_000-gas) calibration. New GasMul = 936_000_000 (78s ×
12k); GasAdd = 180_000_000 (15s × 12k); etc. These exceed the 12M
block-gas-limit, which is the protection: zero Muls fit per block,
so no DoS surface remains.
2. Add an activation gate in fhe.module.Configure that refuses fheConfig
when the chain's effective gasLimit < GasMul × MinSafeGasMulRatio.
The gate uses a new feature-detection interface
contract.FeeConfigReporter (mirrors the StrictPQReporter pattern):
ChainConfigs that implement it engage the gate; those that don't
(cross-chain integrators) remain permissive. The error type is
contract.ErrFHEUnsafeGasLimit.
Tests:
- adversarial_test.go reworked into three explicit fixtures:
TestFHEGasCost_Mul_DoSResistance_{30M_Reference,12M_Mainnet,
20M_PostFeeManager}. Each asserts max ops/block × per-op-ms <= budget.
- module_test.go: 7 activation-gate cases (refuses at 12M/20M/30M;
permits at >= GasMul; permissive without FeeConfigReporter; nil chain
config; wrong config type).
- dos_audit.go + dos_audit_test.go: in-tree cross-precompile DoS audit
table walked by tests at both 12M and 20M gasLimit. ML-DSA, SLH-DSA,
Magnetar, Pulsar, BLS12-381 pairing, ZK Groth16, FROST, CGGMP21, VRF,
HQC, P3Q all measured safe; FHE rows priced out (the activation gate
is what surfaces them explicitly).
- bench_test.go: BenchmarkFHE{Mul,Add}_Uint8 — the canonical
measurement-derivation hooks. Re-run on each arch to regenerate
WallClockMs* constants.
- contract/feeconfig_test.go: 8 cases for RequireGasLimit (no reporter;
nil chainConfig; reporter above/at/below threshold; reporter
abstains; time-aware feeConfigManagerConfig flip; nil block context).
The e2e/fhe_private_auction test was pinning the OLD gas constants
verbatim; updated to assert the bench-derived calibration so a future
constant drift surfaces as test failure rather than silent regression.
NO CAVEAT. Either the gas is sufficient (gasLimit >= GasMul ×
MinSafeGasMulRatio) OR the precompile refuses to activate. The FHE
precompile is currently in the "refuses to activate" regime on real
mainnet C-Chain (gasLimit=12M < 936M required). Recovery is operator-
side: raise gasLimit via feeConfigManagerConfig OR land a faster TFHE
evaluator (see memory/vulkan_tfhe_pbs_fast_path.md — 20.4x landed,
need 78x to close the gap).
157 lines
5.8 KiB
Go
157 lines
5.8 KiB
Go
// fhe_private_auction_test exercises FHE encrypted computation:
|
||
//
|
||
// 1. Encrypt 5 bid values
|
||
// 2. Compare encrypted values (homomorphic gt/lt)
|
||
// 3. Find the max bid
|
||
// 4. Verify the winner
|
||
//
|
||
// Precompiles exercised: FHE (encrypt, add, sub, gt, lt, max, min).
|
||
package scenarios
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/luxfi/geth/common"
|
||
"github.com/luxfi/precompile/e2e/harness"
|
||
"github.com/luxfi/precompile/fhe"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// FHE selector constants (first 4 bytes of keccak256 of function signature)
|
||
var (
|
||
selectorEncrypt = []byte{0xfe, 0x6d, 0x8c, 0x2d} // encrypt(uint256,uint8)
|
||
selectorAdd = []byte{0x23, 0xb8, 0x72, 0xdd} // add(bytes32,bytes32)
|
||
selectorSub = []byte{0x51, 0xca, 0xb0, 0x91} // sub(bytes32,bytes32)
|
||
selectorLt = []byte{0xa9, 0x05, 0x9c, 0xbb} // lt(bytes32,bytes32)
|
||
selectorGt = []byte{0x4b, 0x64, 0xe4, 0x92} // gt(bytes32,bytes32)
|
||
selectorMax = []byte{0x6e, 0x32, 0x91, 0x28} // max(bytes32,bytes32)
|
||
selectorMin = []byte{0x7a, 0x8f, 0x63, 0xb8} // min(bytes32,bytes32)
|
||
selectorEq = []byte{0x1c, 0xf4, 0x86, 0x63} // eq(bytes32,bytes32)
|
||
selectorSelect = []byte{0x8f, 0xa2, 0x8b, 0x71} // select(bytes32,bytes32,bytes32)
|
||
)
|
||
|
||
func TestFHEPrivateAuction_EncryptAndCompare(t *testing.T) {
|
||
caller := common.HexToAddress("0xAA00000000000000000000000000000000000001")
|
||
|
||
// FHE handlers read and write ciphertext storage via the StateDB. Every
|
||
// call needs a stateful harness — calling FHEPrecompile.Run with a nil
|
||
// AccessibleState now returns ErrInvalidInput (was: nil-deref panic).
|
||
state := harness.NewMockAccessibleState()
|
||
|
||
// Step 1: Encrypt 5 bid values
|
||
bids := []uint64{100, 250, 175, 300, 50}
|
||
handles := make([][]byte, len(bids))
|
||
|
||
for i, bid := range bids {
|
||
input := make([]byte, 0, 4+32+1)
|
||
input = append(input, selectorEncrypt...)
|
||
input = append(input, harness.Uint256(bid)...)
|
||
input = append(input, fhe.TypeEuint32) // 32-bit encrypted uint
|
||
|
||
out, gas, err := harness.CallStatefulWithGas(
|
||
fhe.FHEPrecompile,
|
||
state,
|
||
caller,
|
||
fhe.ContractAddress,
|
||
input,
|
||
fhe.GasEncrypt+100_000,
|
||
false, // encrypt is a write operation
|
||
)
|
||
// FHE encrypt may return an error if the global FHE context is not
|
||
// initialized (requires network key). This is expected in unit tests.
|
||
if err != nil {
|
||
t.Logf("FHE encrypt bid[%d]=%d: %v (expected without FHE network key)", i, bid, err)
|
||
continue
|
||
}
|
||
require.Len(t, out, 32, "handle should be 32 bytes")
|
||
handles[i] = out
|
||
harness.GasReport(t, "FHE encrypt", gas)
|
||
}
|
||
|
||
// Step 2: Test homomorphic addition (only if we got real handles — without
|
||
// a network key the ciphertext-storage handles never materialize, and the
|
||
// add handler would return zero-hash for missing operands).
|
||
if handles[0] != nil && handles[1] != nil {
|
||
addInput := make([]byte, 0, 4+64)
|
||
addInput = append(addInput, selectorAdd...)
|
||
addInput = append(addInput, handles[0]...)
|
||
addInput = append(addInput, handles[1]...)
|
||
|
||
_, addGas, err := harness.CallStatefulWithGas(
|
||
fhe.FHEPrecompile,
|
||
state,
|
||
caller,
|
||
fhe.ContractAddress,
|
||
addInput,
|
||
fhe.GasAdd+100_000,
|
||
false,
|
||
)
|
||
if err != nil {
|
||
t.Logf("FHE add: %v (expected without network key)", err)
|
||
} else {
|
||
harness.GasReport(t, "FHE add", addGas)
|
||
}
|
||
}
|
||
|
||
// Step 3: Test comparison (gt)
|
||
if handles[0] != nil && handles[1] != nil {
|
||
gtInput := make([]byte, 0, 4+64)
|
||
gtInput = append(gtInput, selectorGt...)
|
||
gtInput = append(gtInput, handles[0]...)
|
||
gtInput = append(gtInput, handles[1]...)
|
||
|
||
_, gtGas, err := harness.CallStatefulWithGas(
|
||
fhe.FHEPrecompile,
|
||
state,
|
||
caller,
|
||
fhe.ContractAddress,
|
||
gtInput,
|
||
fhe.GasGt+100_000,
|
||
false,
|
||
)
|
||
if err != nil {
|
||
t.Logf("FHE gt: %v (expected without network key)", err)
|
||
} else {
|
||
harness.GasReport(t, "FHE gt", gtGas)
|
||
}
|
||
}
|
||
|
||
// Step 4: Verify gas model is correct.
|
||
//
|
||
// Gas costs were re-derived 2026-06-01 (CLOSE FHE Mul DoS audit) so
|
||
// that at the real C-Chain mainnet gasLimit (12_000_000 — not the
|
||
// historic 30M reference) no FHE op can stall a block beyond the
|
||
// per-block compute budget (1000 ms). Numbers are derived as
|
||
// (measured-ms-on-M1Max × 12_000 gas/ms calibration). See
|
||
// ~/work/lux/precompile/fhe/contract.go and dos_audit.go.
|
||
require.Equal(t, uint64(50_000), fhe.GasEncrypt, "encrypt gas (state op, no bootstrap)")
|
||
require.Equal(t, fhe.WallClockMsAddUint8*12_000, fhe.GasAdd,
|
||
"add gas must equal measured-add-ms × 12_000")
|
||
require.Equal(t, fhe.GasAdd, fhe.GasSub,
|
||
"sub gas must equal add gas (same algorithm)")
|
||
require.Equal(t, fhe.WallClockMsMulUint8*12_000, fhe.GasMul,
|
||
"mul gas must equal measured-mul-ms × 12_000")
|
||
require.Equal(t, fhe.GasAdd, fhe.GasGt, "gt has add-class cost")
|
||
require.Equal(t, fhe.GasAdd, fhe.GasLt, "lt has add-class cost")
|
||
require.Equal(t, fhe.GasAdd*2, fhe.GasMax, "max = compare + select")
|
||
require.Equal(t, fhe.GasAdd*2, fhe.GasMin, "min = compare + select")
|
||
require.Equal(t, fhe.GasAdd, fhe.GasSelect, "select = single mux")
|
||
|
||
_ = caller // used as sender context when FHE network key is available
|
||
t.Logf("FHE auction test complete (gas model verified, %d bids)", len(bids))
|
||
}
|
||
|
||
func TestFHEPrivateAuction_TypeConstants(t *testing.T) {
|
||
// Verify type encoding constants match expected values
|
||
require.Equal(t, uint8(0), fhe.TypeEbool, "ebool = 0")
|
||
require.Equal(t, uint8(1), fhe.TypeEuint4, "euint4 = 1")
|
||
require.Equal(t, uint8(2), fhe.TypeEuint8, "euint8 = 2")
|
||
require.Equal(t, uint8(3), fhe.TypeEuint16, "euint16 = 3")
|
||
require.Equal(t, uint8(4), fhe.TypeEuint32, "euint32 = 4")
|
||
require.Equal(t, uint8(5), fhe.TypeEuint64, "euint64 = 5")
|
||
require.Equal(t, uint8(6), fhe.TypeEuint128, "euint128 = 6")
|
||
require.Equal(t, uint8(7), fhe.TypeEuint160, "euint160 = 7")
|
||
require.Equal(t, uint8(8), fhe.TypeEuint256, "euint256 = 8")
|
||
require.Equal(t, fhe.TypeEuint160, fhe.TypeEaddress, "eaddress = euint160")
|
||
}
|