Skip to content

Commit baecc20

Browse files
feat: EIP-8037 — state gas reservoir model (Amsterdam) (#14)
* feat: EIP-8037 state gas reservoir model + EIP-7778 + EIP-7825 (rebase) Signed-off-by: garyschulte <garyschulte@gmail.com>
1 parent 2e0ff16 commit baecc20

17 files changed

+647
-121
lines changed

src/bytecode/main.zig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,6 @@ pub const BASEFEE: u8 = 0x48;
226226
pub const BLOBHASH: u8 = 0x49;
227227
pub const BLOBBASEFEE: u8 = 0x4A;
228228
pub const SLOTNUM: u8 = 0x4B;
229-
pub const DUPN: u8 = 0xE6;
230-
pub const SWAPN: u8 = 0xE7;
231-
pub const EXCHANGE: u8 = 0xE8;
232229
pub const POP: u8 = 0x50;
233230
pub const MLOAD: u8 = 0x51;
234231
pub const MSTORE: u8 = 0x52;
@@ -314,6 +311,9 @@ pub const LOG1: u8 = 0xA1;
314311
pub const LOG2: u8 = 0xA2;
315312
pub const LOG3: u8 = 0xA3;
316313
pub const LOG4: u8 = 0xA4;
314+
pub const DUPN: u8 = 0xE6;
315+
pub const SWAPN: u8 = 0xE7;
316+
pub const EXCHANGE: u8 = 0xE8;
317317
pub const CREATE: u8 = 0xF0;
318318
pub const CALL: u8 = 0xF1;
319319
pub const CALLCODE: u8 = 0xF2;

src/handler/call_integration_tests.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ test "SSTORE EIP-2200: fails when gas_remaining <= 2300 (Istanbul+)" {
118118
.gas_limit = 2300,
119119
.scheme = .call,
120120
.is_static = false,
121+
.reservoir = 0,
121122
});
122123
try std.testing.expect(!result.success);
123124
}
@@ -149,6 +150,7 @@ test "SSTORE EIP-2200: succeeds with sufficient gas" {
149150
.gas_limit = 50_000,
150151
.scheme = .call,
151152
.is_static = false,
153+
.reservoir = 0,
152154
});
153155
try std.testing.expect(result.success);
154156
}
@@ -188,6 +190,7 @@ test "gas refund propagation: SSTORE clear in sub-call surfaces in CallResult" {
188190
.gas_limit = 100_000,
189191
.scheme = .call,
190192
.is_static = false,
193+
.reservoir = 0,
191194
});
192195

193196
try std.testing.expect(result.success);

