Skip to documentation
SLOP

tiny.reticulum.node.transport.requests

Reference tiny.reticulum node transport requests

Defined in node.transport.

API (15)

Actions

Public operations.

Types and contracts

Public types and contracts.

Values and defaults

Public values and defaults.

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

Source

Called byCallsNo direct callsnode.inboundrunnode.transport.requestsreceivenode.transport.requestsaddressed
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsprivate sourcelib.reticulum.src.node.inboundacceptAnnouncenode.outboundtransmitprivate sourcelib.reticulum.src.node.transport.requestsanswersWaitingwire.hashfullnode.transport.requestsanswerWaiting
Static calls · unresolved targets: 0 · external targets: 5.
Called byCallsNo direct callsnode.transport.requestsreceivetest sourcelib.reticulum.src.node.transport.requests.tes...py:1738-1753 parses every request pay...test sourcelib.reticulum.src.node.transport.requests.tes...py:1755 keys a tag the same across re...private sourcelib.reticulum.src.node.transport.testexpectCorpusParseprivate sourcelib.reticulum.src.properties.transport.Reques...propertynode.transport.requestsparse
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsnode.inboundrunnode.inbounddiagnosticnode.outboundframeCountnode.outboundreservenode.transport.requestsaddressednode.transport.requestsparse+3 morenode.transport.requestsreceive
Static calls · unresolved targets: 0 · external targets: 9.
Called byCallsprivate sourcelib.reticulum.src.node.linkexpireUnactivatednode.outboundframeCountnode.outboundtransmitprivate sourcelib.reticulum.src.node.transport.requestsencodeprivate sourcelib.reticulum.src.node.transport.requeststhrottledwire.hashfullnode.transport.requestsrediscover
Static calls · unresolved targets: 0 · external targets: 3.
Called byCallsprivate sourcelib.reticulum.src.node.linkexpireUnactivatednode.outboundframeCountprivate sourcelib.reticulum.src.node.transport.requeststhrottlednode.transport.requestsrediscoveryFrames
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsnode.transitionrunnode.outboundframeCountnode.outboundreservenode.outboundtransmitprivate sourcelib.reticulum.src.node.transport.requestsencodewire.hashfullnode.transport.requestssend
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsprivate sourcelib.reticulum.src.node.inboundacceptAnnouncenode.outboundframeCountprivate sourcelib.reticulum.src.node.transport.requestsanswersWaitingnode.transport.requestswaitingFrames
Static calls · unresolved targets: 0 · external targets: 3.

Source: lib/reticulum/src/node/transport/requests.zig

