Files
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

135 lines
5.1 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.
// 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{}