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 @@ -529,6 +529,7 @@ public TransactionProcessingResult processTransaction(
.stateGasUsed(initialFrame.getStateGasUsed())
.initialFrameStateGasSpill(initialFrameStateGasSpill)
.stateGasSpillBurned(initialFrame.getStateGasSpillBurned())
.initialFrameRegularHaltBurn(initialFrame.getInitialFrameRegularHaltBurn())
.refundedGas(refundedGas)
.floorCost(floorCost)
.regularGasLimitExceeded(regularGasLimitExceeded)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ public record GasResult(long effectiveStateGas, long gasUsedByTransaction, long
/** Total state gas spilled into gasRemaining from reverted frames. */
public abstract long stateGasSpillBurned();

/**
* Gas that was sitting unused in the initial frame's gasRemaining at the moment of an exceptional
* halt (EIP-7778/EIP-8037). Paid by the sender (receipts) but must be excluded from block regular
* gas since no operation consumed it.
*/
@Value.Default
public long initialFrameRegularHaltBurn() {
return 0L;
}

/** Gas refunded to the sender. */
public abstract long refundedGas();

Expand Down Expand Up @@ -101,7 +111,10 @@ public GasResult calculate() {
// initialFrameStateGasSpill is already included in spillBurned AND stateGas,
// so subtract it from spillBurned to avoid double-counting.
final long regularGas =
executionGas - stateGas - (stateGasSpillBurned() - initialFrameStateGasSpill());
executionGas
- stateGas
- (stateGasSpillBurned() - initialFrameStateGasSpill())
- initialFrameRegularHaltBurn();
if (regularGas < 0) {
// This should not happen under normal circumstances. A negative regularGas indicates a
// bug in gas accounting — log at error level to ensure visibility.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,45 @@ public void zeroStateGas_preAmsterdamEquivalent() {
assertThat(result.usedGas()).isEqualTo(90_000L);
}

@Test
public void initialFrameRegularHaltBurn_excludedFromRegularGas() {
// EIP-3607 collision scenario: CREATE tx with gas_limit=600k halts at
// ContractCreationProcessor.start(). chargeCreateStateGas charged 131488 state gas
// (spilled into gasRemaining). At halt, gasRemaining=438012 was cleared by
// clearGasRemaining() and captured into initialFrameRegularHaltBurn.
// The sender still pays the full 600k via receipts, but block regular gas must
// only reflect intrinsic regular (i.e. 0 executionGas attributable to the frame
// beyond state gas and halt burn).
final var result =
baseBuilder()
.txGasLimit(600_000L)
.remainingGas(0L)
.stateGasReservoir(0L)
.stateGasUsed(131_488L)
.initialFrameRegularHaltBurn(438_012L)
.build()
.calculate();

// executionGas = 600k - 0 - 0 = 600000
// stateGas = 131_488 + 0 = 131_488
// regularGas = 600_000 - 131_488 - 0 - 438_012 = 30_500
// gasUsedByTransaction = max(30_500, 0) + 131_488 = 161_988
// usedGas = 600_000 - 0 = 600_000 (sender pays full gas_limit)
assertThat(result.effectiveStateGas()).isEqualTo(131_488L);
assertThat(result.gasUsedByTransaction()).isEqualTo(161_988L);
assertThat(result.usedGas()).isEqualTo(600_000L);
}

@Test
public void initialFrameRegularHaltBurn_defaultsToZero() {
// When not set (pre-Amsterdam or non-halt paths), the field should default to 0
// and have no effect on the calculation.
final var result = baseBuilder().txGasLimit(100_000L).remainingGas(30_000L).build().calculate();

// Same as normalPath_regularGasComputedCorrectly (without refund)
assertThat(result.gasUsedByTransaction()).isEqualTo(70_000L);
}

@Test
public void build_failsWhenFieldMissing() {
assertThatThrownBy(() -> TransactionGasAccounting.builder().txGasLimit(100_000L).build())
Expand Down
44 changes: 44 additions & 0 deletions evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ public enum Type {
// Metadata fields.
private final Type type;
private State state = State.NOT_STARTED;
// EIP-7778/EIP-8037: Flipped to true once code execution starts; used to distinguish a halt
// that fires during opcode execution (halt-burn counts toward block regular gas) from a halt
// raised pre-execution in the processor's start() (halt-burn must be excluded).
private boolean codeExecuted = false;

// Machine state fields.
private long gasRemaining;
Expand Down Expand Up @@ -969,6 +973,46 @@ public long getStateGasSpillBurned() {
return txValues.stateGasSpillBurned()[0];
}

/**
* Accumulates gas that was sitting unused in the initial frame's gasRemaining at the moment of an
* exceptional halt (EIP-7778/EIP-8037). The sender still pays for this gas via receipts, but it
* did not correspond to any executed regular or state gas, so it must be excluded from the block
* regular gas total. Not undone on revert.
*
* @param amount the gasRemaining snapshot taken immediately before clearGasRemaining on the
* initial frame's exceptional halt
*/
public void accumulateInitialFrameRegularHaltBurn(final long amount) {
txValues.initialFrameRegularHaltBurn()[0] += amount;
}

/**
* Returns the gas burned on the initial frame's exceptional halt.
*
* @return accumulated halt-burned gas
*/
public long getInitialFrameRegularHaltBurn() {
return txValues.initialFrameRegularHaltBurn()[0];
}

/**
* Marks that opcode execution has started on this frame. Once set, an exceptional halt is
* classified as "during code execution" (halt-burned gas counts toward block regular gas) rather
* than pre-execution (halt-burned gas is excluded).
*/
public void markCodeExecuted() {
this.codeExecuted = true;
}

/**
* Returns whether opcode execution has started on this frame.
*
* @return true if {@link #markCodeExecuted()} was invoked
*/
public boolean isCodeExecuted() {
return codeExecuted;
}

/**
* Add recipient to the self-destruct set if not already present.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
* undone on revert
* @param stateGasSpillBurned EIP-8037 accumulated state gas that spilled from reverted child
* frames; NOT undone on revert (permanent burn counter for block accounting)
* @param initialFrameRegularHaltBurn EIP-7778/EIP-8037 gas burned when the initial frame halts
* exceptionally (gasRemaining at halt time). Paid by the sender via receipts, but must be
* excluded from block regular gas. Single-element long[] so it is NOT undone on revert.
*/
public record TxValues(
BlockHashLookup blockHashLookup,
Expand All @@ -75,7 +78,8 @@ public record TxValues(
UndoScalar<Long> gasRefunds,
UndoScalar<Long> stateGasUsed,
UndoScalar<Long> stateGasReservoir,
long[] stateGasSpillBurned) {
long[] stateGasSpillBurned,
long[] initialFrameRegularHaltBurn) {

/**
* Creates a new TxValues for the initial (depth-0) frame of a transaction. EIP-8037 gas tracking
Expand Down Expand Up @@ -120,6 +124,7 @@ public static TxValues forTransaction(
new UndoScalar<>(0L),
new UndoScalar<>(0L),
new UndoScalar<>(0L),
new long[] {0L},
new long[] {0L});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,24 @@ public boolean chargeCodeDelegationStateGas(
}
// New empty accounts incur additional state gas (112 * cpsb)
final long newEmptyAccounts = totalDelegations - alreadyExistingDelegators;
if (newEmptyAccounts > 0) {
return frame.consumeStateGas(
emptyAccountDelegationStateGas(blockGasLimit) * newEmptyAccounts);
if (newEmptyAccounts > 0
&& !frame.consumeStateGas(
emptyAccountDelegationStateGas(blockGasLimit) * newEmptyAccounts)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this modification and it is not inline anymore ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We must continue here and check for alreadyExistingDelegators a bit below as well if we still have state gas here. If we do not have enough state gas frame.consumeStateGas() will return false and we can directly return here.

return false;
}
// EIP-8037: the intrinsic state gas is sized assuming every authority is a new empty
// account (emptyAccountDelegationStateGas per auth). For authorities that already exist,
// that pre-charge was not consumed, park it in the state gas reservoir so it is returned
// to the sender alongside any unused gas on halt/revert, matching the specification's
// set_delegation behavior (intrinsic_state_gas -= refund; state_gas_reservoir += refund).
if (alreadyExistingDelegators > 0) {
final long reservoirCredit =
emptyAccountDelegationStateGas(blockGasLimit) * alreadyExistingDelegators;
if (frame.getRemainingGas() < reservoirCredit) {
return false;
}
frame.decrementRemainingGas(reservoirCredit);
frame.incrementStateGasReservoir(reservoirCredit);
}
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,25 +152,27 @@ private void clearAccumulatedStateBesidesGasAndOutput(final MessageFrame frame)
*
* @param frame The message frame
*/
private void handleStateGasSpill(final MessageFrame frame) {
private void handleStateGasSpill(final MessageFrame frame, final boolean isInitialFrame) {
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;

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);
// EIP-8037: For initial-frame halt/revert, state gas consumed by ops is final for block
// accounting (spec: `tx_state_gas = intrinsic_state_gas + state_gas_used`). The portion
// that spilled from gasRemaining is already accounted via stateGasSpillBurned below; the
// portion drained from the reservoir (reservoirRestored) was rolled back by the undo but
// must still count as consumed state gas, so add it back to stateGasUsed. Then preserve
// the actual pre-rollback reservoir value so drain is reflected in total gas returned to
// the sender.
if (reservoirRestored > 0) {
frame.incrementStateGasUsed(reservoirRestored);
}
frame.setStateGasReservoir(reservoirBefore);
// 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).
Expand All @@ -187,13 +189,39 @@ private void handleStateGasSpill(final MessageFrame frame) {
}
}

/**
* Snapshots the initial frame's gasRemaining into {@code initialFrameRegularHaltBurn} when a
* pre-execution halt fires on the initial frame (e.g. EIP-684 CREATE collision) so that gas paid
* by the sender but never spent on regular or state work is excluded from block regular gas. When
* opcode execution has already run on the frame, the halt-burn must remain in block regular gas
* (no-op here).
*
* @param frame the initial (depth-0) message frame
*/
private static void recordInitialFrameRegularHaltBurn(final MessageFrame frame) {
if (frame.isCodeExecuted()) {
return;
}
final long haltBurn = frame.getRemainingGas();
if (haltBurn > 0) {
frame.accumulateInitialFrameRegularHaltBurn(haltBurn);
}
}

/**
* Gets called when the message frame encounters an exceptional halt.
*
* @param frame The message frame
*/
private void exceptionalHalt(final MessageFrame frame) {
handleStateGasSpill(frame);
final boolean isInitialFrame = frame.getMessageFrameStack().size() == 1;

handleStateGasSpill(frame, isInitialFrame);

if (isInitialFrame) {
recordInitialFrameRegularHaltBurn(frame);
}

frame.clearGasRemaining();
frame.clearOutputData();
frame.setState(MessageFrame.State.COMPLETED_FAILED);
Expand All @@ -205,7 +233,9 @@ private void exceptionalHalt(final MessageFrame frame) {
* @param frame The message frame
*/
protected void revert(final MessageFrame frame) {
handleStateGasSpill(frame);
final boolean isInitialFrame = frame.getMessageFrameStack().size() == 1;
handleStateGasSpill(frame, isInitialFrame);

frame.setState(MessageFrame.State.COMPLETED_FAILED);
}

Expand Down Expand Up @@ -237,6 +267,7 @@ private void completedFailed(final MessageFrame frame) {
* @param operationTracer The tracer recording execution
*/
private void codeExecute(final MessageFrame frame, final OperationTracer operationTracer) {
frame.markCodeExecuted();
try {
evm.runToHalt(frame, operationTracer);
} catch (final ModificationNotAllowedException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ private void executePrecompile(
final PrecompiledContract contract,
final MessageFrame frame,
final OperationTracer operationTracer) {
// EIP-7778/EIP-8037: precompile execution counts as code execution for the purpose of
// halt-burn classification — an OOG halt below should NOT be treated as a pre-execution
// halt and excluded from block regular gas.
frame.markCodeExecuted();
final long gasRequirement = contract.gasRequirement(frame.getInputData());
final Bytes output;
if (frame.getRemainingGas() < gasRequirement) {
Expand Down
Loading