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).
135 lines
5.1 KiB
Go
135 lines
5.1 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||
// See the file LICENSE for licensing terms.
|
||
|
||
package fhe
|
||
|
||
import (
|
||
"errors"
|
||
"math/big"
|
||
"testing"
|
||
|
||
"github.com/luxfi/precompile/contract"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// feeReportingChainConfig is a ChainConfig that also implements
|
||
// contract.FeeConfigReporter for activation-gate tests.
|
||
type feeReportingChainConfig struct {
|
||
limit uint64
|
||
}
|
||
|
||
func (f *feeReportingChainConfig) EffectiveGasLimit(time uint64) uint64 {
|
||
return f.limit
|
||
}
|
||
|
||
// stubBlockContext is a minimal contract.ConfigurationBlockContext
|
||
// — Configure does not call any other method on it.
|
||
type stubBlockContext struct {
|
||
number *big.Int
|
||
timestamp uint64
|
||
}
|
||
|
||
func (s *stubBlockContext) Number() *big.Int { return s.number }
|
||
func (s *stubBlockContext) Timestamp() uint64 { return s.timestamp }
|
||
|
||
// TestConfigure_RefusesAtMainnetGasLimit proves the activation gate
|
||
// refuses when the chain reports the real mainnet C-Chain gas limit
|
||
// (12_000_000). With current TFHE perf (78 s/mul) and GasMul=936M, the
|
||
// chain cannot host even one Mul per block, so the gate fires.
|
||
func TestConfigure_RefusesAtMainnetGasLimit(t *testing.T) {
|
||
cfg := &Config{}
|
||
chainCfg := &feeReportingChainConfig{limit: MainnetCChainGasLimit}
|
||
bc := &stubBlockContext{number: big.NewInt(0), timestamp: 0}
|
||
|
||
c := &configurator{}
|
||
err := c.Configure(chainCfg, cfg, nil, bc)
|
||
require.Error(t, err, "Configure must refuse at mainnet gasLimit")
|
||
require.True(t, errors.Is(err, contract.ErrFHEUnsafeGasLimit),
|
||
"refusal must be the typed contract.ErrFHEUnsafeGasLimit, got %v", err)
|
||
}
|
||
|
||
// TestConfigure_RefusesAtPostFeeManagerGasLimit proves the gate also
|
||
// refuses at the 20M post-feeConfigManagerConfig gasLimit.
|
||
func TestConfigure_RefusesAtPostFeeManagerGasLimit(t *testing.T) {
|
||
cfg := &Config{}
|
||
chainCfg := &feeReportingChainConfig{limit: PostFeeManagerGasLimit}
|
||
bc := &stubBlockContext{number: big.NewInt(0), timestamp: 0}
|
||
|
||
c := &configurator{}
|
||
err := c.Configure(chainCfg, cfg, nil, bc)
|
||
require.ErrorIs(t, err, contract.ErrFHEUnsafeGasLimit,
|
||
"Configure must refuse at 20M post-fee-manager gasLimit")
|
||
}
|
||
|
||
// TestConfigure_RefusesAt30MReferenceGasLimit proves the gate refuses
|
||
// even at the pre-2026 30M reference gasLimit. Documents that a chain
|
||
// would need a gas limit on the order of 1 B to safely host this
|
||
// precompile at current TFHE perf.
|
||
func TestConfigure_RefusesAt30MReferenceGasLimit(t *testing.T) {
|
||
cfg := &Config{}
|
||
chainCfg := &feeReportingChainConfig{limit: ReferenceGasLimit30M}
|
||
bc := &stubBlockContext{number: big.NewInt(0), timestamp: 0}
|
||
|
||
c := &configurator{}
|
||
err := c.Configure(chainCfg, cfg, nil, bc)
|
||
require.ErrorIs(t, err, contract.ErrFHEUnsafeGasLimit,
|
||
"Configure must refuse at 30M reference gasLimit")
|
||
}
|
||
|
||
// TestConfigure_PermitsAtSufficientGasLimit proves the gate is not
|
||
// permanently sealed: when the chain reports a gasLimit at or above
|
||
// GasMul × MinSafeGasMulRatio, Configure succeeds.
|
||
//
|
||
// This is the recovery path for operators once gasLimit is raised via
|
||
// feeConfigManagerConfig admin OR once a faster TFHE evaluator lands.
|
||
func TestConfigure_PermitsAtSufficientGasLimit(t *testing.T) {
|
||
cfg := &Config{}
|
||
chainCfg := &feeReportingChainConfig{limit: GasMul * MinSafeGasMulRatio}
|
||
bc := &stubBlockContext{number: big.NewInt(0), timestamp: 0}
|
||
|
||
c := &configurator{}
|
||
require.NoError(t, c.Configure(chainCfg, cfg, nil, bc),
|
||
"Configure must permit activation at gasLimit >= GasMul × MinSafeGasMulRatio")
|
||
}
|
||
|
||
// TestConfigure_PermissiveWithoutFeeConfigReporter proves cross-chain
|
||
// compatibility: a ChainConfig that doesn't implement FeeConfigReporter
|
||
// (e.g. a non-Lux chain integrating Lux precompiles) is permissive,
|
||
// matching the StrictPQ pattern.
|
||
func TestConfigure_PermissiveWithoutFeeConfigReporter(t *testing.T) {
|
||
cfg := &Config{}
|
||
var chainCfg nonReportingChainConfig // does NOT implement FeeConfigReporter
|
||
bc := &stubBlockContext{number: big.NewInt(0), timestamp: 0}
|
||
|
||
c := &configurator{}
|
||
require.NoError(t, c.Configure(&chainCfg, cfg, nil, bc),
|
||
"Configure must be permissive on non-Lux chains lacking FeeConfigReporter")
|
||
}
|
||
|
||
// TestConfigure_PermissiveOnNilChainConfig proves Configure does not
|
||
// crash or accidentally engage the gate when chainConfig is nil.
|
||
func TestConfigure_PermissiveOnNilChainConfig(t *testing.T) {
|
||
cfg := &Config{}
|
||
bc := &stubBlockContext{number: big.NewInt(0), timestamp: 0}
|
||
|
||
c := &configurator{}
|
||
require.NoError(t, c.Configure(nil, cfg, nil, bc),
|
||
"Configure must accept nil chainConfig for back-compat")
|
||
}
|
||
|
||
// TestConfigure_RejectsWrongConfigType preserves the existing typed
|
||
// rejection. Important: the activation gate must come AFTER the type
|
||
// assertion so a malformed upgrade.json surfaces the structural error
|
||
// rather than the budget refusal.
|
||
func TestConfigure_RejectsWrongConfigType(t *testing.T) {
|
||
bc := &stubBlockContext{number: big.NewInt(0), timestamp: 0}
|
||
c := &configurator{}
|
||
// Pass nil config — must be rejected with type mismatch, not gate refusal.
|
||
err := c.Configure(nil, nil, nil, bc)
|
||
require.Error(t, err)
|
||
}
|
||
|
||
// nonReportingChainConfig is a ChainConfig that does NOT implement
|
||
// FeeConfigReporter, modeling a non-Lux integrator.
|
||
type nonReportingChainConfig struct{}
|