Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions bindings/napi/pubkeys.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const blst_bindings = @import("./blst.zig");
const PubkeyIndexMap = @import("state_transition").PubkeyIndexMap;
const Index2PubkeyCache = @import("state_transition").Index2PubkeyCache;
const napi_io = @import("./io.zig");
const preset = @import("preset").preset;

/// Uses page allocator for internal allocations.
/// It's recommended to never reallocate the pubkey2index after initialization.
Expand All @@ -13,6 +14,11 @@ const allocator = std.heap.page_allocator;
const default_initial_capacity: u32 = 0;
const max_stack_aggregate_pubkeys = 512;

/// Capacity added when a set() outgrows the current capacity. Covers ~3 months of
/// worst-case validator registry growth (MAX_PENDING_DEPOSITS_PER_EPOCH new validators
/// per epoch at 12s slots), so growth stays proportionate at any network scale.
const growth_step: u32 = preset.MAX_PENDING_DEPOSITS_PER_EPOCH * ((90 * 24 * 60 * 60) / (12 * preset.SLOTS_PER_EPOCH));

pub const State = struct {
pubkey2index: PubkeyIndexMap = undefined,
index2pubkey: Index2PubkeyCache = undefined,
Expand Down Expand Up @@ -128,14 +134,14 @@ pub fn load(file_path: js.String) !void {
}

const len = std.mem.readInt(u32, header[4..8], .little);
const capacity = std.mem.readInt(u32, header[8..12], .little);
const saved_capacity = std.mem.readInt(u32, header[8..12], .little);

const file_size = try file.length(io);

state.pubkey2index = PubkeyIndexMap.init(allocator);
try state.pubkey2index.ensureTotalCapacity(capacity);
try state.pubkey2index.ensureTotalCapacity(saved_capacity);
errdefer state.pubkey2index.deinit();
state.index2pubkey = try Index2PubkeyCache.initCapacity(allocator, capacity);
state.index2pubkey = try Index2PubkeyCache.initCapacity(allocator, saved_capacity);
errdefer state.index2pubkey.deinit(allocator);
try state.index2pubkey.resize(allocator, len);

Expand All @@ -151,7 +157,7 @@ pub fn load(file_path: js.String) !void {
try file_reader.interface.readSliceAll(ptr[0..p2i_size]);

state.pubkey2index.unmanaged.size = len;
state.pubkey2index.unmanaged.available = capacity - len;
state.pubkey2index.unmanaged.available = saved_capacity - len;

// Read index2pubkey entries
try file_reader.interface.readSliceAll(std.mem.sliceAsBytes(state.index2pubkey.items));
Expand Down Expand Up @@ -240,9 +246,9 @@ pub fn set(index: js.Number, pubkey: js.Uint8Array) !void {

// Ensure capacity if needed
if (idx >= state.index2pubkey.capacity) {
const new_cap: u32 = @intCast(@max(idx + 1, state.index2pubkey.capacity * 2));
const new_cap: u32 = @intCast(@max(idx + 1, state.index2pubkey.capacity + growth_step));
try state.pubkey2index.ensureTotalCapacity(new_cap);
try state.index2pubkey.ensureTotalCapacity(allocator, new_cap);
try state.index2pubkey.ensureTotalCapacityPrecise(allocator, new_cap);
}

// Extend length if needed
Expand Down Expand Up @@ -273,5 +279,15 @@ pub fn ensureCapacity(new_size: js.Number) !void {
if (requested <= old_size) return;

try state.pubkey2index.ensureTotalCapacity(requested);
// Not precise on purpose, the growth curve overshoot leaves slack for states with
// slightly more validators than reserved, which the zig-side syncPubkeys cannot
// grow safely (it does not own the backing allocator)
try state.index2pubkey.ensureTotalCapacity(allocator, requested);
}

/// JS: pubkeys.capacity() → number
/// Note: zapi DSL does not yet support namespace-level getters, so this is a function.
pub fn capacity() !js.Number {
if (!state.initialized) return error.PubkeyIndexNotInitialized;
return js.Number.from(@as(u32, @intCast(state.index2pubkey.capacity)));
}
7 changes: 6 additions & 1 deletion bindings/src/pubkeys.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ export interface PubkeyCache {
set(index: number, pubkey: Uint8Array): void;
/** Number of entries */
readonly size: number;
/** Allocated native capacity */
readonly capacity: number;
/** Load cache from a PKIX file (clears JS-level cache) */
load(filepath: string): void;
/** Clear native and JS-level cache contents */
reset(): void;
/** Save cache to a PKIX file */
save(filepath: string): void;
/** Pre-allocate native capacity */
/**
* Pre-allocate native capacity. When a set() outgrows the current capacity, it grows
* by a fixed step covering ~3 months of worst-case validator registry growth.
*/
ensureCapacity(capacity: number): void;
}

Expand Down
4 changes: 4 additions & 0 deletions bindings/src/pubkeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export const pubkeyCache = {
return native.size();
},

get capacity() {
return native.capacity();
},

load(filepath) {
pkCache.clear();
native.load(filepath);
Expand Down
16 changes: 16 additions & 0 deletions bindings/test/pubkeys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,20 @@ describe("pubkeys", () => {
expect(pubkeyCache.get(0)).toBeUndefined();
expect(pubkeyCache.getIndex(keypairs[0].pubkeyBytes)).toBeNull();
});

it("exposes native capacity", () => {
expect(pubkeyCache.capacity).toBeGreaterThanOrEqual(1_000);
});

it("grows capacity by a fixed step instead of doubling", () => {
const cap0 = pubkeyCache.capacity;
pubkeyCache.set(cap0, keypairs[0].pubkeyBytes);
const cap1 = pubkeyCache.capacity;
const step = cap1 - cap0;
expect(step).toBeGreaterThan(0);

pubkeyCache.set(cap1, keypairs[1].pubkeyBytes);
const cap2 = pubkeyCache.capacity;
expect(cap2 - cap1).toBe(step);
});
});
Loading