LPsLux Proposals
LP-3690

EVM Security Hardening Suite

Review

Gas caps, RLP limits, MODEXP optimization, and anti-DoS measures

Category
Core
Created
2025-12-25

Abstract

LP-3690 consolidates multiple EVM security hardening measures into a single specification, including per-transaction gas caps, RLP block size limits, MODEXP precompile limits (EIP-7883), and anti-DoS tweaks. These changes make it harder for attackers to spam or stall the network while keeping nodes more resource-efficient.

Motivation

The EVM has accumulated several attack vectors that enable resource exhaustion:

  1. MODEXP Abuse: Unbounded exponent sizes can cause excessive computation
  2. Large RLP Blocks: Oversized blocks can exhaust memory during decoding
  3. Gas Limit Gaming: Uncapped transaction gas can monopolize block space
  4. Memory Expansion: Aggressive memory allocation attacks

Attack Scenarios

Attack VectorImpactMitigation
MODEXP with large exponentCPU exhaustionEIP-7883 limits
100MB+ RLP blocksMemory exhaustion10 MiB cap
Single tx consuming all gasBlock monopolizationPer-tx gas caps
Memory expansion attacksOOM conditionsEnhanced limits

Specification

1. MODEXP Limits (EIP-7883)

Enhanced MODEXP precompile with tighter computational bounds:

// Precompile address: 0x05
const (
    // Maximum input lengths for MODEXP
    MaxBaseLength     = 1024  // bytes
    MaxExponentLength = 1024  // bytes
    MaxModulusLength  = 1024  // bytes
)

// Gas calculation with EIP-7883 adjustments
func modexpGas(baseLen, expLen, modLen uint64) uint64 {
    // Cap effective lengths
    baseLen = min(baseLen, MaxBaseLength)
    expLen = min(expLen, MaxExponentLength)
    modLen = min(modLen, MaxModulusLength)

    // Apply EIP-2565 gas formula with EIP-7883 caps
    mulComplexity := calcMultiplicationComplexity(max(baseLen, modLen))
    iterCount := calcIterationCount(expLen, expHead)

    return max(200, mulComplexity * iterCount / 3)
}

Implementation: geth/core/vm/contracts.go - bigModExp

2. RLP Block Size Limit

Maximum RLP-encoded block size capped at 10 MiB:

const MaxBlockRLPSize = 10 * 1024 * 1024 // 10 MiB

func validateBlockRLP(rlpData []byte) error {
    if len(rlpData) > MaxBlockRLPSize {
        return errors.New("block RLP exceeds maximum size")
    }
    return nil
}

Rationale:

  • Prevents memory exhaustion during block decoding
  • Ensures reasonable sync times
  • Compatible with blob transactions (EIP-4844)

Implementation: geth/core/types/block.go

3. Per-Transaction Gas Caps

Maximum gas per transaction to prevent block monopolization:

const (
    // Maximum gas limit for a single transaction
    MaxTransactionGas = 30_000_000 // 30M gas

    // Block gas target (50% of limit)
    BlockGasTarget = 15_000_000
)

Rationale:

  • Ensures multiple transactions can fit per block
  • Prevents single-tx DoS of block production
  • Maintains healthy fee market dynamics

4. Memory Expansion Limits

Enhanced memory expansion checks:

const (
    // Maximum memory size (1 GiB)
    MaxMemorySize = 1 << 30

    // Maximum memory expansion per opcode
    MaxExpansionPerOp = 1 << 20 // 1 MiB
)

func memoryExpansionCost(currentSize, requestedSize uint64) (uint64, error) {
    if requestedSize > MaxMemorySize {
        return 0, ErrMaxMemoryExceeded
    }
    expansion := requestedSize - currentSize
    if expansion > MaxExpansionPerOp {
        return 0, ErrExpansionTooLarge
    }
    return calcMemoryCost(requestedSize), nil
}

5. Anti-DoS Opcode Limits

Additional limits on expensive opcodes:

OpcodeLimitRationale
EXTCODECOPY24KB maxPrevent large code reads
RETURNDATACOPY24KB maxLimit return data
CREATE2Salt + initcode boundedPredictable addresses
SELFDESTRUCTDeprecated (EIP-6780)Security concerns

Implementation Status

All features implemented in Lux geth:

FeatureEIPImplementationStatus
MODEXP limitsEIP-7883core/vm/contracts.go
RLP block cap-core/types/block.go
TX gas capsEIP-7623core/txpool/
Memory limitsEIP-7825core/vm/memory.go

Test Cases

func TestMODEXPLimits(t *testing.T) {
    // Test excessive exponent is capped
    input := make([]byte, 3*32 + 2048) // 2KB exponent
    copy(input[64:96], big.NewInt(2048).Bytes()) // expLen = 2048

    gas := modexpGas(input)
    require.Less(t, gas, uint64(1_000_000_000)) // Must be bounded
}

func TestRLPBlockSizeLimit(t *testing.T) {
    oversizedBlock := make([]byte, 11*1024*1024) // 11 MiB
    err := validateBlockRLP(oversizedBlock)
    require.Error(t, err)
}

func TestTransactionGasCap(t *testing.T) {
    tx := types.NewTx(&types.DynamicFeeTx{
        Gas: 50_000_000, // Exceeds cap
    })
    err := validateTxGas(tx)
    require.Error(t, err)
}

Security Considerations

  1. Backwards Compatibility: All limits are set above current legitimate usage
  2. DOS Prevention: Caps prevent computational exhaustion attacks
  3. Resource Efficiency: Nodes can run on modest hardware
  4. Network Stability: Prevents resource-based network partitioning

References

Copyright and related rights waived via CC0.