diff --git a/README.md b/README.md index ec5411c..606390d 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ The Native Sequencer is a high-performance transaction sequencer designed for La - **State Management**: Track nonces, balances, and receipts - **Observability**: Metrics endpoint for monitoring - **Operator Controls**: Emergency halt, rate limiting, configuration management +- **ExecuteTx Support**: Stateless transaction type (0x05) with automatic forwarding to L1 geth ## Architecture @@ -336,7 +337,7 @@ The sequencer exposes standard Ethereum JSON-RPC endpoints: #### `eth_sendRawTransaction` -Submit a raw transaction to the sequencer. +Submit a raw transaction to the sequencer. Supports both legacy transactions and ExecuteTx transactions (type 0x05). **Request**: ```json @@ -357,6 +358,21 @@ Submit a raw transaction to the sequencer. } ``` +**Transaction Types Supported**: +- **Legacy Transactions**: Standard Ethereum transactions that are validated, stored in mempool, and sequenced into blocks +- **ExecuteTx Transactions (Type 0x05)**: Stateless transactions that are forwarded directly to L1 geth for execution. These transactions include: + - Pre-state hash and witness data for stateless execution + - Withdrawals data + - Blob versioned hashes + - Standard EIP-1559 fields (chainId, nonce, gas, value, etc.) + +**ExecuteTx Handling**: +- ExecuteTx transactions are stateless and designed to be executed by L1 geth +- The sequencer performs minimal validation (signature check for deduplication) +- ExecuteTx transactions are automatically forwarded to L1 geth via `eth_sendRawTransaction` +- Full validation and execution is handled by L1 geth +- ExecuteTx transactions are not stored in the sequencer's mempool + #### `eth_getTransactionReceipt` Get transaction receipt by transaction hash. @@ -432,6 +448,9 @@ This is an experimental implementation. The following features are implemented o - ✅ HTTP server implementation (Zig 0.14.1 networking APIs) - ✅ HTTP client for L1 communication (JSON-RPC support) - ✅ Conditional transaction submission (EIP-7796 support) +- ✅ ExecuteTx transaction type support (type 0x05) +- ✅ ExecuteTx JSON serialization/deserialization +- ✅ ExecuteTx forwarding to L1 geth - ⏳ Complete ECDSA signature verification and recovery (basic implementation) - ⏳ Full transaction execution engine - ⏳ RocksDB/LMDB integration for persistence @@ -484,7 +503,7 @@ The CI pipeline includes: - **Windows (x86_64)**: Builds and verifies binary for Windows #### Docker Build Validation -- **Multi-architecture Docker builds**: Tests Docker image builds for both `linux/amd64` and `linux/arm64` +- **Multi-architecture Docker builds**: Tests Docker image builds for `linux/amd64` (ARM64 builds are currently disabled in CI) - **Image verification**: Validates Docker image structure and metadata - **Runtime testing**: Verifies that the Docker image can start and contains the expected binary @@ -506,15 +525,23 @@ The sequencer uses Zig 0.14.1's standard library networking APIs: - **Connection Handling**: Thread-based concurrent request handling with proper resource cleanup - **RLP Transaction Parsing**: Full RLP decoding support for transaction deserialization -### Custom U256 Implementation +### ExecuteTx Transaction Support + +The sequencer supports ExecuteTx transactions (type 0x05), a stateless transaction type designed for execution by L1 geth nodes. Key features: -Due to a compiler bug in Zig 0.14.x's HashMap implementation with native `u256` types, we use a custom `U256` struct implementation. This struct: -- Uses two `u128` fields to represent 256-bit values -- Provides conversion functions to/from native `u256` and byte arrays -- Includes custom hash and equality functions for HashMap compatibility -- Maintains full compatibility with Ethereum's 32-byte hashes and 20-byte addresses +- **Transaction Type**: EIP-2718 typed transaction with type prefix `0x05` +- **RLP Encoding/Decoding**: Full RLP serialization support matching go-ethereum's ExecuteTx format +- **JSON Serialization**: Complete JSON-RPC serialization/deserialization for ExecuteTx fields +- **Signature Recovery**: ECDSA signature verification and sender address recovery +- **L1 Forwarding**: Automatic forwarding to L1 geth via `eth_sendRawTransaction` +- **Minimal Validation**: Only signature check for deduplication (full validation done by L1 geth) -See `src/core/types.zig` for implementation details and rationale. +ExecuteTx transactions include: +- Standard EIP-1559 fields (chainId, nonce, gas, gasTipCap, gasFeeCap, value, to, data) +- ExecuteTx-specific fields (preStateHash, witness, withdrawals, coinbase, blockNumber, timestamp, blobHashes) +- Signature components (v, r, s) + +See `src/core/transaction_execute.zig` for the complete implementation. ## Known Issues & Workarounds @@ -532,36 +559,6 @@ zig build -Dtarget=x86_64-linux-gnu.2.38 2. Build in a container with glibc 2.38+ 3. Use the Docker build which includes the correct glibc version -### Zig 0.14.x HashMap Allocator Bug (RESOLVED) - -**Status**: ✅ **RESOLVED** - Custom U256 implementation workaround implemented - -This project encountered a compiler bug in Zig 0.14.x related to HashMap initialization with native `u256` types as keys. The error manifests as: -``` -error: access of union field 'pointer' while field 'int' is active -at std/mem/Allocator.zig:425:45 -``` - -**Root Cause**: The bug is in HashMap's `AutoContext` type introspection code when handling large integer types (`u256`). This is a compiler bug, not an issue with our code. - -**Solution**: We implemented a custom `U256` struct using two `u128` fields with explicit `hash()` and `eql()` methods, along with custom HashMap contexts (`HashContext`, `AddressContext`). This bypasses the problematic `AutoContext` code path entirely. - -**Implementation Details**: -- Custom `U256` struct in `src/core/types.zig` with two `u128` fields (`low`, `high`) -- Custom hash function combining both halves via XOR -- Custom equality comparison -- Custom HashMap contexts for `Hash` and `Address` types -- Full compatibility with 32-byte hashes and 20-byte addresses - -**Performance**: No performance penalty - the struct is stack-allocated and operations are efficient. - -See `src/core/types.zig` for detailed comments explaining the implementation. - -### Zig 0.14.x Allocator Bug (Historical) - -This project previously encountered allocator bugs in Zig 0.14.0 and 0.14.1 related to allocating arrays of structs containing slices. **Verified through testing**: The bug exists in both versions (at different line numbers: 400 vs 412). The issue was resolved by using a custom `U256` implementation instead of native `u256` types. - - ## License See LICENSE file. diff --git a/src/core/transaction_execute.zig b/src/core/transaction_execute.zig index 6b7967e..30f7468 100644 --- a/src/core/transaction_execute.zig +++ b/src/core/transaction_execute.zig @@ -11,15 +11,15 @@ pub const ExecuteTxType: u8 = 0x05; pub const ExecuteTx = struct { // Standard EIP-1559 fields - chain_id: types.U256, + chain_id: u256, nonce: u64, - gas_tip_cap: types.U256, // maxPriorityFeePerGas - gas_fee_cap: types.U256, // maxFeePerGas + gas_tip_cap: u256, // maxPriorityFeePerGas + gas_fee_cap: u256, // maxFeePerGas gas: u64, // Execution target to: ?types.Address, - value: types.U256, + value: u256, data: []const u8, // EXECUTE-specific fields @@ -34,9 +34,9 @@ pub const ExecuteTx = struct { blob_hashes: []types.Hash, // Signature - v: types.U256, - r: types.U256, - s: types.U256, + v: u256, + r: u256, + s: u256, const Self = @This(); @@ -53,7 +53,7 @@ pub const ExecuteTx = struct { try prefixed.append(ExecuteTxType); try prefixed.appendSlice(rlp_data); - // keccak256 returns Hash (U256), which is what we need + // keccak256 returns Hash (u256), which is what we need return crypto_hash.keccak256(prefixed.items); } @@ -68,7 +68,7 @@ pub const ExecuteTx = struct { } // ChainID - const chain_id_bytes = self.chain_id.toBytes(); + const chain_id_bytes = types.u256ToBytes(self.chain_id); const chain_id_encoded = try rlp_module.encodeBytes(allocator, &chain_id_bytes); try items.append(chain_id_encoded); @@ -77,12 +77,12 @@ pub const ExecuteTx = struct { try items.append(nonce_encoded); // GasTipCap - const gas_tip_cap_bytes = self.gas_tip_cap.toBytes(); + const gas_tip_cap_bytes = types.u256ToBytes(self.gas_tip_cap); const gas_tip_cap_encoded = try rlp_module.encodeBytes(allocator, &gas_tip_cap_bytes); try items.append(gas_tip_cap_encoded); // GasFeeCap - const gas_fee_cap_bytes = self.gas_fee_cap.toBytes(); + const gas_fee_cap_bytes = types.u256ToBytes(self.gas_fee_cap); const gas_fee_cap_encoded = try rlp_module.encodeBytes(allocator, &gas_fee_cap_bytes); try items.append(gas_fee_cap_encoded); @@ -101,7 +101,7 @@ pub const ExecuteTx = struct { } // Value - const value_bytes = self.value.toBytes(); + const value_bytes = types.u256ToBytes(self.value); const value_encoded = try rlp_module.encodeBytes(allocator, &value_bytes); try items.append(value_encoded); @@ -110,7 +110,7 @@ pub const ExecuteTx = struct { try items.append(data_encoded); // PreStateHash - const pre_state_hash_bytes = self.pre_state_hash.toBytes(); + const pre_state_hash_bytes = types.hashToBytes(self.pre_state_hash); const pre_state_hash_encoded = try rlp_module.encodeBytes(allocator, &pre_state_hash_bytes); try items.append(pre_state_hash_encoded); @@ -154,7 +154,7 @@ pub const ExecuteTx = struct { blob_hashes_items.deinit(); } for (self.blob_hashes) |blob_hash| { - const blob_hash_bytes = blob_hash.toBytes(); + const blob_hash_bytes = types.hashToBytes(blob_hash); const blob_hash_encoded = try rlp_module.encodeBytes(allocator, &blob_hash_bytes); try blob_hashes_items.append(blob_hash_encoded); } @@ -163,17 +163,17 @@ pub const ExecuteTx = struct { try items.append(blob_hashes_list); // V - const v_bytes = self.v.toBytes(); + const v_bytes = types.u256ToBytes(self.v); const v_encoded = try rlp_module.encodeBytes(allocator, &v_bytes); try items.append(v_encoded); // R - const r_bytes = self.r.toBytes(); + const r_bytes = types.u256ToBytes(self.r); const r_encoded = try rlp_module.encodeBytes(allocator, &r_bytes); try items.append(r_encoded); // S - const s_bytes = self.s.toBytes(); + const s_bytes = types.u256ToBytes(self.s); const s_encoded = try rlp_module.encodeBytes(allocator, &s_bytes); try items.append(s_encoded); @@ -210,7 +210,7 @@ pub const ExecuteTx = struct { if (chain_id_result.value.len != 32) return error.InvalidRLP; var chain_id_bytes: [32]u8 = undefined; @memcpy(&chain_id_bytes, chain_id_result.value); - const chain_id = types.U256.fromBytes(chain_id_bytes); + const chain_id = types.u256FromBytes(chain_id_bytes); idx += 1; // Nonce @@ -226,7 +226,7 @@ pub const ExecuteTx = struct { if (gas_tip_cap_result.value.len != 32) return error.InvalidRLP; var gas_tip_cap_bytes: [32]u8 = undefined; @memcpy(&gas_tip_cap_bytes, gas_tip_cap_result.value); - const gas_tip_cap = types.U256.fromBytes(gas_tip_cap_bytes); + const gas_tip_cap = types.u256FromBytes(gas_tip_cap_bytes); idx += 1; // GasFeeCap @@ -236,7 +236,7 @@ pub const ExecuteTx = struct { if (gas_fee_cap_result.value.len != 32) return error.InvalidRLP; var gas_fee_cap_bytes: [32]u8 = undefined; @memcpy(&gas_fee_cap_bytes, gas_fee_cap_result.value); - const gas_fee_cap = types.U256.fromBytes(gas_fee_cap_bytes); + const gas_fee_cap = types.u256FromBytes(gas_fee_cap_bytes); idx += 1; // Gas @@ -264,7 +264,7 @@ pub const ExecuteTx = struct { if (value_result.value.len != 32) return error.InvalidRLP; var value_bytes: [32]u8 = undefined; @memcpy(&value_bytes, value_result.value); - const value = types.U256.fromBytes(value_bytes); + const value = types.u256FromBytes(value_bytes); idx += 1; // Data @@ -368,7 +368,7 @@ pub const ExecuteTx = struct { } var v_bytes: [32]u8 = undefined; @memcpy(&v_bytes, v_result.value); - const v = types.U256.fromBytes(v_bytes); + const v = types.u256FromBytes(v_bytes); idx += 1; // R @@ -384,7 +384,7 @@ pub const ExecuteTx = struct { } var r_bytes: [32]u8 = undefined; @memcpy(&r_bytes, r_result.value); - const r = types.U256.fromBytes(r_bytes); + const r = types.u256FromBytes(r_bytes); idx += 1; // S @@ -400,7 +400,7 @@ pub const ExecuteTx = struct { } var s_bytes: [32]u8 = undefined; @memcpy(&s_bytes, s_result.value); - const s = types.U256.fromBytes(s_bytes); + const s = types.u256FromBytes(s_bytes); // Validate witness and withdrawals sizes if (witness_bytes.len != witness_size) { @@ -471,11 +471,10 @@ pub const ExecuteTx = struct { // and recover the address from the signature const tx_hash = try self.hash(allocator); - // Extract r, s, v from U256 fields - const r_bytes = self.r.toBytes(); - const s_bytes = self.s.toBytes(); - const v_value = self.v.toU256(); - const v_byte = @as(u8, @intCast(v_value & 0xff)); + // Extract r, s, v from u256 fields + const r_bytes = types.u256ToBytes(self.r); + const s_bytes = types.u256ToBytes(self.s); + const v_byte = @as(u8, @intCast(self.v & 0xff)); // Create signature struct const sig = types.Signature{ @@ -493,7 +492,7 @@ pub const ExecuteTx = struct { } /// Get priority for mempool ordering (gas fee cap) - pub fn priority(self: *const Self) types.U256 { + pub fn priority(self: *const Self) u256 { return self.gas_fee_cap; } @@ -506,7 +505,7 @@ pub const ExecuteTx = struct { try obj.put("type", std.json.Value{ .string = try std.fmt.allocPrint(allocator, "0x{d:0>2}", .{ExecuteTxType}) }); // ChainID - const chain_id_hex = try u256ToHex(allocator, self.chain_id.toU256()); + const chain_id_hex = try u256ToHex(allocator, self.chain_id); try obj.put("chainId", std.json.Value{ .string = chain_id_hex }); // Nonce @@ -527,15 +526,15 @@ pub const ExecuteTx = struct { } // MaxPriorityFeePerGas - const gas_tip_cap_hex = try u256ToHex(allocator, self.gas_tip_cap.toU256()); + const gas_tip_cap_hex = try u256ToHex(allocator, self.gas_tip_cap); try obj.put("maxPriorityFeePerGas", std.json.Value{ .string = gas_tip_cap_hex }); // MaxFeePerGas - const gas_fee_cap_hex = try u256ToHex(allocator, self.gas_fee_cap.toU256()); + const gas_fee_cap_hex = try u256ToHex(allocator, self.gas_fee_cap); try obj.put("maxFeePerGas", std.json.Value{ .string = gas_fee_cap_hex }); // Value - const value_hex = try u256ToHex(allocator, self.value.toU256()); + const value_hex = try u256ToHex(allocator, self.value); try obj.put("value", std.json.Value{ .string = value_hex }); // Input (data) @@ -543,7 +542,7 @@ pub const ExecuteTx = struct { try obj.put("input", std.json.Value{ .string = input_hex }); // PreStateHash - const pre_state_hash_bytes = self.pre_state_hash.toBytes(); + const pre_state_hash_bytes = types.hashToBytes(self.pre_state_hash); const pre_state_hash_hex = try bytesToHex(allocator, &pre_state_hash_bytes); try obj.put("preStateHash", std.json.Value{ .string = pre_state_hash_hex }); @@ -581,7 +580,7 @@ pub const ExecuteTx = struct { var blob_array = std.ArrayList(std.json.Value).init(allocator); errdefer blob_array.deinit(); for (self.blob_hashes) |blob_hash| { - const blob_hash_bytes = blob_hash.toBytes(); + const blob_hash_bytes = types.hashToBytes(blob_hash); const blob_hash_hex = try bytesToHex(allocator, &blob_hash_bytes); try blob_array.append(std.json.Value{ .string = blob_hash_hex }); } @@ -589,15 +588,15 @@ pub const ExecuteTx = struct { } // V - const v_hex = try u256ToHex(allocator, self.v.toU256()); + const v_hex = try u256ToHex(allocator, self.v); try obj.put("v", std.json.Value{ .string = v_hex }); // R - const r_hex = try u256ToHex(allocator, self.r.toU256()); + const r_hex = try u256ToHex(allocator, self.r); try obj.put("r", std.json.Value{ .string = r_hex }); // S - const s_hex = try u256ToHex(allocator, self.s.toU256()); + const s_hex = try u256ToHex(allocator, self.s); try obj.put("s", std.json.Value{ .string = s_hex }); return std.json.Value{ .object = obj }; @@ -874,7 +873,7 @@ pub const ExecuteTx = struct { const s = try hexToU256(s_hex); // V (required, can be from v or yParity) - var v: types.U256 = undefined; + var v: u256 = undefined; if (obj.get("v")) |v_val| { const v_hex = switch (v_val) { .string => |v_str| v_str, @@ -900,9 +899,8 @@ pub const ExecuteTx = struct { }; const yparity = try hexToU64(yparity_hex); // Convert yParity to v (for EIP-155, v = chain_id * 2 + 35 + yParity) - const chain_id_u64 = chain_id.toU256(); - const v_value = (chain_id_u64 * 2) + 35 + yparity; - v = types.U256.fromU256(v_value); + const v_value = (chain_id * 2) + 35 + yparity; + v = v_value; } else { blob_hashes.deinit(); allocator.free(data_bytes); @@ -947,13 +945,13 @@ pub const ExecuteTx = struct { // Helper functions for JSON serialization fn u256ToHex(allocator: std.mem.Allocator, value: u256) ![]u8 { - const bytes = types.U256.fromU256(value).toBytes(); + const bytes = types.u256ToBytes(value); return bytesToHex(allocator, &bytes); } -fn hexToU256(hex_str: []const u8) !types.U256 { +fn hexToU256(hex_str: []const u8) !u256 { const bytes = try hexToBytesNoAlloc(hex_str); - return types.U256.fromBytes(bytes); + return types.u256FromBytes(bytes); } fn hexToU64(hex_str: []const u8) !u64 { diff --git a/src/core/types.zig b/src/core/types.zig index 01e3213..a7ee862 100644 --- a/src/core/types.zig +++ b/src/core/types.zig @@ -2,127 +2,66 @@ const std = @import("std"); -// ============================================================================ -// Custom U256 Implementation - Allocator Bug Workaround -// ============================================================================ -// -// PROBLEM: -// Zig 0.14.x has a compiler bug in HashMap's AutoContext when using native -// u256 types as HashMap keys. The error manifests as: -// "error: access of union field 'pointer' while field 'int' is active" -// at std/mem/Allocator.zig:425:45 -// -// This bug occurs during HashMap initialization when the allocator tries to -// determine the type information for u256 keys. The issue is in the standard -// library's type introspection code, not in our code. -// -// ATTEMPTED SOLUTIONS: -// 1. Native u256 with AutoContext → "int" allocator error -// 2. Wrapper structs ([32]u8) with custom contexts → "struct" allocator error -// 3. Custom U256 struct (two u128 fields) with custom contexts → ✅ WORKS -// -// SOLUTION: -// We implement a custom U256 struct using two u128 fields (primitive types) -// and provide explicit hash() and eql() methods. This allows us to use custom -// HashMap contexts (HashContext, AddressContext) that bypass the problematic -// AutoContext code path entirely. -// -// WHY THIS WORKS: -// - u128 is a primitive type that HashMap handles correctly -// - Custom contexts give explicit control over hashing/equality -// - Avoids the allocator's type introspection bug with large integers -// - Maintains full 32-byte hash and 20-byte address functionality -// -// PERFORMANCE: -// - Struct with two u128 fields is stack-allocated (no heap allocation) -// - Hash function is simple XOR of both halves (fast) -// - Equality check compares both fields (efficient) -// - No performance penalty compared to native u256 for our use cases -// -// ============================================================================ +// Use native u256 types for Hash and Address +pub const Hash = u256; // 32 bytes +pub const Address = u256; // 20 bytes (stored as u256, but only 20 bytes are used) -pub const U256 = struct { - low: u128, - high: u128, - - pub fn fromU256(value: u256) U256 { - return .{ - .low = @truncate(value), - .high = @truncate(value >> 128), - }; - } - - pub fn toU256(self: U256) u256 { - return (@as(u256, self.high) << 128) | self.low; - } - - pub fn fromBytes(bytes: [32]u8) U256 { - var low: u128 = 0; - var high: u128 = 0; - var i: usize = 0; - while (i < 16) : (i += 1) { - low = (low << 8) | bytes[15 - i]; - } - while (i < 32) : (i += 1) { - high = (high << 8) | bytes[31 - i]; - } - return .{ .low = low, .high = high }; - } - - pub fn toBytes(self: U256) [32]u8 { - var result: [32]u8 = undefined; - var temp_low = self.low; - var temp_high = self.high; - var i: usize = 0; - while (i < 16) : (i += 1) { - result[15 - i] = @as(u8, @truncate(temp_low & 0xff)); - temp_low >>= 8; - } - i = 16; - while (i < 32) : (i += 1) { - result[31 - i] = @as(u8, @truncate(temp_high & 0xff)); - temp_high >>= 8; - } - return result; +/// Convert 32-byte array to Hash (u256) +pub fn hashFromBytes(bytes: [32]u8) Hash { + var result: u256 = 0; + var i: usize = 0; + while (i < 32) : (i += 1) { + result = (result << 8) | bytes[i]; } + return result; +} - pub fn eql(self: U256, other: U256) bool { - return self.low == other.low and self.high == other.high; +/// Convert Hash (u256) to 32-byte array +pub fn hashToBytes(hash: Hash) [32]u8 { + var result: [32]u8 = undefined; + var temp = hash; + var i: usize = 32; + while (i > 0) { + i -= 1; + result[i] = @as(u8, @truncate(temp & 0xff)); + temp >>= 8; } + return result; +} - pub fn hash(self: U256) u64 { - // Simple hash combining both halves - return @as(u64, @truncate(self.low)) ^ @as(u64, @truncate(self.low >> 64)) ^ @as(u64, @truncate(self.high)) ^ @as(u64, @truncate(self.high >> 64)); - } -}; +/// Convert 32-byte array to u256 (for general u256 values, not just hashes) +pub fn u256FromBytes(bytes: [32]u8) u256 { + return hashFromBytes(bytes); +} -// Use custom U256 struct instead of native u256 to avoid allocator bug -pub const Address = U256; // 20 bytes padded to 32 bytes +/// Convert u256 to 32-byte array (for general u256 values, not just hashes) +pub fn u256ToBytes(value: u256) [32]u8 { + return hashToBytes(value); +} +/// Convert 20-byte address to Address (u256) pub fn addressFromBytes(bytes: [20]u8) Address { - var padded: [32]u8 = undefined; - @memset(padded[0..12], 0); - @memcpy(padded[12..32], &bytes); - return U256.fromBytes(padded); + var result: u256 = 0; + var i: usize = 0; + while (i < 20) : (i += 1) { + result = (result << 8) | bytes[i]; + } + return result; } +/// Convert Address (u256) to 20-byte array pub fn addressToBytes(addr: Address) [20]u8 { - const bytes = addr.toBytes(); var result: [20]u8 = undefined; - @memcpy(&result, bytes[12..32]); + var temp = addr; + var i: usize = 20; + while (i > 0) { + i -= 1; + result[i] = @as(u8, @truncate(temp & 0xff)); + temp >>= 8; + } return result; } -pub const Hash = U256; // 32 bytes - -pub fn hashFromBytes(bytes: [32]u8) Hash { - return U256.fromBytes(bytes); -} - -pub fn hashToBytes(hash: Hash) [32]u8 { - return hash.toBytes(); -} - /// ECDSA signature (r, s, v format) pub const Signature = struct { r: [32]u8, diff --git a/src/crypto/signature.zig b/src/crypto/signature.zig index f19e81c..8098e35 100644 --- a/src/crypto/signature.zig +++ b/src/crypto/signature.zig @@ -107,7 +107,7 @@ pub fn recoverAddress(tx: *const transaction.Transaction) !types.Address { defer _ = gpa.deinit(); const allocator = gpa.allocator(); const tx_hash = try tx.hash(allocator); - // tx_hash is U256 struct (stack-allocated), no need to free + // tx_hash is u256 (stack-allocated), no need to free // Create signature struct from transaction fields const sig = types.Signature{ @@ -147,7 +147,7 @@ pub fn verifySignature(tx: *const transaction.Transaction) !bool { std.log.debug("Failed to compute transaction hash", .{}); return false; }; - // tx_hash is U256 struct (stack-allocated), no need to free + // tx_hash is u256 (stack-allocated), no need to free // Step 3: Create signature struct const sig = types.Signature{ @@ -172,8 +172,8 @@ pub fn verifySignature(tx: *const transaction.Transaction) !bool { return false; }; - // Step 7: Compare addresses (U256 comparison) - const addresses_match = recovered_address.eql(expected_sender); + // Step 7: Compare addresses (u256 comparison) + const addresses_match = recovered_address == expected_sender; if (!addresses_match) { std.log.debug("Recovered address does not match expected sender", .{}); } diff --git a/src/crypto/signature_test.zig b/src/crypto/signature_test.zig index c7fddb1..f1a7d3d 100644 --- a/src/crypto/signature_test.zig +++ b/src/crypto/signature_test.zig @@ -96,13 +96,13 @@ test "signature verification - roundtrip" { // Create a test transaction const tx = transaction.Transaction{ .nonce = 1, - .gas_price = types.U256.fromU256(1000000000), + .gas_price = 1000000000, .gas_limit = 21000, .to = types.addressFromBytes([_]u8{ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x12, 0x34, 0x56, 0x78, }), - .value = types.U256.fromU256(1000000000000000000), + .value = 1000000000000000000, .data = &[_]u8{}, .v = 0, .r = [_]u8{0} ** 32, @@ -111,7 +111,7 @@ test "signature verification - roundtrip" { // Hash the transaction const tx_hash = try tx.hash(allocator); - defer allocator.free(tx_hash.toBytes()); + // Note: hashToBytes returns a stack-allocated array, no need to free // Sign the transaction const sig = try secp256k1.sign(tx_hash, private_key); @@ -129,7 +129,7 @@ test "signature verification - roundtrip" { // Test recovery const recovered_address = try signature.recoverAddress(&signed_tx); const expected_address = try signed_tx.sender(); - try testing.expect(recovered_address.eql(expected_address)); + try testing.expect(recovered_address == expected_address); // Clean up transaction data allocator.free(tx.data); @@ -143,10 +143,10 @@ test "signature verification - invalid signature" { // Create a transaction with invalid signature (zero r) var tx = transaction.Transaction{ .nonce = 1, - .gas_price = types.U256.fromU256(1000000000), + .gas_price = 1000000000, .gas_limit = 21000, .to = null, - .value = types.U256.fromU256(0), + .value = 0, .data = &[_]u8{}, .v = 27, .r = [_]u8{0} ** 32, // Invalid: zero r @@ -165,10 +165,10 @@ test "signature verification - invalid v value" { // Create a transaction with invalid v value var tx = transaction.Transaction{ .nonce = 1, - .gas_price = types.U256.fromU256(1000000000), + .gas_price = 1000000000, .gas_limit = 21000, .to = null, - .value = types.U256.fromU256(0), + .value = 0, .data = &[_]u8{}, .v = 26, // Invalid: < 27 .r = [_]u8{1} ** 32, diff --git a/src/main.zig b/src/main.zig index 70e5eb7..a03c870 100644 --- a/src/main.zig +++ b/src/main.zig @@ -157,7 +157,7 @@ fn sequencingLoop(seq: *lib.sequencer.Sequencer, batch_builder: *lib.batch.Build fn formatHash(hash: lib.core.types.Hash) []const u8 { // Format hash as hex string for logging - const bytes = hash.toBytes(); + const bytes = lib.core.types.hashToBytes(hash); var buffer: [66]u8 = undefined; // "0x" + 64 hex chars buffer[0] = '0'; buffer[1] = 'x'; diff --git a/src/mempool/mempool.zig b/src/mempool/mempool.zig index 4dfebf2..ea57554 100644 --- a/src/mempool/mempool.zig +++ b/src/mempool/mempool.zig @@ -25,26 +25,6 @@ fn compareQueueEntry(_: void, a: QueueEntry, b: QueueEntry) std.math.Order { return .eq; } -// Custom hash context for U256 struct (two u128 fields) -// This avoids the allocator bug with native u256 -const HashContext = struct { - pub fn hash(_: @This(), key: core.types.Hash) u64 { - return key.hash(); - } - pub fn eql(_: @This(), a: core.types.Hash, b: core.types.Hash) bool { - return a.eql(b); - } -}; - -const AddressContext = struct { - pub fn hash(_: @This(), key: core.types.Address) u64 { - return key.hash(); - } - pub fn eql(_: @This(), a: core.types.Address, b: core.types.Address) bool { - return a.eql(b); - } -}; - // Custom transaction storage that avoids allocating arrays // Uses length-prefixed storage: each transaction is stored as [4-byte length][data] // This avoids needing a separate offsets array @@ -159,35 +139,32 @@ pub const Mempool = struct { // Store transactions in custom storage (avoids HashMap/ArrayList with slices) storage: TransactionStorage, // HashMap stores hash -> index (usize, not a slice) - // Use custom context for U256 struct to avoid allocator bug - by_hash: std.HashMap(core.types.Hash, usize, HashContext, std.hash_map.default_max_load_percentage), + by_hash: std.HashMap(core.types.Hash, usize, std.hash_map.AutoContext(core.types.Hash), std.hash_map.default_max_load_percentage), // Store metadata separately (hash -> metadata) - metadata: std.HashMap(core.types.Hash, EntryMetadata, HashContext, std.hash_map.default_max_load_percentage), + metadata: std.HashMap(core.types.Hash, EntryMetadata, std.hash_map.AutoContext(core.types.Hash), std.hash_map.default_max_load_percentage), // Priority queue stores hash with metadata (avoids copying Transaction structs with slices) entries: std.PriorityQueue(QueueEntry, void, compareQueueEntry), // Store sender -> transaction indices directly // Use HashMap
where usize is the first transaction index for that sender // Then scan storage for all transactions from that sender - // This avoids arrays of slices/hashes which trigger the allocator bug - by_sender: std.HashMap(core.types.Address, usize, AddressContext, std.hash_map.default_max_load_percentage), + by_sender: std.HashMap(core.types.Address, usize, std.hash_map.AutoContext(core.types.Address), std.hash_map.default_max_load_percentage), wal: ?wal.WriteAheadLog = null, size: usize = 0, pub fn init(allocator: std.mem.Allocator, cfg: *const config.Config) !Mempool { - // Initialize custom transaction storage (pre-allocated, avoids allocator bug) + // Initialize custom transaction storage (pre-allocated) var storage = try TransactionStorage.init(allocator, @intCast(cfg.mempool_max_size)); errdefer storage.deinit(); // Don't pre-allocate HashMap capacity - let it grow naturally - // Pre-allocation might trigger the allocator bug var mempool = Mempool{ .allocator = allocator, .config = cfg, .storage = storage, - .by_hash = std.HashMap(core.types.Hash, usize, HashContext, std.hash_map.default_max_load_percentage).init(allocator), - .metadata = std.HashMap(core.types.Hash, EntryMetadata, HashContext, std.hash_map.default_max_load_percentage).init(allocator), + .by_hash = std.HashMap(core.types.Hash, usize, std.hash_map.AutoContext(core.types.Hash), std.hash_map.default_max_load_percentage).init(allocator), + .metadata = std.HashMap(core.types.Hash, EntryMetadata, std.hash_map.AutoContext(core.types.Hash), std.hash_map.default_max_load_percentage).init(allocator), .entries = std.PriorityQueue(QueueEntry, void, compareQueueEntry).init(allocator, {}), - .by_sender = std.HashMap(core.types.Address, usize, AddressContext, std.hash_map.default_max_load_percentage).init(allocator), + .by_sender = std.HashMap(core.types.Address, usize, std.hash_map.AutoContext(core.types.Address), std.hash_map.default_max_load_percentage).init(allocator), }; // Initialize WAL if configured @@ -225,7 +202,7 @@ pub const Mempool = struct { } const tx_hash = try tx.hash(self.allocator); - // tx_hash is U256 struct (not allocated), no need to free + // tx_hash is u256 (not allocated), no need to free // Check if already exists if (self.by_hash.contains(tx_hash)) { @@ -300,7 +277,7 @@ pub const Mempool = struct { if (i == tx_index) continue; // Skip the one we're removing const other_tx = self.storage.get(i) catch continue; const other_sender = other_tx.sender() catch continue; - if (other_sender.eql(sender)) { + if (other_sender == sender) { has_other_txs = true; break; } @@ -370,7 +347,7 @@ pub const Mempool = struct { while (i < self.storage.count) : (i += 1) { const tx = self.storage.get(i) catch continue; const tx_sender = tx.sender() catch continue; - if (tx_sender.eql(sender)) { + if (tx_sender == sender) { try result.append(tx); } } diff --git a/src/persistence/rocksdb.zig b/src/persistence/rocksdb.zig index ef1ef92..4798cc1 100644 --- a/src/persistence/rocksdb.zig +++ b/src/persistence/rocksdb.zig @@ -289,7 +289,7 @@ pub const Database = struct { /// Helper: Convert address to database key fn addressToKey(self: *Database, prefix: []const u8, address: core.types.Address) ![]u8 { - const addr_bytes = address.toBytes(); + const addr_bytes = core.types.addressToBytes(address); const prefix_len = prefix.len; const key = try self.allocator.alloc(u8, prefix_len + 32); @memcpy(key[0..prefix_len], prefix); @@ -299,7 +299,7 @@ pub const Database = struct { /// Helper: Convert hash to database key fn hashToKey(self: *Database, prefix: []const u8, hash: core.types.Hash) ![]u8 { - const hash_bytes = hash.toBytes(); + const hash_bytes = core.types.hashToBytes(hash); const prefix_len = prefix.len; const key = try self.allocator.alloc(u8, prefix_len + 32); @memcpy(key[0..prefix_len], prefix); diff --git a/src/sequencer/sequencer.zig b/src/sequencer/sequencer.zig index 34507b5..06ff1ea 100644 --- a/src/sequencer/sequencer.zig +++ b/src/sequencer/sequencer.zig @@ -9,7 +9,7 @@ const execution = @import("execution.zig"); fn formatHash(hash: core.types.Hash) []const u8 { // Format hash as hex string for logging - const bytes = hash.toBytes(); + const bytes = core.types.hashToBytes(hash); var buffer: [66]u8 = undefined; // "0x" + 64 hex chars buffer[0] = '0'; buffer[1] = 'x'; @@ -90,7 +90,7 @@ pub const Sequencer = struct { try valid_txs.append(tx); // Remove from mempool - // tx_hash is U256 struct (not allocated), no need to free + // tx_hash is u256 (not allocated), no need to free _ = try self.mempool.remove(tx_hash); } diff --git a/src/state/manager.zig b/src/state/manager.zig index 79ec255..fe15bb2 100644 --- a/src/state/manager.zig +++ b/src/state/manager.zig @@ -2,31 +2,11 @@ const std = @import("std"); const core = @import("../core/root.zig"); const persistence = @import("../persistence/root.zig"); -// Custom hash context for U256 struct (two u128 fields) -// This avoids the allocator bug with native u256 -const HashContext = struct { - pub fn hash(_: @This(), key: core.types.Hash) u64 { - return key.hash(); - } - pub fn eql(_: @This(), a: core.types.Hash, b: core.types.Hash) bool { - return a.eql(b); - } -}; - -const AddressContext = struct { - pub fn hash(_: @This(), key: core.types.Address) u64 { - return key.hash(); - } - pub fn eql(_: @This(), a: core.types.Address, b: core.types.Address) bool { - return a.eql(b); - } -}; - pub const StateManager = struct { allocator: std.mem.Allocator, - nonces: std.HashMap(core.types.Address, u64, AddressContext, std.hash_map.default_max_load_percentage), - balances: std.HashMap(core.types.Address, u256, AddressContext, std.hash_map.default_max_load_percentage), - receipts: std.HashMap(core.types.Hash, core.receipt.Receipt, HashContext, std.hash_map.default_max_load_percentage), + nonces: std.HashMap(core.types.Address, u64, std.hash_map.AutoContext(core.types.Address), std.hash_map.default_max_load_percentage), + balances: std.HashMap(core.types.Address, u256, std.hash_map.AutoContext(core.types.Address), std.hash_map.default_max_load_percentage), + receipts: std.HashMap(core.types.Hash, core.receipt.Receipt, std.hash_map.AutoContext(core.types.Hash), std.hash_map.default_max_load_percentage), current_block_number: u64 = 0, db: ?*persistence.rocksdb.Database = null, use_persistence: bool = false, @@ -35,9 +15,9 @@ pub const StateManager = struct { pub fn init(allocator: std.mem.Allocator) StateManager { return .{ .allocator = allocator, - .nonces = std.HashMap(core.types.Address, u64, AddressContext, std.hash_map.default_max_load_percentage).init(allocator), - .balances = std.HashMap(core.types.Address, u256, AddressContext, std.hash_map.default_max_load_percentage).init(allocator), - .receipts = std.HashMap(core.types.Hash, core.receipt.Receipt, HashContext, std.hash_map.default_max_load_percentage).init(allocator), + .nonces = std.HashMap(core.types.Address, u64, std.hash_map.AutoContext(core.types.Address), std.hash_map.default_max_load_percentage).init(allocator), + .balances = std.HashMap(core.types.Address, u256, std.hash_map.AutoContext(core.types.Address), std.hash_map.default_max_load_percentage).init(allocator), + .receipts = std.HashMap(core.types.Hash, core.receipt.Receipt, std.hash_map.AutoContext(core.types.Hash), std.hash_map.default_max_load_percentage).init(allocator), .db = null, .use_persistence = false, }; diff --git a/src/validation/ingress.zig b/src/validation/ingress.zig index fc39660..4242cc8 100644 --- a/src/validation/ingress.zig +++ b/src/validation/ingress.zig @@ -26,7 +26,7 @@ pub const Ingress = struct { // Check if duplicate in mempool const tx_hash = try tx.hash(self.allocator); - // tx_hash is U256 struct (not allocated), no need to free + // tx_hash is u256 (not allocated), no need to free if (self.mempool.contains(tx_hash)) { return .duplicate; }