From e4bacdc26c9587e582a659ab02d6176285ad2bbe Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Tue, 26 Aug 2025 20:34:40 +0800 Subject: [PATCH 01/14] feat:init enr load from file and save to file Signed-off-by: Chen Kai <281165273grape@gmail.com> --- .github/workflows/ci.yml | 54 +++++ build.zig | 72 +++++++ build.zig.zon | 52 +++++ src/root.zig | 451 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 629 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/root.zig diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..46de4f7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: Enr CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +env: + ZIG_VERSION: 0.14.1 + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Cache Zig + id: cache-zig + uses: actions/cache@v4 + with: + path: ~/zig + key: ${{ runner.os }}-zig-${{ env.ZIG_VERSION }} + + - name: Install Zig + if: steps.cache-zig.outputs.cache-hit != 'true' + run: | + wget https://ziglang.org/download/${{ env.ZIG_VERSION }}/zig-x86_64-linux-${{ env.ZIG_VERSION }}.tar.xz + tar -xf zig-x86_64-linux-${{ env.ZIG_VERSION }}.tar.xz + mv zig-x86_64-linux-${{ env.ZIG_VERSION }} ~/zig + + - name: Add Zig to PATH + run: echo "${HOME}/zig" >> $GITHUB_PATH + + - name: Cache Zig build artifacts + uses: actions/cache@v4 # Updated to v4 + with: + path: | + zig-cache + ~/.cache/zig + key: ${{ runner.os }}-zig-build-${{ hashFiles('**/*.zig') }} + restore-keys: | + ${{ runner.os }}-zig-build- + + - name: Formatting + run: zig fmt --check --color on . + + - name: Unit testing + run: zig build test --summary all + + - name: Building + run: zig build -Doptimize=ReleaseSafe \ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..491a2e1 --- /dev/null +++ b/build.zig @@ -0,0 +1,72 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // 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 discv5 = b.dependency("discv5", .{}); + + const enr_mod = discv5.module("enr"); + + // This creates a "module", which represents a collection of source files alongside + // some compilation options, such as optimization mode and linked system libraries. + // Every executable or library we compile will be based on one or more modules. + const lib_mod = b.createModule(.{ + // `root_source_file` is the Zig "entry point" of the module. If a module + // only contains e.g. external object files, you can make this `null`. + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + lib_mod.addImport("enr", enr_mod); + + // Now, we will create a static library based on the module we created above. + // This creates a `std.Build.Step.Compile`, which is the build step responsible + // for actually invoking the compiler. + const lib = b.addLibrary(.{ + .linkage = .static, + .name = "enr", + .root_module = lib_mod, + }); + + // This declares intent for the library to be installed into the standard + // location when the user invokes the "install" step (the default step when + // running `zig build`). + b.installArtifact(lib); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const lib_unit_tests = b.addTest(.{ + .root_module = lib_mod, + }); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_lib_unit_tests.step); + + const install_docs = b.addInstallDirectory(.{ + .source_dir = lib.getEmittedDocs(), + .install_dir = .prefix, + .install_subdir = "docs", + }); + const docs_step = b.step("docs", "Generate documentation"); + docs_step.dependOn(&install_docs.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..eec7cf6 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,52 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = .zig_enr, + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // Together with name, this represents a globally unique package + // identifier. This field is generated by the Zig toolchain when the + // package is first created, and then *never changes*. This allows + // unambiguous detection of one package being an updated version of + // another. + // + // When forking a Zig project, this id should be regenerated (delete the + // field and run `zig build`) if the upstream project is still maintained. + // Otherwise, the fork is *hostile*, attempting to take control over the + // original project's identity. Thus it is recommended to leave the comment + // on the following line intact, so that it shows up in code reviews that + // modify the field. + .fingerprint = 0x499ae5d25c594f6a, // Changing this has security and trust implications. + + // Tracks the earliest Zig version that the package considers to be a + // supported use case. + .minimum_zig_version = "0.14.1", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + .discv5 = .{ + .url = "git+https://github.com/blockblaz/zig-discv5#bc96ea8e6a8c3ddc5880256a11e19df3afe37870", + .hash = "discv5-0.1.0-fkJduBNjAQCUYP7JVBtZ6A5Y79UYpyC8JHz5kzGnB-K5", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..a5c5909 --- /dev/null +++ b/src/root.zig @@ -0,0 +1,451 @@ +const std = @import("std"); +const enrlib = @import("enr"); + +pub const ENR = enrlib.ENR; +pub const EncodedENR = enrlib.EncodedENR; +pub const SignableENR = enrlib.SignableENR; + +/// ENR file I/O error set +pub const EnrFileError = error{ + /// The ENR file is too large to process + EnrFileTooLarge, +}; + +/// Loads ENR from the given file path +pub fn loadEnrFromDisk(enr: *ENR, file_path: []const u8) !void { + const enr_file = try std.fs.cwd().openFile(file_path, .{}); + defer enr_file.close(); + + const file_size = try enr_file.getEndPos(); + if (file_size > enrlib.max_enr_size) return EnrFileError.EnrFileTooLarge; + + var enr_txt: [enrlib.max_enr_size]u8 = undefined; + _ = try enr_file.readAll(enr_txt[0..file_size]); + + try ENR.decodeTxtInto(enr, enr_txt[0..file_size]); +} + +/// Loads EncodedENR from the given file path +pub fn loadEncodedEnrFromDisk(encoded_enr: *EncodedENR, file_path: []const u8) !void { + const enr_file = try std.fs.cwd().openFile(file_path, .{}); + defer enr_file.close(); + + const file_size = try enr_file.getEndPos(); + if (file_size > enrlib.max_enr_size) return EnrFileError.EnrFileTooLarge; + + var temp_txt: [enrlib.max_enr_size]u8 = undefined; + _ = try enr_file.readAll(temp_txt[0..file_size]); + + encoded_enr.* = try EncodedENR.decodeTxtInto(temp_txt[0..file_size]); +} + +/// Saves an ENR to disk with given file path +pub fn saveEnrToDisk(file_path: []const u8, enr: *ENR) !void { + if (std.fs.path.dirname(file_path)) |dir_path| { + try std.fs.cwd().makePath(dir_path); + } + + const file = try std.fs.cwd().createFile(file_path, .{}); + defer file.close(); + + var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; + const out = try enr.encodeToTxt(&txt_buffer); + + try file.writeAll(out); +} + +/// Saves a SignableENR to disk with given file path +pub fn saveSignableEnrToDisk(file_path: []const u8, signable_enr: *SignableENR) !void { + if (std.fs.path.dirname(file_path)) |dir_path| { + try std.fs.cwd().makePath(dir_path); + } + + const file = try std.fs.cwd().createFile(file_path, .{}); + defer file.close(); + + var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; + const out = try signable_enr.encodeToTxt(&txt_buffer); + + try file.writeAll(out); +} + +/// Loads multiple ENRs from a single file +pub fn loadMultipleEnrsFromDisk(enr_list: *std.ArrayList(ENR), file_path: []const u8, delimiter: []const u8) !void { + const enr_file = try std.fs.cwd().openFile(file_path, .{}); + defer enr_file.close(); + + const file_size = try enr_file.getEndPos(); + const file_content = try enr_list.allocator.alloc(u8, file_size); + defer enr_list.allocator.free(file_content); + + _ = try enr_file.readAll(file_content); + + var iterator = std.mem.splitSequence(u8, file_content, delimiter); + + while (iterator.next()) |enr_txt| { + const trimmed = std.mem.trim(u8, enr_txt, " \t\r\n"); + if (trimmed.len == 0) continue; // Skip empty entries + + var enr: ENR = undefined; + try ENR.decodeTxtInto(&enr, trimmed); + + try enr_list.append(enr); + } +} + +/// Loads multiple EncodedENRs from a single file +pub fn loadMultipleEncodedEnrsFromDisk(enr_list: *std.ArrayList(EncodedENR), file_path: []const u8, delimiter: []const u8) !void { + const enr_file = try std.fs.cwd().openFile(file_path, .{}); + defer enr_file.close(); + + const file_size = try enr_file.getEndPos(); + const file_content = try enr_list.allocator.alloc(u8, file_size); + defer enr_list.allocator.free(file_content); + + _ = try enr_file.readAll(file_content); + + var iterator = std.mem.splitSequence(u8, file_content, delimiter); + + while (iterator.next()) |enr_txt| { + const trimmed = std.mem.trim(u8, enr_txt, " \t\r\n"); + if (trimmed.len == 0) continue; // Skip empty entries + + const encoded_enr = try EncodedENR.decodeTxtInto(trimmed); + + try enr_list.append(encoded_enr); + } +} + +/// Saves multiple ENRs to a single file +pub fn saveMultipleEnrsToDisk(file_path: []const u8, enrs: []ENR, delimiter: []const u8) !void { + if (std.fs.path.dirname(file_path)) |dir_path| { + try std.fs.cwd().makePath(dir_path); + } + + const file = try std.fs.cwd().createFile(file_path, .{}); + defer file.close(); + + for (enrs, 0..) |*enr, i| { + var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; + const out = try enr.encodeToTxt(&txt_buffer); + try file.writeAll(out); + + if (i < enrs.len - 1) { + try file.writeAll(delimiter); + } + } +} + +/// Saves multiple SignableENRs to a single file +pub fn saveMultipleSignableEnrsToDisk(file_path: []const u8, signable_enrs: []SignableENR, delimiter: []const u8) !void { + if (std.fs.path.dirname(file_path)) |dir_path| { + try std.fs.cwd().makePath(dir_path); + } + + const file = try std.fs.cwd().createFile(file_path, .{}); + defer file.close(); + + for (signable_enrs, 0..) |*signable_enr, i| { + var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; + const out = try signable_enr.encodeToTxt(&txt_buffer); + try file.writeAll(out); + + if (i < signable_enrs.len - 1) { + try file.writeAll(delimiter); + } + } +} + +const testing = std.testing; + +// Test ENR vectors from various Ethereum clients +const test_enrs = [_][]const u8{ + // Teku team's bootnode + "enr:-KG4QMOEswP62yzDjSwWS4YEjtTZ5PO6r65CPqYBkgTTkrpaedQ8uEUo1uMALtJIvb2w_WWEVmg5yt1UAuK1ftxUU7QDhGV0aDKQu6TalgMAAAD__________4JpZIJ2NIJpcIQEnfA2iXNlY3AyNTZrMaEDfol8oLr6XJ7FsdAYE7lpJhKMls4G_v6qQOGKJUWGb_uDdGNwgiMog3VkcIIjKA", + "enr:-KG4QF4B5WrlFcRhUU6dZETwY5ZzAXnA0vGC__L1Kdw602nDZwXSTs5RFXFIFUnbQJmhNGVU6OIX7KVrCSTODsz1tK4DhGV0aDKQu6TalgMAAAD__________4JpZIJ2NIJpcIQExNYEiXNlY3AyNTZrMaECQmM9vp7KhaXhI-nqL_R0ovULLCFSFTa9CPPSdb1zPX6DdGNwgiMog3VkcIIjKA", + + // Prysm team's bootnodes + "enr:-Ku4QImhMc1z8yCiNJ1TyUxdcfNucje3BGwEHzodEZUan8PherEo4sF7pPHPSIB1NNuSg5fZy7qFsjmUKs2ea1Whi0EBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQOVphkDqal4QzPMksc5wnpuC3gvSC8AfbFOnZY_On34wIN1ZHCCIyg", + "enr:-Ku4QP2xDnEtUXIjzJ_DhlCRN9SN99RYQPJL92TMlSv7U5C1YnYLjwOQHgZIUXw6c-BvRg2Yc2QsZxxoS_pPRVe0yK8Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMeFF5GrS7UZpAH2Ly84aLK-TyvH-dRo0JM1i8yygH50YN1ZHCCJxA", + "enr:-Ku4QPp9z1W4tAO8Ber_NQierYaOStqhDqQdOPY3bB3jDgkjcbk6YrEnVYIiCBbTxuar3CzS528d2iE7TdJsrL-dEKoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMw5fqqkw2hHC4F5HZZDPsNmPdB1Gi8JPQK7pRc9XHh-oN1ZHCCKvg", + + // Lighthouse team's bootnodes + "enr:-Le4QPUXJS2BTORXxyx2Ia-9ae4YqA_JWX3ssj4E_J-3z1A-HmFGrU8BpvpqhNabayXeOZ2Nq_sbeDgtzMJpLLnXFgAChGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISsaa0Zg2lwNpAkAIkHAAAAAPA8kv_-awoTiXNlY3AyNTZrMaEDHAD2JKYevx89W0CcFJFiskdcEzkH_Wdv9iW42qLK79ODdWRwgiMohHVkcDaCI4I", + "enr:-Le4QLHZDSvkLfqgEo8IWGG96h6mxwe_PsggC20CL3neLBjfXLGAQFOPSltZ7oP6ol54OvaNqO02Rnvb8YmDR274uq8ChGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLosQxg2lwNpAqAX4AAAAAAPA8kv_-ax65iXNlY3AyNTZrMaEDBJj7_dLFACaxBfaI8KZTh_SSJUjhyAyfshimvSqo22WDdWRwgiMohHVkcDaCI4I", + "enr:-Le4QH6LQrusDbAHPjU_HcKOuMeXfdEB5NJyXgHWFadfHgiySqeDyusQMvfphdYWOzuSZO9Uq2AMRJR5O4ip7OvVma8BhGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLY9ncg2lwNpAkAh8AgQIBAAAAAAAAAAmXiXNlY3AyNTZrMaECDYCZTZEksF-kmgPholqgVt8IXr-8L7Nu7YrZ7HUpgxmDdWRwgiMohHVkcDaCI4I", + "enr:-Le4QIqLuWybHNONr933Lk0dcMmAB5WgvGKRyDihy1wHDIVlNuuztX62W51voT4I8qD34GcTEOTmag1bcdZ_8aaT4NUBhGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLY04ng2lwNpAkAh8AgAIBAAAAAAAAAA-fiXNlY3AyNTZrMaEDscnRV6n1m-D9ID5UsURk0jsoKNXt1TIrj8uKOGW6iluDdWRwgiMohHVkcDaCI4I", + + // EF bootnodes + "enr:-Ku4QHqVeJ8PPICcWk1vSn_XcSkjOkNiTg6Fmii5j6vUQgvzMc9L1goFnLKgXqBJspJjIsB91LTOleFmyWWrFVATGngBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAMRHkWJc2VjcDI1NmsxoQKLVXFOhp2uX6jeT0DvvDpPcU8FWMjQdR4wMuORMhpX24N1ZHCCIyg", + "enr:-Ku4QG-2_Md3sZIAUebGYT6g0SMskIml77l6yR-M_JXc-UdNHCmHQeOiMLbylPejyJsdAPsTHJyjJB2sYGDLe0dn8uYBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhBLY-NyJc2VjcDI1NmsxoQORcM6e19T1T9gi7jxEZjk_sjVLGFscUNqAY9obgZaxbIN1ZHCCIyg", + "enr:-Ku4QPn5eVhcoF1opaFEvg1b6JNFD2rqVkHQ8HApOKK61OIcIXD127bKWgAtbwI7pnxx6cDyk_nI88TrZKQaGMZj0q0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDayLMaJc2VjcDI1NmsxoQK2sBOLGcUb4AwuYzFuAVCaNHA-dy24UuEKkeFNgCVCsIN1ZHCCIyg", + "enr:-Ku4QEWzdnVtXc2Q0ZVigfCGggOVB2Vc1ZCPEc6j21NIFLODSJbvNaef1g4PxhPwl_3kax86YPheFUSLXPRs98vvYsoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDZBrP2Jc2VjcDI1NmsxoQM6jr8Rb1ktLEsVcKAPa08wCsKUmvoQ8khiOl_SLozf9IN1ZHCCIyg", + + // Nimbus team's bootnodes + "enr:-LK4QA8FfhaAjlb_BXsXxSfiysR7R52Nhi9JBt4F8SPssu8hdE1BXQQEtVDC3qStCW60LSO7hEsVHv5zm8_6Vnjhcn0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAN4aBKJc2VjcDI1NmsxoQJerDhsJ-KxZ8sHySMOCmTO6sHM3iCFQ6VMvLTe948MyYN0Y3CCI4yDdWRwgiOM", + "enr:-LK4QKWrXTpV9T78hNG6s8AM6IO4XH9kFT91uZtFg1GcsJ6dKovDOr1jtAAFPnS2lvNltkOGA9k29BUN7lFh_sjuc9QBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhANAdd-Jc2VjcDI1NmsxoQLQa6ai7y9PMN5hpLe5HmiJSlYzMuzP7ZhwRiwHvqNXdoN0Y3CCI4yDdWRwgiOM", +}; + +test "single ENR file operations" { + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + var enr: ENR = undefined; + try ENR.decodeTxtInto(&enr, test_enrs[0]); + + try std.testing.expectEqual(3, enr.seq); + var ip_buffer: [16]u8 = undefined; + try std.testing.expectEqualStrings("4.157.240.54", (try enr.getIpStr(&ip_buffer)).?); + var sig_buffer: [130]u8 = undefined; + try std.testing.expectEqualStrings("0xc384b303fadb2cc38d2c164b86048ed4d9e4f3baafae423ea6019204d392ba5a79d43cb84528d6e3002ed248bdbdb0fd6584566839cadd5402e2b57edc5453b4", try enr.getSignatureStr(&sig_buffer, .lower)); + var pubkey_buffer: [68]u8 = undefined; + try std.testing.expectEqualStrings("0x037e897ca0bafa5c9ec5b1d01813b96926128c96ce06fefeaa40e18a2545866ffb", try enr.getPublicKeyStr(&pubkey_buffer, .lower)); + try std.testing.expectEqual(@as(u16, 9000), (try enr.getUdp()).?); + + const test_file = "test_single_enr.txt"; + + const tmp_file = try tmp_dir.dir.createFile(test_file, .{}); + var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; + const out = try enr.encodeToTxt(&txt_buffer); + try tmp_file.writeAll(out); + tmp_file.close(); + + var loaded_enr: ENR = undefined; + const read_file = try tmp_dir.dir.openFile(test_file, .{}); + defer read_file.close(); + + const file_size = try read_file.getEndPos(); + var enr_txt: [enrlib.max_enr_size]u8 = undefined; + _ = try read_file.readAll(enr_txt[0..file_size]); + try ENR.decodeTxtInto(&loaded_enr, enr_txt[0..file_size]); + + var original_encoded: [1024]u8 = undefined; + var loaded_encoded: [1024]u8 = undefined; + + try enr.encodeInto(&original_encoded); + try loaded_enr.encodeInto(&loaded_encoded); + + const original_len = enr.encodedLen(); + const loaded_len = loaded_enr.encodedLen(); + + try testing.expectEqual(original_len, loaded_len); + try testing.expectEqualSlices(u8, original_encoded[0..original_len], loaded_encoded[0..loaded_len]); +} + +test "multiple ENRs with newline delimiter" { + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const allocator = testing.allocator; + var enr_list = std.ArrayList(ENR).init(allocator); + defer enr_list.deinit(); + + for (test_enrs[0..5]) |enr_txt| { + var enr: ENR = undefined; + try ENR.decodeTxtInto(&enr, enr_txt); + try enr_list.append(enr); + } + + const test_file = "test_multiple_enrs_newline.txt"; + + const tmp_file = try tmp_dir.dir.createFile(test_file, .{}); + for (enr_list.items, 0..) |*enr, i| { + var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; + const out = try enr.encodeToTxt(&txt_buffer); + try tmp_file.writeAll(out); + + if (i < enr_list.items.len - 1) { + try tmp_file.writeAll("\n"); + } + } + tmp_file.close(); + + var loaded_enr_list = std.ArrayList(ENR).init(allocator); + defer loaded_enr_list.deinit(); + + const read_file = try tmp_dir.dir.openFile(test_file, .{}); + defer read_file.close(); + + const file_size = try read_file.getEndPos(); + const file_content = try allocator.alloc(u8, file_size); + defer allocator.free(file_content); + + _ = try read_file.readAll(file_content); + + var iterator = std.mem.splitSequence(u8, file_content, "\n"); + + while (iterator.next()) |enr_txt| { + const trimmed = std.mem.trim(u8, enr_txt, " \t\r\n"); + if (trimmed.len == 0) continue; + + var enr: ENR = undefined; + try ENR.decodeTxtInto(&enr, trimmed); + try loaded_enr_list.append(enr); + } + + try testing.expectEqual(enr_list.items.len, loaded_enr_list.items.len); + + for (enr_list.items, loaded_enr_list.items) |*original, *loaded| { + var original_encoded: [1024]u8 = undefined; + var loaded_encoded: [1024]u8 = undefined; + + try original.encodeInto(&original_encoded); + try loaded.encodeInto(&loaded_encoded); + + const original_len = original.encodedLen(); + const loaded_len = loaded.encodedLen(); + + try testing.expectEqual(original_len, loaded_len); + try testing.expectEqualSlices(u8, original_encoded[0..original_len], loaded_encoded[0..loaded_len]); + } +} + +test "error handling - file not found" { + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const result = tmp_dir.dir.openFile("nonexistent_file.txt", .{}); + + try testing.expectError(error.FileNotFound, result); +} + +test "error handling - invalid ENR format" { + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const test_file = "test_invalid_enr.txt"; + + const file = try tmp_dir.dir.createFile(test_file, .{}); + try file.writeAll("invalid-enr-content"); + file.close(); + + var enr: ENR = undefined; + const read_file = try tmp_dir.dir.openFile(test_file, .{}); + defer read_file.close(); + + const file_size = try read_file.getEndPos(); + var enr_txt: [enrlib.max_enr_size]u8 = undefined; + _ = try read_file.readAll(enr_txt[0..file_size]); + + const result = ENR.decodeTxtInto(&enr, enr_txt[0..file_size]); + try testing.expectError(error.BadPrefix, result); +} + +test "directory creation" { + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + var enr: ENR = undefined; + try ENR.decodeTxtInto(&enr, test_enrs[0]); + + try tmp_dir.dir.makePath("nested"); + const test_file = "nested/enr.txt"; + + const file = try tmp_dir.dir.createFile(test_file, .{}); + var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; + const out = try enr.encodeToTxt(&txt_buffer); + try file.writeAll(out); + file.close(); + + var loaded_enr: ENR = undefined; + const read_file = try tmp_dir.dir.openFile(test_file, .{}); + defer read_file.close(); + + const file_size = try read_file.getEndPos(); + var enr_txt: [enrlib.max_enr_size]u8 = undefined; + _ = try read_file.readAll(enr_txt[0..file_size]); + try ENR.decodeTxtInto(&loaded_enr, enr_txt[0..file_size]); +} + +test "empty and whitespace handling in multiple ENRs" { + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const allocator = testing.allocator; + + const test_file = "test_empty_entries.txt"; + + const file = try tmp_dir.dir.createFile(test_file, .{}); + const content = test_enrs[0] ++ "\n\n \t \n" ++ test_enrs[1] ++ "\n \n" ++ test_enrs[2]; + try file.writeAll(content); + file.close(); + + var enr_list = std.ArrayList(ENR).init(allocator); + defer enr_list.deinit(); + + const read_file = try tmp_dir.dir.openFile(test_file, .{}); + defer read_file.close(); + + const file_size = try read_file.getEndPos(); + const file_content = try allocator.alloc(u8, file_size); + defer allocator.free(file_content); + + _ = try read_file.readAll(file_content); + + var iterator = std.mem.splitSequence(u8, file_content, "\n"); + + while (iterator.next()) |enr_txt| { + const trimmed = std.mem.trim(u8, enr_txt, " \t\r\n"); + if (trimmed.len == 0) continue; + + var enr: ENR = undefined; + try ENR.decodeTxtInto(&enr, trimmed); + try enr_list.append(enr); + } + + try testing.expectEqual(@as(usize, 3), enr_list.items.len); +} + +test "custom delimiter" { + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const allocator = testing.allocator; + var enr_list = std.ArrayList(ENR).init(allocator); + defer enr_list.deinit(); + + for (test_enrs[12..14]) |enr_txt| { + var enr: ENR = undefined; + try ENR.decodeTxtInto(&enr, enr_txt); + try enr_list.append(enr); + } + + const test_file = "test_custom_delimiter.txt"; + + const file = try tmp_dir.dir.createFile(test_file, .{}); + for (enr_list.items, 0..) |*enr, i| { + var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; + const out = try enr.encodeToTxt(&txt_buffer); + try file.writeAll(out); + + if (i < enr_list.items.len - 1) { + try file.writeAll(" | "); + } + } + file.close(); + + var loaded_enr_list = std.ArrayList(ENR).init(allocator); + defer loaded_enr_list.deinit(); + + const read_file = try tmp_dir.dir.openFile(test_file, .{}); + defer read_file.close(); + + const file_size = try read_file.getEndPos(); + const file_content = try allocator.alloc(u8, file_size); + defer allocator.free(file_content); + + _ = try read_file.readAll(file_content); + + var iterator = std.mem.splitSequence(u8, file_content, " | "); + + while (iterator.next()) |enr_txt| { + const trimmed = std.mem.trim(u8, enr_txt, " \t\r\n"); + if (trimmed.len == 0) continue; + + var enr: ENR = undefined; + try ENR.decodeTxtInto(&enr, trimmed); + try loaded_enr_list.append(enr); + } + + try testing.expectEqual(enr_list.items.len, loaded_enr_list.items.len); +} From 4a6393c73be34b025dcd1fa348566772e7919519 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Tue, 26 Aug 2025 20:47:27 +0800 Subject: [PATCH 02/14] feat:update ci and readme Signed-off-by: Chen Kai <281165273grape@gmail.com> --- README.md | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- build.zig | 1 + 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3707745..224e6d8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,49 @@ # enr -About Ethereum Node Records (ENR) +Ethereum Node Records (ENR) utility in Zig. + +## Prerequisites + +- Zig 0.14.1 + +## Building + +To build the project, run the following command in the root directory of the project: + +```bash +zig build -Doptimize=ReleaseSafe +``` + +## Running Tests + +To run the tests, run the following command in the root directory of the project: + +```bash +zig build test --summary all +``` + +## Docs + +To generate documentation for the project, run the following command in the root directory of the project: + +```bash +zig build docs +``` + +# Usage + +Update `build.zig.zon`: + +```sh +zig fetch --save git+https://github.com/blockblaz/enr +``` + +In your `build.zig`: + +```zig +const enr_dep = b.dependency("zig_enr", .{ + .target = target, + .optimize = optimize, +}); +const enr_module = enr_dep.module("zig-enr"); +root_module.addImport("zig-enr", enr_module); +``` diff --git a/build.zig b/build.zig index 491a2e1..f8a32f6 100644 --- a/build.zig +++ b/build.zig @@ -34,6 +34,7 @@ pub fn build(b: *std.Build) void { lib_mod.addImport("enr", enr_mod); + b.modules.put(b.dupe("zig-enr"), enr_mod) catch @panic("OOM"); // Now, we will create a static library based on the module we created above. // This creates a `std.Build.Step.Compile`, which is the build step responsible // for actually invoking the compiler. From 403cfd7507537b8a36fdf74d612f0aaf0a50f54a Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Tue, 26 Aug 2025 20:54:18 +0800 Subject: [PATCH 03/14] fix: fix wrong module expose Signed-off-by: Chen Kai <281165273grape@gmail.com> --- build.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig b/build.zig index f8a32f6..6bd4fd9 100644 --- a/build.zig +++ b/build.zig @@ -34,7 +34,7 @@ pub fn build(b: *std.Build) void { lib_mod.addImport("enr", enr_mod); - b.modules.put(b.dupe("zig-enr"), enr_mod) catch @panic("OOM"); + b.modules.put(b.dupe("zig-enr"), lib_mod) catch @panic("OOM"); // Now, we will create a static library based on the module we created above. // This creates a `std.Build.Step.Compile`, which is the build step responsible // for actually invoking the compiler. From 5f4eea00e5d8d44de8c176844311809e0fb3b0f7 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Tue, 26 Aug 2025 23:47:36 +0800 Subject: [PATCH 04/14] fix:remove auto-generated comment and throw error Signed-off-by: Chen Kai <281165273grape@gmail.com> --- build.zig | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/build.zig b/build.zig index 6bd4fd9..e6d35a2 100644 --- a/build.zig +++ b/build.zig @@ -1,32 +1,15 @@ const std = @import("std"); -// Although this function looks imperative, note that its job is to -// declaratively construct a build graph that will be executed by an external -// runner. -pub fn build(b: *std.Build) void { - // Standard target options allows the person running `zig build` to choose - // what target to build for. Here we do not override the defaults, which - // means any target is allowed, and the default is native. Other options - // for restricting supported target set are available. +pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); - // 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 discv5 = b.dependency("discv5", .{}); const enr_mod = discv5.module("enr"); - // This creates a "module", which represents a collection of source files alongside - // some compilation options, such as optimization mode and linked system libraries. - // Every executable or library we compile will be based on one or more modules. const lib_mod = b.createModule(.{ - // `root_source_file` is the Zig "entry point" of the module. If a module - // only contains e.g. external object files, you can make this `null`. - // In this case the main source file is merely a path, however, in more - // complicated build scripts, this could be a generated file. .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, @@ -34,32 +17,21 @@ pub fn build(b: *std.Build) void { lib_mod.addImport("enr", enr_mod); - b.modules.put(b.dupe("zig-enr"), lib_mod) catch @panic("OOM"); - // Now, we will create a static library based on the module we created above. - // This creates a `std.Build.Step.Compile`, which is the build step responsible - // for actually invoking the compiler. + try b.modules.put(b.dupe("zig-enr"), lib_mod); const lib = b.addLibrary(.{ .linkage = .static, .name = "enr", .root_module = lib_mod, }); - // This declares intent for the library to be installed into the standard - // location when the user invokes the "install" step (the default step when - // running `zig build`). b.installArtifact(lib); - // Creates a step for unit testing. This only builds the test executable - // but does not run it. const lib_unit_tests = b.addTest(.{ .root_module = lib_mod, }); const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); - // Similar to creating the run step earlier, this exposes a `test` step to - // the `zig build --help` menu, providing a way for the user to request - // running the unit tests. const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_lib_unit_tests.step); From c7698edfa548c64414737fb1bfe3504742379bfa Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Tue, 26 Aug 2025 23:59:55 +0800 Subject: [PATCH 05/14] fix:remove auto generated comment in zon file Signed-off-by: Chen Kai <281165273grape@gmail.com> --- build.zig.zon | 33 ++------------------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index eec7cf6..d3e5e03 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,40 +1,12 @@ .{ - // This is the default name used by packages depending on this one. For - // example, when a user runs `zig fetch --save `, this field is used - // as the key in the `dependencies` table. Although the user can choose a - // different name, most users will stick with this provided value. - // - // It is redundant to include "zig" in this name because it is already - // within the Zig package namespace. .name = .zig_enr, - // This is a [Semantic Version](https://semver.org/). - // In a future version of Zig it will be used for package deduplication. .version = "0.0.0", - // Together with name, this represents a globally unique package - // identifier. This field is generated by the Zig toolchain when the - // package is first created, and then *never changes*. This allows - // unambiguous detection of one package being an updated version of - // another. - // - // When forking a Zig project, this id should be regenerated (delete the - // field and run `zig build`) if the upstream project is still maintained. - // Otherwise, the fork is *hostile*, attempting to take control over the - // original project's identity. Thus it is recommended to leave the comment - // on the following line intact, so that it shows up in code reviews that - // modify the field. .fingerprint = 0x499ae5d25c594f6a, // Changing this has security and trust implications. - // Tracks the earliest Zig version that the package considers to be a - // supported use case. .minimum_zig_version = "0.14.1", - // This field is optional. - // Each dependency must either provide a `url` and `hash`, or a `path`. - // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. - // Once all dependencies are fetched, `zig build` no longer requires - // internet connectivity. .dependencies = .{ .discv5 = .{ .url = "git+https://github.com/blockblaz/zig-discv5#bc96ea8e6a8c3ddc5880256a11e19df3afe37870", @@ -45,8 +17,7 @@ "build.zig", "build.zig.zon", "src", - // For example... - //"LICENSE", - //"README.md", + "LICENSE", + "README.md", }, } From f7572a5c8fe20cdb46f9e10aac1d1603c7044b81 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Wed, 27 Aug 2025 12:15:40 +0800 Subject: [PATCH 06/14] fix:elimate duplicate code Signed-off-by: Chen Kai <281165273grape@gmail.com> --- src/root.zig | 142 +++++++++++++++++++++++---------------------------- 1 file changed, 64 insertions(+), 78 deletions(-) diff --git a/src/root.zig b/src/root.zig index a5c5909..713ec94 100644 --- a/src/root.zig +++ b/src/root.zig @@ -6,80 +6,40 @@ pub const EncodedENR = enrlib.EncodedENR; pub const SignableENR = enrlib.SignableENR; /// ENR file I/O error set -pub const EnrFileError = error{ +pub const ENRFileError = error{ /// The ENR file is too large to process - EnrFileTooLarge, + ENRFileTooLarge, }; /// Loads ENR from the given file path -pub fn loadEnrFromDisk(enr: *ENR, file_path: []const u8) !void { - const enr_file = try std.fs.cwd().openFile(file_path, .{}); - defer enr_file.close(); - - const file_size = try enr_file.getEndPos(); - if (file_size > enrlib.max_enr_size) return EnrFileError.EnrFileTooLarge; - - var enr_txt: [enrlib.max_enr_size]u8 = undefined; - _ = try enr_file.readAll(enr_txt[0..file_size]); - - try ENR.decodeTxtInto(enr, enr_txt[0..file_size]); +pub fn loadENRFromDisk(enr: *ENR, file_path: []const u8) !void { + var buffer: [enrlib.max_enr_size]u8 = undefined; + const content = try readFileContent(file_path, &buffer); + try ENR.decodeTxtInto(enr, content); } /// Loads EncodedENR from the given file path -pub fn loadEncodedEnrFromDisk(encoded_enr: *EncodedENR, file_path: []const u8) !void { - const enr_file = try std.fs.cwd().openFile(file_path, .{}); - defer enr_file.close(); - - const file_size = try enr_file.getEndPos(); - if (file_size > enrlib.max_enr_size) return EnrFileError.EnrFileTooLarge; - - var temp_txt: [enrlib.max_enr_size]u8 = undefined; - _ = try enr_file.readAll(temp_txt[0..file_size]); - - encoded_enr.* = try EncodedENR.decodeTxtInto(temp_txt[0..file_size]); +pub fn loadEncodedENRFromDisk(encoded_enr: *EncodedENR, file_path: []const u8) !void { + var buffer: [enrlib.max_enr_size]u8 = undefined; + const content = try readFileContent(file_path, &buffer); + encoded_enr.* = try EncodedENR.decodeTxtInto(content); } /// Saves an ENR to disk with given file path -pub fn saveEnrToDisk(file_path: []const u8, enr: *ENR) !void { - if (std.fs.path.dirname(file_path)) |dir_path| { - try std.fs.cwd().makePath(dir_path); - } - - const file = try std.fs.cwd().createFile(file_path, .{}); - defer file.close(); - - var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; - const out = try enr.encodeToTxt(&txt_buffer); - - try file.writeAll(out); +pub fn saveENRToDisk(file_path: []const u8, enr: *ENR) !void { + try writeENRToFile(file_path, enr); } /// Saves a SignableENR to disk with given file path -pub fn saveSignableEnrToDisk(file_path: []const u8, signable_enr: *SignableENR) !void { - if (std.fs.path.dirname(file_path)) |dir_path| { - try std.fs.cwd().makePath(dir_path); - } - - const file = try std.fs.cwd().createFile(file_path, .{}); - defer file.close(); - - var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; - const out = try signable_enr.encodeToTxt(&txt_buffer); - - try file.writeAll(out); +pub fn saveSignableENRToDisk(file_path: []const u8, signable_enr: *SignableENR) !void { + try writeENRToFile(file_path, signable_enr); } /// Loads multiple ENRs from a single file -pub fn loadMultipleEnrsFromDisk(enr_list: *std.ArrayList(ENR), file_path: []const u8, delimiter: []const u8) !void { - const enr_file = try std.fs.cwd().openFile(file_path, .{}); - defer enr_file.close(); - - const file_size = try enr_file.getEndPos(); - const file_content = try enr_list.allocator.alloc(u8, file_size); +pub fn loadMultipleENRsFromDisk(enr_list: *std.ArrayList(ENR), file_path: []const u8, delimiter: []const u8) !void { + const file_content = try readFileContentAlloc(enr_list.allocator, file_path); defer enr_list.allocator.free(file_content); - _ = try enr_file.readAll(file_content); - var iterator = std.mem.splitSequence(u8, file_content, delimiter); while (iterator.next()) |enr_txt| { @@ -94,16 +54,10 @@ pub fn loadMultipleEnrsFromDisk(enr_list: *std.ArrayList(ENR), file_path: []cons } /// Loads multiple EncodedENRs from a single file -pub fn loadMultipleEncodedEnrsFromDisk(enr_list: *std.ArrayList(EncodedENR), file_path: []const u8, delimiter: []const u8) !void { - const enr_file = try std.fs.cwd().openFile(file_path, .{}); - defer enr_file.close(); - - const file_size = try enr_file.getEndPos(); - const file_content = try enr_list.allocator.alloc(u8, file_size); +pub fn loadMultipleEncodedENRsFromDisk(enr_list: *std.ArrayList(EncodedENR), file_path: []const u8, delimiter: []const u8) !void { + const file_content = try readFileContentAlloc(enr_list.allocator, file_path); defer enr_list.allocator.free(file_content); - _ = try enr_file.readAll(file_content); - var iterator = std.mem.splitSequence(u8, file_content, delimiter); while (iterator.next()) |enr_txt| { @@ -117,7 +71,40 @@ pub fn loadMultipleEncodedEnrsFromDisk(enr_list: *std.ArrayList(EncodedENR), fil } /// Saves multiple ENRs to a single file -pub fn saveMultipleEnrsToDisk(file_path: []const u8, enrs: []ENR, delimiter: []const u8) !void { +pub fn saveMultipleENRsToDisk(file_path: []const u8, enrs: []ENR, delimiter: []const u8) !void { + try writeMultipleENRsToFile(file_path, enrs, delimiter); +} + +/// Saves multiple SignableENRs to a single file +pub fn saveMultipleSignableENRsToDisk(file_path: []const u8, signable_enrs: []SignableENR, delimiter: []const u8) !void { + try writeMultipleENRsToFile(file_path, signable_enrs, delimiter); +} + +/// Helper function to read entire file content into a buffer +fn readFileContent(file_path: []const u8, buffer: []u8) ![]const u8 { + const file = try std.fs.cwd().openFile(file_path, .{}); + defer file.close(); + + const file_size = try file.getEndPos(); + if (file_size > buffer.len) return ENRFileError.ENRFileTooLarge; + + _ = try file.readAll(buffer[0..file_size]); + return buffer[0..file_size]; +} + +/// Helper function to read entire file content using allocator +fn readFileContentAlloc(allocator: std.mem.Allocator, file_path: []const u8) ![]u8 { + const file = try std.fs.cwd().openFile(file_path, .{}); + defer file.close(); + + const file_size = try file.getEndPos(); + const content = try allocator.alloc(u8, file_size); + _ = try file.readAll(content); + return content; +} + +/// Helper function to write content to file with directory creation +fn writeFileContent(file_path: []const u8, content: []const u8) !void { if (std.fs.path.dirname(file_path)) |dir_path| { try std.fs.cwd().makePath(dir_path); } @@ -125,19 +112,18 @@ pub fn saveMultipleEnrsToDisk(file_path: []const u8, enrs: []ENR, delimiter: []c const file = try std.fs.cwd().createFile(file_path, .{}); defer file.close(); - for (enrs, 0..) |*enr, i| { - var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; - const out = try enr.encodeToTxt(&txt_buffer); - try file.writeAll(out); + try file.writeAll(content); +} - if (i < enrs.len - 1) { - try file.writeAll(delimiter); - } - } +/// Helper function to write ENR text format to file +fn writeENRToFile(file_path: []const u8, enr: anytype) !void { + var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; + const out = try enr.encodeToTxt(&txt_buffer); + try writeFileContent(file_path, out); } -/// Saves multiple SignableENRs to a single file -pub fn saveMultipleSignableEnrsToDisk(file_path: []const u8, signable_enrs: []SignableENR, delimiter: []const u8) !void { +/// Helper function to write multiple ENRs to file with delimiter +fn writeMultipleENRsToFile(file_path: []const u8, enrs: anytype, delimiter: []const u8) !void { if (std.fs.path.dirname(file_path)) |dir_path| { try std.fs.cwd().makePath(dir_path); } @@ -145,12 +131,12 @@ pub fn saveMultipleSignableEnrsToDisk(file_path: []const u8, signable_enrs: []Si const file = try std.fs.cwd().createFile(file_path, .{}); defer file.close(); - for (signable_enrs, 0..) |*signable_enr, i| { + for (enrs, 0..) |*enr, i| { var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; - const out = try signable_enr.encodeToTxt(&txt_buffer); + const out = try enr.encodeToTxt(&txt_buffer); try file.writeAll(out); - if (i < signable_enrs.len - 1) { + if (i < enrs.len - 1) { try file.writeAll(delimiter); } } From 72e8c5d8f7078ef2056c6c466ac758f5619c1072 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Wed, 27 Aug 2025 12:44:50 +0800 Subject: [PATCH 07/14] fix:update install zig in CI Signed-off-by: Chen Kai <281165273grape@gmail.com> --- .github/workflows/ci.yml | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46de4f7..7d54c63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,32 +17,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Cache Zig - id: cache-zig - uses: actions/cache@v4 + - name: Set up Zig + uses: korandoru/setup-zig@v1 with: - path: ~/zig - key: ${{ runner.os }}-zig-${{ env.ZIG_VERSION }} - - - name: Install Zig - if: steps.cache-zig.outputs.cache-hit != 'true' - run: | - wget https://ziglang.org/download/${{ env.ZIG_VERSION }}/zig-x86_64-linux-${{ env.ZIG_VERSION }}.tar.xz - tar -xf zig-x86_64-linux-${{ env.ZIG_VERSION }}.tar.xz - mv zig-x86_64-linux-${{ env.ZIG_VERSION }} ~/zig - - - name: Add Zig to PATH - run: echo "${HOME}/zig" >> $GITHUB_PATH - - - name: Cache Zig build artifacts - uses: actions/cache@v4 # Updated to v4 - with: - path: | - zig-cache - ~/.cache/zig - key: ${{ runner.os }}-zig-build-${{ hashFiles('**/*.zig') }} - restore-keys: | - ${{ runner.os }}-zig-build- + zig-version: 0.14.1 - name: Formatting run: zig fmt --check --color on . From 247fadd8c0dc8429c3d45ced460c88c8e3d2778c Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Wed, 27 Aug 2025 13:06:00 +0800 Subject: [PATCH 08/14] fix:elimate duplicate code in test Signed-off-by: Chen Kai <281165273grape@gmail.com> --- src/root.zig | 236 +++++++++++++++++++++------------------------------ 1 file changed, 98 insertions(+), 138 deletions(-) diff --git a/src/root.zig b/src/root.zig index 713ec94..a025cd8 100644 --- a/src/root.zig +++ b/src/root.zig @@ -172,6 +172,91 @@ const test_enrs = [_][]const u8{ "enr:-LK4QKWrXTpV9T78hNG6s8AM6IO4XH9kFT91uZtFg1GcsJ6dKovDOr1jtAAFPnS2lvNltkOGA9k29BUN7lFh_sjuc9QBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhANAdd-Jc2VjcDI1NmsxoQLQa6ai7y9PMN5hpLe5HmiJSlYzMuzP7ZhwRiwHvqNXdoN0Y3CCI4yDdWRwgiOM", }; +/// Helper to write ENR to temp file +fn writeTempEnr(tmp_dir: *std.testing.TmpDir, file_name: []const u8, enr: *ENR) !void { + const tmp_file = try tmp_dir.dir.createFile(file_name, .{}); + defer tmp_file.close(); + + var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; + const out = try enr.encodeToTxt(&txt_buffer); + try tmp_file.writeAll(out); +} + +/// Helper to read ENR from temp file +fn readTempEnr(tmp_dir: *std.testing.TmpDir, file_name: []const u8, enr: *ENR) !void { + const read_file = try tmp_dir.dir.openFile(file_name, .{}); + defer read_file.close(); + + const file_size = try read_file.getEndPos(); + var enr_txt: [enrlib.max_enr_size]u8 = undefined; + _ = try read_file.readAll(enr_txt[0..file_size]); + try ENR.decodeTxtInto(enr, enr_txt[0..file_size]); +} + +/// Helper to write multiple ENRs to temp file +fn writeTempMultipleEnrs(tmp_dir: *std.testing.TmpDir, file_name: []const u8, enrs: []ENR, delimiter: []const u8) !void { + const tmp_file = try tmp_dir.dir.createFile(file_name, .{}); + defer tmp_file.close(); + + for (enrs, 0..) |*enr, i| { + var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; + const out = try enr.encodeToTxt(&txt_buffer); + try tmp_file.writeAll(out); + + if (i < enrs.len - 1) { + try tmp_file.writeAll(delimiter); + } + } +} + +/// Helper to read multiple ENRs from temp file +fn readTempMultipleEnrs(allocator: std.mem.Allocator, tmp_dir: *std.testing.TmpDir, file_name: []const u8, delimiter: []const u8) !std.ArrayList(ENR) { + const read_file = try tmp_dir.dir.openFile(file_name, .{}); + defer read_file.close(); + + const file_size = try read_file.getEndPos(); + const file_content = try allocator.alloc(u8, file_size); + defer allocator.free(file_content); + + _ = try read_file.readAll(file_content); + + var enr_list = std.ArrayList(ENR).init(allocator); + var iterator = std.mem.splitSequence(u8, file_content, delimiter); + + while (iterator.next()) |enr_txt| { + const trimmed = std.mem.trim(u8, enr_txt, " \t\r\n"); + if (trimmed.len == 0) continue; + + var enr: ENR = undefined; + try ENR.decodeTxtInto(&enr, trimmed); + try enr_list.append(enr); + } + + return enr_list; +} + +/// Helper to compare two ENRs by encoding +fn expectEqualEnrs(original: *ENR, loaded: *ENR) !void { + var original_encoded: [1024]u8 = undefined; + var loaded_encoded: [1024]u8 = undefined; + + try original.encodeInto(&original_encoded); + try loaded.encodeInto(&loaded_encoded); + + const original_len = original.encodedLen(); + const loaded_len = loaded.encodedLen(); + + try testing.expectEqual(original_len, loaded_len); + try testing.expectEqualSlices(u8, original_encoded[0..original_len], loaded_encoded[0..loaded_len]); +} + +/// Helper to create test file with content +fn createTestFileWithContent(tmp_dir: *std.testing.TmpDir, file_name: []const u8, content: []const u8) !void { + const file = try tmp_dir.dir.createFile(file_name, .{}); + defer file.close(); + try file.writeAll(content); +} + test "single ENR file operations" { var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -190,32 +275,12 @@ test "single ENR file operations" { const test_file = "test_single_enr.txt"; - const tmp_file = try tmp_dir.dir.createFile(test_file, .{}); - var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; - const out = try enr.encodeToTxt(&txt_buffer); - try tmp_file.writeAll(out); - tmp_file.close(); + try writeTempEnr(&tmp_dir, test_file, &enr); var loaded_enr: ENR = undefined; - const read_file = try tmp_dir.dir.openFile(test_file, .{}); - defer read_file.close(); + try readTempEnr(&tmp_dir, test_file, &loaded_enr); - const file_size = try read_file.getEndPos(); - var enr_txt: [enrlib.max_enr_size]u8 = undefined; - _ = try read_file.readAll(enr_txt[0..file_size]); - try ENR.decodeTxtInto(&loaded_enr, enr_txt[0..file_size]); - - var original_encoded: [1024]u8 = undefined; - var loaded_encoded: [1024]u8 = undefined; - - try enr.encodeInto(&original_encoded); - try loaded_enr.encodeInto(&loaded_encoded); - - const original_len = enr.encodedLen(); - const loaded_len = loaded_enr.encodedLen(); - - try testing.expectEqual(original_len, loaded_len); - try testing.expectEqualSlices(u8, original_encoded[0..original_len], loaded_encoded[0..loaded_len]); + try expectEqualEnrs(&enr, &loaded_enr); } test "multiple ENRs with newline delimiter" { @@ -234,55 +299,15 @@ test "multiple ENRs with newline delimiter" { const test_file = "test_multiple_enrs_newline.txt"; - const tmp_file = try tmp_dir.dir.createFile(test_file, .{}); - for (enr_list.items, 0..) |*enr, i| { - var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; - const out = try enr.encodeToTxt(&txt_buffer); - try tmp_file.writeAll(out); - - if (i < enr_list.items.len - 1) { - try tmp_file.writeAll("\n"); - } - } - tmp_file.close(); + try writeTempMultipleEnrs(&tmp_dir, test_file, enr_list.items, "\n"); - var loaded_enr_list = std.ArrayList(ENR).init(allocator); + var loaded_enr_list = try readTempMultipleEnrs(allocator, &tmp_dir, test_file, "\n"); defer loaded_enr_list.deinit(); - const read_file = try tmp_dir.dir.openFile(test_file, .{}); - defer read_file.close(); - - const file_size = try read_file.getEndPos(); - const file_content = try allocator.alloc(u8, file_size); - defer allocator.free(file_content); - - _ = try read_file.readAll(file_content); - - var iterator = std.mem.splitSequence(u8, file_content, "\n"); - - while (iterator.next()) |enr_txt| { - const trimmed = std.mem.trim(u8, enr_txt, " \t\r\n"); - if (trimmed.len == 0) continue; - - var enr: ENR = undefined; - try ENR.decodeTxtInto(&enr, trimmed); - try loaded_enr_list.append(enr); - } - try testing.expectEqual(enr_list.items.len, loaded_enr_list.items.len); for (enr_list.items, loaded_enr_list.items) |*original, *loaded| { - var original_encoded: [1024]u8 = undefined; - var loaded_encoded: [1024]u8 = undefined; - - try original.encodeInto(&original_encoded); - try loaded.encodeInto(&loaded_encoded); - - const original_len = original.encodedLen(); - const loaded_len = loaded.encodedLen(); - - try testing.expectEqual(original_len, loaded_len); - try testing.expectEqualSlices(u8, original_encoded[0..original_len], loaded_encoded[0..loaded_len]); + try expectEqualEnrs(original, loaded); } } @@ -301,9 +326,7 @@ test "error handling - invalid ENR format" { const test_file = "test_invalid_enr.txt"; - const file = try tmp_dir.dir.createFile(test_file, .{}); - try file.writeAll("invalid-enr-content"); - file.close(); + try createTestFileWithContent(&tmp_dir, test_file, "invalid-enr-content"); var enr: ENR = undefined; const read_file = try tmp_dir.dir.openFile(test_file, .{}); @@ -327,20 +350,10 @@ test "directory creation" { try tmp_dir.dir.makePath("nested"); const test_file = "nested/enr.txt"; - const file = try tmp_dir.dir.createFile(test_file, .{}); - var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; - const out = try enr.encodeToTxt(&txt_buffer); - try file.writeAll(out); - file.close(); + try writeTempEnr(&tmp_dir, test_file, &enr); var loaded_enr: ENR = undefined; - const read_file = try tmp_dir.dir.openFile(test_file, .{}); - defer read_file.close(); - - const file_size = try read_file.getEndPos(); - var enr_txt: [enrlib.max_enr_size]u8 = undefined; - _ = try read_file.readAll(enr_txt[0..file_size]); - try ENR.decodeTxtInto(&loaded_enr, enr_txt[0..file_size]); + try readTempEnr(&tmp_dir, test_file, &loaded_enr); } test "empty and whitespace handling in multiple ENRs" { @@ -348,37 +361,14 @@ test "empty and whitespace handling in multiple ENRs" { defer tmp_dir.cleanup(); const allocator = testing.allocator; - const test_file = "test_empty_entries.txt"; - const file = try tmp_dir.dir.createFile(test_file, .{}); const content = test_enrs[0] ++ "\n\n \t \n" ++ test_enrs[1] ++ "\n \n" ++ test_enrs[2]; - try file.writeAll(content); - file.close(); + try createTestFileWithContent(&tmp_dir, test_file, content); - var enr_list = std.ArrayList(ENR).init(allocator); + var enr_list = try readTempMultipleEnrs(allocator, &tmp_dir, test_file, "\n"); defer enr_list.deinit(); - const read_file = try tmp_dir.dir.openFile(test_file, .{}); - defer read_file.close(); - - const file_size = try read_file.getEndPos(); - const file_content = try allocator.alloc(u8, file_size); - defer allocator.free(file_content); - - _ = try read_file.readAll(file_content); - - var iterator = std.mem.splitSequence(u8, file_content, "\n"); - - while (iterator.next()) |enr_txt| { - const trimmed = std.mem.trim(u8, enr_txt, " \t\r\n"); - if (trimmed.len == 0) continue; - - var enr: ENR = undefined; - try ENR.decodeTxtInto(&enr, trimmed); - try enr_list.append(enr); - } - try testing.expectEqual(@as(usize, 3), enr_list.items.len); } @@ -398,40 +388,10 @@ test "custom delimiter" { const test_file = "test_custom_delimiter.txt"; - const file = try tmp_dir.dir.createFile(test_file, .{}); - for (enr_list.items, 0..) |*enr, i| { - var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; - const out = try enr.encodeToTxt(&txt_buffer); - try file.writeAll(out); + try writeTempMultipleEnrs(&tmp_dir, test_file, enr_list.items, " | "); - if (i < enr_list.items.len - 1) { - try file.writeAll(" | "); - } - } - file.close(); - - var loaded_enr_list = std.ArrayList(ENR).init(allocator); + var loaded_enr_list = try readTempMultipleEnrs(allocator, &tmp_dir, test_file, " | "); defer loaded_enr_list.deinit(); - const read_file = try tmp_dir.dir.openFile(test_file, .{}); - defer read_file.close(); - - const file_size = try read_file.getEndPos(); - const file_content = try allocator.alloc(u8, file_size); - defer allocator.free(file_content); - - _ = try read_file.readAll(file_content); - - var iterator = std.mem.splitSequence(u8, file_content, " | "); - - while (iterator.next()) |enr_txt| { - const trimmed = std.mem.trim(u8, enr_txt, " \t\r\n"); - if (trimmed.len == 0) continue; - - var enr: ENR = undefined; - try ENR.decodeTxtInto(&enr, trimmed); - try loaded_enr_list.append(enr); - } - try testing.expectEqual(enr_list.items.len, loaded_enr_list.items.len); } From 95b441cfcc13b81396dd221c2d1f09720a4b0918 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Wed, 27 Aug 2025 13:08:06 +0800 Subject: [PATCH 09/14] fix:remove auto generated comment Signed-off-by: Chen Kai <281165273grape@gmail.com> --- build.zig.zon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig.zon b/build.zig.zon index d3e5e03..e270480 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,7 +3,7 @@ .version = "0.0.0", - .fingerprint = 0x499ae5d25c594f6a, // Changing this has security and trust implications. + .fingerprint = 0x499ae5d25c594f6a, .minimum_zig_version = "0.14.1", From ae60122ed6f2eba7f971a34a7cb048cb41b77a95 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Wed, 27 Aug 2025 16:31:12 +0800 Subject: [PATCH 10/14] fix:reduce stack memory used Signed-off-by: Chen Kai <281165273grape@gmail.com> --- src/root.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/root.zig b/src/root.zig index a025cd8..f7db8d4 100644 --- a/src/root.zig +++ b/src/root.zig @@ -131,8 +131,8 @@ fn writeMultipleENRsToFile(file_path: []const u8, enrs: anytype, delimiter: []co const file = try std.fs.cwd().createFile(file_path, .{}); defer file.close(); + var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; for (enrs, 0..) |*enr, i| { - var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; const out = try enr.encodeToTxt(&txt_buffer); try file.writeAll(out); @@ -198,8 +198,8 @@ fn writeTempMultipleEnrs(tmp_dir: *std.testing.TmpDir, file_name: []const u8, en const tmp_file = try tmp_dir.dir.createFile(file_name, .{}); defer tmp_file.close(); + var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; for (enrs, 0..) |*enr, i| { - var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; const out = try enr.encodeToTxt(&txt_buffer); try tmp_file.writeAll(out); From 1f8aad425e998454ee9aeff0600914e79ee9ebe2 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Thu, 28 Aug 2025 11:57:05 +0800 Subject: [PATCH 11/14] fix:fix duplicate and add more tests Signed-off-by: Chen Kai <281165273grape@gmail.com> --- build.zig.zon | 4 +- src/root.zig | 373 +++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 308 insertions(+), 69 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index e270480..32f0260 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -9,8 +9,8 @@ .dependencies = .{ .discv5 = .{ - .url = "git+https://github.com/blockblaz/zig-discv5#bc96ea8e6a8c3ddc5880256a11e19df3afe37870", - .hash = "discv5-0.1.0-fkJduBNjAQCUYP7JVBtZ6A5Y79UYpyC8JHz5kzGnB-K5", + .url = "git+https://github.com/blockblaz/zig-discv5#31f46b3b5e5b25a68638bb320545b74c642c6caa", + .hash = "discv5-0.1.0-fkJduLBkAQBQHUJxdOEqCR2YMKfF1lNhaLM_FgonkFRW", }, }, .paths = .{ diff --git a/src/root.zig b/src/root.zig index f7db8d4..acce2e2 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,10 +1,29 @@ const std = @import("std"); const enrlib = @import("enr"); +/// ENR (Ethereum Node Record) - A fully parsed and validated ENR for network operations. +/// +/// This type provides immediate access to all ENR fields after parsing and validation. +/// It maintains the complete node information including IP address, ports, public key, +/// and signature in memory for fast access during network discovery protocols. pub const ENR = enrlib.ENR; + +/// EncodedENR - A lightweight wrapper around raw ENR text representation. +/// +/// This type stores the ENR in its encoded string format and provides lazy parsing +/// capabilities. Fields are decoded on-demand when accessed, making it memory-efficient +/// for scenarios where not all ENR data needs to be processed immediately. pub const EncodedENR = enrlib.EncodedENR; + +/// SignableENR - A mutable ENR builder for creating and updating node records. +/// +/// This type allows programmatic construction of ENR records by setting various +/// key-value pairs before signing with a private key. It handles the ENR encoding +/// format and ensures proper structure before signature generation. pub const SignableENR = enrlib.SignableENR; +pub const KeyPair = enrlib.KeyPair; +const enr_prefix = "enr:"; /// ENR file I/O error set pub const ENRFileError = error{ /// The ENR file is too large to process @@ -27,12 +46,16 @@ pub fn loadEncodedENRFromDisk(encoded_enr: *EncodedENR, file_path: []const u8) ! /// Saves an ENR to disk with given file path pub fn saveENRToDisk(file_path: []const u8, enr: *ENR) !void { - try writeENRToFile(file_path, enr); + var txt_buffer: [enrlib.max_enr_size * 2 + enr_prefix.len]u8 = undefined; + const out = try enr.encodeToTxt(&txt_buffer); + try writeFileContent(file_path, out); } /// Saves a SignableENR to disk with given file path pub fn saveSignableENRToDisk(file_path: []const u8, signable_enr: *SignableENR) !void { - try writeENRToFile(file_path, signable_enr); + var txt_buffer: [enrlib.max_enr_size * 2 + enr_prefix.len]u8 = undefined; + const out = try signable_enr.encodeToTxt(&txt_buffer); + try writeFileContent(file_path, out); } /// Loads multiple ENRs from a single file @@ -72,12 +95,42 @@ pub fn loadMultipleEncodedENRsFromDisk(enr_list: *std.ArrayList(EncodedENR), fil /// Saves multiple ENRs to a single file pub fn saveMultipleENRsToDisk(file_path: []const u8, enrs: []ENR, delimiter: []const u8) !void { - try writeMultipleENRsToFile(file_path, enrs, delimiter); + if (std.fs.path.dirname(file_path)) |dir_path| { + try std.fs.cwd().makePath(dir_path); + } + + const file = try std.fs.cwd().createFile(file_path, .{}); + defer file.close(); + + var txt_buffer: [enrlib.max_enr_size * 2 + enr_prefix.len]u8 = undefined; + for (enrs, 0..) |*enr, i| { + const out = try enr.encodeToTxt(&txt_buffer); + try file.writeAll(out); + + if (i < enrs.len - 1) { + try file.writeAll(delimiter); + } + } } /// Saves multiple SignableENRs to a single file pub fn saveMultipleSignableENRsToDisk(file_path: []const u8, signable_enrs: []SignableENR, delimiter: []const u8) !void { - try writeMultipleENRsToFile(file_path, signable_enrs, delimiter); + if (std.fs.path.dirname(file_path)) |dir_path| { + try std.fs.cwd().makePath(dir_path); + } + + const file = try std.fs.cwd().createFile(file_path, .{}); + defer file.close(); + + var txt_buffer: [enrlib.max_enr_size * 2 + enr_prefix.len]u8 = undefined; + for (signable_enrs, 0..) |*enr, i| { + const out = try enr.encodeToTxt(&txt_buffer); + try file.writeAll(out); + + if (i < signable_enrs.len - 1) { + try file.writeAll(delimiter); + } + } } /// Helper function to read entire file content into a buffer @@ -115,33 +168,6 @@ fn writeFileContent(file_path: []const u8, content: []const u8) !void { try file.writeAll(content); } -/// Helper function to write ENR text format to file -fn writeENRToFile(file_path: []const u8, enr: anytype) !void { - var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; - const out = try enr.encodeToTxt(&txt_buffer); - try writeFileContent(file_path, out); -} - -/// Helper function to write multiple ENRs to file with delimiter -fn writeMultipleENRsToFile(file_path: []const u8, enrs: anytype, delimiter: []const u8) !void { - if (std.fs.path.dirname(file_path)) |dir_path| { - try std.fs.cwd().makePath(dir_path); - } - - const file = try std.fs.cwd().createFile(file_path, .{}); - defer file.close(); - - var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; - for (enrs, 0..) |*enr, i| { - const out = try enr.encodeToTxt(&txt_buffer); - try file.writeAll(out); - - if (i < enrs.len - 1) { - try file.writeAll(delimiter); - } - } -} - const testing = std.testing; // Test ENR vectors from various Ethereum clients @@ -174,67 +200,110 @@ const test_enrs = [_][]const u8{ /// Helper to write ENR to temp file fn writeTempEnr(tmp_dir: *std.testing.TmpDir, file_name: []const u8, enr: *ENR) !void { - const tmp_file = try tmp_dir.dir.createFile(file_name, .{}); - defer tmp_file.close(); + const allocator = testing.allocator; - var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; - const out = try enr.encodeToTxt(&txt_buffer); - try tmp_file.writeAll(out); + const tmp_dir_path = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(tmp_dir_path); + + const full_path = try std.fs.path.join(allocator, &.{ tmp_dir_path, file_name }); + defer allocator.free(full_path); + + try saveENRToDisk(full_path, enr); } /// Helper to read ENR from temp file fn readTempEnr(tmp_dir: *std.testing.TmpDir, file_name: []const u8, enr: *ENR) !void { - const read_file = try tmp_dir.dir.openFile(file_name, .{}); - defer read_file.close(); + const allocator = testing.allocator; - const file_size = try read_file.getEndPos(); - var enr_txt: [enrlib.max_enr_size]u8 = undefined; - _ = try read_file.readAll(enr_txt[0..file_size]); - try ENR.decodeTxtInto(enr, enr_txt[0..file_size]); + const tmp_dir_path = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(tmp_dir_path); + + const full_path = try std.fs.path.join(allocator, &.{ tmp_dir_path, file_name }); + defer allocator.free(full_path); + + try loadENRFromDisk(enr, full_path); } /// Helper to write multiple ENRs to temp file fn writeTempMultipleEnrs(tmp_dir: *std.testing.TmpDir, file_name: []const u8, enrs: []ENR, delimiter: []const u8) !void { - const tmp_file = try tmp_dir.dir.createFile(file_name, .{}); - defer tmp_file.close(); + const allocator = testing.allocator; - var txt_buffer: [enrlib.max_enr_size * 2 + 4]u8 = undefined; - for (enrs, 0..) |*enr, i| { - const out = try enr.encodeToTxt(&txt_buffer); - try tmp_file.writeAll(out); + const tmp_dir_path = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(tmp_dir_path); - if (i < enrs.len - 1) { - try tmp_file.writeAll(delimiter); - } - } + const full_path = try std.fs.path.join(allocator, &.{ tmp_dir_path, file_name }); + defer allocator.free(full_path); + + try saveMultipleENRsToDisk(full_path, enrs, delimiter); } /// Helper to read multiple ENRs from temp file fn readTempMultipleEnrs(allocator: std.mem.Allocator, tmp_dir: *std.testing.TmpDir, file_name: []const u8, delimiter: []const u8) !std.ArrayList(ENR) { - const read_file = try tmp_dir.dir.openFile(file_name, .{}); - defer read_file.close(); + const tmp_dir_path = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(tmp_dir_path); - const file_size = try read_file.getEndPos(); - const file_content = try allocator.alloc(u8, file_size); - defer allocator.free(file_content); - - _ = try read_file.readAll(file_content); + const full_path = try std.fs.path.join(allocator, &.{ tmp_dir_path, file_name }); + defer allocator.free(full_path); var enr_list = std.ArrayList(ENR).init(allocator); - var iterator = std.mem.splitSequence(u8, file_content, delimiter); + try loadMultipleENRsFromDisk(&enr_list, full_path, delimiter); - while (iterator.next()) |enr_txt| { - const trimmed = std.mem.trim(u8, enr_txt, " \t\r\n"); - if (trimmed.len == 0) continue; + return enr_list; +} - var enr: ENR = undefined; - try ENR.decodeTxtInto(&enr, trimmed); - try enr_list.append(enr); - } +/// Helper to read EncodedENR from temp file +fn readTempEncodedEnr(tmp_dir: *std.testing.TmpDir, file_name: []const u8, encoded_enr: *EncodedENR) !void { + const allocator = testing.allocator; + + const tmp_dir_path = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(tmp_dir_path); + + const full_path = try std.fs.path.join(allocator, &.{ tmp_dir_path, file_name }); + defer allocator.free(full_path); + + try loadEncodedENRFromDisk(encoded_enr, full_path); +} + +/// Helper to read multiple EncodedENRs from temp file +fn readTempMultipleEncodedEnrs(allocator: std.mem.Allocator, tmp_dir: *std.testing.TmpDir, file_name: []const u8, delimiter: []const u8) !std.ArrayList(EncodedENR) { + const tmp_dir_path = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(tmp_dir_path); + + const full_path = try std.fs.path.join(allocator, &.{ tmp_dir_path, file_name }); + defer allocator.free(full_path); + + var enr_list = std.ArrayList(EncodedENR).init(allocator); + try loadMultipleEncodedENRsFromDisk(&enr_list, full_path, delimiter); return enr_list; } +/// Helper to write SignableENR to temp file +fn writeTempSignableEnr(tmp_dir: *std.testing.TmpDir, file_name: []const u8, signable_enr: *SignableENR) !void { + const allocator = testing.allocator; + + const tmp_dir_path = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(tmp_dir_path); + + const full_path = try std.fs.path.join(allocator, &.{ tmp_dir_path, file_name }); + defer allocator.free(full_path); + + try saveSignableENRToDisk(full_path, signable_enr); +} + +/// Helper to write multiple SignableENRs to temp file +fn writeTempMultipleSignableEnrs(tmp_dir: *std.testing.TmpDir, file_name: []const u8, signable_enrs: []SignableENR, delimiter: []const u8) !void { + const allocator = testing.allocator; + + const tmp_dir_path = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(tmp_dir_path); + + const full_path = try std.fs.path.join(allocator, &.{ tmp_dir_path, file_name }); + defer allocator.free(full_path); + + try saveMultipleSignableENRsToDisk(full_path, signable_enrs, delimiter); +} + /// Helper to compare two ENRs by encoding fn expectEqualEnrs(original: *ENR, loaded: *ENR) !void { var original_encoded: [1024]u8 = undefined; @@ -395,3 +464,173 @@ test "custom delimiter" { try testing.expectEqual(enr_list.items.len, loaded_enr_list.items.len); } + +test "EncodedENR file operations" { + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const test_file = "test_encoded_enr.txt"; + + try createTestFileWithContent(&tmp_dir, test_file, test_enrs[0]); + + var loaded_encoded_enr: EncodedENR = undefined; + try readTempEncodedEnr(&tmp_dir, test_file, &loaded_encoded_enr); + + var enr: ENR = undefined; + loaded_encoded_enr.decodeIntoENR(&enr); + + try testing.expectEqual(@as(u64, 3), enr.seq); + var ip_buffer: [16]u8 = undefined; + try testing.expectEqualStrings("4.157.240.54", (try loaded_encoded_enr.getIpStr(&ip_buffer)).?); + var sig_buffer: [130]u8 = undefined; + try std.testing.expectEqualStrings("0xc384b303fadb2cc38d2c164b86048ed4d9e4f3baafae423ea6019204d392ba5a79d43cb84528d6e3002ed248bdbdb0fd6584566839cadd5402e2b57edc5453b4", try loaded_encoded_enr.getSignatureStr(&sig_buffer, .lower)); + var pubkey_buffer: [68]u8 = undefined; + try std.testing.expectEqualStrings("0x037e897ca0bafa5c9ec5b1d01813b96926128c96ce06fefeaa40e18a2545866ffb", try loaded_encoded_enr.getPublicKeyStr(&pubkey_buffer, .lower)); + try std.testing.expectEqual(@as(u16, 9000), (try loaded_encoded_enr.getUdp()).?); +} + +test "multiple EncodedENRs file operations" { + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const allocator = testing.allocator; + const test_file = "test_multiple_encoded_enrs.txt"; + + const content = try std.mem.join(allocator, "\n", test_enrs[0..5]); + defer allocator.free(content); + try createTestFileWithContent(&tmp_dir, test_file, content); + + var loaded_encoded_enr_list = try readTempMultipleEncodedEnrs(allocator, &tmp_dir, test_file, "\n"); + defer loaded_encoded_enr_list.deinit(); + + var loaded_enr_list = try readTempMultipleEnrs(allocator, &tmp_dir, test_file, "\n"); + defer loaded_enr_list.deinit(); + + try testing.expectEqual(@as(usize, 5), loaded_encoded_enr_list.items.len); + try testing.expectEqual(loaded_enr_list.items.len, loaded_encoded_enr_list.items.len); + + for (loaded_encoded_enr_list.items, loaded_enr_list.items) |*encoded_enr, *enr| { + try testing.expectEqual(enr.seq, encoded_enr.seq()); + + var ip_buffer1: [16]u8 = undefined; + var ip_buffer2: [16]u8 = undefined; + + const ip1 = try enr.getIpStr(&ip_buffer1); + const ip2 = try encoded_enr.getIpStr(&ip_buffer2); + + try testing.expectEqualStrings(ip1.?, ip2.?); + + const udp1 = try enr.getUdp(); + const udp2 = try encoded_enr.getUdp(); + try testing.expectEqual(udp1.?, udp2.?); + + var pubkey_buffer1: [68]u8 = undefined; + var pubkey_buffer2: [68]u8 = undefined; + + const pubkey1 = try enr.getPublicKeyStr(&pubkey_buffer1, .lower); + const pubkey2 = try encoded_enr.getPublicKeyStr(&pubkey_buffer2, .lower); + try testing.expectEqualStrings(pubkey1, pubkey2); + + var sig_buffer1: [130]u8 = undefined; + var sig_buffer2: [130]u8 = undefined; + + const sig1 = try enr.getSignatureStr(&sig_buffer1, .lower); + const sig2 = try encoded_enr.getSignatureStr(&sig_buffer2, .lower); + try testing.expectEqualStrings(sig1, sig2); + } +} + +test "error handling - ENR file too large" { + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const test_file = "test_large_enr.txt"; + + var large_content: [enrlib.max_enr_size + 100]u8 = undefined; + @memset(&large_content, 'a'); + + try createTestFileWithContent(&tmp_dir, test_file, &large_content); + + var enr: ENR = undefined; + const allocator = testing.allocator; + const tmp_dir_path = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(tmp_dir_path); + const full_path = try std.fs.path.join(allocator, &.{ tmp_dir_path, test_file }); + defer allocator.free(full_path); + + const result = loadENRFromDisk(&enr, full_path); + try testing.expectError(ENRFileError.ENRFileTooLarge, result); +} + +test "SignableENR file operations" { + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const key_pair = KeyPair.generate(); + var signable_enr = SignableENR.create(key_pair); + defer signable_enr.deinit(); + + try signable_enr.set("ip", &[_]u8{ 127, 0, 0, 1 }); + var udp_bytes: [2]u8 = undefined; + std.mem.writeInt(u16, &udp_bytes, 30303, .big); + try signable_enr.set("udp", &udp_bytes); + + const test_file = "test_signable_enr.txt"; + + try writeTempSignableEnr(&tmp_dir, test_file, &signable_enr); + + var loaded_enr: ENR = undefined; + try readTempEnr(&tmp_dir, test_file, &loaded_enr); + + try testing.expectEqual(@as(u16, 30303), (try loaded_enr.getUdp()).?); + var ip_buffer: [16]u8 = undefined; + try testing.expectEqualStrings("127.0.0.1", (try loaded_enr.getIpStr(&ip_buffer)).?); +} + +test "multiple SignableENRs file operations" { + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const allocator = testing.allocator; + var signable_enr_list = std.ArrayList(SignableENR).init(allocator); + defer { + for (signable_enr_list.items) |*item| { + item.deinit(); + } + signable_enr_list.deinit(); + } + + for (0..3) |i| { + const key_pair = KeyPair.generate(); + var signable_enr = SignableENR.create(key_pair); + try signable_enr.set("ip", &[_]u8{ 127, 0, 0, @intCast(i + 1) }); + + var udp_bytes: [2]u8 = undefined; + std.mem.writeInt(u16, &udp_bytes, @intCast(30303 + i), .big); + try signable_enr.set("udp", &udp_bytes); + + try signable_enr_list.append(signable_enr); + } + + const test_file = "test_multiple_signable_enrs.txt"; + + try writeTempMultipleSignableEnrs(&tmp_dir, test_file, signable_enr_list.items, "\n"); + + var loaded_enr_list = try readTempMultipleEnrs(allocator, &tmp_dir, test_file, "\n"); + defer loaded_enr_list.deinit(); + + try testing.expectEqual(signable_enr_list.items.len, loaded_enr_list.items.len); + + for (loaded_enr_list.items, 0..) |*enr, i| { + try testing.expectEqual(@as(u16, @intCast(30303 + i)), (try enr.getUdp()).?); + + var ip_buffer: [16]u8 = undefined; + const expected_ip = switch (i) { + 0 => "127.0.0.1", + 1 => "127.0.0.2", + 2 => "127.0.0.3", + else => unreachable, + }; + try testing.expectEqualStrings(expected_ip, (try enr.getIpStr(&ip_buffer)).?); + } +} From 38540b35990a17346ecd606d6566a44f2ae96182 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Sun, 31 Aug 2025 11:46:15 +0800 Subject: [PATCH 12/14] feat:remove discv5 dep Signed-off-by: Chen Kai <281165273grape@gmail.com> --- build.zig | 7 +- build.zig.zon | 6 +- src/enr.zig | 958 ++++++++++++++++++++++++++++++++++++++++++++++ src/rlp.zig | 296 ++++++++++++++ src/root.zig | 10 +- src/secp256k1.zig | 89 +++++ 6 files changed, 1358 insertions(+), 8 deletions(-) create mode 100644 src/enr.zig create mode 100644 src/rlp.zig create mode 100644 src/secp256k1.zig diff --git a/build.zig b/build.zig index e6d35a2..16e26bb 100644 --- a/build.zig +++ b/build.zig @@ -5,9 +5,7 @@ pub fn build(b: *std.Build) !void { const optimize = b.standardOptimizeOption(.{}); - const discv5 = b.dependency("discv5", .{}); - - const enr_mod = discv5.module("enr"); + const secp256k1 = b.dependency("secp256k1", .{}); const lib_mod = b.createModule(.{ .root_source_file = b.path("src/root.zig"), @@ -15,7 +13,8 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); - lib_mod.addImport("enr", enr_mod); + lib_mod.addImport("secp256k1", secp256k1.module("secp256k1")); + lib_mod.linkLibrary(secp256k1.artifact("libsecp")); try b.modules.put(b.dupe("zig-enr"), lib_mod); const lib = b.addLibrary(.{ diff --git a/build.zig.zon b/build.zig.zon index 32f0260..369b616 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,9 +8,9 @@ .minimum_zig_version = "0.14.1", .dependencies = .{ - .discv5 = .{ - .url = "git+https://github.com/blockblaz/zig-discv5#31f46b3b5e5b25a68638bb320545b74c642c6caa", - .hash = "discv5-0.1.0-fkJduLBkAQBQHUJxdOEqCR2YMKfF1lNhaLM_FgonkFRW", + .secp256k1 = .{ + .url = "git+https://github.com/zig-bitcoin/libsecp256k1-zig#b96922b375a601f3303bf43be1dceb3d101df6c6", + .hash = "secp256k1-0.0.0-AAAAAPPgAAADPOkS7brnO1B26LjbhZsxwz6Kx8a6jCwB", }, }, .paths = .{ diff --git a/src/enr.zig b/src/enr.zig new file mode 100644 index 0000000..62925dd --- /dev/null +++ b/src/enr.zig @@ -0,0 +1,958 @@ +// This code originally copy from https://github.com/ChainSafe/zig-discv5/blob/aa2cd1735a575b8ce663914ac6279ad0ba3c4ede/src/enr.zig +// with replacements of the secp256k1 algorithm and adding some helper functions. +const std = @import("std"); + +const rlp = @import("rlp.zig"); + +const RLPReader = rlp.RLPReader; +const RLPWriter = rlp.RLPWriter; + +const Keccak = std.crypto.hash.sha3.Keccak256; +const secp256k1 = @import("secp256k1.zig"); + +const digest_size = secp256k1.digest_size; +pub const max_enr_size = 300; +pub const signature_size = 64; +// non-kv bytes +// list_len_len, list_len, sig_len_len, sig_len, sig, seq, id_len, id_byte +pub const max_kvs_size = max_enr_size - signature_size - 7; + +// assuming single-byte keys, empty values +pub const max_kvs = max_kvs_size / 3; + +/// Key-Value store for ENR +pub const KVs = struct { + map: std.StringHashMap([]const u8), + allocator: std.mem.Allocator, + + pub fn init() KVs { + return KVs{ + .map = std.StringHashMap([]const u8).init(std.heap.page_allocator), + .allocator = std.heap.page_allocator, + }; + } + + pub fn deinit(self: *KVs) void { + var it = self.map.iterator(); + while (it.next()) |entry| { + self.allocator.free(entry.key_ptr.*); + self.allocator.free(entry.value_ptr.*); + } + self.map.deinit(); + } + + pub fn put(self: *KVs, key: []const u8, value: []const u8) !void { + const owned_key = try self.allocator.dupe(u8, key); + const owned_value = try self.allocator.dupe(u8, value); + try self.map.put(owned_key, owned_value); + } + + pub fn get(self: *const KVs, key: []const u8) ?[]const u8 { + return self.map.get(key); + } + + pub fn append(self: *KVs, key: []const u8, value: []const u8) !void { + try self.put(key, value); + } + + pub const Iterator = struct { + keys: std.ArrayList([]const u8), + map: *const std.StringHashMap([]const u8), + index: usize, + + pub fn init(kvs: *const KVs) Iterator { + var keys = std.ArrayList([]const u8).init(kvs.allocator); + + var map_it = kvs.map.iterator(); + while (map_it.next()) |entry| { + keys.append(entry.key_ptr.*) catch unreachable; + } + + std.sort.heap([]const u8, keys.items, {}, struct { + fn lessThan(context: void, a: []const u8, b: []const u8) bool { + _ = context; + return std.mem.order(u8, a, b) == .lt; + } + }.lessThan); + + return Iterator{ + .keys = keys, + .map = &kvs.map, + .index = 0, + }; + } + + pub fn next(self: *Iterator) ?[2][]const u8 { + if (self.index >= self.keys.items.len) return null; + + const key = self.keys.items[self.index]; + const value = self.map.get(key).?; + self.index += 1; + + return [2][]const u8{ key, value }; + } + + pub fn deinit(self: *Iterator) void { + self.keys.deinit(); + } + }; + + pub fn iterator(self: *const KVs) Iterator { + return Iterator.init(self); + } +}; + +/// ID scheme for ENR +pub const IDScheme = enum { + v4, + + pub fn init(id: []const u8) Error!IDScheme { + if (std.mem.eql(u8, id, "v4")) { + return IDScheme.v4; + } else { + return Error.BadID; + } + } + + pub fn publicKeyKey(id: IDScheme) []const u8 { + switch (id) { + .v4 => return "secp256k1", + } + } + + pub fn publicKey(id: IDScheme, value: []const u8) Error!PublicKey { + switch (id) { + .v4 => { + return PublicKey{ .v4 = secp256k1.PublicKey.fromSlice(value) catch return Error.BadPubkey }; + }, + } + } + + pub fn publicKeyFromKVs(id: IDScheme, kvs: *KVs) Error!PublicKey { + switch (id) { + .v4 => { + return try id.publicKey(kvs.get(id.publicKeyKey()) orelse return Error.BadPubkey); + }, + } + } +}; + +/// Key pair for ENR +pub const KeyPair = union(IDScheme) { + v4: secp256k1.SecretKey, + + pub fn generate() KeyPair { + return KeyPair{ .v4 = secp256k1.SecretKey.generate() }; + } + + pub fn generateWithRandom(rng: std.Random) KeyPair { + return KeyPair{ .v4 = secp256k1.SecretKey.generateWithRandom(rng) }; + } + + pub fn sign(self: KeyPair, data: []const u8) ![signature_size]u8 { + switch (self) { + .v4 => |kp| { + var hashed: [digest_size]u8 = undefined; + Keccak.hash(data, &hashed, .{}); + + const msg = secp256k1.Message.fromDigest(hashed); + const sig = secp256k1.getSecp256k1Context().signEcdsa(&msg, &kp); + return sig.serializeCompact(); + }, + } + } + + pub fn publicKey(self: KeyPair) PublicKey { + switch (self) { + .v4 => |kp| { + return PublicKey{ .v4 = kp.publicKey(secp256k1.getSecp256k1Context().*) }; + }, + } + } +}; + +/// Public key for ENR +pub const PublicKey = union(IDScheme) { + v4: secp256k1.PublicKey, + + pub fn init(id: IDScheme, data: []const u8) !PublicKey { + switch (id) { + .v4 => { + return PublicKey{ .v4 = try secp256k1.PublicKey.fromSlice(data) }; + }, + } + } + + pub fn verify(self: PublicKey, data: []const u8, signature: []const u8) Error!void { + switch (self) { + .v4 => |pk| { + var hashed: [digest_size]u8 = undefined; + Keccak.hash(data, &hashed, .{}); + + return try secp256k1.getSecp256k1Context().verifyEcdsa(secp256k1.Message.fromDigest(hashed), try secp256k1.Signature.fromCompact(signature), pk); + }, + } + } + + pub fn verifier(self: PublicKey, signature: []const u8) Error!secp256k1.Verifier { + switch (self) { + .v4 => |pk| { + return secp256k1.Verifier.init(secp256k1.Signature.fromCompact(signature) catch return error.BadSignature, pk); + }, + } + } + + pub fn nodeId(self: PublicKey) NodeId { + switch (self) { + .v4 => |pk| { + var node_id: NodeId = undefined; + Keccak.hash(&pk.serializeUncompressed(), &node_id, .{}); + return node_id; + }, + } + } +}; + +pub const node_id_size = 32; +pub const NodeId = [node_id_size]u8; + +const Error = rlp.RLPReader.Error || error{ + TooShort, + TooLong, + BadPrefix, + BadKVs, + BadID, + BadPubkey, + BadSignature, +}; + +/// ENR (Ethereum Node Record) - A fully parsed and validated ENR for network operations. +/// +/// This type provides immediate access to all ENR fields after parsing and validation. +/// It maintains the complete node information including IP address, ports, public key, +/// and signature in memory for fast access during network discovery protocols. +pub const ENR = struct { + kvs: KVs, + seq: u64, + signature: [signature_size]u8, + + pub fn deinit(self: *ENR) void { + self.kvs.deinit(); + } + + pub fn get(self: *ENR, key: []const u8) ?[]const u8 { + return self.kvs.get(key); + } + + pub fn id(self: *ENR) IDScheme { + return IDScheme.init(self.kvs.get("id").?) catch unreachable; + } + + pub fn publicKey(self: *ENR) PublicKey { + return self.id().publicKeyFromKVs(&self.kvs) catch unreachable; + } + + pub fn nodeId(self: *ENR) NodeId { + return self.publicKey().nodeId(); + } + + pub fn encodeInto(self: *ENR, out: []u8) !void { + try encodeIntoFromComponents(out, &self.kvs, self.seq, self.signature); + } + + pub fn encodedLen(self: *ENR) usize { + return totalLen(&self.kvs, self.seq); + } + + /// Encode ENR to base64 text format (with "enr:" prefix) + /// The `out` buffer must be at least `encodedTxtLen()` bytes long + pub fn encodeToTxt(self: *ENR, out: []u8) ![]u8 { + const binary_len = self.encodedLen(); + const encoder = std.base64.url_safe_no_pad.Encoder; + const encoded_len = encoder.calcSize(binary_len); + const required_len = 4 + encoded_len; + + if (out.len < required_len) { + return error.BufferTooSmall; + } + + var binary_buf: [max_enr_size]u8 = undefined; + try self.encodeInto(binary_buf[0..binary_len]); + + @memcpy(out[0..4], "enr:"); + _ = encoder.encode(out[4 .. 4 + encoded_len], binary_buf[0..binary_len]); + + return out[0 .. 4 + encoded_len]; + } + + /// Calculate the length needed for the encoded text format (including "enr:" prefix) + pub fn encodedTxtLen(self: *ENR) usize { + const binary_len = self.encodedLen(); + const encoder = std.base64.url_safe_no_pad.Encoder; + const base64_len = encoder.calcSize(binary_len); + return 4 + base64_len; // "enr:" + base64 + } + + pub fn decodeInto(enr: *ENR, data: []const u8) Error!void { + if (data.len < 8 + signature_size) { + return Error.TooShort; + } + if (data.len > max_enr_size) { + return Error.TooLong; + } + + var outer_reader = RLPReader.init(data); + const list_data = try outer_reader.read(.{.long_list}); + var list_reader = RLPReader.init(list_data); + + const sig = try list_reader.read(.{.long_string}); + + const seq_pos = list_reader.pos; + const seq_bytes = try list_reader.read(.{ .single_byte, .short_string }); + const seq = std.mem.readVarInt(u64, seq_bytes, .big); + + var kvs = KVs.init(); + while (!list_reader.finished()) { + const key = list_reader.read(.{ .short_string, .long_string }) catch unreachable; + const value = list_reader.read(.{ .single_byte, .short_string, .long_string }) catch unreachable; + + kvs.put(key, value) catch unreachable; + } + + const id_scheme = try IDScheme.init(kvs.get("id") orelse return Error.BadID); + const public_key = id_scheme.publicKeyFromKVs(&kvs) catch return Error.BadPubkey; + + // Verify the signature, streaming the signed data to the verifier + var sig_verifier = try public_key.verifier(sig); + + // signed_data_list = length_prefix + elements + { + const elements = list_reader.data[seq_pos..]; + var length_prefix_buf: [2]u8 = undefined; + var writer = RLPWriter.init(&length_prefix_buf); + writer.writeListLength(elements.len) catch unreachable; + const length_prefix = length_prefix_buf[0..writer.pos]; + + // write the length prefix + sig_verifier.update(length_prefix); + // write the elements + sig_verifier.update(elements); + + sig_verifier.verify() catch return Error.BadSignature; + } + + // the ENR has been proven valid, write + @memcpy(&enr.signature, sig); + enr.seq = seq; + enr.kvs = kvs; + } + + pub fn decodeTxtInto(enr: *ENR, source: []const u8) !void { + if (!std.mem.eql(u8, source[0..4], "enr:")) { + return Error.BadPrefix; + } + + var buffer: [max_enr_size]u8 = undefined; + const decoder = std.base64.url_safe_no_pad.Decoder; + const size = try decoder.calcSizeForSlice(source[4..]); + try decoder.decode(buffer[0..size], source[4..]); + + try decodeInto(enr, buffer[0..size]); + } + + pub fn getIp(self: *ENR) !?std.net.Ip4Address { + if (self.get("ip")) |ip_bytes| { + if (ip_bytes.len != 4) return error.InvalidLength; + return std.net.Ip4Address.init(ip_bytes[0..4].*, 0); + } + return null; + } + + pub fn getIpStr(self: *ENR, out: []u8) !?[]const u8 { + if (self.get("ip")) |ip_bytes| { + if (ip_bytes.len != 4) return error.InvalidLength; + const formatted = try std.fmt.bufPrint(out, "{}.{}.{}.{}", .{ ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3] }); + return formatted; + } + return null; + } + + pub fn getUdp(self: *ENR) !?u16 { + if (self.get("udp")) |udp_bytes| { + if (udp_bytes.len != 2) return error.InvalidLength; + return std.mem.readInt(u16, udp_bytes[0..2], .big); + } + return null; + } + + pub fn getPublicKeyStr(self: *ENR, out: []u8, case: std.fmt.Case) ![]const u8 { + const pk = self.publicKey(); + const serialized = switch (pk) { + .v4 => |p| p.serialize(), + }; + + const public_key_hex = std.fmt.bytesToHex(serialized, case); + if (out.len < public_key_hex.len + 2) return error.BufferTooSmall; + @memcpy(out[0..2], "0x"); + @memcpy(out[2 .. public_key_hex.len + 2], public_key_hex[0..public_key_hex.len]); + return out[0 .. public_key_hex.len + 2]; + } + + pub fn getSignatureStr(self: *ENR, out: []u8, case: std.fmt.Case) ![]const u8 { + const signature_hex = std.fmt.bytesToHex(self.signature, case); + if (out.len < signature_hex.len + 2) return error.BufferTooSmall; + @memcpy(out[0..2], "0x"); + @memcpy(out[2 .. signature_hex.len + 2], signature_hex[0..signature_hex.len]); + return out[0 .. signature_hex.len + 2]; + } +}; + +/// SignableENR - A mutable ENR builder for creating and updating node records. +/// +/// This type allows programmatic construction of ENR records by setting various +/// key-value pairs before signing with a private key. It handles the ENR encoding +/// format and ensures proper structure before signature generation. +pub const SignableENR = struct { + kvs: KVs, + seq: u64, + kp: KeyPair, + + const Self = @This(); + + pub fn deinit(self: *Self) void { + self.kvs.deinit(); + } + + pub fn create(key_pair: KeyPair) SignableENR { + var kvs = KVs.init(); + switch (key_pair) { + .v4 => |kp| { + kvs.put("id", "v4") catch unreachable; + kvs.put("secp256k1", &kp.publicKey(secp256k1.getSecp256k1Context().*).serialize()) catch unreachable; + }, + } + return SignableENR{ .kp = key_pair, .kvs = kvs, .seq = 0 }; + } + + pub fn get(self: *Self, key: []const u8) ?[]const u8 { + return self.kvs.get(key); + } + + pub fn set(self: *Self, key: []const u8, value: []const u8) !void { + try self.kvs.put(key, value); + } + + pub fn id(self: *Self) IDScheme { + return IDScheme.init(self.kvs.get("id").?) catch unreachable; + } + + pub fn publicKey(self: *Self) PublicKey { + return self.id().publicKeyFromKVs(&self.kvs) catch unreachable; + } + + pub fn nodeId(self: *Self) NodeId { + return self.publicKey().nodeId(); + } + + pub fn sign(self: *Self) ![signature_size]u8 { + var buffer = [_]u8{0} ** max_enr_size; + try encodeSignedPayload(&buffer, &self.kvs, self.seq); + const signed = buffer[0..signedLen(&self.kvs, self.seq)]; + return try self.kp.sign(signed); + } + + pub fn encodeInto(self: *Self, out: []u8) !void { + const signature = try self.sign(); + try encodeIntoFromComponents(out, &self.kvs, self.seq, signature); + } + + pub fn encodedLen(self: *Self) usize { + return totalLen(&self.kvs, self.seq); + } + + /// Calculate the length needed for the encoded text format (including "enr:" prefix) + pub fn encodedTxtLen(self: *Self) usize { + const binary_len = self.encodedLen(); + const encoder = std.base64.url_safe_no_pad.Encoder; + const base64_len = encoder.calcSize(binary_len); + return 4 + base64_len; // "enr:" + base64 + } + + /// Encode SignableENR to base64 text format (with "enr:" prefix) + /// The `out` buffer must be at least `encodedTxtLen()` bytes long + pub fn encodeToTxt(self: *Self, out: []u8) ![]u8 { + const binary_len = self.encodedLen(); + const encoder = std.base64.url_safe_no_pad.Encoder; + const encoded_len = encoder.calcSize(binary_len); + const required_len = 4 + encoded_len; + + if (out.len < required_len) { + return error.BufferTooSmall; + } + + var binary_buf: [max_enr_size]u8 = undefined; + try self.encodeInto(binary_buf[0..binary_len]); + + @memcpy(out[0..4], "enr:"); + _ = encoder.encode(out[4 .. 4 + encoded_len], binary_buf[0..binary_len]); + + return out[0..required_len]; + } + + pub fn getIp(self: *Self) !?std.net.Ip4Address { + if (self.get("ip")) |ip_bytes| { + if (ip_bytes.len != 4) return error.InvalidLength; + return std.net.Ip4Address.init(ip_bytes[0..4].*, 0); + } + return null; + } + + pub fn getIpStr(self: *Self, out: []u8) !?[]const u8 { + if (self.get("ip")) |ip_bytes| { + if (ip_bytes.len != 4) return error.InvalidLength; + const formatted = try std.fmt.bufPrint(out, "{}.{}.{}.{}", .{ ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3] }); + return formatted; + } + return null; + } + + pub fn getUdp(self: *Self) !?u16 { + if (self.get("udp")) |udp_bytes| { + if (udp_bytes.len != 2) return error.InvalidLength; + return std.mem.readInt(u16, udp_bytes[0..2], .big); + } + return null; + } + + pub fn getPublicKeyStr(self: *Self, out: []u8, case: std.fmt.Case) ![]const u8 { + const pk = self.publicKey(); + const serialized = switch (pk) { + .v4 => |p| p.serialize(), + }; + + const public_key_hex = std.fmt.bytesToHex(serialized, case); + if (out.len < public_key_hex.len + 2) return error.BufferTooSmall; + @memcpy(out[0..2], "0x"); + @memcpy(out[2 .. public_key_hex.len + 2], public_key_hex[0..public_key_hex.len]); + return out[0 .. public_key_hex.len + 2]; + } + + pub fn signStr(self: *Self, out: []u8, case: std.fmt.Case) ![]const u8 { + const signature_hex = std.fmt.bytesToHex(try self.sign(), case); + if (out.len < signature_hex.len + 2) return error.BufferTooSmall; + @memcpy(out[0..2], "0x"); + @memcpy(out[2 .. signature_hex.len + 2], signature_hex[0..signature_hex.len]); + return out[0 .. signature_hex.len + 2]; + } +}; + +fn encodeIntoFromComponents(out: []u8, kvs: *KVs, seq: u64, signature: [signature_size]u8) !void { + var writer = RLPWriter.init(out); + try writer.writeListLength(listLen(kvs, seq)); + try writer.writeString(&signature); + try writer.writeInt(u64, seq); + + var kvs_it = kvs.iterator(); + defer kvs_it.deinit(); + + while (kvs_it.next()) |entry| { + try writer.writeString(entry[0]); + try writer.writeString(entry[1]); + } +} + +fn encodeSignedPayload(out: []u8, kvs: *KVs, seq: u64) !void { + var writer = RLPWriter.init(out); + try writer.writeListLength(signedListLen(kvs, seq)); + try writer.writeInt(u64, seq); + + var kvs_it = kvs.iterator(); + defer kvs_it.deinit(); + + while (kvs_it.next()) |entry| { + try writer.writeString(entry[0]); + try writer.writeString(entry[1]); + } +} + +/// The length of the whole rlp list +fn totalLen(kvs: *KVs, seq: u64) usize { + const list_len = listLen(kvs, seq); + return rlp.elemLen(list_len); +} + +/// The length of all rlp list elements +fn listLen(kvs: *KVs, seq: u64) usize { + return signedListLen(kvs, seq) + rlp.elemLen(signature_size); // signature +} + +/// The length of the rlp list that is signed over +fn signedLen(kvs: *KVs, seq: u64) usize { + const list_len = signedListLen(kvs, seq); + return rlp.elemLen(list_len); +} + +/// The length of the rlp list elements that are signed over +fn signedListLen(kvs: *KVs, seq: u64) usize { + var length: usize = 0; + length += rlp.intLen(u64, seq); // seq + length += kvsLen(kvs); + + return length; +} + +fn kvsLen(kvs: *KVs) usize { + var length: usize = 0; + var it = kvs.iterator(); + defer it.deinit(); + + while (it.next()) |entry| { + length += rlp.elemLen(entry[0].len); + length += rlp.elemLen(entry[1].len); + } + return length; +} + +pub fn decodeTxtIntoRlp(dest: []u8, source: []const u8) ![]u8 { + if (!std.mem.eql(u8, source[0..4], "enr:")) { + return Error.BadPrefix; + } + + const decoder = std.base64.url_safe_no_pad.Decoder; + const size = try decoder.calcSizeForSlice(source[4..]); + try decoder.decode(dest[0..size], source[4..]); + + return dest[0..size]; +} + +/// EncodedENR - A lightweight wrapper around raw ENR text representation. +/// +/// This type stores the ENR in its encoded string format and provides lazy parsing +/// capabilities. Fields are decoded on-demand when accessed, making it memory-efficient +/// for scenarios where not all ENR data needs to be processed immediately. +/// Methods assume that data is a valid RLP-encoded ENR +pub const EncodedENR = struct { + data: [max_enr_size]u8, + len: usize, + + const Self = @This(); + + /// Ensures that `data` is a valid ENR + pub fn init(data: []const u8) Error!Self { + if (data.len > max_enr_size) return Error.TooLong; + + var self = Self{ + .data = undefined, + .len = data.len, + }; + @memcpy(self.data[0..data.len], data); + + try self.verify(); + return self; + } + + pub fn getData(self: *const Self) []const u8 { + return self.data[0..self.len]; + } + + pub fn signature(self: *const Self) [signature_size]u8 { + var outer_reader = RLPReader.init(self.getData()); + const list_data = outer_reader.read(.{.long_list}) catch unreachable; + var list_reader = RLPReader.init(list_data); + + const sig = list_reader.read(.{.long_string}) catch unreachable; + return sig[0..signature_size].*; + } + + pub fn seq(self: *const Self) u64 { + var outer_reader = RLPReader.init(self.getData()); + const list_data = outer_reader.read(.{.long_list}) catch unreachable; + var list_reader = RLPReader.init(list_data); + + _ = list_reader.read(.{.long_string}) catch unreachable; + + const seq_bytes = list_reader.read(.{ .single_byte, .short_string }) catch unreachable; + return std.mem.readVarInt(u64, seq_bytes, .big); + } + + pub fn get(self: *const Self, key: []const u8) ?[]const u8 { + var outer_reader = RLPReader.init(self.getData()); + const list_data = outer_reader.read(.{.long_list}) catch unreachable; + var list_reader = RLPReader.init(list_data); + + // signature + _ = list_reader.read(.{.long_string}) catch unreachable; + // seq + _ = list_reader.read(.{ .single_byte, .short_string }) catch unreachable; + + while (!list_reader.finished()) { + const k = list_reader.read(.{ .short_string, .long_string }) catch unreachable; + if (std.mem.eql(u8, k, key)) { + return list_reader.read(.{ .single_byte, .short_string, .long_string }) catch unreachable; + } else { + _ = list_reader.read(.{ .single_byte, .short_string, .long_string }) catch unreachable; + } + } + return null; + } + + pub fn id(self: *const Self) IDScheme { + return IDScheme.init(self.get("id").?) catch unreachable; + } + + pub fn publicKey(self: *const Self) PublicKey { + const id_scheme = self.id(); + return id_scheme.publicKey(self.get(id_scheme.publicKeyKey()).?) catch unreachable; + } + + pub fn nodeId(self: *const Self) NodeId { + return self.publicKey().nodeId(); + } + + pub fn verify(self: *const Self) Error!void { + const data = self.getData(); + // Sanity bounds checks + if (data.len < 3 + signature_size) { + return Error.TooShort; + } + if (data.len > max_enr_size) { + return Error.TooLong; + } + + // The outer rlp must be a long list because the required elements are > 55 bytes + var outer_reader = RLPReader.init(data); + const list_data = try outer_reader.read(.{.long_list}); + var list_reader = RLPReader.init(list_data); + + const sig = try list_reader.read(.{.long_string}); + + const seq_pos = list_reader.pos; + _ = try list_reader.read(.{ .single_byte, .short_string }); + + // Check the kvs + // - id key must be present + // - keys must be unique + // - keys must be sorted + var kvs = KVs.init(); + defer kvs.deinit(); + while (!list_reader.finished()) { + const key = try list_reader.read(.{ .short_string, .long_string }); + const value = try list_reader.read(.{ .single_byte, .short_string, .long_string }); + kvs.append(key, value) catch return Error.BadKVs; + } + + const id_scheme = try IDScheme.init(kvs.get("id") orelse return Error.BadID); + const public_key = try id_scheme.publicKey(kvs.get(id_scheme.publicKeyKey()).?); + + // Verify the signature, streaming the signed data to the verifier + var sig_verifier = try public_key.verifier(sig); + + // signed_data_list = length_prefix + elements + { + const elements = list_reader.data[seq_pos..]; + var length_prefix_buf: [2]u8 = undefined; + var writer = RLPWriter.init(&length_prefix_buf); + writer.writeListLength(elements.len) catch unreachable; + const length_prefix = length_prefix_buf[0..writer.pos]; + + // write the length prefix + sig_verifier.update(length_prefix); + // write the elements + sig_verifier.update(elements); + + sig_verifier.verify() catch return Error.BadSignature; + } + } + + pub fn decodeIntoENR(self: *const Self, enr: *ENR) void { + var outer_reader = RLPReader.init(self.getData()); + const list_data = outer_reader.read(.{.long_list}) catch unreachable; + var list_reader = RLPReader.init(list_data); + + const sig = list_reader.read(.{.long_string}) catch unreachable; + @memcpy(&enr.signature, sig); + + const seq_bytes = list_reader.read(.{ .single_byte, .short_string }) catch unreachable; + enr.seq = std.mem.readVarInt(u64, seq_bytes, .big); + + enr.kvs = KVs.init(); + while (!list_reader.finished()) { + const key = list_reader.read(.{ .short_string, .long_string }) catch unreachable; + const value = list_reader.read(.{ .single_byte, .short_string, .long_string }) catch unreachable; + + enr.kvs.put(key, value) catch unreachable; + } + } + + pub fn decodeTxtInto(source: []const u8) !Self { + if (!std.mem.eql(u8, source[0..4], "enr:")) { + return Error.BadPrefix; + } + + const decoder = std.base64.url_safe_no_pad.Decoder; + const size = try decoder.calcSizeForSlice(source[4..]); + + if (size > max_enr_size) return Error.TooLong; + + var buffer: [max_enr_size]u8 = undefined; + try decoder.decode(buffer[0..size], source[4..]); + + return Self.init(buffer[0..size]); + } + + pub fn getIp(self: *const Self) !?std.net.Ip4Address { + if (self.get("ip")) |ip_bytes| { + if (ip_bytes.len != 4) return error.InvalidLength; + return std.net.Ip4Address.init(ip_bytes[0..4].*, 0); + } + return null; + } + + pub fn getIpStr(self: *const Self, out: []u8) !?[]const u8 { + if (self.get("ip")) |ip_bytes| { + if (ip_bytes.len != 4) return error.InvalidLength; + const formatted = try std.fmt.bufPrint(out, "{}.{}.{}.{}", .{ ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3] }); + return formatted; + } + return null; + } + + pub fn getUdp(self: *const Self) !?u16 { + if (self.get("udp")) |udp_bytes| { + if (udp_bytes.len != 2) return error.InvalidLength; + return std.mem.readInt(u16, udp_bytes[0..2], .big); + } + return null; + } + + pub fn getPublicKeyStr(self: *const Self, out: []u8, case: std.fmt.Case) ![]const u8 { + const pk = self.publicKey(); + const serialized = switch (pk) { + .v4 => |p| p.serialize(), + }; + + const public_key_hex = std.fmt.bytesToHex(serialized, case); + if (out.len < public_key_hex.len + 2) return error.BufferTooSmall; + @memcpy(out[0..2], "0x"); + @memcpy(out[2 .. public_key_hex.len + 2], public_key_hex[0..public_key_hex.len]); + return out[0 .. public_key_hex.len + 2]; + } + + pub fn getSignatureStr(self: *const Self, out: []u8, case: std.fmt.Case) ![]const u8 { + const signature_hex = std.fmt.bytesToHex(self.signature(), case); + if (out.len < signature_hex.len + 2) return error.BufferTooSmall; + @memcpy(out[0..2], "0x"); + @memcpy(out[2 .. signature_hex.len + 2], signature_hex[0..signature_hex.len]); + return out[0 .. signature_hex.len + 2]; + } +}; + +test "ENR test vector" { + const enr_txt = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"; + var private_key: [32]u8 = undefined; + _ = try std.fmt.hexToBytes(&private_key, "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"); + const kp = try secp256k1.SecretKey.fromSlice(&private_key); + + const public_key = kp.publicKey(secp256k1.getSecp256k1Context().*).serialize(); + var signature: [64]u8 = undefined; + _ = try std.fmt.hexToBytes(&signature, "7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c"); + + const seq: u64 = 1; + const id = "v4"; + const ip = "\x7f\x00\x00\x01"; + const udp = "\x76\x5f"; + + var decoded_enr: ENR = undefined; + try ENR.decodeTxtInto(&decoded_enr, enr_txt); + defer decoded_enr.deinit(); + + // std.debug.print("{any}\n", .{decoded_enr}); + // ensure all decoded values match the test vector + try std.testing.expectEqualSlices(u8, &signature, &decoded_enr.signature); + try std.testing.expectEqual(seq, decoded_enr.seq); + try std.testing.expectEqualSlices(u8, &public_key, decoded_enr.kvs.get("secp256k1").?); + try std.testing.expectEqualSlices(u8, id, decoded_enr.kvs.get("id").?); + try std.testing.expectEqualSlices(u8, ip, decoded_enr.kvs.get("ip").?); + try std.testing.expectEqualSlices(u8, udp, decoded_enr.kvs.get("udp").?); + var ip_out: [16]u8 = undefined; + try std.testing.expectEqualStrings("127.0.0.1", (try decoded_enr.getIpStr(&ip_out)).?); + try std.testing.expectEqual(30303, (try decoded_enr.getUdp()).?); + var public_key_buf: [100]u8 = undefined; + const public_key_out = try decoded_enr.getPublicKeyStr(&public_key_buf, .lower); + try std.testing.expectEqualSlices(u8, "0x03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138", public_key_out); + + try std.testing.expectEqual((try std.net.Address.parseIp4("127.0.0.1", 0)).in, (try decoded_enr.getIp()).?); + var sig_buf: [150]u8 = undefined; + const sig_out = try decoded_enr.getSignatureStr(&sig_buf, .lower); + try std.testing.expectEqualSlices(u8, "0x7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c", sig_out); + var expected_binary: [max_enr_size]u8 = undefined; + const decoder = std.base64.url_safe_no_pad.Decoder; + const expected_size = try decoder.calcSizeForSlice(enr_txt[4..]); + try decoder.decode(expected_binary[0..expected_size], enr_txt[4..]); + + var txt: [max_enr_size]u8 = undefined; + const len = decoded_enr.encodedLen(); + try decoded_enr.encodeInto(txt[0..len]); + try std.testing.expectEqualSlices(u8, txt[0..len], expected_binary[0..expected_size]); + + const expected_txt_len = decoded_enr.encodedTxtLen(); + const actual_txt_len = enr_txt.len; + try std.testing.expectEqual(actual_txt_len, expected_txt_len); + + var encoded_txt_buf: [1000]u8 = undefined; + const encoded_txt = try decoded_enr.encodeToTxt(&encoded_txt_buf); + try std.testing.expectEqualStrings(enr_txt, encoded_txt); + + var small_buf: [10]u8 = undefined; + const encode_result = decoded_enr.encodeToTxt(&small_buf); + try std.testing.expectError(error.BufferTooSmall, encode_result); + + var signable_enr = SignableENR.create(KeyPair{ .v4 = kp }); + defer signable_enr.deinit(); + signable_enr.seq = seq; + try signable_enr.set("ip", ip); + try signable_enr.set("udp", udp); + + try std.testing.expectEqualSlices(u8, signable_enr.get("id").?, decoded_enr.get("id").?); + try std.testing.expectEqualSlices(u8, signable_enr.get("ip").?, decoded_enr.get("ip").?); + try std.testing.expectEqualSlices(u8, signable_enr.get("udp").?, decoded_enr.get("udp").?); + try std.testing.expectEqual(signable_enr.seq, decoded_enr.seq); + try std.testing.expectEqualSlices(u8, signable_enr.kvs.get("secp256k1").?, decoded_enr.kvs.get("secp256k1").?); + var ip_out2: [16]u8 = undefined; + try std.testing.expectEqualStrings("127.0.0.1", (try signable_enr.getIpStr(&ip_out2)).?); + try std.testing.expectEqual(30303, (try signable_enr.getUdp()).?); + var public_key_buf2: [100]u8 = undefined; + const public_key_out2 = try signable_enr.getPublicKeyStr(&public_key_buf2, .lower); + try std.testing.expectEqualSlices(u8, "0x03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138", public_key_out2); + try std.testing.expectEqual((try std.net.Address.parseIp4("127.0.0.1", 0)).in, (try signable_enr.getIp()).?); + var sig_buf1: [150]u8 = undefined; + const sig_out2 = try signable_enr.signStr(&sig_buf1, .lower); + try std.testing.expectEqualSlices(u8, "0x7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c", sig_out2); + + const x = try signable_enr.sign(); + try std.testing.expectEqualSlices(u8, &signature, &x); + + const signable_txt_len = signable_enr.encodedTxtLen(); + var signable_txt_buf: [1000]u8 = undefined; + const signable_encoded_txt = try signable_enr.encodeToTxt(&signable_txt_buf); + + try std.testing.expectEqualStrings(enr_txt, signable_encoded_txt); + try std.testing.expectEqual(enr_txt.len, signable_txt_len); + + const encoded_enr = try EncodedENR.decodeTxtInto(enr_txt); + try std.testing.expectEqualStrings(&decoded_enr.signature, &encoded_enr.signature()); + try std.testing.expectEqual(decoded_enr.seq, encoded_enr.seq()); + try std.testing.expectEqual(decoded_enr.id(), encoded_enr.id()); + try std.testing.expectEqualSlices(u8, encoded_enr.get("ip").?, decoded_enr.get("ip").?); + try std.testing.expectEqualSlices(u8, encoded_enr.get("udp").?, decoded_enr.get("udp").?); + var ip_out3: [16]u8 = undefined; + try std.testing.expectEqualStrings("127.0.0.1", (try encoded_enr.getIpStr(&ip_out3)).?); + try std.testing.expectEqual(30303, (try encoded_enr.getUdp()).?); + var public_key_buf3: [100]u8 = undefined; + const public_key_out3 = try encoded_enr.getPublicKeyStr(&public_key_buf3, .lower); + try std.testing.expectEqualSlices(u8, "0x03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138", public_key_out3); + try std.testing.expectEqual((try std.net.Address.parseIp4("127.0.0.1", 0)).in, (try encoded_enr.getIp()).?); + var sig_buf3: [150]u8 = undefined; + const sig_out3 = try encoded_enr.getSignatureStr(&sig_buf3, .lower); + try std.testing.expectEqualSlices(u8, "0x7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c", sig_out3); +} diff --git a/src/rlp.zig b/src/rlp.zig new file mode 100644 index 0000000..453831f --- /dev/null +++ b/src/rlp.zig @@ -0,0 +1,296 @@ +// This code originally copy from https://github.com/ChainSafe/zig-discv5/blob/aa2cd1735a575b8ce663914ac6279ad0ba3c4ede/src/rlp.zig +// with bug fixes. +const std = @import("std"); + +pub const SingleByte = struct { + data: []const u8, +}; +pub const ShortString = struct { + data: []const u8, +}; +pub const LongString = struct { + data: []const u8, +}; +pub const ShortList = struct { + data: []const u8, +}; +pub const LongList = struct { + data: []const u8, +}; +pub const Element = union(enum) { + single_byte: SingleByte, + short_string: ShortString, + long_string: LongString, + short_list: ShortList, + long_list: LongList, +}; + +fn intBitLen(comptime T: type, num: T) u8 { + return std.math.log2_int(T, num) + 1; +} + +fn intByteLen(comptime T: type, num: T) u8 { + return (intBitLen(T, num) + 7) / 8; +} + +pub fn intLen(comptime T: type, num: T) usize { + comptime { + const type_info = @typeInfo(T); + if (type_info != .int or type_info.int.signedness != .unsigned) { + @compileError("T must be an unsigned integer type"); + } + } + + return if (num < 128) 1 else 1 + intByteLen(T, num); +} + +pub fn elemLen(byte_len: usize) usize { + if (byte_len < 56) { + return 1 + byte_len; + } else { + return 1 + intByteLen(usize, byte_len) + byte_len; + } +} + +pub const RLPWriter = struct { + data: []u8, + pos: usize, + + const WriteError = error{ + BufferTooSmall, + }; + + const Self = @This(); + + pub fn init(data: []u8) Self { + return .{ .data = data, .pos = 0 }; + } + + pub fn writeByte(self: *Self, byte: u8) WriteError!void { + if (self.pos >= self.data.len) { + return WriteError.BufferTooSmall; + } + + self.data[self.pos] = byte; + self.pos += 1; + } + + fn writeBytes(self: *Self, bytes: []const u8) WriteError!void { + if (self.pos + bytes.len > self.data.len) { + return WriteError.BufferTooSmall; + } + + std.mem.copyForwards(u8, self.data[self.pos..], bytes); + self.pos += bytes.len; + } + + fn writeIntRaw(self: *Self, comptime T: type, value: T, byte_len: u8) WriteError!void { + // const bytes_in_type = @divExact(@typeInfo(T).Int.bits, 8); + const bytes_in_type = 8; + var buffer = [_]u8{0} ** bytes_in_type; + std.mem.writeInt(u64, &buffer, @as(T, @intCast(value)), .big); + try self.writeBytes(buffer[bytes_in_type - byte_len ..]); + } + + fn writeLength(self: *Self, len: usize, prefix: u8) WriteError!void { + const bits = std.math.log2_int(usize, len) + 1; + const len_len = (bits + 7) / 8; + try self.writeByte(prefix + len_len); + + try self.writeIntRaw(usize, len, len_len); + } + + pub fn writeListLength(self: *Self, len: usize) WriteError!void { + if (len < 56) { + try self.writeByte(0xc0 + @as(u8, @intCast(len))); + } else { + try self.writeLength(len, 0xf7); + } + } + + pub fn writeString(self: *Self, data: []const u8) WriteError!void { + if (data.len < 56) { + try self.writeByte(0x80 + @as(u8, @intCast(data.len))); + } else { + try self.writeLength(data.len, 0xb7); + } + try self.writeBytes(data); + } + + pub fn writeInt(self: *Self, comptime T: type, value: T) WriteError!void { + if (value == 0) { + try self.writeByte(0x80); + } else if (value < 128) { + try self.writeByte(@as(u8, @intCast(value))); + } else { + const bits = std.math.log2_int(T, value) + 1; + const len = (bits + 7) / 8; + try self.writeByte(0x80 + @as(u8, @intCast(len))); + + try self.writeIntRaw(T, value, len); + } + } + + pub fn writeElement(self: *Self, element: Element) WriteError!void { + switch (element) { + .single_byte => |s| { + try self.writeByte(s.data[0]); + }, + .short_string => |s| { + try self.writeByte(0x80 + s.data.len); + try self.writeBytes(s.data); + }, + .long_string => |s| { + try self.writeLength(s.data.len, 0xb7); + try self.writeBytes(s.data); + }, + .short_list => |l| { + try self.writeByte(0xc0 + l.data.len); + try self.writeBytes(l.data); + }, + .long_list => |l| { + try self.writeLength(l.data.len, 0xf7); + try self.writeBytes(l.data); + }, + } + } +}; + +pub const RLPReader = struct { + data: []const u8, + pos: usize, + + const Self = @This(); + + pub const Error = error{ EndOfSlice, InvalidType }; + + pub fn init(data: []const u8) Self { + return .{ .data = data, .pos = 0 }; + } + + pub fn finished(self: *Self) bool { + return self.pos >= self.data.len; + } + + fn readByte(self: *Self) Error!u8 { + if (self.pos >= self.data.len) { + return Error.EndOfSlice; + } + const b = self.data[self.pos]; + self.pos += 1; + return b; + } + + fn readBytes(self: *Self, size: usize) Error![]const u8 { + const end_pos = self.pos + size; + if (end_pos > self.data.len) { + return Error.EndOfSlice; + } + const data = self.data[self.pos..end_pos]; + self.pos = end_pos; + return data; + } + + fn readLength(self: *Self, len_len: u8) Error!u32 { + if (len_len > 8) { + return Error.EndOfSlice; + } + const len_bytes = try self.readBytes(len_len); + return std.mem.readVarInt(u32, len_bytes, .big); + } + + pub fn readString(self: *Self) Error![]const u8 { + const b = try self.readByte(); + if (b < 0x80) { + return Error.InvalidType; + } else if (b < 0xb8) { + const size = b - 0x80; + return try self.readBytes(size); + } else if (b < 0xc0) { + const size_len = b - 0xb7; + const size = try self.readLength(size_len); + return try self.readBytes(size); + } else { + return Error.InvalidType; + } + } + + pub fn readList(self: *Self) Error![]const u8 { + const b = try self.readByte(); + if (b < 0xc0) { + return Error.InvalidType; + } else if (b < 0xf8) { + const size = b - 0xc0; + return try self.readBytes(size); + } else { + const size_len = b - 0xf7; + const size = try self.readLength(size_len); + return try self.readBytes(size); + } + } + + /// Read the next rlp element, limited to the provided `element_types`. + /// + /// Will error if the next element is not one of the provided types + pub fn read(self: *Self, comptime element_types: anytype) Error![]const u8 { + comptime { + for (element_types) |element_type| { + switch (element_type) { + .single_byte, .short_string, .long_string, .short_list, .long_list => {}, + else => @compileError("Invalid element_type"), + } + } + } + const b = try self.readByte(); + inline for (element_types) |element_type| { + switch (element_type) { + .single_byte => if (b < 0x80) return self.data[self.pos - 1 .. self.pos], + .short_string => if (b >= 0x80 and b < 0xb8) return self.readBytes(b - 0x80), + .long_string => if (b >= 0xb8 and b < 0xc0) return self.readBytes(try self.readLength(b - 0xb7)), + .short_list => if (b >= 0xc0 and b < 0xf8) return self.readBytes(b - 0xc0), + .long_list => if (b >= 0xf8) return self.readBytes(try self.readLength(b - 0xf7)), + else => return Error.InvalidType, + } + } + return Error.InvalidType; + } + + pub fn readElement(self: *Self) Error!Element { + const b = try self.readByte(); + if (b < 0x80) { + return Element{ .single_byte = .{ .data = self.data[self.pos - 1 .. self.pos] } }; + } else if (b < 0xb8) { + const size = b - 0x80; + const data = try self.readBytes(size); + return Element{ .short_string = .{ .data = data } }; + } else if (b < 0xc0) { + const size_len = b - 0xb7; + const size = try self.readLength(size_len); + const data = try self.readBytes(size); + return Element{ .long_string = .{ .data = data } }; + } else if (b < 0xf8) { + const size = b - 0xc0; + const data = try self.readBytes(size); + return Element{ .short_list = .{ .data = data } }; + } else { + const size_len = b - 0xf7; + const size = try self.readLength(size_len); + const data = try self.readBytes(size); + return Element{ .long_list = .{ .data = data } }; + } + } + pub fn next(self: *Self) ?Element { + return self.readElement() catch null; + } +}; + +test "RLPReader" { + var buf = [_]u8{0} ** 128; + var writer = RLPWriter.init(&buf); + try writer.writeString("hello"); + try writer.writeString("world"); + + var reader = RLPReader.init(&buf); + _ = try reader.read(.{.short_string}); + _ = try reader.read(.{.short_string}); +} diff --git a/src/root.zig b/src/root.zig index acce2e2..c28f88c 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,5 +1,6 @@ const std = @import("std"); -const enrlib = @import("enr"); +const enrlib = @import("enr.zig"); +const secp256k1 = @import("secp256k1.zig"); /// ENR (Ethereum Node Record) - A fully parsed and validated ENR for network operations. /// @@ -23,6 +24,13 @@ pub const EncodedENR = enrlib.EncodedENR; pub const SignableENR = enrlib.SignableENR; pub const KeyPair = enrlib.KeyPair; +/// Deinitializes the global context for secp256k1 operations. +/// It should be called in your main function before the program exits. +pub const deinitGlobalSecp256k1Ctx = secp256k1.deinitSecp256k1Context; + +/// Returns a pointer to the global context for secp256k1 operations. +pub const getGlobalSecp256k1Ctx = secp256k1.getSecp256k1Context; + const enr_prefix = "enr:"; /// ENR file I/O error set pub const ENRFileError = error{ diff --git a/src/secp256k1.zig b/src/secp256k1.zig new file mode 100644 index 0000000..c8b39c0 --- /dev/null +++ b/src/secp256k1.zig @@ -0,0 +1,89 @@ +const std = @import("std"); +const secp256k1 = @import("secp256k1"); + +pub const SecretKey = secp256k1.SecretKey; +pub const PublicKey = secp256k1.PublicKey; +pub const Message = secp256k1.Message; +pub const Signature = secp256k1.ecdsa.Signature; +pub const Secp256k1 = secp256k1.Secp256k1; +pub const digest_size = 32; + +/// Global context for secp256k1 operations +var global_secp_ctx: ?Secp256k1 = null; +var secp_once = std.once(initSecp256k1Context); + +/// Initializes the global context for secp256k1 operations. +fn initSecp256k1Context() void { + global_secp_ctx = Secp256k1.genNew(); +} + +/// Deinitializes the global context for secp256k1 operations. +pub fn deinitSecp256k1Context() void { + if (global_secp_ctx) |*ctx| { + ctx.deinit(); + global_secp_ctx = null; + } +} + +/// Returns a pointer to the global context for secp256k1 operations. +pub fn getSecp256k1Context() *Secp256k1 { + secp_once.call(); + return &global_secp_ctx.?; +} + +/// A streaming verifier for secp256k1 signatures +pub const Verifier = struct { + hasher: std.crypto.hash.sha3.Keccak256, + signature: secp256k1.ecdsa.Signature, + public_key: PublicKey, + + pub fn init(signature: Signature, public_key: PublicKey) Verifier { + return Verifier{ + .hasher = std.crypto.hash.sha3.Keccak256.init(.{}), + .signature = signature, + .public_key = public_key, + }; + } + + pub fn update(self: *Verifier, data: []const u8) void { + self.hasher.update(data); + } + + pub fn verify(self: *Verifier) !void { + var hash: [digest_size]u8 = undefined; + self.hasher.final(&hash); + + const message = secp256k1.Message.fromDigest(hash); + + try getSecp256k1Context().verifyEcdsa(message, self.signature, self.public_key); + } +}; + +test "secp256k1 verify" { + // taken from enr test vector + var data_buffer: [1000]u8 = undefined; + var private_key_buffer: [32]u8 = undefined; + var public_key_buffer: [64]u8 = undefined; + var signature_buffer: [64]u8 = undefined; + const data_hex = "f84201826964827634826970847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31388375647082765f"; + const private_key_hex = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"; + const public_key_hex = "03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138"; + const signature_hex = "7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c"; + + const data = try std.fmt.hexToBytes(&data_buffer, data_hex); + const private_key = try std.fmt.hexToBytes(&private_key_buffer, private_key_hex); + const public_key = try std.fmt.hexToBytes(&public_key_buffer, public_key_hex); + const signature = try std.fmt.hexToBytes(&signature_buffer, signature_hex); + + const sk = try SecretKey.fromSlice(private_key); + const pk = try PublicKey.fromSlice(public_key); + const sig = try Signature.fromCompact(signature); + + var hash: [digest_size]u8 = undefined; + std.crypto.hash.sha3.Keccak256.hash(data, &hash, .{}); + const msg = secp256k1.Message.fromDigest(hash); + const sig2 = getSecp256k1Context().signEcdsa(&msg, &sk); + + try getSecp256k1Context().verifyEcdsa(msg, sig, pk); + try std.testing.expectEqualSlices(u8, &sig2.serializeCompact(), &sig.serializeCompact()); +} From de1e4b10995f8b142754a58babde098da610fe64 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Sun, 31 Aug 2025 12:20:50 +0800 Subject: [PATCH 13/14] fix:replace setup-zig Signed-off-by: Chen Kai <281165273grape@gmail.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d54c63..b28831e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,9 +18,9 @@ jobs: - uses: actions/checkout@v4 - name: Set up Zig - uses: korandoru/setup-zig@v1 + uses: mlugg/setup-zig@v2.0.5 with: - zig-version: 0.14.1 + version: 0.14.1 - name: Formatting run: zig fmt --check --color on . From 702fc2262cad4d387ef62385569534f8e20e499e Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Sun, 31 Aug 2025 15:44:17 +0800 Subject: [PATCH 14/14] feat: add helper function for SignableENR Signed-off-by: Chen Kai <281165273grape@gmail.com> --- src/enr.zig | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/enr.zig b/src/enr.zig index 62925dd..828d59c 100644 --- a/src/enr.zig +++ b/src/enr.zig @@ -141,6 +141,16 @@ pub const IDScheme = enum { pub const KeyPair = union(IDScheme) { v4: secp256k1.SecretKey, + pub fn fromSecretKeyString(secret: []const u8) !KeyPair { + const key = try secp256k1.SecretKey.fromString(secret); + return KeyPair{ .v4 = key }; + } + + pub fn fromSecretKeySlice(slice: []const u8) !KeyPair { + const key = try secp256k1.SecretKey.fromSlice(slice); + return KeyPair{ .v4 = key }; + } + pub fn generate() KeyPair { return KeyPair{ .v4 = secp256k1.SecretKey.generate() }; } @@ -434,6 +444,14 @@ pub const SignableENR = struct { return SignableENR{ .kp = key_pair, .kvs = kvs, .seq = 0 }; } + pub fn fromSecretKeyString(key: []const u8) !SignableENR { + return SignableENR.create(try KeyPair.fromSecretKeyString(key)); + } + + pub fn fromSecretKeySlice(key: []const u8) !SignableENR { + return SignableENR.create(try KeyPair.fromSecretKeySlice(key)); + } + pub fn get(self: *Self, key: []const u8) ?[]const u8 { return self.kvs.get(key); } @@ -939,6 +957,27 @@ test "ENR test vector" { try std.testing.expectEqualStrings(enr_txt, signable_encoded_txt); try std.testing.expectEqual(enr_txt.len, signable_txt_len); + const kp1 = try KeyPair.fromSecretKeyString("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"); + var signable_enr1 = SignableENR.create(kp1); + defer signable_enr1.deinit(); + signable_enr1.seq = seq; + try signable_enr1.set("ip", ip); + try signable_enr1.set("udp", udp); + + try std.testing.expectEqualSlices(u8, signable_enr.get("id").?, decoded_enr.get("id").?); + try std.testing.expectEqualSlices(u8, signable_enr.get("ip").?, decoded_enr.get("ip").?); + try std.testing.expectEqualSlices(u8, signable_enr.get("udp").?, decoded_enr.get("udp").?); + + var signable_enr2 = try SignableENR.fromSecretKeyString("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"); + defer signable_enr2.deinit(); + signable_enr2.seq = seq; + try signable_enr2.set("ip", ip); + try signable_enr2.set("udp", udp); + + try std.testing.expectEqualSlices(u8, signable_enr2.get("id").?, decoded_enr.get("id").?); + try std.testing.expectEqualSlices(u8, signable_enr2.get("ip").?, decoded_enr.get("ip").?); + try std.testing.expectEqualSlices(u8, signable_enr2.get("udp").?, decoded_enr.get("udp").?); + const encoded_enr = try EncodedENR.decodeTxtInto(enr_txt); try std.testing.expectEqualStrings(&decoded_enr.signature, &encoded_enr.signature()); try std.testing.expectEqual(decoded_enr.seq, encoded_enr.seq());