Skip to documentation
SLOP

tiny.reticulum.node.link

Reference tiny.reticulum node link

Defined in node.

The two ends of an encrypted session between two programs on a mesh network run inside one node's state machine, from the request that opens the session through its data, the messages that confirm each end is still there, and its close.

API (6)

Actions

Public operations.

No direct callersNo direct callsnodelink
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsnode.transitionrunprivate sourcelib.reticulum.src.node.linkcloseReasonprivate sourcelib.reticulum.src.node.linkreleaseprivate sourcelib.reticulum.src.node.linkteardownnode.outboundframeCountnode.outboundreservenode.linkclose
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsnode.transitionrunprivate sourcelib.reticulum.src.node.linkinitiatorEntryprivate sourcelib.reticulum.src.node.linkrequestFrameprivate sourcelib.reticulum.src.node.linkschedulenode.outboundframeCountnode.outboundreserve+4 morenode.linkopen
Static calls · unresolved targets: 0 · external targets: 11.
Called byCallsnode.transitionrunprivate sourcelib.reticulum.src.node.linkproveOnLinknode.outboundframeCountnode.outboundreservenode.linkprove
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsprivate sourcelib.quic.src.properties.simdrainnode.inboundrunnode.inbounddiagnosticprivate sourcelib.reticulum.src.node.linkrouteLinkPacketprivate sourcelib.reticulum.src.node.linkrouteProofprivate sourcelib.reticulum.src.node.linkrouteRequestnode.linkreceive
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsprivate sourcelib.quic.src.properties.simsendSequencenode.transitionrunprivate sourcelib.reticulum.src.node.linkencryptedFrameprivate sourcelib.reticulum.src.node.linkmarkOutboundprivate sourcelib.reticulum.src.node.linkreceiptTimeoutprivate sourcelib.reticulum.src.node.linksendOnLinknode.outboundframeCount+3 morenode.linksend
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsnode.inboundtimerExpiredprivate sourcelib.reticulum.src.node.linkexpireUnactivatedprivate sourcelib.reticulum.src.node.linkscheduleprivate sourcelib.reticulum.src.node.linkwatchActiveprivate sourcelib.reticulum.src.node.linkwatchStalenode.outboundreservenode.linktimerExpired
Static calls · unresolved targets: 0 · external targets: 2.

Source: lib/reticulum/src/node/link.zig

