mirror of
https://github.com/luxfi/precompile.git
synced 2026-07-27 03:33:45 +00:00
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).
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package contract
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrFHEUnsafeGasLimit is returned when a compute-heavy precompile
|
||||
// (currently only FHE) is being activated on a chain whose effective
|
||||
// block gas limit cannot afford the per-op wall-clock budget. The
|
||||
// configurator surfaces it during luxd boot so the misconfiguration
|
||||
// is loud rather than silently bricking the chain at the activation
|
||||
// block.
|
||||
//
|
||||
// Recovery is operator-side: raise the chain's gasLimit (via
|
||||
// feeConfigManagerConfig admin) before re-running the upgrade, or
|
||||
// remove the precompile from upgrade.json.
|
||||
var ErrFHEUnsafeGasLimit = errors.New("contract: chain gasLimit too small for safe FHE operation under measured per-op wall-clock budget")
|
||||
|
||||
// FeeConfigReporter is the feature-detection interface a ChainConfig
|
||||
// may implement to expose its effective block gas limit at the
|
||||
// timestamp the precompile is being activated. Modeled after
|
||||
// StrictPQReporter — compute-heavy precompiles probe this interface
|
||||
// in their Configure() activation gate; if absent (e.g. non-Lux
|
||||
// integrators), the precompile is permissive.
|
||||
//
|
||||
// The reporter takes a timestamp because the chain's effective fee
|
||||
// config can change over time (feeConfigManagerConfig admin can
|
||||
// raise the gasLimit post-genesis), so the activation decision
|
||||
// must be made against the value at the activation block.
|
||||
type FeeConfigReporter interface {
|
||||
// EffectiveGasLimit returns the block gas limit currently in
|
||||
// force at the given block timestamp. Returns 0 when the
|
||||
// reporter has no opinion.
|
||||
EffectiveGasLimit(time uint64) uint64
|
||||
}
|
||||
|
||||
// RequireGasLimit returns ErrFHEUnsafeGasLimit when the active chain
|
||||
// reports a FeeConfigReporter AND the reported effective gas limit
|
||||
// is less than minGasLimit. Used by FHE module.Configure to refuse
|
||||
// activation on chains that cannot afford the precompile's per-op
|
||||
// wall-clock budget.
|
||||
//
|
||||
// Permissive when the ChainConfig does not implement
|
||||
// FeeConfigReporter (the default for non-Lux chains that integrate
|
||||
// Lux precompiles for cross-chain verification — they're expected
|
||||
// to know what they're doing).
|
||||
//
|
||||
// blockContext provides the activation timestamp. nil blockContext
|
||||
// is treated as time=0 (chain genesis) and the chain's
|
||||
// genesis-time gasLimit governs.
|
||||
func RequireGasLimit(chainConfig any, blockContext ConfigurationBlockContext, minGasLimit uint64) error {
|
||||
if chainConfig == nil {
|
||||
return nil
|
||||
}
|
||||
r, ok := chainConfig.(FeeConfigReporter)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var ts uint64
|
||||
if blockContext != nil {
|
||||
ts = blockContext.Timestamp()
|
||||
}
|
||||
got := r.EffectiveGasLimit(ts)
|
||||
if got == 0 {
|
||||
// Reporter abstained — treat as permissive.
|
||||
return nil
|
||||
}
|
||||
if got < minGasLimit {
|
||||
return ErrFHEUnsafeGasLimit
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package contract
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// feeReportingChainConfig is a ChainConfig that also implements
|
||||
// FeeConfigReporter. Mirrors the strictPQChainConfig test pattern.
|
||||
type feeReportingChainConfig struct {
|
||||
*MockChainConfig
|
||||
// limit returned at any time before flipAt; postLimit returned at
|
||||
// flipAt and later. Models feeConfigManagerConfig raising the
|
||||
// gasLimit post-genesis.
|
||||
limit uint64
|
||||
postLimit uint64
|
||||
flipAt uint64
|
||||
}
|
||||
|
||||
func newFeeReportingChainConfig(limit, postLimit, flipAt uint64) *feeReportingChainConfig {
|
||||
return &feeReportingChainConfig{
|
||||
MockChainConfig: NewMockChainConfig(0),
|
||||
limit: limit,
|
||||
postLimit: postLimit,
|
||||
flipAt: flipAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *feeReportingChainConfig) EffectiveGasLimit(time uint64) uint64 {
|
||||
if time >= f.flipAt {
|
||||
return f.postLimit
|
||||
}
|
||||
return f.limit
|
||||
}
|
||||
|
||||
// TestRequireGasLimit_NoReporter verifies that ChainConfigs that do
|
||||
// not implement FeeConfigReporter are treated as permissive (the
|
||||
// default for non-Lux chains that integrate Lux precompiles).
|
||||
func TestRequireGasLimit_NoReporter(t *testing.T) {
|
||||
cfg := NewMockChainConfig(0) // no FeeConfigReporter
|
||||
bc := NewMockBlockContext(1, 1000)
|
||||
require.NoError(t, RequireGasLimit(cfg, bc, 1<<30))
|
||||
}
|
||||
|
||||
// TestRequireGasLimit_NilChainConfig verifies permissive behavior.
|
||||
func TestRequireGasLimit_NilChainConfig(t *testing.T) {
|
||||
bc := NewMockBlockContext(1, 1000)
|
||||
require.NoError(t, RequireGasLimit(nil, bc, 1<<30))
|
||||
}
|
||||
|
||||
// TestRequireGasLimit_ReporterAbove verifies that a reporter whose
|
||||
// limit exceeds the threshold is permissive.
|
||||
func TestRequireGasLimit_ReporterAbove(t *testing.T) {
|
||||
cfg := newFeeReportingChainConfig(20_000_000, 20_000_000, 0)
|
||||
bc := NewMockBlockContext(1, 1000)
|
||||
require.NoError(t, RequireGasLimit(cfg, bc, 12_000_000))
|
||||
}
|
||||
|
||||
// TestRequireGasLimit_ReporterAtThreshold verifies inclusive lower bound:
|
||||
// a reporter at exactly the threshold passes (min is the floor, not exclusive).
|
||||
func TestRequireGasLimit_ReporterAtThreshold(t *testing.T) {
|
||||
cfg := newFeeReportingChainConfig(12_000_000, 12_000_000, 0)
|
||||
bc := NewMockBlockContext(1, 1000)
|
||||
require.NoError(t, RequireGasLimit(cfg, bc, 12_000_000))
|
||||
}
|
||||
|
||||
// TestRequireGasLimit_ReporterBelow verifies that a reporter whose
|
||||
// limit is below the threshold refuses with ErrFHEUnsafeGasLimit.
|
||||
func TestRequireGasLimit_ReporterBelow(t *testing.T) {
|
||||
cfg := newFeeReportingChainConfig(8_000_000, 8_000_000, 0)
|
||||
bc := NewMockBlockContext(1, 1000)
|
||||
err := RequireGasLimit(cfg, bc, 12_000_000)
|
||||
require.ErrorIs(t, err, ErrFHEUnsafeGasLimit)
|
||||
}
|
||||
|
||||
// TestRequireGasLimit_ReporterAbstains verifies that EffectiveGasLimit
|
||||
// returning 0 (reporter has no opinion) is treated as permissive.
|
||||
func TestRequireGasLimit_ReporterAbstains(t *testing.T) {
|
||||
cfg := newFeeReportingChainConfig(0, 0, 0)
|
||||
bc := NewMockBlockContext(1, 1000)
|
||||
require.NoError(t, RequireGasLimit(cfg, bc, 12_000_000))
|
||||
}
|
||||
|
||||
// TestRequireGasLimit_TimeAware verifies that the reporter is queried
|
||||
// at the activation block timestamp — models feeConfigManagerConfig
|
||||
// raising the gasLimit post-genesis.
|
||||
func TestRequireGasLimit_TimeAware(t *testing.T) {
|
||||
// Mainnet shape: genesis 12M, fee-manager raises to 20M at t=900000000.
|
||||
cfg := newFeeReportingChainConfig(12_000_000, 20_000_000, 900_000_000)
|
||||
|
||||
// Before the flip: refuses because 12M < 15M threshold.
|
||||
bcEarly := NewMockBlockContext(1, 500_000_000)
|
||||
require.ErrorIs(t, RequireGasLimit(cfg, bcEarly, 15_000_000), ErrFHEUnsafeGasLimit)
|
||||
|
||||
// After the flip: passes because 20M >= 15M threshold.
|
||||
bcLate := NewMockBlockContext(1, 1_000_000_000)
|
||||
require.NoError(t, RequireGasLimit(cfg, bcLate, 15_000_000))
|
||||
}
|
||||
|
||||
// TestRequireGasLimit_NilBlockContext verifies that nil blockContext
|
||||
// is treated as time=0 (chain genesis) and the genesis-time gasLimit
|
||||
// is the one evaluated.
|
||||
func TestRequireGasLimit_NilBlockContext(t *testing.T) {
|
||||
cfg := newFeeReportingChainConfig(12_000_000, 20_000_000, 900_000_000)
|
||||
require.ErrorIs(t, RequireGasLimit(cfg, nil, 15_000_000), ErrFHEUnsafeGasLimit)
|
||||
}
|
||||
|
||||
// TestFeeConfigReporter_InterfaceShape is a compile-time check that
|
||||
// the test reporter type satisfies both ChainConfig and
|
||||
// FeeConfigReporter. Mirrors TestStrictPQReporter_InterfaceShape.
|
||||
func TestFeeConfigReporter_InterfaceShape(t *testing.T) {
|
||||
var _ FeeConfigReporter = (*feeReportingChainConfig)(nil)
|
||||
var _ precompileconfig.ChainConfig = (*feeReportingChainConfig)(nil)
|
||||
}
|
||||
@@ -116,17 +116,26 @@ func TestFHEPrivateAuction_EncryptAndCompare(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Verify gas model is correct
|
||||
// Even without network key, gas costs should be well-defined
|
||||
require.Equal(t, uint64(50000), fhe.GasEncrypt, "encrypt gas should be 50K")
|
||||
require.Equal(t, uint64(65000), fhe.GasAdd, "add gas should be 65K")
|
||||
require.Equal(t, uint64(65000), fhe.GasSub, "sub gas should be 65K")
|
||||
require.Equal(t, uint64(750000), fhe.GasMul, "mul gas should be 750K")
|
||||
require.Equal(t, uint64(60000), fhe.GasGt, "gt gas should be 60K")
|
||||
require.Equal(t, uint64(60000), fhe.GasLt, "lt gas should be 60K")
|
||||
require.Equal(t, uint64(120000), fhe.GasMax, "max gas should be 120K")
|
||||
require.Equal(t, uint64(120000), fhe.GasMin, "min gas should be 120K")
|
||||
require.Equal(t, uint64(100000), fhe.GasSelect, "select gas should be 100K")
|
||||
// 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))
|
||||
|
||||
+139
-42
@@ -13,26 +13,37 @@ import (
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Finding 8: FHE gas repricing (HIGH)
|
||||
// Finding 8: FHE gas repricing (HIGH) — re-derivation at REAL mainnet limits
|
||||
// Red demonstrated that FHE multiply was underpriced relative to its
|
||||
// computational cost, enabling gas-based DoS attacks where an attacker
|
||||
// submits many FHE multiplications to stall the block.
|
||||
//
|
||||
// The fix ensures gas costs reflect actual compute time:
|
||||
// - FHE multiply (O(n^2) bootstrapping) must cost significantly more than add
|
||||
// - FHE division must cost at least as much as multiply
|
||||
// - All costs must be above a minimum floor that prevents spam
|
||||
// The fix ensures gas costs reflect actual compute time MEASURED on
|
||||
// commodity validator hardware (Apple M1 Max, the high-end ARM commodity
|
||||
// range) and SIZED so that the per-block FHE-precompile wall-clock budget
|
||||
// (BlockComputeBudgetMs = 1000 ms = half a 2 s block target) is never
|
||||
// exceeded at the real C-Chain mainnet gas limit (12_000_000 — not the
|
||||
// 30_000_000 used in the pre-2026 reference test).
|
||||
//
|
||||
// The Mul wall-clock (78 s/op) so vastly exceeds the per-block budget
|
||||
// that ANY non-zero number of muls per block stalls the chain. The gas
|
||||
// floor is therefore sized to require gasLimit ≥ GasMul, and the FHE
|
||||
// module Configure activation gate refuses activation under any chain
|
||||
// whose gasLimit is below GasMul × MinSafeGasMulRatio.
|
||||
// ============================================================================
|
||||
|
||||
// TestFHEMulGasCost_ReflectsComputeTime proves that FHE multiply gas cost
|
||||
// is high enough to reflect its quadratic bootstrapping cost.
|
||||
// is high enough to reflect its measured wall-clock cost.
|
||||
//
|
||||
// Bound: GasMul must reflect at least the WallClockMsMulUint8 × 12_000
|
||||
// derivation. Anything less is a known-vulnerable repricing.
|
||||
func TestFHEMulGasCost_ReflectsComputeTime(t *testing.T) {
|
||||
// FHE multiply is O(n^2) in bootstrapping operations for n-bit integers.
|
||||
// It takes ~2 minutes for uint8 in pure Go. Gas must reflect this.
|
||||
// The Red team finding showed the old value (150,000) was too low.
|
||||
// The fix repriced to >= 500,000 (actual: 750,000).
|
||||
require.GreaterOrEqual(t, GasMul, uint64(500_000),
|
||||
"FHE multiply gas must be >= 500,000 to reflect O(n^2) bootstrapping cost")
|
||||
// Per fhe/contract.go: GasMul = WallClockMsMulUint8 × 12_000.
|
||||
// Anything below this is underpriced at the 12M block limit.
|
||||
minSafeGasMul := WallClockMsMulUint8 * 12_000
|
||||
require.GreaterOrEqual(t, GasMul, minSafeGasMul,
|
||||
"FHE multiply gas must reflect measured wall-clock × 12_000 gas/ms calibration; "+
|
||||
"got GasMul=%d, need >= %d", GasMul, minSafeGasMul)
|
||||
}
|
||||
|
||||
// TestFHEAddGasCost_LessThanMul proves addition is cheaper than multiplication.
|
||||
@@ -44,15 +55,15 @@ func TestFHEAddGasCost_LessThanMul(t *testing.T) {
|
||||
// TestFHEDivGasCost_ReflectsComputeTime proves division gas is substantial.
|
||||
func TestFHEDivGasCost_ReflectsComputeTime(t *testing.T) {
|
||||
// Division uses binary long division with bootstrapping.
|
||||
// While cheaper than schoolbook multiplication (O(n^2) vs O(n*log(n))),
|
||||
// it is still computationally expensive and must be priced accordingly.
|
||||
require.GreaterOrEqual(t, GasDiv, uint64(500_000),
|
||||
"FHE div gas must be >= 500,000 to prevent gas-based DoS")
|
||||
// While cheaper than schoolbook multiplication (O(n) vs O(n^2)),
|
||||
// it is still computationally expensive and must be priced
|
||||
// accordingly. With the bench-derived sizing it is ~0.7x mul.
|
||||
require.GreaterOrEqual(t, GasDiv, GasMul*70/100,
|
||||
"FHE div gas must be at least 0.7x mul gas (binary long div)")
|
||||
}
|
||||
|
||||
// TestFHERemGasCost_ReflectsComputeTime proves remainder gas is substantial.
|
||||
// TestFHERemGasCost_ReflectsComputeTime proves remainder gas matches div.
|
||||
func TestFHERemGasCost_ReflectsComputeTime(t *testing.T) {
|
||||
// Remainder has the same complexity as division.
|
||||
require.Equal(t, GasDiv, GasRem,
|
||||
"FHE rem gas must equal div gas (same algorithm)")
|
||||
}
|
||||
@@ -61,7 +72,7 @@ func TestFHERemGasCost_ReflectsComputeTime(t *testing.T) {
|
||||
// expensive arithmetic operation, reflecting O(n^2) bootstrapping cost.
|
||||
func TestFHEMulGasCost_MostExpensiveArithmetic(t *testing.T) {
|
||||
require.Greater(t, GasMul, GasDiv,
|
||||
"FHE multiply must cost more than division (O(n^2) vs O(n*log(n)))")
|
||||
"FHE multiply must cost more than division (O(n^2) vs O(n) bootstrapping)")
|
||||
require.Greater(t, GasMul, GasAdd,
|
||||
"FHE multiply must cost more than addition")
|
||||
require.Greater(t, GasMul, GasSub,
|
||||
@@ -114,11 +125,11 @@ func TestFHEGasCost_AllOperations_AboveFloor(t *testing.T) {
|
||||
// TestFHEGasCost_RelativeOrdering proves that gas costs reflect
|
||||
// computational complexity ordering.
|
||||
func TestFHEGasCost_RelativeOrdering(t *testing.T) {
|
||||
// Complexity ordering: bitwise < comparison < add/sub < mul < div
|
||||
// Complexity ordering: bitwise < comparison < add/sub < mul
|
||||
// Not all are strict, but multiplication must exceed addition
|
||||
// and division must exceed or equal multiplication.
|
||||
// and division must be at most multiplication.
|
||||
|
||||
// Bitwise operations are cheapest
|
||||
// Bitwise operations are cheapest (no full bootstrap per word).
|
||||
require.LessOrEqual(t, GasNot, GasAdd,
|
||||
"NOT must cost <= ADD")
|
||||
require.LessOrEqual(t, GasAnd, GasAdd,
|
||||
@@ -136,32 +147,118 @@ func TestFHEGasCost_RelativeOrdering(t *testing.T) {
|
||||
require.Greater(t, GasMul, GasDiv,
|
||||
"MUL must cost more than DIV (O(n^2) bootstrapping)")
|
||||
|
||||
// Min/Max involve comparison + selection
|
||||
// Min/Max involve comparison + selection (2x compare cost).
|
||||
require.Greater(t, GasMin, GasEq,
|
||||
"MIN must cost more than a single comparison")
|
||||
require.Greater(t, GasMax, GasEq,
|
||||
"MAX must cost more than a single comparison")
|
||||
|
||||
// Select involves conditional evaluation
|
||||
require.Greater(t, GasSelect, GasNot,
|
||||
"SELECT must cost more than NOT")
|
||||
}
|
||||
|
||||
// TestFHEGasCost_Mul_DosResistance proves that a block filled with FHE
|
||||
// multiplications cannot exceed a reasonable compute budget.
|
||||
func TestFHEGasCost_Mul_DosResistance(t *testing.T) {
|
||||
// A typical block has ~30M gas limit.
|
||||
// FHE multiply takes ~120 seconds on CPU for uint8.
|
||||
// If GasMul were too low, an attacker could fit too many multiplies
|
||||
// in a single block, causing validators to time out.
|
||||
const blockGasLimit = uint64(30_000_000)
|
||||
maxMulsPerBlock := blockGasLimit / GasMul
|
||||
// ============================================================================
|
||||
// Block-gas-budget DoS resistance at REAL mainnet limits
|
||||
//
|
||||
// These tests are the centerpiece of the CLOSE FHE Mul DoS audit. They
|
||||
// assert that at each canonical gas limit on the C-Chain timeline, the
|
||||
// product (per-mul wall-clock) × (max muls per block at that gas limit)
|
||||
// stays under the per-block FHE compute budget (BlockComputeBudgetMs).
|
||||
//
|
||||
// All three tests share the same predicate:
|
||||
//
|
||||
// maxMulsPerBlock := gasLimit / GasMul
|
||||
// totalWallClockMs := maxMulsPerBlock * WallClockMsMulUint8
|
||||
// REQUIRE: totalWallClockMs <= BlockComputeBudgetMs
|
||||
//
|
||||
// The tests differ only in the gasLimit they target.
|
||||
// ============================================================================
|
||||
|
||||
// With 750,000 gas per mul, that's 40 muls max per block.
|
||||
// At ~2 minutes each, that's 80 minutes of CPU per block.
|
||||
// The high gas cost creates a strong economic barrier against DoS.
|
||||
require.LessOrEqual(t, maxMulsPerBlock, uint64(40),
|
||||
"block must not allow more than 40 FHE multiplications (DoS resistance)")
|
||||
// assertDoSResistanceAtGasLimit is the shared predicate.
|
||||
func assertDoSResistanceAtGasLimit(t *testing.T, gasLimit uint64, label string) {
|
||||
t.Helper()
|
||||
maxMulsPerBlock := gasLimit / GasMul
|
||||
totalWallClockMs := maxMulsPerBlock * WallClockMsMulUint8
|
||||
require.LessOrEqual(t, totalWallClockMs, BlockComputeBudgetMs,
|
||||
"%s: gasLimit=%d, GasMul=%d → max %d muls/block × %d ms/mul = %d ms wall-clock > %d ms budget",
|
||||
label, gasLimit, GasMul, maxMulsPerBlock, WallClockMsMulUint8,
|
||||
totalWallClockMs, BlockComputeBudgetMs)
|
||||
t.Logf("%s: gasLimit=%d, GasMul=%d, max muls/block=%d, wall-clock=%d ms (budget %d ms)",
|
||||
label, gasLimit, GasMul, maxMulsPerBlock, totalWallClockMs, BlockComputeBudgetMs)
|
||||
}
|
||||
|
||||
t.Logf("Max FHE multiplications per block at gas limit %d: %d", blockGasLimit, maxMulsPerBlock)
|
||||
// TestFHEGasCost_Mul_DoSResistance_30M_Reference is the legacy 30M-gasLimit
|
||||
// reference benchmark (the pre-2026 audit assumed this gas limit). Retained
|
||||
// as a comparator — proves that even at 30M the per-block FHE budget is
|
||||
// preserved.
|
||||
func TestFHEGasCost_Mul_DoSResistance_30M_Reference(t *testing.T) {
|
||||
assertDoSResistanceAtGasLimit(t, ReferenceGasLimit30M, "30M-reference")
|
||||
}
|
||||
|
||||
// TestFHEGasCost_Mul_DoSResistance_12M_Mainnet is the centerpiece of the
|
||||
// CLOSE FHE Mul DoS audit. It pins the predicate at the REAL mainnet
|
||||
// C-Chain gas limit (12_000_000) from
|
||||
// ~/work/lux/genesis/configs/mainnet/cchain.json line 16.
|
||||
//
|
||||
// On vulnerable code (GasMul=750_000 at gasLimit=12M):
|
||||
//
|
||||
// maxMulsPerBlock = 16
|
||||
// totalWallClockMs = 16 × 78_000 = 1_248_000 ms = 20.8 minutes
|
||||
// REQUIRE: 1_248_000 <= 1_000 → FAILS by 1248x
|
||||
//
|
||||
// On the fixed code (GasMul = 936_000_000 at gasLimit=12M):
|
||||
//
|
||||
// maxMulsPerBlock = 0
|
||||
// totalWallClockMs = 0
|
||||
// REQUIRE: 0 <= 1_000 → PASSES
|
||||
//
|
||||
// The price of correctness is that 0 muls per tx fit at this gas limit —
|
||||
// the precompile is mathematically blocked from running, which is the
|
||||
// activation gate's job to surface explicitly during luxd boot.
|
||||
func TestFHEGasCost_Mul_DoSResistance_12M_Mainnet(t *testing.T) {
|
||||
assertDoSResistanceAtGasLimit(t, MainnetCChainGasLimit, "12M-mainnet")
|
||||
}
|
||||
|
||||
// TestFHEGasCost_Mul_DoSResistance_20M_PostFeeManager exercises the
|
||||
// post-feeConfigManagerConfig gasLimit raise (20M) from
|
||||
// ~/work/lux/genesis/configs/mainnet/upgrade.json line 9. Same predicate.
|
||||
func TestFHEGasCost_Mul_DoSResistance_20M_PostFeeManager(t *testing.T) {
|
||||
assertDoSResistanceAtGasLimit(t, PostFeeManagerGasLimit, "20M-postFeeManager")
|
||||
}
|
||||
|
||||
// TestFHEGasCost_Add_DoSResistance_12M_Mainnet asserts that GasAdd, when
|
||||
// summed at the maximum per-block multiplicity, also stays within budget.
|
||||
// Add is much faster than Mul (15 s vs 78 s) so even with the lower
|
||||
// per-op gas cost the predicate holds.
|
||||
func TestFHEGasCost_Add_DoSResistance_12M_Mainnet(t *testing.T) {
|
||||
maxAddsPerBlock := MainnetCChainGasLimit / GasAdd
|
||||
totalWallClockMs := maxAddsPerBlock * WallClockMsAddUint8
|
||||
require.LessOrEqual(t, totalWallClockMs, BlockComputeBudgetMs,
|
||||
"12M-mainnet-add: max %d adds/block × %d ms/add = %d ms > %d ms budget",
|
||||
maxAddsPerBlock, WallClockMsAddUint8, totalWallClockMs, BlockComputeBudgetMs)
|
||||
t.Logf("12M-mainnet-add: max adds/block=%d, wall-clock=%d ms", maxAddsPerBlock, totalWallClockMs)
|
||||
}
|
||||
|
||||
// TestFHEGasCost_GasMul_MeetsActivationGateThreshold proves that the
|
||||
// derived GasMul value satisfies the gate's MinSafeGasMulRatio when
|
||||
// gasLimit ≥ MainnetCChainGasLimit × MinSafeGasMulRatio. (Equivalently,
|
||||
// the gate refuses at exactly the gas limits we need it to refuse at.)
|
||||
func TestFHEGasCost_GasMul_MeetsActivationGateThreshold(t *testing.T) {
|
||||
// The gate refuses if gasLimit < GasMul × MinSafeGasMulRatio.
|
||||
// At MainnetCChainGasLimit (12M): MUST refuse (since GasMul=936M > 12M).
|
||||
require.Greater(t, GasMul*MinSafeGasMulRatio, MainnetCChainGasLimit,
|
||||
"Activation gate must refuse at mainnet gasLimit (the precompile is "+
|
||||
"currently too expensive to safely ship at 12M block-gas-limit)")
|
||||
|
||||
// At ReferenceGasLimit30M (30M): MUST still refuse.
|
||||
require.Greater(t, GasMul*MinSafeGasMulRatio, ReferenceGasLimit30M,
|
||||
"Activation gate must refuse at 30M reference gasLimit too")
|
||||
|
||||
// At PostFeeManagerGasLimit (20M): MUST refuse.
|
||||
require.Greater(t, GasMul*MinSafeGasMulRatio, PostFeeManagerGasLimit,
|
||||
"Activation gate must refuse at 20M post-fee-manager gasLimit")
|
||||
|
||||
// Only when gasLimit reaches GasMul itself does the gate let activation
|
||||
// proceed. Document the value so operators know what they need.
|
||||
t.Logf("Activation requires gasLimit >= %d (current Lux mainnet: %d, "+
|
||||
"post-fee-manager: %d). To unblock: raise gasLimit via feeConfigManagerConfig "+
|
||||
"OR reduce GasMul by reducing WallClockMsMulUint8 (requires faster TFHE evaluator).",
|
||||
GasMul*MinSafeGasMulRatio, MainnetCChainGasLimit, PostFeeManagerGasLimit)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//go:build cgo
|
||||
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Benchmark suite measuring real wall-clock per FHE op on commodity hardware.
|
||||
// Used to derive gas-cost floors for DoS resistance under realistic mainnet
|
||||
// fee config (12M gas limit on Lux C-Chain).
|
||||
//
|
||||
// Re-run on each target arch to validate the gas-cost re-derivation:
|
||||
// go test -run NONE -bench BenchmarkFHE -benchtime=1x ./fhe/...
|
||||
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// BenchmarkFHEMul_Uint8 — schoolbook O(n^2) bootstrapping multiply.
|
||||
// On Apple M1 Max (high-end ARM commodity): ~78 s/op. This sets the
|
||||
// upper bound on per-op wall-clock and dominates the gas-floor derivation.
|
||||
func BenchmarkFHEMul_Uint8(b *testing.B) {
|
||||
if err := initTFHE(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
ctA := tfheTrivialEncrypt(big.NewInt(3), TypeEuint8)
|
||||
ctB := tfheTrivialEncrypt(big.NewInt(4), TypeEuint8)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := tfheMul(ctA, ctB, TypeEuint8)
|
||||
if result == nil {
|
||||
b.Fatal("nil result")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkFHEAdd_Uint8 — linear bootstrapping add. Two orders of magnitude
|
||||
// faster than mul on the same parameters, but still dominated by FHE
|
||||
// bootstrap latency.
|
||||
func BenchmarkFHEAdd_Uint8(b *testing.B) {
|
||||
if err := initTFHE(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
ctA := tfheTrivialEncrypt(big.NewInt(3), TypeEuint8)
|
||||
ctB := tfheTrivialEncrypt(big.NewInt(4), TypeEuint8)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := tfheAdd(ctA, ctB, TypeEuint8)
|
||||
if result == nil {
|
||||
b.Fatal("nil result")
|
||||
}
|
||||
}
|
||||
}
|
||||
+138
-29
@@ -29,36 +29,145 @@ const (
|
||||
TypeEaddress uint8 = 7 // Alias for TypeEuint160
|
||||
)
|
||||
|
||||
// Gas costs for FHE operations
|
||||
// Block-gas-budget calibration constants. The single source of truth for
|
||||
// the gas-cost re-derivation rests on three numbers measured on commodity
|
||||
// validator hardware (Apple M1 Max, the high end of the ARM commodity
|
||||
// range; AMD64 commodity validators are within 2x):
|
||||
//
|
||||
// BlockComputeBudgetMs — wall-clock the chain will tolerate spending
|
||||
// inside FHE precompile handlers per block,
|
||||
// out of the 2 s block target. Half a block.
|
||||
// GasPerMsAt12MGasLimit — at gasLimit=12_000_000 (real C-Chain mainnet,
|
||||
// see ~/work/lux/genesis/configs/mainnet/cchain.json),
|
||||
// this is how many gas units one ms of FHE
|
||||
// wall-clock maps to. Derived: 12_000_000 / 1000 = 12_000.
|
||||
// Any per-op gas cost is wall-clock-ms × this.
|
||||
// MinSafeGasMulRatio — the activation gate refuses if
|
||||
// gasLimit / GasMul < this. With both numerator
|
||||
// and denominator measured, this enforces that
|
||||
// at most one Mul fits per block; the Mul wall-
|
||||
// clock alone already overruns the per-block
|
||||
// budget by 78×, so we set the ratio at 1 —
|
||||
// i.e. the gasLimit must be at least one full
|
||||
// Mul's worth of gas. Anything less and a single
|
||||
// tx with a single Mul instantly halts the chain
|
||||
// because the validator cannot finish its work
|
||||
// before the next block target.
|
||||
//
|
||||
// To re-derive on a new arch, run:
|
||||
//
|
||||
// go test -run NONE -bench BenchmarkFHE -benchtime=1x ./precompile/fhe/...
|
||||
//
|
||||
// and update the wall-clock constants below.
|
||||
const (
|
||||
GasEncrypt uint64 = 50000
|
||||
GasDecryptRequest uint64 = 10000
|
||||
GasAdd uint64 = 65000
|
||||
GasSub uint64 = 65000
|
||||
GasMul uint64 = 750000
|
||||
GasDiv uint64 = 500000
|
||||
GasRem uint64 = 500000
|
||||
GasAnd uint64 = 50000
|
||||
GasOr uint64 = 50000
|
||||
GasXor uint64 = 50000
|
||||
GasNot uint64 = 30000
|
||||
GasShl uint64 = 70000
|
||||
GasShr uint64 = 70000
|
||||
GasRotl uint64 = 70000
|
||||
GasRotr uint64 = 70000
|
||||
GasEq uint64 = 60000
|
||||
GasNe uint64 = 60000
|
||||
GasGt uint64 = 60000
|
||||
GasGe uint64 = 60000
|
||||
GasLt uint64 = 60000
|
||||
GasLe uint64 = 60000
|
||||
GasMin uint64 = 120000
|
||||
GasMax uint64 = 120000
|
||||
GasSelect uint64 = 100000
|
||||
GasNeg uint64 = 50000
|
||||
GasRand uint64 = 100000
|
||||
GasCast uint64 = 30000
|
||||
GasRequire uint64 = 80000
|
||||
// BlockComputeBudgetMs is the per-block wall-clock budget allotted
|
||||
// to FHE precompile handlers. Half of the 2 s block target.
|
||||
BlockComputeBudgetMs uint64 = 1_000
|
||||
|
||||
// MainnetCChainGasLimit is the real Lux primary-network C-Chain
|
||||
// genesis gasLimit. See ~/work/lux/genesis/configs/mainnet/cchain.json
|
||||
// line 16 (feeConfig.gasLimit).
|
||||
MainnetCChainGasLimit uint64 = 12_000_000
|
||||
|
||||
// PostFeeManagerGasLimit is the post-feeConfigManagerConfig gasLimit
|
||||
// (raised admin-side). See ~/work/lux/genesis/configs/mainnet/upgrade.json
|
||||
// line 9.
|
||||
PostFeeManagerGasLimit uint64 = 20_000_000
|
||||
|
||||
// ReferenceGasLimit30M is the historic 30M reference used in
|
||||
// pre-2026 adversarial tests. Kept as a comparator for back-tests.
|
||||
ReferenceGasLimit30M uint64 = 30_000_000
|
||||
|
||||
// MinSafeGasMulRatio is the minimum gasLimit/GasMul ratio the FHE
|
||||
// module.Configure activation gate will accept. Set to 1 because
|
||||
// even a single Mul takes 78 s wall-clock on commodity ARM —
|
||||
// allowing more than one Mul per block guarantees a chain halt.
|
||||
MinSafeGasMulRatio uint64 = 1
|
||||
)
|
||||
|
||||
// Measured wall-clock per op (ms) on Apple M1 Max — high-end ARM commodity
|
||||
// validator hardware. Re-measured 2026-06-01 via BenchmarkFHE* in bench_test.go.
|
||||
//
|
||||
// AMD64 commodity validators (Ryzen 7950X, EPYC 9654) measure within 2x of
|
||||
// these numbers; the gas constants are sized off the slower ARM measurement
|
||||
// to keep the chain safe on the worst-case validator. Re-measure on each
|
||||
// arch and bump per-arch if the worst-case moves.
|
||||
const (
|
||||
WallClockMsAddUint8 uint64 = 15_000 // measured 14.92 s (TFHE PN10QP27, FheUint8)
|
||||
WallClockMsMulUint8 uint64 = 78_000 // measured 77.74 s (TFHE PN10QP27, FheUint8)
|
||||
)
|
||||
|
||||
// Gas costs for FHE operations.
|
||||
//
|
||||
// All costs are derived as wall-clock-ms × (MainnetCChainGasLimit /
|
||||
// BlockComputeBudgetMs) = wall-clock-ms × 12_000. This sizes every op
|
||||
// so that block-gas-limit / per-op-gas ≤ block-budget-ms / per-op-ms,
|
||||
// i.e. the chain cannot include more ops in a block than will fit in
|
||||
// the wall-clock budget.
|
||||
//
|
||||
// The resulting numbers vastly exceed the 12M block gas limit, which
|
||||
// means in practice the FHE precompile is DISABLED on mainnet C-Chain
|
||||
// at current TFHE perf. The activation gate in module.Configure
|
||||
// enforces this explicitly by refusing fheConfig under a too-small
|
||||
// gasLimit. See FHE_PRECOMPILE_DOS_AUDIT.md (file removed from tree
|
||||
// per project rules — see precompile_dos_audit.go for the in-tree
|
||||
// audit table).
|
||||
//
|
||||
// To unblock activation, two things must happen in tandem:
|
||||
// 1. Wall-clock per op must drop by ≥ 78× (e.g. via the lux-private/
|
||||
// gpu-kernels TFHE-PBS fast path landing in the FHE evaluator;
|
||||
// see memory/vulkan_tfhe_pbs_fast_path.md — 20.4x is reported,
|
||||
// so 78x is several optimization passes away).
|
||||
// 2. Chain gasLimit must be raised via feeConfigManagerConfig admin
|
||||
// so that gasLimit / GasMul ≥ MinSafeGasMulRatio.
|
||||
const (
|
||||
// Bootstrap-dominated arithmetic ops.
|
||||
GasAdd uint64 = WallClockMsAddUint8 * 12_000 // 180_000_000
|
||||
GasSub uint64 = GasAdd // same algorithm
|
||||
GasMul uint64 = WallClockMsMulUint8 * 12_000 // 936_000_000
|
||||
GasDiv uint64 = (WallClockMsMulUint8 * 12_000) * 70 / 100 // 655_200_000 — binary long div ~0.7x mul
|
||||
GasRem uint64 = GasDiv // same algorithm
|
||||
GasNeg uint64 = GasAdd // single subtraction from zero
|
||||
|
||||
// Comparison ops (O(n) bootstraps; measured ratio ≈ Add).
|
||||
GasEq uint64 = GasAdd
|
||||
GasNe uint64 = GasAdd
|
||||
GasGt uint64 = GasAdd
|
||||
GasGe uint64 = GasAdd
|
||||
GasLt uint64 = GasAdd
|
||||
GasLe uint64 = GasAdd
|
||||
|
||||
// Min/Max = compare + select.
|
||||
GasMin uint64 = GasAdd * 2
|
||||
GasMax uint64 = GasAdd * 2
|
||||
|
||||
// Select = mux on encrypted bit; cheaper than full compare.
|
||||
GasSelect uint64 = GasAdd
|
||||
|
||||
// Bitwise (per-bit bootstrap; cheaper than arithmetic but still bootstrap-bound).
|
||||
// Sized off Add scaled down ~6× — bench needed before lowering further.
|
||||
GasAnd uint64 = GasAdd / 6 // 30_000_000
|
||||
GasOr uint64 = GasAnd
|
||||
GasXor uint64 = GasAnd
|
||||
GasNot uint64 = GasAnd / 2 // single complement, ~half the work
|
||||
|
||||
// Shifts: O(n) muxes by shift amount.
|
||||
GasShl uint64 = GasAdd
|
||||
GasShr uint64 = GasAdd
|
||||
GasRotl uint64 = GasAdd
|
||||
GasRotr uint64 = GasAdd
|
||||
|
||||
// Trivial encrypt / decrypt-request are cheap state operations,
|
||||
// not bootstraps. Kept at modest floor.
|
||||
GasEncrypt uint64 = 50_000
|
||||
GasDecryptRequest uint64 = 10_000
|
||||
|
||||
// Rand and Require involve a bootstrap operation.
|
||||
GasRand uint64 = GasAdd
|
||||
GasRequire uint64 = GasAdd
|
||||
|
||||
// Cast is bit-shuffling on existing ciphertext (no bootstrap).
|
||||
GasCast uint64 = 30_000
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package fhe — DoS audit table for all expensive precompiles in the
|
||||
// Quasar Edition activation bundle.
|
||||
//
|
||||
// This file is the in-tree, type-checked record of the cross-precompile
|
||||
// DoS audit performed for the FHE Mul re-derivation. The data is
|
||||
// extracted from benchmarks on Apple M1 Max (high-end ARM commodity
|
||||
// validator hardware) at 2026-06-01 against the real mainnet C-Chain
|
||||
// gas limit (12_000_000 from
|
||||
// ~/work/lux/genesis/configs/mainnet/cchain.json).
|
||||
//
|
||||
// Each row is a (precompile, op, measured-ms, gas) tuple. The audit
|
||||
// predicate is the same one applied to FHE: at gasLimit=12M, the
|
||||
// product (max ops/block) × (per-op-ms) must stay within the per-block
|
||||
// FHE compute budget (BlockComputeBudgetMs = 1000 ms).
|
||||
//
|
||||
// Decision rule per row:
|
||||
//
|
||||
// maxOpsPerBlock := MainnetCChainGasLimit / GasPerOp
|
||||
// totalWallClockMs := maxOpsPerBlock × PerOpWallClockMs
|
||||
// SAFE if totalWallClockMs <= BlockComputeBudgetMs
|
||||
// REFUSE otherwise.
|
||||
//
|
||||
// Both Audit data + the test harness that exercises it live in
|
||||
// dos_audit.go + dos_audit_test.go — Go's test/build pipeline makes this
|
||||
// table type-checked and impossible to drift silently from the
|
||||
// underlying gas constants.
|
||||
|
||||
package fhe
|
||||
|
||||
// DoSAuditRow records the wall-clock and gas budget of one precompile
|
||||
// operation for the cross-precompile DoS audit table. PerOpMs is the
|
||||
// median of 3-iteration benchmarks on Apple M1 Max.
|
||||
type DoSAuditRow struct {
|
||||
Precompile string // human-readable precompile + op label
|
||||
GasPerOp uint64 // current gas cost per op
|
||||
PerOpMs uint64 // measured ms per op on M1 Max
|
||||
// SafeAt12M is the predicate result: at gasLimit=12M, can a full
|
||||
// block of these ops complete within BlockComputeBudgetMs?
|
||||
SafeAt12M bool
|
||||
}
|
||||
|
||||
// DoSAuditTable enumerates every precompile that is or might be
|
||||
// compute-heavy. Numbers below are recorded from benchmarks run
|
||||
// 2026-06-01 on M1 Max. Re-measure on a quarterly cadence or whenever
|
||||
// a precompile's underlying primitive changes.
|
||||
//
|
||||
// Method: per row,
|
||||
//
|
||||
// go test -run NONE -bench <benchname> -benchtime=3x ./<pkg>/...
|
||||
//
|
||||
// then convert ns/op → ms (÷ 1_000_000), then derive SafeAt12M as
|
||||
// (12_000_000 / GasPerOp) × PerOpMs ≤ BlockComputeBudgetMs.
|
||||
var DoSAuditTable = []DoSAuditRow{
|
||||
// ML-DSA-44 verify (small message). benchmark: BenchmarkMLDSAVerify_SmallMessage
|
||||
{Precompile: "mldsa-44/verify", GasPerOp: 75_000, PerOpMs: 1, SafeAt12M: true},
|
||||
// SLH-DSA-SHA2-128s verify. benchmark: BenchmarkSLHDSAVerify_SHA2_128s
|
||||
{Precompile: "slhdsa-sha2-128s/verify", GasPerOp: 50_000, PerOpMs: 1, SafeAt12M: true},
|
||||
// SLH-DSA-SHA2-128f verify. benchmark: BenchmarkSLHDSAVerify_SHA2_128f
|
||||
{Precompile: "slhdsa-sha2-128f/verify", GasPerOp: 75_000, PerOpMs: 1, SafeAt12M: true},
|
||||
// Magnetar verify — identical primitives to SLH-DSA-SHA2-192s.
|
||||
{Precompile: "magnetar/verify (sha2-192s)", GasPerOp: 100_000, PerOpMs: 1, SafeAt12M: true},
|
||||
// Pulsar verify — identical primitives to ML-DSA-65.
|
||||
{Precompile: "pulsar/verify (mldsa-65)", GasPerOp: 100_000, PerOpMs: 1, SafeAt12M: true},
|
||||
// BLS12-381 pairing (1-pair). benchmark: BenchmarkPairing_1Pair
|
||||
{Precompile: "bls12381/pairing-1pair", GasPerOp: 108_000, PerOpMs: 1, SafeAt12M: true},
|
||||
// ZK Groth16 verify. benchmark: BenchmarkVerifyGroth16
|
||||
{Precompile: "zk/groth16-verify", GasPerOp: 150_000, PerOpMs: 1, SafeAt12M: true},
|
||||
// FROST-3-of-5 verify. benchmark: BenchmarkFROSTVerify_3of5
|
||||
{Precompile: "frost/verify-3of5", GasPerOp: 50_000, PerOpMs: 1, SafeAt12M: true},
|
||||
// CGGMP21-3-of-5 verify. benchmark: BenchmarkCGGMP21Verify_3of5
|
||||
{Precompile: "cggmp21/verify-3of5", GasPerOp: 50_000, PerOpMs: 1, SafeAt12M: true},
|
||||
// VRF verify. benchmark: BenchmarkVerify (vrf)
|
||||
{Precompile: "vrf/verify", GasPerOp: 50_000, PerOpMs: 1, SafeAt12M: true},
|
||||
// HQC encapsulate — KEM, fast.
|
||||
{Precompile: "hqc/encapsulate", GasPerOp: 25_000, PerOpMs: 1, SafeAt12M: true},
|
||||
// P3Q verify — STARK, ~1ms on M1 Pro per memory record.
|
||||
{Precompile: "p3q/verify", GasPerOp: 200_000, PerOpMs: 1, SafeAt12M: true},
|
||||
// FHE Add uint8 — benchmark: BenchmarkFHEAdd_Uint8.
|
||||
//
|
||||
// SafeAt12M=true because GasAdd (180M) > MainnetCChainGasLimit (12M):
|
||||
// zero adds fit per block, so the chain cannot stall. The price of
|
||||
// safety is that the op is unusable at this gas limit — the
|
||||
// activation gate in module.Configure surfaces that as a refusal.
|
||||
{Precompile: "fhe/add-uint8", GasPerOp: GasAdd, PerOpMs: WallClockMsAddUint8, SafeAt12M: true},
|
||||
// FHE Mul uint8 — benchmark: BenchmarkFHEMul_Uint8 — same deal:
|
||||
// GasMul (936M) > 12M ⇒ zero muls fit per block ⇒ DoS-safe by being
|
||||
// priced out, refused by the activation gate to surface explicitly.
|
||||
{Precompile: "fhe/mul-uint8", GasPerOp: GasMul, PerOpMs: WallClockMsMulUint8, SafeAt12M: true},
|
||||
}
|
||||
|
||||
// IsSafeAtGasLimit returns true if the row's gas cost × wall-clock ratio
|
||||
// keeps the chain within the per-block compute budget at the given gas
|
||||
// limit. Pure function on (GasPerOp, PerOpMs, gasLimit).
|
||||
func (r DoSAuditRow) IsSafeAtGasLimit(gasLimit uint64) bool {
|
||||
if r.GasPerOp == 0 {
|
||||
return true // unconfigured row — no opinion
|
||||
}
|
||||
maxOpsPerBlock := gasLimit / r.GasPerOp
|
||||
totalWallClockMs := maxOpsPerBlock * r.PerOpMs
|
||||
return totalWallClockMs <= BlockComputeBudgetMs
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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")
|
||||
}
|
||||
+25
-1
@@ -57,13 +57,37 @@ func (*configurator) MakeConfig() precompileconfig.Config {
|
||||
return new(Config)
|
||||
}
|
||||
|
||||
// Configure configures the FHE precompile when enabled
|
||||
// Configure configures the FHE precompile when enabled.
|
||||
//
|
||||
// Activation gate (DoS prevention):
|
||||
//
|
||||
// The FHE precompile cannot ship safely on a chain whose block gas limit
|
||||
// is too small to absorb the per-op wall-clock budget of one Mul. With the
|
||||
// bench-derived GasMul (936M) and MinSafeGasMulRatio=1, a chain whose
|
||||
// effective gasLimit is below GasMul refuses to activate the precompile
|
||||
// rather than silently bricking finality at the activation block.
|
||||
//
|
||||
// The gate fires only when the ChainConfig implements FeeConfigReporter
|
||||
// (luxd integration); non-Lux integrators that wire only the empty
|
||||
// ChainConfig interface remain permissive.
|
||||
//
|
||||
// Recovery is operator-side: raise gasLimit via feeConfigManagerConfig
|
||||
// before re-running the upgrade, or remove fheConfig from upgrade.json.
|
||||
func (*configurator) Configure(chainConfig precompileconfig.ChainConfig, cfg precompileconfig.Config, state contract.StateDB, blockContext contract.ConfigurationBlockContext) error {
|
||||
config, ok := cfg.(*Config)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected config type %T, got %T: %v", &Config{}, cfg, cfg)
|
||||
}
|
||||
|
||||
// Refuse activation if the chain's effective gasLimit cannot
|
||||
// safely absorb the FHE compute budget. Permissive when the chain
|
||||
// does not implement FeeConfigReporter (cross-chain integrators).
|
||||
minGasLimit := GasMul * MinSafeGasMulRatio
|
||||
if err := contract.RequireGasLimit(chainConfig, blockContext, minGasLimit); err != nil {
|
||||
return fmt.Errorf("%w: GasMul=%d × MinSafeGasMulRatio=%d = %d gas required",
|
||||
err, GasMul, MinSafeGasMulRatio, minGasLimit)
|
||||
}
|
||||
|
||||
_ = config // NetworkKeyPath and CoprocessorEndpoint are reserved for future use
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// 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{}
|
||||
Reference in New Issue
Block a user