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 }