From 11b2cd3c1eefa4756244fd10a14fdbfb11e02c63 Mon Sep 17 00:00:00 2001 From: 0xpolarzero <0xpolarzero@gmail.com> Date: Wed, 24 Sep 2025 20:07:04 +0200 Subject: [PATCH 1/6] feat: handle blob gas, check tx validity in terms of max gas limit and deduct caller balance --- src/block/block_info.zig | 4 + src/block/transaction_context.zig | 3 + src/eips_and_hardforks/eips.zig | 74 ++++++++ src/evm.zig | 81 ++++++-- src/frame/call_params.zig | 50 ++--- src/primitives/blob.zig | 51 ++--- test/blob_gas_test.zig | 304 ++++++++++++++++++++++++++++++ test/root.zig | 3 + 8 files changed, 511 insertions(+), 59 deletions(-) create mode 100644 test/blob_gas_test.zig diff --git a/src/block/block_info.zig b/src/block/block_info.zig index 8a8975829..b697588cd 100644 --- a/src/block/block_info.zig +++ b/src/block/block_info.zig @@ -51,6 +51,10 @@ pub fn BlockInfo(comptime config: BlockInfoConfig) type { /// Empty slice for non-blob transactions /// TODO: this is a transaction-level setting (should be in TransactionContext) blob_versioned_hashes: []const [32]u8 = &.{}, + /// Excess blob gas after this block (for next block's pricing) + excess_blob_gas: u64 = 0, + /// Blob gas used in this block + blob_gas_used: u64 = 0, /// Beacon block root for EIP-4788 (Dencun) /// Contains the parent beacon block root for trust-minimized access to consensus layer beacon_root: ?[32]u8 = null, diff --git a/src/block/transaction_context.zig b/src/block/transaction_context.zig index fd022e658..4c94f2eb1 100644 --- a/src/block/transaction_context.zig +++ b/src/block/transaction_context.zig @@ -28,6 +28,9 @@ pub const TransactionContext = struct { /// Priority fee per gas / tip (EIP-1559 type 2+ transactions) /// For legacy tx, this is 0 max_priority_fee_per_gas: u64 = 0, + /// Maximum fee per blob gas the transaction is willing to pay (EIP-4844) + /// Set to 0 for non-blob transactions + max_fee_per_blob_gas: u256 = 0, }; test "TransactionContext creation and field access" { diff --git a/src/eips_and_hardforks/eips.zig b/src/eips_and_hardforks/eips.zig index 75e2c5077..5e6bb7b2a 100644 --- a/src/eips_and_hardforks/eips.zig +++ b/src/eips_and_hardforks/eips.zig @@ -506,6 +506,80 @@ pub const Eips = struct { return .{ .effective_gas_price = base_fee_per_gas + max_priority_fee, .miner_fee = max_priority_fee }; } + + /// Get target blob gas per block for current hardfork + pub fn target_blob_gas(self: Self) u64 { + if (!self.is_eip_active(4844)) return 0; + // EIP-7691: Increased blob throughput in Prague + return if (self.hardfork.isAtLeast(.PRAGUE)) + primitives.Blob.TARGET_BLOB_GAS_PER_BLOCK_PRAGUE + else + primitives.Blob.TARGET_BLOB_GAS_PER_BLOCK_CANCUN; + } + + /// Get maximum blob gas per block for current hardfork + pub fn max_blob_gas(self: Self) u64 { + if (!self.is_eip_active(4844)) return 0; + // EIP-7691: Increased blob throughput in Prague + return if (self.hardfork.isAtLeast(.PRAGUE)) + primitives.Blob.MAX_BLOB_GAS_PER_BLOCK_PRAGUE + else + primitives.Blob.MAX_BLOB_GAS_PER_BLOCK_CANCUN; + } + + /// Get blob base fee update fraction for current hardfork + pub fn blob_base_fee_update_fraction(self: Self) u64 { + if (!self.is_eip_active(4844)) return 0; + // EIP-7691: Adjusted update fraction in Prague + return if (self.hardfork.isAtLeast(.PRAGUE)) + primitives.Blob.BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE + else + primitives.Blob.BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN; + } + + /// Calculate blob gas price using exponential formula (EIP-4844) + pub fn blob_gas_price(self: Self, excess_gas: u64) u128 { + if (!self.is_eip_active(4844)) return 0; + const update_fraction = self.blob_base_fee_update_fraction(); + return @as(u128, primitives.Blob.calculate_blob_gas_price(excess_gas, update_fraction)); + } + + /// Validate blob gas parameters for a transaction + pub fn validate_blob_gas(self: Self, blob_count: usize, max_fee_per_blob_gas: u256, current_blob_base_fee: u256) bool { + if (!self.is_eip_active(4844)) return true; + if (blob_count == 0) return true; + + if (blob_count > primitives.Blob.MAX_BLOBS_PER_TRANSACTION) return false; + if (max_fee_per_blob_gas == 0) return false; + if (max_fee_per_blob_gas < current_blob_base_fee) return false; + + return true; + } + + /// Calculate total blob gas cost for a transaction + pub fn blob_gas_cost(self: Self, base_fee: u256, blob_count: usize) u256 { + if (!self.is_eip_active(4844)) return 0; + if (blob_count == 0) return 0; + + const blob_gas = @as(u64, blob_count) * primitives.Blob.BLOB_GAS_PER_BLOB; + return @as(u256, blob_gas) * base_fee; + } + + /// Calculate maximum blob gas cost for balance checks + pub fn max_blob_gas_cost(self: Self, max_fee_per_blob_gas: u256, blob_count: usize) u256 { + if (!self.is_eip_active(4844)) return 0; + if (blob_count == 0) return 0; + + const blob_gas = @as(u64, blob_count) * primitives.Blob.BLOB_GAS_PER_BLOB; + return @as(u256, blob_gas) * max_fee_per_blob_gas; + } + + /// Calculate excess blob gas for next block (wrapper for EIP checking) + pub fn excess_blob_gas(self: Self, parent_excess: u64, parent_blob_gas_used: u64) u64 { + if (!self.is_eip_active(4844)) return 0; + const target = self.target_blob_gas(); + return primitives.Blob.excess_blob_gas(parent_excess, parent_blob_gas_used, target); + } }; const std = @import("std"); diff --git a/src/evm.zig b/src/evm.zig index 0ae4b5fc9..7630fa58c 100644 --- a/src/evm.zig +++ b/src/evm.zig @@ -497,6 +497,50 @@ pub fn Evm(config: EvmConfig) type { if (gas_limit < initial_gas_cost) return CallResult.failure(self.getCallArenaAllocator(), 0) catch unreachable; if (gas_limit < floor_gas_cost) return CallResult.failure(self.getCallArenaAllocator(), 0) catch unreachable; } + + // Validate and calculate blob gas costs for EIP-4844 transactions + const blob_count = self.get_blob_versioned_hashes().len; + const blob_base_fee = self.get_blob_base_fee(); + const max_fee_per_blob_gas = self.get_max_fee_per_blob_gas(); + + if (!config.eips.validate_blob_gas( + blob_count, + max_fee_per_blob_gas, + blob_base_fee, + )) { + if (blob_count > primitives.Blob.MAX_BLOBS_PER_TRANSACTION) { + log.err("Too many blobs: {d}, max allowed: {d}", .{ blob_count, primitives.Blob.MAX_BLOBS_PER_TRANSACTION }); + } else if (blob_count > 0 and max_fee_per_blob_gas == 0) { + log.err("Blob transaction requires max_fee_per_blob_gas to be set", .{}); + } else if (blob_count > 0 and max_fee_per_blob_gas < blob_base_fee) { + log.err("Max fee per blob gas too low: max={d}, current={d}", .{ self.context.max_fee_per_blob_gas, self.block_info.blob_base_fee }); + } + return CallResult.failure(0); + } + + const blob_gas_cost = config.eips.blob_gas_cost(blob_base_fee, blob_count); + const max_blob_gas_cost = config.eips.max_blob_gas_cost(max_fee_per_blob_gas, blob_count); + + // Check if sender can afford gas + value + blob gas + // We need to check this before execution so we can revert the transaction without spending gas + // if the transaction is invalid + if (comptime !config.disable_balance_checks) { + const origin_account = self.database.get_account(self.origin.bytes) catch { + log.err("Failed to get origin account for balance check", .{}); + return CallResult.failure(0); + } orelse Account.zero(); + + // Calculate total cost including blob gas + const max_gas_cost = @as(u256, gas_limit) * self.gas_price; + const value_transfer = params.getValue(); + + const total_cost = max_gas_cost + value_transfer + max_blob_gas_cost; + if (origin_account.balance < total_cost) { + log.err("Insufficient balance for gas + value + blob gas: balance={d}, cost={d}", .{ origin_account.balance, total_cost }); + return CallResult.failure(0); + } + } + const execution_gas_limit = gas_limit - initial_gas_cost; // Increment origin nonce for top-level transactions (EIP-2718) @@ -547,11 +591,13 @@ pub fn Evm(config: EvmConfig) type { } } - // Deduct gas fees from sender's balance and pay coinbase - if (gas_consumed > 0 and self.gas_price > 0) { - const gas_consumed_u256: u256 = @intCast(gas_consumed); - const total_gas_fee = self.gas_price * gas_consumed_u256; + // Calculate total fees (execution gas + blob gas) + const gas_consumed_u256: u256 = @intCast(gas_consumed); + const execution_gas_fee = self.gas_price * gas_consumed_u256; + const total_gas_fee = execution_gas_fee + blob_gas_cost; + // Deduct fees from sender's balance (both execution gas and blob gas) + if (total_gas_fee > 0 and self.gas_price > 0) { // Get origin account var origin_account = self.database.get_account(self.origin.bytes) catch |err| { log.debug("Failed to get origin account for gas fee deduction: {}", .{err}); @@ -582,7 +628,8 @@ pub fn Evm(config: EvmConfig) type { }; // Handle coinbase rewards (miner/validator payment) - if (config.eips.eip_1559_is_enabled()) { + // Note: Blob gas fees are burned, not paid to coinbase + if (config.eips.eip_1559_is_enabled() and execution_gas_fee > 0) { // EIP-1559: Only priority fee goes to coinbase, base fee is burned const base_fee = self.block_info.base_fee; const priority_fee_per_gas = if (self.gas_price > base_fee) @@ -609,18 +656,20 @@ pub fn Evm(config: EvmConfig) type { return result; }; } - // Base fee (total_gas_fee - coinbase_reward) is effectively burned - } else { - // Pre-EIP-1559: All fees go to coinbase - var coinbase_account = self.database.get_account(self.block_info.coinbase.bytes) catch { + // Base fee (execution_gas_fee - coinbase_reward) is effectively burned + } else if (execution_gas_fee > 0) { + // Pre-EIP-1559: All execution gas fees go to coinbase (blob gas is still burned) + var coinbase_account = self.database.get_account( + self.block_info.coinbase.bytes + ) catch { return result; } orelse Account.zero(); self.journal.record_balance_change(0, self.block_info.coinbase, coinbase_account.balance) catch { return result; }; - - coinbase_account.balance += total_gas_fee; + + coinbase_account.balance += execution_gas_fee; self.database.set_account(self.block_info.coinbase.bytes, coinbase_account) catch { return result; }; @@ -2010,6 +2059,16 @@ pub fn Evm(config: EvmConfig) type { return self.block_info.blob_base_fee; } + /// Get max fee per blob gas (EIP-4844) + pub fn get_max_fee_per_blob_gas(self: *Self) u256 { + return self.context.max_fee_per_blob_gas; + } + + /// Get blob versioned hashes (EIP-4844) + pub fn get_blob_versioned_hashes(self: *Self) []const [32]u8 { + return self.context.blob_versioned_hashes; + } + /// Add gas refund amount for SSTORE operations /// This is called by SSTORE when it needs to add refunds pub fn add_gas_refund(self: *Self, amount: u64) void { diff --git a/src/frame/call_params.zig b/src/frame/call_params.zig index 530ba38a9..785127319 100644 --- a/src/frame/call_params.zig +++ b/src/frame/call_params.zig @@ -152,15 +152,15 @@ pub fn CallParams(config: anytype) type { }; } - /// Check if this call operation transfers value - pub fn hasValue(self: @This()) bool { + /// Get the value for this call operation + pub fn getValue(self: @This()) u256 { return switch (self) { - .call => |params| params.value > 0, - .callcode => |params| params.value > 0, - .delegatecall => false, // DELEGATECALL preserves value from parent context - .staticcall => false, // STATICCALL cannot transfer value - .create => |params| params.value > 0, - .create2 => |params| params.value > 0, + .call => |params| params.value, + .callcode => |params| params.value, + .delegatecall => 0, // DELEGATECALL preserves value from parent context + .staticcall => 0, // STATICCALL cannot transfer value + .create => |params| params.value, + .create2 => |params| params.value, }; } @@ -345,7 +345,7 @@ test "call params input access" { try std.testing.expectEqualSlices(u8, init_code, create_op.getInput()); } -test "call params has value checks" { +test "call params getValue checks" { const caller = primitives.ZERO_ADDRESS; const to: Address = .{ .bytes = [_]u8{1} ++ [_]u8{0} ** 19 }; const input = &[_]u8{}; @@ -358,7 +358,7 @@ test "call params has value checks" { .input = input, .gas = 21000, } }; - try std.testing.expect(call_with_value.hasValue()); + try std.testing.expect(call_with_value.getValue() == 1000); // CALL without value const call_no_value = DefaultCallParams{ .call = .{ @@ -368,7 +368,7 @@ test "call params has value checks" { .input = input, .gas = 21000, } }; - try std.testing.expect(!call_no_value.hasValue()); + try std.testing.expect(call_no_value.getValue() == 0); // DELEGATECALL never has value const delegatecall_op = DefaultCallParams{ .delegatecall = .{ @@ -377,7 +377,7 @@ test "call params has value checks" { .input = input, .gas = 21000, } }; - try std.testing.expect(!delegatecall_op.hasValue()); + try std.testing.expect(delegatecall_op.getValue() == 0); // STATICCALL never has value const staticcall_op = DefaultCallParams{ .staticcall = .{ @@ -386,7 +386,7 @@ test "call params has value checks" { .input = input, .gas = 21000, } }; - try std.testing.expect(!staticcall_op.hasValue()); + try std.testing.expect(staticcall_op.getValue() == 0); // CREATE with value const create_with_value = DefaultCallParams{ .create = .{ @@ -395,7 +395,7 @@ test "call params has value checks" { .init_code = &[_]u8{0x00}, .gas = 53000, } }; - try std.testing.expect(create_with_value.hasValue()); + try std.testing.expect(create_with_value.getValue() == 500); } test "call params read only checks" { @@ -479,7 +479,7 @@ test "call params edge cases" { .gas = std.math.maxInt(u64), } }; try std.testing.expectEqual(std.math.maxInt(u64), max_gas_call.getGas()); - try std.testing.expect(max_gas_call.hasValue()); + try std.testing.expect(max_gas_call.getValue() == std.math.maxInt(u256)); // CREATE2 with maximum salt const create2_max_salt = DefaultCallParams{ .create2 = .{ @@ -490,7 +490,7 @@ test "call params edge cases" { .gas = 100000, } }; try std.testing.expect(create2_max_salt.isCreate()); - try std.testing.expect(!create2_max_salt.hasValue()); + try std.testing.expect(create2_max_salt.getValue() == 0); // Empty input data const call_empty_input = DefaultCallParams{ @@ -616,7 +616,7 @@ test "call params callcode operation" { try std.testing.expectEqual(@as(u64, 25000), callcode_op.getGas()); try std.testing.expectEqual(caller, callcode_op.getCaller()); try std.testing.expectEqualSlices(u8, input_data, callcode_op.getInput()); - try std.testing.expect(callcode_op.hasValue()); + try std.testing.expect(callcode_op.getValue() == 1000); try std.testing.expect(!callcode_op.isReadOnly()); try std.testing.expect(!callcode_op.isCreate()); } @@ -648,7 +648,7 @@ test "call params create2 salt handling" { try std.testing.expectEqual(@as(u64, 32000), create2_op.getGas()); try std.testing.expectEqual(caller, create2_op.getCaller()); try std.testing.expectEqualSlices(u8, init_code, create2_op.getInput()); - try std.testing.expect(!create2_op.hasValue()); + try std.testing.expect(create2_op.getValue() == 0); try std.testing.expect(!create2_op.isReadOnly()); try std.testing.expect(create2_op.isCreate()); } @@ -714,7 +714,7 @@ test "call params value edge cases" { .input = &[_]u8{}, .gas = 21000, } }; - try std.testing.expect(call_min_value.hasValue()); + try std.testing.expect(call_min_value.getValue() == 1); // Test maximum value const call_max_value = DefaultCallParams{ .call = .{ @@ -724,7 +724,7 @@ test "call params value edge cases" { .input = &[_]u8{}, .gas = 21000, } }; - try std.testing.expect(call_max_value.hasValue()); + try std.testing.expect(call_max_value.getValue() == std.math.maxInt(u256)); // Test CREATE with max value const create_max_value = DefaultCallParams{ .create = .{ @@ -733,7 +733,7 @@ test "call params value edge cases" { .init_code = init_code, .gas = 53000, } }; - try std.testing.expect(create_max_value.hasValue()); + try std.testing.expect(create_max_value.getValue() == std.math.maxInt(u256)); try std.testing.expect(create_max_value.isCreate()); // Test CREATE2 with max value @@ -744,7 +744,7 @@ test "call params value edge cases" { .salt = 0x123, .gas = 53000, } }; - try std.testing.expect(create2_max_value.hasValue()); + try std.testing.expect(create2_max_value.getValue() == std.math.maxInt(u256)); try std.testing.expect(create2_max_value.isCreate()); } @@ -973,7 +973,7 @@ test "call params all operation types coverage" { _ = op.getGas(); _ = op.getCaller(); _ = op.getInput(); - _ = op.hasValue(); + _ = op.getValue(); _ = op.isReadOnly(); _ = op.isCreate(); } @@ -992,7 +992,7 @@ test "call params method consistency" { .gas = 25000, } }; - try std.testing.expect(!delegatecall_op.hasValue()); + try std.testing.expect(delegatecall_op.getValue() == 0); try std.testing.expect(!delegatecall_op.isReadOnly()); try std.testing.expect(!delegatecall_op.isCreate()); @@ -1004,7 +1004,7 @@ test "call params method consistency" { .gas = 25000, } }; - try std.testing.expect(!staticcall_op.hasValue()); + try std.testing.expect(staticcall_op.getValue() == 0); try std.testing.expect(staticcall_op.isReadOnly()); try std.testing.expect(!staticcall_op.isCreate()); } diff --git a/src/primitives/blob.zig b/src/primitives/blob.zig index ac4605ca2..0f1ee40af 100644 --- a/src/primitives/blob.zig +++ b/src/primitives/blob.zig @@ -11,10 +11,20 @@ pub const BYTES_PER_FIELD_ELEMENT = 32; pub const BYTES_PER_BLOB = FIELD_ELEMENTS_PER_BLOB * BYTES_PER_FIELD_ELEMENT; // 131072 pub const MAX_BLOBS_PER_TRANSACTION = 6; pub const BLOB_COMMITMENT_VERSION_KZG = 0x01; -pub const BLOB_BASE_FEE_UPDATE_FRACTION = 3338477; pub const MIN_BLOB_BASE_FEE = 1; pub const BLOB_GAS_PER_BLOB = 131072; // 2^17 +// Hardfork-specific blob gas parameters +// Cancun (EIP-4844) +pub const TARGET_BLOB_GAS_PER_BLOCK_CANCUN = 393216; // 3 * BLOB_GAS_PER_BLOB +pub const MAX_BLOB_GAS_PER_BLOCK_CANCUN = 786432; // 6 * BLOB_GAS_PER_BLOB +pub const BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN = 3338477; + +// Prague/Electra (EIP-7691) +pub const TARGET_BLOB_GAS_PER_BLOCK_PRAGUE = 786432; // 6 * BLOB_GAS_PER_BLOB +pub const MAX_BLOB_GAS_PER_BLOCK_PRAGUE = 1179648; // 9 * BLOB_GAS_PER_BLOB +pub const BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE = 5007716; + // Blob error types pub const BlobError = error{ NoBlobs, @@ -52,9 +62,8 @@ pub fn is_valid_versioned_hash(h: VersionedHash) bool { } // Calculate blob gas price -pub fn calculate_blob_gas_price(excess_blob_gas: u64) u64 { - // fake_exponential(MIN_BLOB_BASE_FEE, excess_blob_gas, BLOB_BASE_FEE_UPDATE_FRACTION) - return fake_exponential(MIN_BLOB_BASE_FEE, excess_blob_gas, BLOB_BASE_FEE_UPDATE_FRACTION); +pub fn calculate_blob_gas_price(excess_gas: u64, update_fraction: u64) u64 { + return fake_exponential(MIN_BLOB_BASE_FEE, excess_gas, update_fraction); } // Fake exponential from EIP-4844 @@ -76,13 +85,11 @@ fn fake_exponential(factor: u64, numerator: u64, denominator: u64) u64 { } // Calculate excess blob gas for next block -pub fn calculate_excess_blob_gas(parent_excess_blob_gas: u64, parent_blob_gas_used: u64) u64 { - const target_blob_gas_per_block = 393216; // 3 * BLOB_GAS_PER_BLOB - - if (parent_excess_blob_gas + parent_blob_gas_used < target_blob_gas_per_block) { +pub fn excess_blob_gas(parent_excess_blob_gas: u64, parent_blob_gas_used: u64, target_blob_gas: u64) u64 { + if (parent_excess_blob_gas + parent_blob_gas_used < target_blob_gas) { return 0; } else { - return parent_excess_blob_gas + parent_blob_gas_used - target_blob_gas_per_block; + return parent_excess_blob_gas + parent_blob_gas_used - target_blob_gas; } } @@ -191,35 +198,33 @@ test "invalid versioned hash" { test "blob gas price calculation" { // Test with no excess gas - const price_zero = calculate_blob_gas_price(0); + const price_zero = calculate_blob_gas_price(0, BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN); try testing.expectEqual(@as(u64, 1), price_zero); // MIN_BLOB_BASE_FEE // Test with some excess gas - const price_low = calculate_blob_gas_price(131072); // 1 blob worth + const price_low = calculate_blob_gas_price(131072, BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN); // 1 blob worth try testing.expect(price_low > 1); // Test with high excess gas - const price_high = calculate_blob_gas_price(10 * BLOB_GAS_PER_BLOB); + const price_high = calculate_blob_gas_price(10 * BLOB_GAS_PER_BLOB, BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN); try testing.expect(price_high > price_low); } test "excess blob gas calculation" { - const target = 393216; // 3 blobs - // No blobs used, no excess - var excess = calculate_excess_blob_gas(0, 0); + var excess = excess_blob_gas(0, 0, TARGET_BLOB_GAS_PER_BLOCK_CANCUN); try testing.expectEqual(@as(u64, 0), excess); // Used exactly target - excess = calculate_excess_blob_gas(0, target); + excess = excess_blob_gas(0, 0, TARGET_BLOB_GAS_PER_BLOCK_CANCUN); try testing.expectEqual(@as(u64, 0), excess); // Used more than target - excess = calculate_excess_blob_gas(0, target + BLOB_GAS_PER_BLOB); + excess = excess_blob_gas(0, 0, TARGET_BLOB_GAS_PER_BLOCK_CANCUN + BLOB_GAS_PER_BLOB); try testing.expectEqual(BLOB_GAS_PER_BLOB, excess); // With existing excess - excess = calculate_excess_blob_gas(BLOB_GAS_PER_BLOB, target); + excess = excess_blob_gas(BLOB_GAS_PER_BLOB, 0, TARGET_BLOB_GAS_PER_BLOCK_CANCUN); try testing.expectEqual(BLOB_GAS_PER_BLOB, excess); } @@ -295,24 +300,24 @@ test "blob sidecar" { test "blob gas economics" { // Simulate block progression - var excess_blob_gas: u64 = 0; + var excess_gas: u64 = 0; // Block 1: 4 blobs used (above target) var blob_gas_used: u64 = 4 * BLOB_GAS_PER_BLOB; - var blob_price = calculate_blob_gas_price(excess_blob_gas); + var blob_price = calculate_blob_gas_price(excess_gas, BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN); try testing.expectEqual(@as(u64, 1), blob_price); // Min price initially - excess_blob_gas = calculate_excess_blob_gas(excess_blob_gas, blob_gas_used); + excess_blob_gas = excess_blob_gas(excess_gas, blob_gas_used, TARGET_BLOB_GAS_PER_BLOCK_CANCUN); try testing.expect(excess_blob_gas > 0); // Should increase // Block 2: Price should have increased - blob_price = calculate_blob_gas_price(excess_blob_gas); + blob_price = calculate_blob_gas_price(excess_gas, BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN); try testing.expect(blob_price > 1); // Block 3: Use only 1 blob (below target) blob_gas_used = BLOB_GAS_PER_BLOB; const old_excess = excess_blob_gas; - excess_blob_gas = calculate_excess_blob_gas(excess_blob_gas, blob_gas_used); + excess_gas = excess_blob_gas(excess_blob_gas, blob_gas_used, TARGET_BLOB_GAS_PER_BLOCK_CANCUN); try testing.expect(excess_blob_gas < old_excess); // Should decrease } diff --git a/test/blob_gas_test.zig b/test/blob_gas_test.zig new file mode 100644 index 000000000..954045b87 --- /dev/null +++ b/test/blob_gas_test.zig @@ -0,0 +1,304 @@ +const std = @import("std"); +const evm = @import("evm"); +const primitives = @import("primitives"); + +const Evm = evm.Evm; +const eips = evm.Eips; +const Hardfork = evm.Hardfork; +const blob = primitives.Blob; +const Address = primitives.Address.Address; +const Account = evm.Account; +const Database = evm.Database; +const BlockInfo = evm.BlockInfo; +const TransactionContext = evm.TransactionContext; + +test "blob gas calculation - single blob" { + const eips_instance = eips{ .hardfork = .CANCUN }; + + // Single blob should cost exactly GAS_PER_BLOB + const blob_count = 1; + const blob_gas_price = 1; // minimum price + const cost = eips_instance.blob_gas_cost(blob_count, blob_gas_price); + + try std.testing.expectEqual(@as(u256, 131072), cost); // 1 * 131072 +} + +test "blob gas calculation - multiple blobs with higher price" { + const eips_instance = eips{ .hardfork = .CANCUN }; + + // 3 blobs at 10 wei per gas + const blob_count = 3; + const blob_gas_price = 10; + const cost = eips_instance.blob_gas_cost(blob_count, blob_gas_price); + + try std.testing.expectEqual(@as(u256, 3932160), cost); // 3 * 131072 * 10 +} + +test "blob gas calculation - pre-Cancun returns zero" { + const eips_instance = eips{ .hardfork = .SHANGHAI }; + + const blob_count = 1; + const blob_gas_price = 100; + const cost = eips_instance.blob_gas_cost(blob_count, blob_gas_price); + + try std.testing.expectEqual(@as(u256, 0), cost); // Pre-Cancun = no blob gas +} + +test "exponential blob pricing - zero excess" { + const eips_instance = eips{ .hardfork = .CANCUN }; + + const price = eips_instance.blob_gas_price(0); + try std.testing.expectEqual(@as(u128, 1), price); // MIN_blob_gas_price +} + +test "exponential blob pricing - with excess" { + const eips_instance = eips{ .hardfork = .CANCUN }; + + // Test with various excess values + const test_cases = [_]struct { excess: u64, min_expected: u128 }{ + .{ .excess = 0, .min_expected = 1 }, + .{ .excess = 1000000, .min_expected = 1 }, // Small excess, price slightly > 1 + .{ .excess = 10000000, .min_expected = 2 }, // Larger excess + }; + + for (test_cases) |tc| { + const price = eips_instance.blob_gas_price(tc.excess); + try std.testing.expect(price >= tc.min_expected); + } +} + +test "excess blob gas calculation" { + // Test excess calculation for next block + const eips_instance = eips{ .hardfork = .CANCUN }; + + // Case 1: Below target, no excess + var excess = eips_instance.excess_blob_gas(0, 100000); + try std.testing.expectEqual(@as(u64, 0), excess); + + // Case 2: Exactly at target, no excess + excess = eips_instance.excess_blob_gas(0, blob.TARGET_BLOB_GAS_PER_BLOCK_CANCUN); + try std.testing.expectEqual(@as(u64, 0), excess); + + // Case 3: Above target, has excess + excess = eips_instance.excess_blob_gas(0, blob.TARGET_BLOB_GAS_PER_BLOCK_CANCUN + 131072); + try std.testing.expectEqual(@as(u64, 131072), excess); // 1 blob worth of excess + + // Case 4: With existing excess + excess = eips_instance.excess_blob_gas(100000, 300000); + const expected = if (100000 + 300000 > blob.TARGET_BLOB_GAS_PER_BLOCK_CANCUN) + 100000 + 300000 - blob.TARGET_BLOB_GAS_PER_BLOCK_CANCUN + else + 0; + try std.testing.expectEqual(@as(u64, expected), excess); +} + +test "blob transaction validation - too many blobs" { + // Test that transactions with > 6 blobs are rejected + const blob_count = 7; + try std.testing.expect(blob_count > blob.MAX_BLOBS_PER_TRANSACTION); +} + +test "integration - blob transaction full flow" { + const allocator = std.testing.allocator; + + // Setup test database and EVM + var db = Database.init(allocator); + defer db.deinit(); + + // Create origin account with balance + const origin = Address{ .bytes = [_]u8{0x01} ** 20 }; + const origin_balance: u256 = 10_000_000_000_000_000_000; // 10 ETH + const origin_account = Account{ + .balance = origin_balance, + .nonce = 0, + .code_hash = primitives.EMPTY_CODE_HASH, + .storage_root = [_]u8{0} ** 32, + }; + try db.set_account(origin.bytes, origin_account); + + // Create block with blob base fee + const block_info = BlockInfo{ + .chain_id = 1, + .number = 18000000, // Post-Cancun + .parent_hash = [_]u8{0} ** 32, + .timestamp = 1700000000, + .difficulty = 0, + .gas_limit = 30_000_000, + .coinbase = Address{ .bytes = [_]u8{0x02} ** 20 }, + .base_fee = 30_000_000_000, // 30 gwei + .prev_randao = [_]u8{0} ** 32, + .blob_base_fee = 1_000_000_000, // 1 gwei blob base fee + .blob_versioned_hashes = &.{}, + .excess_blob_gas = 0, + .blob_gas_used = 0, + }; + + // Create transaction context with blobs + const blob_hash1 = [_]u8{0x01} ** 32; + const blob_hash2 = [_]u8{0x02} ** 32; + const blob_hashes = [_][32]u8{ blob_hash1, blob_hash2 }; + + const tx_context = TransactionContext{ + .gas_limit = 100000, + .coinbase = block_info.coinbase, + .chain_id = 1, + .blob_versioned_hashes = &blob_hashes, + .blob_base_fee = block_info.blob_base_fee, + .max_fee_per_blob_gas = 5_000_000_000, // 5 gwei max fee per blob gas + }; + + // Initialize EVM + var evm_instance = try Evm(.{}).init(allocator, &db, block_info, tx_context, 35_000_000_000, // gas price 35 gwei (5 gwei priority fee) + origin, .CANCUN); + defer evm_instance.deinit(); + + // Execute a simple call with blob transaction + const CallParams = Evm(.{}).CallParams; + const call_params = CallParams{ + .call = .{ + .caller = origin, + .to = Address{ .bytes = [_]u8{0x03} ** 20 }, + .value = 1_000_000_000_000_000_000, // 1 ETH + .input = &.{}, + .gas = 50000, + }, + }; + + var result = evm_instance.call(call_params); + defer result.deinit(allocator); + + // Verify transaction succeeded + try std.testing.expect(result.success); + + // Verify blob gas was charged + const final_origin_account = try db.get_account(origin.bytes); + try std.testing.expect(final_origin_account.?.balance < origin_balance); + + // Calculate expected charges + const execution_gas_cost = 50000 * 35_000_000_000; // execution gas + const blob_gas_cost = 2 * 131072 * 1_000_000_000; // 2 blobs + const value_transfer = 1_000_000_000_000_000_000; + const total_cost = execution_gas_cost + blob_gas_cost + value_transfer; + + const expected_balance = origin_balance - total_cost; + // Allow for gas refunds and other adjustments + const actual_balance = final_origin_account.?.balance; + const difference = if (expected_balance > actual_balance) + expected_balance - actual_balance + else + actual_balance - expected_balance; + + // Check that the difference is within 1% (accounting for gas refunds) + try std.testing.expect(difference < expected_balance / 100); +} + +test "blob gas cost calculation with max blobs" { + const eips_instance = eips{ .hardfork = .CANCUN }; + + // Test with maximum allowed blobs + const blob_count = blob.MAX_BLOBS_PER_TRANSACTION; + const blob_gas_price = 5_000_000_000; // 5 gwei per gas + const cost = eips_instance.blob_gas_cost(blob_count, blob_gas_price); + + // 6 blobs * 131072 gas/blob * 5 gwei/gas + const expected = @as(u256, 6) * 131072 * 5_000_000_000; + try std.testing.expectEqual(expected, cost); +} + +test "blob gas calculation with zero blobs" { + const eips_instance = eips{ .hardfork = .CANCUN }; + + const blob_count = 0; + const blob_gas_price = 1_000_000_000; + const cost = eips_instance.blob_gas_cost(blob_count, blob_gas_price); + + try std.testing.expectEqual(@as(u256, 0), cost); +} + +test "max_blob_gas_cost" { + const eips_instance = eips{ .hardfork = .CANCUN }; + + const blob_count = 2; + const max_fee_per_blob_gas: u256 = 10_000_000_000; // 10 gwei + const max_cost = eips_instance.max_blob_gas_cost(max_fee_per_blob_gas, blob_count); + + // 2 blobs * 131072 gas/blob * 10 gwei/gas + const expected = @as(u256, 2) * 131072 * 10_000_000_000; + try std.testing.expectEqual(expected, max_cost); +} + +test "blob gas calculation" { + // Test blob gas calculation + try std.testing.expectEqual(@as(u64, 0), 0 * blob.BLOB_GAS_PER_BLOB); + try std.testing.expectEqual(@as(u64, 131072), 1 * blob.BLOB_GAS_PER_BLOB); + try std.testing.expectEqual(@as(u64, 262144), 2 * blob.BLOB_GAS_PER_BLOB); + try std.testing.expectEqual(@as(u64, 393216), 3 * blob.BLOB_GAS_PER_BLOB); + try std.testing.expectEqual(@as(u64, 786432), 6 * blob.BLOB_GAS_PER_BLOB); +} + +test "blob gas constants validation" { + // Verify constants are correctly defined + try std.testing.expectEqual(@as(u64, 131072), blob.BLOB_GAS_PER_BLOB); + try std.testing.expectEqual(@as(u64, 1), blob.MIN_BLOB_BASE_FEE); + try std.testing.expectEqual(@as(u64, 393216), blob.TARGET_BLOB_GAS_PER_BLOCK_CANCUN); + try std.testing.expectEqual(@as(u64, 786432), blob.MAX_BLOB_GAS_PER_BLOCK_CANCUN); + try std.testing.expectEqual(@as(u64, 3338477), blob.BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN); + try std.testing.expectEqual(@as(usize, 6), blob.MAX_BLOBS_PER_TRANSACTION); + + // Verify relationships between constants + try std.testing.expectEqual(blob.TARGET_BLOB_GAS_PER_BLOCK_CANCUN, 3 * blob.BLOB_GAS_PER_BLOB); + try std.testing.expectEqual(blob.MAX_BLOB_GAS_PER_BLOCK_CANCUN, 6 * blob.BLOB_GAS_PER_BLOB); +} + +test "hardfork-specific blob gas parameters - Cancun vs Prague" { + // Test Cancun parameters + const eips_cancun = eips{ .hardfork = .CANCUN }; + try std.testing.expectEqual(@as(u64, 393216), eips_cancun.target_blob_gas()); // 3 blobs + try std.testing.expectEqual(@as(u64, 786432), eips_cancun.max_blob_gas()); // 6 blobs + try std.testing.expectEqual(@as(u64, 3338477), eips_cancun.blob_base_fee_update_fraction()); + + // Test Prague parameters (EIP-7691) + const eips_prague = eips{ .hardfork = .PRAGUE }; + try std.testing.expectEqual(@as(u64, 786432), eips_prague.target_blob_gas()); // 6 blobs + try std.testing.expectEqual(@as(u64, 1179648), eips_prague.max_blob_gas()); // 9 blobs + try std.testing.expectEqual(@as(u64, 5007716), eips_prague.blob_base_fee_update_fraction()); + + // Verify the constants are correctly defined + try std.testing.expectEqual(blob.TARGET_BLOB_GAS_PER_BLOCK_CANCUN, 3 * blob.BLOB_GAS_PER_BLOB); + try std.testing.expectEqual(blob.MAX_BLOB_GAS_PER_BLOCK_CANCUN, 6 * blob.BLOB_GAS_PER_BLOB); + try std.testing.expectEqual(blob.TARGET_BLOB_GAS_PER_BLOCK_PRAGUE, 6 * blob.BLOB_GAS_PER_BLOB); + try std.testing.expectEqual(blob.MAX_BLOB_GAS_PER_BLOCK_PRAGUE, 9 * blob.BLOB_GAS_PER_BLOB); +} + +test "blob gas price calculation with different update fractions" { + const eips_cancun = eips{ .hardfork = .CANCUN }; + const eips_prague = eips{ .hardfork = .PRAGUE }; + + // Test with same excess gas but different hardforks + const excess_gas: u64 = 1000000; + + const price_cancun = eips_cancun.blob_gas_price(excess_gas); + const price_prague = eips_prague.blob_gas_price(excess_gas); + + // Prague has a higher update fraction, so it should have a different price curve + // Both should be non-zero but different + try std.testing.expect(price_cancun > 0); + try std.testing.expect(price_prague > 0); + // The exact relationship depends on the exponential formula +} + +test "excess blob gas calculation with different targets" { + const eips_cancun = eips{ .hardfork = .CANCUN }; + const eips_prague = eips{ .hardfork = .PRAGUE }; + + // Test with gas usage that would create excess in Cancun but not in Prague + const parent_excess: u64 = 0; + const parent_used: u64 = 500000; // Between Cancun target (393216) and Prague target (786432) + + const excess_cancun = eips_cancun.excess_blob_gas(parent_excess, parent_used); + const excess_prague = eips_prague.excess_blob_gas(parent_excess, parent_used); + + // Should have excess in Cancun but not in Prague + try std.testing.expectEqual(@as(u64, 106784), excess_cancun); // 500000 - 393216 + try std.testing.expectEqual(@as(u64, 0), excess_prague); // Below Prague target +} diff --git a/test/root.zig b/test/root.zig index 27205cd2e..93bdf3cf5 100644 --- a/test/root.zig +++ b/test/root.zig @@ -125,6 +125,9 @@ test { // C FFI API tracing tests // _ = @import("evm_c_api_tracing_test.zig"); // Temporarily disabled - missing C symbols + // Blob gas tests + _ = @import("blob_gas_test.zig"); + // Benchmarks // _ = @import("benchmark/baseline_benchmark.zig"); } From 28d6a63a322eca52eaf97cebf8ac94106329868c Mon Sep 17 00:00:00 2001 From: 0xpolarzero <0xpolarzero@gmail.com> Date: Wed, 24 Sep 2025 13:46:27 +0200 Subject: [PATCH 2/6] fix: correct and comprehensive gas costs with calldata & floor gas --- src/eips_and_hardforks/eips.zig | 12 +++++------- src/evm.zig | 31 ++++++++++++++----------------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/eips_and_hardforks/eips.zig b/src/eips_and_hardforks/eips.zig index 5e6bb7b2a..e554de568 100644 --- a/src/eips_and_hardforks/eips.zig +++ b/src/eips_and_hardforks/eips.zig @@ -228,7 +228,6 @@ pub const Eips = struct { return false; } - /// EIP-170: Get maximum contract code size based on hardfork pub fn eip_170_max_code_size(self: Self) u32 { // EIP-170: Contract code size limit (Spurious Dragon) @@ -448,7 +447,7 @@ pub const Eips = struct { /// Get calldata gas cost for zero/non-zero bytes depending on hardfork /// Introduced in genesis hardfork and non-zero bytes reduced from 68 to 16 gas in EIP-2028 (Istanbul) - /// + /// /// The zero/non-zero bytes are counted in tokens with hardfork-dependent logic above so we can reuse /// tokens for both calldata and floor gas pub fn calldata_gas_cost(self: Self, calldata_tokens: u64, is_create: bool, input_len: usize) u64 { @@ -459,7 +458,7 @@ pub const Eips = struct { } break :blk 0; }; - + return base_calldata_cost + init_code_cost; } @@ -941,9 +940,9 @@ test "edge cases - large calldata" { test "regression - hardfork ordering" { // Ensure EIPs are activated in correct order const hardforks = [_]Hardfork{ - .FRONTIER, .HOMESTEAD, .DAO, .TANGERINE, .SPURIOUS, .BYZANTIUM, - .CONSTANTINOPLE, .PETERSBURG, .ISTANBUL, .BERLIN, .LONDON, - .MERGE, .SHANGHAI, .CANCUN, .PRAGUE, + .FRONTIER, .HOMESTEAD, .DAO, .TANGERINE, .SPURIOUS, .BYZANTIUM, + .CONSTANTINOPLE, .PETERSBURG, .ISTANBUL, .BERLIN, .LONDON, .MERGE, + .SHANGHAI, .CANCUN, .PRAGUE, }; // EIP-2028 should be active from Istanbul onwards @@ -1213,7 +1212,6 @@ test "initcode_size_boundaries" { try std.testing.expectEqual(pre_shanghai.size_limit() * 2, post_shanghai.size_limit()); } - test "specific eip helper functions" { const frontier = Eips{ .hardfork = Hardfork.FRONTIER }; const spurious = Eips{ .hardfork = Hardfork.SPURIOUS_DRAGON }; diff --git a/src/evm.zig b/src/evm.zig index 7630fa58c..b21673115 100644 --- a/src/evm.zig +++ b/src/evm.zig @@ -125,13 +125,12 @@ pub fn Evm(config: EvmConfig) type { /// Contracts marked for self-destruction (cold - only used in old hardforks) self_destruct: SelfDestruct, - // State dump tracking - persists across calls /// Tracks all addresses that have been touched (for state dump) touched_addresses: std.AutoHashMap(primitives.Address, void), /// Tracks storage slots that have been modified (address -> list of storage keys) touched_storage: std.AutoHashMap(primitives.Address, std.ArrayList(u256)), - + // Tracer - at the very bottom of memory layout for minimal impact on cache performance /// Tracer for debugging and logging - can be accessed via evm.tracer or frame.evm_ptr.tracer tracer: @import("tracer/tracer.zig").Tracer, @@ -297,14 +296,14 @@ pub fn Evm(config: EvmConfig) type { /// State dump structure for post-state validation pub const StateDump = struct { accounts: std.StringHashMap(AccountState), - + pub const AccountState = struct { balance: u256, nonce: u64, code: []const u8, storage: std.AutoHashMap(u256, u256), }; - + pub fn deinit(self: *StateDump, allocator: std.mem.Allocator) void { var it = self.accounts.iterator(); while (it.next()) |entry| { @@ -315,7 +314,7 @@ pub fn Evm(config: EvmConfig) type { self.accounts.deinit(); } }; - + /// Dump the current state of all accounts in the database /// This is useful for post-state validation in tests pub fn dumpState(self: *Self, allocator: std.mem.Allocator) !StateDump { @@ -334,13 +333,13 @@ pub fn Evm(config: EvmConfig) type { std.debug.print("[DUMP] account not found in database\n", .{}); continue; }; - std.debug.print("[DUMP] found account: balance={d}, nonce={d}\n", .{account.balance, account.nonce}); - + std.debug.print("[DUMP] found account: balance={d}, nonce={d}\n", .{ account.balance, account.nonce }); + // Skip zero balance, zero nonce accounts with no code if (account.balance == 0 and account.nonce == 0 and std.mem.eql(u8, &account.code_hash, &([_]u8{0} ** 32))) { continue; } - + // Convert address to hex string var addr_hex: [42]u8 = undefined; @memcpy(addr_hex[0..2], "0x"); @@ -350,17 +349,17 @@ pub fn Evm(config: EvmConfig) type { addr_hex[2 + i * 2 + 1] = chars[byte & 0x0F]; } const addr_str = try allocator.dupe(u8, &addr_hex); - + // Get code if present var code: []u8 = &.{}; if (!std.mem.eql(u8, &account.code_hash, &([_]u8{0} ** 32))) { const db_code = try self.database.get_code(account.code_hash); code = try allocator.dupe(u8, db_code); } - + // Get storage var storage = std.AutoHashMap(u256, u256).init(allocator); - + // Get tracked storage slots for this address if (self.touched_storage.get(addr)) |slots| { for (slots.items) |slot| { @@ -370,7 +369,7 @@ pub fn Evm(config: EvmConfig) type { } } } - + try state_dump.accounts.put(addr_str, StateDump.AccountState{ .balance = account.balance, .nonce = account.nonce, @@ -378,7 +377,7 @@ pub fn Evm(config: EvmConfig) type { .storage = storage, }); } - + return state_dump; } @@ -659,16 +658,14 @@ pub fn Evm(config: EvmConfig) type { // Base fee (execution_gas_fee - coinbase_reward) is effectively burned } else if (execution_gas_fee > 0) { // Pre-EIP-1559: All execution gas fees go to coinbase (blob gas is still burned) - var coinbase_account = self.database.get_account( - self.block_info.coinbase.bytes - ) catch { + var coinbase_account = self.database.get_account(self.block_info.coinbase.bytes) catch { return result; } orelse Account.zero(); self.journal.record_balance_change(0, self.block_info.coinbase, coinbase_account.balance) catch { return result; }; - + coinbase_account.balance += execution_gas_fee; self.database.set_account(self.block_info.coinbase.bytes, coinbase_account) catch { return result; From d736deb23e7105312ef3244baeb86fe39a21f559 Mon Sep 17 00:00:00 2001 From: 0xpolarzero <0xpolarzero@gmail.com> Date: Wed, 24 Sep 2025 20:07:04 +0200 Subject: [PATCH 3/6] feat: handle blob gas, check tx validity in terms of max gas limit and deduct caller balance --- src/evm.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/evm.zig b/src/evm.zig index b21673115..213560859 100644 --- a/src/evm.zig +++ b/src/evm.zig @@ -665,7 +665,6 @@ pub fn Evm(config: EvmConfig) type { self.journal.record_balance_change(0, self.block_info.coinbase, coinbase_account.balance) catch { return result; }; - coinbase_account.balance += execution_gas_fee; self.database.set_account(self.block_info.coinbase.bytes, coinbase_account) catch { return result; From 733a4f573e0322f54407ed779a299fcfc0474d30 Mon Sep 17 00:00:00 2001 From: 0xpolarzero <0xpolarzero@gmail.com> Date: Sun, 5 Oct 2025 10:15:56 +0200 Subject: [PATCH 4/6] fix: correct CallResult failure --- src/evm.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/evm.zig b/src/evm.zig index 213560859..186ccfc95 100644 --- a/src/evm.zig +++ b/src/evm.zig @@ -514,7 +514,7 @@ pub fn Evm(config: EvmConfig) type { } else if (blob_count > 0 and max_fee_per_blob_gas < blob_base_fee) { log.err("Max fee per blob gas too low: max={d}, current={d}", .{ self.context.max_fee_per_blob_gas, self.block_info.blob_base_fee }); } - return CallResult.failure(0); + return CallResult.failure(self.getCallArenaAllocator(), 0) catch unreachable; } const blob_gas_cost = config.eips.blob_gas_cost(blob_base_fee, blob_count); @@ -526,7 +526,7 @@ pub fn Evm(config: EvmConfig) type { if (comptime !config.disable_balance_checks) { const origin_account = self.database.get_account(self.origin.bytes) catch { log.err("Failed to get origin account for balance check", .{}); - return CallResult.failure(0); + return CallResult.failure(self.getCallArenaAllocator(), 0) catch unreachable; } orelse Account.zero(); // Calculate total cost including blob gas @@ -536,7 +536,7 @@ pub fn Evm(config: EvmConfig) type { const total_cost = max_gas_cost + value_transfer + max_blob_gas_cost; if (origin_account.balance < total_cost) { log.err("Insufficient balance for gas + value + blob gas: balance={d}, cost={d}", .{ origin_account.balance, total_cost }); - return CallResult.failure(0); + return CallResult.failure(self.getCallArenaAllocator(), 0) catch unreachable; } } From 0d49b3049f312feafc6186860621bc058320e837 Mon Sep 17 00:00:00 2001 From: 0xpolarzero <0xpolarzero@gmail.com> Date: Sun, 5 Oct 2025 16:48:33 +0200 Subject: [PATCH 5/6] test: add missing fields --- specs/runner.zig | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/specs/runner.zig b/specs/runner.zig index 59b80034e..cdffba1ab 100644 --- a/specs/runner.zig +++ b/specs/runner.zig @@ -77,6 +77,10 @@ pub fn runJsonTest(allocator: std.mem.Allocator, test_case: std.json.Value) !voi try std.fmt.parseInt(u256, env.?.object.get("currentBlobBaseFee").?.string, 0) else 0, + .excess_blob_gas = if (env != null and env.?.object.get("currentExcessBlobGas") != null) + try std.fmt.parseInt(u64, env.?.object.get("currentExcessBlobGas").?.string, 0) + else 0, + .blob_versioned_hashes = blk: { if (env) |e| if (e.object.get("currentBlobVersionedHashes")) |hashes| { const bytes = try primitives.Hex.hex_to_bytes(allocator, hashes.string); @@ -281,6 +285,11 @@ pub fn runJsonTest(allocator: std.mem.Allocator, test_case: std.json.Value) !voi const max_priority_fee_per_gas = if (tx.object.get("maxPriorityFeePerGas")) |m| blk: { break :blk try parseIntFromJson(m); } else 0; + + // Determine max fee per blob gas + const max_fee_per_blob_gas = if (tx.object.get("maxFeePerBlobGas")) |m| blk: { + break :blk try parseIntFromJson(m); + } else 0; // Create EVM with transaction context (create per transaction to set correct origin) const tx_context = evm.TransactionContext{ @@ -289,6 +298,7 @@ pub fn runJsonTest(allocator: std.mem.Allocator, test_case: std.json.Value) !voi .chain_id = 1, .max_fee_per_gas = max_fee_per_gas, .max_priority_fee_per_gas = max_priority_fee_per_gas, + .max_fee_per_blob_gas = max_fee_per_blob_gas, }; var evm_instance = try evm.DefaultEvm.init( From 3e38a063bef74cb3f9ed293c9531d0daf8dee357 Mon Sep 17 00:00:00 2001 From: 0xpolarzero <0xpolarzero@gmail.com> Date: Sun, 5 Oct 2025 17:40:56 +0200 Subject: [PATCH 6/6] fix: correct blob gas --- specs/runner.zig | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/specs/runner.zig b/specs/runner.zig index cdffba1ab..0c8ba39ca 100644 --- a/specs/runner.zig +++ b/specs/runner.zig @@ -48,6 +48,11 @@ pub fn runJsonTest(allocator: std.mem.Allocator, test_case: std.json.Value) !voi // Parse environment const env = test_case.object.get("env"); + + const excess_blob_gas = if (env != null and env.?.object.get("currentExcessBlobGas") != null) + try std.fmt.parseInt(u64, env.?.object.get("currentExcessBlobGas").?.string, 0) + else 0; + const block_info = evm.BlockInfo{ .number = if (env != null and env.?.object.get("currentNumber") != null) try parseIntFromJson(env.?.object.get("currentNumber").?) @@ -73,22 +78,10 @@ pub fn runJsonTest(allocator: std.mem.Allocator, test_case: std.json.Value) !voi try std.fmt.parseInt(u256, env.?.object.get("currentBaseFee").?.string, 0) else 10, - .blob_base_fee = if (env != null and env.?.object.get("currentBlobBaseFee") != null) - try std.fmt.parseInt(u256, env.?.object.get("currentBlobBaseFee").?.string, 0) - else 0, + .excess_blob_gas = excess_blob_gas, - .excess_blob_gas = if (env != null and env.?.object.get("currentExcessBlobGas") != null) - try std.fmt.parseInt(u64, env.?.object.get("currentExcessBlobGas").?.string, 0) - else 0, + .blob_base_fee = primitives.Blob.calculate_blob_gas_price(excess_blob_gas, primitives.Blob.BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE), - .blob_versioned_hashes = blk: { - if (env) |e| if (e.object.get("currentBlobVersionedHashes")) |hashes| { - const bytes = try primitives.Hex.hex_to_bytes(allocator, hashes.string); - break :blk std.mem.bytesAsSlice([32]u8, bytes); - }; - break :blk &.{}; - }, - .prev_randao = blk: { if (env) |e| if (e.object.get("currentRandom")) |rand| { const bytes = try primitives.Hex.hex_to_bytes(allocator, rand.string); @@ -291,6 +284,20 @@ pub fn runJsonTest(allocator: std.mem.Allocator, test_case: std.json.Value) !voi break :blk try parseIntFromJson(m); } else 0; + const blob_versioned_hashes = blk: { + if (tx.object.get("blobVersionedHashes")) |hashes| if (hashes == .array) { + var list = try allocator.alloc([32]u8, hashes.array.items.len); + for (hashes.array.items, 0..) |item, i| { + const bytes = try primitives.Hex.hex_to_bytes(allocator, item.string); + defer allocator.free(bytes); + @memcpy(&list[i], bytes[0..32]); + } + break :blk list; + }; + break :blk &.{}; + }; + defer if (blob_versioned_hashes.len > 0) allocator.free(blob_versioned_hashes); + // Create EVM with transaction context (create per transaction to set correct origin) const tx_context = evm.TransactionContext{ .gas_limit = 10000000, @@ -299,6 +306,7 @@ pub fn runJsonTest(allocator: std.mem.Allocator, test_case: std.json.Value) !voi .max_fee_per_gas = max_fee_per_gas, .max_priority_fee_per_gas = max_priority_fee_per_gas, .max_fee_per_blob_gas = max_fee_per_blob_gas, + .blob_versioned_hashes = blob_versioned_hashes, }; var evm_instance = try evm.DefaultEvm.init(