Files
precompile/fhe/dos_audit_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

90 lines
3.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.
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package fhe
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestDoSAuditTable_QuasarEditionMainnetSafety walks every row in the
// cross-precompile DoS audit table and asserts the row's declared
// SafeAt12M matches the value computed from the row's gas + wall-clock
// budget at gasLimit=12M (real C-Chain mainnet from
// ~/work/lux/genesis/configs/mainnet/cchain.json).
//
// This is the regression guard against silent gas-cost drift. If a
// precompile's gas is later lowered such that it no longer fits the
// 12M-mainnet budget, this test surfaces the divergence loudly.
func TestDoSAuditTable_QuasarEditionMainnetSafety(t *testing.T) {
for _, row := range DoSAuditTable {
got := row.IsSafeAtGasLimit(MainnetCChainGasLimit)
require.Equal(t, row.SafeAt12M, got,
"%s: declared SafeAt12M=%v but computed %v "+
"(GasPerOp=%d, PerOpMs=%d, maxOpsPerBlock=%d, wall-clock-ms=%d, budget=%d)",
row.Precompile, row.SafeAt12M, got,
row.GasPerOp, row.PerOpMs,
MainnetCChainGasLimit/row.GasPerOp,
(MainnetCChainGasLimit/row.GasPerOp)*row.PerOpMs,
BlockComputeBudgetMs)
require.True(t, got,
"row %q must remain DoS-safe at 12M mainnet gasLimit; "+
"audit drift detected", row.Precompile)
}
}
// TestDoSAuditTable_PostFeeManagerSafety verifies the 20M
// post-feeConfigManagerConfig gasLimit also keeps every row within
// the per-block compute budget.
func TestDoSAuditTable_PostFeeManagerSafety(t *testing.T) {
for _, row := range DoSAuditTable {
require.True(t, row.IsSafeAtGasLimit(PostFeeManagerGasLimit),
"row %q must remain DoS-safe at 20M post-fee-manager gasLimit",
row.Precompile)
}
}
// TestDoSAuditTable_FHEActivationStillRefuses pins the inverse: the
// FHE rows look "safe" only because GasMul ≥ MainnetCChainGasLimit
// (their per-op gas exceeds the entire block budget), which means
// zero ops fit per block. The activation gate must still refuse
// rather than ship an unusable precompile silently.
//
// This is the regression that catches someone "fixing" the audit by
// lowering GasMul to make ops actually fit — that would make the row
// genuinely unsafe at 12M, and this test would flag the regression
// before the chain stalled.
func TestDoSAuditTable_FHEActivationStillRefuses(t *testing.T) {
// The activation gate refuses when gasLimit < GasMul × MinSafeGasMulRatio.
gateThreshold := GasMul * MinSafeGasMulRatio
require.Greater(t, gateThreshold, MainnetCChainGasLimit,
"FHE activation gate must refuse at 12M mainnet; "+
"otherwise a single Mul that fits in a block stalls the chain "+
"for %d ms wall-clock", WallClockMsMulUint8)
require.Greater(t, gateThreshold, PostFeeManagerGasLimit,
"FHE activation gate must refuse at 20M post-fee-manager")
require.Greater(t, gateThreshold, ReferenceGasLimit30M,
"FHE activation gate must refuse at 30M reference")
}
// TestDoSAuditTable_RowGasMatchesDeclaration cross-checks the rows
// against the actual constants. If GasMul is changed in contract.go
// without updating the audit table, this test fails.
func TestDoSAuditTable_RowGasMatchesDeclaration(t *testing.T) {
rowsByName := map[string]DoSAuditRow{}
for _, r := range DoSAuditTable {
rowsByName[r.Precompile] = r
}
require.Equal(t, GasAdd, rowsByName["fhe/add-uint8"].GasPerOp,
"fhe/add-uint8 row gas must equal the GasAdd constant")
require.Equal(t, GasMul, rowsByName["fhe/mul-uint8"].GasPerOp,
"fhe/mul-uint8 row gas must equal the GasMul constant")
require.Equal(t, WallClockMsAddUint8, rowsByName["fhe/add-uint8"].PerOpMs,
"fhe/add-uint8 row wall-clock ms must equal the measured constant")
require.Equal(t, WallClockMsMulUint8, rowsByName["fhe/mul-uint8"].PerOpMs,
"fhe/mul-uint8 row wall-clock ms must equal the measured constant")
}