tiny.reticulum.node.transport.requests
Defined in node.transport.
API (15)
Actions
Public operations.
Request.key: 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.addressed: 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.answerWaiting: 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.parse: 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.receive: 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.rediscover: 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.rediscoveryFrames: Counts how many frames asking after this destination again would put out at this second so a caller reserves effects before callingrediscover, because the reservation has to happen before anything changes.send: 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.waitingFrames: 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.
Types and contracts
Public types and contracts.
ParseErrorRequest: One parsed path request, which a caller reads after parsing to see what was asked and who asked.
Values and defaults
Public values and defaults.
destination_hash: 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.payload_bytes_max: 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.rediscovery_interval: 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.response_delay: 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.
Source
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.
tiny.reticulum.node.inbound.diagnostic[function] atlib/reticulum/src/node/inbound.zig:12tiny.reticulum.node.outbound.frameCount[function] atlib/reticulum/src/node/outbound.zig:175tiny.reticulum.node.outbound.reserve[function] atlib/reticulum/src/node/outbound.zig:184tiny.reticulum.node.transport.requests.addressed[function] atlib/reticulum/src/node/transport/requests.zig:102tiny.reticulum.node.transport.requests.parse[function] atlib/reticulum/src/node/transport/requests.zig:85lib.reticulum.src.node.transport.requests.plan[function] — private source atlib/reticulum/src/node/transport/requests.zig:174in nearest public ownertiny.reticulum.node.transport.requestslib.reticulum.src.node.transport.requests.respond[function] — private source atlib/reticulum/src/node/transport/requests.zig:197in nearest public ownertiny.reticulum.node.transport.requeststiny.reticulum.node.transport.retransmit.canSchedule[function] atlib/reticulum/src/node/transport/retransmit.zig:47
Audit
| Definitions | 16 |
|---|---|
| Public names | 16 |
| Members | 5 |
| Version | 26.7.0 |
| Revision | daab053ee433 |