src/handler/main.zig

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,13 @@ pub const ValidationError = validation.ValidationError;
3535
pub const ExecutionResult = struct {
3636
/// Execution status
3737
status: ExecutionStatus,
38-
/// Gas used (final, after refund capping and floor enforcement)
38+
/// Gas used for receipt cumulativeGasUsed (= regular + state for Amsterdam+).
3939
gas_used: u64,
40+
/// EIP-8037 (Amsterdam+): gas used for block gas limit = max(regular, state).
41+
/// Equals gas_used for pre-Amsterdam.
42+
block_gas_used: u64,
43+
/// EIP-8037 (Amsterdam+): total state gas charged during execution.
44+
state_gas_used: u64,
4045
/// Gas refunded (final capped refund, set in postExecution)
4146
gas_refunded: u64,
4247
/// Logs emitted during execution
@@ -51,6 +56,8 @@ pub const ExecutionResult = struct {
5156
return ExecutionResult{
5257
.status = status,
5358
.gas_used = gas_used,
59+
.block_gas_used = gas_used,
60+
.state_gas_used = 0,
5461
.gas_refunded = 0,
5562
.logs = std.ArrayList(primitives.Log){},
5663
.return_data = @constCast(&[_]u8{}),
@@ -135,6 +142,9 @@ pub const FrameResult = struct {
135142
gas_remaining: u64,
136143
/// Raw refund counter from interpreter (before capping)
137144
gas_refunded: i64,
145+
/// EIP-8037 (Amsterdam+): state gas reservoir remaining after execution.
146+
/// Used in gasUsed formula: gas_used = tx.gas_limit - gas_remaining - reservoir_remaining.
147+
reservoir_remaining: u64,
138148
/// Memory
139149
memory: interpreter.Memory,
140150
/// Stack
@@ -146,6 +156,7 @@ pub const FrameResult = struct {
146156
.result = result,
147157
.gas_remaining = gas_remaining,
148158
.gas_refunded = gas_refunded,
159+
.reservoir_remaining = 0,
149160
.memory = interpreter.Memory.new(),
150161
.stack = interpreter.Stack.new(),
151162
};
@@ -305,14 +316,17 @@ pub const Frame = struct {
305316

306317
const gas_used = self.interpreter.gas.getSpent();
307318
const gas_refunded = self.interpreter.gas.refunded;
319+
const state_gas_used = self.interpreter.gas.state_gas_used;
308320
const status: ExecutionStatus = switch (self.interpreter.result) {
309321
.stop, .@"return", .selfdestruct => .Success,
310322
.revert => .Revert,
311323
else => .Halt,
312324
};
313325

326+
var exec_result = ExecutionResult.new(status, gas_used);
327+
exec_result.state_gas_used = state_gas_used;
314328
return FrameResult.new(
315-
ExecutionResult.new(status, gas_used),
329+
exec_result,
316330
self.interpreter.gas.remaining,
317331
gas_refunded,
318332
);

src/handler/mainnet_builder.zig

Lines changed: 165 additions & 45 deletions
Large diffs are not rendered by default.

src/handler/precompile_dispatch_tests.zig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ test "precompile dispatch: IDENTITY returns input unchanged" {
8484
.gas_limit = 100_000,
8585
.scheme = .call,
8686
.is_static = false,
87+
.reservoir = 0,
8788
});
8889

8990
try std.testing.expect(result.success);
@@ -115,6 +116,7 @@ test "precompile dispatch: IDENTITY with no data returns empty" {
115116
.gas_limit = 100_000,
116117
.scheme = .call,
117118
.is_static = false,
119+
.reservoir = 0,
118120
});
119121

120122
try std.testing.expect(result.success);
@@ -145,6 +147,7 @@ test "precompile dispatch: out-of-gas fails and consumes all gas" {
145147
.gas_limit = 10, // not enough
146148
.scheme = .call,
147149
.is_static = false,
150+
.reservoir = 0,
148151
});
149152

150153
try std.testing.expect(!result.success);
@@ -174,6 +177,7 @@ test "precompile dispatch: null precompiles falls back to interpreter (no precom
174177
.gas_limit = 100_000,
175178
.scheme = .call,
176179
.is_static = false,
180+
.reservoir = 0,
177181
});
178182

179183
// Without precompile dispatch, IDENTITY address is an empty contract (STOP)

src/handler/validation.zig

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,21 @@ const primitives = @import("primitives");
33
const context = @import("context");
44
const state = @import("state");
55
const main = @import("main.zig");
6+
const interpreter_mod = @import("interpreter");
67

