diff --git a/bench/build.zig b/bench/build.zig index 8243c74..9dd5206 100644 --- a/bench/build.zig +++ b/bench/build.zig @@ -15,11 +15,13 @@ pub fn build(b: *std.Build) void { "Only run micro benchmark tests whose names contain this filter", ); const profile = buildProfileOption(b); + const native_keccak = nativeKeccakOption(b, profile); const evmz_dep = b.dependency("evmz", .{ .target = target, .optimize = optimize, .profile = profile, + .@"native-keccak" = native_keccak, }); const evmone_dep = b.dependency("evmone", .{ .target = target, .optimize = optimize }); const intx_dep = b.dependency("intx", .{ .target = target, .optimize = optimize }); @@ -194,6 +196,8 @@ pub fn build(b: *std.Build) void { @tagName(compare_optimize), "--profile", profile, + "--native-keccak", + native_keccak, "--support-min", vm_loop_support_min, "--support-max", @@ -211,6 +215,8 @@ pub fn build(b: *std.Build) void { b.graph.zig_exe, "--profile", profile, + "--native-keccak", + native_keccak, }); run_report.setCwd(b.path(".")); if (b.args) |args| run_report.addArgs(args); @@ -222,6 +228,7 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = micro_optimize, .profile = profile, + .@"native-keccak" = native_keccak, }); const micro_evmz_mod = micro_evmz_dep.module("evmz"); micro_evmz_mod.omit_frame_pointer = true; @@ -299,6 +306,14 @@ fn buildProfileOption(b: *std.Build) []const u8 { return profile; } +fn nativeKeccakOption(b: *std.Build, profile: []const u8) []const u8 { + const backend = b.option([]const u8, "native-keccak", "Native Keccak backend: std or xkcp") orelse "std"; + if (!std.mem.eql(u8, backend, "std") and !std.mem.eql(u8, backend, "xkcp")) { + std.debug.panic("unsupported native Keccak backend '{s}' (expected std or xkcp)", .{backend}); + } + return if (std.mem.eql(u8, profile, "native")) backend else "std"; +} + fn addRevmNativeRustFlags(run: *std.Build.Step.Run) void { run.setEnvironmentVariable("RUSTFLAGS", "-C target-cpu=native -C force-frame-pointers=no"); run.setEnvironmentVariable("CARGO_PROFILE_RELEASE_LTO", "fat"); diff --git a/bench/scripts/report.py b/bench/scripts/report.py index c6ac8a2..f2dd94f 100644 --- a/bench/scripts/report.py +++ b/bench/scripts/report.py @@ -86,6 +86,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--zig-exe", default="zig") parser.add_argument("--optimize", default="ReleaseFast") parser.add_argument("--profile", choices=("native", "zkvm"), default="native") + parser.add_argument("--native-keccak", choices=("std", "xkcp"), default="std") parser.add_argument("--out-dir", default="../output/bench-report") parser.add_argument("--report") parser.add_argument("--checkpoint") @@ -94,7 +95,10 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--host-iterations", type=int, default=100_000) parser.add_argument("--repeats", type=int, default=5) parser.add_argument("--warmups", type=int, default=1) - return parser.parse_args() + args = parser.parse_args() + if args.profile == "zkvm": + args.native_keccak = "std" + return args def resolve_path(value: str | Path) -> Path: @@ -110,6 +114,8 @@ def collect_environment(args: argparse.Namespace) -> dict[str, Any]: "platform": f"{platform.system()} {platform.machine()}", "zig": command_version([args.zig_exe, "version"]), "profile": args.profile, + "native_keccak": args.native_keccak, + "keccak_provider": args.native_keccak if args.profile == "native" else "zkvm", "rustc": tool_major_version(command_version(["rustc", "--version"])), "cargo": tool_major_version(command_version(["cargo", "--version"])), "solc": solc_version(), @@ -294,7 +300,7 @@ def vm_loop_scope(engine: str) -> str: def build_profile_args(args: argparse.Namespace) -> list[str]: - return [f"-Dprofile={args.profile}"] + return [f"-Dprofile={args.profile}", f"-Dnative-keccak={args.native_keccak}"] def run_host_matrix(args: argparse.Namespace, raw_dir: Path, out_dir: Path) -> list[dict[str, Any]]: diff --git a/bench/src/compare.zig b/bench/src/compare.zig index 7381ef2..427a140 100644 --- a/bench/src/compare.zig +++ b/bench/src/compare.zig @@ -43,6 +43,7 @@ const Options = struct { zig_exe: []const u8 = "zig", optimize: []const u8 = "ReleaseFast", profile: []const u8 = "native", + native_keccak: []const u8 = "std", support_min: ?[]const u8 = null, support_max: ?[]const u8 = null, engines: std.ArrayList(Engine) = .empty, @@ -150,6 +151,11 @@ fn parseOptions(init: std.process.Init, allocator: std.mem.Allocator) !Options { options.profile = try parseProfile(allocator, value); } else if (stripPrefix(arg, "--profile=")) |value| { options.profile = try parseProfile(allocator, value); + } else if (std.mem.eql(u8, arg, "--native-keccak")) { + const value = args.next() orelse return error.MissingNativeKeccak; + options.native_keccak = try parseNativeKeccak(allocator, value); + } else if (stripPrefix(arg, "--native-keccak=")) |value| { + options.native_keccak = try parseNativeKeccak(allocator, value); } else if (std.mem.eql(u8, arg, "--support-min")) { const value = args.next() orelse return error.MissingSupportMin; options.support_min = try allocator.dupe(u8, value); @@ -199,6 +205,7 @@ fn parseOptions(init: std.process.Init, allocator: std.mem.Allocator) !Options { } } + if (std.mem.eql(u8, options.profile, "zkvm")) options.native_keccak = "std"; return options; } @@ -212,6 +219,7 @@ fn printUsage() void { \\ --zig-exe Zig executable used for child bench steps \\ --optimize Zig/C++ runner optimization mode, default ReleaseFast \\ --profile evmz build profile forwarded to child Zig builds + \\ --native-keccak native Keccak backend forwarded to evmz builds \\ --support-min minimum evmz fork compiled into the VM-loop runner \\ --support-max maximum evmz fork compiled into the VM-loop runner \\ --engine engine filter, repeatable; all, evmz, evmone, revm, evmone-baseline, evmone-advanced, revm-interpreter @@ -287,6 +295,7 @@ fn engineCommand( } if (engine == .evmz) { try argv.append(allocator, try std.fmt.allocPrint(allocator, "-Dprofile={s}", .{options.profile})); + try argv.append(allocator, try std.fmt.allocPrint(allocator, "-Dnative-keccak={s}", .{options.native_keccak})); if (options.support_min) |support_min| { try argv.append(allocator, try std.fmt.allocPrint(allocator, "-Dbench-support-min={s}", .{support_min})); } @@ -690,6 +699,13 @@ fn parseProfile(allocator: std.mem.Allocator, value: []const u8) ![]const u8 { return allocator.dupe(u8, value); } +fn parseNativeKeccak(allocator: std.mem.Allocator, value: []const u8) ![]const u8 { + if (!std.mem.eql(u8, value, "std") and !std.mem.eql(u8, value, "xkcp")) { + return error.UnsupportedNativeKeccak; + } + return allocator.dupe(u8, value); +} + test "parse timing rows ignores non-floats" { const times = try parseTimesMs(std.testing.allocator, "1.0\nnoise\n2.5\n"); defer std.testing.allocator.free(times); diff --git a/build.zig b/build.zig index b7e431a..96f87e2 100644 --- a/build.zig +++ b/build.zig @@ -1,11 +1,34 @@ const std = @import("std"); +const EvmzBuildConfig = struct { + profile: []const u8, + native_keccak: []const u8, +}; + pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const profile = buildProfileOption(b); const is_native_profile = std.mem.eql(u8, profile, "native"); - const build_options = buildOptions(b, profile); + const requested_native_keccak = nativeKeccakOption(b); + const native_keccak = resolveNativeKeccak(profile, target, requested_native_keccak); + const evmz_build = EvmzBuildConfig{ .profile = profile, .native_keccak = native_keccak }; + const use_xkcp = std.mem.eql(u8, native_keccak, "xkcp"); + const xkcp_dep = if (use_xkcp) b.lazyDependency("xkcp", .{}) else null; + if (use_xkcp and xkcp_dep == null) return; + const xkcp_object = if (xkcp_dep) |dep| + buildXkcpObject(b, target, optimize, dep, "xkcp", null) + else + null; + const xkcp_pic_object = if (xkcp_dep) |dep| + buildXkcpObject(b, target, optimize, dep, "xkcp-pic", true) + else + null; + if (xkcp_dep) |dep| { + const install_license = b.addInstallFile(dep.path("LICENSE"), "share/licenses/evmz/XKCP.txt"); + b.getInstallStep().dependOn(&install_license.step); + } + const build_options = buildOptions(b, profile, native_keccak); const bench_optimize = b.option( std.builtin.OptimizeMode, "bench-optimize", @@ -43,6 +66,7 @@ pub fn build(b: *std.Build) void { if (native_precompile_deps) |deps| { addPrecompileNative(b, evmz_mod, deps, evmone_dep); } + addNativeKeccak(evmz_mod, xkcp_object); const ssz_mod = b.addModule("ssz", .{ .root_source_file = b.path("pkg/ssz/src/lib.zig"), @@ -64,6 +88,7 @@ pub fn build(b: *std.Build) void { c_lib_mod.addIncludePath(b.path("include")); c_lib_mod.addIncludePath(evmone_dep.path("evmc/include")); addPrecompileNative(b, c_lib_mod, native_precompile_deps.?, evmone_dep); + addNativeKeccak(c_lib_mod, xkcp_pic_object); const static_c_lib = b.addLibrary(.{ .name = "evmz", @@ -109,6 +134,7 @@ pub fn build(b: *std.Build) void { if (native_precompile_deps) |deps| { addPrecompileNative(b, lib_unit_tests_mod, deps, evmone_dep); } + addNativeKeccak(lib_unit_tests_mod, xkcp_object); const lib_unit_tests = b.addTest(.{ .root_module = lib_unit_tests_mod, .filters = b.args orelse &.{}, @@ -144,6 +170,7 @@ pub fn build(b: *std.Build) void { c_api_tests_mod.addIncludePath(b.path("include")); c_api_tests_mod.addIncludePath(evmone_dep.path("evmc/include")); addPrecompileNative(b, c_api_tests_mod, native_precompile_deps.?, evmone_dep); + addNativeKeccak(c_api_tests_mod, xkcp_object); const c_api_tests = b.addTest(.{ .root_module = c_api_tests_mod, .filters = b.args orelse &.{}, @@ -178,33 +205,33 @@ pub fn build(b: *std.Build) void { const optimize_name = @tagName(optimize); const bench_optimize_name = @tagName(bench_optimize); if (pathExists(b, "eest/build.zig")) { - addEestDelegate(b, "eest-test", "Run sidecar EEST runner tests", "test", optimize_name, null, profile); - addEestDelegate(b, "eest", "Run EEST state-test fixtures", "eest", optimize_name, null, profile); - addEestDelegate(b, "eest-classify", "Classify EEST state-test fixtures", "eest-classify", optimize_name, null, profile); - addEestDelegate(b, "eest-scope", "Report downloaded EEST fixture scope and support status", "eest-scope", optimize_name, null, profile); - addEestDelegate(b, "eest-tx", "Run EEST raw transaction-test fixtures", "eest-tx", optimize_name, null, profile); - addEestDelegate(b, "zkevm", "Run EEST zkEVM stateless SSZ fixtures", "zkevm", optimize_name, null, profile); - addEestDelegate(b, "zkevm-input", "Extract one EEST zkEVM stateless input as ZisK stdin", "zkevm-input", optimize_name, null, profile); - addEestDelegate(b, "zkevm-ere", "Run raw ERE stateless input through native adapter", "zkevm-ere", optimize_name, null, profile); - addEestDelegate(b, "zkevm-ere-bench", "Emit ERE BenchmarkRun rows for zkEVM stateless fixtures", "zkevm-ere-bench", null, bench_optimize_name, profile); - addEestDelegate(b, "eest-block-stf", "Run regular EEST blockchain_tests through BlockSTF", "eest-block-stf", optimize_name, null, profile); - addEestDelegate(b, "eest-stateless-block-stf", "Run witness-backed zkEVM blockchain_tests through stateless BlockSTF", "eest-stateless-block-stf", optimize_name, null, profile); - addEestDelegate(b, "ssz-conformance", "Run consensus-spec generic SSZ fixtures", "ssz-conformance", optimize_name, null, profile); + addEestDelegate(b, "eest-test", "Run sidecar EEST runner tests", "test", optimize_name, null, evmz_build); + addEestDelegate(b, "eest", "Run EEST state-test fixtures", "eest", optimize_name, null, evmz_build); + addEestDelegate(b, "eest-classify", "Classify EEST state-test fixtures", "eest-classify", optimize_name, null, evmz_build); + addEestDelegate(b, "eest-scope", "Report downloaded EEST fixture scope and support status", "eest-scope", optimize_name, null, evmz_build); + addEestDelegate(b, "eest-tx", "Run EEST raw transaction-test fixtures", "eest-tx", optimize_name, null, evmz_build); + addEestDelegate(b, "zkevm", "Run EEST zkEVM stateless SSZ fixtures", "zkevm", optimize_name, null, evmz_build); + addEestDelegate(b, "zkevm-input", "Extract one EEST zkEVM stateless input as ZisK stdin", "zkevm-input", optimize_name, null, evmz_build); + addEestDelegate(b, "zkevm-ere", "Run raw ERE stateless input through native adapter", "zkevm-ere", optimize_name, null, evmz_build); + addEestDelegate(b, "zkevm-ere-bench", "Emit ERE BenchmarkRun rows for zkEVM stateless fixtures", "zkevm-ere-bench", null, bench_optimize_name, evmz_build); + addEestDelegate(b, "eest-block-stf", "Run regular EEST blockchain_tests through BlockSTF", "eest-block-stf", optimize_name, null, evmz_build); + addEestDelegate(b, "eest-stateless-block-stf", "Run witness-backed zkEVM blockchain_tests through stateless BlockSTF", "eest-stateless-block-stf", optimize_name, null, evmz_build); + addEestDelegate(b, "ssz-conformance", "Run consensus-spec generic SSZ fixtures", "ssz-conformance", optimize_name, null, evmz_build); } if (pathExists(b, "bench/build.zig")) { - addBenchDelegate(b, "bench-test", "Run benchmark sidecar tests", "test", null, profile); - addBenchVmLoopDelegate(b, bench_optimize_name, bench_support_min, bench_support_max, profile); - addBenchDelegate(b, "bench-evmone-vm-loop", "Run standalone evmone VM-loop fixture runner", "evmone-vm-loop", bench_optimize_name, profile); - addBenchDelegate(b, "bench-revm-vm-loop", "Run revm VM-loop fixture runner", "revm-vm-loop", null, profile); - addBenchCompareDelegate(b, bench_optimize_name, bench_support_min, bench_support_max, profile); - addBenchDelegate(b, "bench-block-lifecycle", "Run VM block lifecycle benchmark", "block-lifecycle", bench_optimize_name, profile); - addBenchDelegate(b, "bench-host-boundary", "Run host-boundary benchmark runner", "host-boundary", bench_optimize_name, profile); - addBenchDelegate(b, "bench-host-matrix", "Run host-boundary CSV matrix", "host-matrix", bench_optimize_name, profile); - addBenchDelegate(b, "bench-kernel", "Run pure opcode kernel benchmark", "kernel", bench_optimize_name, profile); - addBenchDelegate(b, "bench-code-analysis", "Run code-analysis morphology and timing report", "code-analysis", bench_optimize_name, profile); - addBenchDelegate(b, "bench-revm-kernel", "Run revm opcode kernel benchmark", "revm-kernel", null, profile); - addBenchDelegate(b, "bench-report", "Run all benchmark layers and write a comparison report", "report", bench_optimize_name, profile); - addBenchMicroDelegate(b, bench_optimize_name, bench_micro_filter, profile); + addBenchDelegate(b, "bench-test", "Run benchmark sidecar tests", "test", null, evmz_build); + addBenchVmLoopDelegate(b, bench_optimize_name, bench_support_min, bench_support_max, evmz_build); + addBenchDelegate(b, "bench-evmone-vm-loop", "Run standalone evmone VM-loop fixture runner", "evmone-vm-loop", bench_optimize_name, evmz_build); + addBenchDelegate(b, "bench-revm-vm-loop", "Run revm VM-loop fixture runner", "revm-vm-loop", null, evmz_build); + addBenchCompareDelegate(b, bench_optimize_name, bench_support_min, bench_support_max, evmz_build); + addBenchDelegate(b, "bench-block-lifecycle", "Run VM block lifecycle benchmark", "block-lifecycle", bench_optimize_name, evmz_build); + addBenchDelegate(b, "bench-host-boundary", "Run host-boundary benchmark runner", "host-boundary", bench_optimize_name, evmz_build); + addBenchDelegate(b, "bench-host-matrix", "Run host-boundary CSV matrix", "host-matrix", bench_optimize_name, evmz_build); + addBenchDelegate(b, "bench-kernel", "Run pure opcode kernel benchmark", "kernel", bench_optimize_name, evmz_build); + addBenchDelegate(b, "bench-code-analysis", "Run code-analysis morphology and timing report", "code-analysis", bench_optimize_name, evmz_build); + addBenchDelegate(b, "bench-revm-kernel", "Run revm opcode kernel benchmark", "revm-kernel", null, evmz_build); + addBenchDelegate(b, "bench-report", "Run all benchmark layers and write a comparison report", "report", bench_optimize_name, evmz_build); + addBenchMicroDelegate(b, bench_optimize_name, bench_micro_filter, evmz_build); } if (pathExists(b, "pkg/ssz/build.zig")) { addSszBenchDelegate(b, bench_optimize_name); @@ -252,6 +279,7 @@ pub fn build(b: *std.Build) void { if (native_precompile_deps) |deps| { addPrecompileNative(b, example_mod, deps, evmone_dep); } + addNativeKeccak(example_mod, xkcp_object); const example = b.addExecutable(.{ .name = example_exe_name, .root_module = b.createModule(.{ @@ -321,9 +349,34 @@ fn buildProfileOption(b: *std.Build) []const u8 { return profile; } -fn buildOptions(b: *std.Build, profile: []const u8) *std.Build.Step.Options { +fn nativeKeccakOption(b: *std.Build) []const u8 { + const backend = b.option( + []const u8, + "native-keccak", + "Native Keccak backend: std or xkcp (ignored by profile=zkvm)", + ) orelse "std"; + if (!std.mem.eql(u8, backend, "std") and !std.mem.eql(u8, backend, "xkcp")) { + std.debug.panic("unsupported native Keccak backend '{s}' (expected std or xkcp)", .{backend}); + } + return backend; +} + +fn resolveNativeKeccak( + profile: []const u8, + target: std.Build.ResolvedTarget, + requested: []const u8, +) []const u8 { + if (!std.mem.eql(u8, profile, "native") or !std.mem.eql(u8, requested, "xkcp")) return "std"; + return switch (target.result.cpu.arch) { + .x86_64, .aarch64, .riscv64 => "xkcp", + else => "std", + }; +} + +fn buildOptions(b: *std.Build, profile: []const u8, native_keccak: []const u8) *std.Build.Step.Options { const options = b.addOptions(); options.addOption([]const u8, "profile", profile); + options.addOption([]const u8, "native_keccak", native_keccak); return options; } @@ -525,7 +578,7 @@ fn addGuestZisk( }; const target = guestZiskTarget(b); - const build_options = buildOptions(b, "zkvm"); + const build_options = buildOptions(b, "zkvm", "std"); const guest_options = guestOptions(b, true); const guest_options_mod = guest_options.createModule(); const guest_payload_source = guestPayloadSource(guest_payload) catch unreachable; @@ -680,7 +733,7 @@ fn addEestDelegate( child_step: []const u8, optimize_name: ?[]const u8, bench_optimize_name: ?[]const u8, - profile: []const u8, + config: EvmzBuildConfig, ) void { const run = b.addSystemCommand(&.{ b.graph.zig_exe, @@ -692,7 +745,7 @@ fn addEestDelegate( if (bench_optimize_name) |name| { run.addArg(b.fmt("-Dbench-optimize={s}", .{name})); } - run.addArg(b.fmt("-Dprofile={s}", .{profile})); + addEvmzBuildArgs(run, b, config); run.addArg(child_step); if (b.args) |args| { run.addArg("--"); @@ -710,7 +763,7 @@ fn addBenchDelegate( description: []const u8, child_step: []const u8, optimize_name: ?[]const u8, - profile: []const u8, + config: EvmzBuildConfig, ) void { const run = b.addSystemCommand(&.{ b.graph.zig_exe, @@ -719,7 +772,7 @@ fn addBenchDelegate( if (optimize_name) |name| { run.addArg(b.fmt("-Doptimize={s}", .{name})); } - run.addArg(b.fmt("-Dprofile={s}", .{profile})); + addEvmzBuildArgs(run, b, config); run.addArg(child_step); if (b.args) |args| { run.addArg("--"); @@ -753,14 +806,14 @@ fn addBenchCompareDelegate( optimize_name: []const u8, support_min: ?[]const u8, support_max: ?[]const u8, - profile: []const u8, + config: EvmzBuildConfig, ) void { const run = b.addSystemCommand(&.{ b.graph.zig_exe, "build", }); run.addArg(b.fmt("-Doptimize={s}", .{optimize_name})); - run.addArg(b.fmt("-Dprofile={s}", .{profile})); + addEvmzBuildArgs(run, b, config); if (support_min) |revision| { run.addArg(b.fmt("-Dbench-support-min={s}", .{revision})); } @@ -783,14 +836,14 @@ fn addBenchVmLoopDelegate( optimize_name: []const u8, support_min: ?[]const u8, support_max: ?[]const u8, - profile: []const u8, + config: EvmzBuildConfig, ) void { const run = b.addSystemCommand(&.{ b.graph.zig_exe, "build", }); run.addArg(b.fmt("-Doptimize={s}", .{optimize_name})); - run.addArg(b.fmt("-Dprofile={s}", .{profile})); + addEvmzBuildArgs(run, b, config); if (support_min) |revision| { run.addArg(b.fmt("-Dbench-support-min={s}", .{revision})); } @@ -812,14 +865,14 @@ fn addBenchMicroDelegate( b: *std.Build, optimize_name: []const u8, micro_filter: ?[]const u8, - profile: []const u8, + config: EvmzBuildConfig, ) void { const run = b.addSystemCommand(&.{ b.graph.zig_exe, "build", }); run.addArg(b.fmt("-Doptimize={s}", .{optimize_name})); - run.addArg(b.fmt("-Dprofile={s}", .{profile})); + addEvmzBuildArgs(run, b, config); if (micro_filter) |filter| { run.addArg(b.fmt("-Dmicro-filter={s}", .{filter})); } @@ -830,6 +883,137 @@ fn addBenchMicroDelegate( step.dependOn(&run.step); } +fn addEvmzBuildArgs(run: *std.Build.Step.Run, b: *std.Build, config: EvmzBuildConfig) void { + run.addArg(b.fmt("-Dprofile={s}", .{config.profile})); + run.addArg(b.fmt("-Dnative-keccak={s}", .{config.native_keccak})); +} + +const XkcpLane = enum { + x86_64_dispatch, + aarch64_dispatch, + generic64, +}; + +fn buildXkcpObject( + b: *std.Build, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + dep: *std.Build.Dependency, + name: []const u8, + pic: ?bool, +) *std.Build.Step.Compile { + const lane: XkcpLane = switch (target.result.cpu.arch) { + .x86_64 => if (target.result.os.tag == .windows) .generic64 else .x86_64_dispatch, + .aarch64 => switch (target.result.os.tag) { + .linux, .macos => .aarch64_dispatch, + else => .generic64, + }, + .riscv64 => .generic64, + else => unreachable, + }; + + const config = switch (lane) { + .x86_64_dispatch => + \\#define XKCP_has_KeccakP1600 + \\#define XKCP_has_x86_64_CPU_detection + \\ + , + .aarch64_dispatch => + \\#define XKCP_has_KeccakP1600 + \\#define XKCP_has_aarch64_CPU_detection + \\ + , + .generic64 => + \\#define XKCP_has_KeccakP1600 + \\ + , + }; + const generated = b.addWriteFiles(); + const config_header = generated.add("xkcp/config.h", config); + const module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + .pic = pic, + }); + const c_flags = &[_][]const u8{ + "-std=c11", + "-Wall", + "-Wextra", + }; + + module.addIncludePath(config_header.dirname()); + module.addIncludePath(dep.path("lib/common")); + module.addIncludePath(dep.path("lib/high/Keccak")); + module.addIncludePath(dep.path("lib/low/common")); + module.addIncludePath(dep.path("lib/low/KeccakP-1600/common")); + module.addIncludePath(dep.path("lib/low/KeccakP-1600/plain-64bits")); + module.addCSourceFile(.{ + .file = dep.path("lib/high/Keccak/KeccakSponge.c"), + .flags = c_flags, + }); + module.addCSourceFile(.{ + .file = b.path("src/crypto/xkcp_keccak.c"), + .flags = c_flags, + }); + module.addCSourceFile(.{ + .file = dep.path("lib/low/KeccakP-1600/plain-64bits/KeccakP-1600-opt64.c"), + .flags = c_flags, + }); + + switch (lane) { + .x86_64_dispatch => { + module.addIncludePath(dep.path("lib/low/x86-64-dispatch")); + module.addIncludePath(dep.path("lib/low/KeccakP-1600/AVX2")); + module.addIncludePath(dep.path("lib/low/KeccakP-1600/AVX512")); + module.addCSourceFile(.{ + .file = dep.path("lib/low/x86-64-dispatch/x86-64-dispatch.c"), + .flags = c_flags, + }); + const asm_flags: []const []const u8 = if (target.result.os.tag == .macos) + &.{"-Wa,-defsym,old_gas_syntax=1"} + else + &.{}; + module.addCSourceFile(.{ + .file = dep.path("lib/low/KeccakP-1600/AVX2/KeccakP-1600-AVX2.s"), + .flags = asm_flags, + }); + module.addCSourceFile(.{ + .file = dep.path("lib/low/KeccakP-1600/AVX512/KeccakP-1600-AVX512.s"), + .flags = asm_flags, + }); + }, + .aarch64_dispatch => { + module.addIncludePath(dep.path("lib/low/aarch64-dispatch")); + module.addIncludePath(dep.path("lib/low/KeccakP-1600/ARMv8A-SHA3")); + module.addCSourceFile(.{ + .file = dep.path("lib/low/aarch64-dispatch/aarch64-dispatch.c"), + .flags = c_flags, + }); + module.addCSourceFile(.{ + .file = dep.path("lib/low/KeccakP-1600/ARMv8A-SHA3/KeccakP-1600-x1-v84a.c"), + .flags = c_flags, + }); + module.addCSourceFile(.{ + .file = b.path("src/crypto/xkcp_aarch64.S"), + .flags = &.{ + "-D__ARM_FEATURE_SHA3=1", + "-Wa,-march=armv8.4-a+sha3", + }, + }); + }, + .generic64 => module.addIncludePath(dep.path("lib/low/KeccakP-1600/plain-64bits/SnP")), + } + + return b.addObject(.{ .name = name, .root_module = module }); +} + +fn addNativeKeccak(module: *std.Build.Module, xkcp_object: ?*std.Build.Step.Compile) void { + const object = xkcp_object orelse return; + module.link_libc = true; + module.addObject(object); +} + fn addPrecompileNative( b: *std.Build, module: *std.Build.Module, diff --git a/build.zig.zon b/build.zig.zon index 99ff772..77810a4 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -20,6 +20,11 @@ .url = "https://github.com/herumi/mcl/archive/refs/tags/v3.06.tar.gz", .hash = "N-V-__8AAIbQPgBN3AkA58o43UT3Y6jkn55OesBXTWc63zEF", }, + .xkcp = .{ + .url = "git+https://github.com/XKCP/XKCP.git#78477d2e0b980737deaa07b928b29302257055ca", + .hash = "N-V-__8AAGMY2QG5Ku-5NI36wI2cqcZmaHbh-im2Pj0r0bb_", + .lazy = true, + }, }, .paths = .{ "build.zig", diff --git a/eest/build.zig b/eest/build.zig index f7cd18d..cb68bf3 100644 --- a/eest/build.zig +++ b/eest/build.zig @@ -9,11 +9,13 @@ pub fn build(b: *std.Build) void { "Optimization mode for EEST benchmark-style runners", ) orelse .ReleaseFast; const profile = buildProfileOption(b); + const native_keccak = nativeKeccakOption(b, profile); const evmz_dep = b.dependency("evmz", .{ .target = target, .optimize = optimize, .profile = profile, + .@"native-keccak" = native_keccak, }); const evmz_mod = evmz_dep.module("evmz"); const ssz_mod = evmz_dep.module("ssz"); @@ -26,6 +28,7 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = bench_optimize, .profile = profile, + .@"native-keccak" = native_keccak, }); const bench_evmz_mod = bench_evmz_dep.module("evmz"); @@ -240,6 +243,14 @@ fn buildProfileOption(b: *std.Build) []const u8 { return profile; } +fn nativeKeccakOption(b: *std.Build, profile: []const u8) []const u8 { + const backend = b.option([]const u8, "native-keccak", "Native Keccak backend: std or xkcp") orelse "std"; + if (!std.mem.eql(u8, backend, "std") and !std.mem.eql(u8, backend, "xkcp")) { + std.debug.panic("unsupported native Keccak backend '{s}' (expected std or xkcp)", .{backend}); + } + return if (std.mem.eql(u8, profile, "native")) backend else "std"; +} + fn eestModule( b: *std.Build, root: []const u8, diff --git a/src/crypto.zig b/src/crypto.zig index 769a842..f2afaa8 100644 --- a/src/crypto.zig +++ b/src/crypto.zig @@ -1,13 +1,18 @@ //! Hashing and signature primitives (keccak256, ecrecover). //! //! The backing provider is selected at build time by the crypto profile -//! (`native` or `zkvm`), so callers get the same API regardless of target. +//! (`native` or `zkvm`). Native builds can select the `std` or `xkcp` Keccak +//! backend while zkVM builds stay on their custom accelerator provider. const std = @import("std"); const build_options = @import("build_options"); const zkvm = @import("./crypto/zkvm_accelerators.zig"); pub const provider_name = build_options.profile; +pub const keccak_provider_name = if (std.mem.eql(u8, provider_name, "native")) + build_options.native_keccak +else + "zkvm"; /// Keccak-256 digest of the empty byte string. pub const keccak256_empty = [_]u8{ @@ -24,6 +29,13 @@ else if (std.mem.eql(u8, provider_name, "zkvm")) else @compileError("unsupported profile '" ++ provider_name ++ "'"); +const NativeKeccakProvider = if (std.mem.eql(u8, build_options.native_keccak, "std")) + StdKeccakProvider +else if (std.mem.eql(u8, build_options.native_keccak, "xkcp")) + XkcpKeccakProvider +else + @compileError("unsupported native Keccak backend '" ++ build_options.native_keccak ++ "'"); + pub fn keccak256(input: []const u8) [32]u8 { var digest: [32]u8 = undefined; Provider.keccak256(input, &digest); @@ -47,7 +59,7 @@ pub fn ecrecoverPublicKey( const NativeProvider = struct { fn keccak256(input: []const u8, out: *[32]u8) void { - std.crypto.hash.sha3.Keccak256.hash(input, out, .{}); + NativeKeccakProvider.keccak256(input, out); } fn sha256(input: []const u8, out: *[32]u8) void { @@ -84,6 +96,25 @@ const NativeProvider = struct { } }; +const StdKeccakProvider = struct { + fn keccak256(input: []const u8, out: *[32]u8) void { + std.crypto.hash.sha3.Keccak256.hash(input, out, .{}); + } +}; + +const XkcpKeccakProvider = struct { + extern fn evmz_xkcp_keccak256( + input: [*]const u8, + input_len: usize, + output: [*]u8, + ) c_int; + + fn keccak256(input: []const u8, out: *[32]u8) void { + const rc = evmz_xkcp_keccak256(input.ptr, input.len, out); + if (rc != 0) unreachable; + } +}; + const ZkvmProvider = struct { fn keccak256(input: []const u8, out: *[32]u8) void { var digest: zkvm.Keccak256Hash = undefined; @@ -117,6 +148,19 @@ test keccak256 { try std.testing.expectEqualSlices(u8, &keccak256_empty, &keccak256("")); } +test "native Keccak backend matches std across rate boundaries" { + if (!std.mem.eql(u8, provider_name, "native")) return; + + var storage: [1025]u8 align(8) = undefined; + const input = storage[1..]; + for (input, 0..) |*byte, i| byte.* = @truncate(i *% 17 +% 3); + inline for (.{ 0, 1, 32, 135, 136, 137, 1024 }) |len| { + var expected: [32]u8 = undefined; + std.crypto.hash.sha3.Keccak256.hash(input[0..len], &expected, .{}); + try std.testing.expectEqualSlices(u8, &expected, &keccak256(input[0..len])); + } +} + test sha256 { try std.testing.expectEqualSlices(u8, &[_]u8{ 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, diff --git a/src/crypto/xkcp_aarch64.S b/src/crypto/xkcp_aarch64.S new file mode 100644 index 0000000..1bc0024 --- /dev/null +++ b/src/crypto/xkcp_aarch64.S @@ -0,0 +1,3 @@ +/* XKCP ships this preprocessed assembly with a lowercase .s suffix. Include it + through .S so Clang expands its platform and symbol macros cross-platform. */ +#include "KeccakP-1600-x1-v84a-asm.s" diff --git a/src/crypto/xkcp_keccak.c b/src/crypto/xkcp_keccak.c new file mode 100644 index 0000000..db0c177 --- /dev/null +++ b/src/crypto/xkcp_keccak.c @@ -0,0 +1,47 @@ +#include +#include + +#include "KeccakSponge.h" + +enum { + KECCAK256_RATE_BITS = 1088, + KECCAK256_CAPACITY_BITS = 512, + KECCAK256_RATE_BYTES = KECCAK256_RATE_BITS / 8, + KECCAK256_DIGEST_BYTES = 32, +}; + +int evmz_xkcp_keccak256(const unsigned char *input, size_t input_len, + unsigned char output[KECCAK256_DIGEST_BYTES]) { + if (((uintptr_t)input % sizeof(uint64_t)) == 0 || + input_len < KECCAK256_RATE_BYTES) { + return KeccakWidth1600_Sponge( + KECCAK256_RATE_BITS, KECCAK256_CAPACITY_BITS, input, input_len, + 0x01, output, KECCAK256_DIGEST_BYTES); + } + + /* XKCP fast absorb lanes read uint64_t values. Preserve that fast path for + arbitrary byte slices by copying only full, misaligned rate blocks. */ + KeccakWidth1600_SpongeInstance instance; + ALIGN(8) unsigned char block[KECCAK256_RATE_BYTES]; + int rc = KeccakWidth1600_SpongeInitialize( + &instance, KECCAK256_RATE_BITS, KECCAK256_CAPACITY_BITS); + if (rc != 0) + return rc; + + while (input_len >= KECCAK256_RATE_BYTES) { + memcpy(block, input, sizeof(block)); + rc = KeccakWidth1600_SpongeAbsorb(&instance, block, sizeof(block)); + if (rc != 0) + return rc; + input += sizeof(block); + input_len -= sizeof(block); + } + rc = KeccakWidth1600_SpongeAbsorb(&instance, input, input_len); + if (rc != 0) + return rc; + rc = KeccakWidth1600_SpongeAbsorbLastFewBits(&instance, 0x01); + if (rc != 0) + return rc; + return KeccakWidth1600_SpongeSqueeze( + &instance, output, KECCAK256_DIGEST_BYTES); +}