lib/reticulum/src/node/fixture/test.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const pretty = @import("pretty");
  3 const reticulum = @import("../../root.zig");
  4 const fixture = @import("root.zig");
  5 
  6 const carrier = reticulum.carrier;
  7 const ifac = reticulum.interface.ifac;
  8 const node = reticulum.node;
  9 const packet = reticulum.packet;
 10 const wire = reticulum.wire;
 11 
 12 const Vector = reticulum.conformance.transport.ThreeNodeVector;
 13 const LinkVector = reticulum.conformance.link.ThreeNodeVector;
 14 const World = fixture.World;
 15 
 16 /// The seconds a reverse entry lives, so a relayed proof can still find its way back within them.
 17 const proof_lifetime: fixture.Seconds = 480;
 18 
 19 fn vector() Vector {
 20     const vectors = reticulum.conformance.transport.three_node_vectors;
 21     std.debug.assert(vectors.len == 1);
 22     return vectors[0];
 23 }
 24 
 25 fn codes() !fixture.Codes {
 26     return .{
 27         .a_b = .{ .key = try ifac.derive("tiny", "a-b"), .size = .bytes_16 },
 28         .b_c = .{ .key = try ifac.derive("tiny", "b-c"), .size = .bytes_8 },
 29     };
 30 }
 31 
 32 fn options(keyed: bool, discovery: ?carrier.Index) !fixture.Options {
 33     const proof = vector();
 34     return .{
 35         .start = proof.start_clock,
 36         .transport_hash = proof.transport_identity_hash[0..16].*,
 37         .codes = if (keyed) try codes() else .{},
 38         .discover_paths = discovery,
 39     };
 40 }
 41 
 42 fn open(world: *World, keyed: bool, discovery: ?carrier.Index) !void {
 43     try world.init(try options(keyed, discovery));
 44     const proof = vector();
 45     const owner = world.at(.c);
 46     owner.identities[0] = reticulum.identity.Private.fromBytes(
 47         proof.destination_private_key[0..64].*,
 48     );
 49     try owner.destinations.register(.{
 50         .hash = proof.destination_hash[0..16].*,
 51         .name_hash = proof.destination_name_hash[0..10].*,
 52         .kind = .single,
 53         .proof_strategy = .all,
 54         .identity_index = 0,
 55     });
 56 }
 57 
 58 fn announce(world: *World, response: ?carrier.Index) !void {
 59     const proof = vector();
 60     try world.step(.c, .{ .application_announce = .{
 61         .destination = proof.destination_hash[0..16].*,
 62         .app_data = proof.destination_app_data,
 63         .random = proof.announce_random_hash[0..5].*,
 64         .fresh_rotating_key = null,
 65         .now = world.clock,
 66         .path_response = response,
 67     } });
 68 }
 69 
 70 /// Puts the corpus data packet on the wire from node A toward node C's destination at whatever
 71 /// second the world holds, after exclusive-oring the salt into the final byte of the initialization
 72 /// vector. Salt 0 sends the corpus bytes as generated. Another salt gives a distinct packet, so one
 73 /// world can carry more than one send.
 74 fn sendData(world: *World, salt: u8) !void {
 75     const proof = vector();
 76     var iv = proof.iv[0..16].*;
 77     iv[15] ^= salt;
 78     try world.step(.a, .{ .application_send = .{
 79         .destination = proof.destination_hash[0..16].*,
 80         .now = world.clock,
 81         .plaintext = proof.plaintext,
 82         .ephemeral_private = proof.ephemeral_private_key[0..32].*,
 83         .iv = iv,
 84     } });
 85 }
 86 
 87 /// Alters the corpus tag, which lets a single world carry more than one path request that differs
 88 /// from the rest.
 89 fn tagged(salt: u8) [16]u8 {
 90     var tag = vector().request_tag[0..16].*;
 91     tag[15] ^= salt;
 92     return tag;
 93 }
 94 
 95 fn requestPath(world: *World, tag: [16]u8) !void {
 96     const proof = vector();
 97     try world.step(.a, .{ .application_path_request = .{
 98         .destination = proof.destination_hash[0..16].*,
 99         .tag = tag,
100         .interface = null,
101         .now = world.clock,
102     } });
103 }
104 
105 fn accessOf(keyed: bool, link: fixture.LinkId) !?node.Access {
106     if (!keyed) return null;
107     const pair = try codes();
108     return switch (link) {
109         .a_to_b, .b_to_a => pair.a_b,
110         .b_to_c, .c_to_b => pair.b_c,
111     };
112 }
113 
114 fn expectFrame(
115     world: *const World,
116     keyed: bool,
117     link: fixture.LinkId,
118     index: usize,
119     expected: []const u8,
120 ) !void {
121     const delivery = world.frameOn(link, index) orelse return error.MissingLinkFrame;
122     const access = try accessOf(keyed, link);
123     var stripped: [carrier.frame_bytes_max]u8 = undefined;
124     const raw = if (access) |value|
125         try ifac.strip(&value.key, value.size, delivery.frame.slice(), &stripped)
126     else
127         delivery.frame.slice();
128     if (std.mem.eql(u8, expected, raw)) return;
129     return report(link, index, expected, raw);
130 }
131 
132 fn report(link: fixture.LinkId, index: usize, expected: []const u8, actual: []const u8) !void {
133     var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
134     defer arena.deinit();
135     var value = try pretty.diagnostic.Report.init(arena.allocator(), "World frame mismatch");
136     defer value.deinit();
137     try value.field("link", "{s}", .{@tagName(link)});
138     try value.field("index", "{d}", .{index});
139     try value.field("expected", "{any}", .{expected});
140     try value.field("actual", "{any}", .{actual});
141     pretty.diagnostic.writeStderr(&value, .{ .width = 100 });
142     return error.WorldFrameMismatch;
143 }
144 
145 fn expectCounts(world: *const World, expected: [4]usize) !void {
146     inline for (.{ .a_to_b, .b_to_a, .b_to_c, .c_to_b }, 0..) |link, index| {
147         try std.testing.expectEqual(expected[index], world.frameCount(link));
148     }
149 }
150 
151 fn frameAt(world: *const World, link: fixture.LinkId, index: usize) !fixture.Seconds {
152     const delivery = world.frameOn(link, index) orelse return error.MissingLinkFrame;
153     return delivery.at;
154 }
155 
156 fn deadlineOf(world: *const World, owner: fixture.NodeId) !fixture.Seconds {
157     var found: ?fixture.Seconds = null;
158     for (world.records()) |entry| {
159         if (entry.owner != owner or entry.kind != .timer) continue;
160         if (entry.tag != .receipt) continue;
161         found = entry.deadline;
162     }
163     return found orelse error.MissingReceiptTimer;
164 }
165 
166 fn codeCount(world: *const World, owner: fixture.NodeId, code: node.Code) usize {
167     var count: usize = 0;
168     for (world.records()) |entry| {
169         if (entry.owner != owner or entry.kind != .diagnostic) continue;
170         if (entry.code != code) continue;
171         count += 1;
172     }
173     return count;
174 }
175 
176 /// Carries C's announce to A through B and checks every frame against the corpus, as
177 /// Reticulum@1.5.0 RNS/Transport.py:737-799 rebroadcasts it. It leaves A with a path two hops away
178 /// and leaves C reporting its own announce twice.
179 fn runAnnounceStep(world: *World, keyed: bool) !void {
180     const proof = vector();
181     try announce(world, null);
182     try world.runTo(proof.rebroadcast_clock);
183     try world.runTo(proof.retry_clock);
184     try world.runTo(proof.send_clock - 1);
185     try expectCounts(world, .{ 0, 2, 2, 1 });
186     try expectFrame(world, keyed, .c_to_b, 0, proof.announce_raw);
187     try expectFrame(world, keyed, .b_to_a, 0, proof.rebroadcast_raw);
188     try expectFrame(world, keyed, .b_to_c, 0, proof.rebroadcast_raw);
189     try expectFrame(world, keyed, .b_to_a, 1, proof.retry_raw);
190     try expectFrame(world, keyed, .b_to_c, 1, proof.retry_raw);
191     const learned = world.lastRecord(.a, .announce) orelse return error.MissingAnnounce;
192     try std.testing.expectEqual(@as(u8, 2), learned.hops);
193     try std.testing.expect(!learned.path_response);
194     try std.testing.expectEqual(@as(usize, 1), world.recordCount(.a, .announce));
195     try std.testing.expectEqual(@as(usize, 1), world.recordCount(.b, .announce));
196     try std.testing.expectEqual(@as(usize, 0), world.recordCount(.c, .announce));
197     try std.testing.expectEqual(@as(usize, 2), codeCount(world, .c, .own_announce));
198 }
199 
200 /// Sends the corpus data from A to C and checks that the packet and its proof cross B in both
201 /// directions, as Reticulum@1.5.0 RNS/Transport.py:1345-1356,2670-2677 crosses it. It checks each
202 /// of the four frames against the corpus bytes, the plaintext C delivered, and the receipt A
203 /// concluded. The `sent` counts say how many frames each link has already carried, so the step can
204 /// run in a world that has carried traffic before.
205 fn runRelayStep(world: *World, keyed: bool, sent: [4]usize) !void {
206     const proof = vector();
207     try sendData(world, 0);
208     try expectCounts(world, .{ sent[0] + 1, sent[1] + 1, sent[2] + 1, sent[3] + 1 });
209     try expectFrame(world, keyed, .a_to_b, sent[0], proof.inserted_raw);
210     try expectFrame(world, keyed, .b_to_c, sent[2], proof.stripped_raw);
211     try expectFrame(world, keyed, .c_to_b, sent[3], proof.proof_raw);
212     try expectFrame(world, keyed, .b_to_a, sent[1], proof.relayed_proof_raw);
213     const delivery = world.lastRecord(.c, .delivery) orelse return error.MissingDelivery;
214     try std.testing.expectEqualSlices(u8, proof.plaintext, delivery.plaintext());
215     try std.testing.expectEqual(@as(carrier.Index, 0), delivery.interface);
216     const receipt = world.lastRecord(.a, .receipt) orelse return error.MissingReceipt;
217     try std.testing.expectEqual(packet.receipt.Status.delivered, receipt.status.?);
218 }
219 
220 test "Reticulum@1.5.0 RNS/Transport.py:737-799,1935-1946 relays data and its proof" {
221     var world: World = undefined;
222     try open(&world, false, null);
223     defer world.deinit();
224     try runAnnounceStep(&world, false);
225     try world.runTo(vector().send_clock);
226     try runRelayStep(&world, false, .{ 0, 2, 2, 1 });
227     try std.testing.expectEqual(vector().send_clock + 19, try deadlineOf(&world, .a));
228     try expectCounts(&world, .{ 1, 3, 3, 2 });
229 }
230 
231 test "Reticulum@1.5.0 RNS/Transport.py:1244-1276 relays the same frames behind access codes" {
232     var world: World = undefined;
233     try open(&world, true, null);
234     defer world.deinit();
235     try runAnnounceStep(&world, true);
236     try world.runTo(vector().send_clock);
237     try runRelayStep(&world, true, .{ 0, 2, 2, 1 });
238     const pair = try codes();
239     const delivery = world.frameOn(.a_to_b, 0) orelse return error.MissingLinkFrame;
240     const masked = delivery.frame.slice();
241     try std.testing.expectEqual(vector().inserted_raw.len + pair.a_b.?.size.byte(), masked.len);
242     try std.testing.expect(ifac.hasFlag(masked[0]));
243 }
244 
245 test "Reticulum@1.5.0 RNS/Transport.py:3409-3444 answers a lost path with one path response" {
246     const proof = vector();
247     var world: World = undefined;
248     try open(&world, false, null);
249     defer world.deinit();
250     world.linkAt(.b_to_a).fault(.{ .partition = .{
251         .from = proof.start_clock,
252         .until = proof.start_clock + 8,
253     } });
254     try announce(&world, null);
255     try world.runTo(proof.request_clock);
256     try expectCounts(&world, .{ 0, 0, 2, 1 });
257     try std.testing.expectEqual(@as(usize, 0), world.recordCount(.a, .announce));
258     try requestPath(&world, tagged(0));
259     try expectFrame(&world, false, .a_to_b, 0, proof.request_raw);
260     try world.runTo(proof.response_clock);
261     try expectCounts(&world, .{ 1, 1, 2, 1 });
262     try expectFrame(&world, false, .b_to_a, 0, proof.response_raw);
263     const learned = world.lastRecord(.a, .announce) orelse return error.MissingAnnounce;
264     try std.testing.expectEqual(@as(u8, 2), learned.hops);
265     try std.testing.expect(learned.path_response);
266     try world.runTo(proof.response_clock + 1);
267     try runRelayStep(&world, false, .{ 1, 1, 2, 1 });
268 }
269 
270 test "Reticulum@1.5.0 RNS/Transport.py:3478-3508 forwards one request and answers once" {
271     const proof = vector();
272     var world: World = undefined;
273     try open(&world, false, 0);
274     defer world.deinit();
275     try requestPath(&world, tagged(0));
276     try expectCounts(&world, .{ 1, 0, 1, 0 });
277     try expectFrame(&world, false, .a_to_b, 0, proof.request_raw);
278     const forwarded = world.frameOn(.b_to_c, 0) orelse return error.MissingLinkFrame;
279     const payload = forwarded.frame.slice()[19..];
280     try std.testing.expectEqual(@as(usize, 48), payload.len);
281     try std.testing.expectEqualSlices(u8, proof.destination_hash, payload[0..16]);
282     try std.testing.expectEqualSlices(u8, proof.transport_identity_hash, payload[16..32]);
283     try std.testing.expectEqualSlices(u8, proof.request_tag, payload[32..48]);
284     const asked = world.lastRecord(.c, .path_request) orelse return error.MissingPathRequest;
285     try std.testing.expectEqualSlices(u8, proof.destination_hash, &asked.key);
286     try announce(&world, 0);
287     try expectCounts(&world, .{ 1, 1, 1, 1 });
288     const relayed = world.lastRecord(.b, .announce) orelse return error.MissingAnnounce;
289     try std.testing.expectEqual(@as(u8, 1), relayed.hops);
290     const learned = world.lastRecord(.a, .announce) orelse return error.MissingAnnounce;
291     try std.testing.expectEqual(@as(u8, 2), learned.hops);
292     try std.testing.expect(learned.path_response);
293     try world.runTo(proof.retry_clock);
294     try expectCounts(&world, .{ 1, 1, 1, 1 });
295 }
296 
297 test "Reticulum@1.5.0 RNS/Transport.py:1755-1797 batches a second tag and answers at once" {
298     const proof = vector();
299     var world: World = undefined;
300     try open(&world, false, null);
301     defer world.deinit();
302     try requestPath(&world, tagged(1));
303     try expectCounts(&world, .{ 1, 0, 0, 0 });
304     try std.testing.expectEqual(@as(usize, 0), codeCount(&world, .b, .path_request_batched));
305     try world.runTo(proof.start_clock + 5);
306     try requestPath(&world, tagged(2));
307     try expectCounts(&world, .{ 2, 0, 0, 0 });
308     try std.testing.expectEqual(@as(usize, 1), codeCount(&world, .b, .path_request_batched));
309     try world.runTo(proof.start_clock + 10);
310     try announce(&world, null);
311     try world.runTo(proof.start_clock + 17);
312     try expectCounts(&world, .{ 2, 3, 2, 1 });
313     try std.testing.expectEqual(proof.start_clock + 10, try frameAt(&world, .b_to_a, 0));
314     try std.testing.expectEqual(proof.start_clock + 11, try frameAt(&world, .b_to_a, 1));
315     try std.testing.expectEqual(proof.start_clock + 17, try frameAt(&world, .b_to_a, 2));
316     const answered = world.frameOn(.b_to_a, 0) orelse return error.MissingLinkFrame;
317     const rebroadcast = world.frameOn(.b_to_a, 1) orelse return error.MissingLinkFrame;
318     const retry = world.frameOn(.b_to_a, 2) orelse return error.MissingLinkFrame;
319     try std.testing.expect(!std.mem.eql(u8, answered.frame.slice(), rebroadcast.frame.slice()));
320     try std.testing.expectEqualSlices(u8, rebroadcast.frame.slice(), retry.frame.slice());
321     try std.testing.expectEqual(@as(usize, 1), world.recordCount(.a, .announce));
322     const learned = world.lastRecord(.a, .announce) orelse return error.MissingAnnounce;
323     try std.testing.expect(learned.path_response);
324 }
325 
326 test "Reticulum@1.5.0 RNS/Transport.py:3520-3524 gates at plus 45 and reopens at plus 46" {
327     const proof = vector();
328     var world: World = undefined;
329     try open(&world, false, null);
330     defer world.deinit();
331     try requestPath(&world, tagged(1));
332     try world.runTo(proof.start_clock + 45);
333     try requestPath(&world, tagged(2));
334     try std.testing.expectEqual(@as(usize, 1), codeCount(&world, .b, .path_request_batched));
335     try world.runTo(proof.start_clock + 46);
336     try requestPath(&world, tagged(3));
337     try std.testing.expectEqual(@as(usize, 1), codeCount(&world, .b, .path_request_batched));
338     try expectCounts(&world, .{ 3, 0, 0, 0 });
339     try world.runTo(proof.start_clock + 62);
340     try announce(&world, null);
341     try world.runTo(proof.start_clock + 69);
342     try expectCounts(&world, .{ 3, 2, 2, 1 });
343     try std.testing.expectEqual(proof.start_clock + 63, try frameAt(&world, .b_to_a, 0));
344     try std.testing.expectEqual(proof.start_clock + 69, try frameAt(&world, .b_to_a, 1));
345     const rebroadcast = world.frameOn(.b_to_a, 0) orelse return error.MissingLinkFrame;
346     const retry = world.frameOn(.b_to_a, 1) orelse return error.MissingLinkFrame;
347     try std.testing.expectEqualSlices(u8, rebroadcast.frame.slice(), retry.frame.slice());
348     try std.testing.expectEqual(@as(usize, 1), world.recordCount(.a, .announce));
349     const learned = world.lastRecord(.a, .announce) orelse return error.MissingAnnounce;
350     try std.testing.expect(!learned.path_response);
351 }
352 
353 test "Reticulum@1.5.0 RNS/Transport.py:2137-2213 keeps the newer announce past a late one" {
354     const proof = vector();
355     var world: World = undefined;
356     try open(&world, false, null);
357     defer world.deinit();
358     world.linkAt(.b_to_a).fault(.{ .delay = .{ .ordinal = 0, .seconds = 3 } });
359     try announce(&world, null);
360     try world.runTo(proof.start_clock + 2);
361     try announce(&world, null);
362     try world.runTo(proof.start_clock + 5);
363     try expectCounts(&world, .{ 0, 2, 2, 2 });
364     try std.testing.expectEqual(proof.start_clock + 3, try frameAt(&world, .b_to_a, 0));
365     try std.testing.expectEqual(proof.start_clock + 4, try frameAt(&world, .b_to_a, 1));
366     try std.testing.expectEqual(@as(u32, 1), world.frameOn(.b_to_a, 0).?.ordinal);
367     try std.testing.expectEqual(@as(u32, 0), world.frameOn(.b_to_a, 1).?.ordinal);
368     try std.testing.expectEqual(@as(usize, 1), world.recordCount(.a, .announce));
369     const learned = world.lastRecord(.a, .announce) orelse return error.MissingAnnounce;
370     try std.testing.expectEqual(proof.start_clock + 3, learned.at);
371 }
372 
373 test "Reticulum@1.5.0 RNS/Packet.py:540-548 fails the receipt when B cannot reach C" {
374     const proof = vector();
375     var world: World = undefined;
376     try open(&world, false, null);
377     defer world.deinit();
378     try runAnnounceStep(&world, false);
379     world.linkAt(.b_to_c).fault(.{ .partition = .{
380         .from = proof.send_clock,
381         .until = proof.send_clock + 1,
382     } });
383     try world.runTo(proof.send_clock);
384     try sendData(&world, 0);
385     try expectCounts(&world, .{ 1, 2, 2, 1 });
386     try world.runTo(proof.send_clock + 19);
387     const receipt = world.lastRecord(.a, .receipt) orelse return error.MissingReceipt;
388     try std.testing.expectEqual(packet.receipt.Status.failed, receipt.status.?);
389     try std.testing.expectEqual(proof.send_clock + 19, receipt.at);
390 }
391 
392 fn runDelayedProof(world: *World, delay: fixture.Seconds) !void {
393     const proof = vector();
394     try open(world, false, null);
395     try runAnnounceStep(world, false);
396     world.linkAt(.c_to_b).fault(.{ .delay = .{ .ordinal = 1, .seconds = delay } });
397     try world.runTo(proof.send_clock);
398     try sendData(world, 0);
399     try world.runTo(proof.send_clock + delay);
400 }
401 
402 test "Reticulum@1.5.0 RNS/Transport.py:834-841 relays a proof at plus 480 and drops plus 481" {
403     var relayed: World = undefined;
404     try runDelayedProof(&relayed, proof_lifetime);
405     defer relayed.deinit();
406     try expectCounts(&relayed, .{ 1, 3, 3, 2 });
407     try expectFrame(&relayed, false, .b_to_a, 2, vector().relayed_proof_raw);
408     try std.testing.expectEqual(@as(usize, 0), codeCount(&relayed, .b, .proof_rejected));
409     var dropped: World = undefined;
410     try runDelayedProof(&dropped, proof_lifetime + 1);
411     defer dropped.deinit();
412     try expectCounts(&dropped, .{ 1, 2, 3, 2 });
413     try std.testing.expectEqual(@as(usize, 1), codeCount(&dropped, .b, .proof_rejected));
414 }
415 
416 fn expectDelivery(
417     world: *const World,
418     link: fixture.LinkId,
419     index: usize,
420     ordinal: u32,
421     at: fixture.Seconds,
422 ) !void {
423     const delivery = world.frameOn(link, index) orelse return error.MissingLinkFrame;
424     try std.testing.expectEqual(ordinal, delivery.ordinal);
425     try std.testing.expectEqual(at, delivery.at);
426 }
427 
428 test "fixture frames due in one second leave in arrival order behind an earlier frame" {
429     const proof = vector();
430     var world: World = undefined;
431     try open(&world, false, null);
432     defer world.deinit();
433     try runAnnounceStep(&world, false);
434     try world.runTo(proof.send_clock);
435     const link = world.linkAt(.a_to_b);
436     for ([_]fixture.Seconds{ 3, 3, 1, 3 }, 0..) |seconds, ordinal| {
437         link.fault(.{ .delay = .{ .ordinal = @intCast(ordinal), .seconds = seconds } });
438     }
439     for (0..4) |salt| try sendData(&world, @intCast(salt));
440     try world.runTo(proof.send_clock + 3);
441     try expectDelivery(&world, .a_to_b, 0, 2, proof.send_clock + 1);
442     try expectDelivery(&world, .a_to_b, 1, 0, proof.send_clock + 3);
443     try expectDelivery(&world, .a_to_b, 2, 1, proof.send_clock + 3);
444     try expectDelivery(&world, .a_to_b, 3, 3, proof.send_clock + 3);
445 }
446 
447 fn runSwap(world: *World, delays: [2]fixture.Seconds) !void {
448     const proof = vector();
449     try open(world, false, null);
450     try runAnnounceStep(world, false);
451     try world.runTo(proof.send_clock);
452     const link = world.linkAt(.a_to_b);
453     link.fault(.{ .delay = .{ .ordinal = 0, .seconds = delays[0] } });
454     link.fault(.{ .delay = .{ .ordinal = 1, .seconds = delays[1] } });
455     link.fault(.{ .swap = .{ .first = 0, .second = 1 } });
456     try sendData(world, 0);
457     try sendData(world, 1);
458     try world.runTo(proof.send_clock + @max(delays[0], delays[1]));
459 }
460 
461 test "fixture swap trades places within one second and leaves each frame its own delay" {
462     const start = vector().send_clock;
463     var apart: World = undefined;
464     try runSwap(&apart, .{ 1, 5 });
465     defer apart.deinit();
466     try expectDelivery(&apart, .a_to_b, 0, 0, start + 1);
467     try expectDelivery(&apart, .a_to_b, 1, 1, start + 5);
468     var together: World = undefined;
469     try runSwap(&together, .{ 2, 2 });
470     defer together.deinit();
471     try expectDelivery(&together, .a_to_b, 0, 1, start + 2);
472     try expectDelivery(&together, .a_to_b, 1, 0, start + 2);
473 }
474 
475 test "fixture holds only the timers its nodes hold across ten proved sends" {
476     const proof = vector();
477     var world: World = undefined;
478     try open(&world, false, null);
479     defer world.deinit();
480     try runAnnounceStep(&world, false);
481     try world.runTo(proof.send_clock);
482     for (0..10) |salt| {
483         try sendData(&world, @intCast(salt));
484         inline for (.{ .a, .b, .c }) |owner| {
485             try std.testing.expect(world.timerCount(owner) <= world.at(owner).timers.count());
486         }
487     }
488     try std.testing.expectEqual(@as(usize, 10), world.recordCount(.a, .receipt));
489     try expectCounts(&world, .{ 10, 12, 12, 11 });
490 }
491 
492 /// The recorded three-node link session, which A opens to C across the transport node B.
493 fn session() LinkVector {
494     const vectors = reticulum.conformance.link.three_node_vectors;
495     std.debug.assert(vectors.len == 1);
496     return vectors[0];
497 }
498 
499 /// The relayed link proof is the third frame B offers A, behind the announce rebroadcast and its
500 /// retry. The recording put one second between A's request and that proof, so the world holds the
501 /// proof back one second and A measures the round trip the recording measured.
502 const relayed_proof_ordinal: u32 = 2;
503 const relayed_proof_delay: fixture.Seconds = 1;
504 
505 /// A's link request waits six seconds plus six per hop over a path of two hops.
506 /// Reticulum@1.5.0 RNS/Link.py:281-283.
507 const initiator_establishment: fixture.Seconds = 18;
508 /// C receives a request with two hops, so its establishment timeout is 6 * 2 + 360 seconds.
509 /// Reticulum@1.5.0 RNS/Link.py:204.
510 const responder_establishment: fixture.Seconds = 372;
511 /// B holds an unproved relay for six seconds per remaining hop and drops it one second later.
512 /// Reticulum@1.5.0 RNS/Transport.py:850,1971.
513 const relay_proof_drop: fixture.Seconds = 7;
514 
515 fn openSession(world: *World, keyed: bool) !void {
516     const link = session();
517     try world.init(.{
518         .start = link.start_clock,
519         .transport_hash = link.transport_identity_hash[0..16].*,
520         .codes = if (keyed) try codes() else .{},
521         .entropy = .{
522             .a = link.rtt_entropy[0..32].*,
523             .c = link.responder_entropy[0..32].*,
524         },
525     });
526     const owner = world.at(.c);
527     owner.identities[0] = reticulum.identity.Private.fromBytes(
528         link.destination_private_key[0..64].*,
529     );
530     try owner.destinations.register(.{
531         .hash = link.destination_hash[0..16].*,
532         .name_hash = link.destination_name_hash[0..10].*,
533         .kind = .single,
534         .proof_strategy = .all,
535         .identity_index = 0,
536     });
537 }
538 
539 fn announceSession(world: *World) !void {
540     const link = session();
541     try world.step(.c, .{ .application_announce = .{
542         .destination = link.destination_hash[0..16].*,
543         .app_data = link.destination_app_data,
544         .random = link.announce_random_hash[0..5].*,
545         .fresh_rotating_key = null,
546         .now = world.clock,
547         .path_response = null,
548     } });
549 }
550 
551 fn linkSend(world: *World, id: fixture.NodeId, plaintext: []const u8, iv: [16]u8) !void {
552     try world.step(id, .{ .application_link_send = .{
553         .link_id = session().link_id[0..16].*,
554         .plaintext = plaintext,
555         .iv = iv,
556         .now = world.clock,
557     } });
558 }
559 
560 fn linkClose(world: *World, id: fixture.NodeId, iv: [16]u8) !void {
561     try world.step(id, .{ .application_link_close = .{
562         .link_id = session().link_id[0..16].*,
563         .iv = iv,
564         .now = world.clock,
565     } });
566 }
567 
568 /// Reports whether B's packet filter holds the hash of this frame. The hops byte sits outside the
569 /// hashed part, so a relayed frame and the frame it came from share one hash
570 /// (Reticulum@1.5.0 RNS/Packet.py:353-358).
571 fn relayHolds(world: *World, raw: []const u8) !bool {
572     const hash = try wire.hash.full(raw);
573     return world.at(.b).duplicate_hashes.contains(hash);
574 }
575 
576 fn closeReasonOf(world: *const World, id: fixture.NodeId) !node.LinkCloseReason {
577     const closed = world.lastRecord(id, .link_closed) orelse return error.MissingLinkClose;
578     return closed.reason orelse error.MissingCloseReason;
579 }
580 
581 fn deliveredCount(world: *const World, id: fixture.NodeId) usize {
582     var count: usize = 0;
583     for (world.records()) |entry| {
584         if (entry.owner != id or entry.kind != .receipt) continue;
585         if (entry.status != packet.receipt.Status.delivered) continue;
586         count += 1;
587     }
588     return count;
589 }
590 
591 /// Reticulum@1.5.0 RNS/Transport.py:737-799 carries C's announce to A through B, so A recalls the
592 /// identity that a link request needs.
593 fn runSessionAnnounce(world: *World, keyed: bool) !void {
594     const link = session();
595     try announceSession(world);
596     try world.runTo(link.retry_clock);
597     try expectCounts(world, .{ 0, 2, 2, 1 });
598     try expectFrame(world, keyed, .c_to_b, 0, link.announce_raw);
599     try expectFrame(world, keyed, .b_to_a, 0, link.rebroadcast_raw);
600     try expectFrame(world, keyed, .b_to_a, 1, link.retry_raw);
601     try expectFrame(world, keyed, .b_to_c, 0, link.rebroadcast_raw);
602     try expectFrame(world, keyed, .b_to_c, 1, link.retry_raw);
603     const learned = world.lastRecord(.a, .announce) orelse return error.MissingAnnounce;
604     try std.testing.expectEqual(@as(u8, 2), learned.hops);
605 }
606 
607 fn openLink(world: *World) !void {
608     const link = session();
609     try world.step(.a, .{ .application_link_open = .{
610         .destination = link.destination_hash[0..16].*,
611         .encryption_private = link.initiator_encryption_private_key[0..32].*,
612         .signing_private = link.initiator_signing_private_key[0..32].*,
613         .now = world.clock,
614         .proof_strategy = .all,
615     } });
616 }
617 
618 /// Section 6 step 1. A opens a link to C, B strips and relays the request, C proves it, B relays
619 /// the proof, and A answers with its round trip time.
620 fn runSessionEstablish(world: *World, keyed: bool) !void {
621     const link = session();
622     try runSessionAnnounce(world, keyed);
623     world.linkAt(.b_to_a).fault(.{ .delay = .{
624         .ordinal = relayed_proof_ordinal,
625         .seconds = relayed_proof_delay,
626     } });
627     try world.runTo(link.request_clock);
628     try openLink(world);
629     try expectCounts(world, .{ 1, 2, 3, 2 });
630     try expectFrame(world, keyed, .a_to_b, 0, link.request_raw);
631     try expectFrame(world, keyed, .b_to_c, 2, link.stripped_raw);
632     try expectFrame(world, keyed, .c_to_b, 1, link.proof_raw);
633     try world.runTo(link.proof_clock);
634     try expectCounts(world, .{ 2, 3, 4, 2 });
635     try expectFrame(world, keyed, .b_to_a, 2, link.relayed_proof_raw);
636     try expectFrame(world, keyed, .a_to_b, 1, link.rtt_raw);
637     try expectFrame(world, keyed, .b_to_c, 3, link.relayed_rtt_raw);
638     const opened = world.lastRecord(.a, .link_established) orelse return error.MissingLink;
639     try std.testing.expectEqual(node.LinkRole.initiator, opened.role.?);
640     try std.testing.expectEqualSlices(u8, link.link_id, &opened.key);
641     const answered = world.lastRecord(.c, .link_established) orelse return error.MissingLink;
642     try std.testing.expectEqual(node.LinkRole.responder, answered.role.?);
643     try std.testing.expectEqualSlices(u8, link.link_id, &answered.key);
644     try std.testing.expectEqual(link.proof_clock, answered.at);
645 }
646 
647 /// Section 6 step 2. A sends data and C answers, and each packet's proof reaches its sender.
648 fn runSessionTraffic(world: *World, keyed: bool) !void {
649     const link = session();
650     try world.runTo(link.data_clock);
651     try linkSend(world, .a, link.data_plaintext, link.data_iv[0..16].*);
652     try expectCounts(world, .{ 3, 4, 5, 3 });
653     try expectFrame(world, keyed, .a_to_b, 2, link.data_raw);
654     try expectFrame(world, keyed, .b_to_c, 4, link.relayed_data_raw);
655     try expectFrame(world, keyed, .c_to_b, 2, link.data_proof_raw);
656     try expectFrame(world, keyed, .b_to_a, 3, link.relayed_data_proof_raw);
657     const heard = world.lastRecord(.c, .link_delivery) orelse return error.MissingLinkDelivery;
658     try std.testing.expectEqualSlices(u8, link.data_plaintext, heard.plaintext());
659     try world.runTo(link.answer_clock);
660     try linkSend(world, .c, link.answer_plaintext, link.answer_iv[0..16].*);
661     try expectCounts(world, .{ 4, 5, 6, 4 });
662     try expectFrame(world, keyed, .c_to_b, 3, link.answer_raw);
663     try expectFrame(world, keyed, .b_to_a, 4, link.relayed_answer_raw);
664     try expectFrame(world, keyed, .a_to_b, 3, link.answer_proof_raw);
665     try expectFrame(world, keyed, .b_to_c, 5, link.relayed_answer_proof_raw);
666     const back = world.lastRecord(.a, .link_delivery) orelse return error.MissingLinkDelivery;
667     try std.testing.expectEqualSlices(u8, link.answer_plaintext, back.plaintext());
668     try std.testing.expectEqual(@as(usize, 1), deliveredCount(world, .a));
669     try std.testing.expectEqual(@as(usize, 1), deliveredCount(world, .c));
670 }
671 
672 /// Section 6 step 3. A sends a keepalive request at its deadline and C answers it.
673 fn runSessionKeepalive(world: *World, keyed: bool) !void {
674     const link = session();
675     try world.runTo(link.keepalive_clock - 1);
676     try expectCounts(world, .{ 4, 5, 6, 4 });
677     try world.runTo(link.keepalive_clock);
678     try expectCounts(world, .{ 5, 6, 7, 5 });
679     try expectFrame(world, keyed, .a_to_b, 4, link.keepalive_raw);
680     try expectFrame(world, keyed, .b_to_c, 6, link.relayed_keepalive_raw);
681     try expectFrame(world, keyed, .c_to_b, 4, link.keepalive_answer_raw);
682     try expectFrame(world, keyed, .b_to_a, 5, link.relayed_keepalive_answer_raw);
683     try std.testing.expectEqual(@as(usize, 0), world.recordCount(.a, .link_closed));
684     try std.testing.expectEqual(@as(usize, 0), world.recordCount(.c, .link_closed));
685 }
686 
687 /// Section 6 step 4. A tears the link down and C closes with reason initiator closed.
688 fn runSessionClose(world: *World, keyed: bool) !void {
689     const link = session();
690     try world.runTo(link.close_clock);
691     try linkClose(world, .a, link.close_iv[0..16].*);
692     try expectCounts(world, .{ 6, 6, 8, 5 });
693     try expectFrame(world, keyed, .a_to_b, 5, link.close_raw);
694     try expectFrame(world, keyed, .b_to_c, 7, link.relayed_close_raw);
695     try std.testing.expectEqual(
696         node.LinkCloseReason.initiator_closed,
697         try closeReasonOf(world, .a),
698     );
699     try std.testing.expectEqual(
700         node.LinkCloseReason.initiator_closed,
701         try closeReasonOf(world, .c),
702     );
703 }
704 
705 test "Reticulum@1.5.0 RNS/Link.py:304-322 opens a link to C across the transport node B" {
706     var world: World = undefined;
707     try openSession(&world, false);
708     defer world.deinit();
709     try runSessionEstablish(&world, false);
710 }
711 
712 test "Reticulum@1.5.0 RNS/Link.py:378-389,948-962 carries link data and its proofs both ways" {
713     var world: World = undefined;
714     try openSession(&world, false);
715     defer world.deinit();
716     try runSessionEstablish(&world, false);
717     try runSessionTraffic(&world, false);
718 }
719 
720 test "Reticulum@1.5.0 RNS/Transport.py:1863,2065 stores link hashes but never an LRPROOF one" {
721     const link = session();
722     var world: World = undefined;
723     try openSession(&world, false);
724     defer world.deinit();
725     try runSessionEstablish(&world, false);
726     try runSessionTraffic(&world, false);
727     try std.testing.expect(!try relayHolds(&world, link.proof_raw));
728     try std.testing.expect(try relayHolds(&world, link.request_raw));
729     try std.testing.expect(try relayHolds(&world, link.data_raw));
730     try std.testing.expect(try relayHolds(&world, link.answer_raw));
731     try std.testing.expect(try relayHolds(&world, link.data_proof_raw));
732     try std.testing.expect(try relayHolds(&world, link.answer_proof_raw));
733 }
734 
735 test "Reticulum@1.5.0 RNS/Link.py:744-760,1130-1135 keeps a relayed link alive with keepalives" {
736     var world: World = undefined;
737     try openSession(&world, false);
738     defer world.deinit();
739     try runSessionEstablish(&world, false);
740     try runSessionTraffic(&world, false);
741     try runSessionKeepalive(&world, false);
742 }
743 
744 test "Reticulum@1.5.0 RNS/Link.py:657-683 tears a relayed link down from its initiator" {
745     var world: World = undefined;
746     try openSession(&world, false);
747     defer world.deinit();
748     try runSessionEstablish(&world, false);
749     try runSessionTraffic(&world, false);
750     try runSessionKeepalive(&world, false);
751     try runSessionClose(&world, false);
752 }
753 
754 test "Reticulum@1.5.0 RNS/Link.py:657-683 tears a relayed link down from its responder" {
755     const link = session();
756     var world: World = undefined;
757     try openSession(&world, false);
758     defer world.deinit();
759     try runSessionEstablish(&world, false);
760     try runSessionTraffic(&world, false);
761     try world.runTo(link.answer_clock + 1);
762     try linkClose(&world, .c, link.close_iv[0..16].*);
763     try expectCounts(&world, .{ 4, 6, 6, 5 });
764     try std.testing.expectEqual(
765         node.LinkCloseReason.destination_closed,
766         try closeReasonOf(&world, .c),
767     );
768     try std.testing.expectEqual(
769         node.LinkCloseReason.destination_closed,
770         try closeReasonOf(&world, .a),
771     );
772 }
773 
774 test "Reticulum@1.5.0 RNS/Transport.py:1244-1276 relays every link frame behind access codes" {
775     const link = session();
776     var world: World = undefined;
777     try openSession(&world, true);
778     defer world.deinit();
779     try runSessionEstablish(&world, true);
780     try runSessionTraffic(&world, true);
781     try runSessionKeepalive(&world, true);
782     try runSessionClose(&world, true);
783     const pair = try codes();
784     const request = world.frameOn(.a_to_b, 0) orelse return error.MissingLinkFrame;
785     const masked = request.frame.slice();
786     try std.testing.expectEqual(link.request_raw.len + pair.a_b.?.size.byte(), masked.len);
787     try std.testing.expect(ifac.hasFlag(masked[0]));
788 }
789 
790 test "Reticulum@1.5.0 RNS/Transport.py:858-865 loses the link proof and unwinds all three nodes" {
791     const link = session();
792     var world: World = undefined;
793     try openSession(&world, false);
794     defer world.deinit();
795     try runSessionAnnounce(&world, false);
796     world.linkAt(.c_to_b).fault(.{ .drop = 1 });
797     try world.runTo(link.request_clock);
798     try openLink(&world);
799     try expectCounts(&world, .{ 1, 2, 3, 1 });
800     try std.testing.expectEqual(@as(usize, 1), world.at(.b).transport.link_entries.count());
801     try std.testing.expectEqual(@as(usize, 1), world.at(.c).transport.links.count());
802     try std.testing.expectEqual(@as(usize, 1), world.timerCount(.c));
803     const responder = world.at(.c).transport.links.find(link.link_id[0..16].*) orelse
804         return error.MissingLink;
805     const responder_deadline = responder.request_time + responder.establishment_timeout;
806     try std.testing.expectEqual(link.request_clock + responder_establishment, responder_deadline);
807     try world.runTo(link.request_clock + relay_proof_drop - 1);
808     try std.testing.expectEqual(@as(usize, 1), world.at(.b).transport.link_entries.count());
809     try world.runTo(link.request_clock + relay_proof_drop);
810     try std.testing.expectEqual(@as(usize, 0), world.at(.b).transport.link_entries.count());
811     const destination = link.destination_hash[0..16].*;
812     const path = world.at(.b).transport.paths.find(destination, world.clock) orelse
813         return error.MissingPath;
814     try std.testing.expectEqual(node.transport.path.State.unresponsive, path.state);
815     try world.runTo(link.request_clock + initiator_establishment - 1);
816     try std.testing.expectEqual(@as(usize, 1), world.at(.a).transport.links.count());
817     try world.runTo(link.request_clock + initiator_establishment);
818     try std.testing.expectEqual(node.LinkCloseReason.timeout, try closeReasonOf(&world, .a));
819     try std.testing.expect(world.at(.a).transport.paths.find(destination, world.clock) == null);
820     const asked = world.frameOn(.a_to_b, 1) orelse return error.MissingLinkFrame;
821     try std.testing.expectEqual(link.request_clock + initiator_establishment, asked.at);
822     const payload = asked.frame.slice()[19..];
823     try std.testing.expectEqualSlices(u8, link.destination_hash, payload[0..16]);
824     try std.testing.expectEqual(@as(usize, 1), world.timerCount(.c));
825     try world.runTo(responder_deadline - 1);
826     try std.testing.expectEqual(@as(usize, 1), world.at(.c).transport.links.count());
827     try world.runTo(responder_deadline);
828     try std.testing.expectEqual(@as(usize, 0), world.at(.c).transport.links.count());
829     try std.testing.expectEqual(node.LinkCloseReason.timeout, try closeReasonOf(&world, .c));
830 }
831 
832 test "Reticulum@1.5.0 RNS/Link.py:744-766 times a partitioned link out and still closes C" {
833     const link = session();
834     var world: World = undefined;
835     try openSession(&world, false);
836     defer world.deinit();
837     try runSessionEstablish(&world, false);
838     const quiet_since = link.proof_clock;
839     const stale_at = quiet_since + 412;
840     const close_at = stale_at + 5;
841     world.linkAt(.b_to_a).fault(.{ .partition = .{ .from = quiet_since + 1, .until = close_at } });
842     try world.runTo(stale_at - 1);
843     try std.testing.expectEqual(@as(usize, 0), world.recordCount(.a, .link_closed));
844     try world.runTo(close_at - 1);
845     try std.testing.expectEqual(@as(usize, 0), world.recordCount(.a, .link_closed));
846     try world.runTo(close_at);
847     try std.testing.expectEqual(node.LinkCloseReason.timeout, try closeReasonOf(&world, .a));
848     try std.testing.expectEqual(close_at, world.lastRecord(.a, .link_closed).?.at);
849     try std.testing.expectEqual(
850         node.LinkCloseReason.initiator_closed,
851         try closeReasonOf(&world, .c),
852     );
853     try std.testing.expectEqual(close_at, world.lastRecord(.c, .link_closed).?.at);
854 }
855 
856 test "issue:tiny-6nihz8lx delivers two swapped link data frames and proves each one" {
857     const link = session();
858     var world: World = undefined;
859     try openSession(&world, false);
860     defer world.deinit();
861     try runSessionEstablish(&world, false);
862     const wire_a_b = world.linkAt(.a_to_b);
863     wire_a_b.fault(.{ .delay = .{ .ordinal = 2, .seconds = 2 } });
864     wire_a_b.fault(.{ .delay = .{ .ordinal = 3, .seconds = 2 } });
865     wire_a_b.fault(.{ .swap = .{ .first = 2, .second = 3 } });
866     try world.runTo(link.data_clock);
867     var second_iv = link.data_iv[0..16].*;
868     second_iv[15] ^= 1;
869     try linkSend(&world, .a, link.data_plaintext, link.data_iv[0..16].*);
870     try linkSend(&world, .a, link.data_plaintext, second_iv);
871     try expectCounts(&world, .{ 2, 3, 4, 2 });
872     try world.runTo(link.data_clock + 2);
873     try expectCounts(&world, .{ 4, 5, 6, 4 });
874     try expectDelivery(&world, .a_to_b, 2, 3, link.data_clock + 2);
875     try expectDelivery(&world, .a_to_b, 3, 2, link.data_clock + 2);
876     try std.testing.expectEqual(@as(usize, 2), world.recordCount(.c, .link_delivery));
877     try std.testing.expectEqual(@as(usize, 2), deliveredCount(&world, .a));
878     try std.testing.expectEqual(@as(usize, 0), world.recordCount(.a, .link_closed));
879     try std.testing.expectEqual(@as(usize, 0), world.recordCount(.c, .link_closed));
880     const held = world.at(.a).transport.links.find(link.link_id[0..16].*) orelse
881         return error.MissingLink;
882     try std.testing.expectEqual(node.transport.links.Status.active, held.status);
883 }