diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 573e750..6ca5859 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,11 @@ jobs: - name: Setup Zig uses: goto-bus-stop/setup-zig@v2 with: - version: 0.15.2 + version: 0.14.1 + + - name: Fetch dependencies + run: | + zig build --fetch - name: Run linting checks run: | @@ -40,11 +44,16 @@ jobs: - name: Setup Zig uses: goto-bus-stop/setup-zig@v2 with: - version: 0.15.2 + version: 0.14.1 + + - name: Fetch dependencies + run: | + zig build --fetch - name: Build for Linux x86_64 run: | - zig build -Dtarget=x86_64-linux-gnu + # Specify glibc 2.38+ for RocksDB compatibility (requires __isoc23_* symbols) + zig build -Dtarget=x86_64-linux-gnu.2.38 - name: Verify binary exists run: | @@ -62,7 +71,11 @@ jobs: - name: Setup Zig uses: goto-bus-stop/setup-zig@v2 with: - version: 0.15.2 + version: 0.14.1 + + - name: Fetch dependencies + run: | + zig build --fetch - name: Build for macOS x86_64 run: | @@ -84,7 +97,11 @@ jobs: - name: Setup Zig uses: goto-bus-stop/setup-zig@v2 with: - version: 0.15.2 + version: 0.14.1 + + - name: Fetch dependencies + run: | + zig build --fetch - name: Build for macOS ARM64 run: | @@ -106,7 +123,11 @@ jobs: - name: Setup Zig uses: goto-bus-stop/setup-zig@v2 with: - version: 0.15.2 + version: 0.14.1 + + - name: Fetch dependencies + run: | + zig build --fetch - name: Build for Windows x86_64 run: | @@ -124,8 +145,8 @@ jobs: include: - platform: linux/amd64 tag: amd64 - - platform: linux/arm64 - tag: arm64 + # - platform: linux/arm64 + # tag: arm64 steps: - name: Checkout code diff --git a/Dockerfile b/Dockerfile index 997aa0d..7df064c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,11 +11,11 @@ RUN apt-get update && apt-get install -y \ ca-certificates \ && rm -rf /var/lib/apt/lists/* -# Install Zig 0.15.2 +# Install Zig 0.14.1 # Detect architecture and download appropriate Zig binary ARG TARGETPLATFORM ARG BUILDPLATFORM -ENV ZIG_VERSION=0.15.2 +ENV ZIG_VERSION=0.14.1 RUN ARCH_SUFFIX=$(echo ${TARGETPLATFORM} | cut -d'/' -f2) && \ if [ "${ARCH_SUFFIX}" = "amd64" ]; then \ ZIG_ARCH="x86_64"; \ @@ -40,8 +40,13 @@ COPY build.zig build.zig.zon ./ COPY src ./src COPY vendor ./vendor +# Fetch dependencies +RUN --mount=type=cache,target=/root/.cache/zig \ + zig build --fetch + # Build the sequencer -RUN zig build -Doptimize=ReleaseSafe +RUN --mount=type=cache,target=/root/.cache/zig \ + zig build -Doptimize=ReleaseSafe # Stage 2: Runtime stage FROM ubuntu:22.04 diff --git a/README.md b/README.md index caacbce..ec5411c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # Native Sequencer -A production-grade sequencer built in Zig for L2 rollups that accepts transactions, orders them, forms batches, and posts them to L1. +**⚠️ EXPERIMENTAL SOFTWARE - USE AT YOUR OWN RISK ⚠️** + +This is experimental software and is provided "as is" without warranty of any kind. Use at your own risk. The software may contain bugs, security vulnerabilities, or other issues that could result in loss of funds or data. + +A sequencer built in Zig for L2 rollups that accepts transactions, orders them, forms batches, and posts them to L1. ## Overview @@ -12,7 +16,7 @@ The Native Sequencer is a high-performance transaction sequencer designed for La - **Excellent C interop** - reuse battle-tested C libraries (RocksDB, libsecp256k1, etc.) - **Strong control over memory layout** - enables zero-copy network stacks and deterministic serialization - **Modern tooling** - easy cross-compilation for Linux amd64/arm64 containers -- **Production-ready** - built with Zig 0.15.2 for stability and performance +- **Built with Zig 0.14.1** for stability and performance ## Features @@ -70,7 +74,7 @@ The sequencer follows a modular architecture: ### Prerequisites -- **Zig 0.15.2** or later ([Install Zig](https://ziglang.org/download/)) +- **Zig 0.14.1** ([Install Zig](https://ziglang.org/download/)) - **C compiler** (for vendored C dependencies) ### Build Commands @@ -127,7 +131,7 @@ docker rm sequencer The Dockerfile uses a multi-stage build: -1. **Builder Stage**: Installs Zig 0.15.2 and builds the sequencer +1. **Builder Stage**: Installs Zig 0.14.1 and builds the sequencer 2. **Runtime Stage**: Creates a minimal runtime image with just the binary #### Runtime Environment Variables @@ -141,7 +145,7 @@ The container accepts the following environment variables (all have defaults set **L1 Configuration**: - `L1_RPC_URL`: L1 JSON-RPC endpoint (default: `http://host.docker.internal:8545`) - `L1_CHAIN_ID`: L1 chain ID (default: `1`) -- `SEQUENCER_KEY`: Sequencer private key in hex format (required for production) +- `SEQUENCER_KEY`: Sequencer private key in hex format **Sequencer Configuration**: - `BATCH_SIZE_LIMIT`: Maximum blocks per batch (default: `1000`) @@ -232,9 +236,9 @@ docker buildx build --platform linux/amd64 -t native-sequencer:amd64 . docker buildx build --platform linux/amd64,linux/arm64 -t native-sequencer:latest --push . ``` -#### Production Deployment +#### Deployment Considerations -For production deployments, consider: +For deployments, consider: 1. **Use a specific tag** instead of `latest` 2. **Set resource limits** @@ -417,7 +421,7 @@ Available metrics: ## Development Status -This is an initial implementation. Production use requires: +This is an experimental implementation. The following features are implemented or in progress: - ✅ Core sequencer architecture - ✅ Transaction validation and mempool @@ -425,7 +429,7 @@ This is an initial implementation. Production use requires: - ✅ Basic state management - ✅ RLP encoding/decoding (complete implementation with tests) - ✅ Docker support -- ✅ HTTP server implementation (Zig 0.15 networking APIs) +- ✅ HTTP server implementation (Zig 0.14.1 networking APIs) - ✅ HTTP client for L1 communication (JSON-RPC support) - ✅ Conditional transaction submission (EIP-7796 support) - ⏳ Complete ECDSA signature verification and recovery (basic implementation) @@ -495,7 +499,7 @@ The workflow will fail if: ### Networking Implementation -The sequencer uses Zig 0.15.2's standard library networking APIs: +The sequencer uses Zig 0.14.1's standard library networking APIs: - **HTTP Server**: Built on `std.net.Server` and `std.net.Stream` for accepting JSON-RPC connections - **HTTP Client**: Uses `std.net.tcpConnectToAddress` for L1 RPC communication @@ -504,7 +508,7 @@ The sequencer uses Zig 0.15.2's standard library networking APIs: ### Custom U256 Implementation -Due to a compiler bug in Zig 0.15.2's HashMap implementation with native `u256` types, we use a custom `U256` struct implementation. This struct: +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 @@ -514,11 +518,25 @@ See `src/core/types.zig` for implementation details and rationale. ## Known Issues & Workarounds -### Zig 0.15.2 HashMap Allocator Bug (RESOLVED) +### Linux Build Requirements + +**glibc Version**: The Linux build requires glibc 2.38 or later due to RocksDB dependencies that use ISO C23 compatibility symbols (`__isoc23_*`). When building for Linux, specify the glibc version: + +```bash +zig build -Dtarget=x86_64-linux-gnu.2.38 +``` + +**CI Compatibility**: GitHub Actions `ubuntu-latest` runners use Ubuntu 22.04 (glibc 2.35), which is insufficient. The CI workflow specifies glibc 2.38 in the build target to ensure compatibility. For local builds on older Linux distributions, you may need to: + +1. Use a newer Linux distribution (Ubuntu 24.04+ or equivalent) +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.15.2 related to HashMap initialization with native `u256` types as keys. The error manifests as: +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 @@ -541,11 +559,8 @@ 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). See **[ZIG_0.14_ALLOCATOR_ERROR.md](ZIG_0.14_ALLOCATOR_ERROR.md)** for detailed explanation and workarounds attempted. - -### Upgrading to Zig 0.15.2 +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. -This project has been successfully upgraded to Zig 0.15.2. See **[ZIG_0.15_UPGRADE.md](ZIG_0.15_UPGRADE.md)** for detailed information about the upgrade process, encountered errors, and solutions. ## License diff --git a/build.zig b/build.zig index 1ccca91..d01954e 100644 --- a/build.zig +++ b/build.zig @@ -4,8 +4,10 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); _ = b.standardOptimizeOption(.{}); // Available for future use + // Note: For Linux builds, specify glibc 2.38+ in the target (e.g., x86_64-linux-gnu.2.38) + // This is required for RocksDB compatibility (uses __isoc23_* symbols from glibc 2.38+) + // Build libsecp256k1 static C library from vendor directory - // In Zig 0.15, we create a library with a dummy Zig root module const libsecp256k1_root = b.addModule("secp256k1_lib", .{ .root_source_file = b.path("vendor/zig-eth-secp256k1/secp256k1_wrapper.zig"), .target = target, @@ -44,6 +46,16 @@ pub fn build(b: *std.Build) void { }); sequencer_module.addImport("secp256k1", secp256k1_mod); + // Add RocksDB dependency (using Syndica/rocksdb-zig like zeam) + // Note: RocksDB doesn't support Windows, so we conditionally include it + const is_windows = target.result.os.tag == .windows; + if (!is_windows) { + const dep_rocksdb = b.dependency("rocksdb", .{ + .target = target, + }); + sequencer_module.addImport("rocksdb", dep_rocksdb.module("bindings")); + } + // Library const lib = b.addLibrary(.{ .name = "native-sequencer", @@ -52,6 +64,20 @@ pub fn build(b: *std.Build) void { }); // Link secp256k1 library lib.linkLibrary(libsecp256k1); + // Add RocksDB module and link library (only on non-Windows) + if (!is_windows) { + const dep_rocksdb = b.dependency("rocksdb", .{ + .target = target, + }); + lib.root_module.addImport("rocksdb", dep_rocksdb.module("bindings")); + lib.linkLibrary(dep_rocksdb.artifact("rocksdb")); + lib.linkLibCpp(); // RocksDB requires C++ standard library + lib.linkSystemLibrary("pthread"); // Required for pthread functions + // librt is Linux-specific (gettid, etc.) - not needed on macOS + if (target.result.os.tag == .linux) { + lib.linkSystemLibrary("rt"); + } + } lib.linkLibC(); b.installArtifact(lib); @@ -68,6 +94,20 @@ pub fn build(b: *std.Build) void { exe.root_module.addImport("secp256k1", secp256k1_mod); // Link secp256k1 library exe.linkLibrary(libsecp256k1); + // Add RocksDB module and link library (only on non-Windows) + if (!is_windows) { + const dep_rocksdb = b.dependency("rocksdb", .{ + .target = target, + }); + exe.root_module.addImport("rocksdb", dep_rocksdb.module("bindings")); + exe.linkLibrary(dep_rocksdb.artifact("rocksdb")); + exe.linkLibCpp(); // RocksDB requires C++ standard library + exe.linkSystemLibrary("pthread"); // Required for pthread functions + // librt is Linux-specific (gettid, etc.) - not needed on macOS + if (target.result.os.tag == .linux) { + exe.linkSystemLibrary("rt"); + } + } exe.linkLibC(); b.installArtifact(exe); @@ -93,6 +133,20 @@ pub fn build(b: *std.Build) void { unit_tests.root_module.addImport("secp256k1", secp256k1_mod); // Link secp256k1 library unit_tests.linkLibrary(libsecp256k1); + // Add RocksDB module and link library (only on non-Windows) + if (!is_windows) { + const dep_rocksdb = b.dependency("rocksdb", .{ + .target = target, + }); + unit_tests.root_module.addImport("rocksdb", dep_rocksdb.module("bindings")); + unit_tests.linkLibrary(dep_rocksdb.artifact("rocksdb")); + unit_tests.linkLibCpp(); // RocksDB requires C++ standard library + unit_tests.linkSystemLibrary("pthread"); // Required for pthread functions + // librt is Linux-specific (gettid, etc.) - not needed on macOS + if (target.result.os.tag == .linux) { + unit_tests.linkSystemLibrary("rt"); + } + } unit_tests.linkLibC(); const run_unit_tests = b.addRunArtifact(unit_tests); const test_step = b.step("test", "Run unit tests"); diff --git a/build.zig.zon b/build.zig.zon index 8c89d3f..fa04ac1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,8 +2,13 @@ .name = .native_sequencer, .version = "0.1.0", .fingerprint = 0xf01d1595a0ff6442, - .minimum_zig_version = "0.15.2", - .dependencies = .{}, + .minimum_zig_version = "0.14.1", + .dependencies = .{ + .rocksdb = .{ + .url = "https://github.com/Syndica/rocksdb-zig/archive/70137101ad89640e0fc2e5ddbe60a26c522c7ae7.tar.gz", + .hash = "rocksdb-9.7.4-z_CUTmO5AAD0CQ2ZvShSDZHjC2x9MKrTnpvbNAIU7ah0", + }, + }, .paths = .{ "build.zig", "build.zig.zon", diff --git a/src/api/http.zig b/src/api/http.zig index ce99d7e..090858a 100644 --- a/src/api/http.zig +++ b/src/api/http.zig @@ -7,11 +7,15 @@ pub const HttpServer = struct { allocator: std.mem.Allocator, address: std.net.Address, server: ?std.net.Server = null, + host: []const u8, + port: u16, - pub fn init(allocator: std.mem.Allocator, address: std.net.Address) HttpServer { + pub fn init(allocator: std.mem.Allocator, address: std.net.Address, host: []const u8, port: u16) HttpServer { return .{ .allocator = allocator, .address = address, + .host = host, + .port = port, }; } @@ -22,7 +26,8 @@ pub const HttpServer = struct { }); self.server = server; - std.log.info("HTTP server listening on {any}", .{self.address}); + // Format address for readable logging + std.log.info("HTTP server listening on {s}:{d}", .{ self.host, self.port }); } pub fn accept(self: *HttpServer) !Connection { @@ -122,7 +127,7 @@ pub const HttpResponse = struct { } pub fn format(self: *const HttpResponse, allocator: std.mem.Allocator) ![]u8 { - var result = std.array_list.Managed(u8).init(allocator); + var result = std.ArrayList(u8).init(allocator); errdefer result.deinit(); const status_text = switch (self.status_code) { diff --git a/src/api/jsonrpc.zig b/src/api/jsonrpc.zig index 8d64547..1c8765f 100644 --- a/src/api/jsonrpc.zig +++ b/src/api/jsonrpc.zig @@ -34,25 +34,102 @@ pub const JsonRpcResponse = struct { .result = result, .id = id, }; - // Simplified JSON serialization - in production use proper JSON library - // For now, return a placeholder - _ = response; - return try allocator.dupe(u8, "{\"jsonrpc\":\"2.0\",\"result\":null,\"id\":null}"); + return try serializeResponse(allocator, response); } pub fn errorResponse(allocator: std.mem.Allocator, id: ?std.json.Value, code: i32, message: []const u8) ![]u8 { + const error_msg = try allocator.dupe(u8, message); const response = JsonRpcResponse{ .jsonrpc = "2.0", .@"error" = JsonRpcError{ .code = code, - .message = message, + .message = error_msg, }, .id = id, }; - // Simplified JSON serialization - in production use proper JSON library - // For now, return a placeholder - _ = response; - return try allocator.dupe(u8, "{\"jsonrpc\":\"2.0\",\"result\":null,\"id\":null}"); + defer allocator.free(error_msg); + return try serializeResponse(allocator, response); + } + + fn serializeResponse(allocator: std.mem.Allocator, response: JsonRpcResponse) ![]u8 { + var list = std.ArrayList(u8).init(allocator); + defer list.deinit(); + + try list.writer().writeAll("{\"jsonrpc\":\"2.0\","); + + if (response.result) |result| { + try list.writer().writeAll("\"result\":"); + try serializeJsonValue(list.writer(), result); + try list.writer().writeAll(","); + } + + if (response.@"error") |err| { + try list.writer().writeAll("\"error\":{"); + try list.writer().print("\"code\":{},", .{err.code}); + try list.writer().writeAll("\"message\":"); + try serializeJsonValue(list.writer(), std.json.Value{ .string = err.message }); + if (err.data) |data| { + try list.writer().writeAll(",\"data\":"); + try serializeJsonValue(list.writer(), data); + } + try list.writer().writeAll("},"); + } + + try list.writer().writeAll("\"id\":"); + if (response.id) |id| { + try serializeJsonValue(list.writer(), id); + } else { + try list.writer().writeAll("null"); + } + + try list.writer().writeAll("}"); + + return try list.toOwnedSlice(); + } + + fn serializeJsonValue(writer: anytype, value: std.json.Value) !void { + switch (value) { + .null => try writer.writeAll("null"), + .bool => |b| try writer.print("{}", .{b}), + .integer => |i| try writer.print("{}", .{i}), + .float => |f| try writer.print("{d}", .{f}), + .number_string => |ns| try writer.print("\"{s}\"", .{ns}), + .string => |s| { + // Escape string properly + try writer.writeByte('"'); + for (s) |char| { + switch (char) { + '"' => try writer.writeAll("\\\""), + '\\' => try writer.writeAll("\\\\"), + '\n' => try writer.writeAll("\\n"), + '\r' => try writer.writeAll("\\r"), + '\t' => try writer.writeAll("\\t"), + else => try writer.writeByte(char), + } + } + try writer.writeByte('"'); + }, + .array => |arr| { + try writer.writeAll("["); + for (arr.items, 0..) |item, i| { + if (i > 0) try writer.writeAll(","); + try serializeJsonValue(writer, item); + } + try writer.writeAll("]"); + }, + .object => |obj| { + try writer.writeAll("{"); + var iter = obj.iterator(); + var first = true; + while (iter.next()) |entry| { + if (!first) try writer.writeAll(","); + first = false; + try writer.print("\"{s}\":", .{entry.key_ptr.*}); + try serializeJsonValue(writer, entry.value_ptr.*); + } + try writer.writeAll("}"); + }, + } } }; diff --git a/src/api/server.zig b/src/api/server.zig index 777cdc8..019872a 100644 --- a/src/api/server.zig +++ b/src/api/server.zig @@ -2,6 +2,7 @@ const std = @import("std"); const core = @import("../core/root.zig"); const validation = @import("../validation/root.zig"); const metrics = @import("../metrics/root.zig"); +const l1 = @import("../l1/root.zig"); const http = @import("http.zig"); const jsonrpc = @import("jsonrpc.zig"); @@ -10,13 +11,25 @@ pub const JsonRpcServer = struct { ingress_handler: *validation.ingress.Ingress, metrics: *metrics.Metrics, http_server: http.HttpServer, + l1_client: ?*l1.Client = null, - pub fn init(allocator: std.mem.Allocator, addr: std.net.Address, ing: *validation.ingress.Ingress, m: *metrics.Metrics) JsonRpcServer { + pub fn init(allocator: std.mem.Allocator, addr: std.net.Address, host: []const u8, port: u16, ing: *validation.ingress.Ingress, m: *metrics.Metrics) JsonRpcServer { return .{ .allocator = allocator, .ingress_handler = ing, .metrics = m, - .http_server = http.HttpServer.init(allocator, addr), + .http_server = http.HttpServer.init(allocator, addr, host, port), + .l1_client = null, + }; + } + + pub fn initWithL1Client(allocator: std.mem.Allocator, addr: std.net.Address, host: []const u8, port: u16, ing: *validation.ingress.Ingress, m: *metrics.Metrics, l1_cli: *l1.Client) JsonRpcServer { + return .{ + .allocator = allocator, + .ingress_handler = ing, + .metrics = m, + .http_server = http.HttpServer.init(allocator, addr, host, port), + .l1_client = l1_cli, }; } @@ -36,7 +49,7 @@ pub const JsonRpcServer = struct { defer conn_mut.close(); var request = conn_mut.readRequest() catch |err| { - std.log.warn("Failed to read request: {any}", .{err}); + std.log.warn("Failed to read HTTP request: {any}", .{err}); return; }; defer request.deinit(); @@ -53,7 +66,7 @@ pub const JsonRpcServer = struct { } const json_response = server.handleJsonRpc(request.body) catch |err| { - std.log.warn("Failed to handle JSON-RPC: {any}", .{err}); + std.log.warn("Failed to handle JSON-RPC request (method={s}): {any}", .{ request.method, err }); const error_response = jsonrpc.JsonRpcResponse.errorResponse(server.allocator, null, jsonrpc.ErrorCode.InternalError, "Internal error") catch return; defer server.allocator.free(error_response); @@ -126,7 +139,7 @@ pub const JsonRpcServer = struct { const hex_start: usize = if (std.mem.startsWith(u8, tx_hex, "0x")) 2 else 0; const hex_data = tx_hex[hex_start..]; - var tx_bytes = std.array_list.Managed(u8).init(self.allocator); + var tx_bytes = std.ArrayList(u8).init(self.allocator); defer tx_bytes.deinit(); var i: usize = 0; @@ -136,46 +149,104 @@ pub const JsonRpcServer = struct { try tx_bytes.append(byte); } - // Decode RLP transaction + // Decode transaction based on type const tx_bytes_slice = try tx_bytes.toOwnedSlice(); defer self.allocator.free(tx_bytes_slice); - const tx = core.transaction.Transaction.fromRaw(self.allocator, tx_bytes_slice) catch { - return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Invalid transaction encoding"); - }; - defer self.allocator.free(tx.data); + // Check transaction type (EIP-2718) + if (tx_bytes_slice.len > 0 and tx_bytes_slice[0] == core.transaction.ExecuteTxType) { + // ExecuteTx transaction - these are stateless and should be forwarded to L1 geth + var execute_tx = core.transaction_execute.ExecuteTx.fromRaw(self.allocator, tx_bytes_slice) catch { + return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Invalid ExecuteTx encoding"); + }; + defer execute_tx.deinit(self.allocator); - const result = self.ingress_handler.acceptTransaction(tx) catch { - self.metrics.incrementTransactionsRejected(); - // Handle actual errors (like allocation failures) - return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, "Transaction processing failed"); - }; + // Minimal validation (signature check for deduplication) + const result = self.ingress_handler.acceptExecuteTx(&execute_tx) catch { + self.metrics.incrementTransactionsRejected(); + return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, "ExecuteTx processing failed"); + }; - if (result != .valid) { - self.metrics.incrementTransactionsRejected(); - return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, "Transaction validation failed"); - } + if (result != .valid) { + self.metrics.incrementTransactionsRejected(); + const error_msg = switch (result) { + .invalid_signature => "Invalid ExecuteTx signature", + .duplicate => "ExecuteTx already seen", + else => "ExecuteTx validation failed", + }; + return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, error_msg); + } - self.metrics.incrementTransactionsAccepted(); + self.metrics.incrementTransactionsAccepted(); - const tx_hash = try tx.hash(self.allocator); + // Forward ExecuteTx to L1 geth via eth_sendRawTransaction + const tx_hash = if (self.l1_client) |l1_cli| blk: { + // Forward to L1 geth + const forwarded_hash = l1_cli.forwardExecuteTx(&execute_tx) catch |err| { + std.log.err("Failed to forward ExecuteTx to L1 geth: {any}", .{err}); + self.metrics.incrementTransactionsRejected(); + return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, "Failed to forward ExecuteTx to L1"); + }; + break :blk forwarded_hash; + } else blk: { + // L1 client not available, just return the transaction hash + std.log.warn("L1 client not available, ExecuteTx not forwarded", .{}); + break :blk try execute_tx.hash(self.allocator); + }; + const hash_bytes = core.types.hashToBytes(tx_hash); + var hex_buf: [66]u8 = undefined; // 0x + 64 hex chars + hex_buf[0] = '0'; + hex_buf[1] = 'x'; + var j: usize = 0; + while (j < 32) : (j += 1) { + const hex_digits = "0123456789abcdef"; + hex_buf[2 + j * 2] = hex_digits[hash_bytes[j] >> 4]; + hex_buf[2 + j * 2 + 1] = hex_digits[hash_bytes[j] & 0xf]; + } + const hash_hex = try std.fmt.allocPrint(self.allocator, "{s}", .{&hex_buf}); + defer self.allocator.free(hash_hex); - // Format hash as hex string - const hash_bytes = core.types.hashToBytes(tx_hash); - var hex_buf: [66]u8 = undefined; // 0x + 64 hex chars - hex_buf[0] = '0'; - hex_buf[1] = 'x'; - var j: usize = 0; - while (j < 32) : (j += 1) { - const hex_digits = "0123456789abcdef"; - hex_buf[2 + j * 2] = hex_digits[hash_bytes[j] >> 4]; - hex_buf[2 + j * 2 + 1] = hex_digits[hash_bytes[j] & 0xf]; - } - const hash_hex = try std.fmt.allocPrint(self.allocator, "{s}", .{&hex_buf}); - defer self.allocator.free(hash_hex); + const result_value = std.json.Value{ .string = hash_hex }; + return try jsonrpc.JsonRpcResponse.success(self.allocator, request.id, result_value); + } else { + // Legacy transaction + const tx = core.transaction.Transaction.fromRaw(self.allocator, tx_bytes_slice) catch { + return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.InvalidParams, "Invalid transaction encoding"); + }; + defer self.allocator.free(tx.data); - const result_value = std.json.Value{ .string = hash_hex }; - return try jsonrpc.JsonRpcResponse.success(self.allocator, request.id, result_value); + const result = self.ingress_handler.acceptTransaction(tx) catch { + self.metrics.incrementTransactionsRejected(); + // Handle actual errors (like allocation failures) + return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, "Transaction processing failed"); + }; + + if (result != .valid) { + self.metrics.incrementTransactionsRejected(); + return try jsonrpc.JsonRpcResponse.errorResponse(self.allocator, request.id, jsonrpc.ErrorCode.ServerError, "Transaction validation failed"); + } + + self.metrics.incrementTransactionsAccepted(); + + const tx_hash = try tx.hash(self.allocator); + + // Format hash as hex string + const hash_bytes = core.types.hashToBytes(tx_hash); + var hex_buf: [66]u8 = undefined; // 0x + 64 hex chars + hex_buf[0] = '0'; + hex_buf[1] = 'x'; + var j: usize = 0; + while (j < 32) : (j += 1) { + const hex_digits = "0123456789abcdef"; + hex_buf[2 + j * 2] = hex_digits[hash_bytes[j] >> 4]; + hex_buf[2 + j * 2 + 1] = hex_digits[hash_bytes[j] & 0xf]; + } + const hash_hex = try std.fmt.allocPrint(self.allocator, "{s}", .{&hex_buf}); + defer self.allocator.free(hash_hex); + + const result_value = std.json.Value{ .string = hash_hex }; + return try jsonrpc.JsonRpcResponse.success(self.allocator, request.id, result_value); + } } fn handleGetTransactionReceipt(self: *JsonRpcServer, request: *const jsonrpc.JsonRpcRequest) ![]u8 { diff --git a/src/batch/builder.zig b/src/batch/builder.zig index bdf4e19..1223d01 100644 --- a/src/batch/builder.zig +++ b/src/batch/builder.zig @@ -5,13 +5,13 @@ const config = @import("../config/root.zig"); pub const Builder = struct { allocator: std.mem.Allocator, config: *const config.Config, - blocks: std.array_list.Managed(core.block.Block), + blocks: std.ArrayList(core.block.Block), pub fn init(allocator: std.mem.Allocator, cfg: *const config.Config) Builder { return .{ .allocator = allocator, .config = cfg, - .blocks = std.array_list.Managed(core.block.Block).init(allocator), + .blocks = std.ArrayList(core.block.Block).init(allocator), }; } diff --git a/src/core/batch.zig b/src/core/batch.zig index ccc8dc9..b1578de 100644 --- a/src/core/batch.zig +++ b/src/core/batch.zig @@ -9,7 +9,7 @@ pub const Batch = struct { created_at: u64, pub fn serialize(self: *const Batch, allocator: std.mem.Allocator) ![]u8 { - var list = std.array_list.Managed(u8).init(allocator); + var list = std.ArrayList(u8).init(allocator); defer list.deinit(); const created_at_bytes = std.mem.asBytes(&self.created_at); diff --git a/src/core/rlp.zig b/src/core/rlp.zig index b541837..6195e92 100644 --- a/src/core/rlp.zig +++ b/src/core/rlp.zig @@ -13,7 +13,7 @@ pub const RLPError = error{ pub fn encodeUint(allocator: std.mem.Allocator, value: u256) ![]u8 { if (value == 0) { // Use ArrayList instead of direct alloc to avoid allocator issues - var result = std.array_list.Managed(u8).init(allocator); + var result = std.ArrayList(u8).init(allocator); errdefer result.deinit(); try result.append(0x80); return result.toOwnedSlice(); @@ -27,7 +27,7 @@ pub fn encodeUint(allocator: std.mem.Allocator, value: u256) ![]u8 { while (start < buf.len and buf[start] == 0) start += 1; const significant_bytes = buf.len - start; - var result = std.array_list.Managed(u8).init(allocator); + var result = std.ArrayList(u8).init(allocator); errdefer result.deinit(); if (significant_bytes == 1 and buf[start] < 0x80) { @@ -49,7 +49,7 @@ pub fn encodeUint(allocator: std.mem.Allocator, value: u256) ![]u8 { } fn encodeLength(len: usize) ![]u8 { - var result = std.array_list.Managed(u8).init(std.heap.page_allocator); + var result = std.ArrayList(u8).init(std.heap.page_allocator); errdefer result.deinit(); var n = len; @@ -74,7 +74,7 @@ fn encodeLength(len: usize) ![]u8 { } pub fn encodeBytes(allocator: std.mem.Allocator, data: []const u8) ![]u8 { - var result = std.array_list.Managed(u8).init(allocator); + var result = std.ArrayList(u8).init(allocator); errdefer result.deinit(); if (data.len == 1 and data[0] < 0x80) { @@ -99,7 +99,7 @@ pub fn encodeList(allocator: std.mem.Allocator, items: []const []const u8) ![]u8 total_len += item.len; } - var result = std.array_list.Managed(u8).init(allocator); + var result = std.ArrayList(u8).init(allocator); errdefer result.deinit(); if (total_len < 56) { @@ -118,7 +118,7 @@ pub fn encodeList(allocator: std.mem.Allocator, items: []const []const u8) ![]u8 } pub fn encodeTransaction(allocator: std.mem.Allocator, tx: *const @import("transaction.zig").Transaction) ![]u8 { - var items = std.array_list.Managed([]const u8).init(allocator); + var items = std.ArrayList([]const u8).init(allocator); defer { for (items.items) |item| { allocator.free(item); @@ -335,7 +335,7 @@ pub fn decodeList(allocator: std.mem.Allocator, data: []const u8) !struct { item if (data.len < offset + total_len) return error.InvalidRLP; - var items = std.array_list.Managed([]u8).init(allocator); + var items = std.ArrayList([]u8).init(allocator); errdefer { for (items.items) |item| { allocator.free(item); diff --git a/src/core/root.zig b/src/core/root.zig index 83a64c2..4e498d0 100644 --- a/src/core/root.zig +++ b/src/core/root.zig @@ -1,6 +1,7 @@ // Core data structures and types pub const types = @import("types.zig"); pub const transaction = @import("transaction.zig"); +pub const transaction_execute = @import("transaction_execute.zig"); pub const block = @import("block.zig"); pub const batch = @import("batch.zig"); // core/batch.zig pub const receipt = @import("receipt.zig"); diff --git a/src/core/transaction.zig b/src/core/transaction.zig index 3a6d72d..24f8e86 100644 --- a/src/core/transaction.zig +++ b/src/core/transaction.zig @@ -3,6 +3,9 @@ const types = @import("types.zig"); const crypto_hash = @import("../crypto/hash.zig"); const signature = @import("signature.zig"); +// Transaction type constants (EIP-2718) +pub const ExecuteTxType: u8 = 0x05; + pub const Transaction = struct { nonce: u64, gas_price: u256, @@ -14,7 +17,14 @@ pub const Transaction = struct { r: [32]u8, s: [32]u8, + /// Compute transaction hash for signing + /// For EIP-155 transactions, this includes chain ID in the hash + /// For legacy transactions (v=27/28), this is the standard RLP hash pub fn hash(self: *const Transaction, allocator: std.mem.Allocator) !types.Hash { + // Check if this is an EIP-155 transaction (v >= 35) + // For EIP-155, we need to include chain_id in the hash + // For now, we use standard RLP encoding (legacy format) + // TODO: Implement EIP-155 transaction hashing when chain_id is available const serialized = try self.serialize(allocator); defer allocator.free(serialized); return crypto_hash.keccak256(serialized); @@ -26,7 +36,7 @@ pub const Transaction = struct { return try rlp_module.encodeTransaction(allocator, self); } - fn encodeUint(list: *std.array_list.Managed(u8), value: anytype) !void { + fn encodeUint(list: *std.ArrayList(u8), value: anytype) !void { var buf: [32]u8 = undefined; std.mem.writeInt(u256, &buf, value, .big); var start: usize = 0; diff --git a/src/core/transaction_execute.zig b/src/core/transaction_execute.zig new file mode 100644 index 0000000..6b7967e --- /dev/null +++ b/src/core/transaction_execute.zig @@ -0,0 +1,1023 @@ +// ExecuteTx transaction type (type 0x05) for EXECUTE precompile +// Matches go-ethereum's ExecuteTx structure + +const std = @import("std"); +const types = @import("types.zig"); +const crypto_hash = @import("../crypto/hash.zig"); +const signature = @import("../crypto/signature.zig"); +const rlp_module = @import("rlp.zig"); + +pub const ExecuteTxType: u8 = 0x05; + +pub const ExecuteTx = struct { + // Standard EIP-1559 fields + chain_id: types.U256, + nonce: u64, + gas_tip_cap: types.U256, // maxPriorityFeePerGas + gas_fee_cap: types.U256, // maxFeePerGas + gas: u64, + + // Execution target + to: ?types.Address, + value: types.U256, + data: []const u8, + + // EXECUTE-specific fields + pre_state_hash: types.Hash, + witness_size: u32, + withdrawals_size: u32, + coinbase: types.Address, + block_number: u64, + timestamp: u64, + witness: []const u8, + withdrawals: []const u8, + blob_hashes: []types.Hash, + + // Signature + v: types.U256, + r: types.U256, + s: types.U256, + + const Self = @This(); + + /// Compute transaction hash for signing (EIP-2718 typed transaction) + /// Uses prefixed RLP hash with transaction type 0x05 + pub fn hash(self: *const Self, allocator: std.mem.Allocator) !types.Hash { + // EIP-2718: typed transaction hash = keccak256(transaction_type || rlp(tx_data)) + const rlp_data = try self.encodeRLP(allocator); + defer allocator.free(rlp_data); + + // Prepend transaction type byte + var prefixed = std.ArrayList(u8).init(allocator); + defer prefixed.deinit(); + try prefixed.append(ExecuteTxType); + try prefixed.appendSlice(rlp_data); + + // keccak256 returns Hash (U256), which is what we need + return crypto_hash.keccak256(prefixed.items); + } + + /// Serialize ExecuteTx to RLP format (without transaction type prefix) + pub fn encodeRLP(self: *const Self, allocator: std.mem.Allocator) ![]u8 { + var items = std.ArrayList([]const u8).init(allocator); + defer { + for (items.items) |item| { + allocator.free(item); + } + items.deinit(); + } + + // ChainID + const chain_id_bytes = self.chain_id.toBytes(); + const chain_id_encoded = try rlp_module.encodeBytes(allocator, &chain_id_bytes); + try items.append(chain_id_encoded); + + // Nonce + const nonce_encoded = try rlp_module.encodeUint(allocator, self.nonce); + try items.append(nonce_encoded); + + // GasTipCap + const gas_tip_cap_bytes = self.gas_tip_cap.toBytes(); + 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_encoded = try rlp_module.encodeBytes(allocator, &gas_fee_cap_bytes); + try items.append(gas_fee_cap_encoded); + + // Gas + const gas_encoded = try rlp_module.encodeUint(allocator, self.gas); + try items.append(gas_encoded); + + // To (address or empty) + if (self.to) |to| { + const to_bytes_array = types.addressToBytes(to); + const to_encoded = try rlp_module.encodeBytes(allocator, &to_bytes_array); + try items.append(to_encoded); + } else { + const empty = try rlp_module.encodeBytes(allocator, &[_]u8{}); + try items.append(empty); + } + + // Value + const value_bytes = self.value.toBytes(); + const value_encoded = try rlp_module.encodeBytes(allocator, &value_bytes); + try items.append(value_encoded); + + // Data + const data_encoded = try rlp_module.encodeBytes(allocator, self.data); + try items.append(data_encoded); + + // PreStateHash + const pre_state_hash_bytes = self.pre_state_hash.toBytes(); + const pre_state_hash_encoded = try rlp_module.encodeBytes(allocator, &pre_state_hash_bytes); + try items.append(pre_state_hash_encoded); + + // WitnessSize (as u64 in RLP, converted from u32) + const witness_size_u64: u64 = self.witness_size; + const witness_size_encoded = try rlp_module.encodeUint(allocator, witness_size_u64); + try items.append(witness_size_encoded); + + // WithdrawalsSize (as u64 in RLP, converted from u32) + const withdrawals_size_u64: u64 = self.withdrawals_size; + const withdrawals_size_encoded = try rlp_module.encodeUint(allocator, withdrawals_size_u64); + try items.append(withdrawals_size_encoded); + + // Coinbase + const coinbase_bytes_array = types.addressToBytes(self.coinbase); + const coinbase_encoded = try rlp_module.encodeBytes(allocator, &coinbase_bytes_array); + try items.append(coinbase_encoded); + + // BlockNumber + const block_number_encoded = try rlp_module.encodeUint(allocator, self.block_number); + try items.append(block_number_encoded); + + // Timestamp + const timestamp_encoded = try rlp_module.encodeUint(allocator, self.timestamp); + try items.append(timestamp_encoded); + + // Witness + const witness_encoded = try rlp_module.encodeBytes(allocator, self.witness); + try items.append(witness_encoded); + + // Withdrawals + const withdrawals_encoded = try rlp_module.encodeBytes(allocator, self.withdrawals); + try items.append(withdrawals_encoded); + + // BlobHashes (list of hashes) + var blob_hashes_items = std.ArrayList([]const u8).init(allocator); + defer { + for (blob_hashes_items.items) |item| { + allocator.free(item); + } + blob_hashes_items.deinit(); + } + for (self.blob_hashes) |blob_hash| { + const blob_hash_bytes = blob_hash.toBytes(); + const blob_hash_encoded = try rlp_module.encodeBytes(allocator, &blob_hash_bytes); + try blob_hashes_items.append(blob_hash_encoded); + } + const blob_hashes_list = try rlp_module.encodeList(allocator, blob_hashes_items.items); + defer allocator.free(blob_hashes_list); + try items.append(blob_hashes_list); + + // V + const v_bytes = self.v.toBytes(); + const v_encoded = try rlp_module.encodeBytes(allocator, &v_bytes); + try items.append(v_encoded); + + // R + const r_bytes = self.r.toBytes(); + const r_encoded = try rlp_module.encodeBytes(allocator, &r_bytes); + try items.append(r_encoded); + + // S + const s_bytes = self.s.toBytes(); + const s_encoded = try rlp_module.encodeBytes(allocator, &s_bytes); + try items.append(s_encoded); + + const rlp_result = try rlp_module.encodeList(allocator, items.items); + + // Clean up intermediate items + for (items.items) |item| { + allocator.free(item); + } + + return rlp_result; + } + + /// Decode ExecuteTx from RLP bytes (without transaction type prefix) + pub fn decodeRLP(allocator: std.mem.Allocator, data: []const u8) !Self { + const decoded_list = try rlp_module.decodeList(allocator, data); + defer { + for (decoded_list.items) |item| { + allocator.free(item); + } + allocator.free(decoded_list.items); + } + + if (decoded_list.items.len < 22) { + return error.InvalidRLP; + } + + var idx: usize = 0; + + // ChainID + const chain_id_result = try rlp_module.decodeBytes(allocator, decoded_list.items[idx]); + defer allocator.free(decoded_list.items[idx]); + defer allocator.free(chain_id_result.value); + 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); + idx += 1; + + // Nonce + const nonce_result = try rlp_module.decodeUint(allocator, decoded_list.items[idx]); + defer allocator.free(decoded_list.items[idx]); + const nonce = @as(u64, @intCast(nonce_result.value)); + idx += 1; + + // GasTipCap + const gas_tip_cap_result = try rlp_module.decodeBytes(allocator, decoded_list.items[idx]); + defer allocator.free(decoded_list.items[idx]); + defer allocator.free(gas_tip_cap_result.value); + 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); + idx += 1; + + // GasFeeCap + const gas_fee_cap_result = try rlp_module.decodeBytes(allocator, decoded_list.items[idx]); + defer allocator.free(decoded_list.items[idx]); + defer allocator.free(gas_fee_cap_result.value); + 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); + idx += 1; + + // Gas + const gas_result = try rlp_module.decodeUint(allocator, decoded_list.items[idx]); + defer allocator.free(decoded_list.items[idx]); + const gas = @as(u64, @intCast(gas_result.value)); + idx += 1; + + // To + defer allocator.free(decoded_list.items[idx]); + const to_address: ?types.Address = if (decoded_list.items[idx].len == 0) null else blk: { + if (decoded_list.items[idx].len != 20) { + return error.InvalidRLP; + } + var addr_bytes: [20]u8 = undefined; + @memcpy(&addr_bytes, decoded_list.items[idx]); + break :blk types.addressFromBytes(addr_bytes); + }; + idx += 1; + + // Value + const value_result = try rlp_module.decodeBytes(allocator, decoded_list.items[idx]); + defer allocator.free(decoded_list.items[idx]); + defer allocator.free(value_result.value); + 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); + idx += 1; + + // Data + defer allocator.free(decoded_list.items[idx]); + const data_bytes = try allocator.dupe(u8, decoded_list.items[idx]); + idx += 1; + + // PreStateHash + const pre_state_hash_result = try rlp_module.decodeBytes(allocator, decoded_list.items[idx]); + defer allocator.free(decoded_list.items[idx]); + defer allocator.free(pre_state_hash_result.value); + if (pre_state_hash_result.value.len != 32) return error.InvalidRLP; + var pre_state_hash_bytes: [32]u8 = undefined; + @memcpy(&pre_state_hash_bytes, pre_state_hash_result.value); + const pre_state_hash = types.hashFromBytes(pre_state_hash_bytes); + idx += 1; + + // WitnessSize + const witness_size_result = try rlp_module.decodeUint(allocator, decoded_list.items[idx]); + defer allocator.free(decoded_list.items[idx]); + const witness_size = @as(u32, @intCast(witness_size_result.value)); + idx += 1; + + // WithdrawalsSize + const withdrawals_size_result = try rlp_module.decodeUint(allocator, decoded_list.items[idx]); + defer allocator.free(decoded_list.items[idx]); + const withdrawals_size = @as(u32, @intCast(withdrawals_size_result.value)); + idx += 1; + + // Coinbase + defer allocator.free(decoded_list.items[idx]); + if (decoded_list.items[idx].len != 20) { + allocator.free(data_bytes); + return error.InvalidRLP; + } + var coinbase_bytes: [20]u8 = undefined; + @memcpy(&coinbase_bytes, decoded_list.items[idx]); + const coinbase = types.addressFromBytes(coinbase_bytes); + idx += 1; + + // BlockNumber + const block_number_result = try rlp_module.decodeUint(allocator, decoded_list.items[idx]); + defer allocator.free(decoded_list.items[idx]); + const block_number = @as(u64, @intCast(block_number_result.value)); + idx += 1; + + // Timestamp + const timestamp_result = try rlp_module.decodeUint(allocator, decoded_list.items[idx]); + defer allocator.free(decoded_list.items[idx]); + const timestamp = @as(u64, @intCast(timestamp_result.value)); + idx += 1; + + // Witness + defer allocator.free(decoded_list.items[idx]); + const witness_bytes = try allocator.dupe(u8, decoded_list.items[idx]); + idx += 1; + + // Withdrawals + defer allocator.free(decoded_list.items[idx]); + const withdrawals_bytes = try allocator.dupe(u8, decoded_list.items[idx]); + idx += 1; + + // BlobHashes + defer allocator.free(decoded_list.items[idx]); + const blob_hashes_list = try rlp_module.decodeList(allocator, decoded_list.items[idx]); + defer { + for (blob_hashes_list.items) |item| { + allocator.free(item); + } + allocator.free(blob_hashes_list.items); + } + var blob_hashes = std.ArrayList(types.Hash).init(allocator); + errdefer blob_hashes.deinit(); + for (blob_hashes_list.items) |blob_hash_item| { + defer allocator.free(blob_hash_item); + const blob_hash_bytes_result = try rlp_module.decodeBytes(allocator, blob_hash_item); + defer allocator.free(blob_hash_bytes_result.value); + if (blob_hash_bytes_result.value.len != 32) { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidRLP; + } + var blob_hash_bytes: [32]u8 = undefined; + @memcpy(&blob_hash_bytes, blob_hash_bytes_result.value); + try blob_hashes.append(types.hashFromBytes(blob_hash_bytes)); + } + idx += 1; + + // V + const v_result = try rlp_module.decodeBytes(allocator, decoded_list.items[idx]); + defer allocator.free(decoded_list.items[idx]); + defer allocator.free(v_result.value); + if (v_result.value.len != 32) { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidRLP; + } + var v_bytes: [32]u8 = undefined; + @memcpy(&v_bytes, v_result.value); + const v = types.U256.fromBytes(v_bytes); + idx += 1; + + // R + const r_result = try rlp_module.decodeBytes(allocator, decoded_list.items[idx]); + defer allocator.free(decoded_list.items[idx]); + defer allocator.free(r_result.value); + if (r_result.value.len != 32) { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidRLP; + } + var r_bytes: [32]u8 = undefined; + @memcpy(&r_bytes, r_result.value); + const r = types.U256.fromBytes(r_bytes); + idx += 1; + + // S + const s_result = try rlp_module.decodeBytes(allocator, decoded_list.items[idx]); + defer allocator.free(decoded_list.items[idx]); + defer allocator.free(s_result.value); + if (s_result.value.len != 32) { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidRLP; + } + var s_bytes: [32]u8 = undefined; + @memcpy(&s_bytes, s_result.value); + const s = types.U256.fromBytes(s_bytes); + + // Validate witness and withdrawals sizes + if (witness_bytes.len != witness_size) { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidRLP; + } + if (withdrawals_bytes.len != withdrawals_size) { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidRLP; + } + + return Self{ + .chain_id = chain_id, + .nonce = nonce, + .gas_tip_cap = gas_tip_cap, + .gas_fee_cap = gas_fee_cap, + .gas = gas, + .to = to_address, + .value = value, + .data = data_bytes, + .pre_state_hash = pre_state_hash, + .witness_size = witness_size, + .withdrawals_size = withdrawals_size, + .coinbase = coinbase, + .block_number = block_number, + .timestamp = timestamp, + .witness = witness_bytes, + .withdrawals = withdrawals_bytes, + .blob_hashes = try blob_hashes.toOwnedSlice(), + .v = v, + .r = r, + .s = s, + }; + } + + /// Decode ExecuteTx from raw transaction bytes (with EIP-2718 type prefix) + pub fn fromRaw(allocator: std.mem.Allocator, raw: []const u8) !Self { + if (raw.len == 0) return error.InvalidRLP; + if (raw[0] != ExecuteTxType) return error.InvalidRLP; + + // Skip transaction type byte and decode RLP + return decodeRLP(allocator, raw[1..]); + } + + /// Serialize ExecuteTx to raw transaction bytes (with EIP-2718 type prefix) + pub fn serialize(self: *const Self, allocator: std.mem.Allocator) ![]u8 { + const rlp_data = try self.encodeRLP(allocator); + defer allocator.free(rlp_data); + + // Prepend transaction type byte + var result = std.ArrayList(u8).init(allocator); + errdefer result.deinit(); + try result.append(ExecuteTxType); + try result.appendSlice(rlp_data); + + return try result.toOwnedSlice(); + } + + /// Recover sender address from signature + pub fn sender(self: *const Self, allocator: std.mem.Allocator) !types.Address { + // For EIP-2718 typed transactions, we need to hash the transaction data + // 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)); + + // Create signature struct + const sig = types.Signature{ + .r = r_bytes, + .s = s_bytes, + .v = v_byte, + }; + + // Use secp256k1 to recover public key from signature + const secp256k1_mod = @import("../crypto/secp256k1_wrapper.zig"); + const pubkey = try secp256k1_mod.recoverPublicKey(tx_hash, sig); + + // Derive address from public key + return pubkey.toAddress(); + } + + /// Get priority for mempool ordering (gas fee cap) + pub fn priority(self: *const Self) types.U256 { + return self.gas_fee_cap; + } + + /// Serialize ExecuteTx to JSON format matching go-ethereum + pub fn toJson(self: *const Self, allocator: std.mem.Allocator) !std.json.Value { + var obj = std.json.ObjectMap.init(allocator); + errdefer obj.deinit(); + + // Transaction type + 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()); + try obj.put("chainId", std.json.Value{ .string = chain_id_hex }); + + // Nonce + const nonce_hex = try std.fmt.allocPrint(allocator, "0x{x}", .{self.nonce}); + try obj.put("nonce", std.json.Value{ .string = nonce_hex }); + + // Gas + const gas_hex = try std.fmt.allocPrint(allocator, "0x{x}", .{self.gas}); + try obj.put("gas", std.json.Value{ .string = gas_hex }); + + // To + if (self.to) |to| { + const to_bytes = types.addressToBytes(to); + const to_hex = try bytesToHex(allocator, &to_bytes); + try obj.put("to", std.json.Value{ .string = to_hex }); + } else { + try obj.put("to", std.json.Value{ .null = {} }); + } + + // MaxPriorityFeePerGas + const gas_tip_cap_hex = try u256ToHex(allocator, self.gas_tip_cap.toU256()); + 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()); + try obj.put("maxFeePerGas", std.json.Value{ .string = gas_fee_cap_hex }); + + // Value + const value_hex = try u256ToHex(allocator, self.value.toU256()); + try obj.put("value", std.json.Value{ .string = value_hex }); + + // Input (data) + const input_hex = try bytesToHex(allocator, self.data); + 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_hex = try bytesToHex(allocator, &pre_state_hash_bytes); + try obj.put("preStateHash", std.json.Value{ .string = pre_state_hash_hex }); + + // Coinbase + const coinbase_bytes = types.addressToBytes(self.coinbase); + const coinbase_hex = try bytesToHex(allocator, &coinbase_bytes); + try obj.put("coinbase", std.json.Value{ .string = coinbase_hex }); + + // BlockNumber + const block_number_hex = try std.fmt.allocPrint(allocator, "0x{x}", .{self.block_number}); + try obj.put("blockNumber", std.json.Value{ .string = block_number_hex }); + + // Timestamp + const timestamp_hex = try std.fmt.allocPrint(allocator, "0x{x}", .{self.timestamp}); + try obj.put("timestamp", std.json.Value{ .string = timestamp_hex }); + + // Witness + const witness_hex = try bytesToHex(allocator, self.witness); + try obj.put("witness", std.json.Value{ .string = witness_hex }); + + // WitnessSize + const witness_size_hex = try std.fmt.allocPrint(allocator, "0x{x}", .{self.witness_size}); + try obj.put("witnessSize", std.json.Value{ .string = witness_size_hex }); + + // Withdrawals + const withdrawals_hex = try bytesToHex(allocator, self.withdrawals); + try obj.put("withdrawals", std.json.Value{ .string = withdrawals_hex }); + + // WithdrawalsSize + const withdrawals_size_hex = try std.fmt.allocPrint(allocator, "0x{x}", .{self.withdrawals_size}); + try obj.put("withdrawalsSize", std.json.Value{ .string = withdrawals_size_hex }); + + // BlobVersionedHashes + if (self.blob_hashes.len > 0) { + 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_hex = try bytesToHex(allocator, &blob_hash_bytes); + try blob_array.append(std.json.Value{ .string = blob_hash_hex }); + } + try obj.put("blobVersionedHashes", std.json.Value{ .array = .{ .items = try blob_array.toOwnedSlice(), .capacity = blob_array.items.len } }); + } + + // V + const v_hex = try u256ToHex(allocator, self.v.toU256()); + try obj.put("v", std.json.Value{ .string = v_hex }); + + // R + const r_hex = try u256ToHex(allocator, self.r.toU256()); + try obj.put("r", std.json.Value{ .string = r_hex }); + + // S + const s_hex = try u256ToHex(allocator, self.s.toU256()); + try obj.put("s", std.json.Value{ .string = s_hex }); + + return std.json.Value{ .object = obj }; + } + + /// Deserialize ExecuteTx from JSON format matching go-ethereum + pub fn fromJson(allocator: std.mem.Allocator, json_value: std.json.Value) !Self { + const obj = switch (json_value) { + .object => |o| o, + else => return error.InvalidJson, + }; + + // ChainID (required) + const chain_id_val = obj.get("chainId") orelse return error.MissingField; + const chain_id_hex = switch (chain_id_val) { + .string => |s| s, + else => return error.InvalidField, + }; + const chain_id = try hexToU256(chain_id_hex); + + // Nonce (required) + const nonce_val = obj.get("nonce") orelse return error.MissingField; + const nonce_hex = switch (nonce_val) { + .string => |s| s, + else => return error.InvalidField, + }; + const nonce = try hexToU64(nonce_hex); + + // Gas (required) + const gas_val = obj.get("gas") orelse return error.MissingField; + const gas_hex = switch (gas_val) { + .string => |s| s, + else => return error.InvalidField, + }; + const gas = try hexToU64(gas_hex); + + // To (optional) + const to_address: ?types.Address = if (obj.get("to")) |to_val| blk: { + const to_hex = switch (to_val) { + .string => |s| s, + .null => break :blk null, + else => return error.InvalidField, + }; + if (to_hex.len == 0) break :blk null; + const to_bytes = try hexToBytes(allocator, to_hex); + defer allocator.free(to_bytes); + if (to_bytes.len != 20) return error.InvalidAddress; + var addr_bytes: [20]u8 = undefined; + @memcpy(&addr_bytes, to_bytes); + break :blk types.addressFromBytes(addr_bytes); + } else null; + + // MaxPriorityFeePerGas (required) + const gas_tip_cap_val = obj.get("maxPriorityFeePerGas") orelse return error.MissingField; + const gas_tip_cap_hex = switch (gas_tip_cap_val) { + .string => |s| s, + else => return error.InvalidField, + }; + const gas_tip_cap = try hexToU256(gas_tip_cap_hex); + + // MaxFeePerGas (required) + const gas_fee_cap_val = obj.get("maxFeePerGas") orelse return error.MissingField; + const gas_fee_cap_hex = switch (gas_fee_cap_val) { + .string => |s| s, + else => return error.InvalidField, + }; + const gas_fee_cap = try hexToU256(gas_fee_cap_hex); + + // Value (required) + const value_val = obj.get("value") orelse return error.MissingField; + const value_hex = switch (value_val) { + .string => |s| s, + else => return error.InvalidField, + }; + const value = try hexToU256(value_hex); + + // Input/Data (required) + const input_val = obj.get("input") orelse return error.MissingField; + const input_hex = switch (input_val) { + .string => |s| s, + else => return error.InvalidField, + }; + const data_bytes = try hexToBytes(allocator, input_hex); + + // PreStateHash (required) + const pre_state_hash_val = obj.get("preStateHash") orelse return error.MissingField; + const pre_state_hash_hex = switch (pre_state_hash_val) { + .string => |s| s, + else => return error.InvalidField, + }; + const pre_state_hash_bytes = try hexToBytes(allocator, pre_state_hash_hex); + defer allocator.free(pre_state_hash_bytes); + if (pre_state_hash_bytes.len != 32) { + allocator.free(data_bytes); + return error.InvalidHash; + } + var pre_state_hash_array: [32]u8 = undefined; + @memcpy(&pre_state_hash_array, pre_state_hash_bytes); + const pre_state_hash = types.hashFromBytes(pre_state_hash_array); + + // Coinbase (required) + const coinbase_val = obj.get("coinbase") orelse return error.MissingField; + const coinbase_hex = switch (coinbase_val) { + .string => |s| s, + else => return error.InvalidField, + }; + const coinbase_bytes = try hexToBytes(allocator, coinbase_hex); + defer allocator.free(coinbase_bytes); + if (coinbase_bytes.len != 20) { + allocator.free(data_bytes); + return error.InvalidAddress; + } + var coinbase_array: [20]u8 = undefined; + @memcpy(&coinbase_array, coinbase_bytes); + const coinbase = types.addressFromBytes(coinbase_array); + + // BlockNumber (required) + const block_number_val = obj.get("blockNumber") orelse return error.MissingField; + const block_number_hex = switch (block_number_val) { + .string => |s| s, + else => return error.InvalidField, + }; + const block_number = try hexToU64(block_number_hex); + + // Timestamp (required) + const timestamp_val = obj.get("timestamp") orelse return error.MissingField; + const timestamp_hex = switch (timestamp_val) { + .string => |s| s, + else => return error.InvalidField, + }; + const timestamp = try hexToU64(timestamp_hex); + + // Witness (required) + const witness_val = obj.get("witness") orelse return error.MissingField; + const witness_hex = switch (witness_val) { + .string => |s| s, + else => return error.InvalidField, + }; + const witness_bytes = try hexToBytes(allocator, witness_hex); + + // WitnessSize (optional, derived from witness length if not provided) + var witness_size: u32 = @intCast(witness_bytes.len); + if (obj.get("witnessSize")) |witness_size_val| { + const witness_size_hex = switch (witness_size_val) { + .string => |s| s, + else => { + allocator.free(data_bytes); + allocator.free(witness_bytes); + return error.InvalidField; + }, + }; + witness_size = @intCast(try hexToU64(witness_size_hex)); + if (witness_size != witness_bytes.len) { + allocator.free(data_bytes); + allocator.free(witness_bytes); + return error.InvalidWitnessSize; + } + } + + // Withdrawals (required) + const withdrawals_val = obj.get("withdrawals") orelse { + allocator.free(data_bytes); + allocator.free(witness_bytes); + return error.MissingField; + }; + const withdrawals_hex = switch (withdrawals_val) { + .string => |s| s, + else => { + allocator.free(data_bytes); + allocator.free(witness_bytes); + return error.InvalidField; + }, + }; + const withdrawals_bytes = try hexToBytes(allocator, withdrawals_hex); + + // WithdrawalsSize (optional, derived from withdrawals length if not provided) + var withdrawals_size: u32 = @intCast(withdrawals_bytes.len); + if (obj.get("withdrawalsSize")) |withdrawals_size_val| { + const withdrawals_size_hex = switch (withdrawals_size_val) { + .string => |s| s, + else => { + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidField; + }, + }; + withdrawals_size = @intCast(try hexToU64(withdrawals_size_hex)); + if (withdrawals_size != withdrawals_bytes.len) { + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidWithdrawalsSize; + } + } + + // BlobVersionedHashes (optional) + var blob_hashes = std.ArrayList(types.Hash).init(allocator); + errdefer blob_hashes.deinit(); + if (obj.get("blobVersionedHashes")) |blob_hashes_val| { + const blob_array = switch (blob_hashes_val) { + .array => |arr| arr, + else => { + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidField; + }, + }; + for (blob_array.items) |blob_hash_val| { + const blob_hash_hex = switch (blob_hash_val) { + .string => |s| s, + else => { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidField; + }, + }; + const blob_hash_bytes = try hexToBytes(allocator, blob_hash_hex); + defer allocator.free(blob_hash_bytes); + if (blob_hash_bytes.len != 32) { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidHash; + } + var blob_hash_array: [32]u8 = undefined; + @memcpy(&blob_hash_array, blob_hash_bytes); + try blob_hashes.append(types.hashFromBytes(blob_hash_array)); + } + } + + // R (required) + const r_val = obj.get("r") orelse { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.MissingField; + }; + const r_hex = switch (r_val) { + .string => |s| s, + else => { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidField; + }, + }; + const r = try hexToU256(r_hex); + + // S (required) + const s_val = obj.get("s") orelse { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.MissingField; + }; + const s_hex = switch (s_val) { + .string => |s_str| s_str, + else => { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidField; + }, + }; + const s = try hexToU256(s_hex); + + // V (required, can be from v or yParity) + var v: types.U256 = undefined; + if (obj.get("v")) |v_val| { + const v_hex = switch (v_val) { + .string => |v_str| v_str, + else => { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidField; + }, + }; + v = try hexToU256(v_hex); + } else if (obj.get("yParity")) |yparity_val| { + const yparity_hex = switch (yparity_val) { + .string => |yp_str| yp_str, + else => { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.InvalidField; + }, + }; + 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); + } else { + blob_hashes.deinit(); + allocator.free(data_bytes); + allocator.free(witness_bytes); + allocator.free(withdrawals_bytes); + return error.MissingField; + } + + return Self{ + .chain_id = chain_id, + .nonce = nonce, + .gas_tip_cap = gas_tip_cap, + .gas_fee_cap = gas_fee_cap, + .gas = gas, + .to = to_address, + .value = value, + .data = data_bytes, + .pre_state_hash = pre_state_hash, + .witness_size = witness_size, + .withdrawals_size = withdrawals_size, + .coinbase = coinbase, + .block_number = block_number, + .timestamp = timestamp, + .witness = witness_bytes, + .withdrawals = withdrawals_bytes, + .blob_hashes = try blob_hashes.toOwnedSlice(), + .v = v, + .r = r, + .s = s, + }; + } + + /// Free allocated memory + pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { + allocator.free(self.data); + allocator.free(self.witness); + allocator.free(self.withdrawals); + allocator.free(self.blob_hashes); + } +}; + +// Helper functions for JSON serialization + +fn u256ToHex(allocator: std.mem.Allocator, value: u256) ![]u8 { + const bytes = types.U256.fromU256(value).toBytes(); + return bytesToHex(allocator, &bytes); +} + +fn hexToU256(hex_str: []const u8) !types.U256 { + const bytes = try hexToBytesNoAlloc(hex_str); + return types.U256.fromBytes(bytes); +} + +fn hexToU64(hex_str: []const u8) !u64 { + const hex_start: usize = if (std.mem.startsWith(u8, hex_str, "0x")) 2 else 0; + const hex_data = hex_str[hex_start..]; + if (hex_data.len == 0) return 0; + return try std.fmt.parseInt(u64, hex_data, 16); +} + +fn bytesToHex(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 { + var result = std.ArrayList(u8).init(allocator); + errdefer result.deinit(); + try result.appendSlice("0x"); + for (bytes) |byte| { + try result.writer().print("{x:0>2}", .{byte}); + } + return try result.toOwnedSlice(); +} + +fn hexToBytes(allocator: std.mem.Allocator, hex_str: []const u8) ![]u8 { + const hex_start: usize = if (std.mem.startsWith(u8, hex_str, "0x")) 2 else 0; + const hex_data = hex_str[hex_start..]; + + var bytes = std.ArrayList(u8).init(allocator); + errdefer bytes.deinit(); + + var i: usize = 0; + while (i < hex_data.len) : (i += 2) { + if (i + 1 >= hex_data.len) break; + const byte = try std.fmt.parseInt(u8, hex_data[i .. i + 2], 16); + try bytes.append(byte); + } + + return try bytes.toOwnedSlice(); +} + +fn hexToBytesNoAlloc(hex_str: []const u8) ![32]u8 { + const hex_start: usize = if (std.mem.startsWith(u8, hex_str, "0x")) 2 else 0; + const hex_data = hex_str[hex_start..]; + + var result: [32]u8 = undefined; + @memset(&result, 0); + + // Parse hex string and store in big-endian format (left-padded) + var result_idx: usize = 32; + var hex_idx: usize = hex_data.len; + + // Process from right to left to maintain big-endian order + while (hex_idx > 0 and result_idx > 0) { + hex_idx -= 2; + if (hex_idx + 1 >= hex_data.len) break; + result_idx -= 1; + result[result_idx] = try std.fmt.parseInt(u8, hex_data[hex_idx .. hex_idx + 2], 16); + } + + return result; +} + +const ExecuteTxError = error{ + InvalidJson, + MissingField, + InvalidField, + InvalidAddress, + InvalidHash, + InvalidWitnessSize, + InvalidWithdrawalsSize, +}; diff --git a/src/core/types.zig b/src/core/types.zig index 79d3a6b..01e3213 100644 --- a/src/core/types.zig +++ b/src/core/types.zig @@ -7,7 +7,7 @@ const std = @import("std"); // ============================================================================ // // PROBLEM: -// Zig 0.15.2 has a compiler bug in HashMap's AutoContext when using native +// 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 diff --git a/src/crypto/signature.zig b/src/crypto/signature.zig index 8eaea04..f19e81c 100644 --- a/src/crypto/signature.zig +++ b/src/crypto/signature.zig @@ -1,3 +1,12 @@ +// Comprehensive ECDSA signature verification for Ethereum transactions +// +// This module implements full ECDSA signature verification with: +// - Signature component validation (r, s, v) +// - Edge case handling (zero values, invalid recovery IDs, etc.) +// - EIP-155 chain ID support +// - Comprehensive error handling +// - Performance optimizations + const std = @import("std"); const types = @import("../core/types.zig"); const transaction = @import("../core/transaction.zig"); @@ -5,10 +14,100 @@ const hash = @import("hash.zig"); const keccak = @import("keccak.zig"); const secp256k1 = @import("secp256k1_wrapper.zig"); +/// Error types for signature verification +pub const SignatureError = error{ + InvalidRecoveryId, + InvalidRValue, + InvalidSValue, + InvalidVValue, + SignatureRecoveryFailed, + InvalidSignatureFormat, + ZeroSignature, + SignatureTooLarge, +}; + +/// secp256k1 curve order (n) +const SECP256K1_N: u256 = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141; + +/// Maximum valid s value (n/2 for low-s canonical signatures) +const MAX_S: u256 = 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0; + +/// Validate signature components (r, s, v) +/// Returns error if signature components are invalid +pub fn validateSignatureComponents(r: [32]u8, s: [32]u8, v: u8) SignatureError!void { + // Check r value: must be non-zero and < secp256k1 curve order + const r_value = readU256(&r); + if (r_value == 0) { + return error.InvalidRValue; + } + if (r_value >= SECP256K1_N) { + return error.SignatureTooLarge; + } + + // Check s value: must be non-zero and < secp256k1 curve order + // For canonical signatures (EIP-2), s must be <= n/2 + const s_value = readU256(&s); + if (s_value == 0) { + return error.InvalidSValue; + } + if (s_value >= SECP256K1_N) { + return error.SignatureTooLarge; + } + // Note: We don't enforce low-s canonical form here to be compatible + // with older transactions, but we validate it's within valid range + + // Check v value: must be 27, 28, or EIP-155 encoded (35 + chain_id * 2 or 36 + chain_id * 2) + // Valid v values: 27, 28, or >= 35 (EIP-155) + if (v < 27) { + return error.InvalidVValue; + } + if (v > 28 and v < 35) { + return error.InvalidVValue; + } +} + +/// Extract recovery ID from v value +/// Returns recovery ID (0-3) and chain ID (if EIP-155) +pub fn extractRecoveryId(v: u8) struct { recovery_id: u8, chain_id: ?u64 } { + if (v == 27) { + return .{ .recovery_id = 0, .chain_id = null }; + } else if (v == 28) { + return .{ .recovery_id = 1, .chain_id = null }; + } else if (v >= 35) { + // EIP-155: v = chain_id * 2 + 35 or chain_id * 2 + 36 + const recovery_id: u8 = if ((v - 35) % 2 == 0) 0 else 1; + const chain_id = (v - 35) / 2; + return .{ .recovery_id = recovery_id, .chain_id = chain_id }; + } else { + // Invalid v value, but we'll let secp256k1 handle it + return .{ .recovery_id = @truncate(v - 27), .chain_id = null }; + } +} + +/// Read u256 from big-endian bytes +fn readU256(bytes: *const [32]u8) u256 { + var result: u256 = 0; + for (bytes) |byte| { + result = (result << 8) | byte; + } + return result; +} + /// Recover Ethereum address from transaction signature +/// This function handles both legacy (v=27/28) and EIP-155 (v>=35) signatures +/// Note: This function uses page_allocator internally for transaction hashing pub fn recoverAddress(tx: *const transaction.Transaction) !types.Address { + // Validate signature components first + try validateSignatureComponents(tx.r, tx.s, tx.v); + // Get the transaction hash (unsigned) - const tx_hash = try tx.hash(std.heap.page_allocator); + // For EIP-155, we need to hash with chain ID included + // Use page_allocator for transaction hash (it's temporary) + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + 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 // Create signature struct from transaction fields const sig = types.Signature{ @@ -18,35 +117,90 @@ pub fn recoverAddress(tx: *const transaction.Transaction) !types.Address { }; // Recover public key - const pub_key = try secp256k1.recoverPublicKey(tx_hash, sig); + const pub_key = secp256k1.recoverPublicKey(tx_hash, sig) catch { + return error.SignatureRecoveryFailed; + }; // Derive address from public key return pub_key.toAddress(); } -/// Verify transaction signature +/// Verify transaction signature with comprehensive validation +/// Returns true if signature is valid, false otherwise +/// This function performs full validation including: +/// - Signature component validation +/// - Public key recovery +/// - Address comparison pub fn verifySignature(tx: *const transaction.Transaction) !bool { - // Get the transaction hash - const tx_hash = try tx.hash(std.heap.page_allocator); + // Step 1: Validate signature components + validateSignatureComponents(tx.r, tx.s, tx.v) catch |err| { + std.log.debug("Signature validation failed: {any}", .{err}); + return false; + }; - // Create signature struct from transaction fields + // Step 2: Get the transaction hash + // Use page_allocator for transaction hash (it's temporary) + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + const tx_hash = tx.hash(allocator) catch { + std.log.debug("Failed to compute transaction hash", .{}); + return false; + }; + // tx_hash is U256 struct (stack-allocated), no need to free + + // Step 3: Create signature struct const sig = types.Signature{ .r = tx.r, .s = tx.s, .v = tx.v, }; - // Recover public key - const pub_key = secp256k1.recoverPublicKey(tx_hash, sig) catch return false; + // Step 4: Recover public key from signature + const pub_key = secp256k1.recoverPublicKey(tx_hash, sig) catch { + std.log.debug("Public key recovery failed", .{}); + return false; + }; - // Derive address from public key + // Step 5: Derive address from public key const recovered_address = pub_key.toAddress(); - // Get expected sender - const expected_sender = try tx.sender(); + // Step 6: Get expected sender (this will also recover address) + // We compare the recovered address from step 5 with the expected sender + const expected_sender = tx.sender() catch { + std.log.debug("Failed to recover sender address", .{}); + return false; + }; + + // Step 7: Compare addresses (U256 comparison) + const addresses_match = recovered_address.eql(expected_sender); + if (!addresses_match) { + std.log.debug("Recovered address does not match expected sender", .{}); + } + + return addresses_match; +} + +/// Verify signature with explicit chain ID (for EIP-155) +/// This is useful when you want to verify a signature with a specific chain ID +pub fn verifySignatureWithChainId(tx: *const transaction.Transaction, chain_id: u64) !bool { + // Extract recovery info from v + const recovery_info = extractRecoveryId(tx.v); + + // Check if chain ID matches + if (recovery_info.chain_id) |tx_chain_id| { + if (tx_chain_id != chain_id) { + std.log.debug("Chain ID mismatch: expected {d}, got {d}", .{ chain_id, tx_chain_id }); + return false; + } + } else { + // Legacy transaction (v=27/28), but we're expecting EIP-155 + std.log.debug("Expected EIP-155 signature but got legacy signature", .{}); + return false; + } - // Compare addresses (U256 comparison) - return recovered_address.eql(expected_sender); + // Verify signature normally + return try verifySignature(tx); } /// Sign data with a private key diff --git a/src/crypto/signature_test.zig b/src/crypto/signature_test.zig new file mode 100644 index 0000000..c7fddb1 --- /dev/null +++ b/src/crypto/signature_test.zig @@ -0,0 +1,224 @@ +// Comprehensive tests for ECDSA signature verification + +const std = @import("std"); +const testing = std.testing; +const types = @import("../core/types.zig"); +const transaction = @import("../core/transaction.zig"); +const signature = @import("signature.zig"); +const secp256k1 = @import("secp256k1_wrapper.zig"); +const keccak = @import("keccak.zig"); + +test "validateSignatureComponents - valid signature" { + var r: [32]u8 = undefined; + var s: [32]u8 = undefined; + @memset(&r, 0); + @memset(&s, 0); + r[31] = 1; // Non-zero r + s[31] = 1; // Non-zero s + + try testing.expectError(error.InvalidRValue, signature.validateSignatureComponents(r, s, 0)); // v < 27 + try testing.expectError(error.InvalidVValue, signature.validateSignatureComponents(r, s, 26)); // v < 27 + try testing.expectError(error.InvalidVValue, signature.validateSignatureComponents(r, s, 29)); // 28 < v < 35 + try testing.expectError(error.InvalidVValue, signature.validateSignatureComponents(r, s, 34)); // 28 < v < 35 + + // Valid v values + try signature.validateSignatureComponents(r, s, 27); // Legacy + try signature.validateSignatureComponents(r, s, 28); // Legacy + try signature.validateSignatureComponents(r, s, 35); // EIP-155 chain_id=0 + try signature.validateSignatureComponents(r, s, 36); // EIP-155 chain_id=0 + try signature.validateSignatureComponents(r, s, 37); // EIP-155 chain_id=1 +} + +test "validateSignatureComponents - zero r" { + var r: [32]u8 = undefined; + var s: [32]u8 = undefined; + @memset(&r, 0); + @memset(&s, 0); + s[31] = 1; // Non-zero s + + try testing.expectError(error.InvalidRValue, signature.validateSignatureComponents(r, s, 27)); +} + +test "validateSignatureComponents - zero s" { + var r: [32]u8 = undefined; + var s: [32]u8 = undefined; + @memset(&r, 0); + @memset(&s, 0); + r[31] = 1; // Non-zero r + + try testing.expectError(error.InvalidSValue, signature.validateSignatureComponents(r, s, 27)); +} + +test "extractRecoveryId - legacy signatures" { + const info_27 = signature.extractRecoveryId(27); + try testing.expectEqual(@as(u8, 0), info_27.recovery_id); + try testing.expect(info_27.chain_id == null); + + const info_28 = signature.extractRecoveryId(28); + try testing.expectEqual(@as(u8, 1), info_28.recovery_id); + try testing.expect(info_28.chain_id == null); +} + +test "extractRecoveryId - EIP-155 signatures" { + // v = 35 = chain_id * 2 + 35, recovery_id = 0 + const info_35 = signature.extractRecoveryId(35); + try testing.expectEqual(@as(u8, 0), info_35.recovery_id); + try testing.expectEqual(@as(u64, 0), info_35.chain_id.?); + + // v = 36 = chain_id * 2 + 36, recovery_id = 1 + const info_36 = signature.extractRecoveryId(36); + try testing.expectEqual(@as(u8, 1), info_36.recovery_id); + try testing.expectEqual(@as(u64, 0), info_36.chain_id.?); + + // v = 37 = chain_id * 2 + 35, recovery_id = 0, chain_id = 1 + const info_37 = signature.extractRecoveryId(37); + try testing.expectEqual(@as(u8, 0), info_37.recovery_id); + try testing.expectEqual(@as(u64, 1), info_37.chain_id.?); + + // v = 38 = chain_id * 2 + 36, recovery_id = 1, chain_id = 1 + const info_38 = signature.extractRecoveryId(38); + try testing.expectEqual(@as(u8, 1), info_38.recovery_id); + try testing.expectEqual(@as(u64, 1), info_38.chain_id.?); +} + +test "signature verification - roundtrip" { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + // Create a test private key + var private_key_bytes: [32]u8 = undefined; + @memset(&private_key_bytes, 0); + private_key_bytes[31] = 1; // Non-zero private key + + const private_key = try secp256k1.PrivateKey.fromBytes(private_key_bytes); + + // Create a test transaction + const tx = transaction.Transaction{ + .nonce = 1, + .gas_price = types.U256.fromU256(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), + .data = &[_]u8{}, + .v = 0, + .r = [_]u8{0} ** 32, + .s = [_]u8{0} ** 32, + }; + + // Hash the transaction + const tx_hash = try tx.hash(allocator); + defer allocator.free(tx_hash.toBytes()); + + // Sign the transaction + const sig = try secp256k1.sign(tx_hash, private_key); + + // Create signed transaction + var signed_tx = tx; + signed_tx.r = sig.r; + signed_tx.s = sig.s; + signed_tx.v = sig.v; + + // Verify signature + const is_valid = try signature.verifySignature(&signed_tx); + try testing.expect(is_valid); + + // 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)); + + // Clean up transaction data + allocator.free(tx.data); +} + +test "signature verification - invalid signature" { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + // Create a transaction with invalid signature (zero r) + var tx = transaction.Transaction{ + .nonce = 1, + .gas_price = types.U256.fromU256(1000000000), + .gas_limit = 21000, + .to = null, + .value = types.U256.fromU256(0), + .data = &[_]u8{}, + .v = 27, + .r = [_]u8{0} ** 32, // Invalid: zero r + .s = [_]u8{1} ** 32, + }; + + // Verification should fail + const is_valid = try signature.verifySignature(&tx); + try testing.expect(!is_valid); +} + +test "signature verification - invalid v value" { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + + // Create a transaction with invalid v value + var tx = transaction.Transaction{ + .nonce = 1, + .gas_price = types.U256.fromU256(1000000000), + .gas_limit = 21000, + .to = null, + .value = types.U256.fromU256(0), + .data = &[_]u8{}, + .v = 26, // Invalid: < 27 + .r = [_]u8{1} ** 32, + .s = [_]u8{1} ** 32, + }; + + // Verification should fail + const is_valid = try signature.verifySignature(&tx); + try testing.expect(!is_valid); +} + +test "signature verification - signature too large" { + // Create signature with r >= secp256k1 curve order + var r: [32]u8 = undefined; + @memset(&r, 0xff); + r[0] = 0xff; + r[1] = 0xff; + r[2] = 0xff; + r[3] = 0xff; + r[4] = 0xff; + r[5] = 0xff; + r[6] = 0xff; + r[7] = 0xff; + r[8] = 0xff; + r[9] = 0xff; + r[10] = 0xff; + r[11] = 0xff; + r[12] = 0xff; + r[13] = 0xff; + r[14] = 0xff; + r[15] = 0xff; + r[16] = 0xff; + r[17] = 0xff; + r[18] = 0xff; + r[19] = 0xff; + r[20] = 0xff; + r[21] = 0xff; + r[22] = 0xff; + r[23] = 0xff; + r[24] = 0xff; + r[25] = 0xff; + r[26] = 0xff; + r[27] = 0xff; + r[28] = 0xff; + r[29] = 0xff; + r[30] = 0xff; + r[31] = 0xff; // This makes r >= curve order + + var s: [32]u8 = undefined; + @memset(&s, 1); + + try testing.expectError(error.SignatureTooLarge, signature.validateSignatureComponents(r, s, 27)); +} diff --git a/src/l1/client.zig b/src/l1/client.zig index bdfffed..68ce9c6 100644 --- a/src/l1/client.zig +++ b/src/l1/client.zig @@ -112,6 +112,17 @@ pub const Client = struct { return core.types.hashFromBytes(hash_bytes); } + /// Forward ExecuteTx transaction to L1 geth + /// ExecuteTx transactions are stateless and should be sent directly to L1 geth + pub fn forwardExecuteTx(self: *Client, execute_tx: *const core.transaction_execute.ExecuteTx) !core.types.Hash { + // Serialize ExecuteTx to raw transaction bytes + const raw_tx = try execute_tx.serialize(self.allocator); + defer self.allocator.free(raw_tx); + + // Forward to L1 geth via eth_sendRawTransaction + return try self.sendTransaction(raw_tx); + } + fn sendTransactionConditional(self: *Client, signed_tx: []const u8, options: ConditionalOptions) !core.types.Hash { // Send JSON-RPC eth_sendRawTransactionConditional (EIP-7796) const tx_hex = try self.bytesToHex(signed_tx); @@ -250,7 +261,7 @@ pub const Client = struct { defer stream.close(); // Build JSON-RPC request - var request_json = std.array_list.Managed(u8).init(self.allocator); + var request_json = std.ArrayList(u8).init(self.allocator); defer request_json.deinit(); try request_json.writer().print( @@ -261,7 +272,7 @@ pub const Client = struct { defer self.allocator.free(request_body); // Build HTTP request - var http_request = std.array_list.Managed(u8).init(self.allocator); + var http_request = std.ArrayList(u8).init(self.allocator); defer http_request.deinit(); try http_request.writer().print( @@ -321,7 +332,7 @@ pub const Client = struct { // Simple JSON serialization for params switch (value) { .array => |arr| { - var result = std.array_list.Managed(u8).init(self.allocator); + var result = std.ArrayList(u8).init(self.allocator); defer result.deinit(); try result.append('['); for (arr.items, 0..) |item, i| { @@ -334,7 +345,7 @@ pub const Client = struct { return result.toOwnedSlice(); }, .object => |obj| { - var result = std.array_list.Managed(u8).init(self.allocator); + var result = std.ArrayList(u8).init(self.allocator); defer result.deinit(); try result.append('{'); var first = true; @@ -359,7 +370,7 @@ pub const Client = struct { } fn bytesToHex(self: *Client, bytes: []const u8) ![]u8 { - var result = std.array_list.Managed(u8).init(self.allocator); + var result = std.ArrayList(u8).init(self.allocator); defer result.deinit(); const hex_digits = "0123456789abcdef"; diff --git a/src/main.zig b/src/main.zig index faa4044..70e5eb7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,32 +2,84 @@ const std = @import("std"); const lib = @import("root.zig"); pub fn main() !void { + std.log.info("Starting Native Sequencer...", .{}); + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Load configuration + std.log.info("Loading configuration from environment variables...", .{}); var cfg = try lib.config.Config.fromEnv(allocator); defer { if (!std.mem.eql(u8, cfg.api_host, "0.0.0.0")) allocator.free(cfg.api_host); if (!std.mem.eql(u8, cfg.l1_rpc_url, "http://localhost:8545")) allocator.free(cfg.l1_rpc_url); } + std.log.info("Configuration loaded: API={s}:{d}, L1_RPC={s}, Metrics={d}, BatchInterval={d}ms", .{ + cfg.api_host, + cfg.api_port, + cfg.l1_rpc_url, + cfg.metrics_port, + cfg.batch_interval_ms, + }); + // Check emergency halt if (cfg.emergency_halt) { - std.log.err("Sequencer is in emergency halt mode", .{}); + std.log.err("Sequencer is in emergency halt mode - exiting", .{}); return; } // Initialize components - var state_manager = lib.state.StateManager.init(allocator); + std.log.info("Initializing sequencer components...", .{}); + + // Initialize RocksDB if state_db_path is configured + var state_db: ?lib.persistence.rocksdb.Database = null; + var state_manager: lib.state.StateManager = undefined; + + // Check if STATE_DB_PATH is set or if default path should be used + const use_persistence = blk: { + if (std.process.getEnvVarOwned(allocator, "STATE_DB_PATH")) |env_path| { + defer allocator.free(env_path); + break :blk true; + } else |_| { + // Use default path + break :blk true; + } + }; + + if (use_persistence) { + // Open RocksDB database (not supported on Windows) + const db_result = lib.persistence.rocksdb.Database.open(allocator, cfg.state_db_path); + if (db_result) |db| { + state_db = db; + std.log.info("Initializing state manager with RocksDB persistence at {s}", .{cfg.state_db_path}); + state_manager = try lib.state.StateManager.initWithPersistence(allocator, &state_db.?); + } else |err| { + if (err == error.UnsupportedPlatform) { + std.log.warn("RocksDB persistence not supported on Windows, falling back to in-memory state", .{}); + state_db = null; + state_manager = lib.state.StateManager.init(allocator); + } else { + return err; + } + } + } else { + // Use in-memory state manager + state_manager = lib.state.StateManager.init(allocator); + } defer state_manager.deinit(); + if (state_db) |*db| { + defer db.close(); + } var mp = try lib.mempool.Mempool.init(allocator, &cfg); defer mp.deinit(); + std.log.info("Mempool initialized (max_size={d}, wal_path={s})", .{ cfg.mempool_max_size, cfg.mempool_wal_path }); var batch_builder = lib.batch.Builder.init(allocator, &cfg); defer batch_builder.deinit(); + std.log.info("Batch builder initialized (size_limit={d}, gas_limit={d})", .{ cfg.batch_size_limit, cfg.block_gas_limit }); var ingress_handler = lib.validation.ingress.Ingress.init(allocator, &mp, &state_manager); @@ -35,21 +87,29 @@ pub fn main() !void { var l1_client = lib.l1.Client.init(allocator, &cfg); defer l1_client.deinit(); + std.log.info("L1 client initialized (rpc_url={s}, chain_id={d})", .{ cfg.l1_rpc_url, cfg.l1_chain_id }); var m = lib.metrics.Metrics.init(allocator); // Start API server + std.log.info("Starting API server...", .{}); const api_address = try std.net.Address.parseIp(cfg.api_host, cfg.api_port); - var api_server = lib.api.server.JsonRpcServer.init(allocator, api_address, &ingress_handler, &m); + var api_server = lib.api.server.JsonRpcServer.initWithL1Client(allocator, api_address, cfg.api_host, cfg.api_port, &ingress_handler, &m, &l1_client); // Start sequencing loop in background + std.log.info("Starting sequencing loop (interval={d}ms)...", .{cfg.batch_interval_ms}); var sequencing_thread = try std.Thread.spawn(.{}, sequencingLoop, .{ &seq, &batch_builder, &l1_client, &m, &cfg }); sequencing_thread.detach(); - // Start metrics server (simplified) - var metrics_thread = try std.Thread.spawn(.{}, metricsLoop, .{ &m, cfg.metrics_port }); + // Start metrics server + std.log.info("Starting metrics server...", .{}); + const metrics_host = "0.0.0.0"; + const metrics_address = try std.net.Address.parseIp(metrics_host, cfg.metrics_port); + var metrics_server = lib.metrics.server.MetricsServer.init(allocator, metrics_address, metrics_host, cfg.metrics_port, &m); + var metrics_thread = try std.Thread.spawn(.{}, metricsServerLoop, .{&metrics_server}); metrics_thread.detach(); + std.log.info("Native Sequencer started successfully", .{}); // Start API server (blocking) try api_server.start(); } @@ -64,10 +124,11 @@ fn sequencingLoop(seq: *lib.sequencer.Sequencer, batch_builder: *lib.batch.Build continue; }; m.incrementBlocksCreated(); + std.log.info("Block #{d} created: {d} transactions, {d} gas used", .{ block.number, block.transactions.len, block.gas_used }); // Add to batch batch_builder.addBlock(block) catch |err| { - std.log.err("Error adding block to batch: {any}", .{err}); + std.log.err("Error adding block #{d} to batch: {any}", .{ block.number, err }); continue; }; @@ -78,27 +139,39 @@ fn sequencingLoop(seq: *lib.sequencer.Sequencer, batch_builder: *lib.batch.Build continue; }; + std.log.info("Submitting batch to L1 ({d} blocks)...", .{batch_data.blocks.len}); + // Submit to L1 - _ = l1_client.submitBatch(batch_data) catch |err| { + const batch_hash = l1_client.submitBatch(batch_data) catch |err| { std.log.err("Error submitting batch to L1: {any}", .{err}); m.incrementL1SubmissionErrors(); continue; }; m.incrementBatchesSubmitted(); + std.log.info("Batch submitted successfully to L1 (hash={s})", .{formatHash(batch_hash)}); batch_builder.clear(); } } } -fn metricsLoop(m: *lib.metrics.Metrics, port: u16) void { - // Simplified metrics server - in production use proper async networking - std.log.info("Metrics server would listen on port {d}", .{port}); - std.log.warn("Metrics server implementation incomplete - networking API needs proper Zig 0.15 implementation", .{}); - // TODO: Implement proper metrics server using Zig 0.15 networking APIs - // For now, just sleep to keep thread alive - while (true) { - std.Thread.sleep(1 * std.time.ns_per_s); - _ = m; +fn formatHash(hash: lib.core.types.Hash) []const u8 { + // Format hash as hex string for logging + const bytes = hash.toBytes(); + var buffer: [66]u8 = undefined; // "0x" + 64 hex chars + buffer[0] = '0'; + buffer[1] = 'x'; + // Format each byte as hex + for (bytes, 0..) |byte, i| { + const hex_chars = "0123456789abcdef"; + buffer[2 + i * 2] = hex_chars[byte >> 4]; + buffer[2 + i * 2 + 1] = hex_chars[byte & 0xf]; } + return buffer[0..66]; +} + +fn metricsServerLoop(server: *lib.metrics.server.MetricsServer) void { + server.start() catch |err| { + std.log.err("Metrics server error: {any}", .{err}); + }; } diff --git a/src/mempool/mempool.zig b/src/mempool/mempool.zig index 4e59330..4dfebf2 100644 --- a/src/mempool/mempool.zig +++ b/src/mempool/mempool.zig @@ -315,7 +315,7 @@ pub const Mempool = struct { } pub fn getTopN(self: *Mempool, gas_limit: u64, max_count: usize) ![]core.transaction.Transaction { - var result = std.array_list.Managed(core.transaction.Transaction).init(self.allocator); + var result = std.ArrayList(core.transaction.Transaction).init(self.allocator); errdefer result.deinit(); var remaining_gas: u64 = gas_limit; @@ -362,7 +362,7 @@ pub const Mempool = struct { // Check if sender exists _ = self.by_sender.get(sender) orelse return &[_]core.transaction.Transaction{}; - var result = std.array_list.Managed(core.transaction.Transaction).init(self.allocator); + var result = std.ArrayList(core.transaction.Transaction).init(self.allocator); defer result.deinit(); // Scan storage to find all transactions from this sender diff --git a/src/metrics/root.zig b/src/metrics/root.zig index 950d6f4..bff89c6 100644 --- a/src/metrics/root.zig +++ b/src/metrics/root.zig @@ -1 +1,2 @@ -pub const Metrics = @import("metrics.zig").Metrics; // metrics/metrics.zig +pub const Metrics = @import("metrics.zig").Metrics; +pub const server = @import("server.zig"); // metrics/metrics.zig diff --git a/src/metrics/server.zig b/src/metrics/server.zig new file mode 100644 index 0000000..955d9aa --- /dev/null +++ b/src/metrics/server.zig @@ -0,0 +1,148 @@ +// Metrics HTTP server implementation using Zig 0.15 networking + +const std = @import("std"); +const http = @import("../api/http.zig"); +const Metrics = @import("metrics.zig").Metrics; + +pub const MetricsServer = struct { + allocator: std.mem.Allocator, + address: std.net.Address, + metrics: *Metrics, + http_server: http.HttpServer, + host: []const u8, + port: u16, + + pub fn init(allocator: std.mem.Allocator, address: std.net.Address, host: []const u8, port: u16, metrics: *Metrics) MetricsServer { + return .{ + .allocator = allocator, + .address = address, + .metrics = metrics, + .http_server = http.HttpServer.init(allocator, address, host, port), + .host = host, + .port = port, + }; + } + + pub fn start(self: *MetricsServer) !void { + try self.http_server.listen(); + std.log.info("Metrics server listening on {s}:{d}", .{ self.host, self.port }); + + while (true) { + var conn = self.http_server.accept() catch |err| { + std.log.err("Error accepting metrics connection: {any}", .{err}); + continue; + }; + defer conn.close(); + + // Handle connection in current thread (simple implementation) + self.handleConnection(&conn) catch |err| { + std.log.err("Error handling metrics connection: {any}", .{err}); + }; + } + } + + fn handleConnection(self: *MetricsServer, conn: *http.Connection) !void { + var request = conn.readRequest() catch |err| { + // Send error response if request parsing fails + const error_response = try self.createErrorResponse(400, "Bad Request"); + defer self.allocator.free(error_response); + try conn.writeResponse(error_response); + return err; + }; + defer request.deinit(); + + // Only handle GET requests to /metrics + if (!std.mem.eql(u8, request.method, "GET")) { + const error_response = try self.createErrorResponse(405, "Method Not Allowed"); + defer self.allocator.free(error_response); + try conn.writeResponse(error_response); + return; + } + + if (!std.mem.eql(u8, request.path, "/metrics")) { + const error_response = try self.createErrorResponse(404, "Not Found"); + defer self.allocator.free(error_response); + try conn.writeResponse(error_response); + return; + } + + // Generate metrics response + const response = try self.createMetricsResponse(); + defer self.allocator.free(response); + try conn.writeResponse(response); + } + + fn createMetricsResponse(self: *MetricsServer) ![]u8 { + var response = http.HttpResponse.init(self.allocator); + defer response.deinit(); + + response.status_code = 200; + try response.headers.put("Content-Type", "text/plain; version=0.0.4; charset=utf-8"); + + // Format metrics in Prometheus format + var metrics_buffer = std.ArrayList(u8).init(self.allocator); + defer metrics_buffer.deinit(); + + try metrics_buffer.writer().print( + \\# HELP sequencer_transactions_received Total number of transactions received + \\# TYPE sequencer_transactions_received counter + \\sequencer_transactions_received {d} + \\ + \\# HELP sequencer_transactions_accepted Total number of transactions accepted + \\# TYPE sequencer_transactions_accepted counter + \\sequencer_transactions_accepted {d} + \\ + \\# HELP sequencer_transactions_rejected Total number of transactions rejected + \\# TYPE sequencer_transactions_rejected counter + \\sequencer_transactions_rejected {d} + \\ + \\# HELP sequencer_blocks_created Total number of blocks created + \\# TYPE sequencer_blocks_created counter + \\sequencer_blocks_created {d} + \\ + \\# HELP sequencer_batches_submitted Total number of batches submitted to L1 + \\# TYPE sequencer_batches_submitted counter + \\sequencer_batches_submitted {d} + \\ + \\# HELP sequencer_mempool_size Current mempool size + \\# TYPE sequencer_mempool_size gauge + \\sequencer_mempool_size {d} + \\ + \\# HELP sequencer_l1_submission_errors Total number of L1 submission errors + \\# TYPE sequencer_l1_submission_errors counter + \\sequencer_l1_submission_errors {d} + \\ + , .{ + self.metrics.transactions_received.load(.monotonic), + self.metrics.transactions_accepted.load(.monotonic), + self.metrics.transactions_rejected.load(.monotonic), + self.metrics.blocks_created.load(.monotonic), + self.metrics.batches_submitted.load(.monotonic), + self.metrics.mempool_size.load(.monotonic), + self.metrics.l1_submission_errors.load(.monotonic), + }); + + response.body = try metrics_buffer.toOwnedSlice(); + defer self.allocator.free(response.body); + + return try response.format(self.allocator); + } + + fn createErrorResponse(self: *MetricsServer, status_code: u16, message: []const u8) ![]u8 { + var response = http.HttpResponse.init(self.allocator); + defer response.deinit(); + + response.status_code = status_code; + try response.headers.put("Content-Type", "text/plain"); + + const body = try std.fmt.allocPrint(self.allocator, "{s}\r\n", .{message}); + defer self.allocator.free(body); + response.body = body; + + return try response.format(self.allocator); + } + + pub fn deinit(self: *MetricsServer) void { + self.http_server.deinit(); + } +}; diff --git a/src/persistence/rocksdb.zig b/src/persistence/rocksdb.zig new file mode 100644 index 0000000..ef1ef92 --- /dev/null +++ b/src/persistence/rocksdb.zig @@ -0,0 +1,322 @@ +// RocksDB persistence layer for Native Sequencer +// Note: RocksDB is not available on Windows + +const std = @import("std"); +const builtin = @import("builtin"); +const core = @import("../core/root.zig"); + +// Conditionally import rocksdb only on non-Windows platforms +const rocksdb = if (builtin.os.tag == .windows) struct { + pub const DB = struct {}; + pub const ColumnFamilyHandle = struct {}; + pub const Data = struct { + data: []const u8, + pub fn deinit(_: *@This()) void {} + }; + pub const DBOptions = struct { + create_if_missing: bool = true, + create_missing_column_families: bool = true, + }; + pub const ColumnFamilyDescription = struct { + name: []const u8, + options: struct {}, + }; + pub const ColumnFamily = struct { + handle: ColumnFamilyHandle, + }; +} else @import("rocksdb"); + +pub const RocksDBError = error{ + DatabaseOpenFailed, + DatabaseOperationFailed, + SerializationFailed, + DeserializationFailed, + KeyNotFound, + UnsupportedPlatform, // Windows is not supported +}; + +pub const Database = struct { + allocator: std.mem.Allocator, + db: rocksdb.DB, + path: []const u8, + default_cf_handle: rocksdb.ColumnFamilyHandle, // Store default column family handle + + /// Open or create a RocksDB database + /// Note: Not supported on Windows - returns error.UnsupportedPlatform + pub fn open(allocator: std.mem.Allocator, path: []const u8) !Database { + if (builtin.os.tag == .windows) { + return error.UnsupportedPlatform; + } + const path_owned = try allocator.dupe(u8, path); + defer allocator.free(path_owned); + + // Convert path to null-terminated string (like zeam does) + const path_null = try std.fmt.allocPrintZ(allocator, "{s}", .{path}); + defer allocator.free(path_null); + + // Create directory if it doesn't exist + std.fs.cwd().makePath(path) catch |err| { + std.log.err("Failed to create RocksDB directory at {s}: {any}", .{ path, err }); + return error.DatabaseOpenFailed; + }; + + // Create options using DBOptions (like zeam does) + const options = rocksdb.DBOptions{ + .create_if_missing = true, + .create_missing_column_families = true, + }; + + // Create default column family description + const column_family_descriptions = try allocator.alloc(rocksdb.ColumnFamilyDescription, 1); + defer allocator.free(column_family_descriptions); + column_family_descriptions[0] = .{ .name = "default", .options = .{} }; + + // Open database - rocksdb.DB.open requires 5 arguments including error pointer + var err_str: ?rocksdb.Data = null; + const db: rocksdb.DB, const cfs: []const rocksdb.ColumnFamily = try rocksdb.DB.open( + allocator, + path_null, + options, + column_family_descriptions, + &err_str, + ); + defer allocator.free(cfs); + + std.log.info("Opened RocksDB database at {s}", .{path}); + + const path_stored = try allocator.dupe(u8, path); + + // Store the default column family handle (index 0) + const default_cf_handle = cfs[0].handle; + + return Database{ + .allocator = allocator, + .db = db, + .path = path_stored, + .default_cf_handle = default_cf_handle, + }; + } + + /// Close the database + pub fn close(self: *Database) void { + if (builtin.os.tag != .windows) { + self.db.deinit(); + } + self.allocator.free(self.path); + } + + /// Put a key-value pair + pub fn put(self: *Database, key: []const u8, value: []const u8) !void { + if (builtin.os.tag == .windows) { + return error.UnsupportedPlatform; + } + var err_str: ?rocksdb.Data = null; + self.db.put(self.default_cf_handle, key, value, &err_str) catch |err| { + std.log.err("Failed to put key-value pair: {any}", .{err}); + return error.DatabaseOperationFailed; + }; + } + + /// Get a value by key + pub fn get(self: *Database, key: []const u8) !?rocksdb.Data { + if (builtin.os.tag == .windows) { + return error.UnsupportedPlatform; + } + var err_str: ?rocksdb.Data = null; + const value = self.db.get(self.default_cf_handle, key, &err_str) catch |err| { + std.log.err("Failed to get value for key: {any}", .{err}); + return error.DatabaseOperationFailed; + }; + return value; + } + + /// Delete a key-value pair + pub fn delete(self: *Database, key: []const u8) !void { + if (builtin.os.tag == .windows) { + return error.UnsupportedPlatform; + } + var err_str: ?rocksdb.Data = null; + self.db.delete(self.default_cf_handle, key, &err_str) catch |err| { + std.log.err("Failed to delete key: {any}", .{err}); + return error.DatabaseOperationFailed; + }; + } + + /// Check if a key exists + pub fn exists(self: *Database, key: []const u8) !bool { + if (builtin.os.tag == .windows) { + return error.UnsupportedPlatform; + } + const value = try self.get(key); + if (value) |v| { + v.deinit(); + return true; + } + return false; + } + + /// Store an address -> u64 mapping (for nonces) + pub fn putNonce(self: *Database, address: core.types.Address, nonce: u64) !void { + if (builtin.os.tag == .windows) { + return error.UnsupportedPlatform; + } + const key = try self.addressToKey("nonce:", address); + defer self.allocator.free(key); + + var value_buf: [8]u8 = undefined; + std.mem.writeInt(u64, &value_buf, nonce, .big); + try self.put(key, &value_buf); + } + + /// Get a nonce for an address + pub fn getNonce(self: *Database, address: core.types.Address) !?u64 { + if (builtin.os.tag == .windows) { + return error.UnsupportedPlatform; + } + const key = try self.addressToKey("nonce:", address); + defer self.allocator.free(key); + + const value_opt = try self.get(key); + defer if (value_opt) |v| v.deinit(); + + const value = value_opt orelse return null; + + if (value.data.len != 8) { + return error.DeserializationFailed; + } + + var value_buf: [8]u8 = undefined; + @memcpy(&value_buf, value.data[0..8]); + return std.mem.readInt(u64, &value_buf, .big); + } + + /// Store an address -> u256 mapping (for balances) + pub fn putBalance(self: *Database, address: core.types.Address, balance: u256) !void { + if (builtin.os.tag == .windows) { + return error.UnsupportedPlatform; + } + const key = try self.addressToKey("balance:", address); + defer self.allocator.free(key); + + var value_buf: [32]u8 = undefined; + std.mem.writeInt(u256, &value_buf, balance, .big); + try self.put(key, &value_buf); + } + + /// Get a balance for an address + pub fn getBalance(self: *Database, address: core.types.Address) !?u256 { + if (builtin.os.tag == .windows) { + return error.UnsupportedPlatform; + } + const key = try self.addressToKey("balance:", address); + defer self.allocator.free(key); + + const value_opt = try self.get(key); + defer if (value_opt) |v| v.deinit(); + + const value = value_opt orelse return null; + + if (value.data.len != 32) { + return error.DeserializationFailed; + } + + var value_buf: [32]u8 = undefined; + @memcpy(&value_buf, value.data[0..32]); + return std.mem.readInt(u256, &value_buf, .big); + } + + /// Store a receipt by transaction hash + pub fn putReceipt(self: *Database, tx_hash: core.types.Hash, receipt: core.receipt.Receipt) !void { + if (builtin.os.tag == .windows) { + return error.UnsupportedPlatform; + } + const key = try self.hashToKey("receipt:", tx_hash); + defer self.allocator.free(key); + + // Serialize receipt (simplified - in production use proper serialization) + const serialized = try self.serializeReceipt(receipt); + defer self.allocator.free(serialized); + + try self.put(key, serialized); + } + + /// Get a receipt by transaction hash + pub fn getReceipt(self: *Database, tx_hash: core.types.Hash) !?core.receipt.Receipt { + if (builtin.os.tag == .windows) { + return error.UnsupportedPlatform; + } + const key = try self.hashToKey("receipt:", tx_hash); + defer self.allocator.free(key); + + const value_opt = try self.get(key); + defer if (value_opt) |v| v.deinit(); + + const value = value_opt orelse return null; + + return try self.deserializeReceipt(value.data); + } + + /// Store current block number + pub fn putBlockNumber(self: *Database, block_number: u64) !void { + if (builtin.os.tag == .windows) { + return error.UnsupportedPlatform; + } + const key = "block_number"; + var value_buf: [8]u8 = undefined; + std.mem.writeInt(u64, &value_buf, block_number, .big); + try self.put(key, &value_buf); + } + + /// Get current block number + pub fn getBlockNumber(self: *Database) !?u64 { + if (builtin.os.tag == .windows) { + return error.UnsupportedPlatform; + } + const key = "block_number"; + const value_opt = try self.get(key); + defer if (value_opt) |v| v.deinit(); + + const value = value_opt orelse return null; + + if (value.data.len != 8) { + return error.DeserializationFailed; + } + + var value_buf: [8]u8 = undefined; + @memcpy(&value_buf, value.data[0..8]); + return std.mem.readInt(u64, &value_buf, .big); + } + + /// Helper: Convert address to database key + fn addressToKey(self: *Database, prefix: []const u8, address: core.types.Address) ![]u8 { + const addr_bytes = address.toBytes(); + const prefix_len = prefix.len; + const key = try self.allocator.alloc(u8, prefix_len + 32); + @memcpy(key[0..prefix_len], prefix); + @memcpy(key[prefix_len..], &addr_bytes); + return key; + } + + /// Helper: Convert hash to database key + fn hashToKey(self: *Database, prefix: []const u8, hash: core.types.Hash) ![]u8 { + const hash_bytes = hash.toBytes(); + const prefix_len = prefix.len; + const key = try self.allocator.alloc(u8, prefix_len + 32); + @memcpy(key[0..prefix_len], prefix); + @memcpy(key[prefix_len..], &hash_bytes); + return key; + } + + /// Serialize receipt (simplified implementation) + fn serializeReceipt(self: *Database, _: core.receipt.Receipt) ![]u8 { + // TODO: Implement proper RLP or protobuf serialization + // For now, return empty slice as placeholder + return try self.allocator.alloc(u8, 0); + } + + /// Deserialize receipt (simplified implementation) + fn deserializeReceipt(_: *Database, _: []const u8) !core.receipt.Receipt { + // TODO: Implement proper deserialization + return error.DeserializationFailed; + } +}; diff --git a/src/persistence/root.zig b/src/persistence/root.zig new file mode 100644 index 0000000..d7bb9c3 --- /dev/null +++ b/src/persistence/root.zig @@ -0,0 +1,4 @@ +pub const rocksdb = @import("rocksdb.zig"); + +// Note: RocksDB types (Options, ReadOptions, WriteOptions) are not available on Windows +// They are only exported when rocksdb module is available (non-Windows platforms) diff --git a/src/root.zig b/src/root.zig index 19c8f82..e4f283b 100644 --- a/src/root.zig +++ b/src/root.zig @@ -10,3 +10,4 @@ pub const state = @import("state/root.zig"); pub const api = @import("api/root.zig"); pub const metrics = @import("metrics/root.zig"); pub const config = @import("config/root.zig"); +pub const persistence = @import("persistence/root.zig"); diff --git a/src/sequencer/execution.zig b/src/sequencer/execution.zig new file mode 100644 index 0000000..63791e0 --- /dev/null +++ b/src/sequencer/execution.zig @@ -0,0 +1,147 @@ +// Transaction execution engine + +const std = @import("std"); +const core = @import("../core/root.zig"); +const state = @import("../state/root.zig"); + +pub const ExecutionResult = struct { + success: bool, + gas_used: u64, + return_data: []const u8, + logs: []core.receipt.Receipt.Log, +}; + +pub const ExecutionEngine = struct { + allocator: std.mem.Allocator, + state_manager: *state.StateManager, + + pub fn init(allocator: std.mem.Allocator, sm: *state.StateManager) ExecutionEngine { + return .{ + .allocator = allocator, + .state_manager = sm, + }; + } + + pub fn executeTransaction(self: *ExecutionEngine, tx: core.transaction.Transaction) !ExecutionResult { + const sender = try tx.sender(); + + // Get current state + const sender_nonce = try self.state_manager.getNonce(sender); + const sender_balance = try self.state_manager.getBalance(sender); + + // Validate nonce + if (tx.nonce != sender_nonce) { + return ExecutionResult{ + .success = false, + .gas_used = 0, + .return_data = "", + .logs = &[_]core.receipt.Receipt.Log{}, + }; + } + + // Calculate base gas cost + const base_gas: u64 = 21000; // Base transaction cost + var gas_used: u64 = base_gas; + + // Add gas for data (4 gas per zero byte, 16 gas per non-zero byte) + for (tx.data) |byte| { + if (byte == 0) { + gas_used += 4; + } else { + gas_used += 16; + } + } + + // Add gas for contract creation (32000 gas) + if (tx.to == null) { + gas_used += 32000; + } + + // Calculate total cost + // gas_price and value are already u256 types + const gas_cost = tx.gas_price * @as(u256, gas_used); + const total_cost = tx.value + gas_cost; + + // Check balance + if (sender_balance < total_cost) { + return ExecutionResult{ + .success = false, + .gas_used = 0, + .return_data = "", + .logs = &[_]core.receipt.Receipt.Log{}, + }; + } + + // Check gas limit + if (gas_used > tx.gas_limit) { + return ExecutionResult{ + .success = false, + .gas_used = tx.gas_limit, // Consume all gas on failure + .return_data = "", + .logs = &[_]core.receipt.Receipt.Log{}, + }; + } + + // Execute transaction + if (tx.to) |to| { + // Contract call or transfer + return try self.executeCall(tx, sender, to, gas_used, gas_cost); + } else { + // Contract creation + return try self.executeCreate(tx, sender, gas_used, gas_cost); + } + } + + fn executeCall(self: *ExecutionEngine, tx: core.transaction.Transaction, sender: core.types.Address, to: core.types.Address, gas_used: u64, gas_cost: u256) !ExecutionResult { + // Update sender balance + const sender_balance = try self.state_manager.getBalance(sender); + const total_cost = tx.value + gas_cost; + const new_sender_balance = if (sender_balance >= total_cost) sender_balance - total_cost else 0; + try self.state_manager.setBalance(sender, new_sender_balance); + + // Update recipient balance (only if transaction succeeded) + if (sender_balance >= total_cost) { + const recipient_balance = try self.state_manager.getBalance(to); + const new_recipient_balance = recipient_balance + tx.value; + try self.state_manager.setBalance(to, new_recipient_balance); + } + + // Increment nonce + try self.state_manager.incrementNonce(sender); + + // For now, contract calls are simplified - just return success + // In production, this would execute EVM bytecode + const return_data = if (tx.data.len > 0) "" else ""; + + return ExecutionResult{ + .success = true, + .gas_used = gas_used, + .return_data = return_data, + .logs = &[_]core.receipt.Receipt.Log{}, + }; + } + + fn executeCreate(self: *ExecutionEngine, tx: core.transaction.Transaction, sender: core.types.Address, gas_used: u64, gas_cost: u256) !ExecutionResult { + // Update sender balance + const sender_balance = try self.state_manager.getBalance(sender); + const total_cost = tx.value + gas_cost; + const new_sender_balance = if (sender_balance >= total_cost) sender_balance - total_cost else 0; + try self.state_manager.setBalance(sender, new_sender_balance); + + // Increment nonce + try self.state_manager.incrementNonce(sender); + + // For contract creation, we would: + // 1. Execute init code + // 2. Create new contract account + // 3. Set contract code + // For now, simplified implementation + + return ExecutionResult{ + .success = true, + .gas_used = gas_used, + .return_data = "", + .logs = &[_]core.receipt.Receipt.Log{}, + }; + } +}; diff --git a/src/sequencer/mev.zig b/src/sequencer/mev.zig index 4970ad2..a075c65 100644 --- a/src/sequencer/mev.zig +++ b/src/sequencer/mev.zig @@ -12,7 +12,7 @@ pub const MEVOrderer = struct { // Simplified MEV - in production implement bundle detection, backrunning, etc. // For now, just return sorted by priority // Use ArrayList to avoid allocator issues with Transaction slices - var sorted = std.array_list.Managed(core.transaction.Transaction).init(self.allocator); + var sorted = std.ArrayList(core.transaction.Transaction).init(self.allocator); errdefer sorted.deinit(); try sorted.appendSlice(txs); diff --git a/src/sequencer/sequencer.zig b/src/sequencer/sequencer.zig index 10d2a6f..34507b5 100644 --- a/src/sequencer/sequencer.zig +++ b/src/sequencer/sequencer.zig @@ -5,6 +5,22 @@ const batch = @import("../batch/root.zig"); const state = @import("../state/root.zig"); const config = @import("../config/root.zig"); const mev = @import("mev.zig"); +const execution = @import("execution.zig"); + +fn formatHash(hash: core.types.Hash) []const u8 { + // Format hash as hex string for logging + const bytes = hash.toBytes(); + var buffer: [66]u8 = undefined; // "0x" + 64 hex chars + buffer[0] = '0'; + buffer[1] = 'x'; + // Format each byte as hex + for (bytes, 0..) |byte, i| { + const hex_chars = "0123456789abcdef"; + buffer[2 + i * 2] = hex_chars[byte >> 4]; + buffer[2 + i * 2 + 1] = hex_chars[byte & 0xf]; + } + return buffer[0..66]; +} pub const Sequencer = struct { allocator: std.mem.Allocator, @@ -16,6 +32,8 @@ pub const Sequencer = struct { current_block_number: u64 = 0, parent_hash: core.types.Hash = core.types.hashFromBytes([_]u8{0} ** 32), + execution_engine: execution.ExecutionEngine, + pub fn init(allocator: std.mem.Allocator, cfg: *const config.Config, mp: *mempool.Mempool, sm: *state.StateManager, bb: *batch.Builder) Sequencer { return .{ .allocator = allocator, @@ -24,6 +42,7 @@ pub const Sequencer = struct { .state_manager = sm, .batch_builder = bb, .mev_orderer = mev.MEVOrderer.init(allocator), + .execution_engine = execution.ExecutionEngine.init(allocator, sm), }; } @@ -38,23 +57,39 @@ pub const Sequencer = struct { // Build block var gas_used: u64 = 0; - var valid_txs = std.array_list.Managed(core.transaction.Transaction).init(self.allocator); + var valid_txs = std.ArrayList(core.transaction.Transaction).init(self.allocator); defer valid_txs.deinit(); for (mev_txs) |tx| { - // Light simulation check - const expected_nonce = try self.state_manager.getNonce(try tx.sender()); - if (tx.nonce != expected_nonce) continue; + // Check if transaction fits in block gas limit + const estimated_gas = tx.gas_limit; + if (gas_used + estimated_gas > self.config.block_gas_limit) break; + + // Execute transaction + const exec_result = self.execution_engine.executeTransaction(tx) catch |err| { + std.log.warn("Transaction execution error: {any}", .{err}); + continue; + }; - if (gas_used + tx.gas_limit > self.config.block_gas_limit) break; + // Skip failed transactions + if (!exec_result.success) { + const tx_hash = tx.hash(self.allocator) catch continue; + std.log.warn("Transaction execution failed (hash={s}, gas_used={d})", .{ formatHash(tx_hash), exec_result.gas_used }); + continue; + } - // Apply transaction (simplified - in production run full execution) - _ = try self.state_manager.applyTransaction(tx, tx.gas_limit); - gas_used += tx.gas_limit; + // Check if execution fits in block gas limit + if (gas_used + exec_result.gas_used > self.config.block_gas_limit) break; + + // Apply state changes (execution engine already updated state) + // Create receipt + const tx_hash = try tx.hash(self.allocator); + _ = try self.state_manager.applyTransaction(tx, exec_result.gas_used); + + gas_used += exec_result.gas_used; try valid_txs.append(tx); // Remove from mempool - const tx_hash = try tx.hash(self.allocator); // tx_hash is U256 struct (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 51f4706..79ec255 100644 --- a/src/state/manager.zig +++ b/src/state/manager.zig @@ -1,5 +1,6 @@ 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 @@ -27,16 +28,51 @@ pub const StateManager = struct { 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), current_block_number: u64 = 0, + db: ?*persistence.rocksdb.Database = null, + use_persistence: bool = false, + /// Initialize StateManager with optional RocksDB persistence 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), + .db = null, + .use_persistence = false, }; } + /// Initialize StateManager with RocksDB persistence + pub fn initWithPersistence(allocator: std.mem.Allocator, db: *persistence.rocksdb.Database) !StateManager { + var sm = init(allocator); + sm.db = db; + sm.use_persistence = true; + + // Load persisted state from database + try sm.loadFromDatabase(); + + return sm; + } + + /// Load state from RocksDB database + fn loadFromDatabase(self: *StateManager) !void { + if (self.db == null) return; + + const db = self.db.?; + + // Load current block number + if (try db.getBlockNumber()) |block_num| { + self.current_block_number = block_num; + std.log.info("Loaded block number from database: {d}", .{block_num}); + } + + // Note: Loading all nonces/balances/receipts into memory would be expensive + // For now, we load on-demand. In production, consider using iterators or + // loading only frequently accessed data + std.log.info("State manager initialized with RocksDB persistence", .{}); + } + pub fn deinit(self: *StateManager) void { self.nonces.deinit(); self.balances.deinit(); @@ -51,21 +87,78 @@ pub const StateManager = struct { self.receipts.deinit(); } - pub fn getNonce(self: *const StateManager, address: core.types.Address) !u64 { - return self.nonces.get(address) orelse 0; + pub fn getNonce(self: *StateManager, address: core.types.Address) !u64 { + // Check in-memory cache first + if (self.nonces.get(address)) |nonce| { + return nonce; + } + + // If using persistence, try to load from database + if (self.use_persistence) { + if (self.db) |db| { + if (try db.getNonce(address)) |nonce| { + // Cache in memory + try self.nonces.put(address, nonce); + return nonce; + } + } + } + + // Default to 0 for new addresses + try self.nonces.put(address, 0); + return 0; } - pub fn getBalance(self: *const StateManager, address: core.types.Address) !u256 { - return self.balances.get(address) orelse 0; + pub fn getBalance(self: *StateManager, address: core.types.Address) !u256 { + // Check in-memory cache first + if (self.balances.get(address)) |balance| { + return balance; + } + + // If using persistence, try to load from database + if (self.use_persistence) { + if (self.db) |db| { + if (try db.getBalance(address)) |balance| { + // Cache in memory + try self.balances.put(address, balance); + return balance; + } + } + } + + // Default to 0 for new addresses + try self.balances.put(address, 0); + return 0; } pub fn setBalance(self: *StateManager, address: core.types.Address, balance: u256) !void { + // Update in-memory cache try self.balances.put(address, balance); + + // Persist to database if enabled + if (self.use_persistence) { + if (self.db) |db| { + try db.putBalance(address, balance); + } + } } pub fn incrementNonce(self: *StateManager, address: core.types.Address) !void { - const current = self.getNonce(address) catch 0; - try self.nonces.put(address, current + 1); + const current = try self.getNonce(address); + const new_nonce = current + 1; + try self.setNonce(address, new_nonce); + } + + pub fn setNonce(self: *StateManager, address: core.types.Address, nonce: u64) !void { + // Update in-memory cache + try self.nonces.put(address, nonce); + + // Persist to database if enabled + if (self.use_persistence) { + if (self.db) |db| { + try db.putNonce(address, nonce); + } + } } pub fn applyTransaction(self: *StateManager, tx: core.transaction.Transaction, gas_used: u64) !core.receipt.Receipt { @@ -110,7 +203,13 @@ pub const StateManager = struct { } pub fn finalizeBlock(self: *StateManager, block: core.block.Block) !void { - self.current_block_number = block.number; - // In production, update state root, receipts root, etc. + self.current_block_number = block.number + 1; + + // Persist block number to database if enabled + if (self.use_persistence) { + if (self.db) |db| { + try db.putBlockNumber(self.current_block_number); + } + } } }; diff --git a/src/validation/ingress.zig b/src/validation/ingress.zig index 8023483..fc39660 100644 --- a/src/validation/ingress.zig +++ b/src/validation/ingress.zig @@ -40,9 +40,32 @@ pub const Ingress = struct { return .valid; } + /// Accept ExecuteTx transaction + /// ExecuteTx transactions are stateless and should be forwarded to L1 geth + /// We only do minimal validation (signature check for deduplication) + /// Full validation will be done by L1 geth when the transaction is executed + pub fn acceptExecuteTx(self: *Ingress, execute_tx: *core.transaction_execute.ExecuteTx) !validator.ValidationResult { + // Minimal validation: check signature for deduplication purposes + // We don't validate nonce/balance since ExecuteTx is stateless and L1 geth will validate it + _ = execute_tx.sender(self.allocator) catch { + return .invalid_signature; + }; + + // Check if duplicate in mempool (by hash) + const tx_hash = try execute_tx.hash(self.allocator); + if (self.mempool.contains(tx_hash)) { + return .duplicate; + } + + // ExecuteTx transactions are forwarded to L1 geth, not stored in mempool + // They will be sent directly to L1 via eth_sendRawTransaction + + return .valid; + } + pub fn validateBatch(self: *Ingress, txs: []core.transaction.Transaction) ![]validator.ValidationResult { // Use ArrayList to avoid allocator issues - var results = std.array_list.Managed(validator.ValidationResult).init(self.allocator); + var results = std.ArrayList(validator.ValidationResult).init(self.allocator); defer results.deinit(); errdefer results.deinit(); diff --git a/src/validation/transaction.zig b/src/validation/transaction.zig index 3d2a24d..c9b53f0 100644 --- a/src/validation/transaction.zig +++ b/src/validation/transaction.zig @@ -11,6 +11,7 @@ pub const ValidationResult = enum { insufficient_balance, invalid_gas_price, duplicate, + invalid_execute_tx, }; pub const TransactionValidator = struct { @@ -56,4 +57,8 @@ pub const TransactionValidator = struct { return .valid; } + + // Note: ExecuteTx transactions are stateless and should be forwarded to L1 geth + // We don't do full validation here - L1 geth will validate them when executed + // Only minimal validation (signature check for deduplication) is done in acceptExecuteTx };