ADR-006: EIP-3860 Implementation (Limit and Meter Initcode)¶
Status¶
Accepted
Context¶
EIP-3860 (https://eips.ethereum.org/EIPS/eip-3860) is an Ethereum Improvement Proposal that was activated as part of the Shanghai hard fork on Ethereum mainnet. For Ethereum Classic, this proposal is included in the Spiral hard fork (ECIP-1109: https://ecips.ethereumclassic.org/ECIPs/ecip-1109) at block 19,250,000 on mainnet and block 9,957,000 on Mordor testnet.
The proposal introduces initcode size limits and gas metering for contract creation. Specifically:
- Problem: Prior to EIP-3860, there was no limit on initcode size (the bytecode that runs during contract creation), and no gas charged proportional to initcode size beyond the per-byte transaction data cost. This created performance issues because:
- Jump destination analysis (JUMPDEST) on large initcode was expensive
- Large initcode could cause DOS attacks through expensive EVM operations
-
No upper bound made worst-case performance analysis difficult
-
Solution: Introduce two changes:
- Size limit: Limit maximum initcode size to
MAX_INITCODE_SIZE = 49152bytes (2 × 24576, where 24576 isMAX_CODE_SIZEfrom EIP-170) - Gas metering: Charge
INITCODE_WORD_COST = 2gas per 32-byte word of initcode, calculated as:initcode_cost(initcode) = INITCODE_WORD_COST × ceil(len(initcode) / 32)
The changes affect:
- Contract creation transactions (transactions with empty to field)
- CREATE opcode (0xf0)
- CREATE2 opcode (0xf5)
- Transaction intrinsic gas calculation
- Opcode gas costs
This is a consensus-critical change. It affects: - Transaction validation (transactions can become invalid) - EVM execution (CREATE/CREATE2 can fail with exceptional abort) - Gas costs for contract creation
Decision¶
We implemented EIP-3860 in the Fukuii codebase with the following design decisions:
1. Constants Definition¶
Constants are added to the FeeSchedule trait and implementations:
trait FeeSchedule {
// ... existing fields ...
val G_initcode_word: BigInt // INITCODE_WORD_COST (2 gas per word)
}
class MystiqueFeeSchedule extends MagnetoFeeSchedule {
// ... existing fields ...
override val G_initcode_word: BigInt = 2
}
The MAX_INITCODE_SIZE constant (49152 = 2 × 24576) is derived from the existing maxCodeSize configuration value:
2. Initcode Cost Calculation¶
A new function is added to EvmConfig to calculate initcode gas cost:
def calcInitCodeCost(initCode: ByteString): BigInt = {
if (eip3860Enabled) {
val words = wordsForBytes(initCode.size)
feeSchedule.G_initcode_word * words
} else {
0
}
}
This function uses the existing wordsForBytes utility which correctly implements ceil(len(initcode) / 32).
3. Transaction Intrinsic Gas Update¶
The calcTransactionIntrinsicGas function in EvmConfig is updated to include initcode cost for contract creation transactions:
def calcTransactionIntrinsicGas(
txData: ByteString,
isContractCreation: Boolean,
accessList: Seq[AccessListItem]
): BigInt = {
val txDataZero = txData.count(_ == 0)
val txDataNonZero = txData.length - txDataZero
val accessListPrice =
accessList.size * G_access_list_address +
accessList.map(_.storageKeys.size).sum * G_access_list_storage
val initCodeCost = if (isContractCreation) calcInitCodeCost(txData) else 0
txDataZero * G_txdatazero +
txDataNonZero * G_txdatanonzero + accessListPrice +
(if (isContractCreation) G_txcreate else 0) +
G_transaction +
initCodeCost
}
4. Transaction Validation Update¶
Transaction validation in StdSignedTransactionValidator checks initcode size for contract creation transactions:
private def validateInitCodeSize(
stx: SignedTransaction,
blockHeaderNumber: BigInt
)(implicit blockchainConfig: BlockchainConfig): Either[SignedTransactionError, SignedTransactionValid] = {
import stx.tx
if (tx.isContractInit) {
val config = EvmConfig.forBlock(blockHeaderNumber, blockchainConfig)
config.maxInitCodeSize match {
case Some(maxSize) if config.eip3860Enabled && tx.payload.size > maxSize =>
Left(TransactionInitCodeSizeError(tx.payload.size, maxSize))
case _ =>
Right(SignedTransactionValid)
}
} else {
Right(SignedTransactionValid)
}
}
A new error type is added:
case class TransactionInitCodeSizeError(actualSize: BigInt, maxSize: BigInt) extends SignedTransactionError {
override def toString: String =
s"Transaction initcode size ($actualSize) exceeds maximum ($maxSize)"
}
5. CREATE/CREATE2 Opcode Updates¶
The CreateOp abstract class is updated to:
1. Check initcode size before execution
2. Charge initcode gas cost
abstract class CreateOp(code: Int, delta: Int) extends OpCode(code, delta, 1, _.G_create) {
protected def exec[W <: WorldStateProxy[W, S], S <: Storage[S]](state: ProgramState[W, S]): ProgramState[W, S] = {
val (Seq(endowment, inOffset, inSize), stack1) = state.stack.pop(3)
// Check initcode size limit (EIP-3860)
val maxInitCodeSize = state.config.maxInitCodeSize
if (state.config.eip3860Enabled && maxInitCodeSize.exists(max => inSize > max)) {
// Exceptional abort: initcode too large
return state.withStack(stack1.push(UInt256.Zero)).withError(InitCodeSizeLimit).step()
}
// Calculate gas including initcode cost (EIP-3860)
val initCodeGasCost = if (state.config.eip3860Enabled) {
val words = wordsForBytes(inSize)
state.config.feeSchedule.G_initcode_word * words
} else {
0
}
val baseGas = baseGasFn(state.config.feeSchedule) + varGas(state) + initCodeGasCost
val availableGas = state.gas - baseGas
val startGas = state.config.gasCap(availableGas)
// ... rest of CREATE logic ...
}
}
A new program error is added for initcode size violations:
case object InitCodeSizeLimit extends ProgramError {
override def description: String = "Initcode size exceeds maximum limit (EIP-3860)"
}
6. Fork Configuration¶
The eip3860Enabled flag is added to EvmConfig:
case class EvmConfig(
blockchainConfig: BlockchainConfigForEvm,
feeSchedule: FeeSchedule,
opCodeList: OpCodeList,
exceptionalFailedCodeDeposit: Boolean,
subGasCapDivisor: Option[Long],
chargeSelfDestructForNewAccount: Boolean,
traceInternalTransactions: Boolean,
noEmptyAccounts: Boolean = false,
eip3541Enabled: Boolean = false,
eip3651Enabled: Boolean = false,
eip3860Enabled: Boolean = false
) {
// ...
def maxInitCodeSize: Option[BigInt] =
if (eip3860Enabled) blockchainConfig.maxCodeSize.map(_ * 2) else None
}
The Spiral fork configuration enables EIP-3860:
val SpiralConfigBuilder: EvmConfigBuilder = config =>
MystiqueConfigBuilder(config).copy(
opCodeList = SpiralOpCodes,
eip3651Enabled = true,
eip3860Enabled = true
)
A helper function is added to BlockchainConfigForEvm:
Rationale¶
Gas Cost Per Word¶
The value of INITCODE_WORD_COST = 2 was selected based on performance benchmarks comparing initcode processing performance to KECCAK256 hashing, which is the baseline for the 70 Mgas/s gas limit target. The per-word (32-byte) cost of 2 gas approximates a per-byte cost of 0.0625 gas.
Size Limit Value¶
The MAX_INITCODE_SIZE = 2 × MAX_CODE_SIZE allows:
- MAX_CODE_SIZE (24576 bytes) for the deployed runtime code
- Another MAX_CODE_SIZE for constructor code and initialization logic
This limit is generous for typical contracts while preventing worst-case DOS attacks.
Order of Checks¶
For CREATE/CREATE2 opcodes, the initcode size check and cost are applied early, before: - Contract address calculation - Balance transfer - Initcode execution
This matches the specification's requirement that initcode cost is "deducted before the calculation of the resulting contract address and the execution of initcode."
The exceptional abort for size limit violations is grouped with other early out-of-gas checks (stack underflow, memory expansion, etc.) for consistency.
Backwards Compatibility¶
This EIP requires a "network upgrade" (hard fork) since it modifies consensus rules.
- Existing contracts: Not affected (deployed code size is unchanged)
- New transactions: Some previously valid transactions (with large initcode) become invalid
- CREATE/CREATE2: Can now fail with exceptional abort for large initcode
Consequences¶
Positive¶
- DOS protection: Limits worst-case performance impact of large initcode
- Predictable costs: Gas costs better reflect actual computational work
- Consistency: CREATE and CREATE2 gas costs now account for initcode processing
- Forward compatibility: The initcode cost structure allows future optimizations
Negative¶
- Breaking change: Some transactions that were valid before become invalid
- Increased gas costs: Contract creation becomes slightly more expensive
- Factory contracts: Multi-level contract factories with very large initcode may fail
Risks¶
- Consensus critical: Errors in size checking or gas calculation cause chain splits
- Edge cases: Boundary conditions at MAX_INITCODE_SIZE must be exact
- Gas calculation: Word-based calculation must match specification precisely
Implementation Notes¶
Testing Strategy¶
Tests must cover: 1. CREATE/CREATE2 with initcode at exactly MAX_INITCODE_SIZE (should succeed) 2. CREATE/CREATE2 with initcode at MAX_INITCODE_SIZE + 1 (should fail) 3. Create transaction with large initcode (validation) 4. Gas cost calculations for various initcode sizes 5. Interaction with other gas costs (memory expansion, hashing for CREATE2) 6. Fork activation boundary (before/after Spiral fork)
ETC-Specific Considerations¶
- Activated at block 19,250,000 on ETC mainnet (Spiral fork)
- Activated at block 9,957,000 on Mordor testnet
- Must be controlled by the
spiral-block-numberconfiguration - Part of ECIP-1109 (Spiral hard fork specification)
Performance Impact¶
The changes have minimal performance impact: - Initcode size check: O(1) comparison - Gas cost calculation: O(1) arithmetic - No change to existing contract execution