lib/reticulum/src/node/owner.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const carrier = @import("../carrier/root.zig");
4 const destination = @import("../destination/root.zig");
5 const identity = @import("../identity/root.zig");
6 const packet = @import("../packet/root.zig");
7 const wire = @import("../wire/root.zig");
8 const effect = @import("effect.zig");
9 const errors = @import("error.zig");
10 const event = @import("event.zig");
11 const access = @import("access.zig");
12 const ifac = @import("../interface/root.zig").ifac;
13 const limits = @import("limits.zig");
14 const timer = @import("timer.zig");
15 const transport = @import("transport/root.zig");
16
17 pub const Access = access.Access;
18 pub const Interface = access.Registration;
19
20 pub const ProofMode = enum(u1) {
21 implicit,
22 explicit,
23 };
24
25 pub const RatchetBinding = limits.RatchetBinding;
26
27 pub const Node = struct {
28 phase: alloc_phase.capacity.Phase,
29 capacity: Capacity,
30 storage: Storage,
31 interfaces: []Interface,
32 identities: []identity.Private,
33 ratchet_bindings: []RatchetBinding,
34 destinations: destination.registry.Table,
35 known_identities: identity.known.Identities,
36 known_ratchets: identity.known.Ratchets,
37 receipts: packet.receipt.Table,
38 duplicate_hashes: packet.hashlist.Table,
39 timers: timer.Table,
40 transport: transport.State,
41 effects: effect.Effects,
42 scratch: []u8,
43 proof_mode: ProofMode = .implicit,
44
45 pub const storage_alignment: usize = 8;
46 pub const Storage = []align(storage_alignment) u8;
47 pub const Limits: type = limits.Limits;
48 pub const Capacity: type = limits.Capacity;
49 pub const InitError = Capacity.DeriveError || error{StorageLengthMismatch};
50 pub const work_limits: alloc_phase.capacity.WorkLimits = .{
51 .transition_steps_max = 8,
52 .cleanup_steps_per_call_max = 0,
53 .cleanup_calls_at_capacity_max = 0,
54 };
55 pub const claim: alloc_phase.capacity.Declaration = .{
56 .source = .{
57 .id = "reticulum.node",
58 .kind = .phase_static,
59 .limit_source = .caller,
60 .storage = .{
61 .covered = &.{
62 .{
63 .id = "caller_interface_slots",
64 .lifetime = .transferred,
65 .detail = "caller storage for bounded carrier access registrations",
66 },
67 .{
68 .id = "caller_node_subowners",
69 .lifetime = .transferred,
70 .detail = "caller storage for local identities and eight strict subowners",
71 },
72 .{
73 .id = "caller_node_work",
74 .lifetime = .transferred,
75 .detail = "caller storage for rotating-key bindings and one MTU scratch",
76 },
77 },
78 .excluded = &.{
79 "carrier memory and interface devices",
80 "bound rotating-key rings and application payload inputs",
81 "persistent storage and operating-system state",
82 },
83 },
84 .capacity = .{
85 .inputs = &.{
86 alloc_phase.capacity.bindInput(Limits, "interfaces", "interfaces_max"),
87 alloc_phase.capacity.bindInput(Limits, "destinations", "destinations_max"),
88 alloc_phase.capacity.bindInput(Limits, "identities", "known_identities_max"),
89 alloc_phase.capacity.bindInput(Limits, "ratchets", "known_ratchets_max"),
90 alloc_phase.capacity.bindInput(Limits, "receipts", "receipts_max"),
91 alloc_phase.capacity.bindInput(Limits, "hashes", "duplicate_hashes_max"),
92 alloc_phase.capacity.bindInput(Limits, "timers", "timers_max"),
93 alloc_phase.capacity.bindInput(Limits, "effects", "effects_max"),
94 alloc_phase.capacity.bindInput(Limits, "frames", "effect_frames_max"),
95 alloc_phase.capacity.bindInput(Limits, "paths", "paths_max"),
96 alloc_phase.capacity.bindInput(Limits, "announces", "announces_max"),
97 alloc_phase.capacity.bindInput(
98 Limits,
99 "reverse_entries",
100 "reverse_entries_max",
101 ),
102 alloc_phase.capacity.bindInput(
103 Limits,
104 "path_request_tags",
105 "path_request_tags_max",
106 ),
107 alloc_phase.capacity.bindInput(
108 Limits,
109 "inflight_requests",
110 "inflight_requests_max",
111 ),
112 alloc_phase.capacity.bindInput(Limits, "discoveries", "discoveries_max"),
113 alloc_phase.capacity.bindInput(Limits, "links", "links_max"),
114 },
115 .type_selectors = &.{
116 alloc_phase.capacity.bindType(identity.Private, "private_identity"),
117 alloc_phase.capacity.bindType(RatchetBinding, "ratchet_binding"),
118 alloc_phase.capacity.bindType(
119 destination.registry.Entry,
120 "destination",
121 ),
122 alloc_phase.capacity.bindType(
123 identity.known.IdentityEntry,
124 "identity",
125 ),
126 alloc_phase.capacity.bindType(
127 identity.known.RatchetEntry,
128 "ratchet",
129 ),
130 alloc_phase.capacity.bindType(packet.receipt.Receipt, "receipt"),
131 alloc_phase.capacity.bindType(packet.Hash, "hash"),
132 alloc_phase.capacity.bindType(timer.Timer, "timer"),
133 alloc_phase.capacity.bindType(effect.Effect, "effect"),
134 alloc_phase.capacity.bindType(carrier.Frame, "frame"),
135 alloc_phase.capacity.bindType(Interface, "interface"),
136 alloc_phase.capacity.bindType(transport.path.Entry, "path"),
137 alloc_phase.capacity.bindType(transport.announces.Entry, "announce"),
138 alloc_phase.capacity.bindType(transport.reverse.Entry, "reverse_entry"),
139 alloc_phase.capacity.bindType(transport.inflight.Entry, "inflight_request"),
140 alloc_phase.capacity.bindType(transport.discoveries.Entry, "discovery"),
141 },
142 .nodes = &.{
143 .{ .input = 0 },
144 .{ .scale = .{
145 .node = 0,
146 .coefficient = .{ .size_of_concrete_type = 10 },
147 } },
148 .{ .alignment = .{ .node = 1, .alignment = .{ .literal = 8 } } },
149 .{ .input = 1 },
150 .{ .scale = .{
151 .node = 3,
152 .coefficient = .{ .size_of_concrete_type = 0 },
153 } },
154 .{ .add = .{ .left = 2, .right = 4 } },
155 .{ .scale = .{
156 .node = 3,
157 .coefficient = .{ .size_of_concrete_type = 1 },
158 } },
159 .{ .add = .{ .left = 5, .right = 6 } },
160 .{ .scale = .{
161 .node = 3,
162 .coefficient = .{ .size_of_concrete_type = 2 },
163 } },
164 .{ .add = .{ .left = 7, .right = 8 } },
165 .{ .input = 2 },
166 .{ .scale = .{
167 .node = 10,
168 .coefficient = .{ .size_of_concrete_type = 3 },
169 } },
170 .{ .add = .{ .left = 9, .right = 11 } },
171 .{ .input = 3 },
172 .{ .scale = .{
173 .node = 13,
174 .coefficient = .{ .size_of_concrete_type = 4 },
175 } },
176 .{ .add = .{ .left = 12, .right = 14 } },
177 .{ .input = 4 },
178 .{ .scale = .{
179 .node = 16,
180 .coefficient = .{ .size_of_concrete_type = 5 },
181 } },
182 .{ .add = .{ .left = 15, .right = 17 } },
183 .{ .input = 5 },
184 .{ .scale = .{
185 .node = 19,
186 .coefficient = .{ .size_of_concrete_type = 6 },
187 } },
188 .{ .add = .{ .left = 18, .right = 20 } },
189 .{ .input = 6 },
190 .{ .scale = .{
191 .node = 22,
192 .coefficient = .{ .size_of_concrete_type = 7 },
193 } },
194 .{ .add = .{ .left = 21, .right = 23 } },
195 .{ .input = 7 },
196 .{ .scale = .{
197 .node = 25,
198 .coefficient = .{ .size_of_concrete_type = 8 },
199 } },
200 .{ .add = .{ .left = 24, .right = 26 } },
201 .{ .input = 8 },
202 .{ .scale = .{
203 .node = 28,
204 .coefficient = .{ .size_of_concrete_type = 9 },
205 } },
206 .{ .add = .{ .left = 27, .right = 29 } },
207 .{ .input = 9 },
208 .{ .scale = .{
209 .node = 31,
210 .coefficient = .{ .size_of_concrete_type = 11 },
211 } },
212 .{ .add = .{ .left = 30, .right = 32 } },
213 .{ .input = 10 },
214 .{ .scale = .{
215 .node = 34,
216 .coefficient = .{ .size_of_concrete_type = 12 },
217 } },
218 .{ .add = .{ .left = 33, .right = 35 } },
219 .{ .input = 11 },
220 .{ .scale = .{
221 .node = 37,
222 .coefficient = .{ .size_of_concrete_type = 13 },
223 } },
224 .{ .add = .{ .left = 36, .right = 38 } },
225 .{ .input = 12 },
226 .{ .scale = .{
227 .node = 40,
228 .coefficient = .{ .size_of_concrete_type = 6 },
229 } },
230 .{ .add = .{ .left = 39, .right = 41 } },
231 .{ .input = 13 },
232 .{ .scale = .{
233 .node = 43,
234 .coefficient = .{ .size_of_concrete_type = 14 },
235 } },
236 .{ .add = .{ .left = 42, .right = 44 } },
237 .{ .input = 14 },
238 .{ .scale = .{
239 .node = 46,
240 .coefficient = .{ .size_of_concrete_type = 15 },
241 } },
242 .{ .add = .{ .left = 45, .right = 47 } },
243 .{ .input = 15 },
244 .{ .scale = .{
245 .node = 49,
246 .coefficient = .{ .literal = @sizeOf(transport.links.Entry) },
247 } },
248 .{ .add = .{ .left = 48, .right = 50 } },
249 .{ .constant = 500 },
250 .{ .add = .{ .left = 51, .right = 52 } },
251 },
252 .assertions = &.{.{
253 .scope = .closure_total,
254 .measure = .retained,
255 .relation = .exact,
256 .expression = 53,
257 }},
258 },
259 .overload = .{
260 .kind = .reject_before_seal,
261 .detail = "invalid limits and short caller storage reject before activation",
262 },
263 .risks = .{
264 .transitive = .{
265 .status = .witnessed,
266 .detail = "all retained subowner storage is carved from the node region",
267 },
268 .foreign = .{
269 .status = .excluded,
270 .detail = "the node owner crosses no foreign boundary",
271 },
272 },
273 .work = .{ .equation = "activation and teardown each visit eight subowners" },
274 .dependencies = &.{
275 "reticulum.destinations",
276 "reticulum.known_identities",
277 "reticulum.known_ratchets",
278 "reticulum.receipts",
279 "reticulum.hashlist",
280 "reticulum.timers",
281 "reticulum.effects",
282 "reticulum.transport",
283 },
284 .obligations = &.{
285 .{ .key = "reticulum_node_capacity", .role = .capacity_model },
286 .{ .key = "reticulum_node_overload", .role = .overload },
287 .{ .key = "reticulum_node_transitive", .role = .transitive_risk },
288 .{ .key = "reticulum_node_work", .role = .work_bound },
289 },
290 },
291 .bindings = .{
292 .owner = @This(),
293 .seal = .{
294 .family = alloc_phase.capacity.selector(@This().activate),
295 .premise = .{ .class = .checked_semantic_fact, .authority = .checker },
296 },
297 .teardown = .{
298 .family = alloc_phase.capacity.selector(@This().deinit),
299 .premise = .{ .class = .checked_semantic_fact, .authority = .checker },
300 },
301 },
302 };
303
304 pub fn init(storage: Storage, selected: Limits) InitError!Node {
305 const capacity = try Capacity.derive(selected);
306 if (storage.len != capacity.storage_bytes) return error.StorageLengthMismatch;
307 const interfaces = initializeInterfaces(storage, capacity);
308 const identities = std.mem.bytesAsSlice(identity.Private, region(
309 storage,
310 capacity.identities_offset,
311 capacity.identity_storage_bytes,
312 ));
313 for (identities) |*private| private.* = identity.Private.fromBytes(@splat(0));
314 const ratchet_bindings = std.mem.bytesAsSlice(RatchetBinding, region(
315 storage,
316 capacity.ratchet_bindings_offset,
317 capacity.ratchet_binding_storage_bytes,
318 ));
319 @memset(ratchet_bindings, .{});
320 const subowners = try initSubowners(storage, capacity, selected);
321 const scratch = storage[capacity.scratch_offset..][0..capacity.scratch_storage_bytes];
322 @memset(scratch, 0);
323 return .{
324 .phase = .initialization,
325 .capacity = capacity,
326 .storage = storage,
327 .interfaces = interfaces,
328 .identities = identities,
329 .ratchet_bindings = ratchet_bindings,
330 .destinations = subowners.destinations,
331 .known_identities = subowners.known_identities,
332 .known_ratchets = subowners.known_ratchets,
333 .receipts = subowners.receipts,
334 .duplicate_hashes = subowners.duplicate_hashes,
335 .timers = subowners.timers,
336 .transport = subowners.transport,
337 .effects = subowners.effects,
338 .scratch = scratch,
339 };
340 }
341
342 pub fn activate(self: *Node) void {
343 std.debug.assert(self.phase == .initialization);
344 self.destinations.activate();
345 self.known_identities.activate();
346 self.known_ratchets.activate();
347 self.receipts.activate();
348 self.duplicate_hashes.activate();
349 self.timers.activate();
350 self.transport.activate();
351 self.effects.activate();
352 self.phase = .steady;
353 }
354
355 pub fn identityAt(self: *Node, index: usize) ?*identity.Private {
356 std.debug.assert(self.phase == .steady);
357 if (index >= self.identities.len) return null;
358 return &self.identities[index];
359 }
360
361 /// Registers one carrier index, so the node accepts frames that arrive on it and, with
362 /// `outgoing` set, sends on it as well. An access code given here authenticates every frame in
363 /// both directions on that carrier. An index at or past the caller's carrier maximum returns
364 /// `error.Index`. Registering the same index again zeroes what was there, so its path discovery
365 /// returns to off.
366 pub fn registerCarrier(
367 self: *Node,
368 index: carrier.Index,
369 outgoing: bool,
370 access_config: ?Access,
371 ) error{Index}!void {
372 std.debug.assert(self.phase == .steady);
373 std.debug.assert(self.interfaces.len > 0);
374 std.debug.assert(self.interfaces.len <= @as(usize, std.math.maxInt(carrier.Index)) + 1);
375 if (access_config) |value| {
376 std.debug.assert(value.size.byte() >= ifac.min_size_bytes);
377 std.debug.assert(@as(usize, wire.mtu) + value.size.byte() <= carrier.frame_bytes_max);
378 }
379 const slot: usize = index;
380 if (slot >= self.interfaces.len) return error.Index;
381 std.crypto.secureZero(u8, std.mem.asBytes(&self.interfaces[slot]));
382 self.interfaces[slot] = .{
383 .registered = 1,
384 .outgoing = @intFromBool(outgoing),
385 .access = access_config,
386 };
387 std.debug.assert(self.interfaces[slot].registered == 1);
388 }
389
390 /// Binds a rotating-key ring to one of the node's own identities, so announces from that
391 /// identity carry a rotating public key and data addressed to it is decrypted with the ring's
392 /// keys first. The ring stays the caller's and has to outlive the node, which holds only a
393 /// pointer to it. With `enforce` set, the node decrypts only with the ring's keys, so a packet
394 /// that none of them open fails. An identity index at or past the caller's destination maximum
395 /// returns `error.Index`.
396 pub fn bindRatchetRing(
397 self: *Node,
398 identity_index: usize,
399 ring: *identity.Ring,
400 enforce: bool,
401 ) error{Index}!void {
402 std.debug.assert(self.phase == .steady);
403 if (identity_index >= self.ratchet_bindings.len) return error.Index;
404 self.ratchet_bindings[identity_index] = .{ .ring = ring, .enforce = enforce };
405 }
406
407 /// Chooses whether a proof this node sends carries the proved packet's hash beside the
408 /// signature or the signature alone. The node starts in implicit mode, with the signature
409 /// alone.
410 pub fn setProofMode(self: *Node, mode: ProofMode) void {
411 std.debug.assert(self.phase == .steady);
412 self.proof_mode = mode;
413 }
414
415 /// Turns the node into one that carries traffic for others and gives it the 16 bytes that name
416 /// it to its neighbors, which Reticulum@1.5.0 RNS/Transport.py:1577-1580,1907 sets. An enabled
417 /// transport node rebroadcasts the announces it accepts and relays the packets that name it as
418 /// the next hop.
419 pub fn setTransport(self: *Node, identity_hash: [16]u8, enabled: bool) void {
420 std.debug.assert(self.phase == .steady);
421 self.transport.configure(identity_hash, enabled);
422 }
423
424 /// Lets the node ask one carrier for a path it lacks, which Reticulum@1.5.0
425 /// RNS/Transport.py:3352-3355 searches. An index at or past the carrier maximum, or one the
426 /// caller has yet to register, returns `error.Index`. Registering that carrier again returns
427 /// the setting to off, per Reticulum@1.5.0 RNS/Interfaces/Interface.py:55,121.
428 pub fn setPathDiscovery(self: *Node, index: carrier.Index, enabled: bool) error{Index}!void {
429 std.debug.assert(self.phase == .steady);
430 const slot: usize = index;
431 if (slot >= self.interfaces.len) return error.Index;
432 if (self.interfaces[slot].registered == 0) return error.Index;
433 self.interfaces[slot].discover_paths = @intFromBool(enabled);
434 std.debug.assert(self.interfaces[slot].discover_paths == @intFromBool(enabled));
435 }
436
437 /// Applies one event and returns the effects it produced. The effects point into the node's own
438 /// storage and stay valid until the next call.
439 pub fn step(self: *Node, value: event.Event) errors.Error![]const effect.Effect {
440 return @import("step.zig").run(self, value);
441 }
442
443 pub fn deinit(self: *Node) Storage {
444 std.debug.assert(self.phase == .steady);
445 _ = self.effects.deinit();
446 _ = self.transport.deinit();
447 _ = self.timers.deinit();
448 _ = self.duplicate_hashes.deinit();
449 _ = self.receipts.deinit();
450 _ = self.known_ratchets.deinit();
451 _ = self.known_identities.deinit();
452 _ = self.destinations.deinit();
453 std.crypto.secureZero(u8, std.mem.sliceAsBytes(self.interfaces));
454 @memset(self.ratchet_bindings, .{});
455 for (self.identities) |*private| private.zero();
456 std.crypto.secureZero(u8, self.scratch);
457 self.phase = .teardown;
458 const storage = self.storage;
459 self.* = undefined;
460 return storage;
461 }
462 };
463
464 fn initializeInterfaces(storage: Node.Storage, capacity: Node.Capacity) []Interface {
465 const interfaces = std.mem.bytesAsSlice(
466 Interface,
467 storage[0..capacity.interface_storage_bytes],
468 );
469 @memset(interfaces, .{});
470 return interfaces;
471 }
472
473 const Subowners = struct {
474 destinations: destination.registry.Table,
475 known_identities: identity.known.Identities,
476 known_ratchets: identity.known.Ratchets,
477 receipts: packet.receipt.Table,
478 duplicate_hashes: packet.hashlist.Table,
479 timers: timer.Table,
480 transport: transport.State,
481 effects: effect.Effects,
482 };
483
484 fn initSubowners(
485 storage: Node.Storage,
486 capacity: Node.Capacity,
487 selected: Node.Limits,
488 ) Node.InitError!Subowners {
489 return .{
490 .destinations = try destination.registry.Table.init(
491 region(storage, capacity.destinations_offset, capacity.destinations.storage_bytes),
492 .{ .destinations_max = selected.destinations_max },
493 ),
494 .known_identities = try identity.known.Identities.init(
495 region(
496 storage,
497 capacity.known_identities_offset,
498 capacity.known_identities.storage_bytes,
499 ),
500 .{ .identities_max = selected.known_identities_max },
501 ),
502 .known_ratchets = try identity.known.Ratchets.init(
503 region(storage, capacity.known_ratchets_offset, capacity.known_ratchets.storage_bytes),
504 .{ .ratchets_max = selected.known_ratchets_max },
505 ),
506 .receipts = try packet.receipt.Table.init(
507 region(storage, capacity.receipts_offset, capacity.receipts.storage_bytes),
508 .{ .receipts_max = selected.receipts_max },
509 ),
510 .duplicate_hashes = try packet.hashlist.Table.init(
511 region(
512 storage,
513 capacity.duplicate_hashes_offset,
514 capacity.duplicate_hashes.storage_bytes,
515 ),
516 .{ .hashes_max = selected.duplicate_hashes_max },
517 ),
518 .timers = try timer.Table.init(
519 region(storage, capacity.timers_offset, capacity.timers.storage_bytes),
520 .{ .timers_max = selected.timers_max },
521 ),
522 .transport = try transport.State.init(
523 region(storage, capacity.transport_offset, capacity.transport.storage_bytes),
524 .{
525 .paths_max = selected.paths_max,
526 .announces_max = selected.announces_max,
527 .reverse_entries_max = selected.reverse_entries_max,
528 .path_request_tags_max = selected.path_request_tags_max,
529 .inflight_requests_max = selected.inflight_requests_max,
530 .discoveries_max = selected.discoveries_max,
531 .links_max = selected.links_max,
532 .link_entries_max = selected.link_entries_max,
533 },
534 ),
535 .effects = try effect.Effects.init(
536 region(storage, capacity.effects_offset, capacity.effects.storage_bytes),
537 .{ .effects_max = selected.effects_max, .frames_max = selected.effect_frames_max },
538 ),
539 };
540 }
541
542 fn region(storage: Node.Storage, offset: usize, length: usize) []align(8) u8 {
543 std.debug.assert(offset % Node.storage_alignment == 0);
544 std.debug.assert(offset <= storage.len);
545 std.debug.assert(length <= storage.len - offset);
546 const pointer: [*]align(Node.storage_alignment) u8 = @ptrCast(
547 @alignCast(storage[offset..].ptr),
548 );
549 return pointer[0..length];
550 }
551
552 comptime {
553 alloc_phase.capacity.requireProvisionedExactOwnerShape(Node);
554 }
555
556 fn smallLimits() limits.Limits {
557 return .{
558 .interfaces_max = 3,
559 .destinations_max = 3,
560 .known_identities_max = 3,
561 .known_ratchets_max = 3,
562 .receipts_max = 3,
563 .duplicate_hashes_max = 3,
564 .timers_max = 3,
565 .effects_max = 3,
566 .effect_frames_max = 3,
567 .paths_max = 3,
568 .announces_max = 3,
569 .reverse_entries_max = 3,
570 .path_request_tags_max = 3,
571 .inflight_requests_max = 3,
572 .discoveries_max = 3,
573 .links_max = 3,
574 .link_entries_max = 3,
575 };
576 }
577
578 test "node carves and activates every bounded subowner" {
579 comptime {
580 @stardustClaim(alloc_phase.capacity.witness(
581 Node,
582 "reticulum_node_capacity",
583 ), null, null, null, null, null, null);
584 @stardustClaim(alloc_phase.capacity.witness(
585 Node,
586 "reticulum_node_overload",
587 ), null, null, null, null, null, null);
588 @stardustClaim(alloc_phase.capacity.witness(
589 Node,
590 "reticulum_node_transitive",
591 ), null, null, null, null, null, null);
592 @stardustClaim(alloc_phase.capacity.witness(
593 Node,
594 "reticulum_node_work",
595 ), null, null, null, null, null, null);
596 }
597 const selected = comptime smallLimits();
598 const capacity = comptime limits.Capacity.derive(selected) catch unreachable;
599 var bytes: [capacity.storage_bytes]u8 align(Node.storage_alignment) = undefined;
600 var node = try Node.init(&bytes, selected);
601 node.activate();
602 defer _ = node.deinit();
603 try std.testing.expectEqual(@as(usize, 3), node.interfaces.len);
604 try std.testing.expectEqual(@as(usize, 3), node.identities.len);
605 try std.testing.expectEqual(@as(usize, 3), node.destinations.capacity.destinations_max);
606 try std.testing.expectEqual(@as(usize, 3), node.effects.capacity.effects_max);
607 try std.testing.expectEqual(@as(usize, 3), node.transport.paths.capacity.paths_max);
608 try std.testing.expectEqual(@as(usize, 3), node.transport.announces.capacity.announces_max);
609 try std.testing.expectEqual(
610 @as(usize, 3),
611 node.transport.reverse_entries.capacity.reverse_entries_max,
612 );
613 try std.testing.expectEqual(@as(usize, 3), node.transport.tags.capacity.hashes_max);
614 try std.testing.expectEqual(
615 @as(usize, 3),
616 node.transport.inflight_requests.capacity.inflight_requests_max,
617 );
618 try std.testing.expectEqual(
619 @as(usize, 3),
620 node.transport.discoveries.capacity.discoveries_max,
621 );
622 try std.testing.expectEqual(@as(usize, 3), node.transport.links.capacity.links_max);
623 try std.testing.expectEqual(@as(usize, wire.mtu), node.scratch.len);
624 }
625
626 test "node path discovery defaults off and rejects an unregistered carrier" {
627 const selected = comptime smallLimits();
628 const capacity = comptime limits.Capacity.derive(selected) catch unreachable;
629 var bytes: [capacity.storage_bytes]u8 align(Node.storage_alignment) = undefined;
630 var node = try Node.init(&bytes, selected);
631 node.activate();
632 defer _ = node.deinit();
633 try std.testing.expectError(error.Index, node.setPathDiscovery(0, true));
634 try node.registerCarrier(0, true, null);
635 try std.testing.expectEqual(@as(u1, 0), node.interfaces[0].discover_paths);
636 try node.setPathDiscovery(0, true);
637 try std.testing.expectEqual(@as(u1, 1), node.interfaces[0].discover_paths);
638 try std.testing.expectError(error.Index, node.setPathDiscovery(3, true));
639 try node.registerCarrier(0, true, null);
640 try std.testing.expectEqual(@as(u1, 0), node.interfaces[0].discover_paths);
641 }
642
643 test "node carrier and rotating-key bindings reject maximum plus one" {
644 const selected = comptime smallLimits();
645 const capacity = comptime limits.Capacity.derive(selected) catch unreachable;
646 var bytes: [capacity.storage_bytes]u8 align(Node.storage_alignment) = undefined;
647 var node = try Node.init(&bytes, selected);
648 node.activate();
649 defer _ = node.deinit();
650 const ring_capacity = comptime identity.Ring.Capacity.derive(.{
651 .retained_max = 1,
652 }) catch unreachable;
653 var ring_bytes: [ring_capacity.storage_bytes]u8 align(identity.Ring.storage_alignment) =
654 undefined;
655 var ring = try identity.Ring.init(&ring_bytes, .{ .retained_max = 1 });
656 ring.activate();
657 defer _ = ring.deinit();
658 for (0..selected.interfaces_max) |index| {
659 try node.registerCarrier(@intCast(index), true, null);
660 try node.bindRatchetRing(index, &ring, false);
661 }
662 try std.testing.expectError(error.Index, node.registerCarrier(3, true, null));
663 try std.testing.expectError(error.Index, node.bindRatchetRing(3, &ring, false));
664 }
665
666 test "node keyed carrier registrations reject maximum plus one and zero on teardown" {
667 const selected = comptime smallLimits();
668 const capacity = comptime limits.Capacity.derive(selected) catch unreachable;
669 var bytes: [capacity.storage_bytes]u8 align(Node.storage_alignment) = undefined;
670 var node = try Node.init(&bytes, selected);
671 node.activate();
672 const keyed = Access{ .key = @splat(0xa5), .size = .bytes_64 };
673 for (0..selected.interfaces_max) |index| {
674 try node.registerCarrier(@intCast(index), true, keyed);
675 try std.testing.expect(node.interfaces[index].access != null);
676 }
677 try std.testing.expectError(error.Index, node.registerCarrier(3, true, keyed));
678 const storage = node.deinit();
679 try std.testing.expect(std.mem.allEqual(u8, storage[0..capacity.interface_storage_bytes], 0));
680 }