zig
//! The two ends of an encrypted session between two programs on a mesh network run inside one//! node's state machine, from the request that opens the session through its data, the messages//! that confirm each end is still there, and its close. Two programs that found each other by//! broadcast want a channel whose keys belong to this conversation alone, so a key that leaks later//! reveals nothing said before. Each end has to learn that the other is still there, and has to let//! go of a session whose other end has gone quiet. A sender over the session wants to know that its//! packet arrived. Fresh keys need randomness, and a state machine that draws its own gives a//! different answer on every run. Both ends have to agree on the session's name before either has//! heard from the other, and the request packet is all they share.//!//! *Reticulum 1.5.0*, the reference implementation this package is a port of, runs these sessions//! in RNS/Link.py, and this module follows it at Reticulum@1.5.0//! RNS/Link.py:186-227,274-322,348-375,378-389,391-438,516-538,657-683,722-766,948-968,1130-1135.//! The package generates reference bytes and clocks for these sessions from that release (a//! *conformance corpus*), and the node tests replay them, so each claim about the reference is//! checkable from this tree. The node's clock is a whole second, and Reticulum 1.5.0 computes//! session deadlines in fractional seconds: this difference is the invariant behind the ninth,//! tenth and eleventh departures below.//!//! Each transition derives a key, an initialization vector, or a tag for an encrypted session//! between two endpoints, a *link*, with HKDF-SHA256 from the 32 random bytes its input carried.//! That derivation takes the 16-byte truncated hash of the opening request that names the link at//! every node it passes, the *link id*, as the salt and one of four names as the label://! `reticulum-link-responder-key`, `reticulum-link-rtt-iv`, `reticulum-link-close-iv`,//! `reticulum-link-rediscovery-tag`. Those four names belong to this port. This port departs from//! the reference in eleven places.//!//! First, a link stays pending through a confirmation whose signature fails to verify (a *proof*, a//! packet carrying a signature over an earlier packet's hash, sent back so the sender learns the//! packet arrived) and through a proof naming a mode this port refuses. A proof that arrives later//! and does verify brings the link up at the end that sent the request, the *initiator*.//! Reticulum@1.5.0 RNS/Link.py:350,409 advances the link to HANDSHAKE ahead of the signature check,//! Reticulum@1.5.0 RNS/Link.py:446 lets a failed check pass in silence, and Reticulum@1.5.0//! RNS/Link.py:393 admits a proof for a PENDING link alone, which strands the link. On a mode it//! refuses, Reticulum@1.5.0 RNS/Link.py:398,448-449 closes the link.//!//! Second, a round trip packet counts only on the end the request was sent to, the *responder*, for//! a link in handshake. That packet carries the seconds the link measures between its request and//! the answer, the *round trip time*, which sets its deadlines for checking the other end and for//! letting go of it. Reticulum@1.5.0 RNS/Link.py:938,1022-1024 takes one on every responder link//! that remains open.//!//! Third, a round trip plaintext is nine bytes of msgpack float64, and any other plaintext tears//! the link down. Reticulum@1.5.0 RNS/Link.py:521-522,536-538 takes whatever msgpack decodes as//! long as the value stands in comparison to a float, and ends the link when the decode or that//! comparison raises.//!//! Fourth, an X25519 exchange that lands on an all-zero shared secret fails here: a request that//! reaches it is invalid, and a proof that reaches it is refused even with a signature that//! verifies, which holds the initiator link at pending. Reticulum@1.5.0//! RNS/Cryptography/X25519.py:139-145 returns that secret as it stands and Reticulum@1.5.0//! RNS/Link.py:348-361 derives the link key from it, so the responder there answers such a request//! with a proof, as Reticulum@1.5.0 RNS/Link.py:209-211 does, and the initiator there brings the//! link up on such a proof once the signature validates, as Reticulum@1.5.0//! RNS/Link.py:405-409,415-424 does.//!//! Fifth, a request that derives the id of a link the node already holds is a duplicate and draws//! the named report the node hands its caller when it drops a packet, giving the reason (a//! *diagnostic*). Reticulum@1.5.0 RNS/Transport.py:2864-2869 records a second link under that id.//!//! Sixth, the network interfaces this port drives declare no hardware MTU, and a responder here//! names its link by the id the initiator computed. On an interface of that kind Reticulum@1.5.0//! RNS/Transport.py:2471-2474 takes the three bytes a link request or proof may carry past its//! fixed body, the *signalling bytes*, off the request data, and Reticulum@1.5.0//! RNS/Link.py:336-342 with Reticulum@1.5.0 RNS/Packet.py:353-358 folds those same bytes into the//! hash that names the link, because the hash covers the raw packet.//!//! Seventh, an application send returns `error.LinkNotEstablished` until the link has activated.//! Reticulum@1.5.0 RNS/Packet.py:290-300 withholds a packet on a closed link alone and puts one on//! the wire for a link still pending or in handshake, before the link key exists.//!//! Eighth, four refusals reach the caller here as named diagnostics: a close packet whose plaintext//! differs from the link id draws `link_close_invalid`, a request that arrives at an initiator//! asking whether the link is still alive draws `link_state_mismatch`, data the link key fails to//! open draws `decryption_failed`, and a proof of link data matching no record the node still holds//! of a packet it sent draws `proof_rejected`. Reticulum@1.5.0 RNS/Link.py:674-683,938-946,948-953//! and Reticulum@1.5.0 RNS/Transport.py:2693-2697 pass over all four in silence.//!//! Ninth, an instant stored on a link moves to the later of the value it already holds and the//! clock the transition carries, which keeps every deadline in place when a transition arrives with//! an earlier clock. Reticulum@1.5.0 RNS/Link.py:652-655,938-946 writes the wall clock into those//! fields as it finds it.//!//! Tenth, a link *timer* (a deadline the node holds to the whole second) arriving ahead of the//! deadline it carries is armed again for that deadline or for one second past the current instant,//! whichever falls later. Reticulum@1.5.0 RNS/Link.py:757-759,775 sleeps out the remaining time//! under a ceiling of five seconds on each sleep.//!//! Eleventh, a link turns *stale* when it hears nothing for twice the quiet interval its round trip//! time sets. Five seconds pass between turning stale and its close. Reticulum@1.5.0//! RNS/Link.py:84,88,99-106,754,775 arrives at the same five seconds, and its `STALE_TIME`//! documentation describes a delay under no ceiling, `rtt` times `KEEPALIVE_TIMEOUT_FACTOR` plus//! `STALE_GRACE`, that the ceiling on each watchdog sleep removes.//!//! - *node*: the state machine that holds one Reticulum node's tables, one packet of scratch space,//!   and its effect list.//! - *step*: one transition, taking one event and returning the effects it produced.//! - *event*: the one input a step takes, either a frame that arrived on a carrier, a timer that//!   came due, an application request, or the completion of a caller's storage write.//! - *effect*: a record the node appends in place of doing input or output itself, such as a frame//!   to send on a carrier or a timer to arm.//! - *step entropy*: the 32 bytes the caller draws fresh for each carrier frame and each timer//!   event, which the step expands with HKDF-SHA256 whenever it needs a key or an initialization//!   vector.//! - *carrier*: one network interface a node sends and receives frames over, named by a byte index.//! - *frame*: the bytes handed to one carrier, one packet plus at most a 64-byte signature.//! - *packet*: one Reticulum datagram, at most 500 bytes, carrying a flags byte, a hop count, an//!   optional transport id, a destination hash, a context byte, and a payload.//! - *link request*: the packet an initiator sends to open a link, carrying its ephemeral public//!   keys and three signalling bytes.//! - *link data*: an application payload sent over a link, encrypted with the link key and capped//!   at 431 bytes of plaintext.//! - *LINKCLOSE*: the packet an end sends to close a link, whose plaintext is the link id.//! - *keepalive*: the packet an initiator sends to hold an otherwise quiet link open, and the//!   answer a responder returns.//! - *keepalive period*: how long an active link may stay quiet before its initiator sends a//!   keepalive, between 5 and 360 seconds and derived from the link's round trip time.//! - *receipt*: the record of one packet the node sent, holding its hash, its destination, the//!   instant it went out, and how long it may wait for a proof.const std = @import("std");const reticulum = @import("../root.zig");const crypto = reticulum.crypto;const destination = reticulum.destination;const identity = reticulum.identity;const node = reticulum.node;const packet = reticulum.packet;const wire = reticulum.wire;const links = node.transport.links;const relay = node.transport.link.relay;const Ed25519 = std.crypto.sign.Ed25519;const X25519 = std.crypto.dh.X25519;const per_hop_timeout: node.Seconds = 6;const keepalive_max: node.Seconds = 360;const keepalive_max_seconds: f64 = 360;const keepalive_min_seconds: f64 = 5;const keepalive_max_rtt: f64 = 1.75;const stale_factor: f64 = 2;const stale_grace: node.Seconds = 5;const traffic_timeout_factor: f64 = 6;const traffic_timeout_min: f64 = 5.0 / 1000.0;const seconds_limit: f64 = 18446744073709551616.0;const responder_key_label = "reticulum-link-responder-key";const rtt_iv_label = "reticulum-link-rtt-iv";const close_iv_label = "reticulum-link-close-iv";const rediscovery_tag_label = "reticulum-link-rediscovery-tag";fn expand(entropy: *const [32]u8, link_id: *const [16]u8, label: []const u8, out: []u8) void {    std.debug.assert(out.len > 0);    std.debug.assert(out.len <= 64);    const length: u17 = @intCast(out.len);    const derived = crypto.hkdf.derive(length, entropy, link_id[0..], label, out) catch        unreachable;    std.debug.assert(derived.len == out.len);}const Handshake = struct {    public: [32]u8,    derived: [64]u8,};fn handshake(private: *const [32]u8, peer_public: [32]u8, link_id: *const [16]u8) ?Handshake {    var shared = X25519.scalarmult(private.*, peer_public) catch return null;    defer std.crypto.secureZero(u8, &shared);    var result = Handshake{        .public = X25519.recoverPublicKey(private.*) catch unreachable,        .derived = undefined,    };    const derived = crypto.hkdf.derive(64, &shared, link_id[0..], null, &result.derived) catch        unreachable;    std.debug.assert(derived.len == result.derived.len);    return result;}fn linkPacket(    link_id: [16]u8,    packet_type: wire.PacketType,    context: wire.Context,    payload: []const u8,) wire.Packet {    return .{        .ifac = 0,        .header = .one,        .context_flag = 0,        .transport = .broadcast,        .destination_type = .link,        .packet_type = packet_type,        .hops = 0,        .transport_id = null,        .destination = link_id,        .context = context,        .payload = payload,    };}fn requestFrame(    node_owner: *node.Node,    destination_hash: [16]u8,    public: identity.KeyBytes,) []const u8 {    const value = wire.link.Request{        .encryption_public = public[0..32].*,        .signing_public = public[32..64].*,        .signalling = wire.link.default_signalling,    };    const payload = value.encode(node_owner.scratch[wire.header_one_bytes..]) catch unreachable;    const raw = wire.encode(.{        .ifac = 0,        .header = .one,        .context_flag = 0,        .transport = .broadcast,        .destination_type = .single,        .packet_type = .link_request,        .hops = 0,        .transport_id = null,        .destination = destination_hash,        .context = .none,        .payload = payload,    }, node_owner.scratch) catch unreachable;    std.debug.assert(raw.len == wire.header_one_bytes + wire.link.signalled_request_bytes);    return raw;}fn proofFrame(    node_owner: *node.Node,    link_id: [16]u8,    responder_public: [32]u8,    private: *const identity.Private,) []const u8 {    const signalling = wire.link.default_signalling;    const destination_public = private.publicBytes();    var signed: [wire.link.signed_proof_bytes_max]u8 = undefined;    const message = wire.link.signedProof(        link_id,        responder_public,        destination_public[32..64].*,        signalling,        &signed,    );    const value = wire.link.Proof{        .signature = private.sign(message),        .encryption_public = responder_public,        .signalling = signalling,    };    const payload = value.encode(node_owner.scratch[wire.header_one_bytes..]) catch unreachable;    const proof_packet = linkPacket(link_id, .proof, .lrproof, payload);    const raw = wire.encode(proof_packet, node_owner.scratch) catch unreachable;    std.debug.assert(raw.len == wire.header_one_bytes + wire.link.signalled_proof_bytes);    return raw;}fn encryptedFrame(    node_owner: *node.Node,    entry: *const links.Entry,    context: wire.Context,    iv: [crypto.token.iv_length]u8,    plaintext: []const u8,) error{PacketTooLarge}![]const u8 {    std.debug.assert(entry.status != .pending);    const token = crypto.token.Token.init(&entry.derived_key) catch unreachable;    const payload = token.encrypt(iv, plaintext, node_owner.scratch[wire.header_one_bytes..]) catch        return error.PacketTooLarge;    return wire.encode(linkPacket(entry.id, .data, context, payload), node_owner.scratch) catch        return error.PacketTooLarge;}fn plainFrame(    node_owner: *node.Node,    link_id: [16]u8,    packet_type: wire.PacketType,    context: wire.Context,    payload: []const u8,) []const u8 {    std.debug.assert(payload.len > 0);    std.debug.assert(payload.len <= wire.mtu - wire.header_one_bytes);    const value = linkPacket(link_id, packet_type, context, payload);    const raw = wire.encode(value, node_owner.scratch) catch unreachable;    std.debug.assert(raw.len == wire.header_one_bytes + payload.len);    return raw;}fn schedule(node_owner: *node.Node, link_id: [16]u8, at: node.Seconds) node.StepError!void {    const timer_id = node.TimerId{ .link = link_id };    node_owner.timers.schedule(timer_id, at) catch return error.TimerFull;    node_owner.effects.push(.{ .schedule_timer = .{ .id = timer_id, .at = at } }) catch        return error.EffectsFull;}fn sendOnLink(    node_owner: *node.Node,    entry: *const links.Entry,    frame: []const u8,) node.StepError!void {    std.debug.assert(entry.status != .pending);    const fanout: node.outbound.Fanout = .{ .one = entry.attached };    if (node.outbound.frameCount(node_owner, fanout) == 0) return;    _ = node_owner.duplicate_hashes.insert(wire.hash.full(frame) catch unreachable);    try node.outbound.transmit(node_owner, fanout, frame);}fn release(node_owner: *node.Node, link_id: [16]u8, reason: node.LinkCloseReason) void {    const entry = node_owner.transport.links.find(link_id) orelse unreachable;    const destination_hash = entry.destination;    _ = node_owner.timers.cancel(.{ .link = link_id });    const removed = node_owner.transport.links.remove(link_id);    std.debug.assert(removed);    node_owner.effects.push(.{ .link_closed = .{        .link_id = link_id,        .destination = destination_hash,        .reason = reason,    } }) catch unreachable;    std.debug.assert(node_owner.transport.links.find(link_id) == null);}fn closeReason(role: links.Role) node.LinkCloseReason {    return switch (role) {        .initiator => .initiator_closed,        .responder => .destination_closed,    };}fn secondsAtOrAfter(value: f64) node.Seconds {    std.debug.assert(!std.math.isNan(value));    const rounded = @ceil(value);    if (rounded <= 0) return 0;    if (rounded >= seconds_limit) return std.math.maxInt(node.Seconds);    return @intFromFloat(rounded);}fn floatSeconds(seconds: node.Seconds) f64 {    return @floatFromInt(seconds);}fn keepalivePeriod(entry: *const links.Entry) f64 {    std.debug.assert(std.math.isFinite(entry.rtt));    std.debug.assert(entry.rtt >= 0);    const period = switch (entry.status) {        .pending, .handshake => keepalive_max_seconds,        .active, .stale => @max(            @min(entry.rtt * (keepalive_max_seconds / keepalive_max_rtt), keepalive_max_seconds),            keepalive_min_seconds,        ),    };    std.debug.assert(period >= keepalive_min_seconds);    std.debug.assert(period <= keepalive_max_seconds);    return period;}fn quietSince(entry: *const links.Entry) node.Seconds {    return @max(@max(entry.last_inbound, entry.last_proof), entry.activated_at);}fn keepaliveDue(entry: *const links.Entry) node.Seconds {    std.debug.assert(entry.role == .initiator);    const period = keepalivePeriod(entry);    const quiet = @min(        secondsAtOrAfter(floatSeconds(quietSince(entry)) + period),        secondsAtOrAfter(floatSeconds(entry.last_outbound) + period),    );    return @max(quiet, secondsAtOrAfter(floatSeconds(entry.last_keepalive) + period));}fn staleDue(entry: *const links.Entry) node.Seconds {    const stale_time = keepalivePeriod(entry) * stale_factor;    return secondsAtOrAfter(floatSeconds(quietSince(entry)) + stale_time);}/// Answers with the whole second on which a link that has turned stale emits its LINKCLOSE and/// closes under reason `timeout`, five seconds past the moment it turned, so the stale watchdog/// knows when to send the close packet. Reticulum@1.5.0 RNS/Link.py:754 sets its own delay to `rtt`/// times `KEEPALIVE_TIMEOUT_FACTOR` plus `STALE_GRACE`, whose values are 4 and 5. Reticulum@1.5.0/// RNS/Link.py:775 holds each watchdog sleep under `WATCHDOG_MAX_SLEEP`, whose value is 5. A round/// trip time is at least zero, so `rtt` times 4 plus 5 is at least 5, the cap always wins, and a/// reference link waits exactly five seconds.fn staleCloseAt(entry: *const links.Entry) node.Seconds {    return staleDue(entry) +| stale_grace;}fn answersKeepalive(entry: *const links.Entry, now: node.Seconds) bool {    std.debug.assert(entry.role == .responder);    return now >= secondsAtOrAfter(floatSeconds(entry.last_outbound) + keepalivePeriod(entry));}fn receiptTimeout(sent_at: node.Seconds, rtt: f64) node.Seconds {    std.debug.assert(std.math.isFinite(rtt));    const timeout = @max(rtt * traffic_timeout_factor, traffic_timeout_min);    const last_quiet = @floor(floatSeconds(sent_at) + timeout);    if (last_quiet >= seconds_limit) return std.math.maxInt(node.Seconds) - sent_at;    const quiet_until: node.Seconds = @intFromFloat(last_quiet);    return quiet_until -| sent_at;}fn nextDeadline(entry: *const links.Entry) node.Seconds {    return switch (entry.status) {        .pending, .handshake => entry.request_time +| entry.establishment_timeout,        .active => switch (entry.role) {            .initiator => @min(keepaliveDue(entry), staleDue(entry)),            .responder => staleDue(entry),        },        .stale => entry.close_at,    };}fn markInbound(entry: *links.Entry, now: node.Seconds) void {    std.debug.assert(entry.status != .pending);    entry.last_inbound = @max(entry.last_inbound, now);    if (entry.status == .stale) entry.status = .active;}fn markOutbound(entry: *links.Entry, now: node.Seconds) void {    entry.last_outbound = @max(entry.last_outbound, now);}fn markKeepalive(entry: *links.Entry, now: node.Seconds) void {    markOutbound(entry, now);    entry.last_keepalive = @max(entry.last_keepalive, now);}fn initiatorEntry(    value: node.ApplicationLinkOpen,    link_id: [16]u8,    destination_public: *const identity.KeyBytes,    hops: u8,    timeout: node.Seconds,) links.Entry {    std.debug.assert(timeout >= node.outbound.first_hop_timeout + per_hop_timeout);    return .{        .id = link_id,        .destination = value.destination,        .encryption_private = value.encryption_private,        .signing_private = value.signing_private,        .peer_signing_public = destination_public[32..64].*,        .derived_key = @splat(0),        .request_time = value.now,        .establishment_timeout = timeout,        .activated_at = 0,        .last_inbound = 0,        .last_outbound = value.now,        .last_keepalive = 0,        .last_proof = 0,        .close_at = 0,        .rtt = 0,        .expected_hops = hops,        .attached = 0,        .role = .initiator,        .status = .pending,        .rebalanced = false,        .proof_strategy = value.proof_strategy,    };}/// Opens an encrypted session for the caller so the link id learned here names everything after./// The call puts a link request on the wire toward a destination the node has an identity for, and/// records a pending initiator link carrying one timer set at the establishment deadline. That/// request holds the two public keys matching the private keys the caller supplied, and its/// signalling bytes announce AES-256-CBC with an MTU of 500, as Reticulum@1.5.0 RNS/Link.py:304-322/// sends it. Reticulum@1.5.0 RNS/Link.py:281-283 fixes that deadline at 6 seconds plus 6 seconds/// for each hop, counting one hop at the least and counting 128 hops when no path is known. A/// request routed across more than one hop leaves with a transport header, as Reticulum@1.5.0/// RNS/Transport.py:1345-1356 adds it. A request with no path to follow leaves on every carrier,/// and the node keeps its packet hash, as Reticulum@1.5.0 RNS/Transport.py:1393,1536-1539/// broadcasts it. An identity the node does not recall returns `error.UnknownDestination`, a full/// link pool returns `error.LinksFull`, and keys that give the id of an open link return/// `error.DuplicateLink`. Every one of those errors returns before the node changes anything.pub fn open(node_owner: *node.Node, value: node.ApplicationLinkOpen) node.StepError!void {    const known = node_owner.known_identities.recall(value.destination) orelse        return error.UnknownDestination;    if (node_owner.transport.links.full()) return error.LinksFull;    var private = identity.Private.fromBytes(value.encryption_private ++ value.signing_private);    defer private.zero();    const raw = requestFrame(node_owner, value.destination, private.publicBytes());    const link_id = wire.link.linkId(raw) catch unreachable;    if (node_owner.transport.links.find(link_id) != null) return error.DuplicateLink;    const hash = wire.hash.full(raw) catch unreachable;    const path = node_owner.transport.paths.find(value.destination, value.now);    const fanout: node.outbound.Fanout = if (path) |entry| .{ .one = entry.carrier } else .all;    const carriers = node.outbound.frameCount(node_owner, fanout);    if (carriers == 0) return error.NoOutgoingCarrier;    std.debug.assert(carriers <= node_owner.interfaces.len);    if (!node_owner.timers.canScheduleAfterCancel(.{ .link = link_id }, null)) {        return error.TimerFull;    }    const hops = if (path) |entry| entry.hops else wire.pathfinder_hops;    const inserts = path != null and hops > 1;    const frame = if (inserts)        try node.transport.rewrite.insert(raw, path.?.next_hop, node_owner.scratch)    else        raw;    try node.outbound.reserve(node_owner, carriers + 2, carriers);    const per_hop = per_hop_timeout * @as(node.Seconds, @max(1, hops));    const timeout = node.outbound.first_hop_timeout + per_hop;    const entry = initiatorEntry(value, link_id, &known.public_key, hops, timeout);    _ = node_owner.transport.links.insert(entry) catch unreachable;    std.debug.assert(node_owner.transport.links.find(link_id) != null);    node_owner.effects.push(.{ .link_requested = .{        .link_id = link_id,        .destination = value.destination,    } }) catch unreachable;    try schedule(node_owner, link_id, value.now +| timeout);    if (path == null) _ = node_owner.duplicate_hashes.insert(hash);    if (inserts) path.?.timestamp = value.now;    try node.outbound.transmit(node_owner, fanout, frame);}/// Transmits application bytes across an open link for the caller. The call encrypts the plaintext/// the caller passed under the link key with the initialization vector the caller passed, and puts/// one data packet on the carrier that link uses, as Reticulum@1.5.0 RNS/Link.py:73,/// Reticulum@1.5.0 RNS/Packet.py:290-300,419-420, and Reticulum@1.5.0 RNS/Transport.py:1307-1318/// send it. With a receipt asked for, a receipt that stays in the table fails max(rtt * 6, 0.005)/// seconds after the send when no proof has arrived, and when a later send that asks for a receipt/// finds the receipt table full, the node culls the oldest receipt and reports it to the/// application as a `receipt_update` effect with status `culled` and no round trip time. An unheld/// link id returns `error.UnknownLink`, a link that has yet to activate returns/// `error.LinkNotEstablished`, a carrier that carries no outgoing traffic returns/// `error.NoOutgoingCarrier`, and plaintext over 431 bytes returns `error.PacketTooLarge`. Each of/// those errors returns before the node changes anything.pub fn send(node_owner: *node.Node, value: node.ApplicationLinkSend) node.StepError!void {    const entry = node_owner.transport.links.find(value.link_id) orelse return error.UnknownLink;    switch (entry.status) {        .pending, .handshake => return error.LinkNotEstablished,        .active, .stale => {},    }    const fanout: node.outbound.Fanout = .{ .one = entry.attached };    const carriers = node.outbound.frameCount(node_owner, fanout);    if (carriers == 0) return error.NoOutgoingCarrier;    const frame = try encryptedFrame(node_owner, entry, .none, value.iv, value.plaintext);    const hash = wire.hash.full(frame) catch unreachable;    const culls = value.create_receipt and node_owner.receipts.cullCandidate(hash) != null;    const effects = carriers + @intFromBool(value.create_receipt) + @intFromBool(culls);    try node.outbound.reserve(node_owner, effects, carriers);    if (value.create_receipt) {        const timeout = receiptTimeout(value.now, entry.rtt);        try node.outbound.insertReceipt(node_owner, entry.id, hash, value.now, timeout);    }    markOutbound(entry, value.now);    try sendOnLink(node_owner, entry, frame);}/// Closes a link for the caller so the other end hears the close. The call emits a LINKCLOSE/// carrying the link id as its plaintext, releases the link, and reports the close to the caller,/// as Reticulum@1.5.0 RNS/Link.py:657-672 closes it. An initiator gives the reason/// `initiator_closed` and a responder gives `destination_closed`. A link that has yet to activate/// closes with no packet sent. An unheld link id returns `error.UnknownLink`.pub fn close(node_owner: *node.Node, value: node.ApplicationLinkClose) node.StepError!void {    const entry = node_owner.transport.links.find(value.link_id) orelse return error.UnknownLink;    const reason = closeReason(entry.role);    if (entry.status == .pending) {        try node.outbound.reserve(node_owner, 1, 0);        return release(node_owner, value.link_id, reason);    }    const carriers = node.outbound.frameCount(node_owner, .{ .one = entry.attached });    try node.outbound.reserve(node_owner, carriers + 1, carriers);    try teardown(node_owner, entry, value.iv, reason);}/// Answers a link delivery for an application under the `.app` proof strategy. The call puts a/// proof of one link packet on the link as plaintext, carrying that packet hash together with a/// signature over it, as Reticulum@1.5.0 RNS/Link.py:378-389 and Reticulum@1.5.0/// RNS/Transport.py:2527-2530 send it. The signing key is the ephemeral key on an initiator and the/// destination identity on a responder. An unheld link id returns `error.UnknownLink`, a link that/// is still pending returns `error.LinkNotEstablished`, a packet the node has already forgotten/// returns `error.ProofUnavailable`, and a carrier that carries no outgoing traffic returns/// `error.NoOutgoingCarrier`.pub fn prove(node_owner: *node.Node, value: node.ApplicationLinkProve) node.StepError!void {    const entry = node_owner.transport.links.find(value.link_id) orelse return error.UnknownLink;    if (entry.status == .pending) return error.LinkNotEstablished;    if (!node_owner.duplicate_hashes.contains(value.packet_hash)) return error.ProofUnavailable;    const carriers = node.outbound.frameCount(node_owner, .{ .one = entry.attached });    if (carriers == 0) return error.NoOutgoingCarrier;    try node.outbound.reserve(node_owner, carriers, carriers);    try proveOnLink(node_owner, entry, value.packet_hash, value.now);}/// Handles one arriving packet that belongs to a link and reports whether it did, so the frame/// handler sends link traffic here and keeps the rest. A link request naming a local single/// destination draws a proof and leaves a responder link in handshake, as Reticulum@1.5.0/// RNS/Transport.py:2456-2487 and Reticulum@1.5.0 RNS/Link.py:186-215,366-375 answer it. A request/// proof arriving for a pending initiator link reaches that link. A proof that verifies with a hop/// count differing from the one the link expects corrects the expectation once, as Reticulum@1.5.0/// RNS/Transport.py:2605-2650 corrects it. A proof that verifies and whose X25519 key lands on an/// all-zero shared secret draws `proof_rejected` and holds the link at pending, and a proof that/// verifies later at the expected hops brings the link up. Every other proof that verifies at the/// expected hops brings the link up and sends the round trip packet, as Reticulum@1.5.0/// RNS/Link.py:391-438 does. A round trip packet activates a responder link, as Reticulum@1.5.0/// RNS/Link.py:516-538 activates it. Link data arriving on a carrier the link does not use drops/// that packet hash from the current generation of duplicate hashes and draws/// `link_wrong_interface`, as Reticulum@1.5.0 RNS/Transport.py:2515-2516 checks it. A data packet/// that carries no context arrives on a link that has activated, and under the `.all` proof/// strategy the node decrypts it, hands it to the application, and proves it. A keepalive request/// arriving at a responder draws an answer once one keepalive period has passed since that/// responder last sent a packet. A LINKCLOSE carrying the link id as plaintext closes the link, and/// one carrying any other plaintext draws `link_close_invalid`, as Reticulum@1.5.0/// RNS/Link.py:674-683,948-968,1130-1135 handles them. A request, proof, or packet naming a link/// the node does not hold goes to a transport node's relay table, and `node.transport.link.relay`/// carries it onward, as Reticulum@1.5.0 RNS/Transport.py:1968-2010,2030-2077,2535-2600 relays it./// Link traffic this code carries no support for reports `links_unsupported`.pub fn receive(    node_owner: *node.Node,    value: wire.Packet,    raw: []const u8,    frame: node.CarrierFrame,    hash: packet.Hash,) node.StepError!bool {    std.debug.assert(value.hops >= 1);    if (value.packet_type == .link_request) {        try routeRequest(node_owner, value, raw, frame, hash);        return true;    }    if (value.packet_type == .proof and value.context == .lrproof) {        try routeProof(node_owner, value, raw, frame, hash);        return true;    }    if (value.destination_type == .link) {        try routeLinkPacket(node_owner, value, raw, frame, hash);        return true;    }    if (value.context.encode() < wire.Context.linkidentify.encode()) return false;    node.inbound.diagnostic(node_owner, .links_unsupported, hash);    return true;}fn routeRequest(    node_owner: *node.Node,    value: wire.Packet,    raw: []const u8,    frame: node.CarrierFrame,    hash: packet.Hash,) node.StepError!void {    std.debug.assert(value.packet_type == .link_request);    if (node_owner.destinations.find(value.destination)) |entry| {        return answerRequest(node_owner, entry, value, raw, frame, hash);    }    if (relay.appliesToRequest(node_owner, value)) {        return relay.request(node_owner, value, raw, frame, hash);    }    node.inbound.diagnostic(node_owner, .unknown_destination, hash);}fn routeProof(    node_owner: *node.Node,    value: wire.Packet,    raw: []const u8,    frame: node.CarrierFrame,    hash: packet.Hash,) node.StepError!void {    std.debug.assert(value.context == .lrproof);    if (node_owner.transport.links.find(value.destination)) |entry| {        return acceptProof(node_owner, entry, value, frame, hash);    }    if (relay.find(node_owner, value.destination)) |entry| {        return relay.proof(node_owner, entry, value, raw, frame, hash);    }    node.inbound.diagnostic(node_owner, .unknown_link, hash);}fn routeLinkPacket(    node_owner: *node.Node,    value: wire.Packet,    raw: []const u8,    frame: node.CarrierFrame,    hash: packet.Hash,) node.StepError!void {    std.debug.assert(value.destination_type == .link);    if (value.packet_type == .announce) {        return node.inbound.diagnostic(node_owner, .links_unsupported, hash);    }    const entry = node_owner.transport.links.find(value.destination) orelse {        if (relay.find(node_owner, value.destination)) |relayed| {            return relay.traffic(node_owner, relayed, value, raw, frame, hash);        }        return node.inbound.diagnostic(node_owner, .unknown_link, hash);    };    if (entry.status == .pending) return node.inbound.diagnostic(node_owner, .unknown_link, hash);    if (value.packet_type == .proof) return acceptLinkProof(node_owner, entry, value, frame, hash);    if (value.packet_type != .data) {        return node.inbound.diagnostic(node_owner, .links_unsupported, hash);    }    if (entry.attached != frame.interface) {        _ = node_owner.duplicate_hashes.removeCurrent(hash);        return node.inbound.diagnostic(node_owner, .link_wrong_interface, hash);    }    return receiveTraffic(node_owner, entry, value, frame, hash);}fn receiveTraffic(    node_owner: *node.Node,    entry: *links.Entry,    value: wire.Packet,    frame: node.CarrierFrame,    hash: packet.Hash,) node.StepError!void {    std.debug.assert(value.packet_type == .data);    std.debug.assert(entry.attached == frame.interface);    switch (value.context) {        .none => return deliver(node_owner, entry, value, frame, hash),        .lrrtt => return acceptRtt(node_owner, entry, value, frame, hash),        .linkclose => return acceptClose(node_owner, entry, value, frame, hash),        .keepalive => return acceptKeepalive(node_owner, entry, value, frame, hash),        else => {            markInbound(entry, frame.now);            return node.inbound.diagnostic(node_owner, .links_unsupported, hash);        },    }}fn deliver(    node_owner: *node.Node,    entry: *links.Entry,    value: wire.Packet,    frame: node.CarrierFrame,    hash: packet.Hash,) node.StepError!void {    std.debug.assert(value.context == .none);    const proves = entry.proof_strategy == .all;    const fanout: node.outbound.Fanout = .{ .one = entry.attached };    const carriers = if (proves) node.outbound.frameCount(node_owner, fanout) else 0;    try node.outbound.reserve(node_owner, 1 + carriers, carriers);    markInbound(entry, frame.now);    const token = crypto.token.Token.init(&entry.derived_key) catch unreachable;    const plaintext = token.decrypt(value.payload, node_owner.scratch) catch        return node.inbound.diagnostic(node_owner, .decryption_failed, hash);    node_owner.effects.push(.{ .link_delivery = .{        .link_id = entry.id,        .packet_hash = hash,        .plaintext = plaintext,        .proof_requested = entry.proof_strategy == .app,    } }) catch unreachable;    if (proves) try proveOnLink(node_owner, entry, hash, frame.now);}fn acceptKeepalive(    node_owner: *node.Node,    entry: *links.Entry,    value: wire.Packet,    frame: node.CarrierFrame,    hash: packet.Hash,) node.StepError!void {    std.debug.assert(value.context == .keepalive);    const request = value.payload.len == 1 and value.payload[0] == wire.link.keepalive_request;    if (entry.role == .initiator and request) {        return node.inbound.diagnostic(node_owner, .link_state_mismatch, hash);    }    const carriers = node.outbound.frameCount(node_owner, .{ .one = entry.attached });    try node.outbound.reserve(node_owner, carriers, carriers);    markInbound(entry, frame.now);    if (entry.role == .initiator or !request) return;    if (!answersKeepalive(entry, frame.now)) return;    const answer = [_]u8{wire.link.keepalive_answer};    const answer_frame = plainFrame(node_owner, entry.id, .data, .keepalive, &answer);    try sendOnLink(node_owner, entry, answer_frame);    markKeepalive(entry, frame.now);}fn acceptClose(    node_owner: *node.Node,    entry: *links.Entry,    value: wire.Packet,    frame: node.CarrierFrame,    hash: packet.Hash,) node.StepError!void {    std.debug.assert(value.context == .linkclose);    try node.outbound.reserve(node_owner, 1, 0);    markInbound(entry, frame.now);    const token = crypto.token.Token.init(&entry.derived_key) catch unreachable;    const plaintext = token.decrypt(value.payload, node_owner.scratch) catch        return node.inbound.diagnostic(node_owner, .decryption_failed, hash);    if (!wire.link.closes(plaintext, entry.id)) {        return node.inbound.diagnostic(node_owner, .link_close_invalid, hash);    }    const reason: node.LinkCloseReason = switch (entry.role) {        .initiator => .destination_closed,        .responder => .initiator_closed,    };    release(node_owner, entry.id, reason);}fn proofSignature(    node_owner: *node.Node,    entry: *const links.Entry,    packet_hash: *const [32]u8,) [64]u8 {    switch (entry.role) {        .initiator => {            const empty: [32]u8 = @splat(0);            var private = identity.Private.fromBytes(empty ++ entry.signing_private);            defer private.zero();            return private.sign(packet_hash);        },        .responder => {            const registered = node_owner.destinations.find(entry.destination) orelse                unreachable;            const private = requestIdentity(node_owner, registered) orelse unreachable;            return private.sign(packet_hash);        },    }}fn proveOnLink(    node_owner: *node.Node,    entry: *links.Entry,    packet_hash: [32]u8,    now: node.Seconds,) node.StepError!void {    std.debug.assert(entry.status != .pending);    var payload: [wire.proof.explicit_bytes]u8 = undefined;    payload[0..wire.proof.packet_hash_bytes].* = packet_hash;    payload[wire.proof.packet_hash_bytes..].* = proofSignature(node_owner, entry, &packet_hash);    const proof = plainFrame(node_owner, entry.id, .proof, .none, &payload);    try sendOnLink(node_owner, entry, proof);    markOutbound(entry, now);}fn provedReceipt(    node_owner: *node.Node,    entry: *const links.Entry,    payload: []const u8,) ?packet.Hash {    if (payload.len < wire.proof.explicit_bytes) return null;    const proved: packet.Hash = payload[0..wire.proof.packet_hash_bytes].*;    const receipt = node_owner.receipts.find(proved) orelse return null;    if (receipt.status != .sent) return null;    const signature = payload[wire.proof.packet_hash_bytes..wire.proof.explicit_bytes].*;    const signer = Ed25519.PublicKey.fromBytes(entry.peer_signing_public) catch return null;    Ed25519.Signature.fromBytes(signature).verify(&proved, signer) catch return null;    return proved;}fn acceptLinkProof(    node_owner: *node.Node,    entry: *links.Entry,    value: wire.Packet,    frame: node.CarrierFrame,    hash: packet.Hash,) node.StepError!void {    std.debug.assert(value.packet_type == .proof);    std.debug.assert(entry.status != .pending);    if (value.context == .resource_prf) {        return node.inbound.diagnostic(node_owner, .links_unsupported, hash);    }    const proved = provedReceipt(node_owner, entry, value.payload) orelse        return node.inbound.diagnostic(node_owner, .proof_rejected, hash);    try node.outbound.reserve(node_owner, 1, 0);    entry.last_proof = @max(entry.last_proof, frame.now);    node.inbound.concludeReceipt(node_owner, proved, frame.now);}fn requestIdentity(    node_owner: *node.Node,    entry: *const destination.registry.Entry,) ?*const identity.Private {    if (entry.kind != .single) return null;    const index = entry.identity_index orelse return null;    return node_owner.identityAt(index);}fn acceptsSignalling(signalling: ?wire.link.Signalling) bool {    const value = signalling orelse return true;    if (value.mtu != 0) return true;    return value.mode == wire.link.default_mode;}fn responderEntry(    value: wire.Packet,    frame: node.CarrierFrame,    link_id: [16]u8,    peer_signing_public: [32]u8,    derived_key: *const [64]u8,    timeout: node.Seconds,    proof_strategy: destination.registry.ProofStrategy,) links.Entry {    std.debug.assert(timeout > keepalive_max);    return .{        .id = link_id,        .destination = value.destination,        .encryption_private = @splat(0),        .signing_private = @splat(0),        .peer_signing_public = peer_signing_public,        .derived_key = derived_key.*,        .request_time = frame.now,        .establishment_timeout = timeout,        .activated_at = 0,        .last_inbound = frame.now,        .last_outbound = frame.now,        .last_keepalive = 0,        .last_proof = 0,        .close_at = 0,        .rtt = 0,        .expected_hops = 0,        .attached = frame.interface,        .role = .responder,        .status = .handshake,        .rebalanced = false,        .proof_strategy = proof_strategy,    };}fn answerRequest(    node_owner: *node.Node,    entry: *const destination.registry.Entry,    value: wire.Packet,    raw: []const u8,    frame: node.CarrierFrame,    hash: packet.Hash,) node.StepError!void {    std.debug.assert(value.packet_type == .link_request);    if (@backingInt(entry.kind) != @backingInt(value.destination_type)) {        return node.inbound.diagnostic(node_owner, .destination_type_mismatch, hash);    }    const private = requestIdentity(node_owner, entry) orelse        return node.inbound.diagnostic(node_owner, .link_request_invalid, hash);    const decoded = wire.link.Request.decode(value.payload) catch        return node.inbound.diagnostic(node_owner, .link_request_invalid, hash);    if (!acceptsSignalling(decoded.signalling)) {        return node.inbound.diagnostic(node_owner, .link_request_invalid, hash);    }    const link_id = wire.link.linkId(raw) catch unreachable;    if (node_owner.transport.links.find(link_id) != null) {        return node.inbound.diagnostic(node_owner, .link_request_duplicate, hash);    }    if (node_owner.transport.links.full()) {        return node.inbound.diagnostic(node_owner, .links_full, hash);    }    var responder_private: [32]u8 = undefined;    defer std.crypto.secureZero(u8, &responder_private);    expand(&frame.entropy, &link_id, responder_key_label, &responder_private);    var keys = handshake(&responder_private, decoded.encryption_public, &link_id) orelse        return node.inbound.diagnostic(node_owner, .link_request_invalid, hash);    defer std.crypto.secureZero(u8, &keys.derived);    if (!node_owner.timers.canScheduleAfterCancel(.{ .link = link_id }, null)) {        return error.TimerFull;    }    const carriers = node.outbound.frameCount(node_owner, .{ .one = frame.interface });    try node.outbound.reserve(node_owner, carriers + 1, carriers);    const proof = proofFrame(node_owner, link_id, keys.public, private);    const timeout = per_hop_timeout * @as(node.Seconds, @max(1, value.hops)) + keepalive_max;    const responder = responderEntry(        value,        frame,        link_id,        decoded.signing_public,        &keys.derived,        timeout,        entry.proof_strategy,    );    const inserted = node_owner.transport.links.insert(responder) catch unreachable;    std.debug.assert(inserted.status == .handshake);    try schedule(node_owner, link_id, frame.now +| timeout);    try sendOnLink(node_owner, inserted, proof);}fn verifyProof(entry: *const links.Entry, payload: []const u8) ?[32]u8 {    if (wire.link.proofMode(payload) != wire.link.default_mode) return null;    const decoded = wire.link.Proof.decode(payload) catch return null;    var signed: [wire.link.signed_proof_bytes_max]u8 = undefined;    const message = wire.link.signedProof(        entry.id,        decoded.encryption_public,        entry.peer_signing_public,        decoded.signalling,        &signed,    );    const signer = Ed25519.PublicKey.fromBytes(entry.peer_signing_public) catch return null;    Ed25519.Signature.fromBytes(decoded.signature).verify(message, signer) catch return null;    return decoded.encryption_public;}fn rebalance(node_owner: *node.Node, entry: *links.Entry, hops: u8, now: node.Seconds) void {    std.debug.assert(entry.status == .pending);    std.debug.assert(!entry.rebalanced);    entry.rebalanced = true;    entry.expected_hops = hops;    const path = node_owner.transport.paths.find(entry.destination, now) orelse return;    path.hops = hops;}fn acceptProof(    node_owner: *node.Node,    entry: *links.Entry,    value: wire.Packet,    frame: node.CarrierFrame,    hash: packet.Hash,) node.StepError!void {    std.debug.assert(value.context == .lrproof);    if (entry.role != .initiator or entry.status != .pending) {        return node.inbound.diagnostic(node_owner, .link_state_mismatch, hash);    }    if (!node_owner.timers.canScheduleAfterCancel(.{ .link = entry.id }, null)) {        return error.TimerFull;    }    const carriers = node.outbound.frameCount(node_owner, .{ .one = frame.interface });    try node.outbound.reserve(node_owner, carriers + 2, carriers);    const responder_public = verifyProof(entry, value.payload);    const unbalanced = value.hops != entry.expected_hops;    if (responder_public != null and unbalanced and !entry.rebalanced) {        rebalance(node_owner, entry, value.hops, frame.now);    }    if (value.hops != entry.expected_hops) {        return node.inbound.diagnostic(node_owner, .proof_rejected, hash);    }    _ = node_owner.duplicate_hashes.insert(hash);    const peer_public = responder_public orelse        return node.inbound.diagnostic(node_owner, .proof_rejected, hash);    var keys = handshake(&entry.encryption_private, peer_public, &entry.id) orelse        return node.inbound.diagnostic(node_owner, .proof_rejected, hash);    defer std.crypto.secureZero(u8, &keys.derived);    try establishInitiator(node_owner, entry, &keys.derived, frame);}fn establishInitiator(    node_owner: *node.Node,    entry: *links.Entry,    derived_key: *const [64]u8,    frame: node.CarrierFrame,) node.StepError!void {    std.debug.assert(entry.role == .initiator);    std.debug.assert(entry.status == .pending);    const rtt: f64 = @floatFromInt(frame.now -| entry.request_time);    std.debug.assert(rtt >= 0);    var iv: [crypto.token.iv_length]u8 = undefined;    expand(&frame.entropy, &entry.id, rtt_iv_label, &iv);    entry.derived_key = derived_key.*;    std.crypto.secureZero(u8, &entry.encryption_private);    entry.attached = frame.interface;    entry.status = .active;    entry.activated_at = frame.now;    entry.last_proof = frame.now;    entry.rtt = rtt;    node_owner.effects.push(.{ .link_established = .{        .link_id = entry.id,        .destination = entry.destination,        .role = .initiator,        .rtt = rtt,        .interface = frame.interface,    } }) catch unreachable;    const plaintext = wire.link.encodeRtt(rtt);    const rtt_frame = encryptedFrame(node_owner, entry, .lrrtt, iv, &plaintext) catch        unreachable;    try sendOnLink(node_owner, entry, rtt_frame);    markOutbound(entry, frame.now);    try schedule(node_owner, entry.id, nextDeadline(entry));}fn acceptRtt(    node_owner: *node.Node,    entry: *links.Entry,    value: wire.Packet,    frame: node.CarrierFrame,    hash: packet.Hash,) node.StepError!void {    std.debug.assert(value.context == .lrrtt);    std.debug.assert(entry.attached == frame.interface);    const activates = entry.role == .responder and entry.status == .handshake;    if (activates and !node_owner.timers.canScheduleAfterCancel(.{ .link = entry.id }, null)) {        return error.TimerFull;    }    const carriers = node.outbound.frameCount(node_owner, .{ .one = entry.attached });    try node.outbound.reserve(node_owner, carriers + 2, carriers);    markInbound(entry, frame.now);    if (!activates) return node.inbound.diagnostic(node_owner, .link_state_mismatch, hash);    const token = crypto.token.Token.init(&entry.derived_key) catch unreachable;    const plaintext = token.decrypt(value.payload, node_owner.scratch) catch        return node.inbound.diagnostic(node_owner, .decryption_failed, hash);    const received = wire.link.decodeRtt(plaintext) catch {        var iv: [crypto.token.iv_length]u8 = undefined;        expand(&frame.entropy, &entry.id, close_iv_label, &iv);        return teardown(node_owner, entry, iv, closeReason(entry.role));    };    const measured: f64 = @floatFromInt(frame.now -| entry.request_time);    entry.rtt = @max(measured, received);    std.debug.assert(entry.rtt >= measured);    entry.status = .active;    entry.activated_at = frame.now;    entry.expected_hops = value.hops;    node_owner.effects.push(.{ .link_established = .{        .link_id = entry.id,        .destination = entry.destination,        .role = .responder,        .rtt = entry.rtt,        .interface = entry.attached,    } }) catch unreachable;    try schedule(node_owner, entry.id, nextDeadline(entry));}fn teardown(    node_owner: *node.Node,    entry: *links.Entry,    iv: [crypto.token.iv_length]u8,    reason: node.LinkCloseReason,) node.StepError!void {    std.debug.assert(entry.status != .pending);    const link_id = entry.id;    const close_frame = encryptedFrame(node_owner, entry, .linkclose, iv, &entry.id) catch        unreachable;    try sendOnLink(node_owner, entry, close_frame);    release(node_owner, link_id, reason);}/// Handles every deadline a link holds for the node. The call closes a link still pending or in/// handshake once its establishment deadline arrives, under reason `timeout`, as Reticulum@1.5.0/// RNS/Link.py:722-738,744-766 times it out. On a link that has activated the same call drives the/// keepalive and stale watchdog: an initiator sends a keepalive request after one quiet keepalive/// period, and either end turns stale after two. A stale link sends a LINKCLOSE packet and closes/// with reason `timeout` at its close deadline. A timer arriving ahead of the deadline it carries/// is armed again for that deadline or for one second past the current instant, whichever falls/// later. A link id the node has already dropped cancels the timer.pub fn timerExpired(    node_owner: *node.Node,    link_id: [16]u8,    value: node.TimerExpired,) node.StepError!void {    std.debug.assert(value.id == .link);    const entry = node_owner.transport.links.find(link_id) orelse {        _ = node_owner.timers.cancel(value.id);        return;    };    switch (entry.status) {        .pending, .handshake => {            const deadline = entry.request_time +| entry.establishment_timeout;            if (value.now < deadline) {                try node.outbound.reserve(node_owner, 1, 0);                return schedule(node_owner, link_id, deadline);            }            try expireUnactivated(node_owner, entry, value);        },        .active => try watchActive(node_owner, entry, value),        .stale => try watchStale(node_owner, entry, value),    }}/// Discovers a fresh path for a link attempt that timed out so the node can replace a path that may/// have gone. The call closes a link that has yet to activate and rediscovers its path, as/// Reticulum@1.5.0 RNS/Transport.py:674-697 rediscovers it. A node carrying no traffic for others/// forgets the path its initiator took and asks every carrier for a fresh one, at most once in 20/// seconds for a given destination. A node carrying traffic for others holds its path, because it/// answers path requests on its own.fn expireUnactivated(    node_owner: *node.Node,    entry: *links.Entry,    value: node.TimerExpired,) node.StepError!void {    std.debug.assert(entry.status != .active);    std.debug.assert(entry.status != .stale);    const link_id = entry.id;    const destination_hash = entry.destination;    const rediscovers = entry.role == .initiator and !node_owner.transport.enabled;    const frames = if (rediscovers)        node.transport.requests.rediscoveryFrames(node_owner, destination_hash, value.now)    else        0;    try node.outbound.reserve(node_owner, frames + 1, frames);    release(node_owner, link_id, .timeout);    if (!rediscovers) return;    var tag: [16]u8 = undefined;    expand(&value.entropy, &link_id, rediscovery_tag_label, &tag);    try node.transport.requests.rediscover(node_owner, destination_hash, &tag, value.now);}fn watchActive(    node_owner: *node.Node,    entry: *links.Entry,    value: node.TimerExpired,) node.StepError!void {    std.debug.assert(entry.status == .active);    const carriers = node.outbound.frameCount(node_owner, .{ .one = entry.attached });    try node.outbound.reserve(node_owner, carriers + 1, carriers);    if (!node_owner.timers.canScheduleAfterCancel(value.id, null)) return error.TimerFull;    if (entry.role == .initiator and value.now >= keepaliveDue(entry)) {        const request = [_]u8{wire.link.keepalive_request};        const keepalive = plainFrame(node_owner, entry.id, .data, .keepalive, &request);        try sendOnLink(node_owner, entry, keepalive);        markKeepalive(entry, value.now);    }    if (value.now >= staleDue(entry)) {        entry.status = .stale;        entry.close_at = staleCloseAt(entry);    }    try schedule(node_owner, entry.id, @max(nextDeadline(entry), value.now +| 1));}fn watchStale(    node_owner: *node.Node,    entry: *links.Entry,    value: node.TimerExpired,) node.StepError!void {    std.debug.assert(entry.status == .stale);    if (value.now < entry.close_at) {        try node.outbound.reserve(node_owner, 1, 0);        return schedule(node_owner, entry.id, entry.close_at);    }    const carriers = node.outbound.frameCount(node_owner, .{ .one = entry.attached });    try node.outbound.reserve(node_owner, carriers + 1, carriers);    var iv: [crypto.token.iv_length]u8 = undefined;    expand(&value.entropy, &entry.id, close_iv_label, &iv);    try teardown(node_owner, entry, iv, .timeout);}

Source: lib/reticulum/src/node/root.zig:76

zig
pub const link = @import("link.zig");

Complete call list for node.link.open

9 direct calls.

Complete call list for node.link.send

8 direct calls.

Audit

Definitions7
Public names7
Members0
Version26.7.0
Revisiondaab053ee433