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 }