From fb3d949b72b4fddf235d1a15e03f292a4964380d Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Sun, 24 Aug 2025 16:29:20 +0800 Subject: [PATCH 01/11] fix:using plain map to replace the small buf map Signed-off-by: Chen Kai <281165273grape@gmail.com> --- src/enr.zig | 79 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/src/enr.zig b/src/enr.zig index 02d2e74..2d61f00 100644 --- a/src/enr.zig +++ b/src/enr.zig @@ -1,7 +1,6 @@ const std = @import("std"); const rlp = @import("rlp.zig"); -const SmallBufMap = @import("small_buf_map.zig").SmallBufMap; const RLPReader = rlp.RLPReader; const RLPWriter = rlp.RLPWriter; @@ -17,7 +16,58 @@ 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; -pub const KVs = SmallBufMap(max_kvs_size); + +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 { + // Free all keys and values + 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 { + // Copy key and value to owned memory + 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 { + inner: std.StringHashMap([]const u8).Iterator, + + pub fn next(self: *Iterator) ?struct { key: []const u8, value: []const u8 } { + if (self.inner.next()) |entry| { + return .{ .key = entry.key_ptr.*, .value = entry.value_ptr.* }; + } + return null; + } + }; + + pub fn iterator(self: *const KVs) Iterator { + return Iterator{ .inner = self.map.iterator() }; + } +}; pub const IDScheme = enum { v4, @@ -132,6 +182,10 @@ pub const ENR = struct { 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); } @@ -231,6 +285,10 @@ pub const SignableENR = struct { 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) { @@ -299,8 +357,8 @@ fn encodeSignedPayload(out: []u8, kvs: *KVs, seq: u64) !void { var kvs_it = kvs.iterator(); while (kvs_it.next()) |entry| { - try writer.writeString(entry[0]); - try writer.writeString(entry[1]); + try writer.writeString(entry.key); + try writer.writeString(entry.value); } } @@ -334,8 +392,8 @@ fn kvsLen(kvs: *KVs) usize { var length: usize = 0; var it = kvs.iterator(); while (it.next()) |entry| { - length += rlp.elemLen(entry[0].len); - length += rlp.elemLen(entry[1].len); + length += rlp.elemLen(entry.key.len); + length += rlp.elemLen(entry.value.len); } return length; } @@ -411,7 +469,7 @@ pub const EncodedENR = struct { pub fn publicKey(self: *const Self) PublicKey { const id_scheme = self.id(); - return id_scheme.publicKey(self.kvs.get(id_scheme.publicKeyKey()).?) catch unreachable; + return id_scheme.publicKey(self.get(id_scheme.publicKeyKey()).?) catch unreachable; } pub fn nodeId(self: *const Self) NodeId { @@ -443,6 +501,7 @@ pub const EncodedENR = struct { // - 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 }); @@ -524,6 +583,7 @@ test "ENR test vector" { 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 @@ -535,11 +595,14 @@ test "ENR test vector" { try std.testing.expectEqualSlices(u8, udp, decoded_enr.kvs.get("udp").?); 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.kvs.buffer, &decoded_enr.kvs.buffer); + 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 signable_enr.sign(); // try std.testing.expectEqualSlices(u8, signature, &x); From 5e2c2322b20458c616b2bfabb513257cfe4a7dc9 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Mon, 25 Aug 2025 17:40:02 +0800 Subject: [PATCH 02/11] feat:replace secp256k1 to bitcoin secp256k1 Signed-off-by: Chen Kai <281165273grape@gmail.com> --- build.zig | 10 +++-- build.zig.zon | 6 ++- src/enr.zig | 105 +++++++++++++++++++++++++++++++++------------- src/secp256k1.zig | 79 +++++++++++++++++++++++++++------- 4 files changed, 152 insertions(+), 48 deletions(-) diff --git a/build.zig b/build.zig index 5a2a40e..a77b710 100644 --- a/build.zig +++ b/build.zig @@ -10,6 +10,8 @@ pub fn build(b: *std.Build) void { const dep_xev = b.dependency("xev", .{}); + const secp256k1 = b.dependency("secp256k1", .{}); + const module_rlp = b.createModule(.{ .root_source_file = b.path("src/rlp.zig"), .target = target, @@ -22,6 +24,8 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); + module_enr.linkLibrary(secp256k1.artifact("libsecp")); + module_enr.addImport("secp256k1", secp256k1.module("secp256k1")); b.modules.put(b.dupe("enr"), module_enr) catch @panic("OOM"); const tls_run_test = b.step("test", "Run all tests"); @@ -29,7 +33,7 @@ pub fn build(b: *std.Build) void { const test_rlp = b.addTest(.{ .name = "rlp", .root_module = module_rlp, - .filters = &[_][]const u8{ }, + .filters = &[_][]const u8{}, }); const install_test_rlp = b.addInstallArtifact(test_rlp, .{}); const tls_install_test_rlp = b.step("build-test:rlp", "Install the rlp test"); @@ -43,7 +47,7 @@ pub fn build(b: *std.Build) void { const test_enr = b.addTest(.{ .name = "enr", .root_module = module_enr, - .filters = &[_][]const u8{ }, + .filters = &[_][]const u8{}, }); const install_test_enr = b.addInstallArtifact(test_enr, .{}); const tls_install_test_enr = b.step("build-test:enr", "Install the enr test"); @@ -64,7 +68,7 @@ pub fn build(b: *std.Build) void { const @"test_enr-bench" = b.addTest(.{ .name = "enr-bench", .root_module = @"module_enr-bench", - .filters = &[_][]const u8{ }, + .filters = &[_][]const u8{}, }); const @"install_test_enr-bench" = b.addInstallArtifact(@"test_enr-bench", .{}); const @"tls_install_test_enr-bench" = b.step("build-test:enr-bench", "Install the enr-bench test"); diff --git a/build.zig.zon b/build.zig.zon index 4766d37..f238a1e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -14,6 +14,10 @@ .url = "git+https://github.com/mitchellh/libxev#94ed6af7b2aaaeab987fbf87fcee410063df715b", .hash = "libxev-0.0.0-86vtc-zkEgB7uv1i0Sa6ytJETZQi_lHJrImu9mLb9moi", }, + .secp256k1 = .{ + .url = "git+https://github.com/zig-bitcoin/libsecp256k1-zig#b96922b375a601f3303bf43be1dceb3d101df6c6", + .hash = "secp256k1-0.0.0-AAAAAPPgAAADPOkS7brnO1B26LjbhZsxwz6Kx8a6jCwB", + }, }, .paths = .{ "build.zig", "build.zig.zon", "src" }, -} \ No newline at end of file +} diff --git a/src/enr.zig b/src/enr.zig index 2d61f00..2232f47 100644 --- a/src/enr.zig +++ b/src/enr.zig @@ -6,8 +6,9 @@ const RLPReader = rlp.RLPReader; const RLPWriter = rlp.RLPWriter; const Keccak = std.crypto.hash.sha3.Keccak256; -const Secp256k1 = @import("secp256k1.zig").Secp256k1; +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 @@ -17,7 +18,8 @@ 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; -pub const KVs = struct { +// Conditional compilation: use StringHashMap on macOS, SmallBufMap on other platforms.Because test not working on macOS. +pub const KVs = if (@import("builtin").target.os.tag == .macos) struct { map: std.StringHashMap([]const u8), allocator: std.mem.Allocator, @@ -29,7 +31,6 @@ pub const KVs = struct { } pub fn deinit(self: *KVs) void { - // Free all keys and values var it = self.map.iterator(); while (it.next()) |entry| { self.allocator.free(entry.key_ptr.*); @@ -39,7 +40,6 @@ pub const KVs = struct { } pub fn put(self: *KVs, key: []const u8, value: []const u8) !void { - // Copy key and value to owned memory 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); @@ -54,19 +54,53 @@ pub const KVs = struct { } pub const Iterator = struct { - inner: std.StringHashMap([]const u8).Iterator, + keys: std.ArrayList([]const u8), + map: *const std.StringHashMap([]const u8), + index: usize, - pub fn next(self: *Iterator) ?struct { key: []const u8, value: []const u8 } { - if (self.inner.next()) |entry| { - return .{ .key = entry.key_ptr.*, .value = entry.value_ptr.* }; + 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; } - return null; + + 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) ?struct { key: []const u8, value: []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 .{ .key = key, .value = value }; + } + + pub fn deinit(self: *Iterator) void { + self.keys.deinit(); } }; pub fn iterator(self: *const KVs) Iterator { - return Iterator{ .inner = self.map.iterator() }; + return Iterator.init(self); } +} else blk: { + const SmallBufMap = @import("small_buf_map.zig").SmallBufMap; + break :blk SmallBufMap(max_kvs_size); }; pub const IDScheme = enum { @@ -89,7 +123,7 @@ pub const IDScheme = enum { pub fn publicKey(id: IDScheme, value: []const u8) Error!PublicKey { switch (id) { .v4 => { - return PublicKey{ .v4 = Secp256k1.PublicKey.fromSec1(value) catch return Error.BadPubkey }; + return PublicKey{ .v4 = secp256k1.PublicKey.fromSlice(value) catch return Error.BadPubkey }; }, } } @@ -104,13 +138,17 @@ pub const IDScheme = enum { }; pub const KeyPair = union(IDScheme) { - v4: Secp256k1.KeyPair, + v4: secp256k1.SecretKey, pub fn sign(self: KeyPair, data: []const u8) ![signature_size]u8 { switch (self) { .v4 => |kp| { - const s = try kp.sign(data, null); - return s.toBytes(); + 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(); }, } } @@ -118,19 +156,19 @@ pub const KeyPair = union(IDScheme) { pub fn publicKey(self: KeyPair) PublicKey { switch (self) { .v4 => |kp| { - return PublicKey{ .v4 = kp.public_key }; + return PublicKey{ .v4 = kp.publicKey() }; }, } } }; pub const PublicKey = union(IDScheme) { - v4: Secp256k1.PublicKey, + v4: secp256k1.PublicKey, pub fn init(id: IDScheme, data: []const u8) !PublicKey { switch (id) { .v4 => { - return try Secp256k1.PublicKey.fromSec1(data); + return PublicKey{ .v4 = try secp256k1.PublicKey.fromSlice(data) }; }, } } @@ -138,17 +176,18 @@ pub const PublicKey = union(IDScheme) { pub fn verify(self: PublicKey, data: []const u8, signature: []const u8) Error!void { switch (self) { .v4 => |pk| { - const sig = Secp256k1.Signature.fromBytes(signature[0..signature_size].*); - return sig.verify(data, pk) catch return Error.BadSignature; + 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 { + pub fn verifier(self: PublicKey, signature: []const u8) Error!secp256k1.Verifier { switch (self) { .v4 => |pk| { - const sig = Secp256k1.Signature.fromBytes(signature[0..signature_size].*); - return sig.verifier(pk) catch return Error.BadSignature; + return secp256k1.Verifier.init(secp256k1.Signature.fromCompact(signature) catch return error.BadSignature, pk); }, } } @@ -157,7 +196,7 @@ pub const PublicKey = union(IDScheme) { switch (self) { .v4 => |pk| { var node_id: NodeId = undefined; - Keccak.hash(pk.toUncompressedSec1(), &node_id, .{}); + Keccak.hash(&pk.serializeUncompressed(), &node_id, .{}); return node_id; }, } @@ -294,7 +333,7 @@ pub const SignableENR = struct { switch (key_pair) { .v4 => |kp| { kvs.put("id", "v4") catch unreachable; - kvs.put("secp256k1", &kp.public_key.toCompressedSec1()) catch unreachable; + kvs.put("secp256k1", &kp.publicKey(secp256k1.getSecp256k1Context().*).serialize()) catch unreachable; }, } return SignableENR{ .kp = key_pair, .kvs = kvs, .seq = 0 }; @@ -344,6 +383,8 @@ fn encodeIntoFromComponents(out: []u8, kvs: *KVs, seq: u64, signature: [signatur try writer.writeInt(u64, seq); var kvs_it = kvs.iterator(); + defer if (@import("builtin").target.os.tag == .macos) kvs_it.deinit(); + while (kvs_it.next()) |entry| { try writer.writeString(entry.key); try writer.writeString(entry.value); @@ -356,6 +397,8 @@ fn encodeSignedPayload(out: []u8, kvs: *KVs, seq: u64) !void { try writer.writeInt(u64, seq); var kvs_it = kvs.iterator(); + defer if (@import("builtin").target.os.tag == .macos) kvs_it.deinit(); + while (kvs_it.next()) |entry| { try writer.writeString(entry.key); try writer.writeString(entry.value); @@ -391,6 +434,8 @@ fn signedListLen(kvs: *KVs, seq: u64) usize { fn kvsLen(kvs: *KVs) usize { var length: usize = 0; var it = kvs.iterator(); + defer if (@import("builtin").target.os.tag == .macos) it.deinit(); + while (it.next()) |entry| { length += rlp.elemLen(entry.key.len); length += rlp.elemLen(entry.value.len); @@ -573,8 +618,9 @@ const hex = @import("hex.zig").hex; test "ENR test vector" { const enr_txt = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"; const private_key = try hex("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"); - const kp = try Secp256k1.KeyPair.fromSecretKey(try Secp256k1.SecretKey.fromBytes(private_key)); - const public_key = kp.public_key.toCompressedSec1(); + const kp = try secp256k1.SecretKey.fromSlice(&private_key); + + const public_key = kp.publicKey(secp256k1.getSecp256k1Context().*).serialize(); const signature = try hex("7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c"); const seq: u64 = 1; const id = "v4"; @@ -603,9 +649,11 @@ test "ENR test vector" { 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").?); - _ = try signable_enr.sign(); - // try std.testing.expectEqualSlices(u8, signature, &x); + const x = try signable_enr.sign(); + try std.testing.expectEqualSlices(u8, &signature, &x); var encoded_buffer: [max_enr_size]u8 = undefined; const encoded_enr = try EncodedENR.decodeTxtInto(&encoded_buffer, enr_txt); @@ -614,5 +662,4 @@ test "ENR test vector" { 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, decoded_enr.get("ip").?, encoded_enr.get("ip").?); } diff --git a/src/secp256k1.zig b/src/secp256k1.zig index bc146ed..ee88264 100644 --- a/src/secp256k1.zig +++ b/src/secp256k1.zig @@ -1,8 +1,61 @@ const std = @import("std"); +const secp256k1 = @import("secp256k1"); -pub const Secp256k1 = std.crypto.sign.ecdsa.Ecdsa(std.crypto.ecc.Secp256k1, std.crypto.hash.sha3.Keccak256); +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; -test "secp256k1 - verify" { +var global_secp_ctx: ?Secp256k1 = null; +var secp_once = std.once(initSecp256k1Context); + +fn initSecp256k1Context() void { + global_secp_ctx = Secp256k1.genNew(); +} + +fn deinitSecp256k1Context() void { + if (global_secp_ctx) |*ctx| { + ctx.deinit(); + global_secp_ctx = null; + } +} + +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; @@ -18,19 +71,15 @@ test "secp256k1 - verify" { 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 Secp256k1.KeyPair.fromSecretKey(try Secp256k1.SecretKey.fromBytes(private_key[0..32].*)); - const pk = try Secp256k1.PublicKey.fromSec1(public_key); - const sig = Secp256k1.Signature.fromBytes(signature[0..64].*); - - const sig2 = try sk.sign(data, null); - const x = sig2; - _ = x; + const sk = try SecretKey.fromSlice(private_key); + const pk = try PublicKey.fromSlice(public_key); + const sig = try Signature.fromCompact(signature); - try sig.verify(data, pk); - try std.testing.expectEqualSlices(u8, &pk.toCompressedSec1(), &sk.public_key.toCompressedSec1()); + 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); - // Signatures don't match because zig std and enr test vectors use different algos: - // - zig std https://www.ietf.org/archive/id/draft-mattsson-cfrg-det-sigs-with-noise-04.html#name-updates-to-rfc-8032-eddsa - // - enr test vectors https://www.rfc-editor.org/rfc/rfc6979.txt - // try std.testing.expectEqualSlices(u8, &sig2.toBytes(), &sig.toBytes()); + try getSecp256k1Context().verifyEcdsa(msg, sig, pk); + try std.testing.expectEqualSlices(u8, &sig2.serializeCompact(), &sig.serializeCompact()); } From 63d87d3822abd05c01393770122d332aca0062a7 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Mon, 25 Aug 2025 18:34:17 +0800 Subject: [PATCH 03/11] fix:fix linux compile error Signed-off-by: Chen Kai <281165273grape@gmail.com> --- src/enr.zig | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/enr.zig b/src/enr.zig index 2232f47..45a00f2 100644 --- a/src/enr.zig +++ b/src/enr.zig @@ -80,14 +80,14 @@ pub const KVs = if (@import("builtin").target.os.tag == .macos) struct { }; } - pub fn next(self: *Iterator) ?struct { key: []const u8, value: []const u8 } { + 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 .{ .key = key, .value = value }; + return [2][]const u8{ key, value }; } pub fn deinit(self: *Iterator) void { @@ -222,7 +222,7 @@ pub const ENR = struct { signature: [signature_size]u8, pub fn deinit(self: *ENR) void { - self.kvs.deinit(); + if (@import("builtin").target.os.tag == .macos) self.kvs.deinit(); } pub fn get(self: *ENR, key: []const u8) ?[]const u8 { @@ -325,7 +325,7 @@ pub const SignableENR = struct { const Self = @This(); pub fn deinit(self: *Self) void { - self.kvs.deinit(); + if (@import("builtin").target.os.tag == .macos) self.kvs.deinit(); } pub fn create(key_pair: KeyPair) SignableENR { @@ -386,8 +386,8 @@ fn encodeIntoFromComponents(out: []u8, kvs: *KVs, seq: u64, signature: [signatur defer if (@import("builtin").target.os.tag == .macos) kvs_it.deinit(); while (kvs_it.next()) |entry| { - try writer.writeString(entry.key); - try writer.writeString(entry.value); + try writer.writeString(entry[0]); + try writer.writeString(entry[1]); } } @@ -400,8 +400,8 @@ fn encodeSignedPayload(out: []u8, kvs: *KVs, seq: u64) !void { defer if (@import("builtin").target.os.tag == .macos) kvs_it.deinit(); while (kvs_it.next()) |entry| { - try writer.writeString(entry.key); - try writer.writeString(entry.value); + try writer.writeString(entry[0]); + try writer.writeString(entry[1]); } } @@ -437,8 +437,8 @@ fn kvsLen(kvs: *KVs) usize { defer if (@import("builtin").target.os.tag == .macos) it.deinit(); while (it.next()) |entry| { - length += rlp.elemLen(entry.key.len); - length += rlp.elemLen(entry.value.len); + length += rlp.elemLen(entry[0].len); + length += rlp.elemLen(entry[1].len); } return length; } @@ -546,7 +546,7 @@ pub const EncodedENR = struct { // - keys must be unique // - keys must be sorted var kvs = KVs.init(); - defer kvs.deinit(); + defer if (@import("builtin").target.os.tag == .macos) 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 }); From b8332a84cff4d6ac6c3ac07b8edaae42d683273e Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Mon, 25 Aug 2025 18:46:46 +0800 Subject: [PATCH 04/11] fix:smallbufmap not works on linux too Signed-off-by: Chen Kai <281165273grape@gmail.com> --- src/enr.zig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/enr.zig b/src/enr.zig index 45a00f2..4ba1f54 100644 --- a/src/enr.zig +++ b/src/enr.zig @@ -19,7 +19,7 @@ pub const max_kvs_size = max_enr_size - signature_size - 7; pub const max_kvs = max_kvs_size / 3; // Conditional compilation: use StringHashMap on macOS, SmallBufMap on other platforms.Because test not working on macOS. -pub const KVs = if (@import("builtin").target.os.tag == .macos) struct { +pub const KVs = if (@import("builtin").target.os.tag == .macos or @import("builtin").target.os.tag == .linux) struct { map: std.StringHashMap([]const u8), allocator: std.mem.Allocator, @@ -222,7 +222,7 @@ pub const ENR = struct { signature: [signature_size]u8, pub fn deinit(self: *ENR) void { - if (@import("builtin").target.os.tag == .macos) self.kvs.deinit(); + if (@import("builtin").target.os.tag == .macos or @import("builtin").target.os.tag == .linux) self.kvs.deinit(); } pub fn get(self: *ENR, key: []const u8) ?[]const u8 { @@ -325,7 +325,7 @@ pub const SignableENR = struct { const Self = @This(); pub fn deinit(self: *Self) void { - if (@import("builtin").target.os.tag == .macos) self.kvs.deinit(); + if (@import("builtin").target.os.tag == .macos or @import("builtin").target.os.tag == .linux) self.kvs.deinit(); } pub fn create(key_pair: KeyPair) SignableENR { @@ -383,7 +383,7 @@ fn encodeIntoFromComponents(out: []u8, kvs: *KVs, seq: u64, signature: [signatur try writer.writeInt(u64, seq); var kvs_it = kvs.iterator(); - defer if (@import("builtin").target.os.tag == .macos) kvs_it.deinit(); + defer if (@import("builtin").target.os.tag == .macos or @import("builtin").target.os.tag == .linux) kvs_it.deinit(); while (kvs_it.next()) |entry| { try writer.writeString(entry[0]); @@ -397,7 +397,7 @@ fn encodeSignedPayload(out: []u8, kvs: *KVs, seq: u64) !void { try writer.writeInt(u64, seq); var kvs_it = kvs.iterator(); - defer if (@import("builtin").target.os.tag == .macos) kvs_it.deinit(); + defer if (@import("builtin").target.os.tag == .macos or @import("builtin").target.os.tag == .linux) kvs_it.deinit(); while (kvs_it.next()) |entry| { try writer.writeString(entry[0]); @@ -434,7 +434,7 @@ fn signedListLen(kvs: *KVs, seq: u64) usize { fn kvsLen(kvs: *KVs) usize { var length: usize = 0; var it = kvs.iterator(); - defer if (@import("builtin").target.os.tag == .macos) it.deinit(); + defer if (@import("builtin").target.os.tag == .macos or @import("builtin").target.os.tag == .linux) it.deinit(); while (it.next()) |entry| { length += rlp.elemLen(entry[0].len); @@ -546,7 +546,7 @@ pub const EncodedENR = struct { // - keys must be unique // - keys must be sorted var kvs = KVs.init(); - defer if (@import("builtin").target.os.tag == .macos) kvs.deinit(); + defer if (@import("builtin").target.os.tag == .macos or @import("builtin").target.os.tag == .linux) 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 }); From fa566e7979ab7ef2fa69358215d0de92ffd19635 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Tue, 26 Aug 2025 11:46:03 +0800 Subject: [PATCH 05/11] feat:make encoded enr own data Signed-off-by: Chen Kai <281165273grape@gmail.com> --- src/enr.zig | 45 ++++++++++++++++++++++++++++++--------------- src/enr_bench.zig | 3 +-- src/secp256k1.zig | 2 +- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/enr.zig b/src/enr.zig index 4ba1f54..f82b88f 100644 --- a/src/enr.zig +++ b/src/enr.zig @@ -457,19 +457,31 @@ pub fn decodeTxtIntoRlp(dest: []u8, source: []const u8) ![]u8 { /// Methods assume that data is a valid RLP-encoded ENR pub const EncodedENR = struct { - data: []const u8, + 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 { - const self = Self{ .data = data }; + 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) []const u8 { - var outer_reader = RLPReader.init(self.data); + var outer_reader = RLPReader.init(self.getData()); const list_data = outer_reader.read(.{.long_list}) catch unreachable; var list_reader = RLPReader.init(list_data); @@ -477,7 +489,7 @@ pub const EncodedENR = struct { } pub fn seq(self: *const Self) u64 { - var outer_reader = RLPReader.init(self.data); + var outer_reader = RLPReader.init(self.getData()); const list_data = outer_reader.read(.{.long_list}) catch unreachable; var list_reader = RLPReader.init(list_data); @@ -488,7 +500,7 @@ pub const EncodedENR = struct { } pub fn get(self: *const Self, key: []const u8) ?[]const u8 { - var outer_reader = RLPReader.init(self.data); + var outer_reader = RLPReader.init(self.getData()); const list_data = outer_reader.read(.{.long_list}) catch unreachable; var list_reader = RLPReader.init(list_data); @@ -522,7 +534,7 @@ pub const EncodedENR = struct { } pub fn verify(self: *const Self) Error!void { - const data = self.data; + const data = self.getData(); // Sanity bounds checks if (data.len < 3 + signature_size) { return Error.TooShort; @@ -577,13 +589,13 @@ pub const EncodedENR = struct { } pub fn encodedLen(self: *const Self) usize { - var outer_reader = RLPReader.init(self.data); + var outer_reader = RLPReader.init(self.getData()); const list_data = try outer_reader.read(.{.long_list}); return rlp.elemLen(list_data.len); } pub fn decodeIntoENR(self: *const Self, enr: *ENR) void { - const list_data = RLPReader.init(self.data).read(.{.long_list}) catch unreachable; + const list_data = RLPReader.init(self.getData()).read(.{.long_list}) catch unreachable; var list_reader = RLPReader.init(list_data); const sig = list_reader.read(.{.long_string}) catch unreachable; @@ -601,16 +613,20 @@ pub const EncodedENR = struct { } } - pub fn decodeTxtInto(dest: []u8, source: []const u8) !Self { + 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..]); - try decoder.decode(dest[0..size], source[4..]); - - return EncodedENR.init(dest[0..size]); + + 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]); } }; @@ -655,9 +671,8 @@ test "ENR test vector" { const x = try signable_enr.sign(); try std.testing.expectEqualSlices(u8, &signature, &x); - var encoded_buffer: [max_enr_size]u8 = undefined; - const encoded_enr = try EncodedENR.decodeTxtInto(&encoded_buffer, enr_txt); - std.debug.print("{any}\n", .{encoded_buffer}); + // var encoded_buffer: [max_enr_size]u8 = undefined; + 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()); diff --git a/src/enr_bench.zig b/src/enr_bench.zig index d369525..8d40113 100644 --- a/src/enr_bench.zig +++ b/src/enr_bench.zig @@ -25,8 +25,7 @@ test "bench" { const enr_txt = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"; var decoded_enr: ENR = undefined; try ENR.decodeTxtInto(&decoded_enr, enr_txt); - var encoded_buffer: [enr.max_enr_size]u8 = undefined; - var encoded_enr = try EncodedENR.decodeTxtInto(&encoded_buffer, enr_txt); + var encoded_enr = try EncodedENR.decodeTxtInto(enr_txt); const stdout = std.io.getStdErr().writer(); var bench = zbench.Benchmark.init(std.testing.allocator, .{}); diff --git a/src/secp256k1.zig b/src/secp256k1.zig index ee88264..f5cce94 100644 --- a/src/secp256k1.zig +++ b/src/secp256k1.zig @@ -15,7 +15,7 @@ fn initSecp256k1Context() void { global_secp_ctx = Secp256k1.genNew(); } -fn deinitSecp256k1Context() void { +pub fn deinitSecp256k1Context() void { if (global_secp_ctx) |*ctx| { ctx.deinit(); global_secp_ctx = null; From faccb8bc278c6e6daed63ae2d52818bc0a663616 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Tue, 26 Aug 2025 14:48:42 +0800 Subject: [PATCH 06/11] fix:fix encode function Signed-off-by: Chen Kai <281165273grape@gmail.com> --- src/enr.zig | 113 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 102 insertions(+), 11 deletions(-) diff --git a/src/enr.zig b/src/enr.zig index f82b88f..3843dfc 100644 --- a/src/enr.zig +++ b/src/enr.zig @@ -249,6 +249,35 @@ pub const ENR = struct { 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; @@ -374,12 +403,41 @@ pub const SignableENR = struct { 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]; + } }; fn encodeIntoFromComponents(out: []u8, kvs: *KVs, seq: u64, signature: [signature_size]u8) !void { - const writer = RLPWriter.init(out); + var writer = RLPWriter.init(out); try writer.writeListLength(listLen(kvs, seq)); - try writer.writeString(signature); + try writer.writeString(&signature); try writer.writeInt(u64, seq); var kvs_it = kvs.iterator(); @@ -588,12 +646,6 @@ pub const EncodedENR = struct { } } - pub fn encodedLen(self: *const Self) usize { - var outer_reader = RLPReader.init(self.getData()); - const list_data = try outer_reader.read(.{.long_list}); - return rlp.elemLen(list_data.len); - } - pub fn decodeIntoENR(self: *const Self, enr: *ENR) void { const list_data = RLPReader.init(self.getData()).read(.{.long_list}) catch unreachable; var list_reader = RLPReader.init(list_data); @@ -620,12 +672,12 @@ pub const EncodedENR = struct { 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]); } }; @@ -656,6 +708,34 @@ test "ENR test vector" { try std.testing.expectEqualSlices(u8, ip, decoded_enr.kvs.get("ip").?); try std.testing.expectEqualSlices(u8, udp, decoded_enr.kvs.get("udp").?); + // Fix: decode the expected base64 data first + 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]); + + // === 添加 encodeToTxt 的断言测试 === + + // 1. 测试 encodedTxtLen 计算是否正确 + const expected_txt_len = decoded_enr.encodedTxtLen(); + const actual_txt_len = enr_txt.len; + try std.testing.expectEqual(actual_txt_len, expected_txt_len); + + // 2. 测试 encodeToTxt 是否能正确重现原始文本 + 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); + + // 3. 测试缓冲区长度检查 + 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; @@ -671,6 +751,17 @@ test "ENR test vector" { const x = try signable_enr.sign(); try std.testing.expectEqualSlices(u8, &signature, &x); + // === 测试 SignableENR 的 encodeToTxt === + + // 4. 测试 SignableENR.encodeToTxt + 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); + + // SignableENR 编码后应该与原始 ENR 文本相同(因为签名相同) + try std.testing.expectEqualStrings(enr_txt, signable_encoded_txt); + try std.testing.expectEqual(enr_txt.len, signable_txt_len); + // var encoded_buffer: [max_enr_size]u8 = undefined; const encoded_enr = try EncodedENR.decodeTxtInto(enr_txt); try std.testing.expectEqualStrings(&decoded_enr.signature, encoded_enr.signature()); From bc96ea8e6a8c3ddc5880256a11e19df3afe37870 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Tue, 26 Aug 2025 20:05:21 +0800 Subject: [PATCH 07/11] feat:add more helper function Signed-off-by: Chen Kai <281165273grape@gmail.com> --- src/enr.zig | 195 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 177 insertions(+), 18 deletions(-) diff --git a/src/enr.zig b/src/enr.zig index 3843dfc..882a0b9 100644 --- a/src/enr.zig +++ b/src/enr.zig @@ -234,7 +234,7 @@ pub const ENR = struct { } pub fn publicKey(self: *ENR) PublicKey { - return self.id().publicKeyFromKVs(self.kvs) catch unreachable; + return self.id().publicKeyFromKVs(&self.kvs) catch unreachable; } pub fn nodeId(self: *ENR) NodeId { @@ -344,6 +344,52 @@ pub const ENR = struct { 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]; + } }; pub const SignableENR = struct { @@ -381,7 +427,7 @@ pub const SignableENR = struct { } pub fn publicKey(self: *Self) PublicKey { - return self.id().publicKeyFromKVs(self.kvs) catch unreachable; + return self.id().publicKeyFromKVs(&self.kvs) catch unreachable; } pub fn nodeId(self: *Self) NodeId { @@ -432,6 +478,52 @@ pub const SignableENR = struct { 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 getSignatureStr(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 { @@ -538,12 +630,13 @@ pub const EncodedENR = struct { return self.data[0..self.len]; } - pub fn signature(self: *const Self) []const u8 { + 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); - return list_reader.read(.{.long_string}) catch unreachable; + const sig = list_reader.read(.{.long_string}) catch unreachable; + return sig[0..signature_size].*; } pub fn seq(self: *const Self) u64 { @@ -680,6 +773,52 @@ pub const EncodedENR = struct { 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]; + } }; const hex = @import("hex.zig").hex; @@ -707,8 +846,17 @@ test "ENR test vector" { 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").?); - - // Fix: decode the expected base64 data first + 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..]); @@ -719,19 +867,14 @@ test "ENR test vector" { try decoded_enr.encodeInto(txt[0..len]); try std.testing.expectEqualSlices(u8, txt[0..len], expected_binary[0..expected_size]); - // === 添加 encodeToTxt 的断言测试 === - - // 1. 测试 encodedTxtLen 计算是否正确 const expected_txt_len = decoded_enr.encodedTxtLen(); const actual_txt_len = enr_txt.len; try std.testing.expectEqual(actual_txt_len, expected_txt_len); - // 2. 测试 encodeToTxt 是否能正确重现原始文本 - var encoded_txt_buf: [1000]u8 = undefined; // 足够大的缓冲区 + 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); - // 3. 测试缓冲区长度检查 var small_buf: [10]u8 = undefined; const encode_result = decoded_enr.encodeToTxt(&small_buf); try std.testing.expectError(error.BufferTooSmall, encode_result); @@ -747,25 +890,41 @@ test "ENR test vector" { 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.getSignatureStr(&sig_buf1, .lower); + try std.testing.expectEqualSlices(u8, "0x7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c", sig_out2); const x = try signable_enr.sign(); try std.testing.expectEqualSlices(u8, &signature, &x); - // === 测试 SignableENR 的 encodeToTxt === - - // 4. 测试 SignableENR.encodeToTxt 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); - // SignableENR 编码后应该与原始 ENR 文本相同(因为签名相同) try std.testing.expectEqualStrings(enr_txt, signable_encoded_txt); try std.testing.expectEqual(enr_txt.len, signable_txt_len); - // var encoded_buffer: [max_enr_size]u8 = undefined; const encoded_enr = try EncodedENR.decodeTxtInto(enr_txt); - try std.testing.expectEqualStrings(&decoded_enr.signature, encoded_enr.signature()); + 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); } From 32fb1f8949e0c5ae69b51458a9f357bc5fe607c2 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Wed, 27 Aug 2025 21:29:53 +0800 Subject: [PATCH 08/11] misc:add generate key function and fix decodeintoenr bug Signed-off-by: Chen Kai <281165273grape@gmail.com> --- src/enr.zig | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/enr.zig b/src/enr.zig index 882a0b9..995372b 100644 --- a/src/enr.zig +++ b/src/enr.zig @@ -140,6 +140,14 @@ pub const IDScheme = enum { 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| { @@ -740,7 +748,8 @@ pub const EncodedENR = struct { } pub fn decodeIntoENR(self: *const Self, enr: *ENR) void { - const list_data = RLPReader.init(self.getData()).read(.{.long_list}) catch unreachable; + 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; From 333f3f224d114f219abd36d7339c79407583509e Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Thu, 28 Aug 2025 00:01:03 +0800 Subject: [PATCH 09/11] fix:fix int len for 0 Signed-off-by: Chen Kai <281165273grape@gmail.com> --- src/rlp.zig | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/rlp.zig b/src/rlp.zig index 66e29ae..29bce40 100644 --- a/src/rlp.zig +++ b/src/rlp.zig @@ -32,13 +32,14 @@ fn intByteLen(comptime T: type, num: T) u8 { } pub fn intLen(comptime T: type, num: T) usize { - if (num == 0) { - return 0; - } else if (num < 128) { - return 1; - } else { - return 1 + intByteLen(T, num); + 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 { From a30e7b12a82c50033871d88fab4aa5727b2ba104 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Thu, 28 Aug 2025 11:16:00 +0800 Subject: [PATCH 10/11] fix:fix decodeintoenr Signed-off-by: Chen Kai <281165273grape@gmail.com> --- src/enr.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/enr.zig b/src/enr.zig index 995372b..83655ea 100644 --- a/src/enr.zig +++ b/src/enr.zig @@ -755,7 +755,7 @@ pub const EncodedENR = struct { const sig = list_reader.read(.{.long_string}) catch unreachable; @memcpy(&enr.signature, sig); - const seq_bytes = list_reader.read(.{ .single_byte, .short_string }); + 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(); From 31f46b3b5e5b25a68638bb320545b74c642c6caa Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Thu, 28 Aug 2025 11:40:44 +0800 Subject: [PATCH 11/11] fix:rename signableenr signstr Signed-off-by: Chen Kai <281165273grape@gmail.com> --- src/enr.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/enr.zig b/src/enr.zig index 83655ea..d53b008 100644 --- a/src/enr.zig +++ b/src/enr.zig @@ -525,7 +525,7 @@ pub const SignableENR = struct { return out[0 .. public_key_hex.len + 2]; } - pub fn getSignatureStr(self: *Self, out: []u8, case: std.fmt.Case) ![]const u8 { + 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"); @@ -907,7 +907,7 @@ test "ENR test vector" { 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.getSignatureStr(&sig_buf1, .lower); + const sig_out2 = try signable_enr.signStr(&sig_buf1, .lower); try std.testing.expectEqualSlices(u8, "0x7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c", sig_out2); const x = try signable_enr.sign();