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 }