diff --git a/build.zig b/build.zig index adff67e..dcbcfe3 100644 --- a/build.zig +++ b/build.zig @@ -11,13 +11,19 @@ pub fn build(b: *std.Build) void { // for restricting supported target set are available. const target = b.standardTargetOptions(.{}); - const mod_rlp = b.dependency("zig-rlp", .{}).module("zig-rlp"); - // Standard optimization options allow the person running `zig build` to select // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not // set a preferred release mode, allowing the user to decide how to optimize. const optimize = b.standardOptimizeOption(.{}); + const mod_rlp = b.dependency("zig-rlp", .{}).module("zig-rlp"); + + const zap = b.dependency("zap", .{ + .target = target, + .optimize = optimize, + }); + const mod_zap = zap.module("zap"); + const ethash = b.addStaticLibrary(.{ .name = "ethash", .optimize = optimize, @@ -95,6 +101,8 @@ pub fn build(b: *std.Build) void { exe.linkLibrary(ethash); exe.linkLibrary(evmone); exe.linkLibC(); + exe.addModule("zap", mod_zap); + exe.linkLibrary(zap.artifact("facil.io")); exe.addModule("zig-rlp", mod_rlp); // This declares intent for the executable to be installed into the diff --git a/build.zig.zon b/build.zig.zon index 80f7236..062bdd7 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,10 +1,11 @@ .{ .name = "zig-rlp", .version = "0.0.1-beta-0", - .dependencies = .{ - .@"zig-rlp" = .{ - .url = "https://github.com/gballet/zig-rlp/archive/refs/tags/v0.0.1-beta-0.tar.gz", - .hash = "122000e0811d6cb4758f6122b1de2d384efa32b9b2714caec2236f6b34b0529d699c", - }, - }, + .dependencies = .{ .@"zig-rlp" = .{ + .url = "https://github.com/gballet/zig-rlp/archive/refs/tags/v0.0.1-beta-0.tar.gz", + .hash = "122000e0811d6cb4758f6122b1de2d384efa32b9b2714caec2236f6b34b0529d699c", + }, .zap = .{ + .url = "https://github.com/zigzap/zap/archive/refs/tags/v0.1.14-pre.tar.gz", + .hash = "122067d12bc7abb93f7ce623f61b94cadfdb180cef12da6559d092e6b374202acda3", + } }, } diff --git a/src/engine_api/engine_api.zig b/src/engine_api/engine_api.zig new file mode 100644 index 0000000..c1beb37 --- /dev/null +++ b/src/engine_api/engine_api.zig @@ -0,0 +1,111 @@ +const std = @import("std"); +const fmt = std.fmt; +const Allocator = std.mem.Allocator; +pub const execution_payload = @import("./execution_payload.zig"); +const ExecutionPayload = execution_payload.ExecutionPayload; + +fn prefixedhex2hash(dst: []u8, src: []const u8) !void { + if (src.len < 2 or src.len % 2 != 0) { + return error.InvalidInput; + } + var skip0x: usize = if (src[1] == 'X' or src[1] == 'x') 2 else 0; + if (src[skip0x..].len != 2 * dst.len) { + return error.InvalidOutputLength; + } + _ = try fmt.hexToBytes(dst[0..], src[skip0x..]); +} + +fn prefixedhex2byteslice(allocator: Allocator, src: []const u8) ![]u8 { + // TODO catch the 0x0 corner case + if (src.len < 2 or src.len % 2 != 0) { + return error.InvalidInput; + } + var skip0x: usize = if (src[1] == 'X' or src[1] == 'x') 2 else 0; + var dst: []u8 = try allocator.alloc(u8, src[skip0x..].len / 2); + + _ = try fmt.hexToBytes(dst[0..], src[skip0x..]); + + return dst; +} + +fn prefixedhex2u64(src: []const u8) !u64 { + // execution engine integers can be odd-length :facepalm: + if (src.len < 3) { + return error.InvalidInput; + } + + var skip0x: usize = if (src[1] == 'X' or src[1] == 'x') 2 else 0; + + return std.fmt.parseInt(u64, src[skip0x..], 16); +} + +// This is an intermediate structure used to deserialize the hex strings +// from the JSON request. I have seen some zig libraries that can do this +// out of the box, but it seems that approach hasn't been merged into the +// std yet. +const ExecutionPayloadJSON = struct { + parentHash: []const u8, + feeRecipient: []const u8, + stateRoot: []const u8, + receiptsRoot: []const u8, + logsBloom: []const u8, + prevRandao: []const u8, + blockNumber: []const u8, + gasLimit: []const u8, + gasUsed: []const u8, + timestamp: []const u8, + extraData: []const u8, + baseFeePerGas: []const u8, + blockHash: []const u8, + transactions: [][]const u8, + + pub fn to_execution_payload(self: *const ExecutionPayloadJSON, allocator: Allocator) !ExecutionPayload { + var parentHash: [32]u8 = undefined; + var feeRecipient: [20]u8 = undefined; + var stateRoot: [32]u8 = undefined; + var receiptsRoot: [32]u8 = undefined; + var logsBloom: [256]u8 = undefined; + var prevRandao: [32]u8 = undefined; + var blockHash: [32]u8 = undefined; + + var transactions: [][]const u8 = &[_][]const u8{}; + if (self.transactions.len > 0) { + transactions = try allocator.alloc([]const u8, self.transactions.len); + } + + _ = try prefixedhex2hash(parentHash[0..], self.parentHash); + _ = try prefixedhex2hash(feeRecipient[0..], self.feeRecipient); + _ = try prefixedhex2hash(stateRoot[0..], self.stateRoot); + _ = try prefixedhex2hash(receiptsRoot[0..], self.receiptsRoot); + _ = try prefixedhex2hash(logsBloom[0..], self.logsBloom); + _ = try prefixedhex2hash(prevRandao[0..], self.prevRandao); + _ = try prefixedhex2hash(blockHash[0..], self.blockHash); + + return ExecutionPayload{ + .parentHash = parentHash, + .feeRecipient = feeRecipient, + .stateRoot = stateRoot, + .receiptsRoot = receiptsRoot, + .prevRandao = prevRandao, + .extraData = try prefixedhex2byteslice(allocator, self.extraData), + .blockHash = blockHash, + .logsBloom = logsBloom, + .blockNumber = try prefixedhex2u64(self.blockNumber), + .gasLimit = try prefixedhex2u64(self.gasLimit), + .gasUsed = try prefixedhex2u64(self.gasUsed), + .timestamp = try prefixedhex2u64(self.timestamp), + .baseFeePerGas = try prefixedhex2u64(self.baseFeePerGas), + .transactions = transactions, + .withdrawals = null, + .blobGasUsed = null, + .excessBlobGas = null, + }; + } +}; + +pub const EngineAPIRequest = struct { + jsonrpc: []const u8, + id: u64, + method: []const u8, + params: []const ExecutionPayloadJSON, +}; diff --git a/src/engine_api/execution_payload.zig b/src/engine_api/execution_payload.zig new file mode 100644 index 0000000..7578b30 --- /dev/null +++ b/src/engine_api/execution_payload.zig @@ -0,0 +1,64 @@ +const std = @import("std"); +const types = @import("../types/types.zig"); + +pub const ExecutionPayload = struct { + parentHash: types.Hash32, + feeRecipient: types.Address, + stateRoot: types.Hash32, + receiptsRoot: types.Hash32, + logsBloom: [256]u8, + prevRandao: types.Hash32, + blockNumber: u64, + gasLimit: u64, + gasUsed: u64, + timestamp: u64, + extraData: []const u8, + baseFeePerGas: u256, + blockHash: types.Hash32, + transactions: [][]const u8, + + withdrawals: ?[]types.Withdrawal, + blobGasUsed: ?u64, + excessBlobGas: ?u64, + // executionWitness : ?types.ExecutionWitness, + + pub fn to_block(self: *const ExecutionPayload) types.Block { + return types.Block{ + .header = types.BlockHeader{ + .parent_hash = self.parentHash, + .uncle_hash = types.empty_uncle_hash, + .fee_recipient = self.feeRecipient, + .state_root = self.stateRoot, + .receipts_root = self.receiptsRoot, + .logs_bloom = self.logsBloom, + .prev_randao = self.prevRandao, + .block_number = @intCast(self.blockNumber), + .gas_limit = @intCast(self.gasLimit), + .gas_used = self.gasUsed, + .timestamp = @intCast(self.timestamp), + .extra_data = self.extraData, + .base_fee_per_gas = self.baseFeePerGas, + .transactions_root = [_]u8{0} ** 32, + .mix_hash = 0, + .nonce = [_]u8{0} ** 8, + .blob_gas_used = null, + .withdrawals_root = null, + .excess_blob_gas = null, + }, + // .blockHash = self.blockHash, + // .transactions = self.transactions, + // .withdrawals = self.withdrawals, + }; + } +}; + +pub fn newPayloadV2Handler(params: *const ExecutionPayload) !void { + // TODO reconstruct the proof from the (currently undefined) execution witness + // and verify it. Then execute the block and return the result. + // vm.run_block(params.to_block(), params.transactions); + + // But so far, just print the content of the payload + std.log.info("newPayloadV2Handler: {any}", .{params}); + + _ = params.to_block(); +} diff --git a/src/main.zig b/src/main.zig index 6c679fd..46c70f2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,10 +6,50 @@ const VM = @import("vm/vm.zig").VM; const StateDB = @import("vm/statedb.zig"); const Block = types.Block; const Transaction = types.Transaction; +const zap = @import("zap"); +const engine_api = @import("engine_api/engine_api.zig"); +const json = std.json; + +var allocator: std.mem.Allocator = undefined; + +fn engineAPIHandler(r: zap.SimpleRequest) void { + if (r.body == null) { + r.setStatus(.bad_request); + return; + } + const payload = json.parseFromSlice(engine_api.EngineAPIRequest, allocator, r.body.?, .{ .ignore_unknown_fields = true }) catch |err| { + std.log.err("error parsing json: {} (body={s})", .{ err, r.body.? }); + r.setStatus(.bad_request); + return; + }; + defer payload.deinit(); + + if (std.mem.eql(u8, payload.value.method, "engine_newPayloadV2")) { + const execution_payload_json = payload.value.params[0]; + const execution_payload = execution_payload_json.to_execution_payload(allocator) catch |err| { + std.log.warn("error parsing execution payload: {}", .{err}); + r.setStatus(.bad_request); + return; + }; + engine_api.execution_payload.newPayloadV2Handler(&execution_payload) catch |err| { + std.log.err("error handling newPayloadV2: {}", .{err}); + r.setStatus(.internal_server_error); + return; + }; + r.setStatus(.ok); + } else { + r.setStatus(.internal_server_error); + } + r.setContentType(.JSON) catch |err| { + std.log.err("error setting content type: {}", .{err}); + r.setStatus(.internal_server_error); + return; + }; +} pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - var allocator = gpa.allocator(); + allocator = gpa.allocator(); std.log.info("Welcome to phant! 🐘", .{}); @@ -32,6 +72,9 @@ pub fn main() !void { .mix_hash = 0, .nonce = [_]u8{0} ** 8, .base_fee_per_gas = 10, + .withdrawals_root = null, + .blob_gas_used = null, + .excess_blob_gas = null, }, }; @@ -68,6 +111,18 @@ pub fn main() !void { std.log.err("error executing transaction: {}", .{err}); return; }; + + var listener = zap.SimpleHttpListener.init(.{ + .port = 8551, + .on_request = engineAPIHandler, + .log = true, + }); + try listener.listen(); + std.log.info("Listening on 8551", .{}); + zap.start(.{ + .threads = 1, + .workers = 1, + }); } test "tests" { diff --git a/src/types/block.zig b/src/types/block.zig index c35d487..40ca678 100644 --- a/src/types/block.zig +++ b/src/types/block.zig @@ -19,6 +19,8 @@ pub const Block = struct { } }; +pub const empty_uncle_hash: types.Hash32 = [_]u8{ 29, 204, 77, 232, 222, 199, 93, 122, 171, 133, 181, 103, 182, 204, 212, 26, 211, 18, 69, 27, 148, 138, 116, 19, 240, 161, 66, 253, 64, 212, 147, 71 }; + pub const Header = struct { parent_hash: types.Hash32, uncle_hash: types.Hash32, @@ -36,6 +38,9 @@ pub const Header = struct { mix_hash: u256, nonce: [8]u8, base_fee_per_gas: u256, + withdrawals_root: ?types.Hash32, + blob_gas_used: ?u64, + excess_blob_gas: ?u64, }; var test_allocator = std.testing.allocator; diff --git a/src/types/types.zig b/src/types/types.zig index 338f1e4..3297b3b 100644 --- a/src/types/types.zig +++ b/src/types/types.zig @@ -11,3 +11,6 @@ pub const AccountState = @import("account_state.zig"); pub const Transaction = @import("transaction.zig"); pub const Block = @import("block.zig").Block; pub const BlockHeader = @import("block.zig").Header; +pub const Withdrawal = @import("withdrawal.zig"); + +pub const empty_uncle_hash = @import("block.zig").empty_uncle_hash; diff --git a/src/types/withdrawal.zig b/src/types/withdrawal.zig new file mode 100644 index 0000000..9c44544 --- /dev/null +++ b/src/types/withdrawal.zig @@ -0,0 +1,9 @@ +const std = @import("std"); +const types = @import("types.zig"); + +pub const Withdrawal = struct { + index: u64, + validator: u64, + address: types.Address, + amount: u64, +};