lib/reticulum/src/node/test.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 test {
   2     _ = @import("effect.zig");
   3 }
   4 
   5 test {
   6     _ = @import("timer.zig");
   7 }
   8 
   9 test {
  10     _ = @import("limits.zig");
  11 }
  12 
  13 test {
  14     _ = @import("owner.zig");
  15 }
  16 
  17 test {
  18     _ = @import("transport/path.zig");
  19     _ = @import("transport/rewrite.zig");
  20     _ = @import("transport/announces.zig");
  21     _ = @import("transport/reverse.zig");
  22     _ = @import("transport/state.zig");
  23     _ = @import("transport/inflight.zig");
  24     _ = @import("transport/discoveries.zig");
  25     _ = @import("transport/requests.zig");
  26     _ = @import("transport/links.zig");
  27 }
  28 
  29 test {
  30     _ = @import("announce.zig");
  31     _ = @import("inbound.zig");
  32     _ = @import("outbound.zig");
  33     _ = @import("link.zig");
  34     _ = @import("step.zig");
  35 }
  36 
  37 const std = @import("std");
  38 const pretty = @import("pretty");
  39 const reticulum = @import("../root.zig");
  40 
  41 const node_limits = reticulum.node.Limits{
  42     .interfaces_max = 1,
  43     .destinations_max = 3,
  44     .known_identities_max = 4,
  45     .known_ratchets_max = 4,
  46     .receipts_max = 2,
  47     .duplicate_hashes_max = 16,
  48     .timers_max = 3,
  49     .effects_max = 4,
  50     .effect_frames_max = 4,
  51     .paths_max = 4,
  52     .announces_max = 2,
  53     .reverse_entries_max = 2,
  54     .path_request_tags_max = 4,
  55     .inflight_requests_max = 2,
  56     .discoveries_max = 2,
  57     .links_max = 2,
  58     .link_entries_max = 2,
  59 };
  60 const node_capacity = reticulum.node.Capacity.derive(node_limits) catch unreachable;
  61 
  62 fn initNodeWithAccess(
  63     storage: *align(8) [node_capacity.storage_bytes]u8,
  64     access: ?reticulum.node.Access,
  65 ) !reticulum.node.Node {
  66     var owner = try reticulum.node.Node.init(storage[0..], node_limits);
  67     owner.activate();
  68     try owner.registerCarrier(0, true, access);
  69     return owner;
  70 }
  71 
  72 fn initNode(storage: *align(8) [node_capacity.storage_bytes]u8) !reticulum.node.Node {
  73     return initNodeWithAccess(storage, null);
  74 }
  75 
  76 fn accessConfiguration(size: reticulum.interface.ifac.Size) !reticulum.node.Access {
  77     return .{
  78         .key = try reticulum.interface.ifac.derive("tiny", "secret"),
  79         .size = size,
  80     };
  81 }
  82 
  83 fn packetVector(name: []const u8) reticulum.conformance.packet.Vector {
  84     for (reticulum.conformance.packet.vectors) |vector| {
  85         if (std.mem.eql(u8, vector.name, name)) return vector;
  86     }
  87     unreachable;
  88 }
  89 
  90 fn announceVector(name: []const u8) reticulum.conformance.announce.Vector {
  91     for (reticulum.conformance.announce.vectors) |vector| {
  92         if (std.mem.eql(u8, vector.name, name)) return vector;
  93     }
  94     unreachable;
  95 }
  96 
  97 fn expectCorpusBytes(
  98     vector_name: []const u8,
  99     field_name: []const u8,
 100     expected: []const u8,
 101     actual: []const u8,
 102 ) !void {
 103     if (std.mem.eql(u8, expected, actual)) return;
 104     var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
 105     defer arena.deinit();
 106     var report = try pretty.diagnostic.Report.init(arena.allocator(), "Node corpus mismatch");
 107     defer report.deinit();
 108     try report.field("vector", "{s}", .{vector_name});
 109     try report.field("field", "{s}", .{field_name});
 110     try report.field("expected", "{any}", .{expected});
 111     try report.field("actual", "{any}", .{actual});
 112     pretty.diagnostic.writeStderr(&report, .{ .width = 100 });
 113     return error.ConformanceMismatch;
 114 }
 115 
 116 fn expectCorpusValue(
 117     vector_name: []const u8,
 118     field_name: []const u8,
 119     expected: anytype,
 120     actual: @TypeOf(expected),
 121 ) !void {
 122     if (std.meta.eql(expected, actual)) return;
 123     var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
 124     defer arena.deinit();
 125     var report = try pretty.diagnostic.Report.init(arena.allocator(), "Node corpus mismatch");
 126     defer report.deinit();
 127     try report.field("vector", "{s}", .{vector_name});
 128     try report.field("field", "{s}", .{field_name});
 129     try report.field("expected", "{any}", .{expected});
 130     try report.field("actual", "{any}", .{actual});
 131     pretty.diagnostic.writeStderr(&report, .{ .width = 100 });
 132     return error.ConformanceMismatch;
 133 }
 134 
 135 fn sentFrame(effects: []const reticulum.node.Effect) ?[]const u8 {
 136     for (effects) |effect| switch (effect) {
 137         .carrier_send => |send| return send.frame,
 138         else => {},
 139     };
 140     return null;
 141 }
 142 
 143 fn expectSentFrame(effects: []const reticulum.node.Effect) ![]const u8 {
 144     const frame = sentFrame(effects);
 145     try std.testing.expect(frame != null);
 146     return frame.?;
 147 }
 148 
 149 fn expectEffectTag(
 150     effect: reticulum.node.Effect,
 151     expected: std.meta.Tag(reticulum.node.Effect),
 152 ) !void {
 153     try std.testing.expectEqual(expected, std.meta.activeTag(effect));
 154 }
 155 
 156 const loop_capacity = reticulum.carrier.Memory.Capacity.derive(.{
 157     .frames_max = 4,
 158 }) catch unreachable;
 159 
 160 fn loopback(
 161     effects: []const reticulum.node.Effect,
 162     receiver: *reticulum.node.Node,
 163     now: reticulum.node.Seconds,
 164 ) ![]const reticulum.node.Effect {
 165     var bytes: [loop_capacity.storage_bytes]u8 align(8) = undefined;
 166     var memory = try reticulum.carrier.Memory.init(&bytes, .{ .frames_max = 4 });
 167     memory.activate();
 168     defer _ = memory.deinit();
 169     for (effects) |effect| switch (effect) {
 170         .carrier_send => |send| try memory.push(try reticulum.carrier.Frame.init(send.frame)),
 171         else => {},
 172     };
 173     if (memory.count() != 1) return error.ExpectedOneCarrierFrame;
 174     const frame = memory.pop().?;
 175     return receiver.step(.{ .carrier_frame = .{
 176         .interface = 0,
 177         .now = now,
 178         .bytes = frame.slice(),
 179         .entropy = @splat(0),
 180     } });
 181 }
 182 
 183 fn registerPacketSingle(
 184     owner: *reticulum.node.Node,
 185     strategy: reticulum.destination.registry.ProofStrategy,
 186 ) !void {
 187     const vector = packetVector("data-single-hop-0");
 188     owner.identities[0] = reticulum.identity.Private.fromBytes(
 189         vector.destination_private_key[0..64].*,
 190     );
 191     try owner.destinations.register(.{
 192         .hash = vector.destination_hash[0..16].*,
 193         .kind = .single,
 194         .proof_strategy = strategy,
 195         .identity_index = 0,
 196     });
 197 }
 198 
 199 fn rememberPacketSingle(owner: *reticulum.node.Node, now: u64) !void {
 200     const vector = packetVector("data-single-hop-0");
 201     var private = reticulum.identity.Private.fromBytes(vector.destination_private_key[0..64].*);
 202     defer private.zero();
 203     try owner.known_identities.remember(.{
 204         .destination_hash = vector.destination_hash[0..16].*,
 205         .public_key = private.publicBytes(),
 206         .announce_packet_hash = @splat(0),
 207         .received = now,
 208     });
 209 }
 210 
 211 fn testLimits() reticulum.node.Limits {
 212     return .{
 213         .interfaces_max = 1,
 214         .destinations_max = 1,
 215         .known_identities_max = 1,
 216         .known_ratchets_max = 1,
 217         .receipts_max = 1,
 218         .duplicate_hashes_max = 1,
 219         .timers_max = 1,
 220         .effects_max = 2,
 221         .effect_frames_max = 1,
 222         .paths_max = 1,
 223         .announces_max = 1,
 224         .reverse_entries_max = 1,
 225         .path_request_tags_max = 1,
 226         .inflight_requests_max = 1,
 227         .discoveries_max = 1,
 228         .links_max = 1,
 229         .link_entries_max = 1,
 230     };
 231 }
 232 
 233 test "carrier memory moves one node send effect into another node frame event" {
 234     const limits = comptime testLimits();
 235     const capacity = comptime reticulum.node.Capacity.derive(limits) catch unreachable;
 236     var first_bytes: [capacity.storage_bytes]u8 align(8) = undefined;
 237     var second_bytes: [capacity.storage_bytes]u8 align(8) = undefined;
 238     var first = try reticulum.node.Node.init(&first_bytes, limits);
 239     var second = try reticulum.node.Node.init(&second_bytes, limits);
 240     first.activate();
 241     second.activate();
 242     defer _ = first.deinit();
 243     defer _ = second.deinit();
 244     try first.effects.push(.{ .carrier_send = .{
 245         .interface = 0,
 246         .frame = "loopback",
 247     } });
 248     const memory_capacity = comptime reticulum.carrier.Memory.Capacity.derive(.{
 249         .frames_max = 1,
 250     }) catch unreachable;
 251     var memory_bytes: [memory_capacity.storage_bytes]u8 align(8) = undefined;
 252     var memory = try reticulum.carrier.Memory.init(&memory_bytes, .{ .frames_max = 1 });
 253     memory.activate();
 254     defer _ = memory.deinit();
 255     const send = first.effects.items()[0].carrier_send;
 256     try memory.push(try reticulum.carrier.Frame.init(send.frame));
 257     const frame = memory.pop().?;
 258     const inbound = reticulum.node.Event{ .carrier_frame = .{
 259         .interface = send.interface,
 260         .now = 7,
 261         .bytes = frame.slice(),
 262         .entropy = @splat(0),
 263     } };
 264     try std.testing.expectEqual(@as(usize, 1), second.interfaces.len);
 265     try std.testing.expectEqualSlices(u8, "loopback", inbound.carrier_frame.bytes);
 266 }
 267 
 268 test "undecodable frame is rejected" {
 269     const limits = comptime testLimits();
 270     const capacity = comptime reticulum.node.Capacity.derive(limits) catch unreachable;
 271     var bytes: [capacity.storage_bytes]u8 align(8) = undefined;
 272     var owner = try reticulum.node.Node.init(&bytes, limits);
 273     owner.activate();
 274     defer _ = owner.deinit();
 275     try owner.registerCarrier(0, true, null);
 276     const effects = try owner.step(.{ .carrier_frame = .{
 277         .interface = 0,
 278         .now = 7,
 279         .bytes = "x",
 280         .entropy = @splat(0),
 281     } });
 282     try std.testing.expectEqual(@as(usize, 1), effects.len);
 283     try expectEffectTag(effects[0], .diagnostic);
 284     try std.testing.expectEqual(reticulum.node.Code.malformed_packet, effects[0].diagnostic.code);
 285 }
 286 
 287 test "Reticulum@1.5.0 RNS/Packet.py:178-242 outbound node matches single data corpus" {
 288     const vector = packetVector("data-single-hop-0");
 289     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 290     var owner = try initNode(&storage);
 291     defer _ = owner.deinit();
 292     var private = reticulum.identity.Private.fromBytes(vector.destination_private_key[0..64].*);
 293     defer private.zero();
 294     try owner.known_identities.remember(.{
 295         .destination_hash = vector.destination_hash[0..16].*,
 296         .public_key = private.publicBytes(),
 297         .announce_packet_hash = @splat(0),
 298         .received = 1,
 299     });
 300     const effects = try owner.step(.{ .application_send = .{
 301         .destination = vector.destination_hash[0..16].*,
 302         .now = 2,
 303         .plaintext = vector.plaintext,
 304         .ephemeral_private = vector.ephemeral_private_key[0..32].*,
 305         .iv = vector.iv[0..16].*,
 306     } });
 307     const frame = sentFrame(effects) orelse return error.MissingCarrierSend;
 308     try expectCorpusBytes(vector.name, "raw", vector.raw, frame);
 309     try std.testing.expect(owner.receipts.find(vector.packet_hash[0..32].*) != null);
 310 }
 311 
 312 fn registerAnnounceOwner(
 313     owner: *reticulum.node.Node,
 314     vector: reticulum.conformance.announce.Vector,
 315 ) !void {
 316     owner.identities[0] = reticulum.identity.Private.fromBytes(vector.private_key[0..64].*);
 317     try owner.destinations.register(.{
 318         .hash = vector.destination_hash[0..16].*,
 319         .name_hash = vector.name_hash[0..10].*,
 320         .kind = .single,
 321         .proof_strategy = .all,
 322         .identity_index = 0,
 323     });
 324 }
 325 
 326 test "Reticulum@1.5.0 RNS/Destination.py:283 node matches identity announce corpus" {
 327     const vector = announceVector("identity-app-data");
 328     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 329     var owner = try initNode(&storage);
 330     defer _ = owner.deinit();
 331     try registerAnnounceOwner(&owner, vector);
 332     const effects = try owner.step(.{ .application_announce = .{
 333         .destination = vector.destination_hash[0..16].*,
 334         .app_data = vector.app_data,
 335         .random = vector.random_hash[0..5].*,
 336         .fresh_rotating_key = null,
 337         .now = vector.clock,
 338     } });
 339     try expectCorpusBytes(vector.name, "raw", vector.raw, try expectSentFrame(effects));
 340 }
 341 
 342 test "Reticulum@1.5.0 RNS/Destination.py:285-312 node matches rotating announce corpus" {
 343     const vector = announceVector("ratchet-app-data");
 344     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 345     var owner = try initNode(&storage);
 346     defer _ = owner.deinit();
 347     try registerAnnounceOwner(&owner, vector);
 348     var ring_bytes: [ring_capacity.storage_bytes]u8 align(8) = undefined;
 349     var ring = try reticulum.identity.Ring.init(&ring_bytes, .{ .retained_max = 2 });
 350     ring.activate();
 351     defer _ = ring.deinit();
 352     try owner.bindRatchetRing(0, &ring, false);
 353     const effects = try owner.step(.{ .application_announce = .{
 354         .destination = vector.destination_hash[0..16].*,
 355         .app_data = vector.app_data,
 356         .random = vector.random_hash[0..5].*,
 357         .fresh_rotating_key = vector.ratchet_private_key[0..32].*,
 358         .now = vector.clock,
 359     } });
 360     try expectCorpusBytes(vector.name, "raw", vector.raw, try expectSentFrame(effects));
 361 }
 362 
 363 const ring_capacity = reticulum.identity.Ring.Capacity.derive(.{
 364     .retained_max = 2,
 365 }) catch unreachable;
 366 
 367 fn announceRotating(
 368     sender: *reticulum.node.Node,
 369     receiver: *reticulum.node.Node,
 370     ring: *reticulum.identity.Ring,
 371 ) ![]const reticulum.node.Effect {
 372     const vector = announceVector("ratchet-app-data");
 373     try registerAnnounceOwner(sender, vector);
 374     try sender.bindRatchetRing(0, ring, true);
 375     const effects = try sender.step(.{ .application_announce = .{
 376         .destination = vector.destination_hash[0..16].*,
 377         .app_data = vector.app_data,
 378         .random = vector.random_hash[0..5].*,
 379         .fresh_rotating_key = vector.ratchet_private_key[0..32].*,
 380         .now = vector.clock,
 381     } });
 382     return loopback(effects, receiver, vector.clock + 1);
 383 }
 384 
 385 test "announce over loopback remembers identity and rotating key" {
 386     const vector = announceVector("ratchet-app-data");
 387     var first_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 388     var second_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 389     var first = try initNode(&first_storage);
 390     var second = try initNode(&second_storage);
 391     defer _ = first.deinit();
 392     defer _ = second.deinit();
 393     var ring_bytes: [ring_capacity.storage_bytes]u8 align(8) = undefined;
 394     var ring = try reticulum.identity.Ring.init(&ring_bytes, .{ .retained_max = 2 });
 395     ring.activate();
 396     defer _ = ring.deinit();
 397     const effects = try announceRotating(&first, &second, &ring);
 398     try std.testing.expectEqual(@as(usize, 1), effects.len);
 399     try expectEffectTag(effects[0], .announce_received);
 400     const received = effects[0].announce_received;
 401     try std.testing.expectEqualSlices(u8, vector.app_data, received.app_data);
 402     try std.testing.expect(received.rotating_key_present);
 403     const known_optional = second.known_identities.recall(vector.destination_hash[0..16].*);
 404     try std.testing.expect(known_optional != null);
 405     const known = known_optional.?;
 406     try std.testing.expectEqualSlices(u8, vector.public_key, &known.public_key);
 407     const rotating_optional = second.known_ratchets.get(
 408         vector.destination_hash[0..16].*,
 409         vector.clock + 1,
 410     );
 411     try std.testing.expect(rotating_optional != null);
 412     const rotating = rotating_optional.?;
 413     try std.testing.expectEqualSlices(u8, vector.ratchet_public_key, &rotating);
 414 }
 415 
 416 fn expectProvedSingle(mode: reticulum.node.ProofMode) !void {
 417     const announce_vector = announceVector("ratchet-app-data");
 418     const data_vector = packetVector("data-single-hop-0");
 419     var first_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 420     var second_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 421     var first = try initNode(&first_storage);
 422     var second = try initNode(&second_storage);
 423     defer _ = first.deinit();
 424     defer _ = second.deinit();
 425     var ring_bytes: [ring_capacity.storage_bytes]u8 align(8) = undefined;
 426     var ring = try reticulum.identity.Ring.init(&ring_bytes, .{ .retained_max = 2 });
 427     ring.activate();
 428     defer _ = ring.deinit();
 429     _ = try announceRotating(&first, &second, &ring);
 430     first.setProofMode(mode);
 431     const sent_at = announce_vector.clock + 2;
 432     const outgoing = try second.step(.{ .application_send = .{
 433         .destination = announce_vector.destination_hash[0..16].*,
 434         .now = sent_at,
 435         .plaintext = "proved loopback",
 436         .ephemeral_private = data_vector.ephemeral_private_key[0..32].*,
 437         .iv = data_vector.iv[0..16].*,
 438     } });
 439     const hash = try reticulum.wire.hash.full(try expectSentFrame(outgoing));
 440     const delivery = try loopback(outgoing, &first, sent_at + 1);
 441     try std.testing.expect(delivery.len > 0);
 442     try expectEffectTag(delivery[0], .application_delivery);
 443     try std.testing.expectEqualSlices(
 444         u8,
 445         "proved loopback",
 446         delivery[0].application_delivery.plaintext,
 447     );
 448     try std.testing.expectEqual(ring.latestId(), delivery[0].application_delivery.ratchet_id);
 449     const update = try loopback(delivery, &second, sent_at + 4);
 450     try std.testing.expect(update.len > 0);
 451     try expectEffectTag(update[0], .receipt_update);
 452     try std.testing.expectEqual(
 453         reticulum.packet.receipt.Status.delivered,
 454         update[0].receipt_update.status,
 455     );
 456     try std.testing.expectEqual(@as(?u64, 4), update[0].receipt_update.rtt);
 457     try std.testing.expectEqualSlices(u8, &hash, &update[0].receipt_update.packet_hash);
 458     try std.testing.expectEqual(@as(usize, 0), second.receipts.count());
 459 }
 460 
 461 test "single data over loopback delivers plaintext and proves" {
 462     try expectProvedSingle(.implicit);
 463 }
 464 
 465 test "explicit proof matches by packet hash" {
 466     try expectProvedSingle(.explicit);
 467 }
 468 
 469 fn protectedRaw(
 470     access: *const reticulum.node.Access,
 471     frame: []const u8,
 472     output: *[reticulum.wire.mtu]u8,
 473 ) ![]const u8 {
 474     try std.testing.expect(frame.len > 0);
 475     try std.testing.expect(reticulum.interface.ifac.hasFlag(frame[0]));
 476     const raw = try reticulum.interface.ifac.strip(
 477         &access.key,
 478         access.size,
 479         frame,
 480         output,
 481     );
 482     const size: usize = access.size.byte();
 483     try std.testing.expectEqual(frame.len, raw.len + size);
 484     try std.testing.expect(!std.mem.eql(u8, frame[2 + size ..], raw[2..]));
 485     return raw;
 486 }
 487 
 488 fn expectProtectedFrames(
 489     effects: []const reticulum.node.Effect,
 490     access: *const reticulum.node.Access,
 491 ) !void {
 492     var count: usize = 0;
 493     var raw: [reticulum.wire.mtu]u8 = undefined;
 494     for (effects) |effect| switch (effect) {
 495         .carrier_send => |send| {
 496             _ = try protectedRaw(access, send.frame, &raw);
 497             count += 1;
 498         },
 499         else => {},
 500     };
 501     try std.testing.expectEqual(@as(usize, 1), count);
 502 }
 503 
 504 fn expectKeyedExchange(size: reticulum.interface.ifac.Size) !void {
 505     const announce_vector = announceVector("identity-app-data");
 506     const data_vector = packetVector("data-single-hop-0");
 507     const access = try accessConfiguration(size);
 508     var first_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 509     var second_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 510     var first = try initNodeWithAccess(&first_storage, access);
 511     var second = try initNodeWithAccess(&second_storage, access);
 512     defer _ = first.deinit();
 513     defer _ = second.deinit();
 514 
 515     const announced = try sendIdentityAnnounce(&first);
 516     try expectProtectedFrames(announced, &access);
 517     const remembered = try loopback(announced, &second, announce_vector.clock + 1);
 518     try expectEffectTag(remembered[0], .announce_received);
 519     try std.testing.expect(
 520         second.known_identities.recall(announce_vector.destination_hash[0..16].*) != null,
 521     );
 522 
 523     const sent_at = announce_vector.clock + 2;
 524     const outgoing = try second.step(.{ .application_send = .{
 525         .destination = announce_vector.destination_hash[0..16].*,
 526         .now = sent_at,
 527         .plaintext = "keyed proof",
 528         .ephemeral_private = data_vector.ephemeral_private_key[0..32].*,
 529         .iv = data_vector.iv[0..16].*,
 530     } });
 531     try expectProtectedFrames(outgoing, &access);
 532     var data_raw_storage: [reticulum.wire.mtu]u8 = undefined;
 533     const data_raw = try protectedRaw(&access, try expectSentFrame(outgoing), &data_raw_storage);
 534     const data_hash = try reticulum.wire.hash.full(data_raw);
 535     const delivery = try loopback(outgoing, &first, sent_at + 1);
 536     try expectEffectTag(delivery[0], .application_delivery);
 537     try std.testing.expectEqualStrings("keyed proof", delivery[0].application_delivery.plaintext);
 538     try expectProtectedFrames(delivery, &access);
 539     const update = try loopback(delivery, &second, sent_at + 4);
 540     try expectEffectTag(update[0], .receipt_update);
 541     try std.testing.expectEqual(
 542         reticulum.packet.receipt.Status.delivered,
 543         update[0].receipt_update.status,
 544     );
 545     try std.testing.expectEqualSlices(u8, &data_hash, &update[0].receipt_update.packet_hash);
 546 }
 547 
 548 test "Reticulum@1.5.0 RNS/Transport.py:1244-1266 keyed loopback sizes 1 16 and 64" {
 549     const sizes = [_]reticulum.interface.ifac.Size{ .bytes_1, .bytes_16, .bytes_64 };
 550     for (sizes) |size| try expectKeyedExchange(size);
 551 }
 552 
 553 fn setupPacketPair(
 554     receiver: *reticulum.node.Node,
 555     sender: *reticulum.node.Node,
 556     strategy: reticulum.destination.registry.ProofStrategy,
 557 ) !void {
 558     try registerPacketSingle(receiver, strategy);
 559     try rememberPacketSingle(sender, 1);
 560 }
 561 
 562 fn sendPacketSingle(
 563     sender: *reticulum.node.Node,
 564     now: u64,
 565     plaintext: []const u8,
 566 ) ![]const reticulum.node.Effect {
 567     const vector = packetVector("data-single-hop-0");
 568     return sender.step(.{ .application_send = .{
 569         .destination = vector.destination_hash[0..16].*,
 570         .now = now,
 571         .plaintext = plaintext,
 572         .ephemeral_private = vector.ephemeral_private_key[0..32].*,
 573         .iv = vector.iv[0..16].*,
 574     } });
 575 }
 576 
 577 test "PROVE_APP proves only after application_prove" {
 578     const vector = packetVector("data-single-hop-0");
 579     var first_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 580     var second_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 581     var first = try initNode(&first_storage);
 582     var second = try initNode(&second_storage);
 583     defer _ = first.deinit();
 584     defer _ = second.deinit();
 585     try setupPacketPair(&first, &second, .app);
 586     const outgoing = try sendPacketSingle(&second, 10, "application proof");
 587     const hash = try reticulum.wire.hash.full(try expectSentFrame(outgoing));
 588     const delivery = try loopback(outgoing, &first, 11);
 589     try std.testing.expect(delivery.len > 0);
 590     try expectEffectTag(delivery[0], .application_delivery);
 591     try std.testing.expect(delivery[0].application_delivery.proof_requested);
 592     try std.testing.expect(sentFrame(delivery) == null);
 593     const proof = try first.step(.{ .application_prove = .{
 594         .destination = vector.destination_hash[0..16].*,
 595         .packet_hash = hash,
 596         .interface = 0,
 597         .now = 12,
 598     } });
 599     const update = try loopback(proof, &second, 13);
 600     try std.testing.expect(update.len > 0);
 601     try expectEffectTag(update[0], .receipt_update);
 602     try std.testing.expectEqual(
 603         reticulum.packet.receipt.Status.delivered,
 604         update[0].receipt_update.status,
 605     );
 606 }
 607 
 608 test "PROVE_NONE never proves" {
 609     var first_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 610     var second_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 611     var first = try initNode(&first_storage);
 612     var second = try initNode(&second_storage);
 613     defer _ = first.deinit();
 614     defer _ = second.deinit();
 615     try setupPacketPair(&first, &second, .none);
 616     const outgoing = try sendPacketSingle(&second, 10, "without proof");
 617     const delivery = try loopback(outgoing, &first, 11);
 618     try std.testing.expect(delivery.len > 0);
 619     try expectEffectTag(delivery[0], .application_delivery);
 620     try std.testing.expect(!delivery[0].application_delivery.proof_requested);
 621     try std.testing.expect(sentFrame(delivery) == null);
 622     try std.testing.expectEqual(@as(usize, 1), second.receipts.count());
 623     try std.testing.expectEqual(
 624         reticulum.packet.receipt.Status.sent,
 625         second.receipts.entries[0].status,
 626     );
 627 }
 628 
 629 test "unproved data receipt fails on its deadline" {
 630     var first_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 631     var second_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 632     var first = try initNode(&first_storage);
 633     var second = try initNode(&second_storage);
 634     defer _ = first.deinit();
 635     defer _ = second.deinit();
 636     try setupPacketPair(&first, &second, .none);
 637     const outgoing = try sendPacketSingle(&second, 10, "timeout");
 638     var expiry: ?reticulum.node.TimerExpired = null;
 639     for (outgoing) |effect| switch (effect) {
 640         .schedule_timer => |scheduled| expiry = .{
 641             .id = scheduled.id,
 642             .now = scheduled.at,
 643             .entropy = @splat(0),
 644         },
 645         else => {},
 646     };
 647     try std.testing.expect(expiry != null);
 648     const effects = try second.step(.{ .timer_expired = expiry.? });
 649     try std.testing.expectEqual(@as(usize, 1), effects.len);
 650     try expectEffectTag(effects[0], .receipt_update);
 651     try std.testing.expectEqual(
 652         reticulum.packet.receipt.Status.failed,
 653         effects[0].receipt_update.status,
 654     );
 655     try std.testing.expectEqual(@as(usize, 0), second.receipts.count());
 656 }
 657 
 658 fn registerGroup(owner: *reticulum.node.Node, hash: [16]u8, key: [64]u8) !void {
 659     try owner.destinations.register(.{
 660         .hash = hash,
 661         .kind = .group,
 662         .proof_strategy = .none,
 663         .group_key = key,
 664     });
 665 }
 666 
 667 fn registerPlain(owner: *reticulum.node.Node, hash: [16]u8) !void {
 668     try owner.destinations.register(.{
 669         .hash = hash,
 670         .kind = .plain,
 671         .proof_strategy = .none,
 672     });
 673 }
 674 
 675 test "group data over loopback delivers without a receipt" {
 676     const vector = packetVector("data-group-hop-1");
 677     const hash = vector.destination_hash[0..16].*;
 678     const key = vector.destination_key[0..64].*;
 679     var first_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 680     var second_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 681     var first = try initNode(&first_storage);
 682     var second = try initNode(&second_storage);
 683     defer _ = first.deinit();
 684     defer _ = second.deinit();
 685     try registerGroup(&first, hash, key);
 686     try registerGroup(&second, hash, key);
 687     const outgoing = try second.step(.{ .application_send = .{
 688         .destination = hash,
 689         .now = 10,
 690         .plaintext = "group loopback",
 691         .ephemeral_private = @splat(0),
 692         .iv = vector.iv[0..16].*,
 693         .create_receipt = false,
 694     } });
 695     const delivery = try loopback(outgoing, &first, 11);
 696     try std.testing.expect(delivery.len > 0);
 697     try expectEffectTag(delivery[0], .application_delivery);
 698     try std.testing.expectEqualSlices(
 699         u8,
 700         "group loopback",
 701         delivery[0].application_delivery.plaintext,
 702     );
 703     try std.testing.expectEqual(@as(usize, 0), second.receipts.count());
 704 }
 705 
 706 test "plain data over loopback delivers without a receipt" {
 707     const hash: [16]u8 = @splat(0x73);
 708     var first_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 709     var second_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 710     var first = try initNode(&first_storage);
 711     var second = try initNode(&second_storage);
 712     defer _ = first.deinit();
 713     defer _ = second.deinit();
 714     try registerPlain(&first, hash);
 715     try registerPlain(&second, hash);
 716     const outgoing = try second.step(.{ .application_send = .{
 717         .destination = hash,
 718         .now = 10,
 719         .plaintext = "plain loopback",
 720         .ephemeral_private = @splat(0),
 721         .iv = @splat(0),
 722     } });
 723     const delivery = try loopback(outgoing, &first, 11);
 724     try std.testing.expect(delivery.len > 0);
 725     try expectEffectTag(delivery[0], .application_delivery);
 726     try std.testing.expectEqualSlices(
 727         u8,
 728         "plain loopback",
 729         delivery[0].application_delivery.plaintext,
 730     );
 731     try std.testing.expectEqual(@as(usize, 0), second.receipts.count());
 732 }
 733 
 734 fn sendIdentityAnnounce(owner: *reticulum.node.Node) ![]const reticulum.node.Effect {
 735     const vector = announceVector("identity-app-data");
 736     try registerAnnounceOwner(owner, vector);
 737     return owner.step(.{ .application_announce = .{
 738         .destination = vector.destination_hash[0..16].*,
 739         .app_data = vector.app_data,
 740         .random = vector.random_hash[0..5].*,
 741         .fresh_rotating_key = null,
 742         .now = vector.clock,
 743     } });
 744 }
 745 
 746 test "own announce over loopback is dropped after validation" {
 747     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 748     var owner = try initNode(&storage);
 749     defer _ = owner.deinit();
 750     const outgoing = try sendIdentityAnnounce(&owner);
 751     const effects = try loopback(outgoing, &owner, 1_700_000_002);
 752     try std.testing.expectEqual(@as(usize, 1), effects.len);
 753     try expectEffectTag(effects[0], .diagnostic);
 754     try std.testing.expectEqual(reticulum.node.Code.own_announce, effects[0].diagnostic.code);
 755 }
 756 
 757 test "announce with changed public key is rejected" {
 758     const vector = announceVector("identity-app-data");
 759     var first_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 760     var second_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 761     var first = try initNode(&first_storage);
 762     var second = try initNode(&second_storage);
 763     defer _ = first.deinit();
 764     defer _ = second.deinit();
 765     try second.known_identities.remember(.{
 766         .destination_hash = vector.destination_hash[0..16].*,
 767         .public_key = @splat(0xa5),
 768         .announce_packet_hash = @splat(0),
 769         .received = 1,
 770     });
 771     const outgoing = try sendIdentityAnnounce(&first);
 772     const effects = try loopback(outgoing, &second, vector.clock + 1);
 773     try std.testing.expectEqual(@as(usize, 1), effects.len);
 774     try expectEffectTag(effects[0], .diagnostic);
 775     try std.testing.expectEqual(
 776         reticulum.node.Code.announce_key_changed,
 777         effects[0].diagnostic.code,
 778     );
 779 }
 780 
 781 fn sendIndexedEffects(owner: *reticulum.node.Node, index: u8) ![]const reticulum.node.Effect {
 782     const vector = packetVector("data-single-hop-0");
 783     var iv = vector.iv[0..16].*;
 784     iv[15] +%= index;
 785     const plaintext = [1]u8{index};
 786     return owner.step(.{ .application_send = .{
 787         .destination = vector.destination_hash[0..16].*,
 788         .now = index,
 789         .plaintext = &plaintext,
 790         .ephemeral_private = vector.ephemeral_private_key[0..32].*,
 791         .iv = iv,
 792     } });
 793 }
 794 
 795 fn sendIndexed(owner: *reticulum.node.Node, index: u8) !reticulum.packet.Hash {
 796     const effects = try sendIndexedEffects(owner, index);
 797     return reticulum.wire.hash.full(try expectSentFrame(effects));
 798 }
 799 
 800 test "receipt table at receipts_max culls before insert" {
 801     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 802     var owner = try initNode(&storage);
 803     defer _ = owner.deinit();
 804     try rememberPacketSingle(&owner, 0);
 805     const first = try sendIndexed(&owner, 1);
 806     _ = try sendIndexed(&owner, 2);
 807     try std.testing.expectEqual(node_limits.receipts_max, owner.receipts.count());
 808     _ = try sendIndexed(&owner, 3);
 809     try std.testing.expect(owner.receipts.find(first) == null);
 810     try std.testing.expectEqual(node_limits.receipts_max, owner.receipts.count());
 811 }
 812 
 813 test "full receipt table reports the culled receipt" {
 814     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 815     var owner = try initNode(&storage);
 816     defer _ = owner.deinit();
 817     try rememberPacketSingle(&owner, 0);
 818     const oldest = try sendIndexed(&owner, 1);
 819     for (2..node_limits.receipts_max + 1) |index| {
 820         _ = try sendIndexed(&owner, @intCast(index));
 821     }
 822     try std.testing.expectEqual(node_limits.receipts_max, owner.receipts.count());
 823     const effects = try sendIndexedEffects(&owner, node_limits.receipts_max + 1);
 824     var culled: usize = 0;
 825     for (effects) |effect| switch (effect) {
 826         .receipt_update => |update| {
 827             try std.testing.expectEqualSlices(u8, &oldest, &update.packet_hash);
 828             try std.testing.expectEqual(reticulum.packet.receipt.Status.culled, update.status);
 829             try std.testing.expectEqual(@as(?u64, null), update.rtt);
 830             culled += 1;
 831         },
 832         else => {},
 833     };
 834     try std.testing.expectEqual(@as(usize, 1), culled);
 835 }
 836 
 837 test "receipts_max plus one send" {
 838     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 839     var owner = try initNode(&storage);
 840     defer _ = owner.deinit();
 841     try rememberPacketSingle(&owner, 0);
 842     for (1..node_limits.receipts_max + 2) |index| {
 843         _ = try sendIndexed(&owner, @intCast(index));
 844     }
 845     try std.testing.expectEqual(node_limits.receipts_max, owner.receipts.count());
 846     try std.testing.expectEqual(node_limits.receipts_max, owner.timers.count());
 847 }
 848 
 849 test "frame above mtu is rejected" {
 850     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 851     var owner = try initNode(&storage);
 852     defer _ = owner.deinit();
 853     var frame: [reticulum.wire.mtu + 1]u8 = @splat(0);
 854     const effects = try owner.step(.{ .carrier_frame = .{
 855         .interface = 0,
 856         .now = 1,
 857         .bytes = &frame,
 858         .entropy = @splat(0),
 859     } });
 860     try std.testing.expectEqual(@as(usize, 1), effects.len);
 861     try expectEffectTag(effects[0], .diagnostic);
 862     try std.testing.expectEqual(reticulum.node.Code.frame_too_large, effects[0].diagnostic.code);
 863 }
 864 
 865 fn expectFrameDiagnostic(
 866     owner: *reticulum.node.Node,
 867     frame: []const u8,
 868     expected: reticulum.node.Code,
 869 ) !void {
 870     const effects = try owner.step(.{ .carrier_frame = .{
 871         .interface = 0,
 872         .now = 1,
 873         .bytes = frame,
 874         .entropy = @splat(0),
 875     } });
 876     try std.testing.expectEqual(@as(usize, 1), effects.len);
 877     try expectEffectTag(effects[0], .diagnostic);
 878     try std.testing.expectEqual(expected, effects[0].diagnostic.code);
 879 }
 880 
 881 fn maskedData(
 882     access: *const reticulum.node.Access,
 883     output: *[reticulum.carrier.frame_bytes_max]u8,
 884 ) ![]u8 {
 885     const vector = packetVector("data-single-hop-0");
 886     return reticulum.interface.ifac.apply(&access.key, access.size, vector.raw, output);
 887 }
 888 
 889 test "Reticulum@1.5.0 RNS/Transport.py:1681 rejects an invalid IFAC code" {
 890     const access = try accessConfiguration(.bytes_16);
 891     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 892     var owner = try initNodeWithAccess(&storage, access);
 893     defer _ = owner.deinit();
 894     var frame_storage: [reticulum.carrier.frame_bytes_max]u8 = undefined;
 895     const frame = try maskedData(&access, &frame_storage);
 896     frame[2] ^= 0x01;
 897     try expectFrameDiagnostic(&owner, frame, .ifac_invalid_code);
 898 }
 899 
 900 test "Reticulum@1.5.0 RNS/Transport.py:1687 rejects a missing IFAC flag" {
 901     const access = try accessConfiguration(.bytes_16);
 902     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 903     var owner = try initNodeWithAccess(&storage, access);
 904     defer _ = owner.deinit();
 905     var frame_storage: [reticulum.carrier.frame_bytes_max]u8 = undefined;
 906     const frame = try maskedData(&access, &frame_storage);
 907     frame[0] &= 0x7f;
 908     try expectFrameDiagnostic(&owner, frame, .ifac_missing_flag);
 909 }
 910 
 911 test "Reticulum@1.5.0 RNS/Transport.py:1693-1694 rejects IFAC on a keyless carrier" {
 912     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 913     var owner = try initNode(&storage);
 914     defer _ = owner.deinit();
 915     var frame: [reticulum.wire.header_one_bytes]u8 = @splat(0);
 916     frame[0] = 0x80;
 917     try expectFrameDiagnostic(&owner, &frame, .ifac_unexpected_flag);
 918 }
 919 
 920 test "Reticulum@1.5.0 RNS/Transport.py:1700-1701 rejects keyed frame maximum plus one" {
 921     const access = try accessConfiguration(.bytes_16);
 922     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 923     var owner = try initNodeWithAccess(&storage, access);
 924     defer _ = owner.deinit();
 925     var frame: [reticulum.wire.mtu + 16 + 1]u8 = @splat(0);
 926     frame[0] = 0x80;
 927     try expectFrameDiagnostic(&owner, &frame, .frame_too_large);
 928 }
 929 
 930 test "Reticulum@1.5.0 RNS/Transport.py:1700-1701 admits keyed frame maximum" {
 931     const vector = packetVector("announce-single-payload-max");
 932     const access = try accessConfiguration(.bytes_64);
 933     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 934     var owner = try initNodeWithAccess(&storage, access);
 935     defer _ = owner.deinit();
 936     var frame_storage: [reticulum.carrier.frame_bytes_max]u8 = undefined;
 937     const frame = try reticulum.interface.ifac.apply(
 938         &access.key,
 939         access.size,
 940         vector.raw,
 941         &frame_storage,
 942     );
 943     try std.testing.expectEqual(reticulum.carrier.frame_bytes_max, frame.len);
 944     const effects = try owner.step(.{ .carrier_frame = .{
 945         .interface = 0,
 946         .now = 1_700_000_100,
 947         .bytes = frame,
 948         .entropy = @splat(0),
 949     } });
 950     try expectInboundAnnounce(&owner, vector, effects);
 951 }
 952 
 953 test "Reticulum@1.5.0 RNS/Transport.py:1652,1683 rejects a truncated IFAC frame" {
 954     const access = try accessConfiguration(.bytes_16);
 955     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 956     var owner = try initNodeWithAccess(&storage, access);
 957     defer _ = owner.deinit();
 958     var truncated: [2 + 16]u8 = @splat(0);
 959     truncated[0] = 0x80;
 960     try expectFrameDiagnostic(&owner, &truncated, .ifac_truncated);
 961     var shortest: [2 + 16 + 1]u8 = @splat(0);
 962     shortest[0] = 0x80;
 963     try expectFrameDiagnostic(&owner, &shortest, .ifac_invalid_code);
 964 }
 965 
 966 test "duplicate frame is filtered" {
 967     var first_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 968     var second_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 969     var first = try initNode(&first_storage);
 970     var second = try initNode(&second_storage);
 971     defer _ = first.deinit();
 972     defer _ = second.deinit();
 973     try setupPacketPair(&first, &second, .none);
 974     const outgoing = try sendPacketSingle(&second, 1, "once");
 975     const frame = try reticulum.carrier.Frame.init(try expectSentFrame(outgoing));
 976     const first_effects = try first.step(.{ .carrier_frame = .{
 977         .interface = 0,
 978         .now = 2,
 979         .bytes = frame.slice(),
 980         .entropy = @splat(0),
 981     } });
 982     try std.testing.expectEqual(@as(usize, 1), first_effects.len);
 983     try expectEffectTag(first_effects[0], .application_delivery);
 984     const duplicate = try first.step(.{ .carrier_frame = .{
 985         .interface = 0,
 986         .now = 3,
 987         .bytes = frame.slice(),
 988         .entropy = @splat(0),
 989     } });
 990     try std.testing.expectEqual(@as(usize, 1), duplicate.len);
 991     try expectEffectTag(duplicate[0], .diagnostic);
 992     try std.testing.expectEqual(reticulum.node.Code.duplicate_packet, duplicate[0].diagnostic.code);
 993 }
 994 
 995 test "link request for an unknown destination yields one diagnostic" {
 996     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
 997     var owner = try initNode(&storage);
 998     defer _ = owner.deinit();
 999     var raw: [reticulum.wire.header_one_bytes]u8 = undefined;
1000     const frame = try reticulum.wire.encode(.{
1001         .ifac = 0,
1002         .header = .one,
1003         .context_flag = 0,
1004         .transport = .broadcast,
1005         .destination_type = .single,
1006         .packet_type = .link_request,
1007         .hops = 0,
1008         .transport_id = null,
1009         .destination = @splat(0x42),
1010         .context = .none,
1011         .payload = &.{},
1012     }, &raw);
1013     const effects = try owner.step(.{ .carrier_frame = .{
1014         .interface = 0,
1015         .now = 1,
1016         .bytes = frame,
1017         .entropy = @splat(0),
1018     } });
1019     try expectLinkDiagnostic(effects, .unknown_destination);
1020 }
1021 
1022 const link_session = reticulum.conformance.link.session_vectors[0];
1023 const link_key = reticulum.conformance.link.key_vectors[0];
1024 
1025 fn linkLimits() reticulum.node.Limits {
1026     var selected = node_limits;
1027     selected.interfaces_max = 2;
1028     return selected;
1029 }
1030 const link_capacity = reticulum.node.Capacity.derive(linkLimits()) catch unreachable;
1031 
1032 fn linkVector(name: []const u8) reticulum.conformance.link.Vector {
1033     for (reticulum.conformance.link.vectors) |vector| {
1034         if (std.mem.eql(u8, vector.name, name)) return vector;
1035     }
1036     unreachable;
1037 }
1038 
1039 fn expectLinkDiagnostic(
1040     effects: []const reticulum.node.Effect,
1041     code: reticulum.node.Code,
1042 ) !void {
1043     try std.testing.expectEqual(@as(usize, 1), effects.len);
1044     try expectEffectTag(effects[0], .diagnostic);
1045     try std.testing.expectEqual(code, effects[0].diagnostic.code);
1046 }
1047 
1048 fn registerLinkDestinationProving(
1049     owner: *reticulum.node.Node,
1050     proof_strategy: reticulum.destination.registry.ProofStrategy,
1051 ) !void {
1052     owner.identities[0] = reticulum.identity.Private.fromBytes(
1053         link_session.destination_private_key[0..64].*,
1054     );
1055     try owner.destinations.register(.{
1056         .hash = link_session.destination_hash[0..16].*,
1057         .kind = .single,
1058         .proof_strategy = proof_strategy,
1059         .identity_index = 0,
1060     });
1061 }
1062 
1063 fn registerLinkDestination(owner: *reticulum.node.Node) !void {
1064     try registerLinkDestinationProving(owner, .none);
1065 }
1066 
1067 fn rememberLinkDestination(owner: *reticulum.node.Node) !void {
1068     var private = reticulum.identity.Private.fromBytes(
1069         link_session.destination_private_key[0..64].*,
1070     );
1071     defer private.zero();
1072     try owner.known_identities.remember(.{
1073         .destination_hash = link_session.destination_hash[0..16].*,
1074         .public_key = private.publicBytes(),
1075         .announce_packet_hash = @splat(0),
1076         .received = link_session.announce_clock,
1077     });
1078 }
1079 
1080 fn hear(
1081     owner: *reticulum.node.Node,
1082     interface: reticulum.carrier.Index,
1083     bytes: []const u8,
1084     now: reticulum.node.Seconds,
1085     entropy: [32]u8,
1086 ) ![]const reticulum.node.Effect {
1087     return owner.step(.{ .carrier_frame = .{
1088         .interface = interface,
1089         .now = now,
1090         .bytes = bytes,
1091         .entropy = entropy,
1092     } });
1093 }
1094 
1095 fn hearLinkAnnounce(owner: *reticulum.node.Node) !void {
1096     const effects = try hear(
1097         owner,
1098         0,
1099         link_session.announce_raw,
1100         link_session.announce_clock,
1101         @splat(0),
1102     );
1103     try std.testing.expectEqual(@as(usize, 1), effects.len);
1104     try expectEffectTag(effects[0], .announce_received);
1105 }
1106 
1107 fn hearLinkVector(owner: *reticulum.node.Node, name: []const u8) ![]const reticulum.node.Effect {
1108     const vector = linkVector(name);
1109     return hear(owner, 0, vector.input_raw, vector.clock, vector.entropy[0..32].*);
1110 }
1111 
1112 fn openLink(
1113     owner: *reticulum.node.Node,
1114     encryption_private: [32]u8,
1115     signing_private: [32]u8,
1116     now: reticulum.node.Seconds,
1117 ) ![]const reticulum.node.Effect {
1118     return owner.step(.{ .application_link_open = .{
1119         .destination = link_session.destination_hash[0..16].*,
1120         .encryption_private = encryption_private,
1121         .signing_private = signing_private,
1122         .now = now,
1123     } });
1124 }
1125 
1126 fn openVectorLink(owner: *reticulum.node.Node) ![]const reticulum.node.Effect {
1127     const vector = linkVector("link-request");
1128     return openLink(
1129         owner,
1130         vector.encryption_private_key[0..32].*,
1131         vector.signing_private_key[0..32].*,
1132         vector.clock,
1133     );
1134 }
1135 
1136 fn expireLink(
1137     owner: *reticulum.node.Node,
1138     link_id: []const u8,
1139     now: reticulum.node.Seconds,
1140 ) ![]const reticulum.node.Effect {
1141     return owner.step(.{ .timer_expired = .{
1142         .id = .{ .link = link_id[0..16].* },
1143         .now = now,
1144         .entropy = @splat(0),
1145     } });
1146 }
1147 
1148 fn linkRequestFrame(
1149     out: *[reticulum.wire.mtu]u8,
1150     encryption_public: [32]u8,
1151     signing_public: [32]u8,
1152     signalling: ?reticulum.wire.link.Signalling,
1153 ) ![]const u8 {
1154     const keys = reticulum.wire.link.Request{
1155         .encryption_public = encryption_public,
1156         .signing_public = signing_public,
1157         .signalling = signalling,
1158     };
1159     var payload: [reticulum.wire.link.signalled_request_bytes]u8 = undefined;
1160     return reticulum.wire.encode(.{
1161         .ifac = 0,
1162         .header = .one,
1163         .context_flag = 0,
1164         .transport = .broadcast,
1165         .destination_type = .single,
1166         .packet_type = .link_request,
1167         .hops = 0,
1168         .transport_id = null,
1169         .destination = link_session.destination_hash[0..16].*,
1170         .context = .none,
1171         .payload = try keys.encode(&payload),
1172     }, out);
1173 }
1174 
1175 fn seededRequestFrame(
1176     out: *[reticulum.wire.mtu]u8,
1177     seed: u8,
1178     signalling: ?reticulum.wire.link.Signalling,
1179 ) ![]const u8 {
1180     var private = reticulum.identity.Private.fromBytes(@splat(seed));
1181     defer private.zero();
1182     const public = private.publicBytes();
1183     return linkRequestFrame(out, public[0..32].*, public[32..64].*, signalling);
1184 }
1185 
1186 fn linkDataFrame(
1187     out: *[reticulum.wire.mtu]u8,
1188     context: reticulum.wire.Context,
1189     iv: [16]u8,
1190     plaintext: []const u8,
1191 ) ![]const u8 {
1192     const token = try reticulum.crypto.token.Token.init(link_key.derived_key);
1193     var ciphertext: [reticulum.wire.mtu]u8 = undefined;
1194     return reticulum.wire.encode(.{
1195         .ifac = 0,
1196         .header = .one,
1197         .context_flag = 0,
1198         .transport = .broadcast,
1199         .destination_type = .link,
1200         .packet_type = .data,
1201         .hops = 0,
1202         .transport_id = null,
1203         .destination = link_key.link_id[0..16].*,
1204         .context = context,
1205         .payload = try token.encrypt(iv, plaintext, &ciphertext),
1206     }, out);
1207 }
1208 
1209 fn expectLinkStatus(
1210     owner: *reticulum.node.Node,
1211     link_id: []const u8,
1212     status: reticulum.node.transport.links.Status,
1213 ) !void {
1214     const entry = owner.transport.links.find(link_id[0..16].*) orelse
1215         return error.TestExpectedLink;
1216     try std.testing.expectEqual(status, entry.status);
1217 }
1218 
1219 fn expectLinkTimer(
1220     owner: *reticulum.node.Node,
1221     link_id: []const u8,
1222     at: reticulum.node.Seconds,
1223 ) !void {
1224     const scheduled = owner.timers.scheduledAt(.{ .link = link_id[0..16].* }) orelse
1225         return error.TestExpectedTimer;
1226     try std.testing.expectEqual(at, scheduled);
1227 }
1228 
1229 /// Takes the corpus initiator through to an active link for tests of link traffic: it hears the
1230 /// announce, puts the request out at second 301, and receives the proof with the round trip packet
1231 /// at second 302.
1232 fn activeInitiator(owner: *reticulum.node.Node) !void {
1233     try hearLinkAnnounce(owner);
1234     _ = try openVectorLink(owner);
1235     const effects = try hearLinkVector(owner, "link-rtt");
1236     try std.testing.expectEqual(@as(usize, 3), effects.len);
1237     try expectLinkStatus(owner, link_key.link_id, .active);
1238 }
1239 
1240 /// Takes the corpus responder through to an active link for the other end of the corpus session: it
1241 /// records the destination under the proof strategy the caller names, answers the request at second
1242 /// 301, and receives the round trip packet at second 302.
1243 fn activeResponder(
1244     owner: *reticulum.node.Node,
1245     proof_strategy: reticulum.destination.registry.ProofStrategy,
1246 ) !void {
1247     try registerLinkDestinationProving(owner, proof_strategy);
1248     _ = try hearLinkVector(owner, "link-proof");
1249     const rtt = linkVector("link-rtt");
1250     const effects = try hear(owner, 0, rtt.output_raw, rtt.clock, @splat(0));
1251     try std.testing.expectEqual(@as(usize, 2), effects.len);
1252     try expectLinkStatus(owner, link_key.link_id, .active);
1253 }
1254 
1255 fn sendOnLink(
1256     owner: *reticulum.node.Node,
1257     plaintext: []const u8,
1258     iv: [16]u8,
1259     now: reticulum.node.Seconds,
1260     create_receipt: bool,
1261 ) ![]const reticulum.node.Effect {
1262     return owner.step(.{ .application_link_send = .{
1263         .link_id = link_key.link_id[0..16].*,
1264         .plaintext = plaintext,
1265         .iv = iv,
1266         .now = now,
1267         .create_receipt = create_receipt,
1268     } });
1269 }
1270 
1271 fn closeLink(
1272     owner: *reticulum.node.Node,
1273     iv: [16]u8,
1274     now: reticulum.node.Seconds,
1275 ) ![]const reticulum.node.Effect {
1276     return owner.step(.{ .application_link_close = .{
1277         .link_id = link_key.link_id[0..16].*,
1278         .iv = iv,
1279         .now = now,
1280     } });
1281 }
1282 
1283 /// Replays the corpus link data and its proof through both ends for tests of keepalive and stale
1284 /// deadlines, so each one holds the deadlines of the corpus session. The call checks that the
1285 /// initiator concludes its receipt as delivered.
1286 fn exchangeCorpusData(
1287     initiator: *reticulum.node.Node,
1288     responder: *reticulum.node.Node,
1289 ) !void {
1290     const data = linkVector("link-data");
1291     const proof = linkVector("link-data-proof");
1292     _ = try sendOnLink(initiator, data.plaintext, data.iv[0..16].*, data.clock, true);
1293     _ = try hear(responder, 0, data.output_raw, data.clock, @splat(0));
1294     const concluded = try hear(initiator, 0, proof.output_raw, proof.clock, @splat(0));
1295     try std.testing.expectEqual(@as(usize, 1), concluded.len);
1296     try expectEffectTag(concluded[0], .receipt_update);
1297     try std.testing.expectEqual(
1298         reticulum.packet.receipt.Status.delivered,
1299         concluded[0].receipt_update.status,
1300     );
1301 }
1302 
1303 fn linkProofFrame(out: *[reticulum.wire.mtu]u8, payload: []const u8) ![]const u8 {
1304     return reticulum.wire.encode(.{
1305         .ifac = 0,
1306         .header = .one,
1307         .context_flag = 0,
1308         .transport = .broadcast,
1309         .destination_type = .link,
1310         .packet_type = .proof,
1311         .hops = 0,
1312         .transport_id = null,
1313         .destination = link_key.link_id[0..16].*,
1314         .context = .none,
1315         .payload = payload,
1316     }, out);
1317 }
1318 
1319 test "Reticulum@1.5.0 RNS/Link.py:281-283,304-322 node link request matches corpus" {
1320     const vector = linkVector("link-request");
1321     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1322     var owner = try initNode(&storage);
1323     defer _ = owner.deinit();
1324     try hearLinkAnnounce(&owner);
1325     const effects = try openVectorLink(&owner);
1326     try std.testing.expectEqual(@as(usize, 3), effects.len);
1327     try expectEffectTag(effects[0], .link_requested);
1328     try expectCorpusBytes(
1329         vector.name,
1330         "link_id",
1331         vector.link_id,
1332         &effects[0].link_requested.link_id,
1333     );
1334     try expectEffectTag(effects[1], .schedule_timer);
1335     try std.testing.expectEqual(vector.clock + 12, effects[1].schedule_timer.at);
1336     const sent = try expectSentFrame(effects);
1337     try expectCorpusBytes(vector.name, "output_raw", vector.output_raw, sent);
1338     try std.testing.expect(!owner.duplicate_hashes.contains(try reticulum.wire.hash.full(sent)));
1339     try expectLinkStatus(&owner, vector.link_id, .pending);
1340 }
1341 
1342 fn hearRelayedLinkAnnounce(owner: *reticulum.node.Node, wire_hops: u8) !void {
1343     var announce: [reticulum.wire.mtu]u8 = undefined;
1344     const relayed = announce[0..link_session.announce_raw.len];
1345     @memcpy(relayed, link_session.announce_raw);
1346     relayed[1] = wire_hops;
1347     const effects = try hear(owner, 0, relayed, link_session.announce_clock, @splat(0));
1348     try std.testing.expectEqual(@as(usize, 1), effects.len);
1349     try expectEffectTag(effects[0], .announce_received);
1350 }
1351 
1352 test "Reticulum@1.5.0 RNS/Transport.py:1345-1356 multi-hop link request enters transport" {
1353     const vector = linkVector("link-request");
1354     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1355     var owner = try initNode(&storage);
1356     defer _ = owner.deinit();
1357     try hearRelayedLinkAnnounce(&owner, 2);
1358     const effects = try openVectorLink(&owner);
1359     try std.testing.expectEqual(@as(usize, 3), effects.len);
1360     try std.testing.expectEqual(vector.clock + 6 + 6 * 3, effects[1].schedule_timer.at);
1361     const sent = try expectSentFrame(effects);
1362     try std.testing.expectEqual(vector.output_raw.len + 16, sent.len);
1363     try std.testing.expectEqual(@as(u8, 0x52), sent[0]);
1364     try std.testing.expectEqual(vector.output_raw[1], sent[1]);
1365     try std.testing.expectEqualSlices(u8, link_session.destination_hash, sent[2..18]);
1366     try std.testing.expectEqualSlices(u8, vector.output_raw[2..], sent[18..]);
1367     try std.testing.expect(!owner.duplicate_hashes.contains(try reticulum.wire.hash.full(sent)));
1368     const destination_hash = link_session.destination_hash[0..16].*;
1369     const path = owner.transport.paths.find(destination_hash, vector.clock) orelse
1370         return error.TestExpectedPath;
1371     try std.testing.expectEqual(vector.clock, path.timestamp);
1372     try std.testing.expectEqual(@as(u8, 3), path.hops);
1373     try expectLinkStatus(&owner, vector.link_id, .pending);
1374 }
1375 
1376 test "Reticulum@1.5.0 RNS/Transport.py:1393,1536-1539 link request without a path stores its hash" {
1377     const vector = linkVector("link-request");
1378     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1379     var owner = try initNode(&storage);
1380     defer _ = owner.deinit();
1381     try rememberLinkDestination(&owner);
1382     const effects = try openVectorLink(&owner);
1383     try std.testing.expectEqual(@as(usize, 3), effects.len);
1384     const deadline = vector.clock + 6 + 6 * @as(u64, reticulum.wire.pathfinder_hops);
1385     try std.testing.expectEqual(deadline, effects[1].schedule_timer.at);
1386     const sent = try expectSentFrame(effects);
1387     try expectCorpusBytes(vector.name, "output_raw", vector.output_raw, sent);
1388     try std.testing.expect(owner.duplicate_hashes.contains(try reticulum.wire.hash.full(sent)));
1389 }
1390 
1391 test "Reticulum@1.5.0 RNS/Link.py:186-215,366-375 node link proof matches corpus" {
1392     for ([_][]const u8{ "link-proof", "link-request-stripped" }) |name| {
1393         const vector = linkVector(name);
1394         var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1395         var owner = try initNode(&storage);
1396         defer _ = owner.deinit();
1397         try registerLinkDestination(&owner);
1398         const effects = try hearLinkVector(&owner, name);
1399         try std.testing.expectEqual(@as(usize, 2), effects.len);
1400         try expectEffectTag(effects[0], .schedule_timer);
1401         try std.testing.expectEqual(vector.clock + 366, effects[0].schedule_timer.at);
1402         const sent = try expectSentFrame(effects);
1403         try expectCorpusBytes(name, "output_raw", vector.output_raw, sent);
1404         try std.testing.expect(owner.duplicate_hashes.contains(try reticulum.wire.hash.full(sent)));
1405         try expectLinkStatus(&owner, vector.link_id, .handshake);
1406     }
1407 }
1408 
1409 test "Reticulum@1.5.0 RNS/Link.py:391-438 node link rtt matches corpus" {
1410     const vector = linkVector("link-rtt");
1411     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1412     var owner = try initNode(&storage);
1413     defer _ = owner.deinit();
1414     try hearLinkAnnounce(&owner);
1415     _ = try openVectorLink(&owner);
1416     const effects = try hearLinkVector(&owner, vector.name);
1417     try std.testing.expectEqual(@as(usize, 3), effects.len);
1418     try expectEffectTag(effects[0], .link_established);
1419     const established = effects[0].link_established;
1420     try expectCorpusBytes(vector.name, "link_id", vector.link_id, &established.link_id);
1421     try std.testing.expectEqual(reticulum.node.LinkRole.initiator, established.role);
1422     try std.testing.expectEqual(@as(f64, 1.0), established.rtt);
1423     const sent = try expectSentFrame(effects);
1424     try expectCorpusBytes(vector.name, "output_raw", vector.output_raw, sent);
1425     try expectEffectTag(effects[2], .schedule_timer);
1426     try std.testing.expectEqual(vector.clock + 206, effects[2].schedule_timer.at);
1427     try expectLinkTimer(&owner, vector.link_id, vector.clock + 206);
1428     try expectLinkStatus(&owner, vector.link_id, .active);
1429 }
1430 
1431 test "Reticulum@1.5.0 RNS/Link.py:516-525 responder activates from the corpus rtt" {
1432     const vector = linkVector("link-rtt");
1433     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1434     var owner = try initNode(&storage);
1435     defer _ = owner.deinit();
1436     try registerLinkDestination(&owner);
1437     _ = try hearLinkVector(&owner, "link-proof");
1438     const effects = try hear(&owner, 0, vector.output_raw, vector.clock, @splat(0));
1439     try std.testing.expectEqual(@as(usize, 2), effects.len);
1440     try expectEffectTag(effects[0], .link_established);
1441     const established = effects[0].link_established;
1442     try std.testing.expectEqual(reticulum.node.LinkRole.responder, established.role);
1443     try std.testing.expectEqual(@as(f64, 1.0), established.rtt);
1444     try expectEffectTag(effects[1], .schedule_timer);
1445     try std.testing.expectEqual(vector.clock + 412, effects[1].schedule_timer.at);
1446     try expectLinkTimer(&owner, vector.link_id, vector.clock + 412);
1447     try expectLinkStatus(&owner, vector.link_id, .active);
1448 }
1449 
1450 test "link open refuses unknown destinations, duplicate keys, and a full pool" {
1451     const vector = linkVector("link-request");
1452     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1453     var owner = try initNode(&storage);
1454     defer _ = owner.deinit();
1455     try std.testing.expectError(error.UnknownDestination, openVectorLink(&owner));
1456     try hearLinkAnnounce(&owner);
1457     _ = try openVectorLink(&owner);
1458     try std.testing.expectError(error.DuplicateLink, openVectorLink(&owner));
1459     _ = try openLink(&owner, @splat(0x21), @splat(0x22), vector.clock);
1460     try std.testing.expect(owner.transport.links.full());
1461     const hashes = owner.duplicate_hashes.count();
1462     const refused = openLink(&owner, @splat(0x23), @splat(0x24), vector.clock);
1463     try std.testing.expectError(error.LinksFull, refused);
1464     try std.testing.expectEqual(@as(usize, 0), owner.effects.len);
1465     try std.testing.expectEqual(node_limits.links_max, owner.transport.links.count());
1466     try std.testing.expectEqual(node_limits.links_max, owner.timers.count());
1467     try std.testing.expectEqual(hashes, owner.duplicate_hashes.count());
1468 }
1469 
1470 test "a full link pool drops an inbound request with a diagnostic" {
1471     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1472     var owner = try initNode(&storage);
1473     defer _ = owner.deinit();
1474     try registerLinkDestination(&owner);
1475     const signalling = reticulum.wire.link.default_signalling;
1476     var bytes: [reticulum.wire.mtu]u8 = undefined;
1477     for (0..node_limits.links_max) |index| {
1478         const request = try seededRequestFrame(&bytes, @intCast(index + 1), signalling);
1479         const accepted = try hear(&owner, 0, request, 1, @splat(0x10));
1480         try std.testing.expectEqual(@as(usize, 2), accepted.len);
1481     }
1482     const overflow = try seededRequestFrame(&bytes, 0x7f, signalling);
1483     try expectLinkDiagnostic(try hear(&owner, 0, overflow, 1, @splat(0x10)), .links_full);
1484     try std.testing.expectEqual(node_limits.links_max, owner.transport.links.count());
1485     try std.testing.expectEqual(node_limits.links_max, owner.timers.count());
1486 }
1487 
1488 test "a stripped link request after its signalled form is a duplicate" {
1489     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1490     var owner = try initNode(&storage);
1491     defer _ = owner.deinit();
1492     try registerLinkDestination(&owner);
1493     _ = try hearLinkVector(&owner, "link-proof");
1494     const duplicate = try hearLinkVector(&owner, "link-request-stripped");
1495     try expectLinkDiagnostic(duplicate, .link_request_duplicate);
1496     try std.testing.expectEqual(@as(usize, 1), owner.transport.links.count());
1497 }
1498 
1499 test "Reticulum@1.5.0 RNS/Link.py:149,366-371 mtu zero link requests admit only the enabled mode" {
1500     const vector = linkVector("link-proof");
1501     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1502     var owner = try initNode(&storage);
1503     defer _ = owner.deinit();
1504     try registerLinkDestination(&owner);
1505     const keys = try reticulum.wire.link.Request.decode(
1506         vector.input_raw[reticulum.wire.header_one_bytes..],
1507     );
1508     var bytes: [reticulum.wire.mtu]u8 = undefined;
1509     const unenabled = try linkRequestFrame(
1510         &bytes,
1511         keys.encryption_public,
1512         keys.signing_public,
1513         .{ .mtu = 0, .mode = .aes128_cbc },
1514     );
1515     const refused = try hear(&owner, 0, unenabled, vector.clock, vector.entropy[0..32].*);
1516     try expectLinkDiagnostic(refused, .link_request_invalid);
1517     const enabled = try linkRequestFrame(
1518         &bytes,
1519         keys.encryption_public,
1520         keys.signing_public,
1521         .{ .mtu = 0, .mode = .aes256_cbc },
1522     );
1523     const accepted = try hear(&owner, 0, enabled, vector.clock, vector.entropy[0..32].*);
1524     try std.testing.expectEqual(@as(usize, 2), accepted.len);
1525     const sent = try expectSentFrame(accepted);
1526     try expectCorpusBytes(vector.name, "output_raw", vector.output_raw, sent);
1527 }
1528 
1529 test "Reticulum@1.5.0 RNS/Transport.py:2471-2474 nonzero mtu signalling admits any mode" {
1530     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1531     var owner = try initNode(&storage);
1532     defer _ = owner.deinit();
1533     try registerLinkDestination(&owner);
1534     var bytes: [reticulum.wire.mtu]u8 = undefined;
1535     const request = try seededRequestFrame(&bytes, 0x31, .{ .mtu = 500, .mode = .aes128_cbc });
1536     const accepted = try hear(&owner, 0, request, 1, @splat(0x10));
1537     try std.testing.expectEqual(@as(usize, 2), accepted.len);
1538     try std.testing.expectEqual(@as(usize, 1), owner.transport.links.count());
1539 }
1540 
1541 test "a link request with a low-order encryption key is invalid" {
1542     const vector = linkVector("link-proof");
1543     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1544     var owner = try initNode(&storage);
1545     defer _ = owner.deinit();
1546     try registerLinkDestination(&owner);
1547     const keys = try reticulum.wire.link.Request.decode(
1548         vector.input_raw[reticulum.wire.header_one_bytes..],
1549     );
1550     var bytes: [reticulum.wire.mtu]u8 = undefined;
1551     const request = try linkRequestFrame(&bytes, @splat(0), keys.signing_public, keys.signalling);
1552     const refused = try hear(&owner, 0, request, vector.clock, vector.entropy[0..32].*);
1553     try expectLinkDiagnostic(refused, .link_request_invalid);
1554     try std.testing.expectEqual(@as(usize, 0), owner.transport.links.count());
1555 }
1556 
1557 test "Reticulum@1.5.0 RNS/Transport.py:2605-2650 rebalances an unknown hop count once" {
1558     const vector = linkVector("link-rtt");
1559     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1560     var owner = try initNode(&storage);
1561     defer _ = owner.deinit();
1562     try rememberLinkDestination(&owner);
1563     _ = try openVectorLink(&owner);
1564     var bytes: [reticulum.wire.mtu]u8 = undefined;
1565     const forged = bytes[0..vector.input_raw.len];
1566     @memcpy(forged, vector.input_raw);
1567     forged[reticulum.wire.header_one_bytes] ^= 0x40;
1568     const rejected = try hear(&owner, 0, forged, vector.clock, vector.entropy[0..32].*);
1569     try expectLinkDiagnostic(rejected, .proof_rejected);
1570     try std.testing.expect(!owner.duplicate_hashes.contains(try reticulum.wire.hash.full(forged)));
1571     try expectLinkStatus(&owner, vector.link_id, .pending);
1572     const effects = try hearLinkVector(&owner, vector.name);
1573     try std.testing.expectEqual(@as(usize, 3), effects.len);
1574     try expectEffectTag(effects[0], .link_established);
1575     const sent = try expectSentFrame(effects);
1576     try expectCorpusBytes(vector.name, "output_raw", vector.output_raw, sent);
1577     const proof_hash = try reticulum.wire.hash.full(vector.input_raw);
1578     try std.testing.expect(owner.duplicate_hashes.contains(proof_hash));
1579     const entry = owner.transport.links.find(vector.link_id[0..16].*) orelse
1580         return error.TestExpectedLink;
1581     try std.testing.expectEqual(@as(u8, 1), entry.expected_hops);
1582     try std.testing.expect(entry.rebalanced);
1583 }
1584 
1585 fn signedProofFrame(
1586     out: *[reticulum.wire.mtu]u8,
1587     link_id: []const u8,
1588     encryption_public: [32]u8,
1589     hops: u8,
1590 ) ![]const u8 {
1591     var private = reticulum.identity.Private.fromBytes(
1592         link_session.destination_private_key[0..64].*,
1593     );
1594     defer private.zero();
1595     const destination_public = private.publicBytes();
1596     const signalling = reticulum.wire.link.default_signalling;
1597     var signed: [reticulum.wire.link.signed_proof_bytes_max]u8 = undefined;
1598     const message = reticulum.wire.link.signedProof(
1599         link_id[0..16].*,
1600         encryption_public,
1601         destination_public[32..64].*,
1602         signalling,
1603         &signed,
1604     );
1605     const proof = reticulum.wire.link.Proof{
1606         .signature = private.sign(message),
1607         .encryption_public = encryption_public,
1608         .signalling = signalling,
1609     };
1610     var payload: [reticulum.wire.link.signalled_proof_bytes]u8 = undefined;
1611     return reticulum.wire.encode(.{
1612         .ifac = 0,
1613         .header = .one,
1614         .context_flag = 0,
1615         .transport = .broadcast,
1616         .destination_type = .link,
1617         .packet_type = .proof,
1618         .hops = hops,
1619         .transport_id = null,
1620         .destination = link_id[0..16].*,
1621         .context = .lrproof,
1622         .payload = try proof.encode(&payload),
1623     }, out);
1624 }
1625 
1626 fn expectPathHops(
1627     owner: *reticulum.node.Node,
1628     hops: u8,
1629     now: reticulum.node.Seconds,
1630 ) !void {
1631     const destination_hash = link_session.destination_hash[0..16].*;
1632     const path = owner.transport.paths.find(destination_hash, now) orelse
1633         return error.TestExpectedPath;
1634     try std.testing.expectEqual(hops, path.hops);
1635 }
1636 
1637 test "Reticulum@1.5.0 RNS/Transport.py:2628-2637 rebalances a known path hop count only once" {
1638     const vector = linkVector("link-rtt");
1639     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1640     var owner = try initNode(&storage);
1641     defer _ = owner.deinit();
1642     try hearRelayedLinkAnnounce(&owner, 2);
1643     _ = try openVectorLink(&owner);
1644     try expectPathHops(&owner, 3, vector.clock);
1645     var bytes: [reticulum.wire.mtu]u8 = undefined;
1646     const low_order = try signedProofFrame(&bytes, vector.link_id, @splat(0), 0);
1647     const corrected = try hear(&owner, 0, low_order, vector.clock, vector.entropy[0..32].*);
1648     try expectLinkDiagnostic(corrected, .proof_rejected);
1649     const low_order_hash = try reticulum.wire.hash.full(low_order);
1650     try std.testing.expect(owner.duplicate_hashes.contains(low_order_hash));
1651     try expectPathHops(&owner, 1, vector.clock);
1652     try expectLinkStatus(&owner, vector.link_id, .pending);
1653     var farther_bytes: [reticulum.wire.mtu]u8 = undefined;
1654     const farther = farther_bytes[0..vector.input_raw.len];
1655     @memcpy(farther, vector.input_raw);
1656     farther[1] = 1;
1657     const refused = try hear(&owner, 0, farther, vector.clock, vector.entropy[0..32].*);
1658     try expectLinkDiagnostic(refused, .proof_rejected);
1659     const proof_hash = try reticulum.wire.hash.full(vector.input_raw);
1660     try std.testing.expect(!owner.duplicate_hashes.contains(proof_hash));
1661     try expectPathHops(&owner, 1, vector.clock);
1662     try expectLinkStatus(&owner, vector.link_id, .pending);
1663     const effects = try hearLinkVector(&owner, vector.name);
1664     try std.testing.expectEqual(@as(usize, 3), effects.len);
1665     try expectEffectTag(effects[0], .link_established);
1666     const sent = try expectSentFrame(effects);
1667     try expectCorpusBytes(vector.name, "output_raw", vector.output_raw, sent);
1668     try std.testing.expect(owner.duplicate_hashes.contains(proof_hash));
1669 }
1670 
1671 test "a link proof with a low-order encryption key leaves the link pending" {
1672     const vector = linkVector("link-rtt");
1673     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1674     var owner = try initNode(&storage);
1675     defer _ = owner.deinit();
1676     try hearLinkAnnounce(&owner);
1677     _ = try openVectorLink(&owner);
1678     var bytes: [reticulum.wire.mtu]u8 = undefined;
1679     const low_order = try signedProofFrame(&bytes, vector.link_id, @splat(0), 0);
1680     const rejected = try hear(&owner, 0, low_order, vector.clock, vector.entropy[0..32].*);
1681     try expectLinkDiagnostic(rejected, .proof_rejected);
1682     const low_order_hash = try reticulum.wire.hash.full(low_order);
1683     try std.testing.expect(owner.duplicate_hashes.contains(low_order_hash));
1684     try expectLinkStatus(&owner, vector.link_id, .pending);
1685     try std.testing.expect(owner.timers.contains(.{ .link = vector.link_id[0..16].* }));
1686     const effects = try hearLinkVector(&owner, vector.name);
1687     try std.testing.expectEqual(@as(usize, 3), effects.len);
1688     try expectEffectTag(effects[0], .link_established);
1689 }
1690 
1691 test "invalid link proofs leave the link pending for a later valid proof" {
1692     const vector = linkVector("link-rtt");
1693     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1694     var owner = try initNode(&storage);
1695     defer _ = owner.deinit();
1696     try hearLinkAnnounce(&owner);
1697     _ = try openVectorLink(&owner);
1698     var bytes: [reticulum.wire.mtu]u8 = undefined;
1699     const forged = bytes[0..vector.input_raw.len];
1700     const signature = reticulum.wire.header_one_bytes;
1701     const mode = reticulum.wire.header_one_bytes + reticulum.wire.link.proof_bytes;
1702     for ([_]usize{ signature, mode }) |offset| {
1703         @memcpy(forged, vector.input_raw);
1704         forged[offset] ^= 0x40;
1705         const rejected = try hear(&owner, 0, forged, vector.clock, vector.entropy[0..32].*);
1706         try expectLinkDiagnostic(rejected, .proof_rejected);
1707         try expectLinkStatus(&owner, vector.link_id, .pending);
1708     }
1709     const effects = try hearLinkVector(&owner, vector.name);
1710     try std.testing.expectEqual(@as(usize, 3), effects.len);
1711     try expectEffectTag(effects[0], .link_established);
1712 }
1713 
1714 test "a second rtt on an active link reports a state mismatch" {
1715     const vector = linkVector("link-proof");
1716     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1717     var owner = try initNode(&storage);
1718     defer _ = owner.deinit();
1719     try registerLinkDestination(&owner);
1720     _ = try hearLinkVector(&owner, vector.name);
1721     const rtt = reticulum.wire.link.encodeRtt(1.0);
1722     var bytes: [reticulum.wire.mtu]u8 = undefined;
1723     const first = try linkDataFrame(&bytes, .lrrtt, @splat(0x01), &rtt);
1724     const activated = try hear(&owner, 0, first, vector.clock + 1, @splat(0));
1725     try std.testing.expectEqual(@as(usize, 2), activated.len);
1726     try expectEffectTag(activated[0], .link_established);
1727     const second = try linkDataFrame(&bytes, .lrrtt, @splat(0x02), &rtt);
1728     const repeated = try hear(&owner, 0, second, vector.clock + 2, @splat(0));
1729     try expectLinkDiagnostic(repeated, .link_state_mismatch);
1730     try expectLinkStatus(&owner, vector.link_id, .active);
1731 }
1732 
1733 test "a link proof outside the pending state reports a state mismatch" {
1734     const vector = linkVector("link-rtt");
1735     var bytes: [reticulum.wire.mtu]u8 = undefined;
1736     const altered = bytes[0..vector.input_raw.len];
1737     @memcpy(altered, vector.input_raw);
1738     altered[reticulum.wire.header_one_bytes] ^= 0x40;
1739     var initiator_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1740     var initiator = try initNode(&initiator_storage);
1741     defer _ = initiator.deinit();
1742     try hearLinkAnnounce(&initiator);
1743     _ = try openVectorLink(&initiator);
1744     _ = try hearLinkVector(&initiator, vector.name);
1745     const active = try hear(&initiator, 0, altered, vector.clock + 1, @splat(0));
1746     try expectLinkDiagnostic(active, .link_state_mismatch);
1747     try expectLinkStatus(&initiator, vector.link_id, .active);
1748     var responder_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1749     var responder = try initNode(&responder_storage);
1750     defer _ = responder.deinit();
1751     try registerLinkDestination(&responder);
1752     _ = try hearLinkVector(&responder, "link-proof");
1753     const answering = try hear(&responder, 0, altered, vector.clock, @splat(0));
1754     try expectLinkDiagnostic(answering, .link_state_mismatch);
1755     try expectLinkStatus(&responder, vector.link_id, .handshake);
1756 }
1757 
1758 test "an rtt plaintext other than float64 tears the link down" {
1759     const vector = linkVector("link-proof");
1760     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1761     var owner = try initNode(&storage);
1762     defer _ = owner.deinit();
1763     try registerLinkDestination(&owner);
1764     _ = try hearLinkVector(&owner, vector.name);
1765     const float32_rtt = [_]u8{ 0xca, 0x3f, 0x80, 0x00, 0x00 };
1766     var bytes: [reticulum.wire.mtu]u8 = undefined;
1767     const frame = try linkDataFrame(&bytes, .lrrtt, @splat(0x03), &float32_rtt);
1768     const effects = try hear(&owner, 0, frame, vector.clock + 1, @splat(0x55));
1769     try std.testing.expectEqual(@as(usize, 2), effects.len);
1770     try expectEffectTag(effects[1], .link_closed);
1771     const closed = effects[1].link_closed;
1772     try std.testing.expectEqual(reticulum.node.LinkCloseReason.destination_closed, closed.reason);
1773     const closing = try reticulum.wire.decode(try expectSentFrame(effects));
1774     try std.testing.expectEqual(reticulum.wire.Context.linkclose, closing.context);
1775     const token = try reticulum.crypto.token.Token.init(link_key.derived_key);
1776     var plaintext: [reticulum.wire.mtu]u8 = undefined;
1777     const opened = try token.decrypt(closing.payload, &plaintext);
1778     try std.testing.expect(reticulum.wire.link.closes(opened, link_key.link_id[0..16].*));
1779     try std.testing.expectEqual(@as(usize, 0), owner.transport.links.count());
1780     try std.testing.expect(!owner.timers.contains(.{ .link = vector.link_id[0..16].* }));
1781 }
1782 
1783 test "Reticulum@1.5.0 RNS/Link.py:722-729 initiator establishment deadline closes the link" {
1784     const vector = linkVector("link-request");
1785     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1786     var owner = try initNode(&storage);
1787     defer _ = owner.deinit();
1788     try hearLinkAnnounce(&owner);
1789     _ = try openVectorLink(&owner);
1790     const early = try expireLink(&owner, vector.link_id, vector.clock + 11);
1791     try std.testing.expectEqual(@as(usize, 1), early.len);
1792     try std.testing.expectEqual(vector.clock + 12, early[0].schedule_timer.at);
1793     const expired = try expireLink(&owner, vector.link_id, vector.clock + 12);
1794     try std.testing.expectEqual(@as(usize, 2), expired.len);
1795     try expectEffectTag(expired[0], .link_closed);
1796     const closed = expired[0].link_closed;
1797     try std.testing.expectEqual(reticulum.node.LinkCloseReason.timeout, closed.reason);
1798     try std.testing.expectEqual(@as(usize, 0), owner.transport.links.count());
1799     try std.testing.expect(!owner.timers.contains(.{ .link = vector.link_id[0..16].* }));
1800     const stale = try expireLink(&owner, vector.link_id, vector.clock + 13);
1801     try std.testing.expectEqual(@as(usize, 0), stale.len);
1802 }
1803 
1804 test "Reticulum@1.5.0 RNS/Transport.py:676-695 rediscovers a path after a link never activates" {
1805     const vector = linkVector("link-request");
1806     const wanted = link_session.destination_hash[0..16].*;
1807     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1808     var owner = try initNode(&storage);
1809     defer _ = owner.deinit();
1810     try hearLinkAnnounce(&owner);
1811     _ = try openVectorLink(&owner);
1812     const deadline = vector.clock + 12;
1813     try std.testing.expect(owner.transport.paths.find(wanted, deadline) != null);
1814     const expired = try expireLink(&owner, vector.link_id, deadline);
1815     try std.testing.expectEqual(@as(usize, 2), expired.len);
1816     try expectEffectTag(expired[1], .carrier_send);
1817     const request = try reticulum.wire.decode(expired[1].carrier_send.frame);
1818     const requests = reticulum.node.transport.requests;
1819     try std.testing.expectEqualSlices(u8, &requests.destination_hash, &request.destination);
1820     try std.testing.expectEqual(reticulum.wire.DestinationType.plain, request.destination_type);
1821     try std.testing.expectEqual(@as(usize, 32), request.payload.len);
1822     try std.testing.expectEqualSlices(u8, &wanted, request.payload[0..16]);
1823     try std.testing.expect(owner.transport.paths.find(wanted, deadline) == null);
1824     const gate = owner.transport.inflight_requests.find(wanted, deadline) orelse
1825         return error.TestExpectedGate;
1826     try std.testing.expectEqual(deadline, gate.timestamp);
1827 }
1828 
1829 test "a rediscovery asks for one path once in 20 seconds" {
1830     const vector = linkVector("link-request");
1831     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1832     var owner = try initNode(&storage);
1833     defer _ = owner.deinit();
1834     try hearLinkAnnounce(&owner);
1835     _ = try openVectorLink(&owner);
1836     const second = try openLink(&owner, @splat(0x11), @splat(0x22), vector.clock);
1837     const second_id = second[0].link_requested.link_id;
1838     const deadline = vector.clock + 12;
1839     const asked = try expireLink(&owner, vector.link_id, deadline);
1840     try std.testing.expectEqual(@as(usize, 2), asked.len);
1841     try expectEffectTag(asked[1], .carrier_send);
1842     const throttled = try expireLink(&owner, &second_id, deadline);
1843     try std.testing.expectEqual(@as(usize, 1), throttled.len);
1844     try expectEffectTag(throttled[0], .link_closed);
1845     const third = try openLink(&owner, @splat(0x33), @splat(0x44), deadline);
1846     try std.testing.expectEqual(@as(usize, 3), third.len);
1847     const third_id = third[0].link_requested.link_id;
1848     const pathless = deadline + 6 + 6 * @as(reticulum.node.Seconds, reticulum.wire.pathfinder_hops);
1849     const again = try expireLink(&owner, &third_id, pathless);
1850     try std.testing.expectEqual(@as(usize, 2), again.len);
1851     try expectEffectTag(again[1], .carrier_send);
1852 }
1853 
1854 test "a transport instance keeps its path when a link never activates" {
1855     const vector = linkVector("link-request");
1856     const wanted = link_session.destination_hash[0..16].*;
1857     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1858     var owner = try initNode(&storage);
1859     defer _ = owner.deinit();
1860     owner.setTransport(@splat(0x7a), true);
1861     const heard = try hear(
1862         &owner,
1863         0,
1864         link_session.announce_raw,
1865         link_session.announce_clock,
1866         @splat(0),
1867     );
1868     try expectEffectTag(heard[1], .announce_received);
1869     _ = try openVectorLink(&owner);
1870     const deadline = vector.clock + 12;
1871     const expired = try expireLink(&owner, vector.link_id, deadline);
1872     try std.testing.expectEqual(@as(usize, 1), expired.len);
1873     try expectEffectTag(expired[0], .link_closed);
1874     try std.testing.expect(owner.transport.paths.find(wanted, deadline) != null);
1875 }
1876 
1877 test "Reticulum@1.5.0 RNS/Link.py:732-738 responder handshake deadline closes the link" {
1878     const vector = linkVector("link-proof");
1879     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1880     var owner = try initNode(&storage);
1881     defer _ = owner.deinit();
1882     try registerLinkDestination(&owner);
1883     _ = try hearLinkVector(&owner, vector.name);
1884     const early = try expireLink(&owner, vector.link_id, vector.clock + 365);
1885     try std.testing.expectEqual(@as(usize, 1), early.len);
1886     try std.testing.expectEqual(vector.clock + 366, early[0].schedule_timer.at);
1887     const expired = try expireLink(&owner, vector.link_id, vector.clock + 366);
1888     try std.testing.expectEqual(@as(usize, 1), expired.len);
1889     try expectEffectTag(expired[0], .link_closed);
1890     const closed = expired[0].link_closed;
1891     try std.testing.expectEqual(reticulum.node.LinkCloseReason.timeout, closed.reason);
1892     try std.testing.expectEqual(@as(usize, 0), owner.transport.links.count());
1893 }
1894 
1895 test "Reticulum@1.5.0 RNS/Transport.py:2515-2516 link data from another carrier stays receivable" {
1896     const vector = linkVector("link-rtt");
1897     const selected = comptime linkLimits();
1898     var storage: [link_capacity.storage_bytes]u8 align(8) = undefined;
1899     var owner = try reticulum.node.Node.init(&storage, selected);
1900     owner.activate();
1901     defer _ = owner.deinit();
1902     try owner.registerCarrier(0, true, null);
1903     try owner.registerCarrier(1, true, null);
1904     try registerLinkDestination(&owner);
1905     _ = try hearLinkVector(&owner, "link-proof");
1906     const misrouted = try hear(&owner, 1, vector.output_raw, vector.clock, @splat(0));
1907     try expectLinkDiagnostic(misrouted, .link_wrong_interface);
1908     const effects = try hear(&owner, 0, vector.output_raw, vector.clock, @splat(0));
1909     try std.testing.expectEqual(@as(usize, 2), effects.len);
1910     try expectEffectTag(effects[0], .link_established);
1911     const established = effects[0].link_established;
1912     try std.testing.expectEqual(@as(reticulum.carrier.Index, 0), established.interface);
1913 }
1914 
1915 test "Reticulum@1.5.0 RNS/Link.py:1010-1018 node link data matches corpus" {
1916     const vector = linkVector("link-data");
1917     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1918     var owner = try initNode(&storage);
1919     defer _ = owner.deinit();
1920     try activeInitiator(&owner);
1921     const effects = try sendOnLink(
1922         &owner,
1923         vector.plaintext,
1924         vector.iv[0..16].*,
1925         vector.clock,
1926         true,
1927     );
1928     try std.testing.expectEqual(@as(usize, 2), effects.len);
1929     try expectEffectTag(effects[0], .schedule_timer);
1930     try std.testing.expectEqual(vector.clock + 7, effects[0].schedule_timer.at);
1931     const sent = try expectSentFrame(effects);
1932     try expectCorpusBytes(vector.name, "output_raw", vector.output_raw, sent);
1933     const hash = try reticulum.wire.hash.full(sent);
1934     try expectCorpusBytes(vector.name, "packet_hash", vector.packet_hash, &hash);
1935     try std.testing.expect(owner.duplicate_hashes.contains(hash));
1936 }
1937 
1938 test "Reticulum@1.5.0 RNS/Link.py:378-389,948-968 node link data proof matches corpus" {
1939     const vector = linkVector("link-data-proof");
1940     const data = linkVector("link-data");
1941     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1942     var owner = try initNode(&storage);
1943     defer _ = owner.deinit();
1944     try activeResponder(&owner, .all);
1945     const effects = try hear(&owner, 0, vector.input_raw, vector.clock, @splat(0));
1946     try std.testing.expectEqual(@as(usize, 2), effects.len);
1947     try expectEffectTag(effects[0], .link_delivery);
1948     const delivery = effects[0].link_delivery;
1949     try expectCorpusBytes(data.name, "plaintext", data.plaintext, delivery.plaintext);
1950     try expectCorpusBytes(data.name, "packet_hash", data.packet_hash, &delivery.packet_hash);
1951     try std.testing.expect(!delivery.proof_requested);
1952     const sent = try expectSentFrame(effects);
1953     try expectCorpusBytes(vector.name, "output_raw", vector.output_raw, sent);
1954 }
1955 
1956 test "Reticulum@1.5.0 RNS/Link.py:744-760,795-802 node link keepalive matches corpus" {
1957     const vector = linkVector("link-keepalive");
1958     var initiator_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1959     var initiator = try initNode(&initiator_storage);
1960     defer _ = initiator.deinit();
1961     var responder_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1962     var responder = try initNode(&responder_storage);
1963     defer _ = responder.deinit();
1964     try activeInitiator(&initiator);
1965     try activeResponder(&responder, .all);
1966     try exchangeCorpusData(&initiator, &responder);
1967     const quiet = try expireLink(&initiator, vector.link_id, vector.clock - 1);
1968     try std.testing.expectEqual(@as(usize, 1), quiet.len);
1969     try expectEffectTag(quiet[0], .schedule_timer);
1970     try std.testing.expectEqual(vector.clock, quiet[0].schedule_timer.at);
1971     const effects = try expireLink(&initiator, vector.link_id, vector.clock);
1972     try std.testing.expectEqual(@as(usize, 2), effects.len);
1973     const sent = try expectSentFrame(effects);
1974     try expectCorpusBytes(vector.name, "output_raw", vector.output_raw, sent);
1975     try expectEffectTag(effects[1], .schedule_timer);
1976     try std.testing.expectEqual(vector.clock + 206, effects[1].schedule_timer.at);
1977     try expectLinkStatus(&initiator, vector.link_id, .active);
1978 }
1979 
1980 test "Reticulum@1.5.0 RNS/Link.py:1130-1135 node keepalive answer matches corpus" {
1981     const vector = linkVector("link-keepalive-answer");
1982     var initiator_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1983     var initiator = try initNode(&initiator_storage);
1984     defer _ = initiator.deinit();
1985     var responder_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
1986     var responder = try initNode(&responder_storage);
1987     defer _ = responder.deinit();
1988     try activeInitiator(&initiator);
1989     try activeResponder(&responder, .all);
1990     try exchangeCorpusData(&initiator, &responder);
1991     const early = try hear(&responder, 0, vector.input_raw, vector.clock - 1, @splat(0));
1992     try std.testing.expectEqual(@as(usize, 0), early.len);
1993     const effects = try hear(&responder, 0, vector.input_raw, vector.clock, @splat(0));
1994     try std.testing.expectEqual(@as(usize, 1), effects.len);
1995     const sent = try expectSentFrame(effects);
1996     try expectCorpusBytes(vector.name, "output_raw", vector.output_raw, sent);
1997     const repeated = try hear(&responder, 0, vector.input_raw, vector.clock + 1, @splat(0));
1998     try std.testing.expectEqual(@as(usize, 0), repeated.len);
1999 }
2000 
2001 test "Reticulum@1.5.0 RNS/Link.py:657-683 node link close matches corpus" {
2002     const vector = linkVector("link-close");
2003     var initiator_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2004     var initiator = try initNode(&initiator_storage);
2005     defer _ = initiator.deinit();
2006     var responder_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2007     var responder = try initNode(&responder_storage);
2008     defer _ = responder.deinit();
2009     try activeInitiator(&initiator);
2010     try activeResponder(&responder, .all);
2011     try exchangeCorpusData(&initiator, &responder);
2012     const effects = try closeLink(&initiator, vector.iv[0..16].*, vector.clock);
2013     try std.testing.expectEqual(@as(usize, 2), effects.len);
2014     const sent = try expectSentFrame(effects);
2015     try expectCorpusBytes(vector.name, "output_raw", vector.output_raw, sent);
2016     try expectEffectTag(effects[1], .link_closed);
2017     const closed = effects[1].link_closed;
2018     try std.testing.expectEqual(reticulum.node.LinkCloseReason.initiator_closed, closed.reason);
2019     try std.testing.expectEqual(@as(usize, 0), initiator.transport.links.count());
2020     try std.testing.expect(!initiator.timers.contains(.{ .link = vector.link_id[0..16].* }));
2021     const heard = try hear(&responder, 0, vector.output_raw, vector.clock, @splat(0));
2022     try std.testing.expectEqual(@as(usize, 1), heard.len);
2023     try expectEffectTag(heard[0], .link_closed);
2024     const mirrored = heard[0].link_closed;
2025     try std.testing.expectEqual(reticulum.node.LinkCloseReason.initiator_closed, mirrored.reason);
2026     try std.testing.expectEqual(@as(usize, 0), responder.transport.links.count());
2027 }
2028 
2029 test "Reticulum@1.5.0 RNS/Transport.py:2864-2881 link traffic waits for activation" {
2030     const data = linkVector("link-data");
2031     var initiator_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2032     var initiator = try initNode(&initiator_storage);
2033     defer _ = initiator.deinit();
2034     try hearLinkAnnounce(&initiator);
2035     _ = try openVectorLink(&initiator);
2036     const iv = data.iv[0..16].*;
2037     const refused = sendOnLink(&initiator, data.plaintext, iv, data.clock, true);
2038     try std.testing.expectError(error.LinkNotEstablished, refused);
2039     const unproved = initiator.step(.{ .application_link_prove = .{
2040         .link_id = data.link_id[0..16].*,
2041         .packet_hash = data.packet_hash[0..32].*,
2042         .now = data.clock,
2043     } });
2044     try std.testing.expectError(error.LinkNotEstablished, unproved);
2045     const dropped = try hear(&initiator, 0, data.output_raw, data.clock, @splat(0));
2046     try expectLinkDiagnostic(dropped, .unknown_link);
2047     try expectLinkStatus(&initiator, data.link_id, .pending);
2048     var responder_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2049     var responder = try initNode(&responder_storage);
2050     defer _ = responder.deinit();
2051     try registerLinkDestination(&responder);
2052     _ = try hearLinkVector(&responder, "link-proof");
2053     try expectLinkStatus(&responder, data.link_id, .handshake);
2054     const delivered = try hear(&responder, 0, data.output_raw, data.clock, @splat(0));
2055     try std.testing.expectEqual(@as(usize, 1), delivered.len);
2056     try expectEffectTag(delivered[0], .link_delivery);
2057     const handed = delivered[0].link_delivery;
2058     try expectCorpusBytes(data.name, "plaintext", data.plaintext, handed.plaintext);
2059 }
2060 
2061 test "Reticulum@1.5.0 RNS/Transport.py:1576-1615 a replayed link packet changes nothing" {
2062     const data = linkVector("link-data");
2063     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2064     var owner = try initNode(&storage);
2065     defer _ = owner.deinit();
2066     try activeResponder(&owner, .all);
2067     const first = try hear(&owner, 0, data.output_raw, data.clock, @splat(0));
2068     try std.testing.expectEqual(@as(usize, 2), first.len);
2069     const replayed = try hear(&owner, 0, data.output_raw, data.clock + 1, @splat(0));
2070     try expectLinkDiagnostic(replayed, .duplicate_packet);
2071     try expectLinkStatus(&owner, data.link_id, .active);
2072 }
2073 
2074 test "Reticulum@1.5.0 RNS/Link.py:753-766 an initiator turns stale and closes at its deadline" {
2075     const link_id = link_key.link_id;
2076     var initiator_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2077     var initiator = try initNode(&initiator_storage);
2078     defer _ = initiator.deinit();
2079     var responder_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2080     var responder = try initNode(&responder_storage);
2081     defer _ = responder.deinit();
2082     try activeInitiator(&initiator);
2083     try activeResponder(&responder, .all);
2084     try exchangeCorpusData(&initiator, &responder);
2085     _ = try expireLink(&initiator, link_id, 1700000509);
2086     const quiet = try expireLink(&initiator, link_id, 1700000714);
2087     try std.testing.expectEqual(@as(usize, 1), quiet.len);
2088     try std.testing.expectEqual(@as(u64, 1700000715), quiet[0].schedule_timer.at);
2089     try expectLinkStatus(&initiator, link_id, .active);
2090     const turned = try expireLink(&initiator, link_id, 1700000715);
2091     try std.testing.expectEqual(@as(usize, 2), turned.len);
2092     try expectEffectTag(turned[1], .schedule_timer);
2093     try std.testing.expectEqual(@as(u64, 1700000720), turned[1].schedule_timer.at);
2094     try expectLinkStatus(&initiator, link_id, .stale);
2095     const waiting = try expireLink(&initiator, link_id, 1700000719);
2096     try std.testing.expectEqual(@as(usize, 1), waiting.len);
2097     try std.testing.expectEqual(@as(u64, 1700000720), waiting[0].schedule_timer.at);
2098     const closed = try expireLink(&initiator, link_id, 1700000720);
2099     try std.testing.expectEqual(@as(usize, 2), closed.len);
2100     try expectEffectTag(closed[1], .link_closed);
2101     try std.testing.expectEqual(
2102         reticulum.node.LinkCloseReason.timeout,
2103         closed[1].link_closed.reason,
2104     );
2105     const closing = try reticulum.wire.decode(try expectSentFrame(closed));
2106     try std.testing.expectEqual(reticulum.wire.Context.linkclose, closing.context);
2107     try std.testing.expectEqual(@as(usize, 0), initiator.transport.links.count());
2108     try std.testing.expect(!initiator.timers.contains(.{ .link = link_id[0..16].* }));
2109 }
2110 
2111 test "Reticulum@1.5.0 RNS/Link.py:938-946 inbound traffic returns a stale link to active" {
2112     const link_id = link_key.link_id;
2113     const keepalive = linkVector("link-keepalive");
2114     var initiator_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2115     var initiator = try initNode(&initiator_storage);
2116     defer _ = initiator.deinit();
2117     var responder_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2118     var responder = try initNode(&responder_storage);
2119     defer _ = responder.deinit();
2120     try activeInitiator(&initiator);
2121     try activeResponder(&responder, .all);
2122     try exchangeCorpusData(&initiator, &responder);
2123     const quiet = try expireLink(&responder, link_id, 1700000714);
2124     try std.testing.expectEqual(@as(usize, 1), quiet.len);
2125     try std.testing.expectEqual(@as(u64, 1700000715), quiet[0].schedule_timer.at);
2126     const turned = try expireLink(&responder, link_id, 1700000715);
2127     try std.testing.expectEqual(@as(usize, 1), turned.len);
2128     try std.testing.expectEqual(@as(u64, 1700000720), turned[0].schedule_timer.at);
2129     try expectLinkStatus(&responder, link_id, .stale);
2130     const answered = try hear(&responder, 0, keepalive.output_raw, 1700000716, @splat(0));
2131     try std.testing.expectEqual(@as(usize, 1), answered.len);
2132     try expectEffectTag(answered[0], .carrier_send);
2133     try expectLinkStatus(&responder, link_id, .active);
2134     const rescheduled = try expireLink(&responder, link_id, 1700000720);
2135     try std.testing.expectEqual(@as(usize, 1), rescheduled.len);
2136     try std.testing.expectEqual(@as(u64, 1700001128), rescheduled[0].schedule_timer.at);
2137     try expectLinkStatus(&responder, link_id, .active);
2138 }
2139 
2140 test "Reticulum@1.5.0 RNS/Link.py:674-683 a link close from either side removes the link" {
2141     const close = linkVector("link-close");
2142     var initiator_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2143     var initiator = try initNode(&initiator_storage);
2144     defer _ = initiator.deinit();
2145     var responder_storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2146     var responder = try initNode(&responder_storage);
2147     defer _ = responder.deinit();
2148     try activeInitiator(&initiator);
2149     try activeResponder(&responder, .all);
2150     var bytes: [reticulum.wire.mtu]u8 = undefined;
2151     const forged = try linkDataFrame(&bytes, .linkclose, @splat(0x11), "not the link id");
2152     const ignored = try hear(&initiator, 0, forged, close.clock, @splat(0));
2153     try expectLinkDiagnostic(ignored, .link_close_invalid);
2154     try expectLinkStatus(&initiator, close.link_id, .active);
2155     const closing = try closeLink(&responder, close.iv[0..16].*, close.clock);
2156     try std.testing.expectEqual(@as(usize, 2), closing.len);
2157     try expectEffectTag(closing[1], .link_closed);
2158     try std.testing.expectEqual(
2159         reticulum.node.LinkCloseReason.destination_closed,
2160         closing[1].link_closed.reason,
2161     );
2162     const sent = try expectSentFrame(closing);
2163     const heard = try hear(&initiator, 0, sent, close.clock, @splat(0));
2164     try std.testing.expectEqual(@as(usize, 1), heard.len);
2165     try expectEffectTag(heard[0], .link_closed);
2166     try std.testing.expectEqual(
2167         reticulum.node.LinkCloseReason.destination_closed,
2168         heard[0].link_closed.reason,
2169     );
2170     try std.testing.expectEqual(@as(usize, 0), initiator.transport.links.count());
2171 }
2172 
2173 test "Reticulum@1.5.0 RNS/Link.py:662-664 closing a pending link sends no frame" {
2174     const request = linkVector("link-request");
2175     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2176     var owner = try initNode(&storage);
2177     defer _ = owner.deinit();
2178     try hearLinkAnnounce(&owner);
2179     _ = try openVectorLink(&owner);
2180     const effects = try closeLink(&owner, @splat(0x22), request.clock);
2181     try std.testing.expectEqual(@as(usize, 1), effects.len);
2182     try expectEffectTag(effects[0], .link_closed);
2183     try std.testing.expectEqual(
2184         reticulum.node.LinkCloseReason.initiator_closed,
2185         effects[0].link_closed.reason,
2186     );
2187     try std.testing.expectEqual(@as(usize, 0), owner.transport.links.count());
2188     try std.testing.expect(!owner.timers.contains(.{ .link = request.link_id[0..16].* }));
2189     const unknown = closeLink(&owner, @splat(0x22), request.clock);
2190     try std.testing.expectError(error.UnknownLink, unknown);
2191 }
2192 
2193 test "Reticulum@1.5.0 RNS/Packet.py:419-420,537-538 a link receipt fails at its deadline" {
2194     const data = linkVector("link-data");
2195     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2196     var owner = try initNode(&storage);
2197     defer _ = owner.deinit();
2198     try activeInitiator(&owner);
2199     const sent = try sendOnLink(&owner, data.plaintext, data.iv[0..16].*, data.clock, true);
2200     const hash = try reticulum.wire.hash.full(try expectSentFrame(sent));
2201     const early = try owner.step(.{ .timer_expired = .{
2202         .id = .{ .receipt = hash },
2203         .now = data.clock + 6,
2204         .entropy = @splat(0),
2205     } });
2206     try std.testing.expectEqual(@as(usize, 1), early.len);
2207     try expectEffectTag(early[0], .schedule_timer);
2208     try std.testing.expectEqual(data.clock + 7, early[0].schedule_timer.at);
2209     const failed = try owner.step(.{ .timer_expired = .{
2210         .id = .{ .receipt = hash },
2211         .now = data.clock + 7,
2212         .entropy = @splat(0),
2213     } });
2214     try std.testing.expectEqual(@as(usize, 1), failed.len);
2215     try expectEffectTag(failed[0], .receipt_update);
2216     try std.testing.expectEqual(
2217         reticulum.packet.receipt.Status.failed,
2218         failed[0].receipt_update.status,
2219     );
2220 }
2221 
2222 test "a third link receipt culls the first and reports it" {
2223     const data = linkVector("link-data");
2224     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2225     var owner = try initNode(&storage);
2226     defer _ = owner.deinit();
2227     try activeInitiator(&owner);
2228     var iv = data.iv[0..16].*;
2229     const sent = try sendOnLink(&owner, data.plaintext, iv, data.clock, true);
2230     const first = try reticulum.wire.hash.full(try expectSentFrame(sent));
2231     iv[15] +%= 1;
2232     _ = try sendOnLink(&owner, data.plaintext, iv, data.clock, true);
2233     try std.testing.expectEqual(node_limits.receipts_max, owner.receipts.count());
2234     iv[15] +%= 1;
2235     const effects = try sendOnLink(&owner, data.plaintext, iv, data.clock, true);
2236     var culled: usize = 0;
2237     for (effects) |effect| switch (effect) {
2238         .receipt_update => |update| {
2239             try std.testing.expectEqualSlices(u8, &first, &update.packet_hash);
2240             try std.testing.expectEqual(reticulum.packet.receipt.Status.culled, update.status);
2241             try std.testing.expectEqual(@as(?u64, null), update.rtt);
2242             culled += 1;
2243         },
2244         else => {},
2245     };
2246     try std.testing.expectEqual(@as(usize, 1), culled);
2247     try std.testing.expect(owner.receipts.find(first) == null);
2248 }
2249 
2250 test "Reticulum@1.5.0 RNS/Packet.py:439-452 a longer link proof still concludes the receipt" {
2251     const data = linkVector("link-data");
2252     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2253     var owner = try initNode(&storage);
2254     defer _ = owner.deinit();
2255     try activeInitiator(&owner);
2256     const sent = try sendOnLink(&owner, data.plaintext, data.iv[0..16].*, data.clock, true);
2257     const hash = try reticulum.wire.hash.full(try expectSentFrame(sent));
2258     var private = reticulum.identity.Private.fromBytes(
2259         link_session.destination_private_key[0..64].*,
2260     );
2261     defer private.zero();
2262     var payload: [reticulum.wire.proof.explicit_bytes + 1]u8 = undefined;
2263     payload[0..32].* = hash;
2264     payload[32..96].* = private.sign(&hash);
2265     payload[96] = 0x5a;
2266     var bytes: [reticulum.wire.mtu]u8 = undefined;
2267     payload[40] ^= 0x01;
2268     const forged = try linkProofFrame(&bytes, &payload);
2269     const rejected = try hear(&owner, 0, forged, data.clock, @splat(0));
2270     try expectLinkDiagnostic(rejected, .proof_rejected);
2271     payload[40] ^= 0x01;
2272     payload[0] ^= 0x01;
2273     const mismatched = try linkProofFrame(&bytes, &payload);
2274     const unmatched = try hear(&owner, 0, mismatched, data.clock, @splat(0));
2275     try expectLinkDiagnostic(unmatched, .proof_rejected);
2276     payload[0] ^= 0x01;
2277     const proof = try linkProofFrame(&bytes, &payload);
2278     const effects = try hear(&owner, 0, proof, data.clock, @splat(0));
2279     try std.testing.expectEqual(@as(usize, 1), effects.len);
2280     try expectEffectTag(effects[0], .receipt_update);
2281     try std.testing.expectEqual(
2282         reticulum.packet.receipt.Status.delivered,
2283         effects[0].receipt_update.status,
2284     );
2285 }
2286 
2287 test "Reticulum@1.5.0 RNS/Link.py:749-751 a quiet inbound side brings the keepalive forward" {
2288     const data = linkVector("link-data");
2289     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2290     var owner = try initNode(&storage);
2291     defer _ = owner.deinit();
2292     try activeInitiator(&owner);
2293     _ = try sendOnLink(&owner, data.plaintext, data.iv[0..16].*, data.clock, false);
2294     const quiet = try expireLink(&owner, data.link_id, 1700000507);
2295     try std.testing.expectEqual(@as(usize, 1), quiet.len);
2296     try std.testing.expectEqual(@as(u64, 1700000508), quiet[0].schedule_timer.at);
2297     const effects = try expireLink(&owner, data.link_id, 1700000508);
2298     try std.testing.expectEqual(@as(usize, 2), effects.len);
2299     const keepalive = linkVector("link-keepalive");
2300     const sent = try expectSentFrame(effects);
2301     try expectCorpusBytes(keepalive.name, "output_raw", keepalive.output_raw, sent);
2302     try std.testing.expectEqual(@as(u64, 1700000714), effects[1].schedule_timer.at);
2303 }
2304 
2305 test "Reticulum@1.5.0 RNS/Packet.py:290-300 a handshake responder refuses an application send" {
2306     const data = linkVector("link-data");
2307     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2308     var owner = try initNode(&storage);
2309     defer _ = owner.deinit();
2310     try registerLinkDestination(&owner);
2311     _ = try hearLinkVector(&owner, "link-proof");
2312     try expectLinkStatus(&owner, data.link_id, .handshake);
2313     const refused = sendOnLink(&owner, data.plaintext, data.iv[0..16].*, data.clock, false);
2314     try std.testing.expectError(error.LinkNotEstablished, refused);
2315     try std.testing.expectEqual(@as(usize, 0), owner.receipts.len);
2316 }
2317 
2318 test "Reticulum@1.5.0 RNS/Packet.py:419-420 a link receipt timeout truncates to whole seconds" {
2319     const data = linkVector("link-data");
2320     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2321     var owner = try initNode(&storage);
2322     defer _ = owner.deinit();
2323     try registerLinkDestination(&owner);
2324     _ = try hearLinkVector(&owner, "link-proof");
2325     const rtt = reticulum.wire.link.encodeRtt(1.4);
2326     var bytes: [reticulum.wire.mtu]u8 = undefined;
2327     const frame = try linkDataFrame(&bytes, .lrrtt, @splat(0x07), &rtt);
2328     _ = try hear(&owner, 0, frame, 1700000302, @splat(0));
2329     try expectLinkStatus(&owner, data.link_id, .active);
2330     const effects = try sendOnLink(&owner, data.plaintext, data.iv[0..16].*, data.clock, true);
2331     try std.testing.expectEqual(@as(usize, 2), effects.len);
2332     try expectEffectTag(effects[0], .schedule_timer);
2333     try std.testing.expectEqual(data.clock + 9, effects[0].schedule_timer.at);
2334 }
2335 
2336 test "Reticulum@1.5.0 RNS/Link.py:73, RNS/Packet.py:236-237 a link send fits one frame" {
2337     const data = linkVector("link-data");
2338     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2339     var owner = try initNode(&storage);
2340     defer _ = owner.deinit();
2341     try activeInitiator(&owner);
2342     const plaintext: [432]u8 = @splat(0x7a);
2343     const iv = data.iv[0..16].*;
2344     const admitted = try sendOnLink(&owner, plaintext[0..431], iv, data.clock, false);
2345     try std.testing.expectEqual(@as(usize, 1), admitted.len);
2346     const sent = try expectSentFrame(admitted);
2347     try std.testing.expect(sent.len <= reticulum.wire.mtu);
2348     const refused = sendOnLink(&owner, &plaintext, iv, data.clock, false);
2349     try std.testing.expectError(error.PacketTooLarge, refused);
2350     try expectLinkStatus(&owner, data.link_id, .active);
2351 }
2352 
2353 test "Reticulum@1.5.0 RNS/Link.py:948-962 a link delivery can ask the application to prove" {
2354     const data = linkVector("link-data");
2355     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2356     var owner = try initNode(&storage);
2357     defer _ = owner.deinit();
2358     try activeResponder(&owner, .app);
2359     const effects = try hear(&owner, 0, data.output_raw, data.clock, @splat(0));
2360     try std.testing.expectEqual(@as(usize, 1), effects.len);
2361     try expectEffectTag(effects[0], .link_delivery);
2362     const delivery = effects[0].link_delivery;
2363     try std.testing.expect(delivery.proof_requested);
2364     const proved = try owner.step(.{ .application_link_prove = .{
2365         .link_id = data.link_id[0..16].*,
2366         .packet_hash = delivery.packet_hash,
2367         .now = data.clock,
2368     } });
2369     try std.testing.expectEqual(@as(usize, 1), proved.len);
2370     const sent = try expectSentFrame(proved);
2371     const proof = linkVector("link-data-proof");
2372     try expectCorpusBytes(proof.name, "output_raw", proof.output_raw, sent);
2373     const unknown = owner.step(.{ .application_link_prove = .{
2374         .link_id = data.link_id[0..16].*,
2375         .packet_hash = @splat(0x33),
2376         .now = data.clock,
2377     } });
2378     try std.testing.expectError(error.ProofUnavailable, unknown);
2379 }
2380 
2381 test "Reticulum@1.5.0 RNS/Destination.py:283-315 outbound announce node corpus" {
2382     for (reticulum.conformance.announce.vectors) |vector| {
2383         var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2384         var owner = try initNode(&storage);
2385         try registerAnnounceOwner(&owner, vector);
2386         var ring_bytes: [ring_capacity.storage_bytes]u8 align(8) = undefined;
2387         var ring = try reticulum.identity.Ring.init(&ring_bytes, .{ .retained_max = 2 });
2388         ring.activate();
2389         const has_rotating = vector.ratchet_private_key.len == 32;
2390         if (has_rotating) try owner.bindRatchetRing(0, &ring, false);
2391         const effects = try owner.step(.{ .application_announce = .{
2392             .destination = vector.destination_hash[0..16].*,
2393             .app_data = vector.app_data,
2394             .random = vector.random_hash[0..5].*,
2395             .fresh_rotating_key = if (has_rotating)
2396                 vector.ratchet_private_key[0..32].*
2397             else
2398                 null,
2399             .now = vector.clock,
2400         } });
2401         try expectCorpusBytes(vector.name, "raw", vector.raw, try expectSentFrame(effects));
2402         _ = owner.deinit();
2403         _ = ring.deinit();
2404     }
2405 }
2406 
2407 fn registerInboundVector(
2408     owner: *reticulum.node.Node,
2409     vector: reticulum.conformance.packet.Vector,
2410 ) !void {
2411     const hash = vector.destination_hash[0..16].*;
2412     switch (vector.destination_type) {
2413         0 => {
2414             if (vector.destination_private_key.len != 64) return;
2415             owner.identities[0] = reticulum.identity.Private.fromBytes(
2416                 vector.destination_private_key[0..64].*,
2417             );
2418             try owner.destinations.register(.{
2419                 .hash = hash,
2420                 .kind = .single,
2421                 .proof_strategy = .none,
2422                 .identity_index = 0,
2423             });
2424         },
2425         1 => try registerGroup(owner, hash, vector.destination_key[0..64].*),
2426         2 => try registerPlain(owner, hash),
2427         else => {},
2428     }
2429 }
2430 
2431 fn expectInboundData(
2432     vector: reticulum.conformance.packet.Vector,
2433     effects: []const reticulum.node.Effect,
2434 ) !void {
2435     try expectCorpusValue(vector.name, "effect_count", @as(usize, 1), effects.len);
2436     try expectCorpusValue(
2437         vector.name,
2438         "effect_tag",
2439         std.meta.Tag(reticulum.node.Effect).application_delivery,
2440         std.meta.activeTag(effects[0]),
2441     );
2442     const delivery = effects[0].application_delivery;
2443     try expectCorpusBytes(vector.name, "plaintext", vector.plaintext, delivery.plaintext);
2444     try expectCorpusBytes(vector.name, "packet_hash", vector.packet_hash, &delivery.packet_hash);
2445 }
2446 
2447 fn expectInboundAnnounce(
2448     owner: *reticulum.node.Node,
2449     vector: reticulum.conformance.packet.Vector,
2450     effects: []const reticulum.node.Effect,
2451 ) !void {
2452     try expectCorpusValue(vector.name, "effect_count", @as(usize, 1), effects.len);
2453     try expectCorpusValue(
2454         vector.name,
2455         "effect_tag",
2456         std.meta.Tag(reticulum.node.Effect).announce_received,
2457         std.meta.activeTag(effects[0]),
2458     );
2459     const received = effects[0].announce_received;
2460     try expectCorpusBytes(
2461         vector.name,
2462         "destination_hash",
2463         vector.destination_hash,
2464         &received.destination_hash,
2465     );
2466     var private = reticulum.identity.Private.fromBytes(vector.destination_private_key[0..64].*);
2467     defer private.zero();
2468     const expected_key = private.publicBytes();
2469     try expectCorpusBytes(vector.name, "public_key", &expected_key, &received.public_key);
2470     const recalled = owner.known_identities.recall(received.destination_hash);
2471     try expectCorpusValue(vector.name, "identity_recalled", true, recalled != null);
2472     try expectCorpusBytes(vector.name, "recalled_key", &expected_key, &recalled.?.public_key);
2473 }
2474 
2475 fn expectInboundDiagnostic(
2476     vector: reticulum.conformance.packet.Vector,
2477     effects: []const reticulum.node.Effect,
2478 ) !void {
2479     try expectCorpusValue(vector.name, "effect_count", @as(usize, 1), effects.len);
2480     try expectCorpusValue(
2481         vector.name,
2482         "effect_tag",
2483         std.meta.Tag(reticulum.node.Effect).diagnostic,
2484         std.meta.activeTag(effects[0]),
2485     );
2486     if (std.mem.eql(u8, vector.name, "announce-plain-filtered")) {
2487         try expectCorpusValue(
2488             vector.name,
2489             "diagnostic",
2490             reticulum.node.Code.packet_filtered,
2491             effects[0].diagnostic.code,
2492         );
2493     }
2494     if (vector.decode_verdict) {
2495         try expectCorpusValue(
2496             vector.name,
2497             "diagnostic_hash_present",
2498             true,
2499             effects[0].diagnostic.packet_hash != null,
2500         );
2501         const actual = effects[0].diagnostic.packet_hash.?;
2502         try expectCorpusBytes(vector.name, "packet_hash", vector.packet_hash, &actual);
2503     }
2504 }
2505 
2506 fn expectInboundVector(vector: reticulum.conformance.packet.Vector) !void {
2507     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2508     var owner = try initNode(&storage);
2509     defer _ = owner.deinit();
2510     const is_announce = std.mem.startsWith(u8, vector.name, "announce-");
2511     if (!is_announce) try registerInboundVector(&owner, vector);
2512     const effects = try owner.step(.{ .carrier_frame = .{
2513         .interface = 0,
2514         .now = 1_700_000_100,
2515         .bytes = vector.raw,
2516         .entropy = @splat(0),
2517     } });
2518     if (std.mem.startsWith(u8, vector.name, "data-")) {
2519         return expectInboundData(vector, effects);
2520     }
2521     if (std.mem.startsWith(u8, vector.name, "announce-single-")) {
2522         return expectInboundAnnounce(&owner, vector, effects);
2523     }
2524     return expectInboundDiagnostic(vector, effects);
2525 }
2526 
2527 test "Reticulum@1.5.0 RNS/Transport.py:1597-1600,1700-1716 inbound node packet corpus" {
2528     const names = [_][]const u8{
2529         "data-single-hop-0",
2530         "data-group-hop-1",
2531         "data-plain-hop-127",
2532         "link-request-single-hop-128",
2533         "proof-link-hop-255",
2534         "announce-plain-filtered",
2535     };
2536     for (names) |name| try expectInboundVector(packetVector(name));
2537     for (reticulum.conformance.packet.vectors) |vector| {
2538         if (!std.mem.startsWith(u8, vector.name, "announce-single-")) continue;
2539         if (!vector.pack_verdict) continue;
2540         try expectInboundVector(vector);
2541     }
2542 }
2543 
2544 test "Reticulum@1.5.0 RNS/Identity.py:511-578 node accepts a 481-byte HEADER_1 announce" {
2545     const vector = packetVector("announce-single-header1-payload-max");
2546     try std.testing.expectEqual(@as(usize, reticulum.wire.mtu), vector.raw.len);
2547     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2548     var owner = try initNode(&storage);
2549     defer _ = owner.deinit();
2550     const effects = try owner.step(.{ .carrier_frame = .{
2551         .interface = 0,
2552         .now = 1_700_000_100,
2553         .bytes = vector.raw,
2554         .entropy = @splat(0),
2555     } });
2556     try expectInboundAnnounce(&owner, vector, effects);
2557     const recalled = owner.known_identities.recall(vector.destination_hash[0..16].*).?;
2558     try std.testing.expectEqual(
2559         @as(usize, reticulum.destination.announce.received_app_bytes_max),
2560         recalled.applicationData().len,
2561     );
2562 }
2563 
2564 test "Reticulum@1.5.0 RNS/Packet.py:288 refuses pathfinder hop maximum" {
2565     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2566     var owner = try initNode(&storage);
2567     defer _ = owner.deinit();
2568     try rememberPacketSingle(&owner, 0);
2569     const vector = packetVector("data-single-hop-0");
2570     try std.testing.expectError(error.InvalidHops, owner.step(.{ .application_send = .{
2571         .destination = vector.destination_hash[0..16].*,
2572         .now = 1,
2573         .plaintext = &.{},
2574         .hops = reticulum.wire.pathfinder_hops,
2575         .ephemeral_private = vector.ephemeral_private_key[0..32].*,
2576         .iv = vector.iv[0..16].*,
2577     } }));
2578 }
2579 
2580 test "Reticulum@1.5.0 RNS/Packet.py:178-242 rejects raw packet above mtu" {
2581     const destination_hash: [16]u8 = @splat(0x71);
2582     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2583     var owner = try initNode(&storage);
2584     defer _ = owner.deinit();
2585     try registerPlain(&owner, destination_hash);
2586     const plaintext: [reticulum.wire.mtu - reticulum.wire.header_one_bytes + 1]u8 =
2587         @splat(0xa5);
2588     try std.testing.expectError(error.PacketTooLarge, owner.step(.{ .application_send = .{
2589         .destination = destination_hash,
2590         .now = 1,
2591         .plaintext = &plaintext,
2592         .ephemeral_private = @splat(0),
2593         .iv = @splat(0),
2594     } }));
2595 }
2596 
2597 test "Reticulum@1.5.0 RNS/Transport.py:1540-1552 broadcasts on maximum carriers" {
2598     const selected = reticulum.node.Limits{
2599         .interfaces_max = 256,
2600         .destinations_max = 1,
2601         .known_identities_max = 1,
2602         .known_ratchets_max = 1,
2603         .receipts_max = 1,
2604         .duplicate_hashes_max = 1,
2605         .timers_max = 1,
2606         .effects_max = reticulum.node.effects_per_event_max,
2607         .effect_frames_max = 256,
2608         .paths_max = 1,
2609         .announces_max = 1,
2610         .reverse_entries_max = 1,
2611         .path_request_tags_max = 1,
2612         .inflight_requests_max = 1,
2613         .discoveries_max = 1,
2614         .links_max = 1,
2615         .link_entries_max = 1,
2616     };
2617     const derived = comptime reticulum.node.Capacity.derive(selected) catch unreachable;
2618     var storage: [derived.storage_bytes]u8 align(8) = undefined;
2619     var owner = try reticulum.node.Node.init(&storage, selected);
2620     owner.activate();
2621     defer _ = owner.deinit();
2622     for (0..selected.interfaces_max) |index| {
2623         try owner.registerCarrier(@intCast(index), true, null);
2624     }
2625     const destination_hash: [16]u8 = @splat(0x81);
2626     try registerPlain(&owner, destination_hash);
2627     const effects = try owner.step(.{ .application_send = .{
2628         .destination = destination_hash,
2629         .now = 1,
2630         .plaintext = "broadcast",
2631         .ephemeral_private = @splat(0),
2632         .iv = @splat(0),
2633     } });
2634     try std.testing.expectEqual(@as(usize, 256), effects.len);
2635     try std.testing.expectEqual(@as(usize, 256), owner.effects.frames_used);
2636 }
2637 
2638 test "Reticulum@1.5.0 RNS/Transport.py:803-805 hashlist timer rotates" {
2639     var storage: [node_capacity.storage_bytes]u8 align(8) = undefined;
2640     var owner = try initNode(&storage);
2641     defer _ = owner.deinit();
2642     for (0..9) |index| {
2643         _ = owner.duplicate_hashes.insert(@splat(@as(u8, @intCast(index))));
2644     }
2645     try owner.timers.schedule(.hashlist, 10);
2646     const effects = try owner.step(.{ .timer_expired = .{
2647         .id = .hashlist,
2648         .now = 10,
2649         .entropy = @splat(0),
2650     } });
2651     try std.testing.expectEqual(@as(usize, 0), effects.len);
2652     try std.testing.expectEqual(@as(usize, 0), owner.duplicate_hashes.generation_len);
2653     try std.testing.expectEqual(@as(usize, 0), owner.timers.count());
2654 }