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 @@ -447,20 +447,13 @@ public TransactionProcessingResult processTransaction(
final long initialFrameStateGasSpill =
initialFrame.getStateGasSpillBurned() - spillBurnedBeforeInitialFinal;

// EIP-8037: On exceptional halt of the initial frame, zero the reservoir so all gas is
// consumed. Child frame reverts restore the reservoir via undo, but the initial frame's
// halt means all gas is forfeit. For REVERT, the reservoir was already restored by
// rollback and should be returned to the sender.
if (initialFrame.getExceptionalHaltReason().isPresent()) {
initialFrame.setStateGasReservoir(0L);
}

// EIP-8037: Runtime TX_MAX_GAS_LIMIT enforcement on regular gas only.
// With multidimensional gas, tx.gasLimit can exceed TX_MAX_GAS_LIMIT to accommodate
// state gas, but regular gas consumption is still bounded at runtime.
// For pre-Amsterdam forks, transactionRegularGasLimit() returns Long.MAX_VALUE (always
// passes).
// EIP-8037: Include leftover reservoir in remaining gas for correct consumption calculation
// We also need to include leftover reservoir in remaining gas for correct consumption
// calculation
final long totalRemaining =
initialFrame.getRemainingGas() + initialFrame.getStateGasReservoir();
final long totalConsumed = transaction.getGasLimit() - totalRemaining;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class Eip8037RegularGasLimitTest {
class Eip8037GasLimitTest {

private static final int MAX_STACK_SIZE = 1024;

Expand Down Expand Up @@ -107,19 +107,29 @@ private void setupCommonMocks(final long gasLimit) {
}

@Test
void regularGasExceedingTxMaxGasLimitRevertsTransaction() {
void exceptionalHaltPreservesStateGasReservoirForRefund() {
// EIP-8037: On exceptional halt of the initial frame, the state_gas_reservoir must be
// preserved for transaction-level refund. This test simulates a child frame having refunded
// state gas to the parent's reservoir before the parent runs out of regular gas. The
// refunded reservoir must not be lost: the total gas used should be
// txGasLimit - preserved_reservoir, not the full txGasLimit.
setupCommonMocks(20_000_000L);

// txGasLimit=20M, intrinsic≈21k → gasAvailable≈19,979,000.
// regularBudget = TX_MAX_GAS_LIMIT (16,777,216) - intrinsic ≈ 16,756,216.
// gas_left initial = 16,756,216; reservoir initial = 19,979,000 - 16,756,216 = 3,222,784.
// We simulate: child SSTORE spilled 37,568 into child gas_left, then child halted and the
// spill was restored to the reservoir. The initial frame then runs out of regular gas.
final long childRefund = 37_568L;

doAnswer(
invocation -> {
final MessageFrame frame = invocation.getArgument(0);
// Simulate EXCEPTIONAL_HALT (e.g. ran out of gas mid-execution).
// Setting the halt reason causes MTP to zero the state gas reservoir,
// so totalConsumed = txGasLimit - 0 - 0 = 20M (instead of 20M - reservoir).
// Simulate a child frame halt having added state gas back to the reservoir.
frame.incrementStateGasReservoir(childRefund);
// Now simulate the initial frame running out of regular gas.
frame.setExceptionalHaltReason(Optional.of(ExceptionalHaltReason.INSUFFICIENT_GAS));
frame.setGasRemaining(0);
// stateGas = 2M; regularConsumed = 20M - 2M = 18M > TX_MAX_GAS_LIMIT (16,777,216)
frame.incrementStateGasUsed(2_000_000L);
frame.getMessageFrameStack().pop();
return null;
})
Expand All @@ -138,9 +148,12 @@ void regularGasExceedingTxMaxGasLimitRevertsTransaction() {
Wei.ZERO);

assertThat(result.isSuccessful()).isFalse();
assertThat(result.getEstimateGasUsedByTransaction()).isEqualTo(20_000_000L);
assertThat(result.getValidationResult().getInvalidReason())
.isEqualTo(TransactionInvalidReason.EXECUTION_HALTED);
// The reservoir (initial budget overflow 3,222,784 + childRefund 37,568) must be preserved
// for refund. Total gas used = 20,000,000 - (3,222,784 + 37,568) = 16,739,648.
assertThat(result.getEstimateGasUsedByTransaction())
.isEqualTo(20_000_000L - 3_222_784L - childRefund);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,24 +141,48 @@ private void clearAccumulatedStateBesidesGasAndOutput(final MessageFrame frame)
* parent re-use; for the initial frame it is tracked in stateGasSpillBurned for transaction-level
* gas accounting.
*
* <p>For the initial (top-level) frame, the reservoir must be preserved for transaction-level
* refund. If child frames had restored state gas to the reservoir during the initial frame's
* execution (via their own revert/halt), that refund must not be lost when the initial frame
* subsequently reverts or halts. We therefore restore the reservoir to the higher of the
* pre-rollback value (which may include child refunds) and the post-rollback value (which
* reflects any reservoir drain that rollback undid). We also compute the spill contribution only
* from the positive part of reservoirRestored so that child-refunded gas is never counted as
* burned spill.
*
* @param frame The message frame
*/
private void handleStateGasSpill(final MessageFrame frame) {
final long stateGasUsedBefore = frame.getStateGasUsed();
final long reservoirBefore = frame.getStateGasReservoir();
final boolean isInitialFrame = frame.getMessageFrameStack().size() == 1;

clearAccumulatedStateBesidesGasAndOutput(frame);

final long stateGasRestored = stateGasUsedBefore - frame.getStateGasUsed();
final long reservoirRestored = frame.getStateGasReservoir() - reservoirBefore;
final long spill = Math.max(0L, stateGasRestored - reservoirRestored);
if (spill > 0) {
if (frame.getMessageFrameStack().size() > 1) {

if (isInitialFrame) {
// EIP-8037: Preserve the reservoir for top-level refund. Use the max of the pre-rollback
// value (which may include child frame refunds that must not be lost) and the post-rollback
// value (which reflects any reservoir drain rollback has already restored).
final long reservoirPostRollback = frame.getStateGasReservoir();
final long preservedReservoir = Math.max(reservoirPostRollback, reservoirBefore);
if (preservedReservoir != reservoirPostRollback) {
frame.setStateGasReservoir(preservedReservoir);
}
// Only burn the portion of state gas that actually spilled into gasRemaining (not the
// portion that was drawn from the reservoir and has already been restored, and not the
// portion that child frames had refunded to the reservoir).
final long spill = Math.max(0L, stateGasRestored - Math.max(0L, reservoirRestored));
if (spill > 0) {
frame.accumulateStateGasSpillBurned(spill);
}
} else {
final long spill = Math.max(0L, stateGasRestored - reservoirRestored);
if (spill > 0) {
// Child frame: return spill to reservoir for parent to re-use
frame.incrementStateGasReservoir(spill);
} else {
// Initial frame: track spill for transaction-level gas accounting
frame.accumulateStateGasSpillBurned(spill);
}
}
}
Expand Down
Loading