78
// Gas constants for intrinsic gas calculation
89
const TX_BASE_COST: u64 = 21000;
910
const TX_CREATE_COST: u64 = 32000;
11+
// EIP-8037 (Amsterdam+): reduced regular CREATE cost
12+
const TX_CREATE_COST_AMSTERDAM: u64 = 9000;
1013
const CALLDATA_ZERO_BYTE_COST: u64 = 4;
1114
const CALLDATA_NONZERO_BYTE_COST: u64 = 16;
1215
const ACCESS_LIST_ADDRESS_COST: u64 = 2400;
1316
const ACCESS_LIST_STORAGE_KEY_COST: u64 = 1900;
1417
// EIP-7702: per-authorization intrinsic gas (PER_EMPTY_ACCOUNT_COST per EIP-7702 spec)
1518
const TX_EIP7702_AUTH_COST: u64 = 25000;
19+
// EIP-8037 (Amsterdam+): reduced regular per-auth cost
20+
const TX_EIP7702_AUTH_COST_AMSTERDAM: u64 = 7500;
1621
// EIP-7623: token costs (different from calldata gas costs)
1722
const FLOOR_ZERO_TOKEN_COST: u64 = 1;
1823
const FLOOR_NONZERO_TOKEN_COST: u64 = 4;
@@ -92,20 +97,20 @@ pub const Validation = struct {
9297
const tx = &ctx.tx;
9398
const spec = ctx.cfg.spec;
9499

95-
// EIP-3860 (Shanghai+): initcode size limit for CREATE transactions
96-
// EIP-7954 (Amsterdam+): limit doubled to 65536
100+
// EIP-3860 (Shanghai+): initcode size limit for CREATE transactions.
101+
// EIP-7954 (Amsterdam+): max code size doubles to 32768, so max initcode = 65536.
97102
if (primitives.isEnabledIn(spec, .shanghai)) {
98103
if (tx.kind == .Create) {
99-
const calldata_len = if (tx.data) |d| d.items.len else 0;
100104
const max_initcode: usize = if (primitives.isEnabledIn(spec, .amsterdam)) primitives.AMSTERDAM_MAX_INITCODE_SIZE else primitives.MAX_INITCODE_SIZE;
105+
const calldata_len = if (tx.data) |d| d.items.len else 0;
101106
if (calldata_len > max_initcode) {
102107
return ValidationError.CreateInitcodeOverLimit;
103108
}
104109
}
105110
}
106111

107112
// Calculate initial gas cost
108-
const initial_gas = calculateInitialGas(tx, spec);
113+
const initial_gas = calculateInitialGas(tx, spec, ctx.block.gas_limit);
109114

110115
// Calculate floor gas exec-portion (EIP-7623: tokens * 10, only calldata tokens).
111116
// Returns 0 if EIP-7623 is disabled via cfg flag.
@@ -125,9 +130,26 @@ pub const Validation = struct {
125130
}
126131
}
127132

133+
// EIP-8037 (Amsterdam+): compute the state gas portion of the intrinsic cost.
134+
// This is needed to compute gasUsed = max(regular_gas, state_gas) for receipts.
135+
var initial_state_gas: u64 = 0;
136+
if (primitives.isEnabledIn(spec, .amsterdam)) {
137+
const gas_costs = interpreter_mod.gas_costs;
138+
const cpsb = gas_costs.costPerStateByte(ctx.block.gas_limit);
139+
if (tx.kind == .Create) {
140+
initial_state_gas += gas_costs.STATE_BYTES_PER_NEW_ACCOUNT * cpsb;
141+
}
142+
// EIP-7702: auth list state gas — 135*cpsb per auth (base 23 + new-account 112)
143+
if (tx.authorization_list) |auth_list| {
144+
const num_auths: u64 = @intCast(auth_list.items.len);
145+
initial_state_gas += num_auths * ((gas_costs.STATE_BYTES_PER_AUTH_BASE + gas_costs.STATE_BYTES_PER_NEW_ACCOUNT) * cpsb);
146+
}
147+
}
148+
128149
return InitialAndFloorGas{
129150
.initial_gas = initial_gas,
130151
.floor_gas = floor_gas,
152+
.initial_state_gas = initial_state_gas,
131153
};
132154
}
133155

