lib/reticulum/src/node/announce.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const reticulum = @import("../root.zig");
3
4 const destination = reticulum.destination;
5 const identity = reticulum.identity;
6 const node = reticulum.node;
7 const wire = reticulum.wire;
8
9 const timestamp_max: node.Seconds = std.math.maxInt(u40);
10
11 fn needsRotation(ring: *const identity.Ring, now: node.Seconds) bool {
12 if (ring.count == 0) return true;
13 const latest = ring.latest_time orelse return true;
14 const deadline = std.math.add(node.Seconds, latest, identity.ratchet.reference_interval) catch
15 std.math.maxInt(node.Seconds);
16 return now > deadline;
17 }
18
19 fn randomHash(random: [5]u8, now: node.Seconds) [10]u8 {
20 std.debug.assert(now <= timestamp_max);
21 var result: [10]u8 = undefined;
22 @memcpy(result[0..5], &random);
23 std.mem.writeInt(u40, result[5..10], @intCast(now), .big);
24 return result;
25 }
26
27 fn rotatingKey(
28 node_owner: *node.Node,
29 identity_index: usize,
30 announce: node.ApplicationAnnounce,
31 ) node.StepError!?[32]u8 {
32 const binding = node_owner.ratchet_bindings[identity_index];
33 const ring = binding.ring orelse return null;
34 if (needsRotation(ring, announce.now)) {
35 const fresh_bytes = announce.fresh_rotating_key orelse return error.MissingEntropy;
36 var fresh = identity.Ratchet.fromBytes(fresh_bytes);
37 defer fresh.zero();
38 _ = ring.rotate(announce.now, identity.ratchet.reference_interval, fresh);
39 }
40 const latest = ring.latest() orelse return error.MissingEntropy;
41 return latest.publicBytes();
42 }
43
44 fn buildFrame(
45 node_owner: *node.Node,
46 value: node.ApplicationAnnounce,
47 private: *const identity.Private,
48 name_hash: [10]u8,
49 rotating_public: ?[32]u8,
50 ) node.StepError![]const u8 {
51 const payload_start: usize = wire.header_one_bytes;
52 const payload = destination.announce.build(.{
53 .destination_hash = value.destination,
54 .name_hash = name_hash,
55 .random_hash = randomHash(value.random, value.now),
56 .rotating_public_key = rotating_public,
57 .app_data = value.app_data,
58 }, private, node_owner.scratch[payload_start..]) catch |err| switch (err) {
59 error.AppDataTooLong => return error.AppDataTooLong,
60 error.OutputTooSmall => return error.PacketTooLarge,
61 };
62 return wire.encode(.{
63 .ifac = 0,
64 .header = .one,
65 .context_flag = @intFromBool(rotating_public != null),
66 .transport = .broadcast,
67 .destination_type = .single,
68 .packet_type = .announce,
69 .hops = 0,
70 .transport_id = null,
71 .destination = value.destination,
72 .context = if (value.path_response == null) .none else .path_response,
73 .payload = payload,
74 }, node_owner.scratch) catch return error.PacketTooLarge;
75 }
76
77 fn fanoutFor(value: node.ApplicationAnnounce) node.outbound.Fanout {
78 const index = value.path_response orelse return .all;
79 return .{ .one = index };
80 }
81
82 /// Announces one of the node's own destinations, so every node that hears it learns the
83 /// destination's public keys and how far away it sits, as Reticulum@1.5.0
84 /// RNS/Destination.py:228-315 builds it. The same call answers a path request. With a carrier named
85 /// to answer on, the announce goes out on that carrier alone, following Reticulum@1.5.0
86 /// RNS/Transport.py:3379-3382, and otherwise it goes out on every outgoing carrier. The announce
87 /// stamps the caller's instant into five of its ten random bytes, so an instant past 2^40 - 1
88 /// seconds returns `error.TimestampOutOfRange`. A destination hash with no registration behind it
89 /// returns `error.UnknownDestination`, and a registration of another kind, or one that names no
90 /// local identity, returns `error.InvalidDestination`. A fanout that reaches no outgoing carrier
91 /// returns `error.NoOutgoingCarrier`. With a rotating-key ring bound to the destination's identity,
92 /// the announce carries a rotating public key, and the node rotates a fresh one in when the ring is
93 /// empty or its newest key is more than thirty minutes old. A rotation with no fresh key from the
94 /// caller returns `error.MissingEntropy`. The node remembers the rotating key it published and
95 /// stores the announce's packet hash, so the same announce coming back is a duplicate.
96 pub fn run(node_owner: *node.Node, value: node.ApplicationAnnounce) node.StepError!void {
97 if (value.now > timestamp_max) return error.TimestampOutOfRange;
98 const entry = node_owner.destinations.find(value.destination) orelse
99 return error.UnknownDestination;
100 if (entry.kind != .single) return error.InvalidDestination;
101 const identity_index = entry.identity_index orelse return error.InvalidDestination;
102 const private = node_owner.identityAt(identity_index) orelse return error.InvalidDestination;
103 const fanout = fanoutFor(value);
104 const carriers = node.outbound.frameCount(node_owner, fanout);
105 if (carriers == 0) return error.NoOutgoingCarrier;
106 try node.outbound.reserve(node_owner, carriers, carriers);
107 const rotating_public = try rotatingKey(node_owner, identity_index, value);
108 const frame = try buildFrame(node_owner, value, private, entry.name_hash, rotating_public);
109 if (rotating_public) |public_key| node_owner.known_ratchets.remember(.{
110 .destination_hash = value.destination,
111 .public_key = public_key,
112 .received = value.now,
113 });
114 const packet_hash = wire.hash.full(frame) catch unreachable;
115 _ = node_owner.duplicate_hashes.insert(packet_hash);
116 try node.outbound.transmit(node_owner, fanout, frame);
117 }