Skip to content

Commit 26b7536

Browse files
Exclude halt-burned gas from block regular gas (besu-eth#10225)
* Exclude halt-burned gas from block regular gas Signed-off-by: daniellehrner <daniel.lehrner@consensys.net>
1 parent 4bf07a5 commit 26b7536

8 files changed

Lines changed: 168 additions & 16 deletions

File tree

ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,7 @@ public TransactionProcessingResult processTransaction(
529529
.stateGasUsed(initialFrame.getStateGasUsed())
530530
.initialFrameStateGasSpill(initialFrameStateGasSpill)
531531
.stateGasSpillBurned(initialFrame.getStateGasSpillBurned())
532+
.initialFrameRegularHaltBurn(initialFrame.getInitialFrameRegularHaltBurn())
532533
.refundedGas(refundedGas)
533534
.floorCost(floorCost)
534535
.regularGasLimitExceeded(regularGasLimitExceeded)

ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccounting.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ public record GasResult(long effectiveStateGas, long gasUsedByTransaction, long
5555
/** Total state gas spilled into gasRemaining from reverted frames. */
5656
public abstract long stateGasSpillBurned();
5757

58+
/**
59+
* Gas that was sitting unused in the initial frame's gasRemaining at the moment of an exceptional
60+
* halt (EIP-7778/EIP-8037). Paid by the sender (receipts) but must be excluded from block regular
61+
* gas since no operation consumed it.
62+
*/
63+
@Value.Default
64+
public long initialFrameRegularHaltBurn() {
65+
return 0L;
66+
}
67+
5868
/** Gas refunded to the sender. */
5969
public abstract long refundedGas();
6070

@@ -101,7 +111,10 @@ public GasResult calculate() {
101111
// initialFrameStateGasSpill is already included in spillBurned AND stateGas,
102112
// so subtract it from spillBurned to avoid double-counting.
103113
final long regularGas =
104-
executionGas - stateGas - (stateGasSpillBurned() - initialFrameStateGasSpill());
114+
executionGas
115+
- stateGas
116+
- (stateGasSpillBurned() - initialFrameStateGasSpill())
117+
- initialFrameRegularHaltBurn();
105118
if (regularGas < 0) {
106119
// This should not happen under normal circumstances. A negative regularGas indicates a
107120
// bug in gas accounting — log at error level to ensure visibility.

ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccountingTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,45 @@ public void zeroStateGas_preAmsterdamEquivalent() {
159159
assertThat(result.usedGas()).isEqualTo(90_000L);
160160
}
161161

162+
@Test
163+
public void initialFrameRegularHaltBurn_excludedFromRegularGas() {
164+
// EIP-3607 collision scenario: CREATE tx with gas_limit=600k halts at
165+
// ContractCreationProcessor.start(). chargeCreateStateGas charged 131488 state gas
166+
// (spilled into gasRemaining). At halt, gasRemaining=438012 was cleared by
167+
// clearGasRemaining() and captured into initialFrameRegularHaltBurn.
168+
// The sender still pays the full 600k via receipts, but block regular gas must
169+
// only reflect intrinsic regular (i.e. 0 executionGas attributable to the frame
170+
// beyond state gas and halt burn).
171+
final var result =
172+
baseBuilder()
173+
.txGasLimit(600_000L)
174+
.remainingGas(0L)
175+
.stateGasReservoir(0L)
176+
.stateGasUsed(131_488L)
177+
.initialFrameRegularHaltBurn(438_012L)
178+
.build()
179+
.calculate();
180+
181+
// executionGas = 600k - 0 - 0 = 600000
182+
// stateGas = 131_488 + 0 = 131_488
183+
// regularGas = 600_000 - 131_488 - 0 - 438_012 = 30_500
184+
// gasUsedByTransaction = max(30_500, 0) + 131_488 = 161_988
185+
// usedGas = 600_000 - 0 = 600_000 (sender pays full gas_limit)
186+
assertThat(result.effectiveStateGas()).isEqualTo(131_488L);
187+
assertThat(result.gasUsedByTransaction()).isEqualTo(161_988L);
188+
assertThat(result.usedGas()).isEqualTo(600_000L);
189+
}
190+
191+
@Test
192+
public void initialFrameRegularHaltBurn_defaultsToZero() {
193+
// When not set (pre-Amsterdam or non-halt paths), the field should default to 0
194+
// and have no effect on the calculation.
195+
final var result = baseBuilder().txGasLimit(100_000L).remainingGas(30_000L).build().calculate();
196+
197+
// Same as normalPath_regularGasComputedCorrectly (without refund)
198+
assertThat(result.gasUsedByTransaction()).isEqualTo(70_000L);
199+
}
200+
162201
@Test
163202
public void build_failsWhenFieldMissing() {
164203
assertThatThrownBy(() -> TransactionGasAccounting.builder().txGasLimit(100_000L).build())

evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@ public enum Type {
198198
// Metadata fields.
199199
private final Type type;
200200
private State state = State.NOT_STARTED;
201+
// EIP-7778/EIP-8037: Flipped to true once code execution starts; used to distinguish a halt
202+
// that fires during opcode execution (halt-burn counts toward block regular gas) from a halt
203+
// raised pre-execution in the processor's start() (halt-burn must be excluded).
204+
private boolean codeExecuted = false;
201205

202206
// Machine state fields.
203207
private long gasRemaining;
@@ -969,6 +973,46 @@ public long getStateGasSpillBurned() {
969973
return txValues.stateGasSpillBurned()[0];
970974
}
971975

976+
/**
977+
* Accumulates gas that was sitting unused in the initial frame's gasRemaining at the moment of an
978+
* exceptional halt (EIP-7778/EIP-8037). The sender still pays for this gas via receipts, but it
979+
* did not correspond to any executed regular or state gas, so it must be excluded from the block
980+
* regular gas total. Not undone on revert.
981+
*
982+
* @param amount the gasRemaining snapshot taken immediately before clearGasRemaining on the
983+
* initial frame's exceptional halt
984+
*/
985+
public void accumulateInitialFrameRegularHaltBurn(final long amount) {
986+
txValues.initialFrameRegularHaltBurn()[0] += amount;
987+
}
988+
989+
/**
990+
* Returns the gas burned on the initial frame's exceptional halt.
991+
*
992+
* @return accumulated halt-burned gas
993+
*/
994+
public long getInitialFrameRegularHaltBurn() {
995+
return txValues.initialFrameRegularHaltBurn()[0];
996+
}
997+
998+
/**
999+
* Marks that opcode execution has started on this frame. Once set, an exceptional halt is
1000+
* classified as "during code execution" (halt-burned gas counts toward block regular gas) rather
1001+
* than pre-execution (halt-burned gas is excluded).
1002+
*/
1003+
public void markCodeExecuted() {
1004+
this.codeExecuted = true;
1005+
}
1006+
1007+
/**
1008+
* Returns whether opcode execution has started on this frame.
1009+
*
1010+
* @return true if {@link #markCodeExecuted()} was invoked
1011+
*/
1012+
public boolean isCodeExecuted() {
1013+
return codeExecuted;
1014+
}
1015+
9721016
/**
9731017
* Add recipient to the self-destruct set if not already present.
9741018
*

evm/src/main/java/org/hyperledger/besu/evm/frame/TxValues.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@
5656
* undone on revert
5757
* @param stateGasSpillBurned EIP-8037 accumulated state gas that spilled from reverted child
5858
* frames; NOT undone on revert (permanent burn counter for block accounting)
59+
* @param initialFrameRegularHaltBurn EIP-7778/EIP-8037 gas burned when the initial frame halts
60+
* exceptionally (gasRemaining at halt time). Paid by the sender via receipts, but must be
61+
* excluded from block regular gas. Single-element long[] so it is NOT undone on revert.
5962
*/
6063
public record TxValues(
6164
BlockHashLookup blockHashLookup,
@@ -75,7 +78,8 @@ public record TxValues(
7578
UndoScalar<Long> gasRefunds,
7679
UndoScalar<Long> stateGasUsed,
7780
UndoScalar<Long> stateGasReservoir,
78-
long[] stateGasSpillBurned) {
81+
long[] stateGasSpillBurned,
82+
long[] initialFrameRegularHaltBurn) {
7983

8084
/**
8185
* Creates a new TxValues for the initial (depth-0) frame of a transaction. EIP-8037 gas tracking
@@ -120,6 +124,7 @@ public static TxValues forTransaction(
120124
new UndoScalar<>(0L),
121125
new UndoScalar<>(0L),
122126
new UndoScalar<>(0L),
127+
new long[] {0L},
123128
new long[] {0L});
124129
}
125130

evm/src/main/java/org/hyperledger/besu/evm/gascalculator/Eip8037StateGasCostCalculator.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,24 @@ public boolean chargeCodeDelegationStateGas(
239239
}
240240
// New empty accounts incur additional state gas (112 * cpsb)
241241
final long newEmptyAccounts = totalDelegations - alreadyExistingDelegators;
242-
if (newEmptyAccounts > 0) {
243-
return frame.consumeStateGas(
244-
emptyAccountDelegationStateGas(blockGasLimit) * newEmptyAccounts);
242+
if (newEmptyAccounts > 0
243+
&& !frame.consumeStateGas(
244+
emptyAccountDelegationStateGas(blockGasLimit) * newEmptyAccounts)) {
245+
return false;
246+
}
247+
// EIP-8037: the intrinsic state gas is sized assuming every authority is a new empty
248+
// account (emptyAccountDelegationStateGas per auth). For authorities that already exist,
249+
// that pre-charge was not consumed, park it in the state gas reservoir so it is returned
250+
// to the sender alongside any unused gas on halt/revert, matching the specification's
251+
// set_delegation behavior (intrinsic_state_gas -= refund; state_gas_reservoir += refund).
252+
if (alreadyExistingDelegators > 0) {
253+
final long reservoirCredit =
254+
emptyAccountDelegationStateGas(blockGasLimit) * alreadyExistingDelegators;
255+
if (frame.getRemainingGas() < reservoirCredit) {
256+
return false;
257+
}
258+
frame.decrementRemainingGas(reservoirCredit);
259+
frame.incrementStateGasReservoir(reservoirCredit);
245260
}
246261
return true;
247262
}

evm/src/main/java/org/hyperledger/besu/evm/processor/AbstractMessageProcessor.java

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -152,25 +152,27 @@ private void clearAccumulatedStateBesidesGasAndOutput(final MessageFrame frame)
152152
*
153153
* @param frame The message frame
154154
*/
155-
private void handleStateGasSpill(final MessageFrame frame) {
155+
private void handleStateGasSpill(final MessageFrame frame, final boolean isInitialFrame) {
156156
final long stateGasUsedBefore = frame.getStateGasUsed();
157157
final long reservoirBefore = frame.getStateGasReservoir();
158-
final boolean isInitialFrame = frame.getMessageFrameStack().size() == 1;
159158

160159
clearAccumulatedStateBesidesGasAndOutput(frame);
161160

162161
final long stateGasRestored = stateGasUsedBefore - frame.getStateGasUsed();
163162
final long reservoirRestored = frame.getStateGasReservoir() - reservoirBefore;
164163

165164
if (isInitialFrame) {
166-
// EIP-8037: Preserve the reservoir for top-level refund. Use the max of the pre-rollback
167-
// value (which may include child frame refunds that must not be lost) and the post-rollback
168-
// value (which reflects any reservoir drain rollback has already restored).
169-
final long reservoirPostRollback = frame.getStateGasReservoir();
170-
final long preservedReservoir = Math.max(reservoirPostRollback, reservoirBefore);
171-
if (preservedReservoir != reservoirPostRollback) {
172-
frame.setStateGasReservoir(preservedReservoir);
165+
// EIP-8037: For initial-frame halt/revert, state gas consumed by ops is final for block
166+
// accounting (spec: `tx_state_gas = intrinsic_state_gas + state_gas_used`). The portion
167+
// that spilled from gasRemaining is already accounted via stateGasSpillBurned below; the
168+
// portion drained from the reservoir (reservoirRestored) was rolled back by the undo but
169+
// must still count as consumed state gas, so add it back to stateGasUsed. Then preserve
170+
// the actual pre-rollback reservoir value so drain is reflected in total gas returned to
171+
// the sender.
172+
if (reservoirRestored > 0) {
173+
frame.incrementStateGasUsed(reservoirRestored);
173174
}
175+
frame.setStateGasReservoir(reservoirBefore);
174176
// Only burn the portion of state gas that actually spilled into gasRemaining (not the
175177
// portion that was drawn from the reservoir and has already been restored, and not the
176178
// portion that child frames had refunded to the reservoir).
@@ -187,13 +189,39 @@ private void handleStateGasSpill(final MessageFrame frame) {
187189
}
188190
}
189191

192+
/**
193+
* Snapshots the initial frame's gasRemaining into {@code initialFrameRegularHaltBurn} when a
194+
* pre-execution halt fires on the initial frame (e.g. EIP-684 CREATE collision) so that gas paid
195+
* by the sender but never spent on regular or state work is excluded from block regular gas. When
196+
* opcode execution has already run on the frame, the halt-burn must remain in block regular gas
197+
* (no-op here).
198+
*
199+
* @param frame the initial (depth-0) message frame
200+
*/
201+
private static void recordInitialFrameRegularHaltBurn(final MessageFrame frame) {
202+
if (frame.isCodeExecuted()) {
203+
return;
204+
}
205+
final long haltBurn = frame.getRemainingGas();
206+
if (haltBurn > 0) {
207+
frame.accumulateInitialFrameRegularHaltBurn(haltBurn);
208+
}
209+
}
210+
190211
/**
191212
* Gets called when the message frame encounters an exceptional halt.
192213
*
193214
* @param frame The message frame
194215
*/
195216
private void exceptionalHalt(final MessageFrame frame) {
196-
handleStateGasSpill(frame);
217+
final boolean isInitialFrame = frame.getMessageFrameStack().size() == 1;
218+
219+
handleStateGasSpill(frame, isInitialFrame);
220+
221+
if (isInitialFrame) {
222+
recordInitialFrameRegularHaltBurn(frame);
223+
}
224+
197225
frame.clearGasRemaining();
198226
frame.clearOutputData();
199227
frame.setState(MessageFrame.State.COMPLETED_FAILED);
@@ -205,7 +233,9 @@ private void exceptionalHalt(final MessageFrame frame) {
205233
* @param frame The message frame
206234
*/
207235
protected void revert(final MessageFrame frame) {
208-
handleStateGasSpill(frame);
236+
final boolean isInitialFrame = frame.getMessageFrameStack().size() == 1;
237+
handleStateGasSpill(frame, isInitialFrame);
238+
209239
frame.setState(MessageFrame.State.COMPLETED_FAILED);
210240
}
211241

@@ -237,6 +267,7 @@ private void completedFailed(final MessageFrame frame) {
237267
* @param operationTracer The tracer recording execution
238268
*/
239269
private void codeExecute(final MessageFrame frame, final OperationTracer operationTracer) {
270+
frame.markCodeExecuted();
240271
try {
241272
evm.runToHalt(frame, operationTracer);
242273
} catch (final ModificationNotAllowedException e) {

evm/src/main/java/org/hyperledger/besu/evm/processor/MessageCallProcessor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ private void executePrecompile(
181181
final PrecompiledContract contract,
182182
final MessageFrame frame,
183183
final OperationTracer operationTracer) {
184+
// EIP-7778/EIP-8037: precompile execution counts as code execution for the purpose of
185+
// halt-burn classification — an OOG halt below should NOT be treated as a pre-execution
186+
// halt and excluded from block regular gas.
187+
frame.markCodeExecuted();
184188
final long gasRequirement = contract.gasRequirement(frame.getInputData());
185189
final Bytes output;
186190
if (frame.getRemainingGas() < gasRequirement) {

0 commit comments

Comments
 (0)