tiny.reticulum.node
Defined in tiny.reticulum.
A state machine for one node of a mesh network holds everything the node remembers and turns one input at a time into a list of things for the program around it to do.
API (73)
Actions
Public operations.
Capacity.deriveEffects.activateEffects.deinitEffects.initEffects.itemsEffects.pushEffects.resetNode.activateNode.bindRatchetRing: Binds a rotating-key ring to one of the node's own identities, so announces from that identity carry a rotating public key and data addressed to it is decrypted with the ring's keys first.Node.deinitNode.identityAtNode.initNode.registerCarrier: Registers one carrier index, so the node accepts frames that arrive on it and, withoutgoingset, sends on it as well.Node.setPathDiscovery: Lets the node ask one carrier for a path it lacks, which Reticulum@1.5.0 RNS/Transport.py:3352-3355 searches.Node.setProofMode: Chooses whether a proof this node sends carries the proved packet's hash beside the signature or the signature alone.Node.setTransport: Turns the node into one that carries traffic for others and gives it the 16 bytes that name it to its neighbors, which Reticulum@1.5.0 RNS/Transport.py:1577-1580,1907 sets.Node.step: Applies one event and returns the effects it produced.
Types and contracts
Public types and contracts.
Access: The shared key and code length that authenticate every frame on one carrier, for a caller that runs a closed network and hands one of these toregisterCarrier.ApplicationAnnounceApplicationLinkClose: Asks the node to take down one link it holds open, so a caller can close a link once done with it.ApplicationLinkOpen: Asks the node to open a link toward a single destination it holds an identity for, learned from an announce, for an encrypted session that outlives a single packet.ApplicationLinkProve: Asks the node to answer one packet of link data it has handed up with a proof, so a caller under the.appstrategy can answer a delivery that asked to be proved.ApplicationLinkSend: Asks the node to put application plaintext across a link that has activated, so the caller can send application bytes over the open link.ApplicationPathRequest: Asks any node that knows a path to the named destination to answer with one, carrying the caller's own tag, for a caller that has yet to learn a route to that destination, as Reticulum@1.5.0 RNS/Transport.py:3207-3220 requests it.ApplicationProveApplicationSendCapacityCapacity.DeriveErrorCarrierFrameCodeEffectEffectsEffects.CapacityEffects.ExhaustionEffects.InitErrorEffects.LimitsEffects.StorageEventInterface: What the node remembers about one carrier, so a caller reads one of these to see what the node believes about a carrier it registered: that it is registered, whether the node may send on it, whether it searches for paths it lacks, and its access code, if it has one.LimitsLinkCloseReason: Records why a link closed: a deadline passed, the initiator closed it, or the responder closed it.LinkRoleNodeNode.CapacityNode.InitErrorNode.LimitsNode.StoragePersistKindPersistTokenProofModeRatchetBindingSecondsStepErrorStorageCompleteTimerExpiredTimerId
Namespaces
Public namespaces.
announcefixture: Three nodes of a mesh network are held inside one value, which runs them from a single clock and can lose, delay, reorder, or cut off the traffic between them.inboundlink: 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.outboundtimertransitiontransport: The state and the steps a node needs to work out where destinations are from the broadcasts it hears, and to move traffic toward them.
Values and defaults
Public values and defaults.
Effects.claimEffects.storage_alignmentEffects.work_limitsLimits.reference: One set of limits that pairs the reference receipt, hash, and tag maxima above with a single entry in every other table.Node.claimNode.storage_alignmentNode.work_limitseffect_frames_per_event_max: Gives the most byte slices one event's effects retain, so a caller sizes the effect list's frame storage from this bound.effects_per_event_max: Gives the most effects one event can produce, so a caller sizes its effect storage from this bound.
Source
Source: lib/reticulum/src/node/access.zig:7
/// The shared key and code length that authenticate every frame on one carrier, for a caller that/// runs a closed network and hands one of these to `registerCarrier`. The node appends the code to/// each frame it sends on that carrier and checks and strips it from each frame it receives. The/// key is 64 bytes and the code runs from 1 to 64 bytes.pub const Access = struct { key: ifac.Key, size: ifac.Size,};Source: lib/reticulum/src/node/access.zig:17
/// What the node remembers about one carrier, so a caller reads one of these to see what the node/// believes about a carrier it registered: that it is registered, whether the node may send on it,/// whether it searches for paths it lacks, and its access code, if it has one. The node holds one/// of these for every carrier index its limits allow, in storage the caller supplied. A carrier/// with no access code carries its frames as they are.pub const Registration = struct { registered: u1 = 0, outgoing: u1 = 0, /// Whether the node asks this carrier for a path it lacks, which Reticulum@1.5.0 /// RNS/Interfaces/Interface.py:55,121 leaves off, so a caller turns this on for the carrier it /// wants the node to search when it has no path to a destination. Registering the carrier again /// returns the setting to off. discover_paths: u1 = 0, access: ?Access = null,};Source: lib/reticulum/src/node/effect.zig:15
pub const Code = enum(u8) { unregistered_interface, frame_too_large, ifac_unexpected_flag, ifac_missing_flag, ifac_invalid_code, ifac_truncated, malformed_packet, duplicate_packet, packet_filtered, no_path, path_request_malformed, path_request_duplicate, path_request_batched, invalid_announce, own_announce, announce_key_changed, announce_table_full, discovery_table_full, announce_relay_too_large, unknown_destination, destination_type_mismatch, decryption_failed, proof_rejected, proof_relay_wrong_interface, links_unsupported, links_full, link_request_invalid, link_request_duplicate, unknown_link, link_state_mismatch, link_wrong_interface, link_close_invalid, link_entries_full, link_relay_early, link_relay_no_direction, unsupported_context, storage_failed,};Source: lib/reticulum/src/node/effect.zig:129
pub const Effect = union(enum) { carrier_send: struct { interface: carrier.Index, frame: []const u8, }, application_delivery: struct { destination: [16]u8, packet_hash: [32]u8, plaintext: []const u8, ratchet_id: ?[10]u8, proof_requested: bool, interface: carrier.Index, }, announce_received: AnnounceReceived, path_request: PathRequest, receipt_update: struct { packet_hash: packet.Hash, status: packet.receipt.Status, rtt: ?timer.Seconds, }, persist: struct { kind: PersistKind, key: [16]u8, bytes: []const u8, token: event.PersistToken, }, schedule_timer: struct { id: timer.TimerId, at: timer.Seconds, }, diagnostic: struct { code: Code, packet_hash: ?[32]u8, }, link_requested: LinkRequested, link_established: LinkEstablished, link_delivery: LinkDelivery, link_closed: LinkClosed,};Source: lib/reticulum/src/node/effect.zig:223
pub const Effects = struct { phase: alloc_phase.capacity.Phase, capacity: Capacity, storage: Storage, entries: []Effect, frames: []carrier.Frame, len: usize = 0, frames_used: usize = 0, pub const storage_alignment: usize = 8; pub const Storage = []align(storage_alignment) u8; pub const Limits: type = EffectsLimits; pub const Capacity: type = EffectsCapacity; pub const Exhaustion = error{Full}; pub const InitError = Capacity.DeriveError || error{StorageLengthMismatch}; pub const work_limits: alloc_phase.capacity.WorkLimits = .{ .transition_steps_max = 2, .cleanup_steps_per_call_max = 0, .cleanup_calls_at_capacity_max = 0, }; pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "reticulum.effects", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{ .{ .id = "caller_effect_entries", .lifetime = .transferred, .detail = "caller storage for bounded effect values", }, .{ .id = "caller_effect_frames", .lifetime = .transferred, .detail = "caller storage for bounded inline effect frame copies", }, }, .excluded = &.{ "event input slices", "carrier devices, application callbacks, and persistence state", }, }, .capacity = .{ .inputs = &.{ alloc_phase.capacity.bindInput(EffectsLimits, "effects_max", "effects_max"), alloc_phase.capacity.bindInput(EffectsLimits, "frames_max", "frames_max"), }, .type_selectors = &.{ alloc_phase.capacity.bindType(Effect, "effect"), alloc_phase.capacity.bindType(carrier.Frame, "frame"), }, .nodes = &.{ .{ .input = 0 }, .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 }, } }, .{ .input = 1 }, .{ .scale = .{ .node = 2, .coefficient = .{ .size_of_concrete_type = 1 }, } }, .{ .add = .{ .left = 1, .right = 3 } }, }, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 4, }}, }, .overload = .{ .kind = .reject_before_mutation, .detail = "full effect or frame admission preserves every retained effect", }, .risks = .{ .transitive = .{ .status = .excluded, .detail = "effect copying calls no allocating owner", }, .foreign = .{ .status = .excluded, .detail = "the effect sink crosses no foreign boundary", }, }, .work = .{ .equation = "push copies at most one frame and one effect" }, .obligations = &.{ .{ .key = "reticulum_effects_capacity", .role = .capacity_model }, .{ .key = "reticulum_effects_overload", .role = .overload }, .{ .key = "reticulum_effects_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!Effects { const capacity = try Capacity.derive(limits); if (storage.len != capacity.storage_bytes) return error.StorageLengthMismatch; const entries = std.mem.bytesAsSlice(Effect, storage[0..capacity.effect_bytes]); const frame_storage: []align(@alignOf(carrier.Frame)) u8 = @alignCast( storage[capacity.effect_bytes..], ); const frames = std.mem.bytesAsSlice(carrier.Frame, frame_storage); for (frames) |*frame| frame.* = .{}; return .{ .phase = .initialization, .capacity = capacity, .storage = storage, .entries = entries, .frames = frames, }; } pub fn activate(self: *Effects) void { std.debug.assert(self.phase == .initialization); std.debug.assert(self.len == 0); std.debug.assert(self.frames_used == 0); self.phase = .steady; } pub fn push(self: *Effects, value: Effect) Exhaustion!void { std.debug.assert(self.phase == .steady); if (self.len == self.capacity.effects_max) return error.Full; const retained: Effect = switch (value) { .carrier_send => |send| .{ .carrier_send = .{ .interface = send.interface, .frame = try self.retain(send.frame), } }, .application_delivery => |delivery| .{ .application_delivery = .{ .destination = delivery.destination, .packet_hash = delivery.packet_hash, .plaintext = try self.retain(delivery.plaintext), .ratchet_id = delivery.ratchet_id, .proof_requested = delivery.proof_requested, .interface = delivery.interface, } }, .announce_received => |announce| .{ .announce_received = .{ .destination_hash = announce.destination_hash, .identity_hash = announce.identity_hash, .public_key = announce.public_key, .app_data = try self.retain(announce.app_data), .hops = announce.hops, .rotating_key_present = announce.rotating_key_present, .path_response = announce.path_response, } }, .link_delivery => |delivery| .{ .link_delivery = .{ .link_id = delivery.link_id, .packet_hash = delivery.packet_hash, .plaintext = try self.retain(delivery.plaintext), .proof_requested = delivery.proof_requested, } }, .persist => |persist| .{ .persist = .{ .kind = persist.kind, .key = persist.key, .bytes = try self.retain(persist.bytes), .token = persist.token, } }, .receipt_update, .schedule_timer, .diagnostic, .path_request, .link_requested, .link_established, .link_closed, => value, }; self.entries[self.len] = retained; self.len += 1; } pub fn items(self: *const Effects) []const Effect { std.debug.assert(self.phase == .steady); return self.entries[0..self.len]; } pub fn reset(self: *Effects) void { std.debug.assert(self.phase == .steady); self.len = 0; self.frames_used = 0; } pub fn deinit(self: *Effects) Storage { std.debug.assert(self.phase == .steady); self.phase = .teardown; const storage = self.storage; self.* = undefined; return storage; } fn retain(self: *Effects, bytes: []const u8) Exhaustion![]const u8 { if (self.frames_used == self.capacity.frames_max) return error.Full; const frame = carrier.Frame.init(bytes) catch return error.Full; self.frames[self.frames_used] = frame; const retained = self.frames[self.frames_used].slice(); self.frames_used += 1; return retained; }};Source: lib/reticulum/src/node/effect.zig:58
/// Records why a link closed: a deadline passed, the initiator closed it, or the responder closed/// it. Reticulum@1.5.0 RNS/Link.py:116-118 names the same three teardown reasons `TIMEOUT`,/// `INITIATOR_CLOSED`, and `DESTINATION_CLOSED`.pub const LinkCloseReason = enum(u8) { timeout = 1, initiator_closed = 2, destination_closed = 3,};Source: lib/reticulum/src/node/effect.zig:9
pub const PersistKind = enum(u8) { known_identity, known_ratchet, ratchet_ring,};Source: lib/reticulum/src/node/error.zig:1
pub const Error = error{ EffectsFull, TimerFull, NoOutgoingCarrier, UnknownDestination, InvalidDestination, InvalidHops, PacketTooLarge, EncryptionFailed, AppDataTooLong, MissingEntropy, TimestampOutOfRange, ProofUnavailable, LinksFull, DuplicateLink, UnknownLink, LinkNotEstablished,};Source: lib/reticulum/src/node/event.zig:43
pub const ApplicationAnnounce = struct { destination: [16]u8, app_data: []const u8, random: [5]u8, fresh_rotating_key: ?[32]u8, now: timer.Seconds, /// The carrier to answer a path request on, which a caller sets on an announce so /// Reticulum@1.5.0 RNS/Transport.py:3379-3382 answers on that carrier alone. Left null, the /// announce goes out on every outgoing carrier. path_response: ?carrier.Index = null,};Source: lib/reticulum/src/node/event.zig:111
/// Asks the node to take down one link it holds open, so a caller can close a link once done with/// it. An encrypted LINKCLOSE goes out, the link leaves the pool, and the caller hears that it/// closed. A link that has yet to activate drops at once with no packet sent. The reason is/// `initiator_closed` on an initiator and `destination_closed` on a responder, per Reticulum@1.5.0/// RNS/Link.py:657-672.pub const ApplicationLinkClose = struct { link_id: [16]u8, iv: [16]u8, now: timer.Seconds,};Source: lib/reticulum/src/node/event.zig:77
/// Asks the node to open a link toward a single destination it holds an identity for, learned from/// an announce, for an encrypted session that outlives a single packet. `encryption_private` and/// `signing_private` hold the X25519 and Ed25519 private keys the initiator uses for this session/// alone, and Reticulum@1.5.0 RNS/Link.py:284-285 draws both at random.pub const ApplicationLinkOpen = struct { destination: [16]u8, encryption_private: [32]u8, signing_private: [32]u8, now: timer.Seconds, /// What the node does with data delivered over this link, chosen here for an initiator link /// because it has no registered destination to read it from. Under `.none` the node proves /// nothing, under `.all` it proves every delivered packet at once, and under `.app` it reports /// each delivery with a proof request that a link proof event answers. By contrast, a responder /// reads this from its destination, per Reticulum@1.5.0 RNS/Link.py:958-968. proof_strategy: ProofStrategy = .none,};Source: lib/reticulum/src/node/event.zig:122
/// Asks the node to answer one packet of link data it has handed up with a proof, so a caller under/// the `.app` strategy can answer a delivery that asked to be proved. The application names the/// packet hash from a delivery whose `proof_requested` was set, and the node sends that hash and a/// signature over it as link plaintext. A packet the node has already forgotten returns/// `error.ProofUnavailable`, per Reticulum@1.5.0 RNS/Link.py:378-389.pub const ApplicationLinkProve = struct { link_id: [16]u8, packet_hash: [32]u8, now: timer.Seconds,};Source: lib/reticulum/src/node/event.zig:98
/// Asks the node to put application plaintext across a link that has activated, so the caller can/// send application bytes over the open link. Plaintext over 431 bytes returns/// `error.PacketTooLarge` before the node changes anything, per Reticulum@1.5.0 RNS/Link.py:73./// With `create_receipt` set, the node tracks the delivery, fails a receipt that stays in the table/// max(rtt * 6, 0.005) seconds after the send when no proof has arrived, per Reticulum@1.5.0/// RNS/Packet.py:419-420 and Reticulum@1.5.0 RNS/Transport.py:1307-1318, and culls the oldest/// receipt and reports it as a `receipt_update` effect with status `culled` and no round trip time/// when a later send that asks for a receipt finds the receipt table full.pub const ApplicationLinkSend = struct { link_id: [16]u8, plaintext: []const u8, iv: [16]u8, now: timer.Seconds, create_receipt: bool = true,};Source: lib/reticulum/src/node/event.zig:66
/// Asks any node that knows a path to the named destination to answer with one, carrying the/// caller's own tag, for a caller that has yet to learn a route to that destination, as/// Reticulum@1.5.0 RNS/Transport.py:3207-3220 requests it. Naming a carrier sends the request on/// that carrier alone, and leaving it null sends it on every outgoing carrier.pub const ApplicationPathRequest = struct { destination: [16]u8, tag: [16]u8, interface: ?carrier.Index, now: timer.Seconds,};Source: lib/reticulum/src/node/event.zig:55
pub const ApplicationProve = struct { destination: [16]u8, packet_hash: [32]u8, interface: carrier.Index, now: timer.Seconds,};Source: lib/reticulum/src/node/event.zig:32
pub const ApplicationSend = struct { destination: [16]u8, now: timer.Seconds, plaintext: []const u8, context: wire.Context = .none, hops: u8 = 0, ephemeral_private: [32]u8, iv: [16]u8, create_receipt: bool = true,};Source: lib/reticulum/src/node/event.zig:8
pub const CarrierFrame = struct { interface: carrier.Index, now: timer.Seconds, bytes: []const u8, /// Thirty-two bytes of randomness that the caller renews for every frame it gives the node, so /// a link's keys on the receiving side come out of them. A step that needs a key or an /// initialization vector runs HKDF-SHA256 over those bytes, taking the link id as the salt and /// a label that names the use. A responder gets its X25519 private key from this expansion, and /// Reticulum@1.5.0 RNS/Link.py:276 draws one at random. An initiator's round trip packet and a /// responder's LINKCLOSE take their initialization vectors this way, and Reticulum@1.5.0 /// RNS/Cryptography/Token.py:89 draws a random one. entropy: [32]u8,};Source: lib/reticulum/src/node/event.zig:133
pub const Event = union(enum) { carrier_frame: CarrierFrame, timer_expired: TimerExpired, application_send: ApplicationSend, application_announce: ApplicationAnnounce, application_prove: ApplicationProve, application_path_request: ApplicationPathRequest, application_link_open: ApplicationLinkOpen, application_link_send: ApplicationLinkSend, application_link_close: ApplicationLinkClose, application_link_prove: ApplicationLinkProve, storage_complete: StorageComplete,};Source: lib/reticulum/src/node/event.zig:128
pub const StorageComplete = struct { token: PersistToken, now: timer.Seconds,};Source: lib/reticulum/src/node/event.zig:22
pub const TimerExpired = struct { id: timer.TimerId, now: timer.Seconds, /// Thirty-two bytes of randomness that the caller renews for every timer it reports, so a timer /// that closes a link can encrypt. A step driven by a timer derives its keys and initialization /// vectors the same way, through HKDF-SHA256 with the link id as the salt and a label that /// names the use. entropy: [32]u8,};Source: lib/reticulum/src/node/limits.zig:72
pub const Capacity = struct { interface_storage_bytes: usize, identity_storage_bytes: usize, ratchet_binding_storage_bytes: usize, scratch_storage_bytes: usize, destinations: destination.registry.Table.Capacity, known_identities: identity.known.Identities.Capacity, known_ratchets: identity.known.Ratchets.Capacity, receipts: packet.receipt.Table.Capacity, duplicate_hashes: packet.hashlist.Table.Capacity, timers: timer.Table.Capacity, transport: transport.State.Capacity, effects: effect.Effects.Capacity, identities_offset: usize, ratchet_bindings_offset: usize, destinations_offset: usize, known_identities_offset: usize, known_ratchets_offset: usize, receipts_offset: usize, duplicate_hashes_offset: usize, timers_offset: usize, transport_offset: usize, effects_offset: usize, scratch_offset: usize, storage_bytes: usize, pub const DeriveError = error{ InvalidLimit, CapacityOverflow }; pub fn derive(limits: Limits) DeriveError!Capacity { if (limits.interfaces_max == 0) return error.InvalidLimit; if (limits.interfaces_max > @as(usize, std.math.maxInt(carrier.Index)) + 1) { return error.InvalidLimit; } const subs = try deriveSubcapacities(limits); const interface_storage_bytes = try alloc_phase.capacity.mul( usize, limits.interfaces_max, @sizeOf(access.Registration), ); var cursor = try alignForward(interface_storage_bytes, 8); const identities_offset = try advance(&cursor, try alloc_phase.capacity.mul( usize, limits.destinations_max, @sizeOf(identity.Private), )); const ratchet_bindings_offset = try advance(&cursor, try alloc_phase.capacity.mul( usize, limits.destinations_max, @sizeOf(RatchetBinding), )); const destinations_offset = try advance(&cursor, subs.destinations.storage_bytes); const known_identities_offset = try advance(&cursor, subs.known_identities.storage_bytes); const known_ratchets_offset = try advance(&cursor, subs.known_ratchets.storage_bytes); const receipts_offset = try advance(&cursor, subs.receipts.storage_bytes); const duplicate_hashes_offset = try advance(&cursor, subs.duplicate_hashes.storage_bytes); const timers_offset = try advance(&cursor, subs.timers.storage_bytes); const transport_offset = try advance(&cursor, subs.transport.storage_bytes); const effects_offset = try advance(&cursor, subs.effects.storage_bytes); const scratch_offset = try advance(&cursor, wire.mtu); return .{ .interface_storage_bytes = interface_storage_bytes, .identity_storage_bytes = ratchet_bindings_offset - identities_offset, .ratchet_binding_storage_bytes = destinations_offset - ratchet_bindings_offset, .scratch_storage_bytes = wire.mtu, .destinations = subs.destinations, .known_identities = subs.known_identities, .known_ratchets = subs.known_ratchets, .receipts = subs.receipts, .duplicate_hashes = subs.duplicate_hashes, .timers = subs.timers, .transport = subs.transport, .effects = subs.effects, .identities_offset = identities_offset, .ratchet_bindings_offset = ratchet_bindings_offset, .destinations_offset = destinations_offset, .known_identities_offset = known_identities_offset, .known_ratchets_offset = known_ratchets_offset, .receipts_offset = receipts_offset, .duplicate_hashes_offset = duplicate_hashes_offset, .timers_offset = timers_offset, .transport_offset = transport_offset, .effects_offset = effects_offset, .scratch_offset = scratch_offset, .storage_bytes = cursor, }; }};Source: lib/reticulum/src/node/limits.zig:29
pub const Limits = struct { interfaces_max: usize, destinations_max: usize, known_identities_max: usize, known_ratchets_max: usize, receipts_max: usize, duplicate_hashes_max: usize, timers_max: usize, effects_max: usize, effect_frames_max: usize, paths_max: usize, announces_max: usize, reverse_entries_max: usize, path_request_tags_max: usize, inflight_requests_max: usize, discoveries_max: usize, links_max: usize, link_entries_max: usize, /// One set of limits that pairs the reference receipt, hash, and tag maxima above with a single /// entry in every other table. The tests derive a capacity from it to show that those maxima /// fit and that raising one past the range of its entry type is refused. pub const reference = Limits{ .interfaces_max = 1, .destinations_max = 1, .known_identities_max = 1, .known_ratchets_max = 1, .receipts_max = reference_receipts_max, .duplicate_hashes_max = reference_duplicate_hashes_max, .timers_max = 1, .effects_max = effect.effects_per_event_max, .effect_frames_max = effect.effect_frames_per_event_max, .paths_max = 1, .announces_max = 1, .reverse_entries_max = 1, .path_request_tags_max = reference_path_request_tags_max, .inflight_requests_max = 1, .discoveries_max = 1, .links_max = 1, .link_entries_max = 1, };};Source: lib/reticulum/src/node/limits.zig:24
Source: lib/reticulum/src/node/owner.zig:27
pub const Node = struct { phase: alloc_phase.capacity.Phase, capacity: Capacity, storage: Storage, interfaces: []Interface, identities: []identity.Private, ratchet_bindings: []RatchetBinding, destinations: destination.registry.Table, known_identities: identity.known.Identities, known_ratchets: identity.known.Ratchets, receipts: packet.receipt.Table, duplicate_hashes: packet.hashlist.Table, timers: timer.Table, transport: transport.State, effects: effect.Effects, scratch: []u8, proof_mode: ProofMode = .implicit, pub const storage_alignment: usize = 8; pub const Storage = []align(storage_alignment) u8; pub const Limits: type = limits.Limits; pub const Capacity: type = limits.Capacity; pub const InitError = Capacity.DeriveError || error{StorageLengthMismatch}; pub const work_limits: alloc_phase.capacity.WorkLimits = .{ .transition_steps_max = 8, .cleanup_steps_per_call_max = 0, .cleanup_calls_at_capacity_max = 0, }; pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "reticulum.node", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{ .{ .id = "caller_interface_slots", .lifetime = .transferred, .detail = "caller storage for bounded carrier access registrations", }, .{ .id = "caller_node_subowners", .lifetime = .transferred, .detail = "caller storage for local identities and eight strict subowners", }, .{ .id = "caller_node_work", .lifetime = .transferred, .detail = "caller storage for rotating-key bindings and one MTU scratch", }, }, .excluded = &.{ "carrier memory and interface devices", "bound rotating-key rings and application payload inputs", "persistent storage and operating-system state", }, }, .capacity = .{ .inputs = &.{ alloc_phase.capacity.bindInput(Limits, "interfaces", "interfaces_max"), alloc_phase.capacity.bindInput(Limits, "destinations", "destinations_max"), alloc_phase.capacity.bindInput(Limits, "identities", "known_identities_max"), alloc_phase.capacity.bindInput(Limits, "ratchets", "known_ratchets_max"), alloc_phase.capacity.bindInput(Limits, "receipts", "receipts_max"), alloc_phase.capacity.bindInput(Limits, "hashes", "duplicate_hashes_max"), alloc_phase.capacity.bindInput(Limits, "timers", "timers_max"), alloc_phase.capacity.bindInput(Limits, "effects", "effects_max"), alloc_phase.capacity.bindInput(Limits, "frames", "effect_frames_max"), alloc_phase.capacity.bindInput(Limits, "paths", "paths_max"), alloc_phase.capacity.bindInput(Limits, "announces", "announces_max"), alloc_phase.capacity.bindInput( Limits, "reverse_entries", "reverse_entries_max", ), alloc_phase.capacity.bindInput( Limits, "path_request_tags", "path_request_tags_max", ), alloc_phase.capacity.bindInput( Limits, "inflight_requests", "inflight_requests_max", ), alloc_phase.capacity.bindInput(Limits, "discoveries", "discoveries_max"), alloc_phase.capacity.bindInput(Limits, "links", "links_max"), }, .type_selectors = &.{ alloc_phase.capacity.bindType(identity.Private, "private_identity"), alloc_phase.capacity.bindType(RatchetBinding, "ratchet_binding"), alloc_phase.capacity.bindType( destination.registry.Entry, "destination", ), alloc_phase.capacity.bindType( identity.known.IdentityEntry, "identity", ), alloc_phase.capacity.bindType( identity.known.RatchetEntry, "ratchet", ), alloc_phase.capacity.bindType(packet.receipt.Receipt, "receipt"), alloc_phase.capacity.bindType(packet.Hash, "hash"), alloc_phase.capacity.bindType(timer.Timer, "timer"), alloc_phase.capacity.bindType(effect.Effect, "effect"), alloc_phase.capacity.bindType(carrier.Frame, "frame"), alloc_phase.capacity.bindType(Interface, "interface"), alloc_phase.capacity.bindType(transport.path.Entry, "path"), alloc_phase.capacity.bindType(transport.announces.Entry, "announce"), alloc_phase.capacity.bindType(transport.reverse.Entry, "reverse_entry"), alloc_phase.capacity.bindType(transport.inflight.Entry, "inflight_request"), alloc_phase.capacity.bindType(transport.discoveries.Entry, "discovery"), }, .nodes = &.{ .{ .input = 0 }, .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 10 }, } }, .{ .alignment = .{ .node = 1, .alignment = .{ .literal = 8 } } }, .{ .input = 1 }, .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 0 }, } }, .{ .add = .{ .left = 2, .right = 4 } }, .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 1 }, } }, .{ .add = .{ .left = 5, .right = 6 } }, .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 2 }, } }, .{ .add = .{ .left = 7, .right = 8 } }, .{ .input = 2 }, .{ .scale = .{ .node = 10, .coefficient = .{ .size_of_concrete_type = 3 }, } }, .{ .add = .{ .left = 9, .right = 11 } }, .{ .input = 3 }, .{ .scale = .{ .node = 13, .coefficient = .{ .size_of_concrete_type = 4 }, } }, .{ .add = .{ .left = 12, .right = 14 } }, .{ .input = 4 }, .{ .scale = .{ .node = 16, .coefficient = .{ .size_of_concrete_type = 5 }, } }, .{ .add = .{ .left = 15, .right = 17 } }, .{ .input = 5 }, .{ .scale = .{ .node = 19, .coefficient = .{ .size_of_concrete_type = 6 }, } }, .{ .add = .{ .left = 18, .right = 20 } }, .{ .input = 6 }, .{ .scale = .{ .node = 22, .coefficient = .{ .size_of_concrete_type = 7 }, } }, .{ .add = .{ .left = 21, .right = 23 } }, .{ .input = 7 }, .{ .scale = .{ .node = 25, .coefficient = .{ .size_of_concrete_type = 8 }, } }, .{ .add = .{ .left = 24, .right = 26 } }, .{ .input = 8 }, .{ .scale = .{ .node = 28, .coefficient = .{ .size_of_concrete_type = 9 }, } }, .{ .add = .{ .left = 27, .right = 29 } }, .{ .input = 9 }, .{ .scale = .{ .node = 31, .coefficient = .{ .size_of_concrete_type = 11 }, } }, .{ .add = .{ .left = 30, .right = 32 } }, .{ .input = 10 }, .{ .scale = .{ .node = 34, .coefficient = .{ .size_of_concrete_type = 12 }, } }, .{ .add = .{ .left = 33, .right = 35 } }, .{ .input = 11 }, .{ .scale = .{ .node = 37, .coefficient = .{ .size_of_concrete_type = 13 }, } }, .{ .add = .{ .left = 36, .right = 38 } }, .{ .input = 12 }, .{ .scale = .{ .node = 40, .coefficient = .{ .size_of_concrete_type = 6 }, } }, .{ .add = .{ .left = 39, .right = 41 } }, .{ .input = 13 }, .{ .scale = .{ .node = 43, .coefficient = .{ .size_of_concrete_type = 14 }, } }, .{ .add = .{ .left = 42, .right = 44 } }, .{ .input = 14 }, .{ .scale = .{ .node = 46, .coefficient = .{ .size_of_concrete_type = 15 }, } }, .{ .add = .{ .left = 45, .right = 47 } }, .{ .input = 15 }, .{ .scale = .{ .node = 49, .coefficient = .{ .literal = @sizeOf(transport.links.Entry) }, } }, .{ .add = .{ .left = 48, .right = 50 } }, .{ .constant = 500 }, .{ .add = .{ .left = 51, .right = 52 } }, }, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 53, }}, }, .overload = .{ .kind = .reject_before_seal, .detail = "invalid limits and short caller storage reject before activation", }, .risks = .{ .transitive = .{ .status = .witnessed, .detail = "all retained subowner storage is carved from the node region", }, .foreign = .{ .status = .excluded, .detail = "the node owner crosses no foreign boundary", }, }, .work = .{ .equation = "activation and teardown each visit eight subowners" }, .dependencies = &.{ "reticulum.destinations", "reticulum.known_identities", "reticulum.known_ratchets", "reticulum.receipts", "reticulum.hashlist", "reticulum.timers", "reticulum.effects", "reticulum.transport", }, .obligations = &.{ .{ .key = "reticulum_node_capacity", .role = .capacity_model }, .{ .key = "reticulum_node_overload", .role = .overload }, .{ .key = "reticulum_node_transitive", .role = .transitive_risk }, .{ .key = "reticulum_node_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, selected: Limits) InitError!Node { const capacity = try Capacity.derive(selected); if (storage.len != capacity.storage_bytes) return error.StorageLengthMismatch; const interfaces = initializeInterfaces(storage, capacity); const identities = std.mem.bytesAsSlice(identity.Private, region( storage, capacity.identities_offset, capacity.identity_storage_bytes, )); for (identities) |*private| private.* = identity.Private.fromBytes(@splat(0)); const ratchet_bindings = std.mem.bytesAsSlice(RatchetBinding, region( storage, capacity.ratchet_bindings_offset, capacity.ratchet_binding_storage_bytes, )); @memset(ratchet_bindings, .{}); const subowners = try initSubowners(storage, capacity, selected); const scratch = storage[capacity.scratch_offset..][0..capacity.scratch_storage_bytes]; @memset(scratch, 0); return .{ .phase = .initialization, .capacity = capacity, .storage = storage, .interfaces = interfaces, .identities = identities, .ratchet_bindings = ratchet_bindings, .destinations = subowners.destinations, .known_identities = subowners.known_identities, .known_ratchets = subowners.known_ratchets, .receipts = subowners.receipts, .duplicate_hashes = subowners.duplicate_hashes, .timers = subowners.timers, .transport = subowners.transport, .effects = subowners.effects, .scratch = scratch, }; } pub fn activate(self: *Node) void { std.debug.assert(self.phase == .initialization); self.destinations.activate(); self.known_identities.activate(); self.known_ratchets.activate(); self.receipts.activate(); self.duplicate_hashes.activate(); self.timers.activate(); self.transport.activate(); self.effects.activate(); self.phase = .steady; } pub fn identityAt(self: *Node, index: usize) ?*identity.Private { std.debug.assert(self.phase == .steady); if (index >= self.identities.len) return null; return &self.identities[index]; } /// Registers one carrier index, so the node accepts frames that arrive on it and, with /// `outgoing` set, sends on it as well. An access code given here authenticates every frame in /// both directions on that carrier. An index at or past the caller's carrier maximum returns /// `error.Index`. Registering the same index again zeroes what was there, so its path discovery /// returns to off. pub fn registerCarrier( self: *Node, index: carrier.Index, outgoing: bool, access_config: ?Access, ) error{Index}!void { std.debug.assert(self.phase == .steady); std.debug.assert(self.interfaces.len > 0); std.debug.assert(self.interfaces.len <= @as(usize, std.math.maxInt(carrier.Index)) + 1); if (access_config) |value| { std.debug.assert(value.size.byte() >= ifac.min_size_bytes); std.debug.assert(@as(usize, wire.mtu) + value.size.byte() <= carrier.frame_bytes_max); } const slot: usize = index; if (slot >= self.interfaces.len) return error.Index; std.crypto.secureZero(u8, std.mem.asBytes(&self.interfaces[slot])); self.interfaces[slot] = .{ .registered = 1, .outgoing = @intFromBool(outgoing), .access = access_config, }; std.debug.assert(self.interfaces[slot].registered == 1); } /// Binds a rotating-key ring to one of the node's own identities, so announces from that /// identity carry a rotating public key and data addressed to it is decrypted with the ring's /// keys first. The ring stays the caller's and has to outlive the node, which holds only a /// pointer to it. With `enforce` set, the node decrypts only with the ring's keys, so a packet /// that none of them open fails. An identity index at or past the caller's destination maximum /// returns `error.Index`. pub fn bindRatchetRing( self: *Node, identity_index: usize, ring: *identity.Ring, enforce: bool, ) error{Index}!void { std.debug.assert(self.phase == .steady); if (identity_index >= self.ratchet_bindings.len) return error.Index; self.ratchet_bindings[identity_index] = .{ .ring = ring, .enforce = enforce }; } /// Chooses whether a proof this node sends carries the proved packet's hash beside the /// signature or the signature alone. The node starts in implicit mode, with the signature /// alone. pub fn setProofMode(self: *Node, mode: ProofMode) void { std.debug.assert(self.phase == .steady); self.proof_mode = mode; } /// Turns the node into one that carries traffic for others and gives it the 16 bytes that name /// it to its neighbors, which Reticulum@1.5.0 RNS/Transport.py:1577-1580,1907 sets. An enabled /// transport node rebroadcasts the announces it accepts and relays the packets that name it as /// the next hop. pub fn setTransport(self: *Node, identity_hash: [16]u8, enabled: bool) void { std.debug.assert(self.phase == .steady); self.transport.configure(identity_hash, enabled); } /// Lets the node ask one carrier for a path it lacks, which Reticulum@1.5.0 /// RNS/Transport.py:3352-3355 searches. An index at or past the carrier maximum, or one the /// caller has yet to register, returns `error.Index`. Registering that carrier again returns /// the setting to off, per Reticulum@1.5.0 RNS/Interfaces/Interface.py:55,121. pub fn setPathDiscovery(self: *Node, index: carrier.Index, enabled: bool) error{Index}!void { std.debug.assert(self.phase == .steady); const slot: usize = index; if (slot >= self.interfaces.len) return error.Index; if (self.interfaces[slot].registered == 0) return error.Index; self.interfaces[slot].discover_paths = @intFromBool(enabled); std.debug.assert(self.interfaces[slot].discover_paths == @intFromBool(enabled)); } /// Applies one event and returns the effects it produced. The effects point into the node's own /// storage and stay valid until the next call. pub fn step(self: *Node, value: event.Event) errors.Error![]const effect.Effect { return @import("step.zig").run(self, value); } pub fn deinit(self: *Node) Storage { std.debug.assert(self.phase == .steady); _ = self.effects.deinit(); _ = self.transport.deinit(); _ = self.timers.deinit(); _ = self.duplicate_hashes.deinit(); _ = self.receipts.deinit(); _ = self.known_ratchets.deinit(); _ = self.known_identities.deinit(); _ = self.destinations.deinit(); std.crypto.secureZero(u8, std.mem.sliceAsBytes(self.interfaces)); @memset(self.ratchet_bindings, .{}); for (self.identities) |*private| private.zero(); std.crypto.secureZero(u8, self.scratch); self.phase = .teardown; const storage = self.storage; self.* = undefined; return storage; }};Source: lib/reticulum/src/node/owner.zig:20
pub const ProofMode = enum(u1) { implicit, explicit,};Source: lib/reticulum/src/node/effect.zig:177
/// Gives the most byte slices one event's effects retain, so a caller sizes the effect list's frame/// storage from this bound. One delivery that also proves reaches that number, retaining the/// plaintext and 256 carrier frames.pub const effect_frames_per_event_max: u16 = 257;Source: lib/reticulum/src/node/effect.zig:173
/// Gives the most effects one event can produce, so a caller sizes its effect storage from this/// bound. An announce reaches that number: it reports the announce, arms one timer, and sends one/// answer over every one of 256 carriers. A link open with no known path reaches it too: it reports/// the request, arms one timer, and sends that request over every one of 256 carriers.pub const effects_per_event_max: u16 = 258;Source: lib/reticulum/src/node/event.zig:6
pub const PersistToken = u64;Source: lib/reticulum/src/node/root.zig
//! A state machine for one node of a mesh network holds everything the node remembers and turns one//! input at a time into a list of things for the program around it to do. That network is//! Reticulum, in which nodes find each other by broadcasting signed announcements and reach each//! other over any network interface that moves bytes.//!//! Someone writing such a node has to decide what to put on the wire, what to remember about who is//! out there, and when a deadline has passed, and the program around the node keeps the sockets,//! the clock, and the source of randomness. The node has to run on machines whose memory is fixed//! before the program starts, so it takes the size of each store it keeps from the caller and//! refuses work once a store is full. A port of an existing protocol has to be checkable against//! the original, byte for byte.//!//! Code that reads the wall clock and draws its own randomness gives a different answer on every//! run, which leaves nothing to compare against a reference. Sending from inside the protocol logic//! also needs somewhere for the bytes to live. A transition can run out of room part way through,//! and a half-applied transition would leave the node's stores disagreeing with what the caller was//! told.//!//! The subtree follows *Reticulum 1.5.0*, the reference implementation this package is a port of.//! The package README and the generated conformance material pin the upstream commit. What the//! subtree takes from the reference is its order of handling an arriving packet, its state machines//! for routes and for encrypted sessions, and its timeouts, and each declaration names the//! reference file and line range it follows. The package generates reference bytes and clocks from//! that release (a *conformance corpus*) and replays them in differential tests, so each of those//! claims is checkable from this tree.//!//! A transition (a *step*) takes the instant and the 32 bytes of randomness it needs from its own//! input, so replaying the same inputs gives the same results. The node sends nothing, arms no//! timer, and writes no storage itself: it appends a record of each such request (an *effect*) to a//! list, and the caller performs them. Every transition first reserves room (a *reserve*): it//! checks that the list of effects and the storage for its bytes can hold what it is about to//! append before it changes any store, so a transition that runs out of room leaves the node as it//! was. Every store takes its entry maximum (its *limits*) from the caller and is carved out of one//! block of caller storage whose length has to equal the derived byte count exactly. The node keeps//! one packet-sized buffer (its *scratch*) where a transition builds the bytes it is about to hand//! to one network interface.//!//! The file exports the subtree's pieces: `Node` and `Limits`, the `Event` a transition takes and//! the `Effect` records it returns, `timer`, and the modules `inbound` for arriving frames,//! `outbound` for sends, `announce`, `link`, `transport` for carrying traffic on behalf of other//! nodes, and `fixture`, the three-node test world.//!//! - *node*: the state machine that holds one Reticulum node's tables, one packet of scratch space,//! and its effect list.//! - *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.//! - *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.//! - *table*: an owner over caller-supplied bytes with a fixed entry maximum, which carves the//! caller's storage once and hands it back at teardown.//! - *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.//! - *frame*: the bytes handed to one carrier, one packet plus at most a 64-byte signature.//! - *carrier*: one network interface a node sends and receives frames over, named by a byte index.//! - *announce*: a packet carrying a destination's public keys, a name hash, ten random bytes, and//! a signature, so any node that hears it learns that destination and how far away it sits.//! - *path*: what a node learned from an announce about reaching one destination: the carrier to//! send on, the neighbor to name as the next hop, and how many hops away it sits.//! - *link*: an encrypted session between two endpoints, named by a 16-byte link id that both ends//! derive from the request packet.//! - *timer*: a deadline the node holds, naming what comes due and the whole second it comes due//! at.//! - *transport node*: a node that carries traffic on behalf of other nodes, as well as sending and//! receiving its own.const effect = @import("effect.zig");const errors = @import("error.zig");const event = @import("event.zig");const limits = @import("limits.zig");const owner = @import("owner.zig");pub const timer = @import("timer.zig");pub const outbound = @import("outbound.zig");pub const announce = @import("announce.zig");pub const inbound = @import("inbound.zig");pub const link = @import("link.zig");pub const transition = @import("step.zig");pub const transport = @import("transport/root.zig");pub const fixture = @import("fixture/root.zig");pub const Seconds = timer.Seconds;pub const TimerId = timer.TimerId;pub const PersistToken = event.PersistToken;pub const Limits = limits.Limits;pub const Capacity = limits.Capacity;pub const Node = owner.Node;pub const Access = owner.Access;pub const Interface = owner.Interface;pub const ProofMode = owner.ProofMode;pub const RatchetBinding = owner.RatchetBinding;pub const StepError = errors.Error;pub const Event = event.Event;pub const CarrierFrame = event.CarrierFrame;pub const TimerExpired = event.TimerExpired;pub const ApplicationSend = event.ApplicationSend;pub const ApplicationAnnounce = event.ApplicationAnnounce;pub const ApplicationProve = event.ApplicationProve;pub const ApplicationPathRequest = event.ApplicationPathRequest;pub const ApplicationLinkOpen = event.ApplicationLinkOpen;pub const ApplicationLinkSend = event.ApplicationLinkSend;pub const ApplicationLinkClose = event.ApplicationLinkClose;pub const ApplicationLinkProve = event.ApplicationLinkProve;pub const StorageComplete = event.StorageComplete;pub const Effect = effect.Effect;pub const Effects = effect.Effects;pub const Code = effect.Code;pub const LinkCloseReason = effect.LinkCloseReason;pub const LinkRole = transport.links.Role;pub const PersistKind = effect.PersistKind;pub const effects_per_event_max = effect.effects_per_event_max;pub const effect_frames_per_event_max = effect.effect_frames_per_event_max;Source: lib/reticulum/src/root.zig:65
pub const node = @import("node/root.zig");Complete call list for node.Node.activate
7 direct calls.
tiny.reticulum.destination.registry.Table.activate[method] atlib/reticulum/src/destination/registry.zig:166tiny.reticulum.identity.known.Identities.activate[method] atlib/reticulum/src/identity/known.zig:178tiny.reticulum.identity.known.Ratchets.activate[method] atlib/reticulum/src/identity/known.zig:388tiny.reticulum.node.Effects.activate[method] atlib/reticulum/src/node/effect.zig:347tiny.reticulum.node.timer.Table.activate[method] atlib/reticulum/src/node/timer.zig:158tiny.reticulum.packet.hashlist.Table.activate[method] atlib/reticulum/src/packet/hashlist.zig:128tiny.reticulum.packet.receipt.Table.activate[method] atlib/reticulum/src/packet/receipt.zig:212
Complete call list for node.Node.deinit
7 direct calls.
tiny.reticulum.destination.registry.Table.deinit[method] atlib/reticulum/src/destination/registry.zig:193tiny.reticulum.identity.known.Identities.deinit[method] atlib/reticulum/src/identity/known.zig:229tiny.reticulum.identity.known.Ratchets.deinit[method] atlib/reticulum/src/identity/known.zig:444tiny.reticulum.node.Effects.deinit[method] atlib/reticulum/src/node/effect.zig:415tiny.reticulum.node.timer.Table.deinit[method] atlib/reticulum/src/node/timer.zig:222tiny.reticulum.packet.hashlist.Table.deinit[method] atlib/reticulum/src/packet/hashlist.zig:215tiny.reticulum.packet.receipt.Table.deinit[method] atlib/reticulum/src/packet/receipt.zig:281
Audit
| Definitions | 64 |
|---|---|
| Public names | 64 |
| Members | 206 |
| Version | 26.7.0 |
| Revision | daab053ee433 |