tiny.reticulum.node.transport.links
Defined in node.transport.
API (22)
Actions
Public operations.
Entry.zero: Writes zero over every byte of the record, key material included, so a closed session leaves no key material behind in the pool.Table.activateTable.countTable.deinitTable.find: Looks one link id up in the pool, answering null when the id is absent, so a caller finds the session an arriving packet belongs to.Table.full: Returns whether the pool holdslinks_maxlinks, so a caller asks before it opens or accepts a session, because a full pool refuses one.Table.initTable.insert: Writes a link at the end of the pool and hands back a pointer to it, so a caller records a session it has opened or accepted.Table.remove: Takes one link id out of the pool and answers whether the id was present, so a caller drops a session that closed.
Types and contracts
Public types and contracts.
Entry: Records one link this node holds an end of, so a caller sees the keys and the instants that decide a session's deadlines.Role: Distinguishes which of a link's two ends this node is, so a caller tells which end of a session this node holds, because the two ends answer for different things.SecondsStatus: Tracks how far along a link in the pool is, so a caller knows whether a session may carry traffic yet and how close it is to closing.Table: Provides room forlinks_maxlinks this node holds an end of, carved from bytes the caller supplies.Table.CapacityTable.ExhaustionTable.InitErrorTable.LimitsTable.Storage
Values and defaults
Public values and defaults.
Source
Source: lib/reticulum/src/node/transport/links.zig
zig
const std = @import("std");const alloc_phase = @import("alloc_phase");const carrier = @import("../../carrier/root.zig");const ProofStrategy = @import("../../destination/root.zig").registry.ProofStrategy;pub const Seconds = u64;/// Distinguishes which of a link's two ends this node is, so a caller tells/// which end of a session this node holds, because the two ends answer for/// different things. Reticulum@1.5.0 RNS/Link.py:274-280 carries the same/// distinction on its own object.pub const Role = enum(u1) { initiator, responder,};/// Tracks how far along a link in the pool is, so a caller knows whether a/// session may carry traffic yet and how close it is to closing. The end that/// opened a link waits at `pending` until a proof checks out. Silence lasting/// twice the keepalive period moves a link to `stale`, and five seconds more of/// it closes the link. One packet arriving over the link's own carrier brings/// the link back to `active`, as Reticulum@1.5.0/// RNS/Link.py:110-113,753-766,938-946 arranges.pub const Status = enum(u2) { pending, handshake, active, stale,};/// Records one link this node holds an end of, so a caller sees the keys and/// the instants that decide a session's deadlines. On an initiator, the/// ephemeral Ed25519 private key stays and the X25519 one is wiped at the/// moment the link activates. On a responder, both private key fields read as/// zero throughout. The instants of the last inbound packet, the last outbound/// packet, the last keepalive sent, and the last validated data proof only move/// forward, and they fix the keepalive and stale deadlines. The closing second/// carries a value only while a link sits at `stale`, and reads zero at every/// other moment, as Reticulum@1.5.0 RNS/Link.py:245-249,744-766 arranges.pub const Entry = struct { id: [16]u8, destination: [16]u8, encryption_private: [32]u8, signing_private: [32]u8, peer_signing_public: [32]u8, derived_key: [64]u8, request_time: Seconds, establishment_timeout: Seconds, activated_at: Seconds, last_inbound: Seconds, last_outbound: Seconds, last_keepalive: Seconds, last_proof: Seconds, close_at: Seconds, rtt: f64, expected_hops: u8, attached: carrier.Index, role: Role, status: Status, rebalanced: bool, proof_strategy: ProofStrategy, /// Writes zero over every byte of the record, key material included, so a /// closed session leaves no key material behind in the pool. pub fn zero(self: *Entry) void { std.crypto.secureZero(u8, std.mem.asBytes(self)); }};const TableLimits = struct { links_max: usize,};const TableCapacity = struct { links_max: usize, storage_bytes: usize, pub const DeriveError = error{ InvalidLimit, CapacityOverflow }; pub fn derive(limits: TableLimits) DeriveError!TableCapacity { if (limits.links_max == 0) return error.InvalidLimit; const storage_bytes = alloc_phase.capacity.mul( usize, limits.links_max, @sizeOf(Entry), ) catch return error.CapacityOverflow; return .{ .links_max = limits.links_max, .storage_bytes = storage_bytes }; }};/// Provides room for `links_max` links this node holds an end of, carved from/// bytes the caller supplies. Links sit in the order they were written, and one/// link id appears at most once.pub const Table = struct { phase: alloc_phase.capacity.Phase, capacity: Capacity, storage: Storage, entries: []Entry, len: usize = 0, pub const storage_alignment: usize = 8; pub const Storage = []align(storage_alignment) u8; pub const Limits: type = TableLimits; pub const Capacity: type = TableCapacity; pub const Exhaustion = error{Full}; pub const InitError = Capacity.DeriveError || error{StorageLengthMismatch}; pub const work_limits: alloc_phase.capacity.WorkLimits = .{ .transition_steps_max = 65_536, .cleanup_steps_per_call_max = 0, .cleanup_calls_at_capacity_max = 0, }; pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "reticulum.links", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{.{ .id = "caller_link_table", .lifetime = .transferred, .detail = "caller storage for open link endpoints and their keys", }}, .excluded = &.{ "link frames in effect storage", "local destination identities", }, }, .capacity = .{ .inputs = &.{alloc_phase.capacity.bindInput( Limits, "links_max", "links_max", )}, .type_selectors = &.{alloc_phase.capacity.bindType(Entry, "link")}, .nodes = &.{ .{ .input = 0 }, .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 }, } }, }, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 1, }}, }, .overload = .{ .kind = .reject_before_mutation, .detail = "a full link table rejects a new link and keeps every open link", }, .risks = .{ .transitive = .{ .status = .excluded, .detail = "link operations call no allocating owner", }, .foreign = .{ .status = .excluded, .detail = "link storage crosses no foreign boundary", }, }, .work = .{ .equation = "link operations scan at most links_max entries" }, .obligations = &.{ .{ .key = "reticulum_links_capacity", .role = .capacity_model }, .{ .key = "reticulum_links_overload", .role = .overload }, .{ .key = "reticulum_links_work", .role = .work_bound }, }, }, .bindings = .{ .owner = @This(), .seal = .{ .family = alloc_phase.capacity.selector(@This().activate), .premise = .{ .class = .checked_semantic_fact, .authority = .checker }, }, .teardown = .{ .family = alloc_phase.capacity.selector(@This().deinit), .premise = .{ .class = .checked_semantic_fact, .authority = .checker }, }, }, }; pub fn init(storage: Storage, limits: Limits) InitError!Table { const capacity = try Capacity.derive(limits); if (storage.len != capacity.storage_bytes) return error.StorageLengthMismatch; const entries = std.mem.bytesAsSlice(Entry, storage); std.debug.assert(entries.len == capacity.links_max); for (entries) |*entry| entry.zero(); return .{ .phase = .initialization, .capacity = capacity, .storage = storage, .entries = entries, }; } pub fn activate(self: *Table) void { std.debug.assert(self.phase == .initialization); std.debug.assert(self.len == 0); self.phase = .steady; } /// Looks one link id up in the pool, answering null when the id is absent, /// so a caller finds the session an arriving packet belongs to. The lookup /// asserts along the way that the id matched at most one link. pub fn find(self: *Table, id: [16]u8) ?*Entry { std.debug.assert(self.phase == .steady); std.debug.assert(self.len <= self.capacity.links_max); var found: ?*Entry = null; for (self.entries[0..self.len]) |*entry| { if (!std.mem.eql(u8, &entry.id, &id)) continue; std.debug.assert(found == null); found = entry; } return found; } /// Returns whether the pool holds `links_max` links, so a caller asks /// before it opens or accepts a session, because a full pool refuses one. pub fn full(self: *const Table) bool { std.debug.assert(self.phase == .steady); std.debug.assert(self.len <= self.capacity.links_max); return self.len == self.capacity.links_max; } /// Writes a link at the end of the pool and hands back a pointer to it, so /// a caller records a session it has opened or accepted. The write asserts /// that the link id is new to the pool. A pool already holding `links_max` /// links returns `error.Full` and leaves every link as it was. pub fn insert(self: *Table, entry: Entry) Exhaustion!*Entry { std.debug.assert(self.phase == .steady); std.debug.assert(self.find(entry.id) == null); if (self.full()) return error.Full; self.entries[self.len] = entry; self.len += 1; std.debug.assert(self.len <= self.capacity.links_max); return &self.entries[self.len - 1]; } /// Takes one link id out of the pool and answers whether the id was /// present, so a caller drops a session that closed. Links written after it /// move down one place, and the freed place is written over with zeros. pub fn remove(self: *Table, id: [16]u8) bool { std.debug.assert(self.phase == .steady); for (self.entries[0..self.len], 0..) |entry, index| { if (!std.mem.eql(u8, &entry.id, &id)) continue; std.mem.copyForwards( Entry, self.entries[index .. self.len - 1], self.entries[index + 1 .. self.len], ); self.len -= 1; self.entries[self.len].zero(); std.debug.assert(self.len < self.capacity.links_max); std.debug.assert(self.find(id) == null); return true; } return false; } pub fn count(self: *const Table) usize { std.debug.assert(self.phase == .steady); std.debug.assert(self.len <= self.capacity.links_max); return self.len; } pub fn deinit(self: *Table) Storage { std.debug.assert(self.phase == .steady); std.debug.assert(self.len <= self.capacity.links_max); for (self.entries[0..self.len]) |*entry| entry.zero(); self.phase = .teardown; const storage = self.storage; self.* = undefined; return storage; }};comptime { alloc_phase.capacity.requireProvisionedRejectingOwnerShape(Table);}fn entryFor(byte: u8) Entry { return .{ .id = @splat(byte), .destination = @splat(0xd0), .encryption_private = @splat(0xa1), .signing_private = @splat(0xa2), .peer_signing_public = @splat(0xa4), .derived_key = @splat(0xa5), .request_time = 100, .establishment_timeout = 366, .activated_at = 0, .last_inbound = 100, .last_outbound = 100, .last_keepalive = 0, .last_proof = 0, .close_at = 0, .rtt = 0, .expected_hops = 1, .attached = 0, .role = .responder, .status = .handshake, .rebalanced = false, .proof_strategy = .none, };}test "links admit maximum and reject maximum plus one" { comptime { @stardustClaim(alloc_phase.capacity.witness( Table, "reticulum_links_capacity", ), null, null, null, null, null, null); @stardustClaim(alloc_phase.capacity.witness( Table, "reticulum_links_overload", ), null, null, null, null, null, null); @stardustClaim(alloc_phase.capacity.witness( Table, "reticulum_links_work", ), null, null, null, null, null, null); } const capacity = comptime TableCapacity.derive(.{ .links_max = 3 }) catch unreachable; var bytes: [capacity.storage_bytes]u8 align(Table.storage_alignment) = undefined; var table = try Table.init(&bytes, .{ .links_max = 3 }); table.activate(); defer _ = table.deinit(); for (1..4) |value| _ = try table.insert(entryFor(@intCast(value))); try std.testing.expect(table.full()); try std.testing.expectError(error.Full, table.insert(entryFor(4))); try std.testing.expectEqual(@as(usize, 3), table.count()); for (1..4) |value| try std.testing.expect(table.find(@splat(@intCast(value))) != null); try std.testing.expect(table.find(@splat(4)) == null);}test "link removal keeps order and zeroes the vacated slot" { const capacity = comptime TableCapacity.derive(.{ .links_max = 3 }) catch unreachable; var bytes: [capacity.storage_bytes]u8 align(Table.storage_alignment) = undefined; var table = try Table.init(&bytes, .{ .links_max = 3 }); table.activate(); defer _ = table.deinit(); for (1..4) |value| _ = try table.insert(entryFor(@intCast(value))); try std.testing.expect(table.remove(@splat(1))); try std.testing.expect(!table.remove(@splat(1))); try std.testing.expectEqual(@as(usize, 2), table.count()); try std.testing.expectEqual(@as(u8, 2), table.entries[0].id[0]); try std.testing.expectEqual(@as(u8, 3), table.entries[1].id[0]); try std.testing.expect(std.mem.allEqual(u8, std.mem.asBytes(&table.entries[2]), 0)); _ = try table.insert(entryFor(4)); try std.testing.expect(table.full());}test "link capacity rejects zero and overflowing limits" { try std.testing.expectError(error.InvalidLimit, TableCapacity.derive(.{ .links_max = 0 })); const overflowing = std.math.maxInt(usize) / @sizeOf(Entry) + 1; try std.testing.expectError( error.CapacityOverflow, TableCapacity.derive(.{ .links_max = overflowing }), );}Source: lib/reticulum/src/node/transport/root.zig:106
zig
pub const links = @import("links.zig");Audit
| Definitions | 23 |
|---|---|
| Public names | 23 |
| Members | 33 |
| Version | 26.7.0 |
| Revision | daab053ee433 |