Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@
* block and determines the selection result accordingly.
*
* <p>For EIP-8037 multidimensional gas, this selector tracks both regular and state gas dimensions.
* A transaction fits if its gasLimit does not exceed the remaining capacity of each dimension
* independently. Post-processing verifies the actual gas metered stays within limits.
* Pre-processing checks that the transaction's gasLimit fits within the remaining regular gas
* capacity. State gas is only validated at the block level via max(regular, state) after
* transaction execution.
*/
public class BlockSizeTransactionSelector extends AbstractStatefulTransactionSelector<GasState> {
private static final Logger LOG = LoggerFactory.getLogger(BlockSizeTransactionSelector.class);
Expand Down Expand Up @@ -108,9 +109,9 @@ public TransactionSelectionResult evaluateTransactionPostProcessing(
}

/**
* Checks if the transaction is too large for the block using the gas accounting strategy. For 1D
* gas, this checks regular gas only. For 2D gas (EIP-8037), the tx gas limit must fit within the
* remaining capacity of each dimension independently.
* Checks if the transaction is too large for the block using the gas accounting strategy. For
* both 1D and 2D gas, this checks regular gas capacity only. State gas is validated at the block
* level via max(regular, state) after transaction execution.
*
* <p>The post-processing check verifies that gas metered (max of regular, state) stays within the
* block gas limit after processing reveals the actual regular/state gas split.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ void eip8037TwoDimensionalPreProcessingChecksPerDimension() {
assertThat(selector.getWorkingState().regularGas()).isEqualTo(25_000_000L);
assertThat(selector.getWorkingState().stateGas()).isEqualTo(5_000_000L);

// tx with gasLimit=5M fits: min(30M-25M, 30M-5M) = min(5M, 25M) = 5M >= 5M
// tx with gasLimit=5M fits: remaining_regular = 30M - 25M = 5M >= 5M
final var tx2 = createPendingTransaction(5_000_000L);
final var ctx2 =
new TransactionEvaluationContext(
Expand Down Expand Up @@ -363,7 +363,7 @@ void eip8037PostProcessingTracksGasAndPreProcessingRejectsWhenDimensionExhausted
assertThat(selector.getWorkingState().regularGas()).isEqualTo(20_000_000L);
assertThat(selector.getWorkingState().stateGas()).isEqualTo(10_000_000L);

// Second tx with gasLimit=10M fits: min(30M-20M, 30M-10M) = min(10M, 20M) = 10M >= 10M
// Second tx with gasLimit=10M fits: remaining_regular = 30M - 20M = 10M >= 10M
final var tx2 = createPendingTransaction(10_000_000L);
final var ctx2 =
new TransactionEvaluationContext(
Expand Down Expand Up @@ -409,8 +409,9 @@ void eip8037EffectiveGasUsedDrivesBlockFullCheck() {
assertThat(selector.getWorkingState().regularGas()).isEqualTo(990_000L);
assertThat(selector.getWorkingState().stateGas()).isEqualTo(10_000L);

// tx2 gasLimit=21K > min(1M-990K, 1M-10K) = min(10K, 990K) = 10K
// transactionTooLargeForBlock=true; effectiveGasUsed=990K, remaining=10K < 21K → blockFull
// tx2 gasLimit=21K > remaining_regular = 1M - 990K = 10K
// transactionTooLargeForBlock=true; effectiveGasUsed=max(990K,10K)=990K, remaining=10K < 21K
// result in blockFull
final var tx2 = createPendingTransaction(TRANSFER_GAS_LIMIT);
final var ctx2 =
new TransactionEvaluationContext(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,13 @@ public boolean hasBlockCapacity(
final long cumulativeRegularGas,
final long cumulativeStateGas,
final long blockGasLimit) {
// EIP-8037: Only check regular gas dimension. State gas is validated at block level
// via effectiveGasUsed() = max(regular, state) after transaction execution.
// The tx gas limit is not an accurate proxy for state gas usage, so checking it
// against remaining state capacity would reject valid blocks where individual txs
// exceed the state gas budget but the block total is within limits.
final long remainingRegular = Math.max(0, blockGasLimit - cumulativeRegularGas);
final long remainingState = Math.max(0, blockGasLimit - cumulativeStateGas);
return txGasLimit <= Math.min(remainingRegular, remainingState);
return txGasLimit <= remainingRegular;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,20 +160,27 @@ public void amsterdamStrategy_effectiveGasUsedIsMaxOfDimensions() {
}

@Test
public void amsterdamStrategy_hasBlockCapacityPerDimension() {
public void amsterdamStrategy_hasBlockCapacityChecksRegularGasOnly() {
final long blockGasLimit = 100_000L;
// Regular used: 60k, State used: 40k
// min(remaining_regular, remaining_state) = min(40k, 60k) = 40k
// Regular used: 60k, remaining regular = 40k
// State gas is NOT checked in hasBlockCapacity (only validated at block level)
assertThat(
BlockGasAccountingStrategy.AMSTERDAM.hasBlockCapacity(
40_000L, 60_000L, 40_000L, blockGasLimit))
.isTrue();
// txGasLimit=40001 > min(40k, 60k) = 40k — exceeds tighter dimension
// txGasLimit=40001 > remaining_regular=40k, exceeds regular capacity
assertThat(
BlockGasAccountingStrategy.AMSTERDAM.hasBlockCapacity(
40_001L, 60_000L, 40_000L, blockGasLimit))
.isFalse();

// Even when state gas is high, only regular headroom matters
// Regular used: 40k, State used: 80k, remaining regular = 60k
assertThat(
BlockGasAccountingStrategy.AMSTERDAM.hasBlockCapacity(
30_000L, 40_000L, 80_000L, blockGasLimit))
.isTrue();

// When state gas is 0, only regular headroom matters: 100k - 60k = 40k
assertThat(
BlockGasAccountingStrategy.AMSTERDAM.hasBlockCapacity(
Expand All @@ -184,7 +191,6 @@ public void amsterdamStrategy_hasBlockCapacityPerDimension() {
40_001L, 60_000L, 0L, blockGasLimit))
.isFalse();

// Frontier only checks regular dimension (same as Amsterdam when stateGas=0)
assertThat(
BlockGasAccountingStrategy.FRONTIER.hasBlockCapacity(
40_000L, 60_000L, 0L, blockGasLimit))
Expand Down
2 changes: 1 addition & 1 deletion ethereum/referencetests/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ dependencies {
)
devnetTarConfig(
group: 'ethereum', name: 'execution-spec-tests',
version: 'bal@v5.5.1', classifier: 'fixtures_bal', ext: 'tar.gz'
version: 'bal@v5.6.1', classifier: 'fixtures_bal', ext: 'tar.gz'
)
referenceTestImplementation 'com.fasterxml.jackson.core:jackson-databind'
referenceTestImplementation 'com.google.guava:guava'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) {

frame.clearReturnData();

// Resolve initcode and validate MAX_INIT_CODE_SIZE BEFORE charging state gas.
// A CREATE with oversized initcode must not persist state_gas_used for an account
// that was never created.
final Code code = codeSupplier.get();

if (code != null && code.getSize() > evm.getMaxInitcodeSize()) {
frame.popStackItems(getStackItemsConsumed());
return new OperationResult(cost, ExceptionalHaltReason.CODE_TOO_LARGE);
}
Comment thread
daniellehrner marked this conversation as resolved.
Comment thread
daniellehrner marked this conversation as resolved.

// EIP-8037: Deduct regular gas before charging state gas (ordering requirement).
frame.decrementRemainingGas(cost);

Expand All @@ -100,15 +110,6 @@ public OperationResult execute(final MessageFrame frame, final EVM evm) {
// Add regular gas back — the EVM loop will deduct it via the OperationResult.
frame.incrementRemainingGas(cost);

// Resolve initcode after state gas charge to avoid unnecessary work (e.g. memory read,
// Code object creation) on paths where state gas is insufficient.
final Code code = codeSupplier.get();

if (code != null && code.getSize() > evm.getMaxInitcodeSize()) {
frame.popStackItems(getStackItemsConsumed());
return new OperationResult(cost, ExceptionalHaltReason.CODE_TOO_LARGE);
}

final boolean insufficientBalance = value.compareTo(account.getBalance()) > 0;
final boolean maxDepthReached = frame.getDepth() >= 1024;
final boolean invalidState = account.getNonce() == -1 || code == null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,4 +278,69 @@ public long getGasLimit() {

assertThat(result.getHaltReason()).isEqualTo(ExceptionalHaltReason.INSUFFICIENT_GAS);
}

@Test
void oversizedInitcodeHaltsBeforeChargingStateGas() {
// EIP-8037: A CREATE with initcode exceeding maxInitcodeSize must halt with CODE_TOO_LARGE
// BEFORE any state gas is charged, so the state gas reservoir remains unchanged.
final long blockGasLimit = 36_000_000L;
final GasCalculator amsterdamCalc = new AmsterdamGasCalculator();
final FakeCreateOperation amsterdamOp = new FakeCreateOperation(amsterdamCalc);

final EVM evm = MainnetEVMs.amsterdam(EvmConfiguration.DEFAULT);
final int maxInitcodeSize = evm.getMaxInitcodeSize();
// Size just over the limit
final int oversizedLength = maxInitcodeSize + 1;

final UInt256 memoryOffset = UInt256.ZERO;
final MessageFrame frame =
MessageFrame.builder()
.type(MessageFrame.Type.CONTRACT_CREATION)
.contract(Address.ZERO)
.inputData(Bytes.EMPTY)
.sender(Address.fromHexString(SENDER))
.value(Wei.ZERO)
.apparentValue(Wei.ZERO)
.code(new Code(SIMPLE_CREATE))
.completer(__ -> {})
.address(Address.fromHexString(SENDER))
.blockHashLookup((__, ___) -> Hash.ZERO)
.blockValues(
new FakeBlockValues(1337) {
@Override
public long getGasLimit() {
return blockGasLimit;
}
})
.gasPrice(Wei.ZERO)
.miningBeneficiary(Address.ZERO)
.originator(Address.ZERO)
.initialGas(10_000_000L)
.worldUpdater(worldUpdater)
.build();

// Push CREATE args: value=0, offset=0, size=oversizedLength
frame.pushStackItem(Bytes.ofUnsignedLong(oversizedLength));
frame.pushStackItem(memoryOffset);
frame.pushStackItem(Bytes.EMPTY); // value = 0

when(account.getNonce()).thenReturn(55L);
when(account.getBalance()).thenReturn(Wei.ZERO);
when(worldUpdater.getAccount(any())).thenReturn(account);
when(worldUpdater.get(any())).thenReturn(account);
when(worldUpdater.getSenderAccount(any())).thenReturn(account);
when(worldUpdater.getOrCreate(any())).thenReturn(newAccount);
when(newAccount.getCode()).thenReturn(Bytes.EMPTY);
when(newAccount.isStorageEmpty()).thenReturn(true);
when(worldUpdater.updater()).thenReturn(worldUpdater);

final long stateGasBefore = frame.getStateGasReservoir();

final Operation.OperationResult result = amsterdamOp.execute(frame, evm);

assertThat(result.getHaltReason()).isEqualTo(ExceptionalHaltReason.CODE_TOO_LARGE);
assertThat(frame.getStateGasReservoir())
.as("State gas reservoir must be unchanged — no state gas charged for oversized initcode")
.isEqualTo(stateGasBefore);
}
}
6 changes: 3 additions & 3 deletions gradle/verification-metadata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2095,9 +2095,9 @@
<sha256 value="3d598b53708759fc4652a322bddbf9065676bed6dff740b5be4cb08d8b9e9d7f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="ethereum" name="execution-spec-tests" version="bal@v5.5.1">
<artifact name="execution-spec-tests-bal@v5.5.1-fixtures_bal.tar.gz">
<sha256 value="79f81379bc456b9f05d4e7298eba939855d0147b525cd2cadd1206513284ab9e" origin="Manual"/>
<component group="ethereum" name="execution-spec-tests" version="bal@v5.6.1">
<artifact name="execution-spec-tests-bal@v5.6.1-fixtures_bal.tar.gz">
<sha256 value="741530c88f6a48c15184d1504316c02c3a76c2322c410a04b643a85185dc62e9" origin="Manual"/>
</artifact>
</component>
<component group="ethereum" name="execution-spec-tests" version="v5.3.0">
Expand Down
Loading