Files
precompile/e2e/scenarios/fhe_private_auction_test.go
zeekay aadb570cbd precompile/fhe: close Mul DoS — re-derive gas at real 12M mainnet limit + activation gate
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).
2026-06-01 21:25:19 -07:00

157 lines
5.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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")
}