From d479f339a643c8673f719cbcaa7d0d4a6a79bfe6 Mon Sep 17 00:00:00 2001 From: bing Date: Mon, 10 Aug 2026 18:56:43 +0800 Subject: [PATCH 1/3] feat(pubkeys): add getPubkeyBytes binding Expose the cached 48-byte compressed pubkey bytes by validator index without materializing a PublicKey wrapper or serializing in JS. The BLS worker job path in Lodestar needs Uint8Array pubkeys, so this avoids the getOrThrow() + toBytes() round trip on the SingleAttestation gossip validation hot path. Refs https://github.com/ChainSafe/lodestar/pull/9728#issuecomment-5237784361 --- bindings/napi/pubkeys.zig | 10 ++++++++++ bindings/src/pubkeys.d.ts | 2 ++ bindings/src/pubkeys.js | 4 ++++ bindings/test/pubkeys.test.ts | 16 ++++++++++++++++ src/state_transition/cache/pubkey_cache.zig | 14 ++++++++++++++ 5 files changed, 46 insertions(+) diff --git a/bindings/napi/pubkeys.zig b/bindings/napi/pubkeys.zig index 50af3c254..0c1a1176c 100644 --- a/bindings/napi/pubkeys.zig +++ b/bindings/napi/pubkeys.zig @@ -132,6 +132,16 @@ pub fn get(index: js.Number) !?blst_bindings.PublicKey { return .{ .raw = public_key }; } +/// JS: pubkeys.getPubkeyBytes(index) → Uint8Array | undefined +pub fn getPubkeyBytes(index: js.Number) !?js.Uint8Array { + if (!state.initialized) return error.PubkeyIndexNotInitialized; + + const idx = try index.toU32(); + const io = js.io(); + const pubkey_bytes = state.cache.getPubkeyBytes(io, idx) orelse return null; + return try js.Uint8Array.fromExternal(pubkey_bytes[0..]); +} + /// Aggregate multiple `PublicKey`s by the given /// validator `indices` into one. /// diff --git a/bindings/src/pubkeys.d.ts b/bindings/src/pubkeys.d.ts index 1a7c647d9..e973b8613 100644 --- a/bindings/src/pubkeys.d.ts +++ b/bindings/src/pubkeys.d.ts @@ -5,6 +5,8 @@ export interface PubkeyCache { get(index: number): PublicKey | undefined; /** Same as get(), but throws if the index is not in the cache */ getOrThrow(index: number): PublicKey; + /** Get the cached 48-byte compressed pubkey bytes without materializing a PublicKey wrapper. */ + getPubkeyBytes(index: number): Uint8Array | undefined; /** Aggregate cached public keys by validator index */ aggregate(indices: number[]): PublicKey; /** Get validator index by pubkey bytes */ diff --git a/bindings/src/pubkeys.js b/bindings/src/pubkeys.js index 9ec87a1e0..a175759ff 100644 --- a/bindings/src/pubkeys.js +++ b/bindings/src/pubkeys.js @@ -25,6 +25,10 @@ export const pubkeyCache = { return pk; }, + getPubkeyBytes(index) { + return native.getPubkeyBytes(index); + }, + aggregate(indices) { if (indices.length === 1) return pubkeyCache.getOrThrow(indices[0]); return native.aggregate(indices); diff --git a/bindings/test/pubkeys.test.ts b/bindings/test/pubkeys.test.ts index 64578e374..944b3e707 100644 --- a/bindings/test/pubkeys.test.ts +++ b/bindings/test/pubkeys.test.ts @@ -105,6 +105,22 @@ describe("pubkeys", () => { expect(pubkeyCache.get(0xffffffff)).toBeUndefined(); }); + it("getPubkeyBytes returns the compressed pubkey bytes", () => { + for (const {index, pubkeyBytes} of keypairs) { + expect(pubkeyCache.getPubkeyBytes(index)).toEqual(pubkeyBytes); + } + }); + + it("getPubkeyBytes matches getOrThrow().toBytes() and returns copies", () => { + const bytes = pubkeyCache.getPubkeyBytes(0); + expect(bytes).toEqual(pubkeyCache.getOrThrow(0).toBytes()); + expect(pubkeyCache.getPubkeyBytes(0)).not.toBe(bytes); + }); + + it("getPubkeyBytes returns undefined for out-of-range index", () => { + expect(pubkeyCache.getPubkeyBytes(0xffffffff)).toBeUndefined(); + }); + it("getIndex returns null for unknown pubkey", () => { expect(pubkeyCache.getIndex(new Uint8Array(48))).toBeNull(); }); diff --git a/src/state_transition/cache/pubkey_cache.zig b/src/state_transition/cache/pubkey_cache.zig index 3d4b51282..7bf76ce70 100644 --- a/src/state_transition/cache/pubkey_cache.zig +++ b/src/state_transition/cache/pubkey_cache.zig @@ -164,6 +164,20 @@ pub const PubkeyCache = struct { return self.entries.values()[@intCast(index)]; } + /// Get the compressed pubkey bytes for a validator index. A value is + /// returned so no pointer into movable map storage escapes the shared + /// lock. + pub fn getPubkeyBytes( + self: *const PubkeyCache, + io: std.Io, + index: u64, + ) ?[48]u8 { + self.lockShared(io); + defer self.unlockShared(io); + if (index >= self.entries.count()) return null; + return self.entries.keys()[@intCast(index)]; + } + /// Copy affine pubkeys for a batch of validator indices while holding one /// shared lock. The output is left unchanged when an index is invalid. pub fn getPubkeys( From 5071c7cf66e353de19109fa021f9412b18e62582 Mon Sep 17 00:00:00 2001 From: bing Date: Wed, 12 Aug 2026 15:44:45 +0800 Subject: [PATCH 2/3] fromExternal -> from --- bindings/napi/pubkeys.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/napi/pubkeys.zig b/bindings/napi/pubkeys.zig index 0c1a1176c..23c72d329 100644 --- a/bindings/napi/pubkeys.zig +++ b/bindings/napi/pubkeys.zig @@ -139,7 +139,7 @@ pub fn getPubkeyBytes(index: js.Number) !?js.Uint8Array { const idx = try index.toU32(); const io = js.io(); const pubkey_bytes = state.cache.getPubkeyBytes(io, idx) orelse return null; - return try js.Uint8Array.fromExternal(pubkey_bytes[0..]); + return js.Uint8Array.from(pubkey_bytes[0..]); } /// Aggregate multiple `PublicKey`s by the given From 0cdefd5d358f46018568781166ef18aa543632ce Mon Sep 17 00:00:00 2001 From: bing Date: Wed, 12 Aug 2026 16:36:05 +0800 Subject: [PATCH 3/3] feat: add OrThrow method --- bindings/src/pubkeys.d.ts | 2 ++ bindings/src/pubkeys.js | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/bindings/src/pubkeys.d.ts b/bindings/src/pubkeys.d.ts index e973b8613..a08a2debc 100644 --- a/bindings/src/pubkeys.d.ts +++ b/bindings/src/pubkeys.d.ts @@ -7,6 +7,8 @@ export interface PubkeyCache { getOrThrow(index: number): PublicKey; /** Get the cached 48-byte compressed pubkey bytes without materializing a PublicKey wrapper. */ getPubkeyBytes(index: number): Uint8Array | undefined; + /** Same as getPubkeyBytes(), but throws if the index is not in the cache. */ + getPubkeyBytesOrThrow(index: number): Uint8Array; /** Aggregate cached public keys by validator index */ aggregate(indices: number[]): PublicKey; /** Get validator index by pubkey bytes */ diff --git a/bindings/src/pubkeys.js b/bindings/src/pubkeys.js index a175759ff..f5b0f8e7d 100644 --- a/bindings/src/pubkeys.js +++ b/bindings/src/pubkeys.js @@ -29,6 +29,15 @@ export const pubkeyCache = { return native.getPubkeyBytes(index); }, + getPubkeyBytesOrThrow(index) { + const pubkey = native.getPubkeyBytes(index); + if (pubkey === undefined) { + throw new Error(`pubkeyCache: index ${index} not found`); + } + + return pubkey; + }, + aggregate(indices) { if (indices.length === 1) return pubkeyCache.getOrThrow(indices[0]); return native.aggregate(indices);