Skip to content

Commit 3acc71e

Browse files
committed
fix(bindings): harden peerManager NAPI surface
1 parent 0258816 commit 3acc71e

File tree

3 files changed

+238
-20
lines changed

3 files changed

+238
-20
lines changed

bindings/napi/peer_manager.zig

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const PeerAction = peer_manager.PeerAction;
1313
const GoodbyeReasonCode = peer_manager.GoodbyeReasonCode;
1414
const GossipScoreUpdate = peer_manager.GossipScoreUpdate;
1515
const RequestedSubnet = peer_manager.RequestedSubnet;
16+
const parseExternalPeerActionName = peer_manager.parseExternalPeerActionName;
1617

1718
/// Allocator for internal allocations.
1819
const allocator = std.heap.page_allocator;
@@ -201,6 +202,34 @@ fn readPeerId(value: napi.Value) ![]const u8 {
201202
return try value.getValueStringUtf8(&buf);
202203
}
203204

205+
fn readOwnedPeerId(value: napi.Value) ![]u8 {
206+
var buf: [128]u8 = undefined;
207+
const peer_id = try value.getValueStringUtf8(&buf);
208+
return allocator.dupe(u8, peer_id);
209+
}
210+
211+
fn parsePeerActionName(action_name: []const u8) ?PeerAction {
212+
return parseExternalPeerActionName(action_name);
213+
}
214+
215+
fn parseOptionalU32Array(value: napi.Value) !?[]u32 {
216+
const value_type = try value.typeof();
217+
if (value_type == .undefined or value_type == .null) {
218+
return null;
219+
}
220+
221+
const len = try value.getArrayLength();
222+
const items = try allocator.alloc(u32, len);
223+
errdefer allocator.free(items);
224+
225+
for (0..len) |i| {
226+
const elem = try value.getElement(@intCast(i));
227+
items[i] = try elem.getValueUint32();
228+
}
229+
230+
return items;
231+
}
232+
204233
// ── Lifecycle ────────────────────────────────────────────────────────
205234