@@ -258,18 +280,28 @@ pub const Validation = struct {
258280
///
259281
/// Breakdown:
260282
/// 21,000 base (always)
261-
/// + 32,000 for CREATE transactions
283+
/// + 32,000 for CREATE transactions (pre-Amsterdam) or 9,000 + 112*cpsb (Amsterdam+)
262284
/// + 4 per zero calldata byte, 16 per non-zero calldata byte
263285
/// + 2,400 per access-list address, 1,900 per access-list storage slot
264-
/// + 25,000 per EIP-7702 authorization list entry (PER_EMPTY_ACCOUNT_COST, Prague+)
286+
/// + 25,000 per EIP-7702 authorization list entry (pre-Amsterdam) or 7,500 + 135*cpsb (Amsterdam+)
265287
/// + GAS_PER_BLOB per EIP-4844 blob hash
266-
pub fn calculateInitialGas(tx: *const context.TxEnv, spec: primitives.SpecId) u64 {
288+
pub fn calculateInitialGas(tx: *const context.TxEnv, spec: primitives.SpecId, block_gas_limit: u64) u64 {
289+
const gas_costs = interpreter_mod.gas_costs;
290+
const cpsb: u64 = if (primitives.isEnabledIn(spec, .amsterdam))
291+
gas_costs.costPerStateByte(block_gas_limit)
292+
else
293+
0;
267294
var gas: u64 = TX_BASE_COST;
268295

269296
// CREATE adds extra base cost (EIP-2, Homestead+).
270-
// Frontier does NOT charge G_TXCREATE (32000) for CREATE transactions.
297+
// Frontier does NOT charge G_TXCREATE for CREATE transactions.
298+
// EIP-8037 (Amsterdam+): regular CREATE cost drops to 9000; state gas (112*cpsb) added.
271299
if (tx.kind == .Create and primitives.isEnabledIn(spec, .homestead)) {
272-
gas += TX_CREATE_COST;
300+
if (primitives.isEnabledIn(spec, .amsterdam)) {
301+
gas += TX_CREATE_COST_AMSTERDAM + gas_costs.STATE_BYTES_PER_NEW_ACCOUNT * cpsb;
302+
} else {
303+
gas += TX_CREATE_COST;
304+
}
273305
}
274306

275307
// Calldata costs:
@@ -310,9 +342,16 @@ pub const Validation = struct {
310342
// Blob fees are deducted in validateAgainstStateAndDeductCaller.
311343

312344
// EIP-7702: authorization list intrinsic gas (Prague+)
345+
// EIP-8037 (Amsterdam+): per-auth cost = 7500 regular + 135*cpsb state (= 23+112 state bytes)
313346
if (primitives.isEnabledIn(spec, .prague)) {
314347
if (tx.authorization_list) |auth_list| {
315-
gas += @as(u64, auth_list.items.len) * TX_EIP7702_AUTH_COST;
348+
const num_auths: u64 = @intCast(auth_list.items.len);
349+
if (primitives.isEnabledIn(spec, .amsterdam)) {
350+
const per_auth = TX_EIP7702_AUTH_COST_AMSTERDAM + (gas_costs.STATE_BYTES_PER_AUTH_BASE + gas_costs.STATE_BYTES_PER_NEW_ACCOUNT) * cpsb;
351+
gas += num_auths * per_auth;
352+
} else {
353+
gas += num_auths * TX_EIP7702_AUTH_COST;
354+
}
316355
}
317356
}
318357

@@ -425,6 +464,13 @@ pub const InitialAndFloorGas = struct {
425464
/// 25,000 (PER_EMPTY_ACCOUNT_COST) added for each valid authorization that sets code.
426465
/// Applied in postExecution with the standard 1/5 cap against total gas used.
427466
auth_refund: i64 = 0,
467+
/// EIP-8037 (Amsterdam+): state gas portion of the intrinsic cost.
468+
/// For CREATE: STATE_BYTES_PER_NEW_ACCOUNT * CPSB.
469+
/// For EIP-7702 auth entries: (STATE_BYTES_PER_AUTH_BASE + STATE_BYTES_PER_NEW_ACCOUNT) * CPSB per auth.
470+
initial_state_gas: u64 = 0,
471+
/// EIP-8037 (Amsterdam+): state gas refunded for valid auths to existing accounts.
472+
/// 112*cpsb per valid auth applied to an existing (non-empty) account. Bypasses 1/5 cap.
473+
auth_state_refund: u64 = 0,
428474
};
429475

430476
/// Validation errors

src/handler/validation_tests.zig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ const InitialAndFloorGas = validation.InitialAndFloorGas;
1919
test "intrinsic gas: base CALL = 21000" {
2020
var tx = context.TxEnv.default();
2121
defer tx.deinit();
22-
const gas = Validation.calculateInitialGas(&tx, primitives.SpecId.prague);
22+
const gas = Validation.calculateInitialGas(&tx, primitives.SpecId.prague, 120_000_000);
2323
try std.testing.expectEqual(@as(u64, 21000), gas);
2424
}
2525

26-
test "intrinsic gas: CREATE adds 32000" {
26+
test "intrinsic gas: CREATE adds 32000 (pre-Amsterdam)" {
2727
var tx = context.TxEnv.default();
2828
defer tx.deinit();
2929
tx.kind = context.TxKind.Create;
30-
const gas = Validation.calculateInitialGas(&tx, primitives.SpecId.prague);
30+
const gas = Validation.calculateInitialGas(&tx, primitives.SpecId.prague, 120_000_000);
3131
try std.testing.expectEqual(@as(u64, 21000 + 32000), gas);
3232
}
3333

@@ -38,7 +38,7 @@ test "intrinsic gas: zero byte costs 4, nonzero costs 16" {
3838
try data.append(std.heap.c_allocator, 0x00); // 4 gas
3939
try data.append(std.heap.c_allocator, 0xFF); // 16 gas
4040
tx.data = data;
41-
const gas = Validation.calculateInitialGas(&tx, primitives.SpecId.prague);
41+
const gas = Validation.calculateInitialGas(&tx, primitives.SpecId.prague, 120_000_000);
4242
try std.testing.expectEqual(@as(u64, 21000 + 4 + 16), gas);
4343
}
4444

src/interpreter/gas.zig

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@ const primitives = @import("primitives");
55
pub const Gas = struct {
66
/// The initial gas limit. This is constant throughout execution.
77
limit: u64,
8-
/// The remaining gas.
8+
/// The remaining regular gas.
99
remaining: u64,
1010
/// Refunded gas. This is used only at the end of execution.
1111
refunded: i64,
12+
/// EIP-8037 (Amsterdam+): total state gas charged during this frame.
13+
state_gas_used: u64,
14+
/// EIP-8037 (Amsterdam+): state gas reservoir (state_gas_left).
15+
/// State gas charges draw from here first, then spill into `remaining`.
16+
reservoir: u64,
1217
/// Memoisation of values for memory expansion cost.
1318
memory: MemoryGas,
1419

@@ -18,6 +23,8 @@ pub const Gas = struct {
1823
.limit = limit,
1924
.remaining = limit,
2025
.refunded = 0,
26+
.state_gas_used = 0,
27+
.reservoir = 0,
2128
.memory = MemoryGas.new(),
2229
};
2330
}
@@ -28,6 +35,8 @@ pub const Gas = struct {
2835
.limit = limit,
2936
.remaining = 0,
3037
.refunded = 0,
38+
.state_gas_used = 0,
39+
.reservoir = 0,
3140
.memory = MemoryGas.new(),
3241
};
3342
}
@@ -78,6 +87,28 @@ pub const Gas = struct {
7887
return true;
7988
}
8089

90+
/// EIP-8037 (Amsterdam+): Charge state gas.
91+
/// Draws from the reservoir first; spills the remainder into `remaining`.
92+
/// Returns false (OOG) if neither pool has enough gas.
93+
pub fn spendStateGas(self: *Gas, amount: u64) bool {
94+
if (self.reservoir >= amount) {
95+
self.reservoir -= amount;
96+
} else if (self.reservoir +| self.remaining >= amount) {
97+
const spill = amount - self.reservoir;
98+
self.reservoir = 0;
99+
self.remaining -= spill;
100+
} else {
101+
return false;
102+
}
103+
self.state_gas_used +|= amount;
104+
return true;
105+
}
106+
107+
/// EIP-8037: Add state gas from a successful sub-frame.
108+
pub fn addStateGasFromChild(self: *Gas, child_state_gas: u64) void {
109+
self.state_gas_used += child_state_gas;
110+
}
111+
81112
/// Spend all remaining gas
82113
pub fn spendAll(self: *Gas) void {
83114
self.remaining = 0;

0 commit comments

Comments
 (0)