zig
const std = @import("std");const reticulum = @import("../../root.zig");const announces = @import("announces.zig");const retransmit = @import("retransmit.zig");const carrier = reticulum.carrier;const destination = reticulum.destination;const node = reticulum.node;const packet = reticulum.packet;const wire = reticulum.wire;const hash_bytes = reticulum.hash.truncated_bytes;/// The destination every path request is addressed to so any node recognizes/// the question without arranging anything in advance, which Reticulum@1.5.0/// RNS/Transport.py:347-351 names rnstransport.path.request.pub const destination_hash: [hash_bytes]u8 = hash: {    @setEvalBranchQuota(1_000_000);    break :hash destination.hash(        .{ .name_bytes_max = 64 },        "rnstransport",        &.{ "path", "request" },        null,    ) catch unreachable;};/// Three hashes for sizing a buffer to hold the largest payload a request can/// take, the room Reticulum@1.5.0 RNS/Transport.py:3220-3224 needs for a/// destination, a transport identity, and a tag.pub const payload_bytes_max: usize = 3 * hash_bytes;/// One second, the pause Reticulum@1.5.0 RNS/Transport.py:133,3425 leaves/// before replying from a path the node holds so questions arriving together/// share one answer.pub const response_delay: node.Seconds = 1;/// One parsed path request, which a caller reads after parsing to see what was/// asked and who asked. The tag points into the payload bytes the packet/// arrived in, so it lives exactly as long as they do.pub const Request = struct {    destination: [hash_bytes]u8,    requestor: ?[hash_bytes]u8,    tag: []const u8,    /// Hashes the destination and tag bytes into the key the node deduplicates    /// on, so the node recognizes the same request arriving again by another    /// route, following Reticulum@1.5.0 RNS/Transport.py:1755.    pub fn key(self: Request) packet.Hash {        std.debug.assert(self.tag.len >= 1);        std.debug.assert(self.tag.len <= hash_bytes);        var hasher = reticulum.hash.Hasher.init();        hasher.update(&self.destination);        hasher.update(self.tag);        return hasher.finalFull();    }};pub const ParseError = error{ Short, Tagless };const Answer = enum {    local,    known_path,    oversized,    silent,    discover,    engaged,    ignore,    /// Sorts the answers that settle a request from the ones that leave it    /// open, which Reticulum@1.5.0 RNS/Transport.py:3520-3524 uses to decide    /// when the gate goes.    fn answered(self: Answer) bool {        return switch (self) {            .local, .known_path, .oversized, .silent => true,            .discover, .engaged, .ignore => false,        };    }};/// Splits a path request payload so the caller gets the destination, the asker,/// and the tag, following Reticulum@1.5.0 RNS/Transport.py:1738-1753,3310-3330./// A payload shorter than one hash returns `error.Short`, and one of exactly a/// hash returns `error.Tagless`. A payload past two hashes carries the asker's/// transport identity, and the tag follows it.pub fn parse(payload: []const u8) ParseError!Request {    if (payload.len < hash_bytes) return error.Short;    if (payload.len == hash_bytes) return error.Tagless;    const transported = payload.len > 2 * hash_bytes;    const tag_start: usize = if (transported) 2 * hash_bytes else hash_bytes;    const tag_end = @min(payload.len, tag_start + hash_bytes);    std.debug.assert(tag_start < tag_end);    return .{        .destination = payload[0..hash_bytes].*,        .requestor = if (transported) payload[hash_bytes .. 2 * hash_bytes].* else null,        .tag = payload[tag_start..tag_end],    };}/// Answers whether an arriving packet is one of these requests so the receive/// path hands it over before ordinary delivery, which holds for a data packet/// of the plain kind carrying the well-known destination.pub fn addressed(value: wire.Packet) bool {    if (value.packet_type != .data) return false;    if (value.destination_type != .plain) return false;    return std.mem.eql(u8, &value.destination, &destination_hash);}/// Takes one arriving request through every check before the node commits to an/// answer, as Reticulum@1.5.0 RNS/Transport.py:1734-1797 does. A payload that/// fails to parse reports `path_request_malformed`. A request whose key the/// node already holds reports `path_request_duplicate`. A request for a/// destination the node is itself waiting on adds the asking carrier to the/// discovery and reports `path_request_batched`. The step reserves room for/// every frame the answer may send before it changes any table. The node stores/// the packet hash and the request key after the gate has chosen an answer, so/// a request turned away early leaves neither behind.pub fn receive(    node_owner: *node.Node,    value: wire.Packet,    interface: carrier.Index,    hash: packet.Hash,    now: node.Seconds,) node.StepError!void {    std.debug.assert(addressed(value));    const request = parse(value.payload) catch {        try node.outbound.reserve(node_owner, 1, 0);        node.inbound.diagnostic(node_owner, .path_request_malformed, hash);        return;    };    const state = &node_owner.transport;    const key = request.key();    if (state.tags.contains(key)) {        try node.outbound.reserve(node_owner, 1, 0);        node.inbound.diagnostic(node_owner, .path_request_duplicate, hash);        return;    }    if (state.inflight_requests.find(request.destination, now) != null) {        try node.outbound.reserve(node_owner, 1, 0);        _ = state.tags.insert(key);        state.discoveries.batch(request.destination, interface, now) catch {            node.inbound.diagnostic(node_owner, .discovery_table_full, hash);            return;        };        node.inbound.diagnostic(node_owner, .path_request_batched, hash);        return;    }    const answer = plan(node_owner, request, interface, now);    const forwards = if (answer == .discover)        node.outbound.frameCount(node_owner, .{ .all_except = interface })    else        0;    try node.outbound.reserve(node_owner, forwards + 1, forwards);    if (answer == .known_path and !retransmit.canSchedule(node_owner)) return error.TimerFull;    _ = state.tags.insert(key);    _ = node_owner.duplicate_hashes.insert(hash);    if (answer.answered()) {        _ = state.inflight_requests.remove(request.destination);    } else {        state.inflight_requests.insert(request.destination, now);    }    try respond(node_owner, answer, request, interface, hash, now);}/// Chooses the first answer that matches, following Reticulum@1.5.0/// RNS/Transport.py:3375-3518, so the node decides what to say about a/// destination. A destination this node owns is answered by the node itself/// when it is a single destination, and ignored otherwise. A node that carries/// no traffic for others ignores the request. A known path whose announce/// payload was too large to keep reports `announce_relay_too_large`. A known/// path at or past the pathfinder maximum is left unanswered, as is a path/// whose next hop is the asker. A destination with no path is passed on, unless/// the arriving carrier is configured to ask nothing or a discovery for it has/// already gone out.fn plan(    node_owner: *node.Node,    request: Request,    interface: carrier.Index,    now: node.Seconds,) Answer {    if (node_owner.destinations.find(request.destination)) |entry| {        return if (entry.kind == .single) .local else .ignore;    }    const state = &node_owner.transport;    if (!state.enabled) return .ignore;    if (state.paths.find(request.destination, now)) |path| {        if (path.announcePayload() == null) return .oversized;        if (path.hops >= wire.pathfinder_hops) return .silent;        const requestor = request.requestor orelse return .known_path;        if (std.mem.eql(u8, &requestor, &path.next_hop)) return .silent;        return .known_path;    }    if (node_owner.interfaces[interface].discover_paths == 0) return .ignore;    const waiting = state.discoveries.find(request.destination, now) orelse return .discover;    return if (waiting.engaged) .engaged else .discover;}fn respond(    node_owner: *node.Node,    answer: Answer,    request: Request,    interface: carrier.Index,    hash: packet.Hash,    now: node.Seconds,) node.StepError!void {    switch (answer) {        .local => node_owner.effects.push(.{ .path_request = .{            .destination = request.destination,            .interface = interface,        } }) catch unreachable,        .known_path => answerKnown(node_owner, request, interface, hash, now),        .discover => try discover(node_owner, request, interface, hash, now),        .oversized => node.inbound.diagnostic(node_owner, .announce_relay_too_large, hash),        .silent, .engaged, .ignore => {},    }}/// Queues an answer built from the announce the path kept, marked so the asker/// takes the path and passes it no further, as Reticulum@1.5.0/// RNS/Transport.py:3409-3444 does. The answer goes out on the carrier the/// question arrived on, one second later. A destination with a rebroadcast/// already queued keeps that record behind the answer. A full announce table/// reports `announce_table_full`.fn answerKnown(    node_owner: *node.Node,    request: Request,    interface: carrier.Index,    hash: packet.Hash,    now: node.Seconds,) void {    const state = &node_owner.transport;    const path = state.paths.find(request.destination, now) orelse unreachable;    const due = now +| response_delay;    const record = announces.Record.init(.{        .destination = request.destination,        .due = due,        .timestamp = now,        .retries = announces.retries_max,        .hops = path.hops,        .block_rebroadcasts = true,        .attached = interface,        .context_flag = path.context_flag,        .payload = path.announcePayload() orelse unreachable,    });    if (state.announces.find(request.destination)) |entry| {        entry.hold(record);    } else {        _ = state.announces.insert(record) catch {            node.inbound.diagnostic(node_owner, .announce_table_full, hash);            return;        };    }    retransmit.schedule(node_owner, due);}/// Engages discovery and forwards the same tag so the answer is recognized on/// the way back, following Reticulum@1.5.0 RNS/Transport.py:3478-3508. A full/// discovery table reports `discovery_table_full`. The forwarded request goes/// to every carrier but the one it arrived on.fn discover(    node_owner: *node.Node,    request: Request,    interface: carrier.Index,    hash: packet.Hash,    now: node.Seconds,) node.StepError!void {    node_owner.transport.discoveries.engage(request.destination, interface, now) catch {        node.inbound.diagnostic(node_owner, .discovery_table_full, hash);        return;    };    const fanout: node.outbound.Fanout = .{ .all_except = interface };    if (node.outbound.frameCount(node_owner, fanout) == 0) return;    const frame = encode(node_owner, request.destination, request.tag);    _ = node_owner.duplicate_hashes.insert(wire.hash.full(frame) catch unreachable);    try node.outbound.transmit(node_owner, fanout, frame);}/// Puts a request to the network carrying tag bytes the caller chose so an/// application asks for a path to a destination, as Reticulum@1.5.0/// RNS/Transport.py:3219-3250 does. A node with no carrier that sends returns/// `error.NoOutgoingCarrier`.pub fn send(node_owner: *node.Node, value: node.ApplicationPathRequest) node.StepError!void {    const fanout: node.outbound.Fanout = if (value.interface) |index| .{ .one = index } else .all;    const carriers = node.outbound.frameCount(node_owner, fanout);    if (carriers == 0) return error.NoOutgoingCarrier;    try node.outbound.reserve(node_owner, carriers, carriers);    const frame = encode(node_owner, value.destination, &value.tag);    _ = node_owner.duplicate_hashes.insert(wire.hash.full(frame) catch unreachable);    try node.outbound.transmit(node_owner, fanout, frame);}/// Twenty seconds, the least time Reticulum@1.5.0 RNS/Transport.py:135 leaves/// between two requests about one destination so queries for a failing/// destination remain spaced. The second of the last request comes from the/// gate itself, because this port writes down nothing else about the requests/// it sends.pub const rediscovery_interval: node.Seconds = 20;fn throttled(node_owner: *const node.Node, requested: [hash_bytes]u8, now: node.Seconds) bool {    const gate = node_owner.transport.inflight_requests.find(requested, now) orelse return false;    return now -| gate.timestamp < rediscovery_interval;}/// Counts how many frames asking after this destination again would put out at/// this second so a caller reserves effects before calling `rediscover`,/// because the reservation has to happen before anything changes.pub fn rediscoveryFrames(    node_owner: *const node.Node,    requested: [hash_bytes]u8,    now: node.Seconds,) usize {    if (throttled(node_owner, requested, now)) return 0;    return node.outbound.frameCount(node_owner, .all);}/// Drops the path to the destination and asks every carrier for a new one,/// following Reticulum@1.5.0 RNS/Transport.py:676-695, so a caller whose link/// failed throws away what the node knew and asks again. The path goes whether/// the request travels or the spacing holds it back, which leaves the/// destination open to a later announce from any distance. The caller reserves/// the frames that `rediscoveryFrames` counts.pub fn rediscover(    node_owner: *node.Node,    requested: [hash_bytes]u8,    tag: *const [hash_bytes]u8,    now: node.Seconds,) node.StepError!void {    _ = node_owner.transport.paths.expire(requested, now);    if (throttled(node_owner, requested, now)) return;    if (node.outbound.frameCount(node_owner, .all) == 0) return;    const frame = encode(node_owner, requested, tag);    _ = node_owner.duplicate_hashes.insert(wire.hash.full(frame) catch unreachable);    node_owner.transport.inflight_requests.insert(requested, now);    try node.outbound.transmit(node_owner, .all, frame);}/// Builds a HEADER_1 PLAIN request frame for every path request this node sends/// or passes on, following Reticulum@1.5.0 RNS/Transport.py:3219-3227. A node/// that carries traffic for others puts its transport identity between the/// destination and the tag.fn encode(node_owner: *node.Node, requested: [hash_bytes]u8, tag: []const u8) []const u8 {    std.debug.assert(tag.len >= 1);    std.debug.assert(tag.len <= hash_bytes);    var payload: [payload_bytes_max]u8 = undefined;    @memcpy(payload[0..hash_bytes], &requested);    var length: usize = hash_bytes;    if (node_owner.transport.enabled) {        const transport_hash = node_owner.transport.identity_hash orelse unreachable;        @memcpy(payload[length..][0..hash_bytes], &transport_hash);        length += hash_bytes;    }    @memcpy(payload[length..][0..tag.len], tag);    length += tag.len;    return wire.encode(.{        .ifac = 0,        .header = .one,        .context_flag = 0,        .transport = .broadcast,        .destination_type = .plain,        .packet_type = .data,        .hops = 0,        .transport_id = null,        .destination = destination_hash,        .context = .none,        .payload = payload[0..length],    }, node_owner.scratch) catch unreachable;}fn answersWaiting(node_owner: *const node.Node, value: wire.Packet) bool {    if (node_owner.transport.identity_hash == null) return false;    if (value.hops >= wire.pathfinder_hops) return false;    return value.payload.len <= destination.announce.payload_bytes_max;}/// Counts how many frames replying to the carriers queued behind this announce/// would put out so a caller reserves effects before accepting an announce,/// because answering the waiting carriers happens in the same step.pub fn waitingFrames(node_owner: *node.Node, value: wire.Packet, now: node.Seconds) usize {    std.debug.assert(value.packet_type == .announce);    if (!answersWaiting(node_owner, value)) return 0;    const waiting = node_owner.transport.discoveries.find(value.destination, now) orelse return 0;    var frames: usize = 0;    var requesters = waiting.requesters.iterator(.{});    while (requesters.next()) |index| {        frames += node.outbound.frameCount(node_owner, .{ .one = @intCast(index) });    }    return frames;}/// Answers waiting carriers at once so one announce replies to every carrier/// that asked about its destination, following Reticulum@1.5.0/// RNS/Transport.py:2347-2371. The answer carries this node's transport/// identity and is marked a path response. An announce at or past the/// pathfinder maximum, or one whose payload is too large to relay, answers/// nobody.pub fn answerWaiting(    node_owner: *node.Node,    value: wire.Packet,    now: node.Seconds,) node.StepError!void {    std.debug.assert(value.packet_type == .announce);    const waiting = node_owner.transport.discoveries.take(value.destination, now) orelse return;    if (!answersWaiting(node_owner, value)) return;    const frame = wire.encode(.{        .ifac = 0,        .header = .two,        .context_flag = value.context_flag,        .transport = .transport,        .destination_type = .single,        .packet_type = .announce,        .hops = value.hops,        .transport_id = node_owner.transport.identity_hash,        .destination = value.destination,        .context = .path_response,        .payload = value.payload,    }, node_owner.scratch) catch unreachable;    _ = node_owner.duplicate_hashes.insert(wire.hash.full(frame) catch unreachable);    var requesters = waiting.requesters.iterator(.{});    while (requesters.next()) |index| {        try node.outbound.transmit(node_owner, .{ .one = @intCast(index) }, frame);    }}test "Reticulum@1.5.0 RNS/Transport.py:1738-1753 parses every request payload length class" {    var payload: [64]u8 = undefined;    for (&payload, 0..) |*byte, index| byte.* = @intCast(index);    try std.testing.expectError(error.Short, parse(payload[0..15]));    try std.testing.expectError(error.Tagless, parse(payload[0..16]));    const one = try parse(payload[0..17]);    try std.testing.expect(one.requestor == null);    try std.testing.expectEqualSlices(u8, payload[16..17], one.tag);    const endpoint = try parse(payload[0..32]);    try std.testing.expect(endpoint.requestor == null);    try std.testing.expectEqualSlices(u8, payload[16..32], endpoint.tag);    const short_tag = try parse(payload[0..33]);    try std.testing.expectEqualSlices(u8, payload[16..32], &short_tag.requestor.?);    try std.testing.expectEqualSlices(u8, payload[32..33], short_tag.tag);    const transported = try parse(payload[0..48]);    try std.testing.expectEqualSlices(u8, payload[32..48], transported.tag);    const long = try parse(payload[0..64]);    try std.testing.expectEqualSlices(u8, payload[0..16], &long.destination);    try std.testing.expectEqualSlices(u8, payload[32..48], long.tag);}test "Reticulum@1.5.0 RNS/Transport.py:1755 keys a tag the same across request shapes" {    var endpoint: [32]u8 = @splat(0x11);    var transported: [48]u8 = @splat(0x11);    @memset(transported[16..32], 0x22);    const first = (try parse(&endpoint)).key();    const second = (try parse(&transported)).key();    try std.testing.expectEqualSlices(u8, &first, &second);    endpoint[31] = 0x12;    const changed = (try parse(&endpoint)).key();    try std.testing.expect(!std.mem.eql(u8, &first, &changed));}

Source: lib/reticulum/src/node/transport/root.zig:109

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

Complete call list for node.transport.requests.receive

8 direct calls.

Audit

Definitions16
Public names16
Members5
Version26.7.0
Revisiondaab053ee433