206235
pub fn PeerManager_init(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value {
@@ -235,7 +264,8 @@ pub fn PeerManager_checkPingAndStatus(env: napi.Env, _: napi.CallbackInfo(0)) !n
235264

236265
pub fn PeerManager_onConnectionOpen(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value {
237266
const m = try getManager();
238-
const peer_id = try readPeerId(cb.arg(0));
267+
const peer_id = try readOwnedPeerId(cb.arg(0));
268+
defer allocator.free(peer_id);
239269
var dir_buf: [16]u8 = undefined;
240270
const dir_str = try cb.arg(1).getValueStringUtf8(&dir_buf);
241271
const direction = std.meta.stringToEnum(Direction, dir_str) orelse
@@ -246,14 +276,16 @@ pub fn PeerManager_onConnectionOpen(env: napi.Env, cb: napi.CallbackInfo(2)) !na
246276

247277
pub fn PeerManager_onConnectionClose(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value {
248278
const m = try getManager();
249-
const peer_id = try readPeerId(cb.arg(0));
279+
const peer_id = try readOwnedPeerId(cb.arg(0));
280+
defer allocator.free(peer_id);
250281
const actions = try m.onConnectionClose(peer_id);
251282
return actionsToNapiArray(env, actions);
252283
}
253284

254285
pub fn PeerManager_onStatusReceived(env: napi.Env, cb: napi.CallbackInfo(4)) !napi.Value {
255286
const m = try getManager();
256-
const peer_id = try readPeerId(cb.arg(0));
287+
const peer_id = try readOwnedPeerId(cb.arg(0));
288+
defer allocator.free(peer_id);
257289
const remote_status = try statusFromObject(env, try cb.arg(1).coerceToObject());
258290
const local_status = try statusFromObject(env, try cb.arg(2).coerceToObject());
259291
const current_slot: u64 = @intCast(try cb.arg(3).getValueInt64());
@@ -263,7 +295,8 @@ pub fn PeerManager_onStatusReceived(env: napi.Env, cb: napi.CallbackInfo(4)) !na
263295

264296
pub fn PeerManager_onMetadataReceived(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value {
265297
const m = try getManager();
266-
const peer_id = try readPeerId(cb.arg(0));
298+
const peer_id = try readOwnedPeerId(cb.arg(0));
299+
defer allocator.free(peer_id);
267300
const md_obj = try cb.arg(1).coerceToObject();
268301

269302
var metadata: Metadata = undefined;
@@ -278,23 +311,27 @@ pub fn PeerManager_onMetadataReceived(env: napi.Env, cb: napi.CallbackInfo(2)) !
278311
@memcpy(&metadata.syncnets, syncnets_info.data[0..1]);
279312

280313
metadata.custody_group_count = @intCast(try (try md_obj.getNamedProperty("custodyGroupCount")).getValueInt64());
281-
metadata.custody_groups = null;
282-
metadata.sampling_groups = null;
314+
metadata.custody_groups = try parseOptionalU32Array(try md_obj.getNamedProperty("custodyGroups"));
315+
errdefer if (metadata.custody_groups) |groups| allocator.free(groups);
316+
metadata.sampling_groups = try parseOptionalU32Array(try md_obj.getNamedProperty("samplingGroups"));
317+
errdefer if (metadata.sampling_groups) |groups| allocator.free(groups);
283318

284319
m.onMetadataReceived(peer_id, metadata);
285320
return env.getUndefined();
286321
}
287322

288323
pub fn PeerManager_onMessageReceived(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value {
289324
const m = try getManager();
290-
const peer_id = try readPeerId(cb.arg(0));
325+
const peer_id = try readOwnedPeerId(cb.arg(0));
326+
defer allocator.free(peer_id);
291327
m.onMessageReceived(peer_id);
292328
return env.getUndefined();
293329
}
294330

295331
pub fn PeerManager_onGoodbye(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value {
296332
const m = try getManager();
297-
const peer_id = try readPeerId(cb.arg(0));
333+
const peer_id = try readOwnedPeerId(cb.arg(0));
334+
defer allocator.free(peer_id);
298335
const reason_raw: u64 = @intCast(try cb.arg(1).getValueInt64());
299336
const reason: GoodbyeReasonCode = @enumFromInt(reason_raw);
300337
const actions = try m.onGoodbye(peer_id, reason);
@@ -303,7 +340,8 @@ pub fn PeerManager_onGoodbye(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Valu
303340

304341
pub fn PeerManager_onPing(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value {
305342
const m = try getManager();
306-
const peer_id = try readPeerId(cb.arg(0));
343+
const peer_id = try readOwnedPeerId(cb.arg(0));
344+
defer allocator.free(peer_id);
307345
const seq_number: u64 = @intCast(try cb.arg(1).getValueInt64());
308346
const actions = try m.onPing(peer_id, seq_number);
309347
return actionsToNapiArray(env, actions);
@@ -313,10 +351,11 @@ pub fn PeerManager_onPing(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value {
313351

314352
pub fn PeerManager_reportPeer(env: napi.Env, cb: napi.CallbackInfo(2)) !napi.Value {
315353
const m = try getManager();
316-
const peer_id = try readPeerId(cb.arg(0));
354+
const peer_id = try readOwnedPeerId(cb.arg(0));
355+
defer allocator.free(peer_id);
317356
var action_buf: [32]u8 = undefined;
318357
const action_str = try cb.arg(1).getValueStringUtf8(&action_buf);
319-
const action = std.meta.stringToEnum(PeerAction, action_str) orelse
358+
const action = parsePeerActionName(action_str) orelse
320359
return error.InvalidPeerAction;
321360
m.reportPeer(peer_id, action);
322361
return env.getUndefined();
@@ -328,17 +367,24 @@ pub fn PeerManager_updateGossipScores(env: napi.Env, cb: napi.CallbackInfo(1)) !
328367
const len = try arr.getArrayLength();
329368
const scores = try allocator.alloc(GossipScoreUpdate, len);
330369
defer allocator.free(scores);
370+
const peer_ids = try allocator.alloc([]u8, len);
371+
defer allocator.free(peer_ids);
372+
var initialized: usize = 0;
373+
errdefer {
374+
for (peer_ids[0..initialized]) |pid| allocator.free(pid);
375+
}
331376

332377
for (0..len) |i| {
333378
const entry = try arr.getElement(@intCast(i));
334-
var pid_buf: [128]u8 = undefined;
335-
const pid = try (try entry.getNamedProperty("peerId")).getValueStringUtf8(&pid_buf);
379+
peer_ids[i] = try readOwnedPeerId(try entry.getNamedProperty("peerId"));
380+
initialized += 1;
336381
scores[i] = .{
337-
.peer_id = pid,
382+
.peer_id = peer_ids[i],
338383
.new_score = try (try entry.getNamedProperty("score")).getValueDouble(),
339384
};
340385
}
341386
m.updateGossipScores(scores);
387+
for (peer_ids) |pid| allocator.free(pid);
342388
return env.getUndefined();
343389
}
344390

@@ -425,7 +471,8 @@ pub fn PeerManager_getConnectedPeers(env: napi.Env, _: napi.CallbackInfo(0)) !na
425471

426472
pub fn PeerManager_getPeerData(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value {
427473
const m = try getManager();
428-
const peer_id = try readPeerId(cb.arg(0));
474+
const peer_id = try readOwnedPeerId(cb.arg(0));
475+
defer allocator.free(peer_id);
429476
const peer = m.getPeerData(peer_id) orelse return env.getNull();
430477

431478
const obj = try env.createObject();
@@ -459,28 +506,32 @@ pub fn PeerManager_getPeerData(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Va
459506

460507
pub fn PeerManager_getEncodingPreference(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value {
461508
const m = try getManager();
462-
const peer_id = try readPeerId(cb.arg(0));
509+
const peer_id = try readOwnedPeerId(cb.arg(0));
510+
defer allocator.free(peer_id);
463511
const encoding = m.getEncodingPreference(peer_id) orelse return env.getNull();
464512
return env.createStringUtf8(@tagName(encoding));
465513
}
466514

467515
pub fn PeerManager_getPeerKind(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value {
468516
const m = try getManager();
469-
const peer_id = try readPeerId(cb.arg(0));
517+
const peer_id = try readOwnedPeerId(cb.arg(0));
518+
defer allocator.free(peer_id);
470519
const kind = m.getPeerKind(peer_id) orelse return env.getNull();
471520
return env.createStringUtf8(@tagName(kind));
472521
}
473522

474523
pub fn PeerManager_getAgentVersion(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value {
475524
const m = try getManager();
476-
const peer_id = try readPeerId(cb.arg(0));
525+
const peer_id = try readOwnedPeerId(cb.arg(0));
526+
defer allocator.free(peer_id);
477527
const av = m.getAgentVersion(peer_id) orelse return env.getNull();
478528
return env.createStringUtf8(av);
479529
}
480530

481531
pub fn PeerManager_getPeerScore(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value {
482532
const m = try getManager();
483-
const peer_id = try readPeerId(cb.arg(0));
533+
const peer_id = try readOwnedPeerId(cb.arg(0));
534+
defer allocator.free(peer_id);
484535
return env.createDouble(m.getPeerScore(peer_id));
485536
}
486537

@@ -526,3 +577,16 @@ pub fn register(env: napi.Env, exports: napi.Value) !void {
526577

527578
try exports.setNamedProperty("peerManager", pm_obj);
528579
}
580+
581+
test "parsePeerActionName accepts Lodestar JS action names" {
582+
try std.testing.expectEqual(PeerAction.mid_tolerance, parsePeerActionName("MidToleranceError").?);
583+
try std.testing.expectEqual(PeerAction.low_tolerance, parsePeerActionName("LowToleranceError").?);
584+
try std.testing.expectEqual(PeerAction.high_tolerance, parsePeerActionName("HighToleranceError").?);
585+
try std.testing.expectEqual(PeerAction.fatal, parsePeerActionName("Fatal").?);
586+
}
587+
588+
test "parsePeerActionName accepts Zig enum names" {
589+
try std.testing.expectEqual(PeerAction.mid_tolerance, parsePeerActionName("mid_tolerance").?);
590+
try std.testing.expectEqual(PeerAction.fatal, parsePeerActionName("fatal").?);
591+
try std.testing.expect(parsePeerActionName("not-a-real-action") == null);
592+
}

bindings/src/index.d.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,150 @@ interface CompactMultiProof {
5858
descriptor: Uint8Array;
5959
}
6060

61+
type PeerManagerDirection = "inbound" | "outbound";
62+
63+
type PeerManagerForkName =
64+
| "phase0"
65+
| "altair"
66+
| "bellatrix"
67+
| "capella"
68+
| "deneb"
69+
| "electra"
70+
| "fulu"
71+
| "gloas"
72+
| "heze";
73+
74+
type PeerManagerReportPeerAction =
75+
| "Fatal"
76+
| "LowToleranceError"
77+
| "MidToleranceError"
78+
| "HighToleranceError"
79+
| "fatal"
80+
| "low_tolerance"
81+
| "mid_tolerance"
82+
| "high_tolerance";
83+
84+
interface PeerManagerConfig {
85+
targetPeers: number;
86+
maxPeers: number;
87+
targetGroupPeers: number;
88+
pingIntervalInboundMs: number;
89+
pingIntervalOutboundMs: number;
90+
statusIntervalMs: number;
91+
statusInboundGracePeriodMs: number;
92+
gossipsubNegativeScoreWeight: number;
93+
gossipsubPositiveScoreWeight: number;
94+
negativeGossipScoreIgnoreThreshold: number;
95+
disablePeerScoring: boolean;
96+
initialForkName: PeerManagerForkName;
97+
numberOfCustodyGroups: number;
98+
custodyRequirement: number;
99+
samplesPerSlot: number;
100+
slotsPerEpoch: number;
101+
}
102+
103+
interface PeerManagerStatus {
104+
forkDigest: Uint8Array;
105+
finalizedRoot: Uint8Array;
106+
finalizedEpoch: number;
107+
headRoot: Uint8Array;
108+
headSlot: number;
109+
earliestAvailableSlot?: number | null;
110+
}
111+
112+
interface PeerManagerMetadata {
113+
seqNumber: number;
114+
attnets: Uint8Array;
115+
syncnets: Uint8Array;
116+
custodyGroupCount: number;
117+
custodyGroups?: number[] | null;
118+
samplingGroups?: number[] | null;
119+
}
120+
121+
interface PeerManagerRequestedSubnet {
122+
subnet: number;
123+
toSlot: number;
124+
}
125+
126+
interface PeerManagerGossipScoreUpdate {
127+
peerId: string;
128+
score: number;
129+
}
130+
131+
interface PeerManagerDiscoveryQuery {
132+
subnet: number;
133+
toSlot: number;
134+
maxPeersToDiscover: number;
135+
}
136+
137+
interface PeerManagerCustodyGroupQuery {
138+
group: number;
139+
maxPeersToDiscover: number;
140+
}
141+
142+
type PeerManagerAction =
143+
| {type: "send_ping"; peerId: string}
144+
| {type: "send_status"; peerId: string}
145+
| {type: "send_goodbye"; peerId: string; reason: number}
146+
| {type: "request_metadata"; peerId: string}
147+
| {type: "disconnect_peer"; peerId: string}
148+
| {
149+
type: "request_discovery";
150+
peersToConnect: number;
151+
attnetQueries: PeerManagerDiscoveryQuery[];
152+
syncnetQueries: PeerManagerDiscoveryQuery[];
153+
custodyGroupQueries: PeerManagerCustodyGroupQuery[];
154+
}
155+
| {type: "tag_peer_relevant"; peerId: string}
156+
| {type: "emit_peer_connected"; peerId: string; direction: PeerManagerDirection}
157+
| {type: "emit_peer_disconnected"; peerId: string};
158+
159+
interface PeerManagerPeerData {
160+
peerId: string;
161+
direction: PeerManagerDirection;
162+
relevantStatus: "unknown" | "relevant" | "irrelevant";
163+
connectedUnixTsMs: number;
164+
lastReceivedMsgUnixTsMs: number;
165+
lastStatusUnixTsMs: number;
166+
agentVersion: string | null;
167+
agentClient: string | null;
168+
encodingPreference: string | null;
169+
}
170+
171+
interface PeerManagerApi {
172+
init: (config: PeerManagerConfig) => void;
173+
close: () => void;
174+
heartbeat: (currentSlot: number, localStatus: PeerManagerStatus) => PeerManagerAction[];
175+
checkPingAndStatus: () => PeerManagerAction[];
176+
onConnectionOpen: (peerId: string, direction: PeerManagerDirection) => PeerManagerAction[];
177+
onConnectionClose: (peerId: string) => PeerManagerAction[];
178+
onStatusReceived: (
179+
peerId: string,
180+
remoteStatus: PeerManagerStatus,
181+
localStatus: PeerManagerStatus,
182+
currentSlot: number
183+
) => PeerManagerAction[];
184+
onMetadataReceived: (peerId: string, metadata: PeerManagerMetadata) => void;
185+
onMessageReceived: (peerId: string) => void;
186+
onGoodbye: (peerId: string, reason: number) => PeerManagerAction[];
187+
onPing: (peerId: string, seqNumber: number) => PeerManagerAction[];
188+
reportPeer: (peerId: string, action: PeerManagerReportPeerAction) => void;
189+
updateGossipScores: (scores: PeerManagerGossipScoreUpdate[]) => void;
190+
setSubnetRequirements: (
191+
attnets: PeerManagerRequestedSubnet[],
192+
syncnets: PeerManagerRequestedSubnet[]
193+
) => void;
194+
setForkName: (forkName: PeerManagerForkName) => void;
195+
setSamplingGroups: (groups: number[]) => void;
196+
getConnectedPeerCount: () => number;
197+
getConnectedPeers: () => string[];
198+
getPeerData: (peerId: string) => PeerManagerPeerData | null;
199+
getEncodingPreference: (peerId: string) => string | null;
200+
getPeerKind: (peerId: string) => string | null;
201+
getAgentVersion: (peerId: string) => string | null;
202+
getPeerScore: (peerId: string) => number;
203+
}
204+
61205
/** Options to control how state transition is run */
62206
interface TransitionOpts {
63207
/** Verify the post-state root matches the block's state root. Default: true. */
@@ -240,6 +384,7 @@ declare const bindings: {
240384
init: () => void;
241385
scrapeMetrics: () => string;
242386
};
387+
peerManager: PeerManagerApi;
243388
BeaconStateView: typeof BeaconStateView;
244389
};
245390

0 commit comments

Comments
 (0)